Merge ~kissiel/checkbox-support:add-network-tool into checkbox-support:master
- Git
- lp:~kissiel/checkbox-support
- add-network-tool
- Merge into master
Proposed by
Maciej Kisielewski
Status: | Merged |
---|---|
Approved by: | Maciej Kisielewski |
Approved revision: | c62303ba08e0310e5096cf470cdd4781d8b5e439 |
Merged at revision: | 941d3ee75924ee9435d29fcb288e1d08a2ba48e8 |
Proposed branch: | ~kissiel/checkbox-support:add-network-tool |
Merge into: | checkbox-support:master |
Diff against target: |
746 lines (+729/-0) 2 files modified
checkbox_support/scripts/network.py (+727/-0) setup.py (+2/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Sylvain Pineau (community) | Needs Fixing | ||
Jonathan Cave (community) | Approve | ||
Review via email:
|
Commit message
Description of the change
Include 'network' test script in checkbox-script.
It's now used in plainbox-
To post a comment you must log in.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Maciej Kisielewski (kissiel) wrote : | # |
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Jonathan Cave (jocave) wrote : | # |
Straight import from elsewhere, looks fine to me.
review:
Approve
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Sylvain Pineau (sylvain-pineau) wrote : | # |
just grep'ed for ethtool, there's a comment with it we could update with mii-tool instead. Otherwise +1
review:
Needs Fixing
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Maciej Kisielewski (kissiel) wrote : | # |
> just grep'ed for ethtool, there's a comment with it we could update with mii-
> tool instead. Otherwise +1
Nice catch!
Amended, pushed.
Landing...
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/checkbox_support/scripts/network.py b/checkbox_support/scripts/network.py | |||
2 | 0 | new file mode 100755 | 0 | new file mode 100755 |
3 | index 0000000..f66c4ad | |||
4 | --- /dev/null | |||
5 | +++ b/checkbox_support/scripts/network.py | |||
6 | @@ -0,0 +1,727 @@ | |||
7 | 1 | #!/usr/bin/env python3 | ||
8 | 2 | """ | ||
9 | 3 | Copyright (C) 2012-2015 Canonical Ltd. | ||
10 | 4 | |||
11 | 5 | Authors | ||
12 | 6 | Jeff Marcom <jeff.marcom@canonical.com> | ||
13 | 7 | Daniel Manrique <roadmr@ubuntu.com> | ||
14 | 8 | Jeff Lane <jeff@ubuntu.com> | ||
15 | 9 | |||
16 | 10 | This program is free software: you can redistribute it and/or modify | ||
17 | 11 | it under the terms of the GNU General Public License version 3, | ||
18 | 12 | as published by the Free Software Foundation. | ||
19 | 13 | |||
20 | 14 | This program is distributed in the hope that it will be useful, | ||
21 | 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
22 | 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
23 | 17 | GNU General Public License for more details. | ||
24 | 18 | |||
25 | 19 | You should have received a copy of the GNU General Public License | ||
26 | 20 | along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
27 | 21 | """ | ||
28 | 22 | |||
29 | 23 | from argparse import ( | ||
30 | 24 | ArgumentParser, | ||
31 | 25 | RawTextHelpFormatter | ||
32 | 26 | ) | ||
33 | 27 | import datetime | ||
34 | 28 | import fcntl | ||
35 | 29 | import ipaddress | ||
36 | 30 | import logging | ||
37 | 31 | import os | ||
38 | 32 | import re | ||
39 | 33 | import shlex | ||
40 | 34 | import socket | ||
41 | 35 | import struct | ||
42 | 36 | import subprocess | ||
43 | 37 | import tempfile | ||
44 | 38 | from subprocess import ( | ||
45 | 39 | CalledProcessError, | ||
46 | 40 | check_call, | ||
47 | 41 | check_output, | ||
48 | 42 | STDOUT | ||
49 | 43 | ) | ||
50 | 44 | import sys | ||
51 | 45 | import time | ||
52 | 46 | |||
53 | 47 | |||
54 | 48 | class IPerfPerformanceTest(object): | ||
55 | 49 | """Measures performance of interface using iperf client | ||
56 | 50 | and target. Calculated speed is measured against theorectical | ||
57 | 51 | throughput of selected interface""" | ||
58 | 52 | |||
59 | 53 | def __init__( | ||
60 | 54 | self, | ||
61 | 55 | interface, | ||
62 | 56 | target, | ||
63 | 57 | fail_threshold, | ||
64 | 58 | cpu_load_fail_threshold, | ||
65 | 59 | iperf3, | ||
66 | 60 | protocol="tcp", | ||
67 | 61 | data_size="1", | ||
68 | 62 | run_time=None, | ||
69 | 63 | scan_timeout=3600): | ||
70 | 64 | |||
71 | 65 | self.iface = Interface(interface) | ||
72 | 66 | self.target = target | ||
73 | 67 | self.protocol = protocol | ||
74 | 68 | self.fail_threshold = fail_threshold | ||
75 | 69 | self.cpu_load_fail_threshold = cpu_load_fail_threshold | ||
76 | 70 | self.iperf3 = iperf3 | ||
77 | 71 | self.data_size = data_size | ||
78 | 72 | self.run_time = run_time | ||
79 | 73 | self.scan_timeout = scan_timeout | ||
80 | 74 | |||
81 | 75 | def run(self): | ||
82 | 76 | # if max_speed is 0, assume it's wifi and move on | ||
83 | 77 | if self.iface.max_speed == 0: | ||
84 | 78 | logging.warning("No max speed detected, assuming Wireless device " | ||
85 | 79 | "and continuing with test.") | ||
86 | 80 | # Otherwise, no sense in running if we're not running at full speed. | ||
87 | 81 | elif self.iface.max_speed > self.iface.link_speed: | ||
88 | 82 | logging.error("Detected link speed (%s) is lower than " | ||
89 | 83 | "detected max speed (%s)" % | ||
90 | 84 | (self.iface.link_speed, self.iface.max_speed)) | ||
91 | 85 | logging.error("Check your device configuration and try again") | ||
92 | 86 | return 1 | ||
93 | 87 | |||
94 | 88 | # Set the correct binary to run | ||
95 | 89 | if (self.iperf3): | ||
96 | 90 | self.executable = "iperf3 -V" | ||
97 | 91 | else: | ||
98 | 92 | self.executable = "iperf" | ||
99 | 93 | |||
100 | 94 | # If we set run_time, use that instead to build the command. | ||
101 | 95 | if self.run_time is not None: | ||
102 | 96 | cmd = "{} -c {} -t {} -i 1 -f m".format( | ||
103 | 97 | self.executable, self.target, self.run_time) | ||
104 | 98 | else: | ||
105 | 99 | # Because we can vary the data size, we need to vary the timeout as | ||
106 | 100 | # well. It takes an estimated 15 minutes to send 1GB over 10Mb/s. | ||
107 | 101 | # 802.11b is 11 Mb/s. So we'll assume 1.2x15 minutes or 18 minutes | ||
108 | 102 | # or 1080 seconds per Gigabit. This will allow for a long period of | ||
109 | 103 | # time without timeout to catch devices that slow down, and also | ||
110 | 104 | # not prematurely end iperf on low-bandwidth devices. | ||
111 | 105 | self.timeout = 1080*int(self.data_size) | ||
112 | 106 | cmd = "timeout {} {} -c {} -n {}G -i 1 -f -m".format( | ||
113 | 107 | self.timeout, self.executable, self.target, self.data_size) | ||
114 | 108 | |||
115 | 109 | logging.debug("Executing command {}".format(cmd)) | ||
116 | 110 | logging.info("Starting iperf against {}, this could take a while...". | ||
117 | 111 | format(self.target)) | ||
118 | 112 | try: | ||
119 | 113 | iperf_return = check_output( | ||
120 | 114 | shlex.split(cmd), universal_newlines=True) | ||
121 | 115 | except CalledProcessError as iperf_exception: | ||
122 | 116 | if iperf_exception.returncode != 124: | ||
123 | 117 | # timeout command will return 124 if iperf timed out, so any | ||
124 | 118 | # other return value means something did fail | ||
125 | 119 | logging.error("Failed executing iperf: %s", | ||
126 | 120 | iperf_exception.output) | ||
127 | 121 | return iperf_exception.returncode | ||
128 | 122 | else: | ||
129 | 123 | # this is normal so we "except" this exception and we | ||
130 | 124 | # "pass through" whatever output iperf did manage to produce. | ||
131 | 125 | # When confronted with SIGTERM iperf should stop and output | ||
132 | 126 | # a partial (but usable) result. | ||
133 | 127 | logging.info("iperf timed out - this should be OK") | ||
134 | 128 | iperf_return = iperf_exception.output | ||
135 | 129 | |||
136 | 130 | logging.debug(iperf_return) | ||
137 | 131 | # "CPU Utilization" line present only in iperf3 output | ||
138 | 132 | cpu = re.findall(r"CPU Utilization.*local/sender\s([\w\.]+)", | ||
139 | 133 | iperf_return) | ||
140 | 134 | # iperf3 provides "sender" and "receiver" summaries; remove them | ||
141 | 135 | iperf_return = re.sub(r".*(sender|receiver)", "", iperf_return) | ||
142 | 136 | speeds = list(map(float, re.findall(r"([\w\.]+)\sMbits/sec", | ||
143 | 137 | iperf_return))) | ||
144 | 138 | invalid_speed = False | ||
145 | 139 | if speeds: | ||
146 | 140 | throughput = sum(speeds)/len(speeds) | ||
147 | 141 | try: | ||
148 | 142 | percent = throughput / int(self.iface.max_speed) * 100 | ||
149 | 143 | except (ZeroDivisionError, TypeError): | ||
150 | 144 | # Catches a condition where the interface functions fine but | ||
151 | 145 | # mii-tool fails to properly report max speed. In this case | ||
152 | 146 | # it's up to the reviewer to pass or fail. | ||
153 | 147 | percent = 0 | ||
154 | 148 | invalid_speed = True | ||
155 | 149 | |||
156 | 150 | logging.info("Min Transfer speed: {} Mb/s".format(min(speeds))) | ||
157 | 151 | logging.info("Max Transfer speed: {} Mb/s".format(max(speeds))) | ||
158 | 152 | logging.info("Avg Transfer speed: {} Mb/s".format(throughput)) | ||
159 | 153 | if invalid_speed: | ||
160 | 154 | # If we have no link_speed (e.g. wireless interfaces don't | ||
161 | 155 | # report this), then we shouldn't penalize them because | ||
162 | 156 | # the transfer may have been reasonable. So in this case, | ||
163 | 157 | # we'll exit with a pass-warning. | ||
164 | 158 | logging.info("Unable to obtain maximum speed.") | ||
165 | 159 | logging.info("Considering the test as passed.") | ||
166 | 160 | return 0 | ||
167 | 161 | # Below is guaranteed to not throw an exception because we'll | ||
168 | 162 | # have exited above if it did. | ||
169 | 163 | logging.info("{:03.2f}% of theoretical max {} Mb/s". | ||
170 | 164 | format(percent, int(self.iface.max_speed))) | ||
171 | 165 | if cpu: | ||
172 | 166 | logging.info("") | ||
173 | 167 | logging.info("CPU utilization: {}%".format(cpu[0])) | ||
174 | 168 | cpu_load = float(cpu[0]) | ||
175 | 169 | else: | ||
176 | 170 | cpu_load = 0.0 | ||
177 | 171 | if percent < self.fail_threshold or \ | ||
178 | 172 | cpu_load > self.cpu_load_fail_threshold: | ||
179 | 173 | logging.warn("Poor network performance detected against {}". | ||
180 | 174 | format(self.target)) | ||
181 | 175 | if percent < self.fail_threshold: | ||
182 | 176 | logging.warn(" Transfer speed: {} Mb/s". | ||
183 | 177 | format(throughput)) | ||
184 | 178 | logging.warn(" {:03.2f}% of theoretical max {} Mb/s\n". | ||
185 | 179 | format(percent, int(self.iface.max_speed))) | ||
186 | 180 | if cpu_load > self.cpu_load_fail_threshold: | ||
187 | 181 | logging.warn(" CPU load: {}%".format(cpu_load)) | ||
188 | 182 | logging.warn(" CPU load is above {}% maximum\n". | ||
189 | 183 | format(self.cpu_load_fail_threshold)) | ||
190 | 184 | return 30 | ||
191 | 185 | |||
192 | 186 | logging.debug("Passed benchmark against {}".format(self.target)) | ||
193 | 187 | else: | ||
194 | 188 | logging.error("Failed iperf benchmark against {}". | ||
195 | 189 | format(self.target)) | ||
196 | 190 | return 1 | ||
197 | 191 | |||
198 | 192 | |||
199 | 193 | class StressPerformanceTest: | ||
200 | 194 | |||
201 | 195 | def __init__(self, interface, target, iperf3): | ||
202 | 196 | self.interface = interface | ||
203 | 197 | self.target = target | ||
204 | 198 | self.iperf3 = iperf3 | ||
205 | 199 | |||
206 | 200 | def run(self): | ||
207 | 201 | if self.iperf3: | ||
208 | 202 | iperf_cmd = 'timeout 320 iperf3 -c {} -t 300'.format(self.target) | ||
209 | 203 | else: | ||
210 | 204 | iperf_cmd = 'timeout 320 iperf -c {} -t 300'.format(self.target) | ||
211 | 205 | print("Running iperf...") | ||
212 | 206 | iperf = subprocess.Popen(shlex.split(iperf_cmd)) | ||
213 | 207 | |||
214 | 208 | ping_cmd = 'ping -I {} {}'.format(self.interface, self.target) | ||
215 | 209 | ping = subprocess.Popen(shlex.split(ping_cmd), stdout=subprocess.PIPE) | ||
216 | 210 | iperf.communicate() | ||
217 | 211 | |||
218 | 212 | ping.terminate() | ||
219 | 213 | (out, err) = ping.communicate() | ||
220 | 214 | |||
221 | 215 | if iperf.returncode != 0: | ||
222 | 216 | return iperf.returncode | ||
223 | 217 | |||
224 | 218 | print("Running ping test...") | ||
225 | 219 | result = 0 | ||
226 | 220 | time_re = re.compile('(?<=time=)[0-9]*') | ||
227 | 221 | for line in out.decode().split('\n'): | ||
228 | 222 | time = time_re.search(line) | ||
229 | 223 | |||
230 | 224 | if time and int(time.group()) > 2000: | ||
231 | 225 | print(line) | ||
232 | 226 | print("ICMP packet was delayed by > 2000 ms.") | ||
233 | 227 | result = 1 | ||
234 | 228 | if 'unreachable' in line.lower(): | ||
235 | 229 | print(line) | ||
236 | 230 | result = 1 | ||
237 | 231 | |||
238 | 232 | return result | ||
239 | 233 | |||
240 | 234 | |||
241 | 235 | class Interface(socket.socket): | ||
242 | 236 | """ | ||
243 | 237 | Simple class that provides network interface information. | ||
244 | 238 | """ | ||
245 | 239 | |||
246 | 240 | def __init__(self, interface): | ||
247 | 241 | |||
248 | 242 | super(Interface, self).__init__( | ||
249 | 243 | socket.AF_INET, socket.IPPROTO_ICMP) | ||
250 | 244 | |||
251 | 245 | self.interface = interface | ||
252 | 246 | |||
253 | 247 | self.dev_path = os.path.join("/sys/class/net", self.interface) | ||
254 | 248 | |||
255 | 249 | def _read_data(self, type): | ||
256 | 250 | try: | ||
257 | 251 | return open(os.path.join(self.dev_path, type)).read().strip() | ||
258 | 252 | except OSError: | ||
259 | 253 | logging.info("%s: Attribute not found", type) | ||
260 | 254 | |||
261 | 255 | @property | ||
262 | 256 | def ipaddress(self): | ||
263 | 257 | freq = struct.pack('256s', self.interface[:15].encode()) | ||
264 | 258 | |||
265 | 259 | try: | ||
266 | 260 | nic_data = fcntl.ioctl(self.fileno(), 0x8915, freq) | ||
267 | 261 | except IOError: | ||
268 | 262 | logging.error("No IP address for %s", self.interface) | ||
269 | 263 | return 1 | ||
270 | 264 | return socket.inet_ntoa(nic_data[20:24]) | ||
271 | 265 | |||
272 | 266 | @property | ||
273 | 267 | def netmask(self): | ||
274 | 268 | freq = struct.pack('256s', self.interface.encode()) | ||
275 | 269 | |||
276 | 270 | try: | ||
277 | 271 | mask_data = fcntl.ioctl(self.fileno(), 0x891b, freq) | ||
278 | 272 | except IOError: | ||
279 | 273 | logging.error("No netmask for %s", self.interface) | ||
280 | 274 | return 1 | ||
281 | 275 | return socket.inet_ntoa(mask_data[20:24]) | ||
282 | 276 | |||
283 | 277 | @property | ||
284 | 278 | def link_speed(self): | ||
285 | 279 | return int(self._read_data("speed")) | ||
286 | 280 | |||
287 | 281 | @property | ||
288 | 282 | def max_speed(self): | ||
289 | 283 | # Parse mii-tool data for max speed | ||
290 | 284 | # search for numbers in the line starting with 'capabilities' | ||
291 | 285 | # return largest number as max_speed | ||
292 | 286 | try: | ||
293 | 287 | info = check_output(['mii-tool', '-v', self.interface], | ||
294 | 288 | universal_newlines=True, | ||
295 | 289 | stderr=STDOUT).split('\n') | ||
296 | 290 | except FileNotFoundError: | ||
297 | 291 | logging.warning('mii-tool not found! Unable to get max speed') | ||
298 | 292 | ethinfo = None | ||
299 | 293 | except CalledProcessError as e: | ||
300 | 294 | logging.error('mii-tool returned an error!') | ||
301 | 295 | logging.error(e.output) | ||
302 | 296 | ethinfo = None | ||
303 | 297 | finally: | ||
304 | 298 | regex = re.compile(r'(\d+)(base)([A-Z]+)') | ||
305 | 299 | speeds = [0] | ||
306 | 300 | for line in filter(lambda l: 'capabilities' in l, info): | ||
307 | 301 | for s in line.split(' '): | ||
308 | 302 | hit = regex.search(s) | ||
309 | 303 | if hit: | ||
310 | 304 | speeds.append(int(re.sub("\D", "", hit.group(0)))) | ||
311 | 305 | return max(speeds) | ||
312 | 306 | |||
313 | 307 | @property | ||
314 | 308 | def macaddress(self): | ||
315 | 309 | return self._read_data("address") | ||
316 | 310 | |||
317 | 311 | @property | ||
318 | 312 | def duplex_mode(self): | ||
319 | 313 | return self._read_data("duplex") | ||
320 | 314 | |||
321 | 315 | @property | ||
322 | 316 | def status(self): | ||
323 | 317 | return self._read_data("operstate") | ||
324 | 318 | |||
325 | 319 | @property | ||
326 | 320 | def device_name(self): | ||
327 | 321 | return self._read_data("device/label") | ||
328 | 322 | |||
329 | 323 | |||
330 | 324 | def get_test_parameters(args, environ): | ||
331 | 325 | # Decide the actual values for test parameters, which can come | ||
332 | 326 | # from one of two possible sources: command-line | ||
333 | 327 | # arguments, or environment variables. | ||
334 | 328 | # - If command-line args were given, they take precedence | ||
335 | 329 | # - Next come environment variables, if set. | ||
336 | 330 | |||
337 | 331 | params = {"test_target_iperf": None} | ||
338 | 332 | |||
339 | 333 | # See if we have environment variables | ||
340 | 334 | for key in params.keys(): | ||
341 | 335 | params[key] = os.environ.get(key.upper(), "") | ||
342 | 336 | |||
343 | 337 | # Finally, see if we have the command-line arguments that are the ultimate | ||
344 | 338 | # override. | ||
345 | 339 | if args.target: | ||
346 | 340 | params["test_target_iperf"] = args.target | ||
347 | 341 | |||
348 | 342 | return params | ||
349 | 343 | |||
350 | 344 | |||
351 | 345 | def can_ping(the_interface, test_target): | ||
352 | 346 | working_interface = False | ||
353 | 347 | num_loops = 0 | ||
354 | 348 | while (not working_interface) and (num_loops < 48): | ||
355 | 349 | working_interface = True | ||
356 | 350 | |||
357 | 351 | try: | ||
358 | 352 | with open(os.devnull, 'wb') as DEVNULL: | ||
359 | 353 | check_call(["ping", "-I", the_interface, | ||
360 | 354 | "-c", "1", test_target], | ||
361 | 355 | stdout=DEVNULL, stderr=DEVNULL) | ||
362 | 356 | except CalledProcessError as excp: | ||
363 | 357 | working_interface = False | ||
364 | 358 | if num_loops == 0: | ||
365 | 359 | logging.warning("Ping failure on {} ({})". | ||
366 | 360 | format(the_interface, excp)) | ||
367 | 361 | |||
368 | 362 | if not working_interface: | ||
369 | 363 | time.sleep(5) | ||
370 | 364 | num_loops += 1 | ||
371 | 365 | |||
372 | 366 | return working_interface | ||
373 | 367 | |||
374 | 368 | |||
375 | 369 | def run_test(args, test_target): | ||
376 | 370 | # Ensure that interface is fully up by waiting until it can | ||
377 | 371 | # ping the test server | ||
378 | 372 | if not can_ping(args.interface, test_target): | ||
379 | 373 | logging.error("Can't ping test server {} on {}".format(test_target, | ||
380 | 374 | args.interface)) | ||
381 | 375 | return 1 | ||
382 | 376 | |||
383 | 377 | # Execute requested networking test | ||
384 | 378 | if args.test_type.lower() == "iperf": | ||
385 | 379 | error_number = 0 | ||
386 | 380 | iperf_benchmark = IPerfPerformanceTest(args.interface, test_target, | ||
387 | 381 | args.fail_threshold, | ||
388 | 382 | args.cpu_load_fail_threshold, | ||
389 | 383 | args.iperf3) | ||
390 | 384 | if args.datasize: | ||
391 | 385 | iperf_benchmark.data_size = args.datasize | ||
392 | 386 | if args.runtime: | ||
393 | 387 | iperf_benchmark.run_time = args.runtime | ||
394 | 388 | run_num = 0 | ||
395 | 389 | while not error_number and run_num < args.num_runs: | ||
396 | 390 | error_number = iperf_benchmark.run() | ||
397 | 391 | run_num += 1 | ||
398 | 392 | logging.info(" Finished run number %s ".center(60, "-"), run_num) | ||
399 | 393 | elif args.test_type.lower() == "stress": | ||
400 | 394 | stress_benchmark = StressPerformanceTest(args.interface, | ||
401 | 395 | test_target, args.iperf3) | ||
402 | 396 | error_number = stress_benchmark.run() | ||
403 | 397 | else: | ||
404 | 398 | logging.error("Unknown test type {}".format(args.test_type)) | ||
405 | 399 | return 10 | ||
406 | 400 | return error_number | ||
407 | 401 | |||
408 | 402 | |||
409 | 403 | def make_target_list(iface, test_targets, log_warnings): | ||
410 | 404 | """Convert comma-separated string of test targets into a list form. | ||
411 | 405 | |||
412 | 406 | Converts test target list in string form into Python list form, omitting | ||
413 | 407 | entries that are not on the current network segment. | ||
414 | 408 | :param iface: | ||
415 | 409 | Name of network interface device (eth0, etc.) | ||
416 | 410 | :param test_targets: | ||
417 | 411 | Input test targets as string of comma-separated IP addresses or | ||
418 | 412 | hostnames | ||
419 | 413 | :param emit_warnings: | ||
420 | 414 | Whether to log warning messages | ||
421 | 415 | :returns: | ||
422 | 416 | List form of input string, minus invalid values | ||
423 | 417 | """ | ||
424 | 418 | test_targets_list = test_targets.split(",") | ||
425 | 419 | net = ipaddress.IPv4Network("{}/{}".format(Interface(iface).ipaddress, | ||
426 | 420 | Interface(iface).netmask), | ||
427 | 421 | False) | ||
428 | 422 | first_addr = net.network_address + 1 | ||
429 | 423 | last_addr = first_addr + net.num_addresses - 2 | ||
430 | 424 | return_list = list(test_targets_list) | ||
431 | 425 | for test_target in test_targets_list: | ||
432 | 426 | try: | ||
433 | 427 | test_target_ip = socket.gethostbyname(test_target) | ||
434 | 428 | except OSError: | ||
435 | 429 | test_target_ip = test_target | ||
436 | 430 | try: | ||
437 | 431 | target = ipaddress.IPv4Address(test_target_ip) | ||
438 | 432 | if (target < first_addr) or (target > last_addr): | ||
439 | 433 | if log_warnings: | ||
440 | 434 | logging.warning("test server {} ({}) is NOT ". | ||
441 | 435 | format(test_target, target)) | ||
442 | 436 | logging.warning("within {}; skipping".format(net)) | ||
443 | 437 | return_list.remove(test_target) | ||
444 | 438 | except ValueError: | ||
445 | 439 | if log_warnings: | ||
446 | 440 | logging.warning("Invalid address: {}; skipping". | ||
447 | 441 | format(test_target)) | ||
448 | 442 | return_list.remove(test_target) | ||
449 | 443 | return_list.reverse() | ||
450 | 444 | return return_list | ||
451 | 445 | |||
452 | 446 | |||
453 | 447 | def interface_test(args): | ||
454 | 448 | if not ("test_type" in vars(args)): | ||
455 | 449 | return | ||
456 | 450 | |||
457 | 451 | # Get the actual test data from one of two possible sources | ||
458 | 452 | test_parameters = get_test_parameters(args, os.environ) | ||
459 | 453 | |||
460 | 454 | if (args.test_type.lower() == "iperf" or | ||
461 | 455 | args.test_type.lower() == "stress"): | ||
462 | 456 | test_targets = test_parameters["test_target_iperf"] | ||
463 | 457 | test_targets_list = make_target_list(args.interface, test_targets, True) | ||
464 | 458 | |||
465 | 459 | # Validate that we got reasonable values | ||
466 | 460 | if not test_targets_list or "example.com" in test_targets: | ||
467 | 461 | # Default values found in config file | ||
468 | 462 | logging.error("Valid target server has not been supplied.") | ||
469 | 463 | logging.info("Configuration settings can be configured 3 different " | ||
470 | 464 | "ways:") | ||
471 | 465 | logging.info("1- If calling the script directly, pass the --target " | ||
472 | 466 | "option") | ||
473 | 467 | logging.info("2- Define the TEST_TARGET_IPERF environment variable") | ||
474 | 468 | logging.info("3- (If running the test via checkbox/plainbox, define " | ||
475 | 469 | "the ") | ||
476 | 470 | logging.info("target in /etc/xdg/canonical-certification.conf)") | ||
477 | 471 | logging.info("Please run this script with -h to see more details on " | ||
478 | 472 | "how to configure") | ||
479 | 473 | sys.exit(1) | ||
480 | 474 | |||
481 | 475 | # Testing begins here! | ||
482 | 476 | # | ||
483 | 477 | # Make sure that the interface is indeed connected | ||
484 | 478 | try: | ||
485 | 479 | check_call(["ip", "link", "set", "dev", args.interface, "up"]) | ||
486 | 480 | except CalledProcessError as interface_failure: | ||
487 | 481 | logging.error("Failed to use %s:%s", args.interface, interface_failure) | ||
488 | 482 | return 1 | ||
489 | 483 | |||
490 | 484 | # Back up routing table, since network down/up process | ||
491 | 485 | # tends to trash it.... | ||
492 | 486 | temp = tempfile.TemporaryFile() | ||
493 | 487 | try: | ||
494 | 488 | check_call(["ip", "route", "save", "table", "all"], stdout=temp) | ||
495 | 489 | except CalledProcessError as route_error: | ||
496 | 490 | logging.warning("Unable to save routing table: %s", route_error) | ||
497 | 491 | |||
498 | 492 | error_number = 0 | ||
499 | 493 | # Stop all other interfaces | ||
500 | 494 | extra_interfaces = \ | ||
501 | 495 | [iface for iface in os.listdir("/sys/class/net") | ||
502 | 496 | if iface != "lo" and iface != args.interface] | ||
503 | 497 | |||
504 | 498 | for iface in extra_interfaces: | ||
505 | 499 | logging.debug("Shutting down interface:%s", iface) | ||
506 | 500 | try: | ||
507 | 501 | check_call(["ip", "link", "set", "dev", iface, "down"]) | ||
508 | 502 | except CalledProcessError as interface_failure: | ||
509 | 503 | logging.error("Failed to shut down %s:%s", | ||
510 | 504 | iface, interface_failure) | ||
511 | 505 | error_number = 3 | ||
512 | 506 | |||
513 | 507 | if error_number == 0: | ||
514 | 508 | start_time = datetime.datetime.now() | ||
515 | 509 | first_loop = True | ||
516 | 510 | # Keep testing until a success or we run out of both targets and time | ||
517 | 511 | while test_targets_list: | ||
518 | 512 | test_target = test_targets_list.pop().strip() | ||
519 | 513 | error_number = run_test(args, test_target) | ||
520 | 514 | elapsed_seconds = (datetime.datetime.now() - start_time).seconds | ||
521 | 515 | if (elapsed_seconds > args.scan_timeout and not first_loop) or \ | ||
522 | 516 | not error_number: | ||
523 | 517 | break | ||
524 | 518 | if not test_targets_list: | ||
525 | 519 | logging.info(" Exhausted test target list; trying again " | ||
526 | 520 | .center(60, "=")) | ||
527 | 521 | test_targets_list = make_target_list(args.interface, test_targets, False) | ||
528 | 522 | time.sleep(30) | ||
529 | 523 | first_loop = False | ||
530 | 524 | |||
531 | 525 | for iface in extra_interfaces: | ||
532 | 526 | logging.debug("Restoring interface:%s", iface) | ||
533 | 527 | try: | ||
534 | 528 | check_call(["ip", "link", "set", "dev", iface, "up"]) | ||
535 | 529 | except CalledProcessError as interface_failure: | ||
536 | 530 | logging.error("Failed to restore %s:%s", iface, interface_failure) | ||
537 | 531 | error_number = 3 | ||
538 | 532 | |||
539 | 533 | # Restore routing table to original state | ||
540 | 534 | temp.seek(0) | ||
541 | 535 | try: | ||
542 | 536 | # Harmless "RTNETLINK answers: File exists" messages on stderr | ||
543 | 537 | with open(os.devnull, 'wb') as DEVNULL: | ||
544 | 538 | check_call(["ip", "route", "restore"], stdin=temp, | ||
545 | 539 | stderr=DEVNULL) | ||
546 | 540 | except CalledProcessError as restore_failure: | ||
547 | 541 | logging.warning("Unable to restore routing table: %s", restore_failure) | ||
548 | 542 | temp.close() | ||
549 | 543 | |||
550 | 544 | return error_number | ||
551 | 545 | |||
552 | 546 | |||
553 | 547 | def interface_info(args): | ||
554 | 548 | |||
555 | 549 | info_set = "" | ||
556 | 550 | if "all" in vars(args): | ||
557 | 551 | info_set = args.all | ||
558 | 552 | |||
559 | 553 | for key, value in vars(args).items(): | ||
560 | 554 | if value is True or info_set is True: | ||
561 | 555 | key = key.replace("-", "_") | ||
562 | 556 | try: | ||
563 | 557 | print( | ||
564 | 558 | key + ":", getattr(Interface(args.interface), key), | ||
565 | 559 | file=sys.stderr) | ||
566 | 560 | except AttributeError: | ||
567 | 561 | pass | ||
568 | 562 | |||
569 | 563 | |||
570 | 564 | def main(): | ||
571 | 565 | |||
572 | 566 | intro_message = """ | ||
573 | 567 | Network module | ||
574 | 568 | |||
575 | 569 | This script provides benchmarking and information for a specified network | ||
576 | 570 | interface. | ||
577 | 571 | |||
578 | 572 | Example NIC information usage: | ||
579 | 573 | network info -i eth0 --max-speed | ||
580 | 574 | |||
581 | 575 | For running iperf test: | ||
582 | 576 | network test -i eth0 -t iperf --target 192.168.0.1 | ||
583 | 577 | NOTE: The iperf test requires an iperf server running on the same network | ||
584 | 578 | segment that the test machine is running on. | ||
585 | 579 | |||
586 | 580 | Also, you can use iperf3 rather than iperf2 by specifying the -3 or --iperf3 | ||
587 | 581 | option like so: | ||
588 | 582 | |||
589 | 583 | network test -i eth0 -t iperf -3 --target 192.168.0.1 | ||
590 | 584 | |||
591 | 585 | Configuration | ||
592 | 586 | ============= | ||
593 | 587 | |||
594 | 588 | Configuration can be supplied in three different ways, with the following | ||
595 | 589 | priorities: | ||
596 | 590 | |||
597 | 591 | 1- Command-line parameters (see above). | ||
598 | 592 | 2- Environment variables (example will follow). | ||
599 | 593 | 3- If run via checkbox/plainbox, /etc/xdg/checkbox-certification.conf | ||
600 | 594 | can have the below-mentioned environment variables defined in the | ||
601 | 595 | [environment] section. An example file is provided and can be simply | ||
602 | 596 | modified with the correct values. | ||
603 | 597 | |||
604 | 598 | Environment variables | ||
605 | 599 | ===================== | ||
606 | 600 | The variables are: | ||
607 | 601 | TEST_TARGET_IPERF | ||
608 | 602 | |||
609 | 603 | example config file | ||
610 | 604 | =================== | ||
611 | 605 | [environment] | ||
612 | 606 | TEST_TARGET_IPERF = iperf-server.example.com | ||
613 | 607 | |||
614 | 608 | |||
615 | 609 | **NOTE** | ||
616 | 610 | |||
617 | 611 | """ | ||
618 | 612 | |||
619 | 613 | parser = ArgumentParser( | ||
620 | 614 | description=intro_message, formatter_class=RawTextHelpFormatter) | ||
621 | 615 | subparsers = parser.add_subparsers() | ||
622 | 616 | |||
623 | 617 | # Main cli options | ||
624 | 618 | test_parser = subparsers.add_parser( | ||
625 | 619 | 'test', help=("Run network performance test")) | ||
626 | 620 | info_parser = subparsers.add_parser( | ||
627 | 621 | 'info', help=("Gather network info")) | ||
628 | 622 | |||
629 | 623 | # Sub test options | ||
630 | 624 | action = test_parser.add_mutually_exclusive_group() | ||
631 | 625 | |||
632 | 626 | test_parser.add_argument( | ||
633 | 627 | '-i', '--interface', type=str, required=True) | ||
634 | 628 | test_parser.add_argument( | ||
635 | 629 | '-t', '--test_type', type=str, | ||
636 | 630 | choices=("iperf", "stress"), default="iperf", | ||
637 | 631 | help=("[iperf *Default*]")) | ||
638 | 632 | test_parser.add_argument( | ||
639 | 633 | '-3', '--iperf3', default=False, action="store_true", | ||
640 | 634 | help=("Tells the script to use iperf3 for testing, rather than the " | ||
641 | 635 | "default of iperf2")) | ||
642 | 636 | test_parser.add_argument('--target', type=str) | ||
643 | 637 | action.add_argument( | ||
644 | 638 | '--datasize', type=str, | ||
645 | 639 | default="1", | ||
646 | 640 | help=("CANNOT BE USED WITH --runtime. Amount of data to send. For " | ||
647 | 641 | "iperf tests this will direct iperf to send DATASIZE GB of " | ||
648 | 642 | "data to the target.")) | ||
649 | 643 | action.add_argument( | ||
650 | 644 | '--runtime', type=int, | ||
651 | 645 | default=60, | ||
652 | 646 | help=("CANNOT BE USED WITH --datasize. Send data for *runtime* " | ||
653 | 647 | "seconds. For iperf tests, this will send data for the amount " | ||
654 | 648 | "of time indicated, rather than until a certain file size is " | ||
655 | 649 | "reached.")) | ||
656 | 650 | test_parser.add_argument( | ||
657 | 651 | '--scan-timeout', type=int, | ||
658 | 652 | default=60, | ||
659 | 653 | help=("Sets the maximum time, in seconds, the test will scan for " | ||
660 | 654 | "iperf servers before giving up.")) | ||
661 | 655 | test_parser.add_argument( | ||
662 | 656 | '--config', type=str, | ||
663 | 657 | default="/etc/checkbox.d/network.cfg", | ||
664 | 658 | help="Supply config file for target/host network parameters") | ||
665 | 659 | test_parser.add_argument( | ||
666 | 660 | '--fail-threshold', type=int, | ||
667 | 661 | default=40, | ||
668 | 662 | help=("IPERF Test ONLY. Set the failure threshold (Percent of maximum " | ||
669 | 663 | "theoretical bandwidth) as a number like 80. (Default is " | ||
670 | 664 | "%(default)s)")) | ||
671 | 665 | test_parser.add_argument( | ||
672 | 666 | '--cpu-load-fail-threshold', type=int, | ||
673 | 667 | default=100, | ||
674 | 668 | help=("(IPERF Test ONLY and meaningful ONLY with --iperf3. Set the " | ||
675 | 669 | "failure threshold (above which the CPU load must not rise) as " | ||
676 | 670 | "a number like 80. (Default is %(default)s)")) | ||
677 | 671 | test_parser.add_argument( | ||
678 | 672 | '--num_runs', type=int, | ||
679 | 673 | default=1, | ||
680 | 674 | help=("Number of times to run the test. (Default is %(default)s)")) | ||
681 | 675 | test_parser.add_argument( | ||
682 | 676 | '--debug', default=False, action="store_true", | ||
683 | 677 | help="Turn on verbose output") | ||
684 | 678 | |||
685 | 679 | # Sub info options | ||
686 | 680 | info_parser.add_argument( | ||
687 | 681 | '-i', '--interface', type=str, required=True) | ||
688 | 682 | info_parser.add_argument( | ||
689 | 683 | '--all', default=False, action="store_true") | ||
690 | 684 | info_parser.add_argument( | ||
691 | 685 | '--duplex-mode', default=False, action="store_true") | ||
692 | 686 | info_parser.add_argument( | ||
693 | 687 | '--link-speed', default=False, action="store_true") | ||
694 | 688 | info_parser.add_argument( | ||
695 | 689 | '--max-speed', default=False, action="store_true") | ||
696 | 690 | info_parser.add_argument( | ||
697 | 691 | '--ipaddress', default=False, action="store_true") | ||
698 | 692 | info_parser.add_argument( | ||
699 | 693 | '--netmask', default=False, action="store_true") | ||
700 | 694 | info_parser.add_argument( | ||
701 | 695 | '--device-name', default=False, action="store_true") | ||
702 | 696 | info_parser.add_argument( | ||
703 | 697 | '--macaddress', default=False, action="store_true") | ||
704 | 698 | info_parser.add_argument( | ||
705 | 699 | '--status', default=False, action="store_true", | ||
706 | 700 | help=("displays connection status")) | ||
707 | 701 | info_parser.add_argument( | ||
708 | 702 | '--debug', default=False, action="store_true", | ||
709 | 703 | help="Turn on verbose output") | ||
710 | 704 | |||
711 | 705 | test_parser.set_defaults(func=interface_test) | ||
712 | 706 | info_parser.set_defaults(func=interface_info) | ||
713 | 707 | |||
714 | 708 | args = parser.parse_args() | ||
715 | 709 | if (args.func.__name__ is interface_test and | ||
716 | 710 | not args.cpu_load_fail_threshold != 100 and | ||
717 | 711 | not args.iperf3): | ||
718 | 712 | parser.error('--cpu-load-fail-threshold can only be set with ' | ||
719 | 713 | '--iperf3.') | ||
720 | 714 | |||
721 | 715 | if args.debug: | ||
722 | 716 | logging.basicConfig(level=logging.DEBUG) | ||
723 | 717 | else: | ||
724 | 718 | logging.basicConfig(level=logging.INFO) | ||
725 | 719 | |||
726 | 720 | if 'func' not in args: | ||
727 | 721 | parser.print_help() | ||
728 | 722 | else: | ||
729 | 723 | return args.func(args) | ||
730 | 724 | |||
731 | 725 | |||
732 | 726 | if __name__ == "__main__": | ||
733 | 727 | sys.exit(main()) | ||
734 | diff --git a/setup.py b/setup.py | |||
735 | index 6fcfef8..86866ad 100755 | |||
736 | --- a/setup.py | |||
737 | +++ b/setup.py | |||
738 | @@ -80,6 +80,8 @@ setup( | |||
739 | 80 | "checkbox_support.scripts.run_watcher:main"), | 80 | "checkbox_support.scripts.run_watcher:main"), |
740 | 81 | ("checkbox-support-fwts_test=" | 81 | ("checkbox-support-fwts_test=" |
741 | 82 | "checkbox_support.scripts.fwts_test:main"), | 82 | "checkbox_support.scripts.fwts_test:main"), |
742 | 83 | ("checkbox-support-network=" | ||
743 | 84 | "checkbox_support.scripts.network:main"), | ||
744 | 83 | ], | 85 | ], |
745 | 84 | }, | 86 | }, |
746 | 85 | ) | 87 | ) |
Launchpad encountered an internal error during the following operation: generating the diff for a merge proposal. It was logged with id OOPS-08e58488e719650225fdf09e931e7103. Sorry for the inconvenience.
ugh