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