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