~bladernr/maas-cert-server/foo

« back to all changes in this revision

Viewing changes to network

  • Committer: Jeff Lane
  • Date: 2014-03-27 21:08:50 UTC
  • Revision ID: jeffrey.lane@canonical.com-20140327210850-xv3gyug1omm3nect

* Initial release. Added files from work on project to create maas server for
certification purposes.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python3
 
2
"""
 
3
Copyright (C) 2012-2014 Canonical Ltd.
 
4
 
 
5
Authors
 
6
  Jeff Marcom <jeff.marcom@canonical.com>
 
7
  Daniel Manrique <roadmr@ubuntu.com>
 
8
 
 
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.
 
12
 
 
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.
 
17
 
 
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/>.
 
20
"""
 
21
 
 
22
from argparse import (
 
23
    ArgumentParser,
 
24
    RawTextHelpFormatter
 
25
)
 
26
import configparser
 
27
import fcntl
 
28
import ftplib
 
29
from ftplib import FTP
 
30
import logging
 
31
import os
 
32
import re
 
33
import shlex
 
34
import socket
 
35
import struct
 
36
import subprocess
 
37
from subprocess import (
 
38
    CalledProcessError,
 
39
    check_call,
 
40
    check_output
 
41
)
 
42
import sys
 
43
import time
 
44
 
 
45
logging.basicConfig(level=logging.DEBUG)
 
46
 
 
47
 
 
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"""
 
52
 
 
53
    def __init__(
 
54
            self,
 
55
            interface,
 
56
            target,
 
57
            fail_threshold,
 
58
            protocol="tcp",
 
59
            mbytes="1024M"):
 
60
 
 
61
        self.iface = Interface(interface)
 
62
        self.target = target
 
63
        self.protocol = protocol
 
64
        self.fail_threshold = fail_threshold
 
65
 
 
66
        self.mbytes = mbytes
 
67
 
 
68
    def run(self):
 
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')
 
72
        logging.debug(cmd)
 
73
        try:
 
74
            check_call(shlex.split(cmd), universal_newlines=True)
 
75
        except:
 
76
            logging.error('Pinging of target failed! Check cables and networking.')
 
77
            return 10
 
78
 
 
79
        cmd = "timeout 180 iperf -c {} -n {}".format(self.target, self.mbytes)
 
80
 
 
81
        logging.debug(cmd)
 
82
        try:
 
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
 
92
            else:
 
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
 
99
 
 
100
        # 930 Mbits/sec\n'
 
101
        print(iperf_return)
 
102
        match = re.search(r'\d+\s([GM])bits', iperf_return)
 
103
        if match:
 
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)
 
109
            if units == 'G':
 
110
                scaled_throughput *= 1000
 
111
            if units == 'K':
 
112
                scaled_throughput /= 1000
 
113
            try:
 
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.")
 
122
                percent = 0
 
123
 
 
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))
 
127
 
 
128
            if percent < self.fail_threshold:
 
129
                logging.warn("Poor network performance detected")
 
130
                return 30
 
131
 
 
132
            logging.debug("Passed benchmark")
 
133
        else:
 
134
            print("Failed iperf benchmark")
 
135
            return 1
 
136
 
 
137
 
 
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."""
 
143
 
 
144
    def __init__(
 
145
            self,
 
146
            target,
 
147
            username,
 
148
            password,
 
149
            interface,
 
150
            binary_size=1,
 
151
            file2send="ftp_performance_test"):
 
152
 
 
153
        self.target = target
 
154
        self.username = username
 
155
        self.password = password
 
156
        self.iface = Interface(interface)
 
157
        self.binary_size = binary_size
 
158
        self.file2send = file2send
 
159
 
 
160
    def _make_file2send(self):
 
161
        """
 
162
        Makes binary file to send over FTP.
 
163
        Size defaults to 1GB if not supplied.
 
164
        """
 
165
 
 
166
        logging.debug("Creating %sGB file", self.binary_size)
 
