普通文本  |  155行  |  6.07 KB

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