3
Copyright (C) 2012 Canonical Ltd.
6
Jeff Marcom <jeff.marcom@canonical.com>
8
This program is free software: you can redistribute it and/or modify
9
it under the terms of the GNU General Public License version 3,
10
as published by the Free Software Foundation.
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.
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/>.
21
from argparse import ArgumentParser
25
from ftplib import FTP
31
from subprocess import check_call, CalledProcessError
36
logging.basicConfig(level=logging.DEBUG)
39
class FTPPerformanceTest(object):
41
Provides file transfer rate based information while
42
using the FTP protocol and sending a file (DEFAULT=1GB)
43
over the local or public network using a specified network
44
interface on the host.
54
self.iface = Interface(interface)
56
self.username = username
57
self.password = password
61
self.file2send = "ftp_performance_test"
63
def _make_file2send(self):
65
Makes binary file to send over FTP.
66
Size defaults to 1GB if not supplied.
69
logging.debug("Creating %sGB file", self.binary_size)
71
file_size = (1024 * 1024 * 1024) * self.binary_size
72
with open(self.file2send, "wb") as out:
73
out.seek((file_size) - 1)
74
out.write('\0'.encode())
76
def send_file(self, filename=None):
78
Sends file over the network using FTP and returns the
79
amount of bytes sent and delay between send and completed.
83
file = open(self.file2send, 'rb')
84
filename = self.file2send
86
send_time = time.time()
89
logging.debug("Sending file")
90
self.remote.storbinary("STOR " + filename, file, 1024)
91
except ftplib.all_errors as send_failure:
92
logging.error("Failed to send file to %s", self.target)
93
logging.error("Reason: %s", send_failure)
98
time_lapse = time.time() - send_time
99
bytes_sent = os.stat(filename).st_size
101
return bytes_sent, time_lapse
103
def close_connection(self):
105
Close connection to remote FTP target
111
Connects to FTP target and set the current directory as /
114
logging.debug("Connecting to %s", self.target)
116
self.remote = FTP(self.target)
117
self.remote.set_debuglevel(2)
118
self.remote.set_pasv(True)
119
except socket.error as connect_exception:
120
logging.error("Failed to connect to: %s", self.target)
123
logging.debug("Logging in")
124
logging.debug("{USER:%s, PASS:%s}", self.username, self.password)
127
self.remote.login(self.username, self.password)
128
except ftplib.error_perm as login_exception:
129
logging.error("failed to log into target: %s", self.target)
133
self.remote.cwd(default_out_dir)
139
"Interface": self.iface.interface,
140
"HWAddress": self.iface.macaddress,
141
"Duplex": self.iface.duplex_mode,
142
"Speed": self.iface.max_speed,
143
"Status": self.iface.status
148
if not os.path.isfile(self.file2send):
149
self._make_file2send()
151
# Connect to FTP target and send file
152
connected = self.connect()
153
filesize, delay = self.send_file()
155
# Remove created binary
157
os.remove(self.file2send)
158
except (IOError, OSError) as file_delete_error:
159
logging.error("Could not remove previous ftp file")
160
logging.error(file_delete_error)
162
if connected and filesize > 0:
164
logging.debug("Bytes sent (%s): %.2f seconds", filesize, delay)
166
# Calculate transfer rate and determine pass/fail status
167
mbs_speed = float(filesize / 131072) / float(delay)
168
percent = (mbs_speed / int(info["Speed"])) * 100
169
logging.debug("Transfer speed:")
170
logging.debug("%3.2f%% of", percent)
171
logging.debug("theoretical max %smbs", int(info["Speed"]))
174
logging.warn("Poor network performance detected")
177
logging.debug("Passed benchmark")
180
class Interface(socket.socket):
182
Simple class that provides network interface information.
185
def __init__(self, interface):
187
super(Interface, self).__init__(
188
socket.AF_INET, socket.IPPROTO_ICMP)
190
self.interface = interface
192
self.dev_path = os.path.join("/sys/class/net", self.interface)
194
def _read_data(self, type):
195
return open(os.path.join(self.dev_path, type)).read().strip()
199
freq = struct.pack('256s', self.interface[:15].encode())
202
nic_data = fcntl.ioctl(self.fileno(), 0x8915, freq)
204
logging.error("No IP address for %s", self.interface)
206
return socket.inet_ntoa(nic_data[20:24])
210
freq = struct.pack('256s', self.interface.encode())
213
mask_data = fcntl.ioctl(self.fileno(), 0x891b, freq)
215
logging.error("No netmask for %s", self.interface)
217
return socket.inet_ntoa(mask_data[20:24])
221
return self._read_data("speed")
224
def macaddress(self):
225
return self._read_data("address")
228
def duplex_mode(self):
229
return self._read_data("duplex")
233
return self._read_data("operstate")
236
def device_name(self):
237
return self._read_data("device/label")
240
def interface_test(args):
241
if not "test_type" in vars(args):
244
# Check and make sure that interface is indeed connected
246
cmd = "ip link set dev %s up" % args.interface
247
check_call(shlex.split(cmd))
248
except CalledProcessError as interface_failure:
249
logging.error("Failed to use %s:%s", cmd, interface_failure)
252
# Give interface enough time to get DHCP address
255
# Open Network config file
256
DEFAULT_CFG = "/etc/checkbox.d/network.cfg"
257
if not "config" in vars(args):
258
config_file = DEFAULT_CFG
260
config_file = args.config
262
config = configparser.SafeConfigParser()
263
config.readfp(open(config_file))
265
# Set default network config options
266
test_target = args.target
267
test_user = args.username
268
test_pass = args.password
270
# Stop all other interfaces
272
[iface for iface in os.listdir("/sys/class/net")
273
if iface != "lo" and iface != args.interface]
275
for iface in extra_interfaces:
276
logging.debug("Shutting down interface:%s", iface)
278
cmd = "ip link set dev %s down" % iface
279
check_call(shlex.split(cmd))
280
except CalledProcessError as interface_failure:
281
logging.error("Failed to use %s:%s", cmd, interface_failure)
284
if args.test_type.lower() == "ftp":
285
if test_target is None:
286
# Set FTP parameters based on config file
287
test_target = config.get("FTP", "Target")
288
test_user = config.get("FTP", "User")
289
test_pass = config.get("FTP", "Pass")
291
if test_target == "canonical.com":
292
# Default values found in config file
293
logging.error("Please supply target via: %s", config_file)
296
# Being FTP benchmark for specified interface
297
ftp_benchmark = FTPPerformanceTest(
304
ftp_benchmark.binary_size = int(args.filesize)
305
return ftp_benchmark.run()
308
def interface_info(args):
311
if "all" in vars(args):
314
for key, value in vars(args).items():
315
if value is True or info_set is True:
316
key = key.replace("-", "_")
319
key + ":", getattr(Interface(args.interface), key),
321
except AttributeError:
326
parser = ArgumentParser(description="Network test module")
327
subparsers = parser.add_subparsers()
330
test_parser = subparsers.add_parser(
331
'test', help=("Run network performance test"))
332
info_parser = subparsers.add_parser(
333
'info', help=("Gather network info"))
336
test_parser.add_argument(
337
'-i', '--interface', type=str, required=True)
338
test_parser.add_argument(
339
'-t', '--test_type', type=str, default="ftp",
340
help=("Choices [FTP *Default*]"))
341
test_parser.add_argument('--target', type=str)
342
test_parser.add_argument('--username', type=str)
343
test_parser.add_argument('--password', type=str)
344
test_parser.add_argument(
345
'--filesize', type=str,
346
help="Size (GB) of binary file to send over network")
347
test_parser.add_argument(
348
'--config', type=str,
349
default="/etc/checkbox.d/network.cfg",
350
help="Supply config file for target/host network parameters")
353
info_parser.add_argument(
354
'-i', '--interface', type=str, required=True)
355
info_parser.add_argument(
356
'--all', default=False, action="store_true")
357
info_parser.add_argument(
358
'--duplex-mode', default=False, action="store_true")
359
info_parser.add_argument(
360
'--max-speed', default=False, action="store_true")
361
info_parser.add_argument(
362
'--ipaddress', default=False, action="store_true")
363
info_parser.add_argument(
364
'--netmask', default=False, action="store_true")
365
info_parser.add_argument(
366
'--device-name', default=False, action="store_true")
367
info_parser.add_argument(
368
'--macaddress', default=False, action="store_true")
369
info_parser.add_argument(
370
'--status', default=False, action="store_true",
371
help=("displays connection status"))
373
test_parser.set_defaults(func=interface_test)
374
info_parser.set_defaults(func=interface_info)
376
args = parser.parse_args()
381
if __name__ == "__main__":