167
 
 
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())
 
172
 
 
173
    def send_file(self, filename=None):
 
174
        """
 
175
        Sends file over the network using FTP and returns the
 
176
        amount of bytes sent and delay between send and completed.
 
177
        """
 
178
 
 
179
        if filename is None:
 
180
            file = open(self.file2send, 'rb')
 
181
            filename = self.file2send
 
182
 
 
183
        send_time = time.time()
 
184
 
 
185
        try:
 
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)
 
191
            return 0, 0
 
192
 
 
193
        file.close()
 
194
 
 
195
        time_lapse = time.time() - send_time
 
196
        bytes_sent = os.stat(filename).st_size
 
197
 
 
198
        return bytes_sent, time_lapse
 
199
 
 
200
    def close_connection(self):
 
201
        """
 
202
        Close connection to remote FTP target
 
203
        """
 
204
        self.remote.close()
 
205
 
 
206
    def connect(self):
 
207
        """
 
208
        Connects to FTP target and set the current directory as /
 
209
        """
 
210
 
 
211
        logging.debug("Connecting to %s", self.target)
 
212
        try:
 
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)
 
218
            return False
 
219
 
 
220
        logging.debug("Logging in")
 
221
        logging.debug("{USER:%s, PASS:%s}", self.username, self.password)
 
222
 
 
223
        try:
 
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)
 
227
            return False
 
228
 
 
229
        default_out_dir = ""
 
230
        self.remote.cwd(default_out_dir)
 
231
        return True
 
232
 
 
233
    def run(self):
 
234
 
 
235
        info = {
 
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
 
241
        }
 
242
 
 
243
        logging.debug(info)
 
244
 
 
245
        if not os.path.isfile(self.file2send):
 
246
            self._make_file2send()
 
247
 
 
248
        # Connect to FTP target and send file
 
249
        connected = self.connect()
 
250
 
 
251
        if connected is False:
 
252
            return 3
 
253
 
 
254
        filesize, delay = self.send_file()
 
255
 
 
256
        # Remove created binary
 
257
        try:
 
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)
 
262
 
 
263
        if connected and filesize > 0:
 
264
 
 
265
            logging.debug("Bytes sent (%s): %.2f seconds", filesize, delay)
 
266
 
 
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"]))
 
273
 
 
274
            if percent < 40:
 
275
                logging.warn("Poor network performance detected")
 
276
                return 30
 
277
 
 
278
            logging.debug("Passed benchmark")
 
279
        else:
 
280
            print("Failed sending file via ftp")
 
281
            return 1
 
282
 
 
283
 
 
284
class StressPerformanceTest:
 
285
 
 
286
    def __init__(self, interface, target):
 
287
        self.interface = interface
 
288
        self.target = target
 
289
 
 
290
    def run(self):
 
291
        iperf_cmd = 'timeout 320 iperf -c {} -t 300'.format(self.target)
 
292
        print("Running iperf...")
 
293
        iperf = subprocess.Popen(shlex.split(iperf_cmd))
 
294
 
 
295
        ping_cmd = 'ping -I {} {}'.format(self.interface, self.target)
 
296
        ping = subprocess.Popen(shlex.split(ping_cmd), stdout=subprocess.PIPE)
 
297
        iperf.communicate()
 
298
 
 
299
        ping.terminate()
 
300
        (out, err) = ping.communicate()
 
301
 
 
302
        if iperf.returncode != 0:
 
303
            return iperf.returncode
 
304
 
 
305
        print("Running ping test...")
 
306
        result = 0
 
307
        time_re = re.compile('(?<=time=)[0-9]*')
 
308
        for line in out.decode().split('\n'):
 
309
            time = time_re.search(line)
 
310
 
 
311
            if time and int(time.group()) > 2000:
 
312
                print(line)
 
313
                print("ICMP packet was delayed by > 2000 ms.")
 
314
                result = 1
 
315
            if 'unreachable' in line.lower():
 
316
                print(line)
 
