• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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