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