Home | History | Annotate | Download | only in network_EthernetStressPlug
      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 
      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, 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