1# Lint as: python2, python3 2# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import re 7 8from autotest_lib.client.common_lib import error 9from autotest_lib.client.common_lib.cros import path_utils 10 11 12class ArpingRunner(object): 13 """Delegate to run arping on a remote host.""" 14 15 DEFAULT_COUNT = 10 16 SSH_TIMEOUT_MARGIN = 120 17 18 19 def __init__(self, host, ping_interface): 20 self._host = host 21 self._arping_command = path_utils.must_be_installed( 22 '/usr/bin/arping', host=host) 23 self._ping_interface = ping_interface 24 25 26 def arping(self, target_ip, count=None, timeout_seconds=None): 27 """Run arping on a remote host. 28 29 @param target_ip: string IP address to use as the ARP target. 30 @param count: int number of ARP packets to send. The command 31 will take roughly |count| seconds to complete, since arping 32 sends a packet out once a second. 33 @param timeout_seconds: int number of seconds to wait for arping 34 to complete. Override the default of one second per packet. 35 Note that this doesn't change packet spacing. 36 37 """ 38 if count is None: 39 count = self.DEFAULT_COUNT 40 if timeout_seconds is None: 41 timeout_seconds = count 42 command_pieces = [self._arping_command] 43 command_pieces.append('-b') # Default to only sending broadcast ARPs. 44 command_pieces.append('-w %d' % timeout_seconds) 45 command_pieces.append('-c %d' % count) 46 command_pieces.append('-I %s %s' % (self._ping_interface, target_ip)) 47 result = self._host.run( 48 ' '.join(command_pieces), 49 timeout=timeout_seconds + self.SSH_TIMEOUT_MARGIN, 50 ignore_status=True) 51 return ArpingResult(result.stdout) 52 53 54class ArpingResult(object): 55 """Can parse raw arping output and present a summary.""" 56 57 DEFAULT_LOSS_THRESHOLD = 30.0 58 59 60 def __init__(self, stdout): 61 """Construct an ArpingResult from the stdout of arping. 62 63 A successful run looks something like this: 64 65 ARPING 192.168.2.193 from 192.168.2.254 eth0 66 Unicast reply from 192.168.2.193 [14:7D:C5:E1:53:83] 2.842ms 67 Unicast reply from 192.168.2.193 [14:7D:C5:E1:53:83] 5.851ms 68 Unicast reply from 192.168.2.193 [14:7D:C5:E1:53:83] 2.565ms 69 Unicast reply from 192.168.2.193 [14:7D:C5:E1:53:83] 2.595ms 70 Unicast reply from 192.168.2.193 [14:7D:C5:E1:53:83] 2.534ms 71 Unicast reply from 192.168.2.193 [14:7D:C5:E1:53:83] 3.217ms 72 Unicast request from 192.168.2.193 [14:7D:C5:E1:53:83] 748.657ms 73 Sent 6 probes (6 broadcast(s)) 74 Received 7 response(s) (1 request(s)) 75 76 @param stdout string raw stdout of arping command. 77 78 """ 79 latencies = [] 80 responders = set() 81 num_sent = None 82 regex = re.compile(r'(([0-9]{1,3}\.){3}[0-9]{1,3}) ' 83 r'\[(([0-9A-Fa-f]{1,2}:){5}[0-9A-Fa-f]{1,2})\] +' 84 r'([0-9\.]+)ms') 85 requests = 0 86 for line in stdout.splitlines(): 87 if line.find('Unicast reply from') == 0: 88 match = re.search(regex, line.strip()) 89 if match is None: 90 raise error.TestError('arping result parsing code failed ' 91 'to anticipate line: ' % line) 92 93 responder_ip = match.group(1) # Maybe useful in the future? 94 responder_mac = match.group(3) 95 latency = float(match.group(5)) 96 latencies.append(latency) 97 responders.add(responder_mac) 98 if line.find('Unicast request from') == 0: 99 # We don't care about these really, but they mess up our 100 # primitive line counting. 101 requests += 1 102 elif line.find('Sent ') == 0: 103 num_sent = int(line.split()[1]) 104 elif line.find('Received ') == 0: 105 count = int(line.split()[1]) 106 if count != len(latencies) + requests: 107 raise error.TestFail('Failed to parse accurate latencies ' 108 'from stdout: %r. Got %d, ' 109 'wanted %d.' % (stdout, len(latencies), 110 count)) 111 if num_sent is None: 112 raise error.TestFail('Failed to parse number of arpings sent ' 113 'from %r' % stdout) 114 115 if num_sent < 1: 116 raise error.TestFail('No arpings sent.') 117 118 self.loss = 100.0 * float(num_sent - len(latencies)) / num_sent 119 self.average_latency = 0.0 120 if latencies: 121 self.average_latency = sum(latencies) / len(latencies) 122 self.latencies = latencies 123 self.responders = responders 124 125 126 def was_successful(self, max_average_latency=None, valid_responders=None, 127 max_loss=DEFAULT_LOSS_THRESHOLD): 128 """Checks if the arping was some definition of successful. 129 130 @param max_average_latency float maximum value for average latency in 131 milliseconds. 132 @param valid_responders iterable object of responder MAC addresses. 133 We'll check that we got only responses from valid responders. 134 @param max_loss float maximum loss expressed as a percentage. 135 @return True iff all criterion set to not None values hold. 136 137 """ 138 if (max_average_latency is not None and 139 self.average_latency > max_average_latency): 140 return False 141 142 if (valid_responders is not None and 143 self.responders.difference(valid_responders)): 144 return False 145 146 if max_loss is not None and self.loss > max_loss: 147 return False 148 149 return True 150 151 152 def __repr__(self): 153 return ('%s(loss=%r, average_latency=%r, latencies=%r, responders=%r)' % 154 (self.__class__.__name__, self.loss, self.average_latency, 155 self.latencies, self.responders)) 156