blob: 7303a5cbbd904a55a493b090c0abc4a9684afba5 [file] [log] [blame]
# 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
PLATFORM_LINUX = 'LINUX'
PLATFORM_MACOS = 'MAC_OS'
def _get_platform_delegate(platform):
if platform == PLATFORM_LINUX:
return LinuxPingDelegate
elif platform == PLATFORM_MACOS:
return MacPingDelegate
else:
raise error.TestError('%s is not a valid platform type', platform)
def _regex_int_from_string(pattern, line):
"""Retrieve an integer from a string, using regex.
@param pattern: The regular expression to apply to the input string.
@param line: String input to retrieve an integer from.
@return integer retrieved from the input string, or None if there is no
match.
"""
m = re.search(pattern, line)
if m is not None:
return int(m.group(1))
return None
def _regex_float_from_string(pattern, line):
"""Retrieve a float from a string, using regex.
@param pattern: The regular expression to apply to the input string.
@param line: String input to retrieve a float from.
@return float retrieved from the input string, or None if there is no
match.
"""
m = re.search(pattern, line)
if m is not None:
return float(m.group(1))
return None
class MacPingDelegate(object):
"""Implement ping functionality for MacOS hosts."""
@staticmethod
def ping_arguments(ping_config):
"""
@param ping_config PingConfig object describing the ping test for which
arguments are needed.
@return list of parameters to ping.
"""
args = []
args.append('-c %d' % ping_config.count)
if ping_config.size is not None:
args.append('-s %d' % ping_config.size)
if ping_config.interval is not None:
args.append('-i %f' % ping_config.interval)
if ping_config.source_iface is not None:
args.append('-b %s' % ping_config.source_iface)
if ping_config.qos is not None:
if ping_config.qos == 'be':
ping_config.append('-k 0')
elif ping_config.qos == 'bk':
ping_config.append('-k 1')
elif ping_config.qos == 'vi':
args.append('-k 2')
elif ping_config.qos == 'vo':
args.append('-k 3')
else:
raise error.TestFail('Unknown QoS value: %s' % ping_config.qos)
# The last argument is the IP address to ping.
args.append(ping_config.target_ip)
return args
@staticmethod
def parse_from_output(ping_output):
"""Extract the ping results from stdout.
@param ping_output string stdout from a ping/ping6 command.
stdout from ping command looks like:
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: icmp_seq=0 ttl=57 time=3.770 ms
64 bytes from 8.8.8.8: icmp_seq=1 ttl=57 time=4.165 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=57 time=4.901 ms
--- 8.8.8.8 ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 3.770/4.279/4.901/0.469 ms
stdout from ping6 command looks like:
16 bytes from fdd2:8741:1993:8::, icmp_seq=16 hlim=64 time=1.783 ms
16 bytes from fdd2:8741:1993:8::, icmp_seq=17 hlim=64 time=2.150 ms
16 bytes from fdd2:8741:1993:8::, icmp_seq=18 hlim=64 time=2.516 ms
16 bytes from fdd2:8741:1993:8::, icmp_seq=19 hlim=64 time=1.401 ms
--- fdd2:8741:1993:8:: ping6 statistics ---
20 packets transmitted, 20 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 1.401/2.122/3.012/0.431 ms
This function will look for both 'stdev' and 'std-dev' in test results
to support both ping and ping6 commands.
"""
loss_line = (filter(lambda x: x.find('packets transmitted') > 0,
ping_output.splitlines()) or [''])[0]
sent = _regex_int_from_string('([0-9]+) packets transmitted', loss_line)
received = _regex_int_from_string('([0-9]+) packets received',
loss_line)
loss = _regex_float_from_string('([0-9]+\.[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 min\/avg\/max\/std-?dev = ([0-9.]+)\/([0-9.]+)'
'\/([0-9.]+)\/([0-9.]+) ms', ping_output)
if m is not None:
return PingResult(sent, received, loss,
min_latency=float(m.group(1)),
avg_latency=float(m.group(2)),
max_latency=float(m.group(3)),
dev_latency=float(m.group(4)))
if received > 0:
raise error.TestFail('Failed to parse latency statistics.')
return PingResult(sent, received, loss)
class LinuxPingDelegate(object):
"""Implement ping functionality specific to the linux platform."""
@staticmethod
def ping_arguments(ping_config):
"""
@param ping_config PingConfig object describing the ping test for which
arguments are needed.
@return list of parameters to ping.
"""
args = []
args.append('-c %d' % ping_config.count)
if ping_config.size is not None:
args.append('-s %d' % ping_config.size)
if ping_config.interval is not None:
args.append('-i %f' % ping_config.interval)
if ping_config.source_iface is not None:
args.append('-I %s' % ping_config.source_iface)
if ping_config.qos is not None:
if ping_config.qos == 'be':
args.append('-Q 0x04')
elif ping_config.qos == 'bk':
args.append('-Q 0x02')
elif ping_config.qos == 'vi':
args.append('-Q 0x08')
elif ping_config.qos == 'vo':
args.append('-Q 0x10')
else:
raise error.TestFail('Unknown QoS value: %s' % ping_config.qos)
# The last argument is the IP address to ping.
args.append(ping_config.target_ip)
return args
@staticmethod
def parse_from_output(ping_output):
"""Extract the ping results from stdout.
@param ping_output string stdout from a ping command.
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
"""
loss_line = (filter(lambda x: x.find('packets transmitted') > 0,
ping_output.splitlines()) or [''])[0]
sent = _regex_int_from_string('([0-9]+) packets transmitted', loss_line)
received = _regex_int_from_string('([0-9]+) received', loss_line)
loss = _regex_float_from_string('([0-9]+(\.[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)
class PingConfig(object):
"""Describes the parameters for a ping command."""
DEFAULT_COUNT = 10
PACKET_WAIT_MARGIN_SECONDS = 120
def __init__(self, target_ip, count=DEFAULT_COUNT, size=None,
interval=None, qos=None, source_iface=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.source_iface = source_iface
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."""
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,
platform=PLATFORM_LINUX):
"""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
self._platform_delegate = _get_platform_delegate(platform)
def simple_ping(self, host_name):
"""Quickly test that a hostname or IPv4 address responds to ping.
@param host_name: string name or IPv4 address.
@return True if host_name responds to at least one ping.
"""
ping_config = PingConfig(host_name, count=3, interval=0.5,
ignore_status=True, ignore_result=True)
ping_result = self.ping(ping_config)
if ping_result is None or ping_result.received == 0:
return False
return True
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] +
self._platform_delegate.ping_arguments(ping_config))
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 = self._platform_delegate.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