You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
155 lines
6.1 KiB
155 lines
6.1 KiB
# 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))
|