Home | History | Annotate | Download | only in network
      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 
      5 import collections
      6 import logging
      7 import re
      8 import time
      9 
     10 from autotest_lib.client.common_lib import error
     11 from autotest_lib.client.common_lib.cros.network import xmlrpc_datatypes
     12 
     13 
     14 # Used to represent stations we parse out of scan results.
     15 Station = collections.namedtuple('Station',
     16                                  ['bssid', 'frequency', 'signal', 'ssid'])
     17 
     18 class WpaCliProxy(object):
     19     """Interacts with a DUT through wpa_cli rather than shill."""
     20 
     21     SCANNING_INTERVAL_SECONDS = 5
     22     POLLING_INTERVAL_SECONDS = 0.5
     23     # From wpa_supplicant.c:wpa_supplicant_state_txt()
     24     WPA_SUPPLICANT_ASSOCIATING_STATES = (
     25             'AUTHENTICATING',
     26             'ASSOCIATING',
     27             'ASSOCIATED',
     28             '4WAY_HANDSHAKE',
     29             'GROUP_HANDSHAKE')
     30     WPA_SUPPLICANT_ASSOCIATED_STATES = (
     31             'COMPLETED',)
     32     ANDROID_CMD_FORMAT = '/system/bin/wpa_cli IFNAME={0[ifname]} {0[cmd]}'
     33     BRILLO_CMD_FORMAT = 'su system /system/bin/wpa_cli -i{0[ifname]} -p/data/misc/wifi/sockets {0[cmd]}'
     34     CROS_CMD_FORMAT = 'su wpa -s /bin/bash -c "/usr/bin/wpa_cli {0[cmd]}"'
     35 
     36 
     37 
     38     def __init__(self, host, wifi_if):
     39         self._host = host
     40         self._wifi_if = wifi_if
     41         self._created_networks = {}
     42         # TODO(wiley) Hardcoding this IFNAME prefix makes some big assumptions.
     43         #             we'll need to discover this parameter as it becomes more
     44         #             generally useful.
     45         if host.get_os_type() == 'android':
     46             self._wpa_cli_cmd_format = self.ANDROID_CMD_FORMAT
     47         elif host.get_os_type() == 'brillo':
     48             self._wpa_cli_cmd_format = self.BRILLO_CMD_FORMAT
     49         elif host.get_os_type() == 'cros':
     50             self._wpa_cli_cmd_format = self.CROS_CMD_FORMAT
     51 
     52 
     53     def _add_network(self, ssid):
     54         """
     55         Add a wpa_supplicant network for ssid.
     56 
     57         @param ssid string: name of network to add.
     58         @return int network id of added network.
     59 
     60         """
     61         add_result = self.run_wpa_cli_cmd('add_network', check_result=False)
     62         network_id = int(add_result.stdout.splitlines()[-1])
     63         self.run_wpa_cli_cmd('set_network %d ssid \\"%s\\"' %
     64                              (network_id, ssid))
     65         self._created_networks[ssid] = network_id
     66         logging.debug('Added network %s=%d', ssid, network_id)
     67         return network_id
     68 
     69 
     70     def run_wpa_cli_cmd(self, command, check_result=True):
     71         """
     72         Run a wpa_cli command and optionally check the result.
     73 
     74         @param command string: suffix of a command to be prefixed with
     75                 an appropriate wpa_cli for this host.
     76         @param check_result bool: True iff we want to check that the
     77                 command comes back with an 'OK' response.
     78         @return result object returned by host.run.
     79 
     80         """
     81         cmd = self._wpa_cli_cmd_format.format(
     82                 {'ifname' : self._wifi_if, 'cmd' : command})
     83         result = self._host.run(cmd)
     84         if check_result and not result.stdout.strip().endswith('OK'):
     85             raise error.TestFail('wpa_cli command failed: %s' % command)
     86 
     87         return result
     88 
     89 
     90     def _get_status_dict(self):
     91         """
     92         Gets the status output for a WiFi interface.
     93 
     94         Get the output of wpa_cli status.  This summarizes what wpa_supplicant
     95         is doing with respect to the WiFi interface.
     96 
     97         Example output:
     98 
     99             Using interface 'wlan0'
    100             wpa_state=INACTIVE
    101             p2p_device_address=32:76:6f:f2:a6:c4
    102             address=30:76:6f:f2:a6:c4
    103 
    104         @return dict of key/value pairs parsed from output using = as divider.
    105 
    106         """
    107         status_result = self.run_wpa_cli_cmd('status', check_result=False)
    108         return dict([line.strip().split('=', 1)
    109                      for line in status_result.stdout.splitlines()
    110                      if line.find('=') > 0])
    111 
    112 
    113     def _is_associating_or_associated(self):
    114         """@return True if the DUT is assocating or associated with a BSS."""
    115         state = self._get_status_dict().get('wpa_state', None)
    116         return state in (self.WPA_SUPPLICANT_ASSOCIATING_STATES +
    117                          self.WPA_SUPPLICANT_ASSOCIATED_STATES)
    118 
    119 
    120     def _is_associated(self, ssid):
    121         """
    122         Check if the DUT is associated to a given SSID.
    123 
    124         @param ssid string: SSID of the network we're concerned about.
    125         @return True if we're associated with the specified SSID.
    126 
    127         """
    128         status_dict = self._get_status_dict()
    129         return (status_dict.get('ssid', None) == ssid and
    130                 status_dict.get('wpa_state', None) in
    131                         self.WPA_SUPPLICANT_ASSOCIATED_STATES)
    132 
    133 
    134     def _is_connected(self, ssid):
    135         """
    136         Check that we're connected to |ssid| and have an IP address.
    137 
    138         @param ssid string: SSID of the network we're concerned about.
    139         @return True if we have an IP and we're associated with |ssid|.
    140 
    141         """
    142         status_dict = self._get_status_dict()
    143         return (status_dict.get('ssid', None) == ssid and
    144                 status_dict.get('ip_address', None))
    145 
    146 
    147     def _wait_until(self, value_check, timeout_seconds):
    148         """
    149         Call a function repeatedly until we time out.
    150 
    151         Call value_check() every POLLING_INTERVAL_SECONDS seconds
    152         until |timeout_seconds| have passed.  Return whether
    153         value_check() returned a True value and the time we spent in this
    154         function.
    155 
    156         @param timeout_seconds numeric: number of seconds to wait.
    157         @return a tuple (success, duration_seconds) where success is a boolean
    158                 and duration is a float.
    159 
    160         """
    161         start_time = time.time()
    162         while time.time() - start_time < timeout_seconds:
    163             duration = time.time() - start_time
    164             if value_check():
    165                 return (True, duration)
    166 
    167             time.sleep(self.POLLING_INTERVAL_SECONDS)
    168         duration = time.time() - start_time
    169         return (False, duration)
    170 
    171 
    172     def clean_profiles(self):
    173         """Remove state associated with past networks we've connected to."""
    174         # list_networks output looks like:
    175         # Using interface 'wlan0'^M
    176         # network id / ssid / bssid / flags^M
    177         # 0    SimpleConnect_jstja_ch1 any     [DISABLED]^M
    178         # 1    SimpleConnect_gjji2_ch6 any     [DISABLED]^M
    179         # 2    SimpleConnect_xe9d1_ch11        any     [DISABLED]^M
    180         list_networks_result = self.run_wpa_cli_cmd(
    181                 'list_networks', check_result=False)
    182         start_parsing = False
    183         for line in list_networks_result.stdout.splitlines():
    184             if not start_parsing:
    185                 if line.startswith('network id'):
    186                     start_parsing = True
    187                 continue
    188 
    189             network_id = int(line.split()[0])
    190             self.run_wpa_cli_cmd('remove_network %d' % network_id)
    191         self._created_networks = {}
    192 
    193 
    194     def create_profile(self, _):
    195         """
    196         This is a no op, since we don't have profiles.
    197 
    198         @param _ ignored.
    199 
    200         """
    201         logging.info('Skipping create_profile on %s', self.__class__.__name__)
    202 
    203 
    204     def pop_profile(self, _):
    205         """
    206         This is a no op, since we don't have profiles.
    207 
    208         @param _ ignored.
    209 
    210         """
    211         logging.info('Skipping pop_profile on %s', self.__class__.__name__)
    212 
    213 
    214     def push_profile(self, _):
    215         """
    216         This is a no op, since we don't have profiles.
    217 
    218         @param _ ignored.
    219 
    220         """
    221         logging.info('Skipping push_profile on %s', self.__class__.__name__)
    222 
    223 
    224     def remove_profile(self, _):
    225         """
    226         This is a no op, since we don't have profiles.
    227 
    228         @param _ ignored.
    229 
    230         """
    231         logging.info('Skipping remove_profile on %s', self.__class__.__name__)
    232 
    233 
    234     def init_test_network_state(self):
    235         """Create a clean slate for tests with respect to remembered networks.
    236 
    237         For wpa_cli hosts, this means removing all remembered networks.
    238 
    239         @return True iff operation succeeded, False otherwise.
    240 
    241         """
    242         self.clean_profiles()
    243         return True
    244 
    245 
    246     def connect_wifi(self, assoc_params):
    247         """
    248         Connect to the WiFi network described by AssociationParameters.
    249 
    250         @param assoc_params AssociationParameters object.
    251         @return serialized AssociationResult object.
    252 
    253         """
    254         logging.debug('connect_wifi()')
    255         # Ouptut should look like:
    256         #   Using interface 'wlan0'
    257         #   0
    258         assoc_result = xmlrpc_datatypes.AssociationResult()
    259         network_id = self._add_network(assoc_params.ssid)
    260         if assoc_params.is_hidden:
    261             self.run_wpa_cli_cmd('set_network %d %s %s' %
    262                                  (network_id, 'scan_ssid', '1'))
    263 
    264         sec_config = assoc_params.security_config
    265         for field, value in sec_config.get_wpa_cli_properties().iteritems():
    266             self.run_wpa_cli_cmd('set_network %d %s %s' %
    267                                  (network_id, field, value))
    268         self.run_wpa_cli_cmd('select_network %d' % network_id)
    269 
    270         # Wait for an appropriate BSS to appear in scan results.
    271         scan_results_pattern = '\t'.join(['([0-9a-f:]{17})', # BSSID
    272                                           '([0-9]+)',  # Frequency
    273                                           '(-[0-9]+)',  # Signal level
    274                                           '(.*)',  # Encryption types
    275                                           '(.*)'])  # SSID
    276         last_scan_time = -1.0
    277         start_time = time.time()
    278         while time.time() - start_time < assoc_params.discovery_timeout:
    279             assoc_result.discovery_time = time.time() - start_time
    280             if self._is_associating_or_associated():
    281                 # Internally, wpa_supplicant writes its scan_results response
    282                 # to a 4kb buffer.  When there are many BSS's, the buffer fills
    283                 # up, and we'll never see the BSS we care about in some cases.
    284                 break
    285 
    286             scan_result = self.run_wpa_cli_cmd('scan_results',
    287                                                check_result=False)
    288             found_stations = []
    289             for line in scan_result.stdout.strip().splitlines():
    290                 match = re.match(scan_results_pattern, line)
    291                 if match is None:
    292                     continue
    293                 found_stations.append(
    294                         Station(bssid=match.group(1), frequency=match.group(2),
    295                                 signal=match.group(3), ssid=match.group(5)))
    296             logging.debug('Found stations: %r',
    297                           [station.ssid for station in found_stations])
    298             if [station for station in found_stations
    299                     if station.ssid == assoc_params.ssid]:
    300                 break
    301 
    302             if time.time() - last_scan_time > self.SCANNING_INTERVAL_SECONDS:
    303                 # Sometimes this might fail with a FAIL-BUSY if the previous
    304                 # scan hasn't finished.
    305                 scan_result = self.run_wpa_cli_cmd('scan', check_result=False)
    306                 if scan_result.stdout.strip().endswith('OK'):
    307                     last_scan_time = time.time()
    308             time.sleep(self.POLLING_INTERVAL_SECONDS)
    309         else:
    310             assoc_result.failure_reason = 'Discovery timed out'
    311             return assoc_result.serialize()
    312 
    313         # Wait on association to finish.
    314         success, assoc_result.association_time = self._wait_until(
    315                 lambda: self._is_associated(assoc_params.ssid),
    316                 assoc_params.association_timeout)
    317         if not success:
    318             assoc_result.failure_reason = 'Association timed out'
    319             return assoc_result.serialize()
    320 
    321         # Then wait for ip configuration to finish.
    322         success, assoc_result.configuration_time = self._wait_until(
    323                 lambda: self._is_connected(assoc_params.ssid),
    324                 assoc_params.configuration_timeout)
    325         if not success:
    326             assoc_result.failure_reason = 'DHCP negotiation timed out'
    327             return assoc_result.serialize()
    328 
    329         assoc_result.success = True
    330         logging.info('Connected to %s', assoc_params.ssid)
    331         return assoc_result.serialize()
    332 
    333 
    334     def disconnect(self, ssid):
    335         """
    336         Disconnect from a WiFi network named |ssid|.
    337 
    338         @param ssid string: name of network to disable in wpa_supplicant.
    339 
    340         """
    341         logging.debug('disconnect()')
    342         if ssid not in self._created_networks:
    343             return False
    344         self.run_wpa_cli_cmd('disable_network %d' %
    345                              self._created_networks[ssid])
    346         return True
    347 
    348 
    349     def delete_entries_for_ssid(self, ssid):
    350         """Delete a profile entry.
    351 
    352         @param ssid string of WiFi service for which to delete entries.
    353         @return True on success, False otherwise.
    354         """
    355         return self.disconnect(ssid)
    356 
    357 
    358     def set_device_enabled(self, wifi_interface, enabled):
    359         """Enable or disable the WiFi device.
    360 
    361         @param wifi_interface: string name of interface being modified.
    362         @param enabled: boolean; true if this device should be enabled,
    363                 false if this device should be disabled.
    364         @return True if it worked; false, otherwise
    365 
    366         """
    367         return False
    368 
    369 
    370     def sync_time_to(self, epoch_seconds):
    371         """
    372         Sync time on the DUT to |epoch_seconds| from the epoch.
    373 
    374         @param epoch_seconds float: number of seconds since the epoch.
    375 
    376         """
    377         # This will claim to fail, but will work anyway.
    378         self._host.run('date -u %f' % epoch_seconds, ignore_status=True)
    379