Home | History | Annotate | Download | only in network_EthernetStressPlug
      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 
      5 import fcntl
      6 import logging
      7 import os
      8 import pyudev
      9 import random
     10 import re
     11 import socket
     12 import struct
     13 import subprocess
     14 import sys
     15 import time
     16 
     17 from autotest_lib.client.bin import test, utils
     18 from autotest_lib.client.common_lib import error
     19 from autotest_lib.client.cros import flimflam_test_path
     20 
     21 
     22 class 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 
     38 class 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