317
                result = 1
 
318
 
 
319
        return result
 
320
 
 
321
 
 
322
class Interface(socket.socket):
 
323
    """
 
324
    Simple class that provides network interface information.
 
325
    """
 
326
 
 
327
    def __init__(self, interface):
 
328
 
 
329
        super(Interface, self).__init__(
 
330
            socket.AF_INET, socket.IPPROTO_ICMP)
 
331
 
 
332
        self.interface = interface
 
333
 
 
334
        self.dev_path = os.path.join("/sys/class/net", self.interface)
 
335
 
 
336
    def _read_data(self, type):
 
337
        try:
 
338
            return open(os.path.join(self.dev_path, type)).read().strip()
 
339
        except OSError:
 
340
            print("{}: Attribute not found".format(type))
 
341
 
 
342
    @property
 
343
    def ipaddress(self):
 
344
        freq = struct.pack('256s', self.interface[:15].encode())
 
345
 
 
346
        try:
 
347
            nic_data = fcntl.ioctl(self.fileno(), 0x8915, freq)
 
348
        except IOError:
 
349
            logging.error("No IP address for %s", self.interface)
 
350
            return 1
 
351
        return socket.inet_ntoa(nic_data[20:24])
 
352
 
 
353
    @property
 
354
    def netmask(self):
 
355
        freq = struct.pack('256s', self.interface.encode())
 
356
 
 
357
        try:
 
358
            mask_data = fcntl.ioctl(self.fileno(), 0x891b, freq)
 
359
        except IOError:
 
360
            logging.error("No netmask for %s", self.interface)
 
361
            return 1
 
362
        return socket.inet_ntoa(mask_data[20:24])
 
363
 
 
364
    @property
 
365
    def max_speed(self):
 
366
        return self._read_data("speed")
 
367
 
 
368
    @property
 
369
    def macaddress(self):
 
370
        return self._read_data("address")
 
371
 
 
372
    @property
 
373
    def duplex_mode(self):
 
374
        return self._read_data("duplex")
 
375
 
 
376
    @property
 
377
    def status(self):
 
378
        return self._read_data("operstate")
 
379
 
 
380
    @property
 
381
    def device_name(self):
 
382
        return self._read_data("device/label")
 
383
 
 
384
 
 
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.
 
392
 
 
393
    params = {"test_target_ftp": None,
 
394
              "test_user": None,
 
395
              "test_pass": None,
 
396
              "test_target_iperf": None}
 
397
 
 
398
    #First (try to) load values from config file
 
399
    config = configparser.SafeConfigParser()
 
400
 
 
401
    try:
 
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
 
410
 
 
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()]
 
417
 
 
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
 
425
 
 
426
    return params
 
427
 
 
428
 
 
429
def interface_test(args):
 
430
    if not "test_type" in vars(args):
 
431
        return
 
432
 
 
433
    # Determine whether to use the default or user-supplied config
 
434
    # file name.
 
435
    DEFAULT_CFG = "/etc/checkbox.d/network.cfg"
 
436
    if not "config" in vars(args):
 
437
        config_filename = DEFAULT_CFG
 
438
    else:
 
439
        config_filename = args.config
 
440
 
 
441
    # Get the actual test data from one of three possible sources
 
442
    test_parameters = get_test_parameters(args, os.environ, config_filename)
 
443
 
 
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"]
 
449
    else:
 
450
        test_target = test_parameters["test_target_ftp"]
 
451
 
 
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)
 
456
        sys.exit(1)
 
457
 
 
458
    # Testing begins here!
 
459
    #
 
460
    # Check and make sure that interface is indeed connected
 
461
    try:
 
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)
 
466
        return 1
 
467
 
 
468
    # Give interface enough time to get DHCP address
 
469
    time.sleep(10)
 
470
 
 
471
    result = 0
 
472
    # Stop all other interfaces
 
473
    extra_interfaces = \
 
474
        [iface for iface in os.listdir("/sys/class/net")
 
475
         if iface != "lo" and iface != args.interface]
 
