3
Copyright (C) 2012-2014 Canonical Ltd.
6
Jeff Marcom <jeff.marcom@canonical.com>
7
Daniel Manrique <roadmr@ubuntu.com>
9
This program is free software: you can redistribute it and/or modify
10
it under the terms of the GNU General Public License version 3,
11
as published by the Free Software Foundation.
13
This program is distributed in the hope that it will be useful,
14
but WITHOUT ANY WARRANTY; without even the implied warranty of
15
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
GNU General Public License for more details.
18
You should have received a copy of the GNU General Public License
19
along with this program. If not, see <http://www.gnu.org/licenses/>.
22
from argparse import (
29
from ftplib import FTP
37
from subprocess import (
45
logging.basicConfig(level=logging.DEBUG)
48
class IPerfPerformanceTest(object):
49
"""Measures performance of interface using iperf client
50
and target. Calculated speed is measured against theorectical
51
throughput of selected interface"""
61
self.iface = Interface(interface)
63
self.protocol = protocol
64
self.fail_threshold = fail_threshold
69
# Bugfix to make sure route tables are fixed
70
cmd = "ping -c 3 {}".format(self.target)
71
logging.debug('Pinging target to ensure routing is updated')
74
check_call(shlex.split(cmd), universal_newlines=True)
76
logging.error('Pinging of target failed! Check cables and networking.')
79
cmd = "timeout 180 iperf -c {} -n {}".format(self.target, self.mbytes)
83
iperf_return = check_output(
84
shlex.split(cmd), universal_newlines=True)
85
except CalledProcessError as iperf_exception:
86
if iperf_exception.returncode != 124:
87
# timeout command will return 124 if iperf timed out, so any
88
# other return value means something did fail
89
logging.error("Failed executing iperf: %s",
90
iperf_exception.output)
91
return iperf_exception.returncode
93
# this is normal so we "except" this exception and we
94
# "pass through" whatever output iperf did manage to produce.
95
# When confronted with SIGTERM iperf should stop and output
96
# a partial (but usable) result.
97
logging.warning("iperf timed out - this should be OK")
98
iperf_return = iperf_exception.output
102
match = re.search(r'\d+\s([GM])bits', iperf_return)
104
throughput = match.group(0).split()[0]
105
units = match.group(1)
106
# self.iface.max_speed is always in mb/s, so we need to scale
107
# throughput to match
108
scaled_throughput = int(throughput)
110
scaled_throughput *= 1000
112
scaled_throughput /= 1000
114
percent = scaled_throughput / int(self.iface.max_speed) * 100
115
except ZeroDivisionError:
116
# Catches a condition where the interface functions fine but
117
# ethtool fails to properly report max speed. In this case
118
# it's up to the reviewer to pass or fail.
119
logging.error("Max Speed was not reported properly. Run "
120
"ethtool and verify that the card is properly "
121
"reporting its capabilities.")
124
print("\nTransfer speed: {} {}b/s".format(throughput, units))
125
print("%3.2f%% of " % percent, end="")
126
print("theoretical max %sMb/s\n" % int(self.iface.max_speed))
128
if percent < self.fail_threshold:
129
logging.warn("Poor network performance detected")
132
logging.debug("Passed benchmark")
134
print("Failed iperf benchmark")
138
class FTPPerformanceTest(object):
139
"""Provides file transfer rate based information while
140
using the FTP protocol and sending a file (DEFAULT=1GB)
141
over the local or public network using a specified network
142
interface on the host."""
151
file2send="ftp_performance_test"):
154
self.username = username
155
self.password = password
156
self.iface = Interface(interface)
157
self.binary_size = binary_size
158
self.file2send = file2send
160
def _make_file2send(self):
162
Makes binary file to send over FTP.
163
Size defaults to 1GB if not supplied.
166
logging.debug("Creating %sGB file", self.binary_size)
168
file_size = (1024 * 1024 * 1024) * self.binary_size
169
with open(self.file2send, "wb") as out:
170
out.seek((file_size) - 1)
171
out.write('\0'.encode())
173
def send_file(self, filename=None):
175
Sends file over the network using FTP and returns the
176
amount of bytes sent and delay between send and completed.
180
file = open(self.file2send, 'rb')
181
filename = self.file2send
183
send_time = time.time()
186
logging.debug("Sending file")
187
self.remote.storbinary("STOR " + filename, file, 1024)
188
except (ftplib.all_errors) as send_failure:
189
logging.error("Failed to send file to %s", self.target)
190
logging.error("Reason: %s", send_failure)
195
time_lapse = time.time() - send_time
196
bytes_sent = os.stat(filename).st_size
198
return bytes_sent, time_lapse
200
def close_connection(self):
202
Close connection to remote FTP target
208
Connects to FTP target and set the current directory as /
211
logging.debug("Connecting to %s", self.target)
213
self.remote = FTP(self.target)
214
self.remote.set_debuglevel(2)
215
self.remote.set_pasv(True)
216
except socket.error as connect_exception:
217
logging.error("Failed to connect to: %s", self.target)
220
logging.debug("Logging in")
221
logging.debug("{USER:%s, PASS:%s}", self.username, self.password)
224
self.remote.login(self.username, self.password)
225
except ftplib.error_perm as login_exception:
226
logging.error("failed to log into target: %s", self.target)
230
self.remote.cwd(default_out_dir)
236
"Interface": self.iface.interface,
237
"HWAddress": self.iface.macaddress,
238
"Duplex": self.iface.duplex_mode,
239
"Speed": self.iface.max_speed,
240
"Status": self.iface.status
245
if not os.path.isfile(self.file2send):
246
self._make_file2send()
248
# Connect to FTP target and send file
249
connected = self.connect()
251
if connected is False:
254
filesize, delay = self.send_file()
256
# Remove created binary
258
os.remove(self.file2send)
259
except (IOError, OSError) as file_delete_error:
260
logging.error("Could not remove previous ftp file")
261
logging.error(file_delete_error)
263
if connected and filesize > 0:
265
logging.debug("Bytes sent (%s): %.2f seconds", filesize, delay)
267
# Calculate transfer rate and determine pass/fail status
268
mbs_speed = float(filesize / 131072) / float(delay)
269
percent = (mbs_speed / int(info["Speed"])) * 100
270
print("Transfer speed:")
271
print("%3.2f%% of" % percent)
272
print("theoretical max %smbs" % int(info["Speed"]))
275
logging.warn("Poor network performance detected")
278
logging.debug("Passed benchmark")
280
print("Failed sending file via ftp")
284
class StressPerformanceTest:
286
def __init__(self, interface, target):
287
self.interface = interface
291
iperf_cmd = 'timeout 320 iperf -c {} -t 300'.format(self.target)
292
print("Running iperf...")
293
iperf = subprocess.Popen(shlex.split(iperf_cmd))
295
ping_cmd = 'ping -I {} {}'.format(self.interface, self.target)
296
ping = subprocess.Popen(shlex.split(ping_cmd), stdout=subprocess.PIPE)
300
(out, err) = ping.communicate()
302
if iperf.returncode != 0:
303
return iperf.returncode
305
print("Running ping test...")
307
time_re = re.compile('(?<=time=)[0-9]*')
308
for line in out.decode().split('\n'):
309
time = time_re.search(line)
311
if time and int(time.group()) > 2000:
313
print("ICMP packet was delayed by > 2000 ms.")
315
if 'unreachable' in line.lower():
322
class Interface(socket.socket):
324
Simple class that provides network interface information.
327
def __init__(self, interface):
329
super(Interface, self).__init__(
330
socket.AF_INET, socket.IPPROTO_ICMP)
332
self.interface = interface
334
self.dev_path = os.path.join("/sys/class/net", self.interface)
336
def _read_data(self, type):
338
return open(os.path.join(self.dev_path, type)).read().strip()
340
print("{}: Attribute not found".format(type))
344
freq = struct.pack('256s', self.interface[:15].encode())
347
nic_data = fcntl.ioctl(self.fileno(), 0x8915, freq)
349
logging.error("No IP address for %s", self.interface)
351
return socket.inet_ntoa(nic_data[20:24])
355
freq = struct.pack('256s', self.interface.encode())
358
mask_data = fcntl.ioctl(self.fileno(), 0x891b, freq)
360
logging.error("No netmask for %s", self.interface)
362
return socket.inet_ntoa(mask_data[20:24])
366
return self._read_data("speed")
369
def macaddress(self):
370
return self._read_data("address")
373
def duplex_mode(self):
374
return self._read_data("duplex")
378
return self._read_data("operstate")
381
def device_name(self):
382
return self._read_data("device/label")
385
def get_test_parameters(args, environ, config_filename):
386
# Decide the actual values for test parameters, which can come
387
# from one of three possible sources: a config file, command-line
388
# arguments, or environment variables.
389
# - If command-line args were given, they take precedence
390
# - Next come environment variables, if set.
391
# - Last, values in the config file are used if present.
393
params = {"test_target_ftp": None,
396
"test_target_iperf": None}
398
#First (try to) load values from config file
399
config = configparser.SafeConfigParser()
402
with open(config_filename) as config_file:
403
config.readfp(config_file)
404
params["test_target_ftp"] = config.get("FTP", "Target")
405
params["test_user"] = config.get("FTP", "User")
406
params["test_pass"] = config.get("FTP", "Pass")
407
params["test_target_iperf"] = config.get("IPERF", "Target")
408
except FileNotFoundError as err:
409
pass # No biggie, we can still get configs from elsewhere
411
# Next see if we have environment variables to override the config file
412
# "partial" overrides are not allowed; if an env variable is missing,
413
# we won't use this at all.
414
if all([param.upper() in os.environ for param in params.keys()]):
415
for key in params.keys():
416
params[key] = os.environ[key.upper()]
418
# Finally, see if we have the command-line arguments that are the ultimate
419
# override. Again, we will only override if we have all of them.
420
if args.target and args.username and args.password:
421
params["test_target_ftp"] = args.target
422
params["test_user"] = args.username
423
params["test_pass"] = args.password
424
params["test_target_iperf"] = args.target
429
def interface_test(args):
430
if not "test_type" in vars(args):
433
# Determine whether to use the default or user-supplied config
435
DEFAULT_CFG = "/etc/checkbox.d/network.cfg"
436
if not "config" in vars(args):
437
config_filename = DEFAULT_CFG
439
config_filename = args.config
441
# Get the actual test data from one of three possible sources
442
test_parameters = get_test_parameters(args, os.environ, config_filename)
444
test_user = test_parameters["test_user"]
445
test_pass = test_parameters["test_pass"]
446
if (args.test_type.lower() == "iperf" or
447
args.test_type.lower() == "stress"):
448
test_target = test_parameters["test_target_iperf"]
450
test_target = test_parameters["test_target_ftp"]
452
# Validate that we got reasonable values
453
if "example.com" in test_target:
454
# Default values found in config file
455
logging.error("Please supply target via: %s", config_filename)
458
# Testing begins here!
460
# Check and make sure that interface is indeed connected
462
cmd = "ip link set dev %s up" % args.interface
463
check_call(shlex.split(cmd))
464
except CalledProcessError as interface_failure:
465
logging.error("Failed to use %s:%s", cmd, interface_failure)
468
# Give interface enough time to get DHCP address
472
# Stop all other interfaces
474
[iface for iface in os.listdir("/sys/class/net")
475
if iface != "lo" and iface != args.interface]
477
for iface in extra_interfaces:
478
logging.debug("Shutting down interface:%s", iface)
480
cmd = "ip link set dev %s down" % iface
481
check_call(shlex.split(cmd))
482
except CalledProcessError as interface_failure:
483
logging.error("Failed to use %s:%s", cmd, interface_failure)
487
# Execute FTP transfer benchmarking test
488
if args.test_type.lower() == "ftp":
489
ftp_benchmark = FTPPerformanceTest(
490
test_target, test_user, test_pass, args.interface)
493
ftp_benchmark.binary_size = int(args.filesize)
494
result = ftp_benchmark.run()
496
elif args.test_type.lower() == "iperf":
497
iperf_benchmark = IPerfPerformanceTest(args.interface, test_target,
499
result = iperf_benchmark.run()
501
elif args.test_type.lower() == "stress":
502
stress_benchmark = StressPerformanceTest(args.interface,
504
result = stress_benchmark.run()
506
for iface in extra_interfaces:
507
logging.debug("Restoring interface:%s", iface)
509
cmd = "ip link set dev %s up" % iface
510
check_call(shlex.split(cmd))
511
except CalledProcessError as interface_failure:
512
logging.error("Failed to use %s:%s", cmd, interface_failure)
518
def interface_info(args):
521
if "all" in vars(args):
524
for key, value in vars(args).items():
525
if value is True or info_set is True:
526
key = key.replace("-", "_")
529
key + ":", getattr(Interface(args.interface), key),
531
except AttributeError:
540
This script provides benchmarking and information for a specified network
543
Example NIC information usage:
544
network info -i eth0 --max-speed
546
For running ftp benchmark test:
547
network test -i eth0 -t ftp
548
--target 192.168.0.1 --username USERID --password PASSW0RD
554
Configuration can be supplied in three different ways, with the following
557
1- Command-line parameters (see above).
558
2- Environment variables (example will follow).
559
3- Configuration file (example will follow).
560
Default config location is /etc/checkbox.d/network.cfg
562
Environment variables
563
=====================
564
ALL environment variables must be defined, even if empty, for them to be
565
picked up. The variables are:
585
parser = ArgumentParser(
586
description=intro_message, formatter_class=RawTextHelpFormatter)
587
subparsers = parser.add_subparsers()
590
test_parser = subparsers.add_parser(
591
'test', help=("Run network performance test"))
592
info_parser = subparsers.add_parser(
593
'info', help=("Gather network info"))
596
test_parser.add_argument(
597
'-i', '--interface', type=str, required=True)
598
test_parser.add_argument(
599
'-t', '--test_type', type=str,
600
choices=("ftp", "iperf", "stress"), default="ftp",
601
help=("[FTP *Default*]"))
602
test_parser.add_argument('--target', type=str)
603
test_parser.add_argument(
604
'--username', type=str, help=("For FTP test only"))
605
test_parser.add_argument(
606
'--password', type=str, help=("For FTP test only"))
607
test_parser.add_argument(
608
'--filesize', type=str,
609
help="Size (GB) of binary file to send **Note** for FTP test only")
610
test_parser.add_argument(
611
'--config', type=str,
612
default="/etc/checkbox.d/network.cfg",
613
help="Supply config file for target/host network parameters")
614
test_parser.add_argument(
615
'--fail-threshold', type=int,
617
help=("IPERF Test ONLY. Set the failure threshold (Percent of maximum "
618
"theoretical bandwidth) as a number like 80. (Default is "
622
info_parser.add_argument(
623
'-i', '--interface', type=str, required=True)
624
info_parser.add_argument(
625
'--all', default=False, action="store_true")
626
info_parser.add_argument(
627
'--duplex-mode', default=False, action="store_true")
628
info_parser.add_argument(
629
'--max-speed', default=False, action="store_true")
630
info_parser.add_argument(
631
'--ipaddress', default=False, action="store_true")
632
info_parser.add_argument(
633
'--netmask', default=False, action="store_true")
634
info_parser.add_argument(
635
'--device-name', default=False, action="store_true")
636
info_parser.add_argument(
637
'--macaddress', default=False, action="store_true")
638
info_parser.add_argument(
639
'--status', default=False, action="store_true",
640
help=("displays connection status"))
642
test_parser.set_defaults(func=interface_test)
643
info_parser.set_defaults(func=interface_info)
645
args = parser.parse_args()
647
return args.func(args)
650
if __name__ == "__main__":