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