• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
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):
42        """ Determines and defines the bus information and interface info. """
43
44        def get_ethernet_interface():
45            """ Gets the correct interface based on link and duplex status."""
46            avail_eth_interfaces =[x for x in os.listdir("/sys/class/net/")
47                                   if x.startswith("eth")]
48
49            for interface in avail_eth_interfaces:
50                # This is not the (bridged) eth dev we are looking for.
51                if os.path.exists("/sys/class/net/" + interface + "/brport"):
52                    continue
53
54                try:
55                    link_file = open("/sys/class/net/" + interface +
56                                     "/operstate")
57                    link_status = link_file.readline().strip()
58                    link_file.close()
59                except:
60                    pass
61
62                try:
63                    duplex_file = open("/sys/class/net/" + interface +
64                                       "/duplex")
65                    duplex_status = duplex_file.readline().strip()
66                    duplex_file.close()
67                except:
68                    pass
69
70                if link_status == 'up' and duplex_status == 'full':
71                    return interface
72            return 'eth0'
73
74        def get_net_device_path(device='eth0'):
75            """ Uses udev to get the path of the desired internet device.
76            Args:
77                device: look for the /sys entry for this ethX device
78            Returns:
79                /sys pathname for the found ethX device or raises an error.
80            """
81            net_list = pyudev.Context().list_devices(subsystem='net')
82            for dev in net_list:
83                if dev.sys_path.endswith("net/%s" % device):
84                    return dev.sys_path
85
86            raise error.TestError('Could not find /sys device path for %s'
87                                  % device)
88
89        self.interface = get_ethernet_interface()
90        self.eth_syspath = get_net_device_path(self.interface)
91        self.eth_flagspath = os.path.join(self.eth_syspath, 'flags')
92
93        # USB Dongles: "authorized" file will disable the USB port and
94        # in some cases powers off the port. In either case, net/eth* goes
95        # away. And thus "../../.." won't be valid to access "authorized".
96        # Build the pathname that goes directly to authpath.
97        auth_path = os.path.join(self.eth_syspath, '../../../authorized')
98        if os.path.exists(auth_path):
99            # now rebuild the path w/o use of '..'
100            auth_path = os.path.split(self.eth_syspath)[0]
101            auth_path = os.path.split(auth_path)[0]
102            auth_path = os.path.split(auth_path)[0]
103
104            self.eth_authpath = os.path.join(auth_path,'authorized')
105        else:
106            self.eth_authpath = None
107
108        # Stores the status of the most recently run iteration.
109        self.test_status = {
110            'ipaddress': None,
111            'eth_state': None,
112            'reason': None,
113            'last_wait': 0
114        }
115
116        self.secs_before_warning = 10
117
118        # Represents the current number of instances in which ethernet
119        # took longer than dhcp_warning_level to come up.
120        self.warning_count = 0
121
122        # The percentage of test warnings before we fail the test.
123        self.warning_threshold = .25
124
125    def GetIPAddress(self):
126        """ Obtains the ipaddress of the interface. """
127        try:
128            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
129            return socket.inet_ntoa(fcntl.ioctl(
130                   s.fileno(), 0x8915,  # SIOCGIFADDR
131                   struct.pack('256s', self.interface[:15]))[20:24])
132        except:
133            return None
134
135    def GetEthernetStatus(self):
136        """
137        Updates self.test_status with the status of the ethernet interface.
138
139        Returns:
140            True if the ethernet device is up.  False otherwise.
141        """
142
143        def ReadEthVal(param):
144            """ Reads the network parameters of the interface. """
145            eth_path = os.path.join('/', 'sys', 'class', 'net', self.interface,
146                                    param)
147            val = None
148            try:
149                fp = open(eth_path)
150                val = fp.readline().strip()
151                fp.close()
152            except:
153                pass
154            return val
155
156        eth_out = self.ParseEthTool()
157        ethernet_status = {
158            'ifconfig_status': utils.system('ifconfig %s' % self.interface,
159                                            ignore_status=True),
160            'duplex': eth_out.get('Duplex'),
161            'speed': eth_out.get('Speed'),
162            'mac_address': ReadEthVal('address'),
163            'ipaddress': self.GetIPAddress()
164        }
165
166        self.test_status['ipaddress'] = ethernet_status['ipaddress']
167
168        for param, val in ethernet_status.iteritems():
169            if self.dongle.GetParam(param) is None:
170                # For parameters with expected values none, we check the
171                # existence of a value.
172                if not bool(val):
173                    self.test_status['eth_state'] = False
174                    self.test_status['reason'] = '%s is not ready: %s == %s' \
175                                                 % (self.interface, param, val)
176                    return False
177            else:
178                if val != self.dongle.GetParam(param):
179                    self.test_status['eth_state'] = False
180                    self.test_status['reason'] = '%s is not ready. (%s)\n' \
181                                                 "  Expected: '%s'\n" \
182                                                 "  Received: '%s'" \
183                                                 % (self.interface, param,
184                                                 self.dongle.GetParam(param),
185                                                 val)
186                    return False
187
188        self.test_status['eth_state'] = True
189        self.test_status['reason'] = None
190        return True
191
192    def _PowerEthernet(self, power=1):
193        """ Sends command to change the power state of ethernet.
194        Args:
195          power: 0 to unplug, 1 to plug.
196        """
197
198        if self.eth_authpath:
199            try:
200                fp = open(self.eth_authpath, 'w')
201                fp.write('%d' % power)
202                fp.close()
203            except:
204                raise error.TestError('Could not write %d to %s' %
205                                      (power, self.eth_authpath))
206
207        # Linux can set network link state by frobbing "flags" bitfields.
208        # Bit fields are documented in include/uapi/linux/if.h.
209        # Bit 0 is IFF_UP (link up=1 or down=0).
210        elif os.path.exists(self.eth_flagspath):
211            try:
212                fp = open(self.eth_flagspath, mode='r')
213                val= int(fp.readline().strip(), 16)
214                fp.close()
215            except:
216                raise error.TestError('Could not read %s' % self.eth_flagspath)
217
218            if power:
219                newval = val | 1
220            else:
221                newval = val &  ~1
222
223            if val != newval:
224                try:
225                    fp = open(self.eth_flagspath, mode='w')
226                    fp.write('0x%x' % newval)
227                    fp.close()
228                except:
229                    raise error.TestError('Could not write 0x%x to %s' %
230                                          (newval, self.eth_flagspath))
231                logging.debug("eth flags: 0x%x to 0x%x" % (val, newval))
232
233        # else use ifconfig eth0 up/down to switch
234        else:
235            logging.warning('plug/unplug event control not found. '
236                            'Use ifconfig %s %s instead' %
237                            (self.interface, 'up' if power else 'down'))
238            result = subprocess.check_call(['ifconfig', self.interface,
239                                            'up' if power else 'down'])
240            if result:
241                raise error.TestError('Fail to change the power state of %s' %
242                                      self.interface)
243
244    def TestPowerEthernet(self, power=1, timeout=45):
245        """ Tests enabling or disabling the ethernet.
246        Args:
247            power: 0 to unplug, 1 to plug.
248            timeout: Indicates approximately the number of seconds to timeout
249                     how long we should check for the success of the ethernet
250                     state change.
251
252        Returns:
253            The time in seconds required for device to transfer to the desired
254            state.
255
256        Raises:
257            error.TestFail if the ethernet status is not in the desired state.
258        """
259
260        start_time = time.time()
261        end_time = start_time + timeout
262
263        power_str = ['off', 'on']
264        self._PowerEthernet(power)
265
266        while time.time() < end_time:
267            status = self.GetEthernetStatus()
268
269            # If ethernet is enabled  and has an IP, OR
270            # if ethernet is disabled and does not have an IP,
271            # then we are in the desired state.
272            # Return the number of "seconds" for this to happen.
273            # (translated to an approximation of the number of seconds)
274            if (power and status and \
275                self.test_status['ipaddress'] is not None) \
276                or \
277                (not power and not status and \
278                self.test_status['ipaddress'] is None):
279                return time.time()-start_time
280
281            time.sleep(1)
282
283        logging.debug(self.test_status['reason'])
284        raise error.TestFail('ERROR: TIMEOUT : %s IP is %s after setting '
285                             'power %s (last_wait = %.2f seconds)' %
286                             (self.interface, self.test_status['ipaddress'],
287                             power_str[power], self.test_status['last_wait']))
288
289    def RandSleep(self, min_sleep, max_sleep):
290        """ Sleeps for a random duration.
291
292        Args:
293            min_sleep: Minimum sleep parameter in miliseconds.
294            max_sleep: Maximum sleep parameter in miliseconds.
295        """
296        duration = random.randint(min_sleep, max_sleep)/1000.0
297        self.test_status['last_wait'] = duration
298        time.sleep(duration)
299
300    def _ParseEthTool_LinkModes(self, line):
301        """ Parses Ethtool Link Mode Entries.
302
303        Inputs:
304            line: Space separated string of link modes that have the format
305                  (\d+)baseT/(Half|Full) (eg. 100baseT/Full).
306
307        Outputs:
308            List of dictionaries where each dictionary has the format
309            { 'Speed': '<speed>', 'Duplex': '<duplex>' }
310        """
311        parameters = []
312        for speed_to_parse in line.split():
313            speed_duplex = speed_to_parse.split('/')
314            parameters.append(
315                {
316                    'Speed': re.search('(\d*)', speed_duplex[0]).groups()[0],
317                    'Duplex': speed_duplex[1],
318                }
319            )
320        return parameters
321
322    def ParseEthTool(self):
323        """
324        Parses the output of Ethtools into a dictionary and returns
325        the dictionary with some cleanup in the below areas:
326            Speed: Remove the unit of speed.
327            Supported link modes: Construct a list of dictionaries.
328                                  The list is ordered (relying on ethtool)
329                                  and each of the dictionaries contains a Speed
330                                  kvp and a Duplex kvp.
331            Advertised link modes: Same as 'Supported link modes'.
332
333        Sample Ethtool Output:
334            Supported ports: [ TP MII ]
335            Supported link modes:   10baseT/Half 10baseT/Full
336                                    100baseT/Half 100baseT/Full
337                                    1000baseT/Half 1000baseT/Full
338            Supports auto-negotiation: Yes
339            Advertised link modes:  10baseT/Half 10baseT/Full
340                                    100baseT/Half 100baseT/Full
341                                    1000baseT/Full
342            Advertised auto-negotiation: Yes
343            Speed: 1000Mb/s
344            Duplex: Full
345            Port: MII
346            PHYAD: 2
347            Transceiver: internal
348            Auto-negotiation: on
349            Supports Wake-on: pg
350            Wake-on: d
351            Current message level: 0x00000007 (7)
352            Link detected: yes
353
354        Returns:
355          A dictionary representation of the above ethtool output, or an empty
356          dictionary if no ethernet dongle is present.
357          Eg.
358            {
359              'Supported ports': '[ TP MII ]',
360              'Supported link modes': [{'Speed': '10', 'Duplex': 'Half'},
361                                       {...},
362                                       {'Speed': '1000', 'Duplex': 'Full'}],
363              'Supports auto-negotiation: 'Yes',
364              'Advertised link modes': [{'Speed': '10', 'Duplex': 'Half'},
365                                        {...},
366                                        {'Speed': '1000', 'Duplex': 'Full'}],
367              'Advertised auto-negotiation': 'Yes'
368              'Speed': '1000',
369              'Duplex': 'Full',
370              'Port': 'MII',
371              'PHYAD': '2',
372              'Transceiver': 'internal',
373              'Auto-negotiation': 'on',
374              'Supports Wake-on': 'pg',
375              'Wake-on': 'd',
376              'Current message level': '0x00000007 (7)',
377              'Link detected': 'yes',
378            }
379        """
380        parameters = {}
381        ethtool_out = os.popen('ethtool %s' % self.interface).read().split('\n')
382        if 'No data available' in ethtool_out:
383            return parameters
384
385        # For multiline entries, keep track of the key they belong to.
386        current_key = ''
387        for line in ethtool_out:
388            current_line = line.strip().partition(':')
389            if current_line[1] == ':':
390                current_key = current_line[0]
391
392                # Assumes speed does not span more than one line.
393                # Also assigns empty string if speed field
394                # is not available.
395                if current_key == 'Speed':
396                    speed = re.search('^\s*(\d*)', current_line[2])
397                    parameters[current_key] = ''
398                    if speed:
399                        parameters[current_key] = speed.groups()[0]
400                elif (current_key == 'Supported link modes' or
401                      current_key == 'Advertised link modes'):
402                    parameters[current_key] = []
403                    parameters[current_key] += \
404                        self._ParseEthTool_LinkModes(current_line[2])
405                else:
406                    parameters[current_key] = current_line[2].strip()
407            else:
408              if (current_key == 'Supported link modes' or
409                  current_key == 'Advertised link modes'):
410                  parameters[current_key] += \
411                      self._ParseEthTool_LinkModes(current_line[0])
412              else:
413                  parameters[current_key]+=current_line[0].strip()
414
415        return parameters
416
417    def GetDongle(self):
418        """ Returns the ethernet dongle object associated with what's connected.
419
420        Dongle uniqueness is retrieved from the 'product' file that is
421        associated with each usb dongle in
422        /sys/devices/pci.*/0000.*/usb.*/.*-.*/product.  The correct
423        dongle object is determined and returned.
424
425        Returns:
426          Object of type EthernetDongle.
427
428        Raises:
429          error.TestFail if ethernet dongle is not found.
430        """
431        ethtool_dict = self.ParseEthTool()
432
433        if not ethtool_dict:
434            raise error.TestFail('Unable to parse ethtool output for %s.' %
435                                 self.interface)
436
437        # Ethtool output is ordered in terms of speed so this obtains the
438        # fastest speed supported by dongle.
439        max_link = ethtool_dict['Supported link modes'][-1]
440
441        return EthernetDongle(expect_speed=max_link['Speed'],
442                              expect_duplex=max_link['Duplex'])
443
444    def run_once(self, num_iterations=1):
445        try:
446            self.dongle = self.GetDongle()
447
448            #Sleep for a random duration between .5 and 2 seconds
449            #for unplug and plug scenarios.
450            for i in range(num_iterations):
451                logging.debug('Iteration: %d start' % i)
452                linkdown_time = self.TestPowerEthernet(power=0)
453                linkdown_wait = self.test_status['last_wait']
454                if linkdown_time > self.secs_before_warning:
455                    self.warning_count+=1
456
457                self.RandSleep(500, 2000)
458
459                linkup_time = self.TestPowerEthernet(power=1)
460                linkup_wait = self.test_status['last_wait']
461
462                if linkup_time > self.secs_before_warning:
463                    self.warning_count+=1
464
465                self.RandSleep(500, 2000)
466                logging.debug('Iteration: %d end (down:%f/%d up:%f/%d)' %
467                              (i, linkdown_wait, linkdown_time,
468                               linkup_wait, linkup_time))
469
470                if self.warning_count > num_iterations * self.warning_threshold:
471                    raise error.TestFail('ERROR: %.2f%% of total runs (%d) '
472                                         'took longer than %d seconds for '
473                                         'ethernet to come up.' %
474                                         (self.warning_threshold*100,
475                                          num_iterations,
476                                          self.secs_before_warning))
477
478        except Exception as e:
479            exc_info = sys.exc_info()
480            self._PowerEthernet(1)
481            raise exc_info[0], exc_info[1], exc_info[2]
482