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 6from __future__ import absolute_import 7from __future__ import division 8from __future__ import print_function 9 10import logging 11import math 12import re 13import six 14 15from autotest_lib.client.bin import utils 16from autotest_lib.client.common_lib import error 17 18PLATFORM_LINUX = 'LINUX' 19PLATFORM_MACOS = 'MAC_OS' 20 21 22def _get_platform_delegate(platform): 23 if platform == PLATFORM_LINUX: 24 return LinuxPingDelegate 25 elif platform == PLATFORM_MACOS: 26 return MacPingDelegate 27 else: 28 raise error.TestError('%s is not a valid platform type', platform) 29 30 31def _regex_int_from_string(pattern, line): 32 """Retrieve an integer from a string, using regex. 33 34 @param pattern: The regular expression to apply to the input string. 35 @param line: String input to retrieve an integer from. 36 @return integer retrieved from the input string, or None if there is no 37 match. 38 """ 39 m = re.search(pattern, line) 40 if m is not None: 41 return int(m.group(1)) 42 return None 43 44 45def _regex_float_from_string(pattern, line): 46 """Retrieve a float from a string, using regex. 47 48 @param pattern: The regular expression to apply to the input string. 49 @param line: String input to retrieve a float from. 50 @return float retrieved from the input string, or None if there is no 51 match. 52 """ 53 m = re.search(pattern, line) 54 if m is not None: 55 return float(m.group(1)) 56 return None 57 58 59def _extract_ping_loss(output): 60 for line in output.splitlines(): 61 if line.find('packets transmitted') > 0: 62 return line 63 return '' 64 65 66class MacPingDelegate(object): 67 """Implement ping functionality for MacOS hosts.""" 68 69 @staticmethod 70 def ping_arguments(ping_config): 71 """ 72 @param ping_config PingConfig object describing the ping test for which 73 arguments are needed. 74 @return list of parameters to ping. 75 """ 76 args = [] 77 args.append('-c %d' % ping_config.count) 78 if ping_config.size is not None: 79 args.append('-s %d' % ping_config.size) 80 if ping_config.interval is not None: 81 args.append('-i %f' % ping_config.interval) 82 if ping_config.source_iface is not None: 83 args.append('-b %s' % ping_config.source_iface) 84 if ping_config.qos is not None: 85 if ping_config.qos == 'be': 86 ping_config.append('-k 0') 87 elif ping_config.qos == 'bk': 88 ping_config.append('-k 1') 89 elif ping_config.qos == 'vi': 90 args.append('-k 2') 91 elif ping_config.qos == 'vo': 92 args.append('-k 3') 93 else: 94 raise error.TestFail('Unknown QoS value: %s' % ping_config.qos) 95 96 # The last argument is the IP address to ping. 97 args.append(ping_config.target_ip) 98 return args 99 100 101 @staticmethod 102 def parse_from_output(ping_output): 103 """Extract the ping results from stdout. 104 105 @param ping_output string stdout from a ping/ping6 command. 106 107 stdout from ping command looks like: 108 109 PING 8.8.8.8 (8.8.8.8): 56 data bytes 110 64 bytes from 8.8.8.8: icmp_seq=0 ttl=57 time=3.770 ms 111 64 bytes from 8.8.8.8: icmp_seq=1 ttl=57 time=4.165 ms 112 64 bytes from 8.8.8.8: icmp_seq=2 ttl=57 time=4.901 ms 113 114 --- 8.8.8.8 ping statistics --- 115 3 packets transmitted, 3 packets received, 0.0% packet loss 116 round-trip min/avg/max/stddev = 3.770/4.279/4.901/0.469 ms 117 118 stdout from ping6 command looks like: 119 120 16 bytes from fdd2:8741:1993:8::, icmp_seq=16 hlim=64 time=1.783 ms 121 16 bytes from fdd2:8741:1993:8::, icmp_seq=17 hlim=64 time=2.150 ms 122 16 bytes from fdd2:8741:1993:8::, icmp_seq=18 hlim=64 time=2.516 ms 123 16 bytes from fdd2:8741:1993:8::, icmp_seq=19 hlim=64 time=1.401 ms 124 125 --- fdd2:8741:1993:8:: ping6 statistics --- 126 20 packets transmitted, 20 packets received, 0.0% packet loss 127 round-trip min/avg/max/std-dev = 1.401/2.122/3.012/0.431 ms 128 129 This function will look for both 'stdev' and 'std-dev' in test results 130 to support both ping and ping6 commands. 131 """ 132 loss_line = _extract_ping_loss(ping_output) 133 sent = _regex_int_from_string('([0-9]+) packets transmitted', loss_line) 134 received = _regex_int_from_string('([0-9]+) packets received', 135 loss_line) 136 loss = _regex_float_from_string('([0-9]+\.[0-9]+)% packet loss', 137 loss_line) 138 if None in (sent, received, loss): 139 raise error.TestFail('Failed to parse transmission statistics.') 140 m = re.search('round-trip min\/avg\/max\/std-?dev = ([0-9.]+)\/([0-9.]+)' 141 '\/([0-9.]+)\/([0-9.]+) ms', ping_output) 142 if m is not None: 143 return PingResult(sent, received, loss, 144 min_latency=float(m.group(1)), 145 avg_latency=float(m.group(2)), 146 max_latency=float(m.group(3)), 147 dev_latency=float(m.group(4))) 148 if received > 0: 149 raise error.TestFail('Failed to parse latency statistics.') 150 151 return PingResult(sent, received, loss) 152 153 154class LinuxPingDelegate(object): 155 """Implement ping functionality specific to the linux platform.""" 156 @staticmethod 157 def ping_arguments(ping_config): 158 """ 159 @param ping_config PingConfig object describing the ping test for which 160 arguments are needed. 161 @return list of parameters to ping. 162 """ 163 args = [] 164 args.append('-c %d' % ping_config.count) 165 if ping_config.size is not None: 166 args.append('-s %d' % ping_config.size) 167 if ping_config.interval is not None: 168 args.append('-i %f' % ping_config.interval) 169 if ping_config.source_iface is not None: 170 args.append('-I %s' % ping_config.source_iface) 171 if ping_config.qos is not None: 172 if ping_config.qos == 'be': 173 args.append('-Q 0x04') 174 elif ping_config.qos == 'bk': 175 args.append('-Q 0x02') 176 elif ping_config.qos == 'vi': 177 args.append('-Q 0x08') 178 elif ping_config.qos == 'vo': 179 args.append('-Q 0x10') 180 else: 181 raise error.TestFail('Unknown QoS value: %s' % ping_config.qos) 182 183 # The last argument is the IP address to ping. 184 args.append(ping_config.target_ip) 185 return args 186 187 188 @staticmethod 189 def parse_from_output(ping_output): 190 """Extract the ping results from stdout. 191 192 @param ping_output string stdout from a ping command. 193 On error, some statistics may be missing entirely from the output. 194 195 An example of output with some errors is: 196 197 PING 192.168.0.254 (192.168.0.254) 56(84) bytes of data. 198 From 192.168.0.124 icmp_seq=1 Destination Host Unreachable 199 From 192.168.0.124 icmp_seq=2 Destination Host Unreachable 200 From 192.168.0.124 icmp_seq=3 Destination Host Unreachable 201 64 bytes from 192.168.0.254: icmp_req=4 ttl=64 time=1171 ms 202 [...] 203 64 bytes from 192.168.0.254: icmp_req=10 ttl=64 time=1.95 ms 204 205 --- 192.168.0.254 ping statistics --- 206 10 packets transmitted, 7 received, +3 errors, 30% packet loss, 207 time 9007ms 208 rtt min/avg/max/mdev = 1.806/193.625/1171.174/403.380 ms, pipe 3 209 210 A more normal run looks like: 211 212 PING google.com (74.125.239.137) 56(84) bytes of data. 213 64 bytes from 74.125.239.137: icmp_req=1 ttl=57 time=1.77 ms 214 64 bytes from 74.125.239.137: icmp_req=2 ttl=57 time=1.78 ms 215 [...] 216 64 bytes from 74.125.239.137: icmp_req=5 ttl=57 time=1.79 ms 217 218 --- google.com ping statistics --- 219 5 packets transmitted, 5 received, 0% packet loss, time 4007ms 220 rtt min/avg/max/mdev = 1.740/1.771/1.799/0.042 ms 221 222 We also sometimes see result lines like: 223 9 packets transmitted, 9 received, +1 duplicates, 0% packet loss, 224 time 90 ms 225 226 """ 227 loss_line = _extract_ping_loss(ping_output) 228 sent = _regex_int_from_string('([0-9]+) packets transmitted', loss_line) 229 received = _regex_int_from_string('([0-9]+) received', loss_line) 230 loss = _regex_float_from_string('([0-9]+(\.[0-9]+)?)% packet loss', 231 loss_line) 232 if None in (sent, received, loss): 233 raise error.TestFail('Failed to parse transmission statistics.') 234 235 m = re.search('(round-trip|rtt) min[^=]*= ' 236 '([0-9.]+)/([0-9.]+)/([0-9.]+)/([0-9.]+)', ping_output) 237 if m is not None: 238 return PingResult(sent, received, loss, 239 min_latency=float(m.group(2)), 240 avg_latency=float(m.group(3)), 241 max_latency=float(m.group(4)), 242 dev_latency=float(m.group(5))) 243 if received > 0: 244 raise error.TestFail('Failed to parse latency statistics.') 245 246 return PingResult(sent, received, loss) 247 248 249class PingConfig(object): 250 """Describes the parameters for a ping command.""" 251 252 DEFAULT_COUNT = 10 253 PACKET_WAIT_MARGIN_SECONDS = 120 254 255 def __init__(self, target_ip, count=DEFAULT_COUNT, size=None, 256 interval=None, qos=None, source_iface=None, 257 ignore_status=False, ignore_result=False): 258 super(PingConfig, self).__init__() 259 self.target_ip = target_ip 260 self.count = count 261 self.size = size 262 self.interval = interval 263 if qos: 264 qos = qos.lower() 265 self.qos = qos 266 self.source_iface = source_iface 267 self.ignore_status = ignore_status 268 self.ignore_result = ignore_result 269 interval_seconds = self.interval or 1 270 command_time = math.ceil(interval_seconds * self.count) 271 self.command_timeout_seconds = int(command_time + 272 self.PACKET_WAIT_MARGIN_SECONDS) 273 274 275class PingResult(object): 276 """Represents a parsed ping command result.""" 277 def __init__(self, sent, received, loss, 278 min_latency=-1.0, avg_latency=-1.0, 279 max_latency=-1.0, dev_latency=-1.0): 280 """Construct a PingResult. 281 282 @param sent: int number of packets sent. 283 @param received: int number of replies received. 284 @param loss: int loss as a percentage (0-100) 285 @param min_latency: float min response latency in ms. 286 @param avg_latency: float average response latency in ms. 287 @param max_latency: float max response latency in ms. 288 @param dev_latency: float response latency deviation in ms. 289 290 """ 291 super(PingResult, self).__init__() 292 self.sent = sent 293 self.received = received 294 self.loss = loss 295 self.min_latency = min_latency 296 self.avg_latency = avg_latency 297 self.max_latency = max_latency 298 self.dev_latency = dev_latency 299 300 301 def __repr__(self): 302 return '%s(%s)' % (self.__class__.__name__, ', '.join( 303 ['%s=%r' % item for item in six.iteritems(vars(self))])) 304 305 306class PingRunner(object): 307 """Delegate to run the ping command on a local or remote host.""" 308 DEFAULT_PING_COMMAND = 'ping' 309 PING_LOSS_THRESHOLD = 20 # A percentage. 310 311 312 def __init__(self, command_ping=DEFAULT_PING_COMMAND, host=None, 313 platform=PLATFORM_LINUX): 314 """Construct a PingRunner. 315 316 @param command_ping optional path or alias of the ping command. 317 @param host optional host object when a remote host is desired. 318 319 """ 320 super(PingRunner, self).__init__() 321 self._run = utils.run 322 if host is not None: 323 self._run = host.run 324 self.command_ping = command_ping 325 self._platform_delegate = _get_platform_delegate(platform) 326 327 328 def simple_ping(self, host_name): 329 """Quickly test that a hostname or IPv4 address responds to ping. 330 331 @param host_name: string name or IPv4 address. 332 @return True if host_name responds to at least one ping. 333 334 """ 335 ping_config = PingConfig(host_name, count=3, interval=0.5, 336 ignore_status=True, ignore_result=True) 337 ping_result = self.ping(ping_config) 338 if ping_result is None or ping_result.received == 0: 339 return False 340 return True 341 342 343 def ping(self, ping_config): 344 """Run ping with the given |ping_config|. 345 346 Will assert that the ping had reasonable levels of loss unless 347 requested not to in |ping_config|. 348 349 @param ping_config PingConfig object describing the ping to run. 350 351 """ 352 command_pieces = ([self.command_ping] + 353 self._platform_delegate.ping_arguments(ping_config)) 354 command = ' '.join(command_pieces) 355 command_result = self._run(command, 356 timeout=ping_config.command_timeout_seconds, 357 ignore_status=True, 358 ignore_timeout=True) 359 if not command_result: 360 if ping_config.ignore_status: 361 logging.warning('Ping command timed out; cannot parse output.') 362 return PingResult(ping_config.count, 0, 100) 363 364 raise error.TestFail('Ping command timed out unexpectedly.') 365 366 if not command_result.stdout: 367 logging.warning('Ping command returned no output; stderr was %s.', 368 command_result.stderr) 369 if ping_config.ignore_result: 370 return PingResult(ping_config.count, 0, 100) 371 raise error.TestFail('Ping command failed to yield any output') 372 373 if command_result.exit_status and not ping_config.ignore_status: 374 raise error.TestFail('Ping command failed with code=%d' % 375 command_result.exit_status) 376 377 ping_result = self._platform_delegate.parse_from_output( 378 command_result.stdout) 379 if ping_config.ignore_result: 380 return ping_result 381 382 if ping_result.loss > self.PING_LOSS_THRESHOLD: 383 raise error.TestFail('Lost ping packets: %r.' % ping_result) 384 385 logging.info('Ping successful.') 386 return ping_result 387