blob: 9e03a025ad82e4641210df02dbf5a8767106bb49 [file] [log] [blame]
Pau Espin Pedrol8a725862018-10-26 15:54:28 +02001# osmo_gsm_tester: specifics for running an iperf3 client and server
2#
3# Copyright (C) 2018 by sysmocom - s.f.m.c. GmbH
4#
5# Author: Pau Espin Pedrol <pespin@sysmocom.de>
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as
9# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20import os
21import json
22
Pau Espin Pedrole1a58bd2020-04-10 20:46:07 +020023from ..core import log, util, config, process, remote
Pau Espin Pedrolea8c3d42020-05-04 12:05:05 +020024from ..core import schema
Pau Espin Pedrole8bbcbf2020-04-10 19:51:31 +020025from . import pcap_recorder, run_node
Pau Espin Pedrol8a725862018-10-26 15:54:28 +020026
Pau Espin Pedrolea8c3d42020-05-04 12:05:05 +020027def on_register_schemas():
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +020028 schema_types = {
29 'iperf3_protocol': IPerf3Client.validate_protocol,
30 }
31 schema.register_schema_types(schema_types)
Pau Espin Pedrolea8c3d42020-05-04 12:05:05 +020032 config_schema = {
33 'time': schema.DURATION,
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +020034 'protocol': 'iperf3_protocol',
AlaiaLe9d7f5f2021-05-10 16:10:14 +020035 'packet_length' : schema.UINT,
Pau Espin Pedrolea8c3d42020-05-04 12:05:05 +020036 }
37 schema.register_config_schema('iperf3cli', config_schema)
38
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +020039def iperf3_result_to_json(log_obj, data):
40 try:
41 # Drop non-interesting self-generated output before json:
42 if not data.startswith('{\n'):
43 data = "{\n" + data.split("\n{\n")[1]
44 # Sometimes iperf3 provides 2 dictionaries, the 2nd one being an error about being interrupted (by us).
45 # json parser doesn't support (raises exception) parsing several dictionaries at a time (not a valid json object).
46 # We are only interested in the first dictionary, the regular results one:
47 data = data.split("\n}")[0] + "\n}"
48 j = json.loads(data)
49 return j
50 except Exception as e:
51 log_obj.log('failed parsing iperf3 output: "%s"' % data)
52 raise e
Pau Espin Pedrol64f0b1b2018-11-09 17:44:10 +010053
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +020054def print_result_node_udp(result, node_str):
55 try:
56 sum = result['end']['sum']
57 print("Result %s:" % node_str)
58 print("\tSUM: %d KB, %d kbps, %d seconds %d/%d lost" % (sum['bytes']/1000, sum['bits_per_second']/1000, sum['seconds'], sum['lost_packets'], sum['packets']))
59 except Exception as e:
60 print("Exception while using iperf3 %s results: %r" % (node_str, repr(result)))
61 raise e
62
63def print_result_node_tcp(result, node_str):
64 try:
65 sent = result['end']['sum_sent']
66 recv = result['end']['sum_received']
67 print("Result %s:" % node_str)
68 print("\tSEND: %d KB, %d kbps, %d seconds (%s retrans)" % (sent['bytes']/1000, sent['bits_per_second']/1000, sent['seconds'], str(sent.get('retransmits', 'unknown'))))
69 print("\tRECV: %d KB, %d kbps, %d seconds" % (recv['bytes']/1000, recv['bits_per_second']/1000, recv['seconds']))
70 except Exception as e:
71 print("Exception while using iperf3 %s results: %r" % (node_str, repr(result)))
72 raise e
Pau Espin Pedrol64f0b1b2018-11-09 17:44:10 +010073
Andre Puschmann55b455c2020-06-17 15:36:30 +020074def get_received_mbps(result, isUdp=True):
75 try:
76 recv = result['end']['sum' if isUdp else 'sum_received']
77 return recv['bits_per_second']/1e6
78 except Exception as e:
79 print("Exception while using iperf3 results: %r" % (repr(result)))
80 raise e
81
Pau Espin Pedrol8a725862018-10-26 15:54:28 +020082class IPerf3Server(log.Origin):
83
Pau Espin Pedrolcf6a3602018-11-14 16:38:19 +010084 DEFAULT_SRV_PORT = 5003
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +010085 LOGFILE = 'iperf3_srv.json'
86 REMOTE_DIR = '/tmp'
Pau Espin Pedrolcf6a3602018-11-14 16:38:19 +010087
Pau Espin Pedrola442cb82020-05-05 12:54:37 +020088 def __init__(self, testenv, ip_address):
Pau Espin Pedrol8a725862018-10-26 15:54:28 +020089 super().__init__(log.C_RUN, 'iperf3-srv_%s' % ip_address.get('addr'))
90 self.run_dir = None
Pau Espin Pedrol8a725862018-10-26 15:54:28 +020091 self.process = None
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +010092 self._run_node = None
Pau Espin Pedrola442cb82020-05-05 12:54:37 +020093 self.testenv = testenv
Pau Espin Pedrol8a725862018-10-26 15:54:28 +020094 self.ip_address = ip_address
Pau Espin Pedrolcf6a3602018-11-14 16:38:19 +010095 self._port = IPerf3Server.DEFAULT_SRV_PORT
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +010096 self.log_file = None
97 self.rem_host = None
98 self.remote_log_file = None
99 self.log_copied = False
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +0200100 self.logfile_supported = False # some older versions of iperf doesn't support --logfile arg
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100101
102 def cleanup(self):
103 if self.process is None:
104 return
Pau Espin Pedrol03fab092020-07-13 19:13:52 +0200105 if self.runs_locally() or not self.logfile_supported or self.log_copied:
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100106 return
107 # copy back files (may not exist, for instance if there was an early error of process):
108 try:
109 self.rem_host.scpfrom('scp-back-log', self.remote_log_file, self.log_file)
Pau Espin Pedrol03fab092020-07-13 19:13:52 +0200110 self.log_copied = True
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100111 except Exception as e:
112 self.log(repr(e))
113
114 def runs_locally(self):
115 locally = not self._run_node or self._run_node.is_local()
116 return locally
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200117
118 def start(self):
119 self.log('Starting iperf3-srv')
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100120 self.log_copied = False
Pau Espin Pedrol2a2d8462020-05-11 10:56:52 +0200121 self.run_dir = util.Dir(self.testenv.test().get_run_dir().new_dir(self.name()))
Pau Espin Pedrol6b9a50e2020-07-13 19:07:20 +0200122 self.log_file = self.run_dir.new_child(IPerf3Server.LOGFILE)
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100123 if self.runs_locally():
124 self.start_locally()
125 else:
126 self.start_remotely()
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200127
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100128 def start_remotely(self):
129 self.rem_host = remote.RemoteHost(self.run_dir, self._run_node.ssh_user(), self._run_node.ssh_addr())
130 remote_prefix_dir = util.Dir(IPerf3Server.REMOTE_DIR)
131 remote_run_dir = util.Dir(remote_prefix_dir.child('srv-' + str(self)))
132 self.remote_log_file = remote_run_dir.child(IPerf3Server.LOGFILE)
133
134 self.rem_host.recreate_remote_dir(remote_run_dir)
135
136 args = ('iperf3', '-s', '-B', self.addr(),
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +0200137 '-p', str(self._port), '-J')
138 if self.logfile_supported:
139 args += ('--logfile', self.remote_log_file,)
140
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100141 self.process = self.rem_host.RemoteProcess(self.name(), args)
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200142 self.testenv.remember_to_stop(self.process)
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100143 self.process.launch()
144
145 def start_locally(self):
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200146 pcap_recorder.PcapRecorder(self.testenv, self.run_dir.new_dir('pcap'), None,
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200147 'host %s and port not 22' % self.addr())
148
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100149 args = ('iperf3', '-s', '-B', self.addr(),
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +0200150 '-p', str(self._port), '-J')
151 if self.logfile_supported:
152 args += ('--logfile', os.path.abspath(self.log_file),)
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100153
154 self.process = process.Process(self.name(), self.run_dir, args, env={})
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200155 self.testenv.remember_to_stop(self.process)
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200156 self.process.launch()
157
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100158 def set_run_node(self, run_node):
159 self._run_node = run_node
160
Pau Espin Pedrolcf6a3602018-11-14 16:38:19 +0100161 def set_port(self, port):
162 self._port = port
163
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200164 def stop(self):
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200165 self.testenv.stop_process(self.process)
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200166
167 def get_results(self):
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +0200168 if self.logfile_supported:
169 if not self.runs_locally() and not self.log_copied:
170 self.rem_host.scpfrom('scp-back-log', self.remote_log_file, self.log_file)
171 self.log_copied = True
172 with open(self.log_file) as f:
173 return iperf3_result_to_json(self, f.read())
174 else:
175 return iperf3_result_to_json(self, self.process.get_stdout())
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200176
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +0200177 def print_results(self, client_was_udp):
178 if client_was_udp:
179 print_result_node_udp(self.get_results(), 'server')
180 else:
181 print_result_node_tcp(self.get_results(), 'server')
182
Andre Puschmann55b455c2020-06-17 15:36:30 +0200183 def get_received_mbps(self, client_was_udp):
184 return get_received_mbps(self.get_results(), client_was_udp)
185
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200186 def addr(self):
187 return self.ip_address.get('addr')
188
189 def port(self):
190 return self._port
191
Pau Espin Pedrol0df63172018-11-14 16:39:30 +0100192 def __str__(self):
193 return "%s:%u" %(self.addr(), self.port())
194
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200195 def running(self):
196 return not self.process.terminated()
197
198 def create_client(self):
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200199 return IPerf3Client(self.testenv, self)
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200200
201class IPerf3Client(log.Origin):
202
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100203 REMOTE_DIR = '/tmp'
204 LOGFILE = 'iperf3_cli.json'
205
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +0200206 PROTO_TCP = "tcp"
207 PROTO_UDP = "udp"
208
Pau Espin Pedrolf4ab97f2020-05-25 13:54:30 +0200209 DIR_UL = "ul"
210 DIR_DL = "dl"
211 DIR_BI = "bi"
212
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +0200213 @classmethod
214 def validate_protocol(cls, val):
215 return val in (cls.PROTO_TCP, cls.PROTO_UDP)
216
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200217 def __init__(self, testenv, iperf3srv):
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200218 super().__init__(log.C_RUN, 'iperf3-cli_%s' % iperf3srv.addr())
219 self.run_dir = None
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200220 self.process = None
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100221 self._run_node = None
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200222 self.server = iperf3srv
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200223 self.testenv = testenv
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +0200224 self._proto = None
Andre Puschmann219ec752020-06-16 16:06:30 +0200225 self._time_sec = None
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100226 self.log_file = None
227 self.rem_host = None
228 self.remote_log_file = None
229 self.log_copied = False
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +0200230 self.logfile_supported = False # some older versions of iperf doesn't support --logfile arg
Nils Fürstea8263f42020-11-23 14:45:15 +0100231 self.is_android_ue = False
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100232
233 def runs_locally(self):
234 locally = not self._run_node or self._run_node.is_local()
235 return locally
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200236
Pau Espin Pedrol56802552020-06-15 18:07:24 +0200237 def prepare_test_proc(self, dir=None, netns=None, time_sec=None, proto=None, bitrate=0, tos=None):
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +0200238 values = config.get_defaults('iperf3cli')
239 config.overlay(values, self.testenv.suite().config().get('iperf3cli', {}))
Pau Espin Pedrolf4ab97f2020-05-25 13:54:30 +0200240
241 if dir is None:
242 dir = self.DIR_UL
243
Andre Puschmann2dcc4312020-03-28 15:34:00 +0100244 if time_sec is None:
Andre Puschmann2dcc4312020-03-28 15:34:00 +0100245 time_sec_str = values.get('time', time_sec)
246
247 # Convert duration to seconds
248 if isinstance(time_sec_str, str) and time_sec_str.endswith('h'):
249 time_sec = int(time_sec_str[:-1]) * 3600
250 elif isinstance(time_sec_str, str) and time_sec_str.endswith('m'):
251 time_sec = int(time_sec_str[:-1]) * 60
252 else:
253 time_sec = int(time_sec_str)
Andre Puschmann2dcc4312020-03-28 15:34:00 +0100254 assert(time_sec)
Andre Puschmann219ec752020-06-16 16:06:30 +0200255 self._time_sec = time_sec
Andre Puschmann2dcc4312020-03-28 15:34:00 +0100256
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +0200257 if proto is None:
258 proto = values.get('protocol', IPerf3Client.PROTO_TCP)
259 self._proto = proto
260
261 self.log('Preparing iperf3-client connecting to %s:%d (proto=%s,time=%ds)' % (self.server.addr(), self.server.port(), self._proto, time_sec))
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100262 self.log_copied = False
Pau Espin Pedrol2a2d8462020-05-11 10:56:52 +0200263 self.run_dir = util.Dir(self.testenv.test().get_run_dir().new_dir(self.name()))
Pau Espin Pedrol6b9a50e2020-07-13 19:07:20 +0200264 self.log_file = self.run_dir.new_child(IPerf3Client.LOGFILE)
Pau Espin Pedrol229f1162020-06-15 17:52:33 +0200265
266 popen_args = ('iperf3', '-c', self.server.addr(),
267 '-p', str(self.server.port()), '-J',
268 '-t', str(time_sec))
269 if dir == IPerf3Client.DIR_DL:
270 popen_args += ('-R',)
271 elif dir == IPerf3Client.DIR_BI:
272 popen_args += ('--bidir',)
273 if proto == IPerf3Client.PROTO_UDP:
274 popen_args += ('-u', '-b', str(bitrate))
AlaiaLe9d7f5f2021-05-10 16:10:14 +0200275 # Add the buffer length.
276 if values.get('packet_length'):
277 packet_length = str(values.get('packet_length'))
278 popen_args += ('-l', packet_length)
Pau Espin Pedrol56802552020-06-15 18:07:24 +0200279 if tos is not None:
280 popen_args += ('-S', str(tos))
281
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100282 if self.runs_locally():
Pau Espin Pedrol229f1162020-06-15 17:52:33 +0200283 proc = self.prepare_test_proc_locally(netns, popen_args)
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100284 else:
Pau Espin Pedrol229f1162020-06-15 17:52:33 +0200285 proc = self.prepare_test_proc_remotely(netns, popen_args)
Andre Puschmann053ee5f2020-07-15 10:54:46 +0200286 proc.set_default_wait_timeout(time_sec + 120) # leave extra time for remote run, ctrl conn establishment, buffer draining, etc.
Pau Espin Pedrola9bc93d2020-06-12 15:34:28 +0200287 return proc
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200288
Pau Espin Pedrol229f1162020-06-15 17:52:33 +0200289 def prepare_test_proc_remotely(self, netns, popen_args):
Nils Fürstea8263f42020-11-23 14:45:15 +0100290 self.rem_host = remote.RemoteHost(self.run_dir, self._run_node.ssh_user(), self._run_node.ssh_addr(), None,
291 self._run_node.ssh_port())
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100292
293 remote_prefix_dir = util.Dir(IPerf3Client.REMOTE_DIR)
294 remote_run_dir = util.Dir(remote_prefix_dir.child('cli-' + str(self)))
295 self.remote_log_file = remote_run_dir.child(IPerf3Client.LOGFILE)
296
297 self.rem_host.recreate_remote_dir(remote_run_dir)
298
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +0200299 if self.logfile_supported:
300 popen_args += ('--logfile', self.remote_log_file,)
Pau Espin Pedrol905e5032020-03-02 15:35:33 +0100301
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100302 if netns:
303 self.process = self.rem_host.RemoteNetNSProcess(self.name(), netns, popen_args, env={})
304 else:
305 self.process = self.rem_host.RemoteProcess(self.name(), popen_args, env={})
306 return self.process
307
Pau Espin Pedrol229f1162020-06-15 17:52:33 +0200308 def prepare_test_proc_locally(self, netns, popen_args):
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200309 pcap_recorder.PcapRecorder(self.testenv, self.run_dir.new_dir('pcap'), None,
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200310 'host %s and port not 22' % self.server.addr(), netns)
311
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +0200312 if self.logfile_supported:
313 popen_args += ('--logfile', os.path.abspath(self.log_file),)
Pau Espin Pedrol905e5032020-03-02 15:35:33 +0100314
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200315 if netns:
316 self.process = process.NetNSProcess(self.name(), self.run_dir, netns, popen_args, env={})
Nils Fürstea8263f42020-11-23 14:45:15 +0100317 elif self._run_node.adb_serial_id():
318 self.process = process.AdbProcess(self.name(), self.run_dir, self._run_node.adb_serial_id(), popen_args, env={})
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200319 else:
320 self.process = process.Process(self.name(), self.run_dir, popen_args, env={})
Pau Espin Pedrol0fc74372018-11-14 16:44:57 +0100321 return self.process
322
323 def run_test_sync(self, netns=None):
324 self.prepare_test_proc(netns)
Pau Espin Pedrol79df7392018-11-12 18:15:30 +0100325 self.process.launch_sync()
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200326 return self.get_results()
327
328 def get_results(self):
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +0200329 if self.logfile_supported:
330 if not self.runs_locally() and not self.log_copied:
331 self.rem_host.scpfrom('scp-back-log', self.remote_log_file, self.log_file)
332 self.log_copied = True
333 with open(self.log_file) as f:
334 return iperf3_result_to_json(self, f.read())
335 else:
336 return iperf3_result_to_json(self, self.process.get_stdout())
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200337
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +0200338 def print_results(self):
339 if self.proto() == self.PROTO_UDP:
340 print_result_node_udp(self.get_results(), 'client')
341 else:
342 print_result_node_tcp(self.get_results(), 'client')
343
Andre Puschmann55b455c2020-06-17 15:36:30 +0200344 def get_received_mbps(self):
345 if self.proto() == self.PROTO_UDP:
346 return get_received_mbps(self.get_results(), isUdp=True)
347 else:
348 return get_received_mbps(self.get_results(), isUdp=False)
349
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100350 def set_run_node(self, run_node):
351 self._run_node = run_node
352
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +0200353 def proto(self):
354 return self._proto
355
Andre Puschmann219ec752020-06-16 16:06:30 +0200356 def time_sec(self):
357 return self._time_sec
358
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100359 def __str__(self):
360 # FIXME: somehow differentiate between several clients connected to same server?
361 return "%s:%u" %(self.server.addr(), self.server.port())
362
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200363# vim: expandtab tabstop=4 shiftwidth=4