# 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))