• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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