• 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
19from autotest_lib.client.cros import flimflam_test_path
20
21
22class EthernetDongle(object):
23    """ Used for definining the desired module expect states. """
24
25    def __init__(self, expect_speed='100', expect_duplex='full'):
26        # Expected values for parameters.
27        self.expected_parameters = {
28            'ifconfig_status': 0,
29            'duplex': expect_duplex,
30            'speed': expect_speed,
31            'mac_address': None,
32            'ipaddress': None,
33        }
34
35    def GetParam(self, parameter):
36        return self.expected_parameters[parameter]
37
38class network_EthernetStressPlug(test.test):
39    version = 1
40
41    def initialize(self, 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