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