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 logging
      6 import math
      7 import re
      8 
      9 from autotest_lib.client.bin import utils
     10 from autotest_lib.client.common_lib import error
     11 
     12 PLATFORM_LINUX = 'LINUX'
     13 PLATFORM_MACOS = 'MAC_OS'
     14 
     15 
     16 def _get_platform_delegate(platform):
     17     if platform == PLATFORM_LINUX:
     18         return LinuxPingDelegate
     19     elif platform == PLATFORM_MACOS:
     20         return MacPingDelegate
     21     else:
     22       raise error.TestError('%s is not a valid platform type', platform)
     23 
     24 
     25 def _regex_int_from_string(pattern, line):
     26     """Retrieve an integer from a string, using regex.
     27 
     28     @param pattern: The regular expression to apply to the input string.
     29     @param line: String input to retrieve an integer from.
     30     @return integer retrieved from the input string, or None if there is no
     31         match.
     32     """
     33     m = re.search(pattern, line)
     34     if m is not None:
     35         return int(m.group(1))
     36     return None
     37 
     38 
     39 def _regex_float_from_string(pattern, line):
     40     """Retrieve a float from a string, using regex.
     41 
     42     @param pattern: The regular expression to apply to the input string.
     43     @param line: String input to retrieve a float from.
     44     @return float retrieved from the input string, or None if there is no
     45         match.
     46     """
     47     m = re.search(pattern, line)
     48     if m is not None:
     49         return float(m.group(1))
     50     return None
     51 
     52 
     53 class MacPingDelegate(object):
     54     """Implement ping functionality for MacOS hosts."""
     55 
     56     @staticmethod
     57     def ping_arguments(ping_config):
     58         """
     59         @param ping_config PingConfig object describing the ping test for which
     60            arguments are needed.
     61         @return list of parameters to ping.
     62         """
     63         args = []
     64         args.append('-c %d' % ping_config.count)
     65         if ping_config.size is not None:
     66             args.append('-s %d' % ping_config.size)
     67         if ping_config.interval is not None:
     68             args.append('-i %f' % ping_config.interval)
     69         if ping_config.qos is not None:
     70             if ping_config.qos == 'be':
     71                 ping_config.append('-k 0')
     72             elif ping_config.qos == 'bk':
     73                 ping_config.append('-k 1')
     74             elif ping_config.qos == 'vi':
     75                 args.append('-k 2')
     76             elif ping_config.qos == 'vo':
     77                 args.append('-k 3')
     78             else:
     79                 raise error.TestFail('Unknown QoS value: %s' % ping_config.qos)
     80 
     81         # The last argument is the IP address to ping.
     82         args.append(ping_config.target_ip)
     83         return args
     84 
     85 
     86     @staticmethod
     87     def parse_from_output(ping_output):
     88         """Extract the ping results from stdout.
     89 
     90         @param ping_output string stdout from a ping/ping6 command.
     91 
     92         stdout from ping command looks like:
     93 
     94         PING 8.8.8.8 (8.8.8.8): 56 data bytes
     95         64 bytes from 8.8.8.8: icmp_seq=0 ttl=57 time=3.770 ms
     96         64 bytes from 8.8.8.8: icmp_seq=1 ttl=57 time=4.165 ms
     97         64 bytes from 8.8.8.8: icmp_seq=2 ttl=57 time=4.901 ms
     98 
     99         --- 8.8.8.8 ping statistics ---
    100         3 packets transmitted, 3 packets received, 0.0% packet loss
    101         round-trip min/avg/max/stddev = 3.770/4.279/4.901/0.469 ms
    102 
    103         stdout from ping6 command looks like:
    104 
    105         16 bytes from fdd2:8741:1993:8::, icmp_seq=16 hlim=64 time=1.783 ms
    106         16 bytes from fdd2:8741:1993:8::, icmp_seq=17 hlim=64 time=2.150 ms
    107         16 bytes from fdd2:8741:1993:8::, icmp_seq=18 hlim=64 time=2.516 ms
    108         16 bytes from fdd2:8741:1993:8::, icmp_seq=19 hlim=64 time=1.401 ms
    109 
    110         --- fdd2:8741:1993:8:: ping6 statistics ---
    111         20 packets transmitted, 20 packets received, 0.0% packet loss
    112         round-trip min/avg/max/std-dev = 1.401/2.122/3.012/0.431 ms
    113 
    114         This function will look for both 'stdev' and 'std-dev' in test results
    115         to support both ping and ping6 commands.
    116         """
    117         loss_line = (filter(lambda x: x.find('packets transmitted') > 0,
    118                             ping_output.splitlines()) or [''])[0]
    119         sent = _regex_int_from_string('([0-9]+) packets transmitted', loss_line)
    120         received = _regex_int_from_string('([0-9]+) packets received',
    121                                           loss_line)
    122         loss = _regex_float_from_string('([0-9]+\.[0-9]+)% packet loss',
    123                                         loss_line)
    124         if None in (sent, received, loss):
    125             raise error.TestFail('Failed to parse transmission statistics.')
    126         m = re.search('round-trip min\/avg\/max\/std-?dev = ([0-9.]+)\/([0-9.]+)'
    127                       '\/([0-9.]+)\/([0-9.]+) ms', ping_output)
    128         if m is not None:
    129             return PingResult(sent, received, loss,
    130                               min_latency=float(m.group(1)),
    131                               avg_latency=float(m.group(2)),
    132                               max_latency=float(m.group(3)),
    133                               dev_latency=float(m.group(4)))
    134         if received > 0:
    135             raise error.TestFail('Failed to parse latency statistics.')
    136 
    137         return PingResult(sent, received, loss)
    138 
    139 
    140 class LinuxPingDelegate(object):
    141     """Implement ping functionality specific to the linux platform."""
    142     @staticmethod
    143     def ping_arguments(ping_config):
    144         """
    145         @param ping_config PingConfig object describing the ping test for which
    146            arguments are needed.
    147         @return list of parameters to ping.
    148         """
    149         args = []
    150         args.append('-c %d' % ping_config.count)
    151         if ping_config.size is not None:
    152             args.append('-s %d' % ping_config.size)
    153         if ping_config.interval is not None:
    154             args.append('-i %f' % ping_config.interval)
    155         if ping_config.qos is not None:
    156             if ping_config.qos == 'be':
    157                 args.append('-Q 0x04')
    158             elif ping_config.qos == 'bk':
    159                 args.append('-Q 0x02')
    160             elif ping_config.qos == 'vi':
    161                 args.append('-Q 0x08')
    162             elif ping_config.qos == 'vo':
    163                 args.append('-Q 0x10')
    164             else:
    165                 raise error.TestFail('Unknown QoS value: %s' % ping_config.qos)
    166 
    167         # The last argument is the IP address to ping.
    168         args.append(ping_config.target_ip)
    169         return args
    170 
    171 
    172     @staticmethod
    173     def parse_from_output(ping_output):
    174         """Extract the ping results from stdout.
    175 
    176         @param ping_output string stdout from a ping command.
    177         On error, some statistics may be missing entirely from the output.
    178 
    179         An example of output with some errors is:
    180 
    181         PING 192.168.0.254 (192.168.0.254) 56(84) bytes of data.
    182         From 192.168.0.124 icmp_seq=1 Destination Host Unreachable
    183         From 192.168.0.124 icmp_seq=2 Destination Host Unreachable
    184         From 192.168.0.124 icmp_seq=3 Destination Host Unreachable
    185         64 bytes from 192.168.0.254: icmp_req=4 ttl=64 time=1171 ms
    186         [...]
    187         64 bytes from 192.168.0.254: icmp_req=10 ttl=64 time=1.95 ms
    188 
    189         --- 192.168.0.254 ping statistics ---
    190         10 packets transmitted, 7 received, +3 errors, 30% packet loss,
    191             time 9007ms
    192         rtt min/avg/max/mdev = 1.806/193.625/1171.174/403.380 ms, pipe 3
    193 
    194         A more normal run looks like:
    195 
    196         PING google.com (74.125.239.137) 56(84) bytes of data.
    197         64 bytes from 74.125.239.137: icmp_req=1 ttl=57 time=1.77 ms
    198         64 bytes from 74.125.239.137: icmp_req=2 ttl=57 time=1.78 ms
    199         [...]
    200         64 bytes from 74.125.239.137: icmp_req=5 ttl=57 time=1.79 ms
    201 
    202         --- google.com ping statistics ---
    203         5 packets transmitted, 5 received, 0% packet loss, time 4007ms
    204         rtt min/avg/max/mdev = 1.740/1.771/1.799/0.042 ms
    205 
    206         We also sometimes see result lines like:
    207         9 packets transmitted, 9 received, +1 duplicates, 0% packet loss,
    208             time 90 ms
    209 
    210         """
    211         loss_line = (filter(lambda x: x.find('packets transmitted') > 0,
    212                             ping_output.splitlines()) or [''])[0]
    213         sent = _regex_int_from_string('([0-9]+) packets transmitted', loss_line)
    214         received = _regex_int_from_string('([0-9]+) received', loss_line)
    215         loss = _regex_int_from_string('([0-9]+)% packet loss', loss_line)
    216         if None in (sent, received, loss):
    217             raise error.TestFail('Failed to parse transmission statistics.')
    218 
    219         m = re.search('(round-trip|rtt) min[^=]*= '
    220                       '([0-9.]+)/([0-9.]+)/([0-9.]+)/([0-9.]+)', ping_output)
    221         if m is not None:
    222             return PingResult(sent, received, loss,
    223                               min_latency=float(m.group(2)),
    224                               avg_latency=float(m.group(3)),
    225                               max_latency=float(m.group(4)),
    226                               dev_latency=float(m.group(5)))
    227         if received > 0:
    228             raise error.TestFail('Failed to parse latency statistics.')
    229 
    230         return PingResult(sent, received, loss)
    231 
    232 
    233 class PingConfig(object):
    234     """Describes the parameters for a ping command."""
    235 
    236     DEFAULT_COUNT = 10
    237     PACKET_WAIT_MARGIN_SECONDS = 120
    238 
    239     def __init__(self, target_ip, count=DEFAULT_COUNT, size=None,
    240                  interval=None, qos=None,
    241                  ignore_status=False, ignore_result=False):
    242         super(PingConfig, self).__init__()
    243         self.target_ip = target_ip
    244         self.count = count
    245         self.size = size
    246         self.interval = interval
    247         if qos:
    248             qos = qos.lower()
    249         self.qos = qos
    250         self.ignore_status = ignore_status
    251         self.ignore_result = ignore_result
    252         interval_seconds = self.interval or 1
    253         command_time = math.ceil(interval_seconds * self.count)
    254         self.command_timeout_seconds = int(command_time +
    255                                            self.PACKET_WAIT_MARGIN_SECONDS)
    256 
    257 
    258 class PingResult(object):
    259     """Represents a parsed ping command result."""
    260     def __init__(self, sent, received, loss,
    261                  min_latency=-1.0, avg_latency=-1.0,
    262                  max_latency=-1.0, dev_latency=-1.0):
    263         """Construct a PingResult.
    264 
    265         @param sent: int number of packets sent.
    266         @param received: int number of replies received.
    267         @param loss: int loss as a percentage (0-100)
    268         @param min_latency: float min response latency in ms.
    269         @param avg_latency: float average response latency in ms.
    270         @param max_latency: float max response latency in ms.
    271         @param dev_latency: float response latency deviation in ms.
    272 
    273         """
    274         super(PingResult, self).__init__()
    275         self.sent = sent
    276         self.received = received
    277         self.loss = loss
    278         self.min_latency = min_latency
    279         self.avg_latency = avg_latency
    280         self.max_latency = max_latency
    281         self.dev_latency = dev_latency
    282 
    283 
    284     def __repr__(self):
    285         return '%s(%s)' % (self.__class__.__name__,
    286                            ', '.join(['%s=%r' % item
    287                                       for item in vars(self).iteritems()]))
    288 
    289 
    290 class PingRunner(object):
    291     """Delegate to run the ping command on a local or remote host."""
    292     DEFAULT_PING_COMMAND = 'ping'
    293     PING_LOSS_THRESHOLD = 20  # A percentage.
    294 
    295 
    296     def __init__(self, command_ping=DEFAULT_PING_COMMAND, host=None,
    297                  platform=PLATFORM_LINUX):
    298         """Construct a PingRunner.
    299 
    300         @param command_ping optional path or alias of the ping command.
    301         @param host optional host object when a remote host is desired.
    302 
    303         """
    304         super(PingRunner, self).__init__()
    305         self._run = utils.run
    306         if host is not None:
    307             self._run = host.run
    308         self.command_ping = command_ping
    309         self._platform_delegate = _get_platform_delegate(platform)
    310 
    311 
    312     def simple_ping(self, host_name):
    313         """Quickly test that a hostname or IPv4 address responds to ping.
    314 
    315         @param host_name: string name or IPv4 address.
    316         @return True if host_name responds to at least one ping.
    317 
    318         """
    319         ping_config = PingConfig(host_name, count=3, interval=0.5,
    320                                  ignore_status=True, ignore_result=True)
    321         ping_result = self.ping(ping_config)
    322         if ping_result is None or ping_result.received == 0:
    323             return False
    324         return True
    325 
    326 
    327     def ping(self, ping_config):
    328         """Run ping with the given |ping_config|.
    329 
    330         Will assert that the ping had reasonable levels of loss unless
    331         requested not to in |ping_config|.
    332 
    333         @param ping_config PingConfig object describing the ping to run.
    334 
    335         """
    336         command_pieces = ([self.command_ping] +
    337                           self._platform_delegate.ping_arguments(ping_config))
    338         command = ' '.join(command_pieces)
    339         command_result = self._run(command,
    340                                    timeout=ping_config.command_timeout_seconds,
    341                                    ignore_status=True,
    342                                    ignore_timeout=True)
    343         if not command_result:
    344             if ping_config.ignore_status:
    345                 logging.warning('Ping command timed out; cannot parse output.')
    346                 return PingResult(ping_config.count, 0, 100)
    347 
    348             raise error.TestFail('Ping command timed out unexpectedly.')
    349 
    350         if not command_result.stdout:
    351             logging.warning('Ping command returned no output; stderr was %s.',
    352                             command_result.stderr)
    353             if ping_config.ignore_result:
    354                 return PingResult(ping_config.count, 0, 100)
    355             raise error.TestFail('Ping command failed to yield any output')
    356 
    357         if command_result.exit_status and not ping_config.ignore_status:
    358             raise error.TestFail('Ping command failed with code=%d' %
    359                                  command_result.exit_status)
    360 
    361         ping_result = self._platform_delegate.parse_from_output(
    362                 command_result.stdout)
    363         if ping_config.ignore_result:
    364             return ping_result
    365 
    366         if ping_result.loss > self.PING_LOSS_THRESHOLD:
    367             raise error.TestFail('Lost ping packets: %r.' % ping_result)
    368 
    369         logging.info('Ping successful.')
    370         return ping_result
    371