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 
     20 
     21 class 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 
     37 class 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