1# Copyright (c) 2016 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 fcntl 6import logging 7import os 8import pyudev 9import random 10import re 11import socket 12import struct 13import subprocess 14import sys 15import time 16 17from autotest_lib.client.bin import test, utils 18from autotest_lib.client.common_lib import error 19from autotest_lib.client.cros import flimflam_test_path 20 21 22class EthernetDongle(object): 23 """ Used for definining the desired module expect states. """ 24 25 def __init__(self, expect_speed='100', expect_duplex='full'): 26 # Expected values for parameters. 27 self.expected_parameters = { 28 'ifconfig_status': 0, 29 'duplex': expect_duplex, 30 'speed': expect_speed, 31 'mac_address': None, 32 'ipaddress': None, 33 } 34 35 def GetParam(self, parameter): 36 return self.expected_parameters[parameter] 37 38class network_EthernetStressPlug(test.test): 39 version = 1 40 41 def initialize(self, interface=None): 42 """ Determines and defines the bus information and interface info. """ 43 44 self.link_speed_failures = 0 45 sysnet = os.path.join('/', 'sys', 'class', 'net') 46 47 def get_ethernet_interface(interface): 48 """ Valid interface requires link and duplex status.""" 49 avail_eth_interfaces=[] 50 if interface is None: 51 # This is not the (bridged) eth dev we are looking for. 52 for x in os.listdir(sysnet): 53 sysdev = os.path.join(sysnet, x, 'device') 54 syswireless = os.path.join(sysnet, x, 'wireless') 55 if os.path.exists(sysdev) and not os.path.exists(syswireless): 56 avail_eth_interfaces.append(x) 57 else: 58 sysdev = os.path.join(sysnet, interface, 'device') 59 if os.path.exists(sysdev): 60 avail_eth_interfaces.append(interface) 61 else: 62 raise error.TestError('Network Interface %s is not a device ' % iface) 63 64 link_status = 'unknown' 65 duplex_status = 'unknown' 66 iface = 'unknown' 67 68 for iface in avail_eth_interfaces: 69 syslink = os.path.join(sysnet, iface, 'operstate') 70 try: 71 link_file = open(syslink) 72 link_status = link_file.readline().strip() 73 link_file.close() 74 except: 75 pass 76 77 sysduplex = os.path.join(sysnet, iface, 'duplex') 78 try: 79 duplex_file = open(sysduplex) 80 duplex_status = duplex_file.readline().strip() 81 duplex_file.close() 82 except: 83 pass 84 85 if link_status == 'up' and duplex_status == 'full': 86 return iface 87 88 raise error.TestError('Network Interface %s not usable (%s, %s)' 89 % (iface, link_status, duplex_status)) 90 91 def get_net_device_path(device=''): 92 """ Uses udev to get the path of the desired internet device. 93 Args: 94 device: look for the /sys entry for this ethX device 95 Returns: 96 /sys pathname for the found ethX device or raises an error. 97 """ 98 net_list = pyudev.Context().list_devices(subsystem='net') 99 for dev in net_list: 100 if dev.sys_path.endswith('net/%s' % device): 101 return dev.sys_path 102 103 raise error.TestError('Could not find /sys device path for %s' 104 % device) 105 106 self.interface = get_ethernet_interface(interface) 107 self.eth_syspath = get_net_device_path(self.interface) 108 self.eth_flagspath = os.path.join(self.eth_syspath, 'flags') 109 110 # USB Dongles: "authorized" file will disable the USB port and 111 # in some cases powers off the port. In either case, net/eth* goes 112 # away. And thus "../../.." won't be valid to access "authorized". 113 # Build the pathname that goes directly to authpath. 114 auth_path = os.path.join(self.eth_syspath, '../../../authorized') 115 if os.path.exists(auth_path): 116 # now rebuild the path w/o use of '..' 117 auth_path = os.path.split(self.eth_syspath)[0] 118 auth_path = os.path.split(auth_path)[0] 119 auth_path = os.path.split(auth_path)[0] 120 121 self.eth_authpath = os.path.join(auth_path,'authorized') 122 else: 123 self.eth_authpath = None 124 125 # Stores the status of the most recently run iteration. 126 self.test_status = { 127 'ipaddress': None, 128 'eth_state': None, 129 'reason': None, 130 'last_wait': 0 131 } 132 133 self.secs_before_warning = 10 134 135 # Represents the current number of instances in which ethernet 136 # took longer than dhcp_warning_level to come up. 137 self.warning_count = 0 138 139 # The percentage of test warnings before we fail the test. 140 self.warning_threshold = .25 141 142 def GetIPAddress(self): 143 """ Obtains the ipaddress of the interface. """ 144 try: 145 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 146 return socket.inet_ntoa(fcntl.ioctl( 147 s.fileno(), 0x8915, # SIOCGIFADDR 148 struct.pack('256s', self.interface[:15]))[20:24]) 149 except: 150 return None 151 152 def GetEthernetStatus(self): 153 """ 154 Updates self.test_status with the status of the ethernet interface. 155 156 Returns: 157 True if the ethernet device is up. False otherwise. 158 """ 159 160 def ReadEthVal(param): 161 """ Reads the network parameters of the interface. """ 162 eth_path = os.path.join('/', 'sys', 'class', 'net', self.interface, 163 param) 164 val = None 165 try: 166 fp = open(eth_path) 167 val = fp.readline().strip() 168 fp.close() 169 except: 170 pass 171 return val 172 173 eth_out = self.ParseEthTool() 174 ethernet_status = { 175 'ifconfig_status': utils.system('ifconfig %s' % self.interface, 176 ignore_status=True), 177 'duplex': eth_out.get('Duplex'), 178 'speed': eth_out.get('Speed'), 179 'mac_address': ReadEthVal('address'), 180 'ipaddress': self.GetIPAddress() 181 } 182 183 self.test_status['ipaddress'] = ethernet_status['ipaddress'] 184 185 for param, val in ethernet_status.iteritems(): 186 if self.dongle.GetParam(param) is None: 187 # For parameters with expected values none, we check the 188 # existence of a value. 189 if not bool(val): 190 self.test_status['eth_state'] = False 191 self.test_status['reason'] = '%s is not ready: %s == %s' \ 192 % (self.interface, param, val) 193 return False 194 else: 195 if val != self.dongle.GetParam(param): 196 self.test_status['eth_state'] = False 197 self.test_status['reason'] = '%s is not ready. (%s)\n' \ 198 " Expected: '%s'\n" \ 199 " Received: '%s'" \ 200 % (self.interface, param, 201 self.dongle.GetParam(param), 202 val) 203 return False 204 205 self.test_status['eth_state'] = True 206 self.test_status['reason'] = None 207 return True 208 209 def _PowerEthernet(self, power=1): 210 """ Sends command to change the power state of ethernet. 211 Args: 212 power: 0 to unplug, 1 to plug. 213 """ 214 215 if self.eth_authpath: 216 try: 217 fp = open(self.eth_authpath, 'w') 218 fp.write('%d' % power) 219 fp.close() 220 except: 221 raise error.TestError('Could not write %d to %s' % 222 (power, self.eth_authpath)) 223 224 # Linux can set network link state by frobbing "flags" bitfields. 225 # Bit fields are documented in include/uapi/linux/if.h. 226 # Bit 0 is IFF_UP (link up=1 or down=0). 227 elif os.path.exists(self.eth_flagspath): 228 try: 229 fp = open(self.eth_flagspath, mode='r') 230 val= int(fp.readline().strip(), 16) 231 fp.close() 232 except: 233 raise error.TestError('Could not read %s' % self.eth_flagspath) 234 235 if power: 236 newval = val | 1 237 else: 238 newval = val & ~1 239 240 if val != newval: 241 try: 242 fp = open(self.eth_flagspath, mode='w') 243 fp.write('0x%x' % newval) 244 fp.close() 245 except: 246 raise error.TestError('Could not write 0x%x to %s' % 247 (newval, self.eth_flagspath)) 248 logging.debug("eth flags: 0x%x to 0x%x" % (val, newval)) 249 250 # else use ifconfig eth0 up/down to switch 251 else: 252 logging.warning('plug/unplug event control not found. ' 253 'Use ifconfig %s %s instead' % 254 (self.interface, 'up' if power else 'down')) 255 result = subprocess.check_call(['ifconfig', self.interface, 256 'up' if power else 'down']) 257 if result: 258 raise error.TestError('Fail to change the power state of %s' % 259 self.interface) 260 261 def TestPowerEthernet(self, power=1, timeout=45): 262 """ Tests enabling or disabling the ethernet. 263 Args: 264 power: 0 to unplug, 1 to plug. 265 timeout: Indicates approximately the number of seconds to timeout 266 how long we should check for the success of the ethernet 267 state change. 268 269 Returns: 270 The time in seconds required for device to transfer to the desired 271 state. 272 273 Raises: 274 error.TestFail if the ethernet status is not in the desired state. 275 """ 276 277 start_time = time.time() 278 end_time = start_time + timeout 279 280 power_str = ['off', 'on'] 281 self._PowerEthernet(power) 282 283 while time.time() < end_time: 284 status = self.GetEthernetStatus() 285 286 287 # If GetEthernetStatus() detects the wrong link rate, "bouncing" 288 # the link _should_ recover. Keep count of how many times this 289 # happens. Test should fail if happens "frequently". 290 if power and not status and 'speed' in self.test_status['reason']: 291 self._PowerEthernet(0) 292 time.sleep(1) 293 self._PowerEthernet(power) 294 self.link_speed_failures += 1 295 logging.warning('Link Renegotiated ' + 296 self.test_status['reason']) 297 298 # If ethernet is enabled and has an IP, OR 299 # if ethernet is disabled and does not have an IP, 300 # then we are in the desired state. 301 # Return the number of "seconds" for this to happen. 302 # (translated to an approximation of the number of seconds) 303 if (power and status and \ 304 self.test_status['ipaddress'] is not None) \ 305 or \ 306 (not power and not status and \ 307 self.test_status['ipaddress'] is None): 308 return time.time()-start_time 309 310 time.sleep(1) 311 312 logging.debug(self.test_status['reason']) 313 raise error.TestFail('ERROR: TIMEOUT : %s IP is %s after setting ' 314 'power %s (last_wait = %.2f seconds)' % 315 (self.interface, self.test_status['ipaddress'], 316 power_str[power], self.test_status['last_wait'])) 317 318 def RandSleep(self, min_sleep, max_sleep): 319 """ Sleeps for a random duration. 320 321 Args: 322 min_sleep: Minimum sleep parameter in miliseconds. 323 max_sleep: Maximum sleep parameter in miliseconds. 324 """ 325 duration = random.randint(min_sleep, max_sleep)/1000.0 326 self.test_status['last_wait'] = duration 327 time.sleep(duration) 328 329 def _ParseEthTool_LinkModes(self, line): 330 """ Parses Ethtool Link Mode Entries. 331 Inputs: 332 line: Space separated string of link modes that have the format 333 (\d+)baseT/(Half|Full) (eg. 100baseT/Full). 334 335 Outputs: 336 List of dictionaries where each dictionary has the format 337 { 'Speed': '<speed>', 'Duplex': '<duplex>' } 338 """ 339 parameters = [] 340 341 # QCA ESS EDMA driver doesn't report "Supported link modes:" 342 if 'Not reported' in line: 343 return parameters 344 345 for speed_to_parse in line.split(): 346 speed_duplex = speed_to_parse.split('/') 347 parameters.append( 348 { 349 'Speed': re.search('(\d*)', speed_duplex[0]).groups()[0], 350 'Duplex': speed_duplex[1], 351 } 352 ) 353 return parameters 354 355 def ParseEthTool(self): 356 """ 357 Parses the output of Ethtools into a dictionary and returns 358 the dictionary with some cleanup in the below areas: 359 Speed: Remove the unit of speed. 360 Supported link modes: Construct a list of dictionaries. 361 The list is ordered (relying on ethtool) 362 and each of the dictionaries contains a Speed 363 kvp and a Duplex kvp. 364 Advertised link modes: Same as 'Supported link modes'. 365 366 Sample Ethtool Output: 367 Supported ports: [ TP MII ] 368 Supported link modes: 10baseT/Half 10baseT/Full 369 100baseT/Half 100baseT/Full 370 1000baseT/Half 1000baseT/Full 371 Supports auto-negotiation: Yes 372 Advertised link modes: 10baseT/Half 10baseT/Full 373 100baseT/Half 100baseT/Full 374 1000baseT/Full 375 Advertised auto-negotiation: Yes 376 Speed: 1000Mb/s 377 Duplex: Full 378 Port: MII 379 PHYAD: 2 380 Transceiver: internal 381 Auto-negotiation: on 382 Supports Wake-on: pg 383 Wake-on: d 384 Current message level: 0x00000007 (7) 385 Link detected: yes 386 387 Returns: 388 A dictionary representation of the above ethtool output, or an empty 389 dictionary if no ethernet dongle is present. 390 Eg. 391 { 392 'Supported ports': '[ TP MII ]', 393 'Supported link modes': [{'Speed': '10', 'Duplex': 'Half'}, 394 {...}, 395 {'Speed': '1000', 'Duplex': 'Full'}], 396 'Supports auto-negotiation: 'Yes', 397 'Advertised link modes': [{'Speed': '10', 'Duplex': 'Half'}, 398 {...}, 399 {'Speed': '1000', 'Duplex': 'Full'}], 400 'Advertised auto-negotiation': 'Yes' 401 'Speed': '1000', 402 'Duplex': 'Full', 403 'Port': 'MII', 404 'PHYAD': '2', 405 'Transceiver': 'internal', 406 'Auto-negotiation': 'on', 407 'Supports Wake-on': 'pg', 408 'Wake-on': 'd', 409 'Current message level': '0x00000007 (7)', 410 'Link detected': 'yes', 411 } 412 """ 413 parameters = {} 414 ethtool_out = os.popen('ethtool %s' % self.interface).read().split('\n') 415 if 'No data available' in ethtool_out: 416 return parameters 417 418 # bridged interfaces only have two lines of ethtool output. 419 if len(ethtool_out) < 3: 420 return parameters 421 422 # For multiline entries, keep track of the key they belong to. 423 current_key = '' 424 for line in ethtool_out: 425 current_line = line.strip().partition(':') 426 if current_line[1] == ':': 427 current_key = current_line[0] 428 429 # Assumes speed does not span more than one line. 430 # Also assigns empty string if speed field 431 # is not available. 432 if current_key == 'Speed': 433 speed = re.search('^\s*(\d*)', current_line[2]) 434 parameters[current_key] = '' 435 if speed: 436 parameters[current_key] = speed.groups()[0] 437 elif (current_key == 'Supported link modes' or 438 current_key == 'Advertised link modes'): 439 parameters[current_key] = [] 440 parameters[current_key] += \ 441 self._ParseEthTool_LinkModes(current_line[2]) 442 else: 443 parameters[current_key] = current_line[2].strip() 444 else: 445 if (current_key == 'Supported link modes' or 446 current_key == 'Advertised link modes'): 447 parameters[current_key] += \ 448 self._ParseEthTool_LinkModes(current_line[0]) 449 else: 450 parameters[current_key]+=current_line[0].strip() 451 452 return parameters 453 454 def GetDongle(self): 455 """ Returns the ethernet dongle object associated with what's connected. 456 457 Dongle uniqueness is retrieved from the 'product' file that is 458 associated with each usb dongle in 459 /sys/devices/pci.*/0000.*/usb.*/.*-.*/product. The correct 460 dongle object is determined and returned. 461 462 Returns: 463 Object of type EthernetDongle. 464 465 Raises: 466 error.TestFail if ethernet dongle is not found. 467 """ 468 ethtool_dict = self.ParseEthTool() 469 470 if not ethtool_dict: 471 raise error.TestFail('Unable to parse ethtool output for %s.' % 472 self.interface) 473 474 # Ethtool output is ordered in terms of speed so this obtains the 475 # fastest speed supported by dongle. 476 # QCA ESS EDMA driver doesn't report "Supported link modes". 477 max_link = ethtool_dict['Advertised link modes'][-1] 478 479 return EthernetDongle(expect_speed=max_link['Speed'], 480 expect_duplex=max_link['Duplex']) 481 482 def run_once(self, num_iterations=1): 483 try: 484 self.dongle = self.GetDongle() 485 486 #Sleep for a random duration between .5 and 2 seconds 487 #for unplug and plug scenarios. 488 for i in range(num_iterations): 489 logging.debug('Iteration: %d start' % i) 490 linkdown_time = self.TestPowerEthernet(power=0) 491 linkdown_wait = self.test_status['last_wait'] 492 if linkdown_time > self.secs_before_warning: 493 self.warning_count+=1 494 495 self.RandSleep(500, 2000) 496 497 linkup_time = self.TestPowerEthernet(power=1) 498 linkup_wait = self.test_status['last_wait'] 499 500 if linkup_time > self.secs_before_warning: 501 self.warning_count+=1 502 503 self.RandSleep(500, 2000) 504 logging.debug('Iteration: %d end (down:%f/%d up:%f/%d)' % 505 (i, linkdown_wait, linkdown_time, 506 linkup_wait, linkup_time)) 507 508 if self.warning_count > num_iterations * self.warning_threshold: 509 raise error.TestFail('ERROR: %.2f%% of total runs (%d) ' 510 'took longer than %d seconds for ' 511 'ethernet to come up.' % 512 (self.warning_threshold*100, 513 num_iterations, 514 self.secs_before_warning)) 515 516 # Link speed failures are secondary. 517 # Report after all iterations complete. 518 if self.link_speed_failures > 1: 519 raise error.TestFail('ERROR: %s : Link Renegotiated %d times' 520 % (self.interface, self.link_speed_failures)) 521 522 except Exception as e: 523 exc_info = sys.exc_info() 524 self._PowerEthernet(1) 525 raise exc_info[0], exc_info[1], exc_info[2] 526