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 logging 6import math 7import re 8 9from autotest_lib.client.bin import utils 10from autotest_lib.client.common_lib import error 11 12 13class PingConfig(object): 14 """Describes the parameters for a ping command.""" 15 16 DEFAULT_COUNT = 10 17 PACKET_WAIT_MARGIN_SECONDS = 120 18 19 @property 20 def ping_args(self): 21 """@return list of parameters to ping.""" 22 args = [] 23 args.append('-c %d' % self.count) 24 if self.size is not None: 25 args.append('-s %d' % self.size) 26 if self.interval is not None: 27 args.append('-i %f' % self.interval) 28 if self.qos is not None: 29 if self.qos == 'be': 30 args.append('-Q 0x04') 31 elif self.qos == 'bk': 32 args.append('-Q 0x02') 33 elif self.qos == 'vi': 34 args.append('-Q 0x08') 35 elif self.qos == 'vo': 36 args.append('-Q 0x10') 37 else: 38 raise error.TestFail('Unknown QoS value: %s' % self.qos) 39 40 # The last argument is the IP addres to ping. 41 args.append(self.target_ip) 42 return args 43 44 45 def __init__(self, target_ip, count=DEFAULT_COUNT, size=None, 46 interval=None, qos=None, 47 ignore_status=False, ignore_result=False): 48 super(PingConfig, self).__init__() 49 self.target_ip = target_ip 50 self.count = count 51 self.size = size 52 self.interval = interval 53 if qos: 54 qos = qos.lower() 55 self.qos = qos 56 self.ignore_status = ignore_status 57 self.ignore_result = ignore_result 58 interval_seconds = self.interval or 1 59 command_time = math.ceil(interval_seconds * self.count) 60 self.command_timeout_seconds = int(command_time + 61 self.PACKET_WAIT_MARGIN_SECONDS) 62 63 64class PingResult(object): 65 """Represents a parsed ping command result. 66 67 On error, some statistics may be missing entirely from the output. 68 69 An example of output with some errors is: 70 71 PING 192.168.0.254 (192.168.0.254) 56(84) bytes of data. 72 From 192.168.0.124 icmp_seq=1 Destination Host Unreachable 73 From 192.168.0.124 icmp_seq=2 Destination Host Unreachable 74 From 192.168.0.124 icmp_seq=3 Destination Host Unreachable 75 64 bytes from 192.168.0.254: icmp_req=4 ttl=64 time=1171 ms 76 [...] 77 64 bytes from 192.168.0.254: icmp_req=10 ttl=64 time=1.95 ms 78 79 --- 192.168.0.254 ping statistics --- 80 10 packets transmitted, 7 received, +3 errors, 30% packet loss, time 9007ms 81 rtt min/avg/max/mdev = 1.806/193.625/1171.174/403.380 ms, pipe 3 82 83 A more normal run looks like: 84 85 PING google.com (74.125.239.137) 56(84) bytes of data. 86 64 bytes from 74.125.239.137: icmp_req=1 ttl=57 time=1.77 ms 87 64 bytes from 74.125.239.137: icmp_req=2 ttl=57 time=1.78 ms 88 [...] 89 64 bytes from 74.125.239.137: icmp_req=5 ttl=57 time=1.79 ms 90 91 --- google.com ping statistics --- 92 5 packets transmitted, 5 received, 0% packet loss, time 4007ms 93 rtt min/avg/max/mdev = 1.740/1.771/1.799/0.042 ms 94 95 We also sometimes see result lines like: 96 9 packets transmitted, 9 received, +1 duplicates, 0% packet loss, time 90 ms 97 98 """ 99 100 @staticmethod 101 def _regex_int_from_string(regex, value): 102 m = re.search(regex, value) 103 if m is None: 104 return None 105 106 return int(m.group(1)) 107 108 109 @staticmethod 110 def parse_from_output(ping_output): 111 """Construct a PingResult from ping command output. 112 113 @param ping_output string stdout from a ping command. 114 115 """ 116 loss_line = (filter(lambda x: x.find('packets transmitted') > 0, 117 ping_output.splitlines()) or [''])[0] 118 sent = PingResult._regex_int_from_string('([0-9]+) packets transmitted', 119 loss_line) 120 received = PingResult._regex_int_from_string('([0-9]+) received', 121 loss_line) 122 loss = PingResult._regex_int_from_string('([0-9]+)% packet loss', 123 loss_line) 124 if None in (sent, received, loss): 125 raise error.TestFail('Failed to parse transmission statistics.') 126 127 m = re.search('(round-trip|rtt) min[^=]*= ' 128 '([0-9.]+)/([0-9.]+)/([0-9.]+)/([0-9.]+)', ping_output) 129 if m is not None: 130 return PingResult(sent, received, loss, 131 min_latency=float(m.group(2)), 132 avg_latency=float(m.group(3)), 133 max_latency=float(m.group(4)), 134 dev_latency=float(m.group(5))) 135 if received > 0: 136 raise error.TestFail('Failed to parse latency statistics.') 137 138 return PingResult(sent, received, loss) 139 140 141 def __init__(self, sent, received, loss, 142 min_latency=-1.0, avg_latency=-1.0, 143 max_latency=-1.0, dev_latency=-1.0): 144 """Construct a PingResult. 145 146 @param sent: int number of packets sent. 147 @param received: int number of replies received. 148 @param loss: int loss as a percentage (0-100) 149 @param min_latency: float min response latency in ms. 150 @param avg_latency: float average response latency in ms. 151 @param max_latency: float max response latency in ms. 152 @param dev_latency: float response latency deviation in ms. 153 154 """ 155 super(PingResult, self).__init__() 156 self.sent = sent 157 self.received = received 158 self.loss = loss 159 self.min_latency = min_latency 160 self.avg_latency = avg_latency 161 self.max_latency = max_latency 162 self.dev_latency = dev_latency 163 164 165 def __repr__(self): 166 return '%s(%s)' % (self.__class__.__name__, 167 ', '.join(['%s=%r' % item 168 for item in vars(self).iteritems()])) 169 170 171class PingRunner(object): 172 """Delegate to run the ping command on a local or remote host.""" 173 DEFAULT_PING_COMMAND = 'ping' 174 PING_LOSS_THRESHOLD = 20 # A percentage. 175 176 177 def __init__(self, command_ping=DEFAULT_PING_COMMAND, host=None): 178 """Construct a PingRunner. 179 180 @param command_ping optional path or alias of the ping command. 181 @param host optional host object when a remote host is desired. 182 183 """ 184 super(PingRunner, self).__init__() 185 self._run = utils.run 186 if host is not None: 187 self._run = host.run 188 self.command_ping = command_ping 189 190 191 def simple_ping(self, host_name): 192 """Quickly test that a hostname or IPv4 address responds to ping. 193 194 @param host_name: string name or IPv4 address. 195 @return True iff host_name responds to at least one ping. 196 197 """ 198 ping_config = PingConfig(host_name, count=3, 199 interval=0.5, ignore_result=True, 200 ignore_status=True) 201 ping_result = self.ping(ping_config) 202 if ping_result is None or ping_result.received == 0: 203 return False 204 return True 205 206 207 def ping(self, ping_config): 208 """Run ping with the given |ping_config|. 209 210 Will assert that the ping had reasonable levels of loss unless 211 requested not to in |ping_config|. 212 213 @param ping_config PingConfig object describing the ping to run. 214 215 """ 216 command_pieces = [self.command_ping] + ping_config.ping_args 217 command = ' '.join(command_pieces) 218 command_result = self._run(command, 219 timeout=ping_config.command_timeout_seconds, 220 ignore_status=True, 221 ignore_timeout=True) 222 if not command_result: 223 if ping_config.ignore_status: 224 logging.warning('Ping command timed out; cannot parse output.') 225 return PingResult(ping_config.count, 0, 100) 226 227 raise error.TestFail('Ping command timed out unexpectedly.') 228 229 if not command_result.stdout: 230 logging.warning('Ping command returned no output; stderr was %s.', 231 command_result.stderr) 232 if ping_config.ignore_result: 233 return PingResult(ping_config.count, 0, 100) 234 raise error.TestFail('Ping command failed to yield any output') 235 236 if command_result.exit_status and not ping_config.ignore_status: 237 raise error.TestFail('Ping command failed with code=%d' % 238 command_result.exit_status) 239 240 ping_result = PingResult.parse_from_output(command_result.stdout) 241 if ping_config.ignore_result: 242 return ping_result 243 244 if ping_result.loss > self.PING_LOSS_THRESHOLD: 245 raise error.TestFail('Lost ping packets: %r.' % ping_result) 246 247 logging.info('Ping successful.') 248 return ping_result 249