blob: 13fd4554b16b47734e39daf127cb989e1c41fb78 [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',
Pau Espin Pedrolea8c3d42020-05-04 12:05:05 +020035 }
36 schema.register_config_schema('iperf3cli', config_schema)
37
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +020038def iperf3_result_to_json(log_obj, data):
39 try:
40 # Drop non-interesting self-generated output before json:
41 if not data.startswith('{\n'):
42 data = "{\n" + data.split("\n{\n")[1]
43 # Sometimes iperf3 provides 2 dictionaries, the 2nd one being an error about being interrupted (by us).
44 # json parser doesn't support (raises exception) parsing several dictionaries at a time (not a valid json object).
45 # We are only interested in the first dictionary, the regular results one:
46 data = data.split("\n}")[0] + "\n}"
47 j = json.loads(data)
48 return j
49 except Exception as e:
50 log_obj.log('failed parsing iperf3 output: "%s"' % data)
51 raise e
Pau Espin Pedrol64f0b1b2018-11-09 17:44:10 +010052
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +020053def print_result_node_udp(result, node_str):
54 try:
55 sum = result['end']['sum']
56 print("Result %s:" % node_str)
57 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']))
58 except Exception as e:
59 print("Exception while using iperf3 %s results: %r" % (node_str, repr(result)))
60 raise e
61
62def print_result_node_tcp(result, node_str):
63 try:
64 sent = result['end']['sum_sent']
65 recv = result['end']['sum_received']
66 print("Result %s:" % node_str)
67 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'))))
68 print("\tRECV: %d KB, %d kbps, %d seconds" % (recv['bytes']/1000, recv['bits_per_second']/1000, recv['seconds']))
69 except Exception as e:
70 print("Exception while using iperf3 %s results: %r" % (node_str, repr(result)))
71 raise e
Pau Espin Pedrol64f0b1b2018-11-09 17:44:10 +010072
Pau Espin Pedrol8a725862018-10-26 15:54:28 +020073class IPerf3Server(log.Origin):
74
Pau Espin Pedrolcf6a3602018-11-14 16:38:19 +010075 DEFAULT_SRV_PORT = 5003
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +010076 LOGFILE = 'iperf3_srv.json'
77 REMOTE_DIR = '/tmp'
Pau Espin Pedrolcf6a3602018-11-14 16:38:19 +010078
Pau Espin Pedrola442cb82020-05-05 12:54:37 +020079 def __init__(self, testenv, ip_address):
Pau Espin Pedrol8a725862018-10-26 15:54:28 +020080 super().__init__(log.C_RUN, 'iperf3-srv_%s' % ip_address.get('addr'))
81 self.run_dir = None
Pau Espin Pedrol8a725862018-10-26 15:54:28 +020082 self.process = None
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +010083 self._run_node = None
Pau Espin Pedrola442cb82020-05-05 12:54:37 +020084 self.testenv = testenv
Pau Espin Pedrol8a725862018-10-26 15:54:28 +020085 self.ip_address = ip_address
Pau Espin Pedrolcf6a3602018-11-14 16:38:19 +010086 self._port = IPerf3Server.DEFAULT_SRV_PORT
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +010087 self.log_file = None
88 self.rem_host = None
89 self.remote_log_file = None
90 self.log_copied = False
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +020091 self.logfile_supported = False # some older versions of iperf doesn't support --logfile arg
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +010092
93 def cleanup(self):
94 if self.process is None:
95 return
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +020096 if self.runs_locally() or not self.logfile_supported:
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +010097 return
98 # copy back files (may not exist, for instance if there was an early error of process):
99 try:
100 self.rem_host.scpfrom('scp-back-log', self.remote_log_file, self.log_file)
101 except Exception as e:
102 self.log(repr(e))
103
104 def runs_locally(self):
105 locally = not self._run_node or self._run_node.is_local()
106 return locally
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200107
108 def start(self):
109 self.log('Starting iperf3-srv')
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100110 self.log_copied = False
Pau Espin Pedrol2a2d8462020-05-11 10:56:52 +0200111 self.run_dir = util.Dir(self.testenv.test().get_run_dir().new_dir(self.name()))
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100112 self.log_file = self.run_dir.new_file(IPerf3Server.LOGFILE)
113 if self.runs_locally():
114 self.start_locally()
115 else:
116 self.start_remotely()
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200117
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100118 def start_remotely(self):
119 self.rem_host = remote.RemoteHost(self.run_dir, self._run_node.ssh_user(), self._run_node.ssh_addr())
120 remote_prefix_dir = util.Dir(IPerf3Server.REMOTE_DIR)
121 remote_run_dir = util.Dir(remote_prefix_dir.child('srv-' + str(self)))
122 self.remote_log_file = remote_run_dir.child(IPerf3Server.LOGFILE)
123
124 self.rem_host.recreate_remote_dir(remote_run_dir)
125
126 args = ('iperf3', '-s', '-B', self.addr(),
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +0200127 '-p', str(self._port), '-J')
128 if self.logfile_supported:
129 args += ('--logfile', self.remote_log_file,)
130
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100131 self.process = self.rem_host.RemoteProcess(self.name(), args)
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200132 self.testenv.remember_to_stop(self.process)
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100133 self.process.launch()
134
135 def start_locally(self):
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200136 pcap_recorder.PcapRecorder(self.testenv, self.run_dir.new_dir('pcap'), None,
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200137 'host %s and port not 22' % self.addr())
138
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100139 args = ('iperf3', '-s', '-B', self.addr(),
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +0200140 '-p', str(self._port), '-J')
141 if self.logfile_supported:
142 args += ('--logfile', os.path.abspath(self.log_file),)
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100143
144 self.process = process.Process(self.name(), self.run_dir, args, env={})
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200145 self.testenv.remember_to_stop(self.process)
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200146 self.process.launch()
147
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100148 def set_run_node(self, run_node):
149 self._run_node = run_node
150
Pau Espin Pedrolcf6a3602018-11-14 16:38:19 +0100151 def set_port(self, port):
152 self._port = port
153
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200154 def stop(self):
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200155 self.testenv.stop_process(self.process)
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200156
157 def get_results(self):
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +0200158 if self.logfile_supported:
159 if not self.runs_locally() and not self.log_copied:
160 self.rem_host.scpfrom('scp-back-log', self.remote_log_file, self.log_file)
161 self.log_copied = True
162 with open(self.log_file) as f:
163 return iperf3_result_to_json(self, f.read())
164 else:
165 return iperf3_result_to_json(self, self.process.get_stdout())
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200166
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +0200167 def print_results(self, client_was_udp):
168 if client_was_udp:
169 print_result_node_udp(self.get_results(), 'server')
170 else:
171 print_result_node_tcp(self.get_results(), 'server')
172
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200173 def addr(self):
174 return self.ip_address.get('addr')
175
176 def port(self):
177 return self._port
178
Pau Espin Pedrol0df63172018-11-14 16:39:30 +0100179 def __str__(self):
180 return "%s:%u" %(self.addr(), self.port())
181
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200182 def running(self):
183 return not self.process.terminated()
184
185 def create_client(self):
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200186 return IPerf3Client(self.testenv, self)
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200187
188class IPerf3Client(log.Origin):
189
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100190 REMOTE_DIR = '/tmp'
191 LOGFILE = 'iperf3_cli.json'
192
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +0200193 PROTO_TCP = "tcp"
194 PROTO_UDP = "udp"
195
Pau Espin Pedrolf4ab97f2020-05-25 13:54:30 +0200196 DIR_UL = "ul"
197 DIR_DL = "dl"
198 DIR_BI = "bi"
199
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +0200200 @classmethod
201 def validate_protocol(cls, val):
202 return val in (cls.PROTO_TCP, cls.PROTO_UDP)
203
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200204 def __init__(self, testenv, iperf3srv):
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200205 super().__init__(log.C_RUN, 'iperf3-cli_%s' % iperf3srv.addr())
206 self.run_dir = None
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200207 self.process = None
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100208 self._run_node = None
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200209 self.server = iperf3srv
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200210 self.testenv = testenv
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +0200211 self._proto = None
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100212 self.log_file = None
213 self.rem_host = None
214 self.remote_log_file = None
215 self.log_copied = False
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +0200216 self.logfile_supported = False # some older versions of iperf doesn't support --logfile arg
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100217
218 def runs_locally(self):
219 locally = not self._run_node or self._run_node.is_local()
220 return locally
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200221
Pau Espin Pedrold84a8382020-05-25 14:23:37 +0200222 def prepare_test_proc(self, dir=None, netns=None, time_sec=None, proto=None, bitrate=0):
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +0200223 values = config.get_defaults('iperf3cli')
224 config.overlay(values, self.testenv.suite().config().get('iperf3cli', {}))
Pau Espin Pedrolf4ab97f2020-05-25 13:54:30 +0200225
226 if dir is None:
227 dir = self.DIR_UL
228
Andre Puschmann2dcc4312020-03-28 15:34:00 +0100229 if time_sec is None:
Andre Puschmann2dcc4312020-03-28 15:34:00 +0100230 time_sec_str = values.get('time', time_sec)
231
232 # Convert duration to seconds
233 if isinstance(time_sec_str, str) and time_sec_str.endswith('h'):
234 time_sec = int(time_sec_str[:-1]) * 3600
235 elif isinstance(time_sec_str, str) and time_sec_str.endswith('m'):
236 time_sec = int(time_sec_str[:-1]) * 60
237 else:
238 time_sec = int(time_sec_str)
Andre Puschmann2dcc4312020-03-28 15:34:00 +0100239 assert(time_sec)
240
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +0200241 if proto is None:
242 proto = values.get('protocol', IPerf3Client.PROTO_TCP)
243 self._proto = proto
244
245 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 +0100246 self.log_copied = False
Pau Espin Pedrol2a2d8462020-05-11 10:56:52 +0200247 self.run_dir = util.Dir(self.testenv.test().get_run_dir().new_dir(self.name()))
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100248 self.log_file = self.run_dir.new_file(IPerf3Client.LOGFILE)
249 if self.runs_locally():
Pau Espin Pedrola9bc93d2020-06-12 15:34:28 +0200250 proc = self.prepare_test_proc_locally(dir, netns, time_sec, proto == IPerf3Client.PROTO_UDP, bitrate)
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100251 else:
Pau Espin Pedrola9bc93d2020-06-12 15:34:28 +0200252 proc = self.prepare_test_proc_remotely(dir, netns, time_sec, proto == IPerf3Client.PROTO_UDP, bitrate)
Andre Puschmannf678c4d2020-06-12 21:40:45 +0200253 proc.set_default_wait_timeout(time_sec + 30) # leave 30 extra sec for remote run, ctrl conn establishment, etc.
Pau Espin Pedrola9bc93d2020-06-12 15:34:28 +0200254 return proc
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200255
Pau Espin Pedrold84a8382020-05-25 14:23:37 +0200256 def prepare_test_proc_remotely(self, dir, netns, time_sec, use_udp, bitrate):
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100257 self.rem_host = remote.RemoteHost(self.run_dir, self._run_node.ssh_user(), self._run_node.ssh_addr())
258
259 remote_prefix_dir = util.Dir(IPerf3Client.REMOTE_DIR)
260 remote_run_dir = util.Dir(remote_prefix_dir.child('cli-' + str(self)))
261 self.remote_log_file = remote_run_dir.child(IPerf3Client.LOGFILE)
262
263 self.rem_host.recreate_remote_dir(remote_run_dir)
264
265 popen_args = ('iperf3', '-c', self.server.addr(),
266 '-p', str(self.server.port()), '-J',
Pau Espin Pedrol085a17e2020-03-02 18:14:02 +0100267 '-t', str(time_sec))
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +0200268 if self.logfile_supported:
269 popen_args += ('--logfile', self.remote_log_file,)
Pau Espin Pedrolf4ab97f2020-05-25 13:54:30 +0200270 if dir == IPerf3Client.DIR_DL:
Pau Espin Pedrol905e5032020-03-02 15:35:33 +0100271 popen_args += ('-R',)
Pau Espin Pedrolf4ab97f2020-05-25 13:54:30 +0200272 elif dir == IPerf3Client.DIR_BI:
273 popen_args += ('--bidir',)
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +0200274 if use_udp:
Pau Espin Pedrold84a8382020-05-25 14:23:37 +0200275 popen_args += ('-u', '-b', str(bitrate))
Pau Espin Pedrol905e5032020-03-02 15:35:33 +0100276
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100277 if netns:
278 self.process = self.rem_host.RemoteNetNSProcess(self.name(), netns, popen_args, env={})
279 else:
280 self.process = self.rem_host.RemoteProcess(self.name(), popen_args, env={})
281 return self.process
282
Pau Espin Pedrolf4ab97f2020-05-25 13:54:30 +0200283 def prepare_test_proc_locally(self, dir, netns, time_sec, use_udp):
Pau Espin Pedrola442cb82020-05-05 12:54:37 +0200284 pcap_recorder.PcapRecorder(self.testenv, self.run_dir.new_dir('pcap'), None,
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200285 'host %s and port not 22' % self.server.addr(), netns)
286
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200287 popen_args = ('iperf3', '-c', self.server.addr(),
288 '-p', str(self.server.port()), '-J',
Pau Espin Pedrol085a17e2020-03-02 18:14:02 +0100289 '-t', str(time_sec))
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +0200290 if self.logfile_supported:
291 popen_args += ('--logfile', os.path.abspath(self.log_file),)
Pau Espin Pedrolf4ab97f2020-05-25 13:54:30 +0200292 if dir == IPerf3Client.DIR_DL:
Pau Espin Pedrol905e5032020-03-02 15:35:33 +0100293 popen_args += ('-R',)
Pau Espin Pedrolf4ab97f2020-05-25 13:54:30 +0200294 elif dir == IPerf3Client.DIR_BI:
295 popen_args += ('--bidir',)
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +0200296 if use_udp:
297 popen_args += ('-u', '-b', '0')
Pau Espin Pedrol905e5032020-03-02 15:35:33 +0100298
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200299 if netns:
300 self.process = process.NetNSProcess(self.name(), self.run_dir, netns, popen_args, env={})
301 else:
302 self.process = process.Process(self.name(), self.run_dir, popen_args, env={})
Pau Espin Pedrol0fc74372018-11-14 16:44:57 +0100303 return self.process
304
305 def run_test_sync(self, netns=None):
306 self.prepare_test_proc(netns)
Pau Espin Pedrol79df7392018-11-12 18:15:30 +0100307 self.process.launch_sync()
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200308 return self.get_results()
309
310 def get_results(self):
Pau Espin Pedrolfdd8e3a2020-04-14 11:30:24 +0200311 if self.logfile_supported:
312 if not self.runs_locally() and not self.log_copied:
313 self.rem_host.scpfrom('scp-back-log', self.remote_log_file, self.log_file)
314 self.log_copied = True
315 with open(self.log_file) as f:
316 return iperf3_result_to_json(self, f.read())
317 else:
318 return iperf3_result_to_json(self, self.process.get_stdout())
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200319
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +0200320 def print_results(self):
321 if self.proto() == self.PROTO_UDP:
322 print_result_node_udp(self.get_results(), 'client')
323 else:
324 print_result_node_tcp(self.get_results(), 'client')
325
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100326 def set_run_node(self, run_node):
327 self._run_node = run_node
328
Pau Espin Pedrol3a0dea62020-05-21 14:54:46 +0200329 def proto(self):
330 return self._proto
331
Pau Espin Pedrolc852ad82020-02-11 17:43:58 +0100332 def __str__(self):
333 # FIXME: somehow differentiate between several clients connected to same server?
334 return "%s:%u" %(self.server.addr(), self.server.port())
335
Pau Espin Pedrol8a725862018-10-26 15:54:28 +0200336# vim: expandtab tabstop=4 shiftwidth=4