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