Home | History | Annotate | Download | only in network
      1 # Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import re
      6 
      7 from autotest_lib.client.common_lib import error
      8 from autotest_lib.client.common_lib.cros import path_utils
      9 
     10 
     11 class ArpingRunner(object):
     12     """Delegate to run arping on a remote host."""
     13 
     14     DEFAULT_COUNT = 10
     15     SSH_TIMEOUT_MARGIN = 120
     16 
     17 
     18     def __init__(self, host, ping_interface):
     19         self._host = host
     20         self._arping_command = path_utils.must_be_installed(
     21                 '/usr/bin/arping', host=host)
     22         self._ping_interface = ping_interface
     23 
     24 
     25     def arping(self, target_ip, count=None, timeout_seconds=None):
     26         """Run arping on a remote host.
     27 
     28         @param target_ip: string IP address to use as the ARP target.
     29         @param count: int number of ARP packets to send.  The command
     30             will take roughly |count| seconds to complete, since arping
     31             sends a packet out once a second.
     32         @param timeout_seconds: int number of seconds to wait for arping
     33             to complete.  Override the default of one second per packet.
     34             Note that this doesn't change packet spacing.
     35 
     36         """
     37         if count is None:
     38             count = self.DEFAULT_COUNT
     39         if timeout_seconds is None:
     40             timeout_seconds  = count
     41         command_pieces = [self._arping_command]
     42         command_pieces.append('-b')  # Default to only sending broadcast ARPs.
     43         command_pieces.append('-w %d' % timeout_seconds)
     44         command_pieces.append('-c %d' % count)
     45         command_pieces.append('-I %s %s' % (self._ping_interface, target_ip))
     46         result = self._host.run(
     47                 ' '.join(command_pieces),
     48                 timeout=timeout_seconds + self.SSH_TIMEOUT_MARGIN,
     49                 ignore_status=True)
     50         return ArpingResult(result.stdout)
     51 
     52 
     53 class ArpingResult(object):
     54     """Can parse raw arping output and present a summary."""
     55 
     56     DEFAULT_LOSS_THRESHOLD = 30.0
     57 
     58 
     59     def __init__(self, stdout):
     60         """Construct an ArpingResult from the stdout of arping.
     61 
     62         A successful run looks something like this:
     63 
     64         ARPING 192.168.2.193 from 192.168.2.254 eth0
     65         Unicast reply from 192.168.2.193 [14:7D:C5:E1:53:83] 2.842ms
     66         Unicast reply from 192.168.2.193 [14:7D:C5:E1:53:83] 5.851ms
     67         Unicast reply from 192.168.2.193 [14:7D:C5:E1:53:83] 2.565ms
     68         Unicast reply from 192.168.2.193 [14:7D:C5:E1:53:83] 2.595ms
     69         Unicast reply from 192.168.2.193 [14:7D:C5:E1:53:83] 2.534ms
     70         Unicast reply from 192.168.2.193 [14:7D:C5:E1:53:83] 3.217ms
     71         Unicast request from 192.168.2.193 [14:7D:C5:E1:53:83] 748.657ms
     72         Sent 6 probes (6 broadcast(s))
     73         Received 7 response(s) (1 request(s))
     74 
     75         @param stdout string raw stdout of arping command.
     76 
     77         """
     78         latencies = []
     79         responders = set()
     80         num_sent = None
     81         regex = re.compile(r'(([0-9]{1,3}\.){3}[0-9]{1,3}) '
     82                            r'\[(([0-9A-Fa-f]{1,2}:){5}[0-9A-Fa-f]{1,2})\] +'
     83                            r'([0-9\.]+)ms')
     84         requests = 0
     85         for line in stdout.splitlines():
     86             if line.find('Unicast reply from') == 0:
     87                 match = re.search(regex, line.strip())
     88                 if match is None:
     89                     raise error.TestError('arping result parsing code failed '
     90                                           'to anticipate line: ' % line)
     91 
     92                 responder_ip = match.group(1)  # Maybe useful in the future?
     93                 responder_mac = match.group(3)
     94                 latency = float(match.group(5))
     95                 latencies.append(latency)
     96                 responders.add(responder_mac)
     97             if line.find('Unicast request from') == 0:
     98                 # We don't care about these really, but they mess up our
     99                 # primitive line counting.
    100                 requests += 1
    101             elif line.find('Sent ') == 0:
    102                 num_sent = int(line.split()[1])
    103             elif line.find('Received ') == 0:
    104                 count = int(line.split()[1])
    105                 if count != len(latencies) + requests:
    106                     raise error.TestFail('Failed to parse accurate latencies '
    107                                          'from stdout: %r.  Got %d, '
    108                                          'wanted %d.' % (stdout, len(latencies),
    109                                                          count))
    110         if num_sent is None:
    111             raise error.TestFail('Failed to parse number of arpings sent '
    112                                  'from %r' % stdout)
    113 
    114         if num_sent < 1:
    115             raise error.TestFail('No arpings sent.')
    116 
    117         self.loss = 100.0 * float(num_sent - len(latencies)) / num_sent
    118         self.average_latency = 0.0
    119         if latencies:
    120             self.average_latency = sum(latencies) / len(latencies)
    121         self.latencies = latencies
    122         self.responders = responders
    123 
    124 
    125     def was_successful(self, max_average_latency=None, valid_responders=None,
    126                        max_loss=DEFAULT_LOSS_THRESHOLD):
    127         """Checks if the arping was some definition of successful.
    128 
    129         @param max_average_latency float maximum value for average latency in
    130                 milliseconds.
    131         @param valid_responders iterable object of responder MAC addresses.
    132                 We'll check that we got only responses from valid responders.
    133         @param max_loss float maximum loss expressed as a percentage.
    134         @return True iff all criterion set to not None values hold.
    135 
    136         """
    137         if (max_average_latency is not None and
    138                 self.average_latency > max_average_latency):
    139             return False
    140 
    141         if (valid_responders is not None and
    142                 self.responders.difference(valid_responders)):
    143             return False
    144 
    145         if max_loss is not None and self.loss > max_loss:
    146             return False
    147 
    148         return True
    149 
    150 
    151     def __repr__(self):
    152         return ('%s(loss=%r, average_latency=%r, latencies=%r, responders=%r)' %
    153                 (self.__class__.__name__, self.loss, self.average_latency,
    154                  self.latencies, self.responders))
    155