# 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 re
from autotest_lib.client.common_lib import error
from autotest_lib.client.common_lib.cros import path_utils
class ArpingRunner(object):
"""Delegate to run arping on a remote host."""
DEFAULT_COUNT = 10
SSH_TIMEOUT_MARGIN = 120
def __init__(self, host, ping_interface):
self._host = host
self._arping_command = path_utils.must_be_installed(
'/usr/bin/arping', host=host)
self._ping_interface = ping_interface
def arping(self, target_ip, count=None, timeout_seconds=None):
"""Run arping on a remote host.
@param target_ip: string IP address to use as the ARP target.
@param count: int number of ARP packets to send. The command
will take roughly |count| seconds to complete, since arping
sends a packet out once a second.
@param timeout_seconds: int number of seconds to wait for arping
to complete. Override the default of one second per packet.
Note that this doesn't change packet spacing.
"""
if count is None:
count = self.DEFAULT_COUNT
if timeout_seconds is None:
timeout_seconds = count
command_pieces = [self._arping_command]
command_pieces.append('-b') # Default to only sending broadcast ARPs.
command_pieces.append('-w %d' % timeout_seconds)
command_pieces.append('-c %d' % count)
command_pieces.append('-I %s %s' % (self._ping_interface, target_ip))
result = self._host.run(
' '.join(command_pieces),
timeout=timeout_seconds + self.SSH_TIMEOUT_MARGIN,
ignore_status=True)
return ArpingResult(result.stdout)
class ArpingResult(object):
"""Can parse raw arping output and present a summary."""
DEFAULT_LOSS_THRESHOLD = 30.0
def __init__(self, stdout):
"""Construct an ArpingResult from the stdout of arping.
A successful run looks something like this:
ARPING 192.168.2.193 from 192.168.2.254 eth0
Unicast reply from 192.168.2.193 [14:7D:C5:E1:53:83] 2.842ms
Unicast reply from 192.168.2.193 [14:7D:C5:E1:53:83] 5.851ms
Unicast reply from 192.168.2.193 [14:7D:C5:E1:53:83] 2.565ms
Unicast reply from 192.168.2.193 [14:7D:C5:E1:53:83] 2.595ms
Unicast reply from 192.168.2.193 [14:7D:C5:E1:53:83] 2.534ms
Unicast reply from 192.168.2.193 [14:7D:C5:E1:53:83] 3.217ms
Unicast request from 192.168.2.193 [14:7D:C5:E1:53:83] 748.657ms
Sent 6 probes (6 broadcast(s))
Received 7 response(s) (1 request(s))
@param stdout string raw stdout of arping command.
"""
latencies = []
responders = set()
num_sent = None
regex = re.compile(r'(([0-9]{1,3}\.){3}[0-9]{1,3}) '
r'\[(([0-9A-Fa-f]{1,2}:){5}[0-9A-Fa-f]{1,2})\] +'
r'([0-9\.]+)ms')
requests = 0
for line in stdout.splitlines():
if line.find('Unicast reply from') == 0:
match = re.search(regex, line.strip())
if match is None:
raise error.TestError('arping result parsing code failed '
'to anticipate line: ' % line)
responder_ip = match.group(1) # Maybe useful in the future?
responder_mac = match.group(3)
latency = float(match.group(5))
latencies.append(latency)
responders.add(responder_mac)
if line.find('Unicast request from') == 0:
# We don't care about these really, but they mess up our
# primitive line counting.
requests += 1
elif line.find('Sent ') == 0:
num_sent = int(line.split()[1])
elif line.find('Received ') == 0:
count = int(line.split()[1])
if count != len(latencies) + requests:
raise error.TestFail('Failed to parse accurate latencies '
'from stdout: %r. Got %d, '
'wanted %d.' % (stdout, len(latencies),
count))
if num_sent is None:
raise error.TestFail('Failed to parse number of arpings sent '
'from %r' % stdout)
if num_sent < 1:
raise error.TestFail('No arpings sent.')
self.loss = 100.0 * float(num_sent - len(latencies)) / num_sent
self.average_latency = 0.0
if latencies:
self.average_latency = sum(latencies) / len(latencies)
self.latencies = latencies
self.responders = responders
def was_successful(self, max_average_latency=None, valid_responders=None,
max_loss=DEFAULT_LOSS_THRESHOLD):
"""Checks if the arping was some definition of successful.
@param max_average_latency float maximum value for average latency in
milliseconds.
@param valid_responders iterable object of responder MAC addresses.
We'll check that we got only responses from valid responders.
@param max_loss float maximum loss expressed as a percentage.
@return True iff all criterion set to not None values hold.
"""
if (max_average_latency is not None and
self.average_latency > max_average_latency):
return False
if (valid_responders is not None and
self.responders.difference(valid_responders)):
return False
if max_loss is not None and self.loss > max_loss:
return False
return True
def __repr__(self):
return ('%s(loss=%r, average_latency=%r, latencies=%r, responders=%r)' %
(self.__class__.__name__, self.loss, self.average_latency,
self.latencies, self.responders))