476
 
 
477
    for iface in extra_interfaces:
 
478
        logging.debug("Shutting down interface:%s", iface)
 
479
        try:
 
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)
 
484
            result = 3
 
485
 
 
486
    if result == 0:
 
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)
 
491
 
 
492
            if args.filesize:
 
493
                ftp_benchmark.binary_size = int(args.filesize)
 
494
            result = ftp_benchmark.run()
 
495
 
 
496
        elif args.test_type.lower() == "iperf":
 
497
            iperf_benchmark = IPerfPerformanceTest(args.interface, test_target, 
 
498
                                                   args.fail_threshold)
 
499
            result = iperf_benchmark.run()
 
500
 
 
501
        elif args.test_type.lower() == "stress":
 
502
            stress_benchmark = StressPerformanceTest(args.interface,
 
503
                                                     test_target)
 
504
            result = stress_benchmark.run()
 
505
 
 
506
    for iface in extra_interfaces:
 
507
        logging.debug("Restoring interface:%s", iface)
 
508
        try:
 
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)
 
513
            result = 3
 
514
 
 
515
    return result
 
516
 
 
517
 
 
518
def interface_info(args):
 
519
 
 
520
    info_set = ""
 
521
    if "all" in vars(args):
 
522
        info_set = args.all
 
523
 
 
524
    for key, value in vars(args).items():
 
525
        if value is True or info_set is True:
 
526
            key = key.replace("-", "_")
 
527
            try:
 
528
                print(
 
529
                    key + ":", getattr(Interface(args.interface), key),
 
530
                    file=sys.stderr)
 
531
            except AttributeError:
 
532
                pass
 
533
 
 
534
 
 
535
def main():
 
536
 
 
537
    intro_message = """
 
538
Network module
 
539
 
 
540
This script provides benchmarking and information for a specified network
 
541
interface.
 
542
 
 
543
Example NIC information usage:
 
544
network info -i eth0 --max-speed
 
545
 
 
546
For running ftp benchmark test:
 
547
network test -i eth0 -t ftp
 
548
--target 192.168.0.1 --username USERID --password PASSW0RD
 
549
--filesize-2
 
550
 
 
551
Configuration
 
552
=============
 
553
 
 
554
Configuration can be supplied in three different ways, with the following
 
555
priorities:
 
556
 
 
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
 
561
 
 
562
Environment variables
 
563
=====================
 
564
ALL environment variables must be defined, even if empty, for them to be
 
565
picked up. The variables are:
 
566
TEST_TARGET_FTP
 
567
TEST_USER
 
568
TEST_PASS
 
569
TEST_TARGET_IPERF
 
570
 
 
571
example config file
 
572
===================
 
573
[FTP]
 
574
 
 
575
Target: 192.168.1.23
 
576
User: FTPUser
 
577
Pass:PassW0Rd
 
578
 
 
579
[IPERF]
 
580
Target: 192.168.1.45
 
581
**NOTE**
 
582
 
 
583
"""
 
584
 
 
585
    parser = ArgumentParser(
 
586
        description=intro_message, formatter_class=RawTextHelpFormatter)
 
587
    subparsers = parser.add_subparsers()
 
588
 
 
589
    # Main cli options
 
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"))
 
594
 
 
595
    # Sub test options
 
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,
 
616
        default=40,
 
617
        help=("IPERF Test ONLY. Set the failure threshold (Percent of maximum "
 
618
              "theoretical bandwidth) as a number like 80.  (Default is "
 
619
              "%(default)s)"))
 
620
 
 
621
    # Sub info options
 
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"))
 
641
 
 
642
    test_parser.set_defaults(func=interface_test)
 
643
    info_parser.set_defaults(func=interface_info)
 
644
 
 
645
    args = parser.parse_args()
 
646
 
 
647
    return args.func(args)
 
648
 
 
649
 
 
650
if __name__ == "__main__":
 
651
    sys.exit(main())