• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (c) 2013 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 collections
6import logging
7import os
8import re
9
10from autotest_lib.client.bin import local_host
11from autotest_lib.client.bin import utils
12from autotest_lib.client.common_lib import error
13from autotest_lib.client.common_lib.cros.network import netblock
14
15# A tuple consisting of a readable part number (one of NAME_* below)
16# and a kernel module that provides the driver for this part (e.g. ath9k).
17DeviceDescription = collections.namedtuple('DeviceDescription',
18                                           ['name', 'kernel_module'])
19
20
21# A tuple describing a default route, consisting of an interface name,
22# gateway IP address, and the metric value visible in the routing table.
23DefaultRoute = collections.namedtuple('DefaultRoute', ['interface_name',
24                                                       'gateway',
25                                                       'metric'])
26
27NAME_MARVELL_88W8797_SDIO = 'Marvell 88W8797 SDIO'
28NAME_MARVELL_88W8887_SDIO = 'Marvell 88W8887 SDIO'
29NAME_MARVELL_88W8897_SDIO = 'Marvell 88W8897 SDIO'
30NAME_MARVELL_88W8897_PCIE = 'Marvell 88W8897 PCIE'
31NAME_MARVELL_88W8997_PCIE = 'Marvell 88W8997 PCIE'
32NAME_ATHEROS_AR9280 = 'Atheros AR9280'
33NAME_ATHEROS_AR9382 = 'Atheros AR9382'
34NAME_ATHEROS_AR9462 = 'Atheros AR9462'
35NAME_QUALCOMM_ATHEROS_QCA6174 = 'Qualcomm Atheros QCA6174'
36NAME_QUALCOMM_WCN3990 = 'Qualcomm WCN3990'
37NAME_INTEL_7260 = 'Intel 7260'
38NAME_INTEL_7265 = 'Intel 7265'
39NAME_INTEL_9000 = 'Intel 9000'
40NAME_INTEL_9260 = 'Intel 9260'
41NAME_INTEL_22260 = 'Intel 22260'
42NAME_BROADCOM_BCM4354_SDIO = 'Broadcom BCM4354 SDIO'
43NAME_BROADCOM_BCM4356_PCIE = 'Broadcom BCM4356 PCIE'
44NAME_BROADCOM_BCM4371_PCIE = 'Broadcom BCM4371 PCIE'
45NAME_UNKNOWN = 'Unknown WiFi Device'
46
47DEVICE_INFO_ROOT = '/sys/class/net'
48
49DeviceInfo = collections.namedtuple('DeviceInfo', ['vendor', 'device',
50                                                   'compatible'])
51# Provide default values for parameters.
52DeviceInfo.__new__.__defaults__ = (None, None, None)
53
54DEVICE_NAME_LOOKUP = {
55    DeviceInfo('0x02df', '0x9129'): NAME_MARVELL_88W8797_SDIO,
56    DeviceInfo('0x02df', '0x912d'): NAME_MARVELL_88W8897_SDIO,
57    DeviceInfo('0x02df', '0x9135'): NAME_MARVELL_88W8887_SDIO,
58    DeviceInfo('0x11ab', '0x2b38'): NAME_MARVELL_88W8897_PCIE,
59    DeviceInfo('0x1b4b', '0x2b42'): NAME_MARVELL_88W8997_PCIE,
60    DeviceInfo('0x168c', '0x002a'): NAME_ATHEROS_AR9280,
61    DeviceInfo('0x168c', '0x0030'): NAME_ATHEROS_AR9382,
62    DeviceInfo('0x168c', '0x0034'): NAME_ATHEROS_AR9462,
63    DeviceInfo('0x168c', '0x003e'): NAME_QUALCOMM_ATHEROS_QCA6174,
64    DeviceInfo('0x105b', '0xe09d'): NAME_QUALCOMM_ATHEROS_QCA6174,
65    DeviceInfo('0x8086', '0x08b1'): NAME_INTEL_7260,
66    DeviceInfo('0x8086', '0x08b2'): NAME_INTEL_7260,
67    DeviceInfo('0x8086', '0x095a'): NAME_INTEL_7265,
68    DeviceInfo('0x8086', '0x095b'): NAME_INTEL_7265,
69    DeviceInfo('0x8086', '0x9df0'): NAME_INTEL_9000,
70    DeviceInfo('0x8086', '0x31dc'): NAME_INTEL_9000,
71    DeviceInfo('0x8086', '0x2526'): NAME_INTEL_9260,
72    DeviceInfo('0x8086', '0x2723'): NAME_INTEL_22260,
73    DeviceInfo('0x02d0', '0x4354'): NAME_BROADCOM_BCM4354_SDIO,
74    DeviceInfo('0x14e4', '0x43ec'): NAME_BROADCOM_BCM4356_PCIE,
75    DeviceInfo('0x14e4', '0x440d'): NAME_BROADCOM_BCM4371_PCIE,
76
77    DeviceInfo(compatible='qcom,wcn3990-wifi'): NAME_QUALCOMM_WCN3990,
78}
79
80class Interface:
81    """Interace is a class that contains the queriable address properties
82    of an network device.
83    """
84    ADDRESS_TYPE_MAC = 'link/ether'
85    ADDRESS_TYPE_IPV4 = 'inet'
86    ADDRESS_TYPE_IPV6 = 'inet6'
87    ADDRESS_TYPES = [ ADDRESS_TYPE_MAC, ADDRESS_TYPE_IPV4, ADDRESS_TYPE_IPV6 ]
88
89
90    @staticmethod
91    def get_connected_ethernet_interface(ignore_failures=False):
92        """Get an interface object representing a connected ethernet device.
93
94        Raises an exception if no such interface exists.
95
96        @param ignore_failures bool function will return None instead of raising
97                an exception on failures.
98        @return an Interface object except under the conditions described above.
99
100        """
101        # Assume that ethernet devices are called ethX until proven otherwise.
102        for device_name in ['eth%d' % i for i in range(5)]:
103            ethernet_if = Interface(device_name)
104            if ethernet_if.exists and ethernet_if.ipv4_address:
105                return ethernet_if
106
107        else:
108            if ignore_failures:
109                return None
110
111            raise error.TestFail('Failed to find ethernet interface.')
112
113
114    def __init__(self, name, host=None):
115        self._name = name
116        if host is None:
117            self.host = local_host.LocalHost()
118        else:
119            self.host = host
120        self._run = self.host.run
121
122
123    @property
124    def name(self):
125        """@return name of the interface (e.g. 'wlan0')."""
126        return self._name
127
128
129    @property
130    def addresses(self):
131        """@return the addresses (MAC, IP) associated with interface."""
132        # "ip addr show %s 2> /dev/null" returns something that looks like:
133        #
134        # 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast
135        #    link/ether ac:16:2d:07:51:0f brd ff:ff:ff:ff:ff:ff
136        #    inet 172.22.73.124/22 brd 172.22.75.255 scope global eth0
137        #    inet6 2620:0:1000:1b02:ae16:2dff:fe07:510f/64 scope global dynamic
138        #       valid_lft 2591982sec preferred_lft 604782sec
139        #    inet6 fe80::ae16:2dff:fe07:510f/64 scope link
140        #       valid_lft forever preferred_lft forever
141        #
142        # We extract the second column from any entry for which the first
143        # column is an address type we are interested in.  For example,
144        # for "inet 172.22.73.124/22 ...", we will capture "172.22.73.124/22".
145        result = self._run('ip addr show %s 2> /dev/null' % self._name,
146                           ignore_status=True)
147        address_info = result.stdout
148        if result.exit_status != 0:
149            # The "ip" command will return non-zero if the interface does
150            # not exist.
151            return {}
152
153        addresses = {}
154        for address_line in address_info.splitlines():
155            address_parts = address_line.lstrip().split()
156            if len(address_parts) < 2:
157                continue
158            address_type, address_value = address_parts[:2]
159            if address_type in self.ADDRESS_TYPES:
160                if address_type not in addresses:
161                    addresses[address_type] = []
162                addresses[address_type].append(address_value)
163        return addresses
164
165
166    @property
167    def device_path(self):
168        """@return the sysfs path of the interface device"""
169        # This assumes that our path separator is the same as the remote host.
170        device_path = os.path.join(DEVICE_INFO_ROOT, self._name, 'device')
171        if not self.host.path_exists(device_path):
172            logging.error('No device information found at %s', device_path)
173            return None
174
175        return device_path
176
177
178    @property
179    def wiphy_name(self):
180        """
181        @return name of the wiphy (e.g., 'phy0'), if available.
182        Otherwise None.
183        """
184        readlink_result = self._run('readlink "%s"' %
185                os.path.join(DEVICE_INFO_ROOT, self._name, 'phy80211'),
186                ignore_status=True)
187        if readlink_result.exit_status != 0:
188            return None
189
190        return os.path.basename(readlink_result.stdout.strip())
191
192
193    @property
194    def module_name(self):
195        """@return Name of kernel module in use by this interface."""
196        module_readlink_result = self._run('readlink "%s"' %
197                os.path.join(self.device_path, 'driver', 'module'),
198                ignore_status=True)
199        if module_readlink_result.exit_status != 0:
200            return None
201
202        return os.path.basename(module_readlink_result.stdout.strip())
203
204    @property
205    def parent_device_name(self):
206        """
207        @return Name of device at which wiphy device is present. For example,
208        for a wifi NIC present on a PCI bus, this would be the same as
209        PCI_SLOT_PATH. """
210        path_readlink_result = self._run('readlink "%s"' % self.device_path)
211        if path_readlink_result.exit_status != 0:
212            return None
213
214        return os.path.basename(path_readlink_result.stdout.strip())
215
216    @property
217    def device_description(self):
218        """@return DeviceDescription object for a WiFi interface, or None."""
219        read_file = (lambda path: self._run('cat "%s"' % path).stdout.rstrip()
220                     if self.host.path_exists(path) else None)
221        if not self.is_wifi_device():
222            logging.error('Device description not supported on non-wifi '
223                          'interface: %s.', self._name)
224            return None
225
226        device_path = self.device_path
227        if not device_path:
228            logging.error('No device path found')
229            return None
230
231        # Try to identify using either vendor/product ID, or using device tree
232        # "OF_COMPATIBLE_x".
233        vendor_id = read_file(os.path.join(device_path, 'vendor'))
234        product_id = read_file(os.path.join(device_path, 'device'))
235        uevent = read_file(os.path.join(device_path, 'uevent'))
236
237        # Vendor/product ID.
238        infos = [DeviceInfo(vendor_id, product_id)]
239
240        # Compatible value(s).
241        for line in uevent.splitlines():
242            key, _, value = line.partition('=')
243            if re.match('^OF_COMPATIBLE_[0-9]+$', key):
244                infos += [DeviceInfo(compatible=value)]
245
246        for info in infos:
247            if info in DEVICE_NAME_LOOKUP:
248                device_name = DEVICE_NAME_LOOKUP[info]
249                logging.debug('Device is %s',  device_name)
250                break
251        else:
252            logging.error('Device is unknown. Info: %r', infos)
253            device_name = NAME_UNKNOWN
254        module_name = self.module_name
255        if module_name is not None:
256            kernel_release = self._run('uname -r').stdout.strip()
257            module_path = self._run('find '
258                                    '/lib/modules/%s/kernel/drivers/net '
259                                    '-name %s.ko -printf %%P' %
260                                    (kernel_release, module_name)).stdout
261        else:
262            module_path = 'Unknown (kernel might have modules disabled)'
263        return DeviceDescription(device_name, module_path)
264
265
266    @property
267    def exists(self):
268        """@return True if this interface exists, False otherwise."""
269        # No valid interface has no addresses at all.
270        return bool(self.addresses)
271
272
273
274    def get_ip_flags(self):
275        """@return List of flags from 'ip addr show'."""
276        # "ip addr show %s 2> /dev/null" returns something that looks like:
277        #
278        # 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast
279        #    link/ether ac:16:2d:07:51:0f brd ff:ff:ff:ff:ff:ff
280        #    inet 172.22.73.124/22 brd 172.22.75.255 scope global eth0
281        #    inet6 2620:0:1000:1b02:ae16:2dff:fe07:510f/64 scope global dynamic
282        #       valid_lft 2591982sec preferred_lft 604782sec
283        #    inet6 fe80::ae16:2dff:fe07:510f/64 scope link
284        #       valid_lft forever preferred_lft forever
285        #
286        # We only cares about the flags in the first line.
287        result = self._run('ip addr show %s 2> /dev/null' % self._name,
288                           ignore_status=True)
289        address_info = result.stdout
290        if result.exit_status != 0:
291            # The "ip" command will return non-zero if the interface does
292            # not exist.
293            return []
294        status_line = address_info.splitlines()[0]
295        flags_str = status_line[status_line.find('<')+1:status_line.find('>')]
296        return flags_str.split(',')
297
298
299    @property
300    def is_up(self):
301        """@return True if this interface is UP, False otherwise."""
302        return 'UP' in self.get_ip_flags()
303
304
305    @property
306    def is_lower_up(self):
307        """
308        Check if the interface is in LOWER_UP state. This usually means (e.g.,
309        for ethernet) a link is detected.
310
311        @return True if this interface is LOWER_UP, False otherwise."""
312        return 'LOWER_UP' in self.get_ip_flags()
313
314
315    def is_link_operational(self):
316        """@return True if RFC 2683 IfOperStatus is UP (i.e., is able to pass
317        packets).
318        """
319        command = 'ip link show %s' % self._name
320        result = self._run(command, ignore_status=True)
321        if result.exit_status:
322            return False
323        return result.stdout.find('state UP') >= 0
324
325
326    @property
327    def mac_address(self):
328        """@return the (first) MAC address, e.g., "00:11:22:33:44:55"."""
329        return self.addresses.get(self.ADDRESS_TYPE_MAC, [None])[0]
330
331
332    @property
333    def ipv4_address_and_prefix(self):
334        """@return the IPv4 address/prefix, e.g., "192.186.0.1/24"."""
335        return self.addresses.get(self.ADDRESS_TYPE_IPV4, [None])[0]
336
337
338    @property
339    def ipv4_address(self):
340        """@return the (first) IPv4 address, e.g., "192.168.0.1"."""
341        netblock_addr = self.netblock
342        return netblock_addr.addr if netblock_addr else None
343
344
345    @property
346    def ipv4_prefix(self):
347        """@return the IPv4 address prefix e.g., 24."""
348        addr = self.netblock
349        return addr.prefix_len if addr else None
350
351
352    @property
353    def ipv4_subnet(self):
354        """@return string subnet of IPv4 address (e.g. '192.168.0.0')"""
355        addr = self.netblock
356        return addr.subnet if addr else None
357
358
359    @property
360    def ipv4_subnet_mask(self):
361        """@return the IPv4 subnet mask e.g., "255.255.255.0"."""
362        addr = self.netblock
363        return addr.netmask if addr else None
364
365
366    def is_wifi_device(self):
367        """@return True if iw thinks this is a wifi device."""
368        if self._run('iw dev %s info' % self._name,
369                     ignore_status=True).exit_status:
370            logging.debug('%s does not seem to be a wireless device.',
371                          self._name)
372            return False
373        return True
374
375
376    @property
377    def netblock(self):
378        """Return Netblock object for this interface's IPv4 address.
379
380        @return Netblock object (or None if no IPv4 address found).
381
382        """
383        netblock_str = self.ipv4_address_and_prefix
384        return netblock.from_addr(netblock_str) if netblock_str else None
385
386
387    @property
388    def signal_level(self):
389        """Get the signal level for an interface.
390
391        This is currently only defined for WiFi interfaces.
392
393        localhost test # iw dev mlan0 link
394        Connected to 04:f0:21:03:7d:b2 (on mlan0)
395                SSID: Perf_slvf0_ch36
396                freq: 5180
397                RX: 699407596 bytes (8165441 packets)
398                TX: 58632580 bytes (9923989 packets)
399                signal: -54 dBm
400                tx bitrate: 130.0 MBit/s MCS 15
401
402                bss flags:
403                dtim period:    2
404                beacon int:     100
405
406        @return signal level in dBm (a negative, integral number).
407
408        """
409        if not self.is_wifi_device():
410            return None
411
412        result_lines = self._run('iw dev %s link' %
413                                 self._name).stdout.splitlines()
414        signal_pattern = re.compile('signal:\s+([-0-9]+)\s+dbm')
415        for line in result_lines:
416            cleaned = line.strip().lower()
417            match = re.search(signal_pattern, cleaned)
418            if match is not None:
419                return int(match.group(1))
420
421        logging.error('Failed to find signal level for %s.', self._name)
422        return None
423
424
425    @property
426    def mtu(self):
427        """@return the interface configured maximum transmission unit (MTU)."""
428        # "ip addr show %s 2> /dev/null" returns something that looks like:
429        #
430        # 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast
431        #    link/ether ac:16:2d:07:51:0f brd ff:ff:ff:ff:ff:ff
432        #    inet 172.22.73.124/22 brd 172.22.75.255 scope global eth0
433        #    inet6 2620:0:1000:1b02:ae16:2dff:fe07:510f/64 scope global dynamic
434        #       valid_lft 2591982sec preferred_lft 604782sec
435        #    inet6 fe80::ae16:2dff:fe07:510f/64 scope link
436        #       valid_lft forever preferred_lft forever
437        #
438        # We extract the 'mtu' value (in this example "1500")
439        try:
440            result = self._run('ip addr show %s 2> /dev/null' % self._name)
441            address_info = result.stdout
442        except error.CmdError, e:
443            # The "ip" command will return non-zero if the interface does
444            # not exist.
445            return None
446
447        match = re.search('mtu\s+(\d+)', address_info)
448        if not match:
449            raise error.TestFail('MTU information is not available.')
450        return int(match.group(1))
451
452
453    def noise_level(self, frequency_mhz):
454        """Get the noise level for an interface at a given frequency.
455
456        This is currently only defined for WiFi interfaces.
457
458        This only works on some devices because 'iw survey dump' (the method
459        used to get the noise) only works on some devices.  On other devices,
460        this method returns None.
461
462        @param frequency_mhz: frequency at which the noise level should be
463               measured and reported.
464        @return noise level in dBm (a negative, integral number) or None.
465
466        """
467        if not self.is_wifi_device():
468            return None
469
470        # This code has to find the frequency and then find the noise
471        # associated with that frequency because 'iw survey dump' output looks
472        # like this:
473        #
474        # localhost test # iw dev mlan0 survey dump
475        # ...
476        # Survey data from mlan0
477        #     frequency:              5805 MHz
478        #     noise:                  -91 dBm
479        #     channel active time:    124 ms
480        #     channel busy time:      1 ms
481        #     channel receive time:   1 ms
482        #     channel transmit time:  0 ms
483        # Survey data from mlan0
484        #     frequency:              5825 MHz
485        # ...
486
487        result_lines = self._run('iw dev %s survey dump' %
488                                 self._name).stdout.splitlines()
489        my_frequency_pattern = re.compile('frequency:\s*%d mhz' %
490                                          frequency_mhz)
491        any_frequency_pattern = re.compile('frequency:\s*\d{4} mhz')
492        inside_desired_frequency_block = False
493        noise_pattern = re.compile('noise:\s*([-0-9]+)\s+dbm')
494        for line in result_lines:
495            cleaned = line.strip().lower()
496            if my_frequency_pattern.match(cleaned):
497                inside_desired_frequency_block = True
498            elif inside_desired_frequency_block:
499                match = noise_pattern.match(cleaned)
500                if match is not None:
501                    return int(match.group(1))
502                if any_frequency_pattern.match(cleaned):
503                    inside_desired_frequency_block = False
504
505        logging.error('Failed to find noise level for %s at %d MHz.',
506                      self._name, frequency_mhz)
507        return None
508
509
510def get_interfaces():
511    """
512    Retrieve the list of network interfaces found on the system.
513
514    @return List of interfaces.
515
516    """
517    return [Interface(nic.strip()) for nic in os.listdir(DEVICE_INFO_ROOT)]
518
519
520def get_prioritized_default_route(host=None, interface_name_regex=None):
521    """
522    Query a local or remote host for its prioritized default interface
523    and route.
524
525    @param interface_name_regex string regex to filter routes by interface.
526    @return DefaultRoute tuple, or None if no default routes are found.
527
528    """
529    # Build a list of default routes, filtered by interface if requested.
530    # Example command output: 'default via 172.23.188.254 dev eth0  metric 2'
531    run = host.run if host is not None else utils.run
532    output = run('ip route show').stdout
533    output_regex_str = 'default\s+via\s+(\S+)\s+dev\s+(\S+)\s+metric\s+(\d+)'
534    output_regex = re.compile(output_regex_str)
535    defaults = []
536    for item in output.splitlines():
537        if 'default' not in item:
538            continue
539        match = output_regex.match(item.strip())
540        if match is None:
541            raise error.TestFail('Unexpected route output: %s' % item)
542        gateway = match.group(1)
543        interface_name = match.group(2)
544        metric = int(match.group(3))
545        if interface_name_regex is not None:
546            if re.match(interface_name_regex, interface_name) is None:
547                continue
548        defaults.append(DefaultRoute(interface_name=interface_name,
549                                     gateway=gateway, metric=metric))
550    if not defaults:
551        return None
552
553    # Sort and return the route with the lowest metric value.
554    defaults.sort(key=lambda x: x.metric)
555    return defaults[0]
556
557