Home | History | Annotate | Download | only in network_WiFi_RegDomain
      1 # Copyright (c) 2015 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 
      5 import logging
      6 import subprocess
      7 import tempfile
      8 
      9 from autotest_lib.client.bin import utils
     10 from autotest_lib.client.common_lib import error
     11 from autotest_lib.client.common_lib.cros.network import iw_runner
     12 from autotest_lib.client.common_lib.cros.network import xmlrpc_datatypes
     13 from autotest_lib.server import test
     14 from autotest_lib.server.cros.network import hostap_config
     15 from autotest_lib.server.cros.network import wifi_test_context_manager
     16 
     17 
     18 class network_WiFi_RegDomain(test.test):
     19     """Verifies that a DUT connects, or fails to connect, on particular
     20     channels, in particular regions, per expectations."""
     21     version = 1
     22 
     23 
     24     MISSING_SSID = "MissingSsid"
     25     # TODO(quiche): Shrink or remove the repeat count, once we've
     26     # figured out why tcpdump sometimes misses data. crbug.com/477536
     27     PASSIVE_SCAN_REPEAT_COUNT = 30
     28     REBOOT_TIMEOUT_SECS = 60
     29     # TODO(quiche): Migrate to the shiny new pyshark code from rpius.
     30     TSHARK_COMMAND = 'tshark'
     31     TSHARK_DISABLE_NAME_RESOLUTION = '-n'
     32     TSHARK_READ_FILE = '-r'
     33     TSHARK_SRC_FILTER = 'wlan.sa == %s'
     34     VPD_CACHE_FILE = \
     35         '/mnt/stateful_partition/unencrypted/cache/vpd/full-v2.txt'
     36     VPD_CLEAN_COMMAND ='dump_vpd_log --clean'
     37 
     38 
     39     @staticmethod
     40     def assert_equal(description, actual, expected):
     41         """Verifies that |actual| equals |expected|.
     42 
     43         @param description A string describing the data being checked.
     44         @param actual The actual value encountered by the test.
     45         @param expected The value we expected to encounter.
     46         @raise error.TestFail If actual != expected.
     47 
     48         """
     49         if actual != expected:
     50             raise error.TestFail(
     51                 'Expected %s |%s|, but got |%s|.' %
     52                 (description, expected, actual))
     53 
     54 
     55     @staticmethod
     56     def phy_list_to_channel_expectations(phy_list):
     57         """Maps phy information to expected scanning/connection behavior.
     58 
     59         Converts phy information from iw_runner.IwRunner.list_phys()
     60         into a map from channel numbers to expected connection
     61         behavior. This mapping is useful for comparison with the
     62         expectations programmed into the control file.
     63 
     64         @param phy_list The return value of iw_runner.IwRunner.list_phys()
     65         @return A dict from channel numbers to expected behaviors.
     66 
     67         """
     68         channel_to_expectation = {}
     69         for phy in phy_list:
     70             for band in phy.bands:
     71                 for frequency, flags in band.frequency_flags.iteritems():
     72                     channel = (
     73                         hostap_config.HostapConfig.get_channel_for_frequency(
     74                             frequency))
     75                     # While we don't expect a channel to have both
     76                     # CHAN_FLAG_DISABLED, and (CHAN_FLAG_PASSIVE_SCAN
     77                     # or CHAN_FLAG_NO_IR), we still test the most
     78                     # restrictive flag first.
     79                     if iw_runner.CHAN_FLAG_DISABLED in flags:
     80                         channel_to_expectation[channel] = 'no-connect'
     81                     elif (iw_runner.CHAN_FLAG_PASSIVE_SCAN in flags or
     82                         iw_runner.CHAN_FLAG_NO_IR in flags):
     83                         channel_to_expectation[channel] = 'passive-scan'
     84                     else:
     85                         channel_to_expectation[channel] = 'connect'
     86         return channel_to_expectation
     87 
     88 
     89     @staticmethod
     90     def test_connect(wifi_context, frequency, expect_connect, hide_ssid):
     91         """Verifies that a DUT does/does not connect on a particular frequency.
     92 
     93         @param wifi_context: A WiFiTestContextManager.
     94         @param frequency: int frequency to test.
     95         @param expect_connect: bool whether or not connection should succeed.
     96         @param hide_ssid: bool whether or not the AP should hide its SSID.
     97         @raise error.TestFail if behavior does not match expectation.
     98 
     99         """
    100         try:
    101             router_ssid = None
    102             if hide_ssid:
    103                 pcap_name = '%d_connect_hidden.pcap' % frequency
    104                 test_description = 'hidden'
    105             else:
    106                 pcap_name = '%d_connect_visible.pcap' % frequency
    107                 test_description = 'visible'
    108             wifi_context.capture_host.start_capture(frequency,
    109                                                     filename=pcap_name)
    110             wifi_context.router.hostap_configure(
    111                 hostap_config.HostapConfig(
    112                     frequency=frequency,
    113                     hide_ssid=hide_ssid,
    114                     mode=hostap_config.HostapConfig.MODE_11N_MIXED))
    115             router_ssid = wifi_context.router.get_ssid()
    116             client_conf = xmlrpc_datatypes.AssociationParameters(
    117                 ssid=router_ssid,
    118                 is_hidden=hide_ssid,
    119                 expect_failure=not expect_connect
    120                 )
    121             wifi_context.assert_connect_wifi(client_conf, test_description)
    122         finally:
    123             if router_ssid:
    124                 wifi_context.client.shill.delete_entries_for_ssid(router_ssid)
    125             wifi_context.capture_host.stop_capture()
    126 
    127 
    128     @classmethod
    129     def count_mismatched_phy_configs(cls, dut_host, expected_channel_configs):
    130         """Verifies that phys on the DUT place the expected restrictions on
    131         channels.
    132 
    133         Compares the restrictions reported by the running system to
    134         the restrictions in |expected_channel_configs|. Returns a
    135         count of the number of mismatches.
    136 
    137         Note that this method deliberately ignores channels that are
    138         reported by the running system, but not mentioned in
    139         |expected_channel_configs|. This allows us to program the
    140         control file with "spot checks", rather than an exhaustive
    141         list of channels.
    142 
    143         @param dut_host The host object for the DUT.
    144         @param expected_channel_configs A channel_infos list.
    145         @return int count of mismatches
    146 
    147         """
    148         actual_channel_expectations = cls.phy_list_to_channel_expectations(
    149             iw_runner.IwRunner(dut_host).list_phys())
    150         mismatches = 0
    151         for expected_config in expected_channel_configs:
    152             channel = expected_config['chnum']
    153             expected = expected_config['expect']
    154             actual = actual_channel_expectations[channel]
    155             if actual != expected:
    156                 logging.error(
    157                     'Expected phy config for channel %d of |%s|, but got |%s|.',
    158                     channel, expected, actual)
    159                 mismatches += 1
    160         return mismatches
    161 
    162 
    163     @classmethod
    164     def assert_scanning_is_passive(cls, client, capturer, scan_freq):
    165         """Initiates single-channel scans, and verifies no probes are sent.
    166 
    167         @param client The WiFiClient object for the DUT.
    168         @param capturer The LinuxSystem object for the router or pcap_host.
    169         @param scan_freq The frequency (in MHz) on which to scan.
    170         """
    171         try:
    172             client.claim_wifi_if()  # Stop shill/supplicant scans.
    173             capturer.start_capture(
    174                     scan_freq, filename='%d_scan.pcap' % scan_freq)
    175             for i in range(0, cls.PASSIVE_SCAN_REPEAT_COUNT):
    176                 # We pass in an SSID here, to check that even hidden
    177                 # SSIDs do not cause probe requests to be sent.
    178                 client.scan(
    179                     [scan_freq], [cls.MISSING_SSID], require_match=False)
    180             pcap_path = capturer.stop_capture()[0].local_pcap_path
    181             dut_frames = subprocess.check_output(
    182                 [cls.TSHARK_COMMAND,
    183                  cls.TSHARK_DISABLE_NAME_RESOLUTION,
    184                  cls.TSHARK_READ_FILE, pcap_path,
    185                  cls.TSHARK_SRC_FILTER % client.wifi_mac])
    186             if len(dut_frames):
    187                 raise error.TestFail('Saw unexpected frames from DUT.')
    188         finally:
    189             client.release_wifi_if()
    190             capturer.stop_capture()
    191 
    192 
    193     @classmethod
    194     def assert_scanning_fails(cls, client, scan_freq):
    195         """Initiates a single-channel scan, and verifies that it fails.
    196 
    197         @param client The WiFiClient object for the DUT.
    198         @param scan_freq The frequency (in MHz) on which to scan.
    199         """
    200         client.claim_wifi_if()  # Stop shill/supplicant scans.
    201         try:
    202             # We use IwRunner directly here, because WiFiClient.scan()
    203             # wants a scan to succeed, while we want the scan to fail.
    204             if iw_runner.IwRunner(client.host).timed_scan(
    205                 client.wifi_if, [scan_freq], [cls.MISSING_SSID]):
    206                 # We should have got None, to represent failure.
    207                 raise error.TestFail(
    208                     'Scan succeeded (and was expected to fail).')
    209         finally:
    210             client.release_wifi_if()
    211 
    212 
    213     def fake_up_region(self, region):
    214         """Modifies VPD cache to force a particular region, and reboots system
    215         into to faked state.
    216 
    217         @param region: The region we want to force the host into.
    218 
    219         """
    220         self.host.run(self.VPD_CLEAN_COMMAND)
    221         temp_vpd = tempfile.NamedTemporaryFile()
    222         temp_vpd.write('"region"="%s"' % region)
    223         temp_vpd.flush()
    224         self.host.send_file(temp_vpd.name, self.VPD_CACHE_FILE)
    225         self.host.reboot(timeout=self.REBOOT_TIMEOUT_SECS, wait=True)
    226 
    227 
    228     def warmup(self, host, raw_cmdline_args, additional_params):
    229         """Stashes away parameters for use by run_once().
    230 
    231         @param host Host object representing the client DUT.
    232         @param raw_cmdline_args Raw input from autotest.
    233         @param additional_params One item from CONFIGS in control file.
    234 
    235         """
    236         self.host = host
    237         self.cmdline_args = utils.args_to_dict(raw_cmdline_args)
    238         self.channel_infos = additional_params['channel_infos']
    239         self.expected_country_code = additional_params['country_code']
    240         self.region_name = additional_params['region_name']
    241 
    242 
    243     def test_channel(self, wifi_context, channel_config):
    244         """Verifies that a DUT's behavior on a channel is per expectations.
    245 
    246         - Verifies that scanning behavior is per expectations.
    247         - Verifies that connect behavior is per expectations.
    248         - Verifies that connect behavior is the same for hidden networks,
    249           as it is for visible networks.
    250 
    251         @param wifi_context: A WiFiTestContextManager.
    252         @param channel_config: A dict with 'chnum' and 'expect' keys.
    253 
    254         """
    255         router_freq = hostap_config.HostapConfig.get_frequency_for_channel(
    256             channel_config['chnum'])
    257 
    258         # Test scanning behavior, as appropriate. To ensure that,
    259         # e.g., AP beacons don't affect the DUT's behavior, this is
    260         # done with no AP running.
    261         if channel_config['expect'] == 'passive-scan':
    262             self.assert_scanning_is_passive(
    263                 wifi_context.client, wifi_context.capture_host,
    264                 router_freq)
    265         elif channel_config['expect'] == 'no-connect':
    266             self.assert_scanning_fails(wifi_context.client, router_freq)
    267 
    268         for hide_ssid in (False, True):  # Simple case first.
    269             self.test_connect(
    270                 wifi_context,
    271                 router_freq,
    272                 expect_connect=channel_config['expect'] in (
    273                     'connect', 'passive-scan'),
    274                 hide_ssid=hide_ssid)
    275 
    276 
    277     def run_once(self):
    278         """Configures a DUT to behave as if it was manufactured for a
    279         particular region. Then verifies that the DUT connects, or
    280         fails to connect, per expectations.
    281 
    282         """
    283         num_failures = 0
    284         try:
    285             self.fake_up_region(self.region_name)
    286             self.assert_equal(
    287               'country code',
    288               iw_runner.IwRunner(self.host).get_regulatory_domain(),
    289               self.expected_country_code)
    290             num_mismatches = self.count_mismatched_phy_configs(
    291                 self.host, self.channel_infos)
    292             if num_mismatches:
    293                 raise error.TestFail(
    294                     '%d phy configs were not as expected (see below)' %
    295                     num_mismatches)
    296             wifi_context = wifi_test_context_manager.WiFiTestContextManager(
    297                 self.__class__.__name__,
    298                 self.host,
    299                 self.cmdline_args,
    300                 self.debugdir)
    301             with wifi_context:
    302                 wifi_context.router.reboot(timeout=self.REBOOT_TIMEOUT_SECS)
    303                 for channel_config in self.channel_infos:
    304                     try:
    305                         self.test_channel(wifi_context, channel_config)
    306                     except error.TestFail as e:
    307                         # Log the error, but keep going. This way, we
    308                         # get a full report of channels where behavior
    309                         # differs from expectations.
    310                         logging.error('Verification failed for |%s|: %s',
    311                                       self.region_name, channel_config)
    312                         logging.error(e)
    313                         num_failures += 1
    314         finally:
    315             if num_failures:
    316                 raise error.TestFail(
    317                     'Verification failed for %d channel configs (see below)' %
    318                     num_failures)
    319             self.host.run(self.VPD_CLEAN_COMMAND)
    320             self.host.reboot(timeout=self.REBOOT_TIMEOUT_SECS, wait=True)
    321