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 
     13 class PingConfig(object):
     14     """Describes the parameters for a ping command."""
     15 
     16     DEFAULT_COUNT = 10
     17     PACKET_WAIT_MARGIN_SECONDS = 120
     18 
     19     @property
     20     def ping_args(self):
     21         """@return list of parameters to ping."""
     22         args = []
     23         args.append('-c %d' % self.count)
     24         if self.size is not None:
     25             args.append('-s %d' % self.size)
     26         if self.interval is not None:
     27             args.append('-i %f' % self.interval)
     28         if self.qos is not None:
     29             if self.qos == 'be':
     30                 args.append('-Q 0x04')
     31             elif self.qos == 'bk':
     32                 args.append('-Q 0x02')
     33             elif self.qos == 'vi':
     34                 args.append('-Q 0x08')
     35             elif self.qos == 'vo':
     36                 args.append('-Q 0x10')
     37             else:
     38                 raise error.TestFail('Unknown QoS value: %s' % self.qos)
     39 
     40         # The last argument is the IP addres to ping.
     41         args.append(self.target_ip)
     42         return args
     43 
     44 
     45     def __init__(self, target_ip, count=DEFAULT_COUNT, size=None,
     46                  interval=None, qos=None,
     47                  ignore_status=False, ignore_result=False):
     48         super(PingConfig, self).__init__()
     49         self.target_ip = target_ip
     50         self.count = count
     51         self.size = size
     52         self.interval = interval
     53         if qos:
     54             qos = qos.lower()
     55         self.qos = qos
     56         self.ignore_status = ignore_status
     57         self.ignore_result = ignore_result
     58         interval_seconds = self.interval or 1
     59         command_time = math.ceil(interval_seconds * self.count)
     60         self.command_timeout_seconds = int(command_time +
     61                                            self.PACKET_WAIT_MARGIN_SECONDS)
     62 
     63 
     64 class PingResult(object):
     65     """Represents a parsed ping command result.
     66 
     67     On error, some statistics may be missing entirely from the output.
     68 
     69     An example of output with some errors is:
     70 
     71     PING 192.168.0.254 (192.168.0.254) 56(84) bytes of data.
     72     From 192.168.0.124 icmp_seq=1 Destination Host Unreachable
     73     From 192.168.0.124 icmp_seq=2 Destination Host Unreachable
     74     From 192.168.0.124 icmp_seq=3 Destination Host Unreachable
     75     64 bytes from 192.168.0.254: icmp_req=4 ttl=64 time=1171 ms
     76     [...]
     77     64 bytes from 192.168.0.254: icmp_req=10 ttl=64 time=1.95 ms
     78 
     79     --- 192.168.0.254 ping statistics ---
     80     10 packets transmitted, 7 received, +3 errors, 30% packet loss, time 9007ms
     81     rtt min/avg/max/mdev = 1.806/193.625/1171.174/403.380 ms, pipe 3
     82 
     83     A more normal run looks like:
     84 
     85     PING google.com (74.125.239.137) 56(84) bytes of data.
     86     64 bytes from 74.125.239.137: icmp_req=1 ttl=57 time=1.77 ms
     87     64 bytes from 74.125.239.137: icmp_req=2 ttl=57 time=1.78 ms
     88     [...]
     89     64 bytes from 74.125.239.137: icmp_req=5 ttl=57 time=1.79 ms
     90 
     91     --- google.com ping statistics ---
     92     5 packets transmitted, 5 received, 0% packet loss, time 4007ms
     93     rtt min/avg/max/mdev = 1.740/1.771/1.799/0.042 ms
     94 
     95     We also sometimes see result lines like:
     96     9 packets transmitted, 9 received, +1 duplicates, 0% packet loss, time 90 ms
     97 
     98     """
     99 
    100     @staticmethod
    101     def _regex_int_from_string(regex, value):
    102         m = re.search(regex, value)
    103         if m is None:
    104             return None
    105 
    106         return int(m.group(1))
    107 
    108 
    109     @staticmethod
    110     def parse_from_output(ping_output):
    111         """Construct a PingResult from ping command output.
    112 
    113         @param ping_output string stdout from a ping command.
    114 
    115         """
    116         loss_line = (filter(lambda x: x.find('packets transmitted') > 0,
    117                             ping_output.splitlines()) or [''])[0]
    118         sent = PingResult._regex_int_from_string('([0-9]+) packets transmitted',
    119                                                  loss_line)
    120         received = PingResult._regex_int_from_string('([0-9]+) received',
    121                                                      loss_line)
    122         loss = PingResult._regex_int_from_string('([0-9]+)% packet loss',
    123                                                  loss_line)
    124         if None in (sent, received, loss):
    125             raise error.TestFail('Failed to parse transmission statistics.')
    126 
    127         m = re.search('(round-trip|rtt) min[^=]*= '
    128                       '([0-9.]+)/([0-9.]+)/([0-9.]+)/([0-9.]+)', ping_output)
    129         if m is not None:
    130             return PingResult(sent, received, loss,
    131                               min_latency=float(m.group(2)),
    132                               avg_latency=float(m.group(3)),
    133                               max_latency=float(m.group(4)),
    134                               dev_latency=float(m.group(5)))
    135         if received > 0:
    136             raise error.TestFail('Failed to parse latency statistics.')
    137 
    138         return PingResult(sent, received, loss)
    139 
    140 
    141     def __init__(self, sent, received, loss,
    142                  min_latency=-1.0, avg_latency=-1.0,
    143                  max_latency=-1.0, dev_latency=-1.0):
    144         """Construct a PingResult.
    145 
    146         @param sent: int number of packets sent.
    147         @param received: int number of replies received.
    148         @param loss: int loss as a percentage (0-100)
    149         @param min_latency: float min response latency in ms.
    150         @param avg_latency: float average response latency in ms.
    151         @param max_latency: float max response latency in ms.
    152         @param dev_latency: float response latency deviation in ms.
    153 
    154         """
    155         super(PingResult, self).__init__()
    156         self.sent = sent
    157         self.received = received
    158         self.loss = loss
    159         self.min_latency = min_latency
    160         self.avg_latency = avg_latency
    161         self.max_latency = max_latency
    162         self.dev_latency = dev_latency
    163 
    164 
    165     def __repr__(self):
    166         return '%s(%s)' % (self.__class__.__name__,
    167                            ', '.join(['%s=%r' % item
    168                                       for item in vars(self).iteritems()]))
    169 
    170 
    171 class PingRunner(object):
    172     """Delegate to run the ping command on a local or remote host."""
    173     DEFAULT_PING_COMMAND = 'ping'
    174     PING_LOSS_THRESHOLD = 20  # A percentage.
    175 
    176 
    177     def __init__(self, command_ping=DEFAULT_PING_COMMAND, host=None):
    178         """Construct a PingRunner.
    179 
    180         @param command_ping optional path or alias of the ping command.
    181         @param host optional host object when a remote host is desired.
    182 
    183         """
    184         super(PingRunner, self).__init__()
    185         self._run = utils.run
    186         if host is not None:
    187             self._run = host.run
    188         self.command_ping = command_ping
    189 
    190 
    191     def simple_ping(self, host_name):
    192         """Quickly test that a hostname or IPv4 address responds to ping.
    193 
    194         @param host_name: string name or IPv4 address.
    195         @return True iff host_name responds to at least one ping.
    196 
    197         """
    198         ping_config = PingConfig(host_name, count=3,
    199                                  interval=0.5, ignore_result=True,
    200                                  ignore_status=True)
    201         ping_result = self.ping(ping_config)
    202         if ping_result is None or ping_result.received == 0:
    203             return False
    204         return True
    205 
    206 
    207     def ping(self, ping_config):
    208         """Run ping with the given |ping_config|.
    209 
    210         Will assert that the ping had reasonable levels of loss unless
    211         requested not to in |ping_config|.
    212 
    213         @param ping_config PingConfig object describing the ping to run.
    214 
    215         """
    216         command_pieces = [self.command_ping] + ping_config.ping_args
    217         command = ' '.join(command_pieces)
    218         command_result = self._run(command,
    219                                    timeout=ping_config.command_timeout_seconds,
    220                                    ignore_status=True,
    221                                    ignore_timeout=True)
    222         if not command_result:
    223             if ping_config.ignore_status:
    224                 logging.warning('Ping command timed out; cannot parse output.')
    225                 return PingResult(ping_config.count, 0, 100)
    226 
    227             raise error.TestFail('Ping command timed out unexpectedly.')
    228 
    229         if not command_result.stdout:
    230             logging.warning('Ping command returned no output; stderr was %s.',
    231                             command_result.stderr)
    232             if ping_config.ignore_result:
    233                 return PingResult(ping_config.count, 0, 100)
    234             raise error.TestFail('Ping command failed to yield any output')
    235 
    236         if command_result.exit_status and not ping_config.ignore_status:
    237             raise error.TestFail('Ping command failed with code=%d' %
    238                                  command_result.exit_status)
    239 
    240         ping_result = PingResult.parse_from_output(command_result.stdout)
    241         if ping_config.ignore_result:
    242             return ping_result
    243 
    244         if ping_result.loss > self.PING_LOSS_THRESHOLD:
    245             raise error.TestFail('Lost ping packets: %r.' % ping_result)
    246 
    247         logging.info('Ping successful.')
    248         return ping_result
    249