| # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import logging |
| import math |
| import re |
| |
| from autotest_lib.client.bin import utils |
| from autotest_lib.client.common_lib import error |
| |
| |
| class PingConfig(object): |
| """Describes the parameters for a ping command.""" |
| |
| DEFAULT_COUNT = 10 |
| PACKET_WAIT_MARGIN_SECONDS = 10 |
| |
| @property |
| def ping_args(self): |
| """@return list of parameters to ping.""" |
| args = [] |
| args.append('-c %d' % self.count) |
| if self.size is not None: |
| args.append('-s %d' % self.size) |
| if self.interval is not None: |
| args.append('-i %f' % self.interval) |
| if self.qos is not None: |
| if self.qos == 'be': |
| args.append('-Q 0x04') |
| elif self.qos == 'bk': |
| args.append('-Q 0x02') |
| elif self.qos == 'vi': |
| args.append('-Q 0x08') |
| elif self.qos == 'vo': |
| args.append('-Q 0x10') |
| else: |
| raise error.TestFail('Unknown QoS value: %s' % self.qos) |
| |
| # The last argument is the IP addres to ping. |
| args.append(self.target_ip) |
| return args |
| |
| |
| def __init__(self, target_ip, count=DEFAULT_COUNT, size=None, |
| interval=None, qos=None, |
| ignore_status=False, ignore_result=False): |
| super(PingConfig, self).__init__() |
| self.target_ip = target_ip |
| self.count = count |
| self.size = size |
| self.interval = interval |
| if qos: |
| qos = qos.lower() |
| self.qos = qos |
| self.ignore_status = ignore_status |
| self.ignore_result = ignore_result |
| interval_seconds = self.interval or 1 |
| command_time = math.ceil(interval_seconds * self.count) |
| self.command_timeout_seconds = int(command_time + |
| self.PACKET_WAIT_MARGIN_SECONDS) |
| |
| |
| class PingResult(object): |
| """Represents a parsed ping command result. |
| |
| On error, some statistics may be missing entirely from the output. |
| |
| An example of output with some errors is: |
| |
| PING 192.168.0.254 (192.168.0.254) 56(84) bytes of data. |
| From 192.168.0.124 icmp_seq=1 Destination Host Unreachable |
| From 192.168.0.124 icmp_seq=2 Destination Host Unreachable |
| From 192.168.0.124 icmp_seq=3 Destination Host Unreachable |
| 64 bytes from 192.168.0.254: icmp_req=4 ttl=64 time=1171 ms |
| [...] |
| 64 bytes from 192.168.0.254: icmp_req=10 ttl=64 time=1.95 ms |
| |
| --- 192.168.0.254 ping statistics --- |
| 10 packets transmitted, 7 received, +3 errors, 30% packet loss, time 9007ms |
| rtt min/avg/max/mdev = 1.806/193.625/1171.174/403.380 ms, pipe 3 |
| |
| A more normal run looks like: |
| |
| PING google.com (74.125.239.137) 56(84) bytes of data. |
| 64 bytes from 74.125.239.137: icmp_req=1 ttl=57 time=1.77 ms |
| 64 bytes from 74.125.239.137: icmp_req=2 ttl=57 time=1.78 ms |
| [...] |
| 64 bytes from 74.125.239.137: icmp_req=5 ttl=57 time=1.79 ms |
| |
| --- google.com ping statistics --- |
| 5 packets transmitted, 5 received, 0% packet loss, time 4007ms |
| rtt min/avg/max/mdev = 1.740/1.771/1.799/0.042 ms |
| |
| We also sometimes see result lines like: |
| 9 packets transmitted, 9 received, +1 duplicates, 0% packet loss, time 90 ms |
| |
| """ |
| |
| @staticmethod |
| def _regex_int_from_string(regex, value): |
| m = re.search(regex, value) |
| if m is None: |
| return None |
| |
| return int(m.group(1)) |
| |
| |
| @staticmethod |
| def parse_from_output(ping_output): |
| """Construct a PingResult from ping command output. |
| |
| @param ping_output string stdout from a ping command. |
| |
| """ |
| loss_line = (filter(lambda x: x.find('packets transmitted') > 0, |
| ping_output.splitlines()) or [''])[0] |
| sent = PingResult._regex_int_from_string('([0-9]+) packets transmitted', |
| loss_line) |
| received = PingResult._regex_int_from_string('([0-9]+) received', |
| loss_line) |
| loss = PingResult._regex_int_from_string('([0-9]+)% packet loss', |
| loss_line) |
| if None in (sent, received, loss): |
| raise error.TestFail('Failed to parse transmission statistics.') |
| |
| m = re.search('(round-trip|rtt) min[^=]*= ' |
| '([0-9.]+)/([0-9.]+)/([0-9.]+)/([0-9.]+)', ping_output) |
| if m is not None: |
| return PingResult(sent, received, loss, |
| min_latency=float(m.group(2)), |
| avg_latency=float(m.group(3)), |
| max_latency=float(m.group(4)), |
| dev_latency=float(m.group(5))) |
| if received > 0: |
| raise error.TestFail('Failed to parse latency statistics.') |
| |
| return PingResult(sent, received, loss) |
| |
| |
| def __init__(self, sent, received, loss, |
| min_latency=-1.0, avg_latency=-1.0, |
| max_latency=-1.0, dev_latency=-1.0): |
| """Construct a PingResult. |
| |
| @param sent: int number of packets sent. |
| @param received: int number of replies received. |
| @param loss: int loss as a percentage (0-100) |
| @param min_latency: float min response latency in ms. |
| @param avg_latency: float average response latency in ms. |
| @param max_latency: float max response latency in ms. |
| @param dev_latency: float response latency deviation in ms. |
| |
| """ |
| super(PingResult, self).__init__() |
| self.sent = sent |
| self.received = received |
| self.loss = loss |
| self.min_latency = min_latency |
| self.avg_latency = avg_latency |
| self.max_latency = max_latency |
| self.dev_latency = dev_latency |
| |
| |
| def __repr__(self): |
| return '%s(%s)' % (self.__class__.__name__, |
| ', '.join(['%s=%r' % item |
| for item in vars(self).iteritems()])) |
| |
| |
| class PingRunner(object): |
| """Delegate to run the ping command on a local or remote host.""" |
| DEFAULT_PING_COMMAND = 'ping' |
| PING_LOSS_THRESHOLD = 20 # A percentage. |
| |
| |
| def __init__(self, command_ping=DEFAULT_PING_COMMAND, host=None): |
| """Construct a PingRunner. |
| |
| @param command_ping optional path or alias of the ping command. |
| @param host optional host object when a remote host is desired. |
| |
| """ |
| super(PingRunner, self).__init__() |
| self._run = utils.run |
| if host is not None: |
| self._run = host.run |
| self.command_ping = command_ping |
| |
| |
| def ping(self, ping_config): |
| """Run ping with the given |ping_config|. |
| |
| Will assert that the ping had reasonable levels of loss unless |
| requested not to in |ping_config|. |
| |
| @param ping_config PingConfig object describing the ping to run. |
| |
| """ |
| command_pieces = [self.command_ping] + ping_config.ping_args |
| command = ' '.join(command_pieces) |
| command_result = self._run(command, |
| timeout=ping_config.command_timeout_seconds, |
| ignore_status=True, |
| ignore_timeout=True) |
| if not command_result: |
| if ping_config.ignore_status: |
| logging.warning('Ping command timed out; cannot parse output.') |
| return PingResult(ping_config.count, 0, 100) |
| |
| raise error.TestFail('Ping command timed out unexpectedly.') |
| |
| if not command_result.stdout: |
| logging.warning('Ping command returned no output; stderr was %s.', |
| command_result.stderr) |
| if ping_config.ignore_result: |
| return PingResult(ping_config.count, 0, 100) |
| raise error.TestFail('Ping command failed to yield any output') |
| |
| if command_result.exit_status and not ping_config.ignore_status: |
| raise error.TestFail('Ping command failed with code=%d' % |
| command_result.exit_status) |
| |
| ping_result = PingResult.parse_from_output(command_result.stdout) |
| if ping_config.ignore_result: |
| return ping_result |
| |
| if ping_result.loss > self.PING_LOSS_THRESHOLD: |
| raise error.TestFail('Lost ping packets: %r.' % ping_result) |
| |
| logging.info('Ping successful.') |
| return ping_result |