374 lines
17 KiB
Python
374 lines
17 KiB
Python
# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
import dbus
|
|
import logging
|
|
import time
|
|
|
|
from autotest_lib.client.common_lib import utils
|
|
from autotest_lib.client.cros.networking import shill_proxy
|
|
|
|
|
|
class WifiProxy(shill_proxy.ShillProxy):
|
|
"""Wrapper around shill dbus interface used by wifi tests."""
|
|
|
|
|
|
def set_logging_for_wifi_test(self):
|
|
"""Set the logging in shill for a test of wifi technology.
|
|
|
|
Set the log level to |ShillProxy.LOG_LEVEL_FOR_TEST| and the log scopes
|
|
to the ones defined in |ShillProxy.LOG_SCOPES_FOR_TEST| for
|
|
|ShillProxy.TECHNOLOGY_WIFI|.
|
|
|
|
"""
|
|
self.set_logging_for_test(self.TECHNOLOGY_WIFI)
|
|
|
|
|
|
def remove_all_wifi_entries(self):
|
|
"""Iterate over all pushed profiles and remove WiFi entries."""
|
|
profiles = self.get_profiles()
|
|
for profile in profiles:
|
|
profile_properties = profile.GetProperties(utf8_strings=True)
|
|
entries = profile_properties[self.PROFILE_PROPERTY_ENTRIES]
|
|
for entry_id in entries:
|
|
try:
|
|
entry = profile.GetEntry(entry_id)
|
|
except dbus.exceptions.DBusException as e:
|
|
logging.error('Unable to retrieve entry %s:%r', entry_id, e)
|
|
continue
|
|
if entry[self.ENTRY_FIELD_TYPE] == 'wifi':
|
|
profile.DeleteEntry(entry_id)
|
|
|
|
|
|
def configure_wifi_service(self, ssid, security, security_parameters=None,
|
|
save_credentials=True, station_type=None,
|
|
hidden_network=False, guid=None,
|
|
autoconnect=None):
|
|
"""Configure a WiFi service.
|
|
|
|
@param ssid string name of network to connect to.
|
|
@param security string type of security used in network (e.g. psk)
|
|
@param security_parameters dict of service property/value pairs that
|
|
make up the credentials and settings for the given security
|
|
type (e.g. the passphrase for psk security).
|
|
@param save_credentials bool True if we should save EAP credentials.
|
|
@param station_type string one of SUPPORTED_WIFI_STATION_TYPES.
|
|
@param hidden_network bool True when the SSID is not broadcasted.
|
|
@param guid string unique identifier for network.
|
|
@param autoconnect bool or None. None indicates that this should not
|
|
be set one way or the other, while a boolean indicates a desired
|
|
value.
|
|
|
|
"""
|
|
# |mode| is derived from the station type we're attempting to join. It
|
|
# does not refer to the 802.11x (802.11a/b/g/n) type. It refers to a
|
|
# shill connection mode.
|
|
mode = self.SUPPORTED_WIFI_STATION_TYPES[station_type]
|
|
|
|
if security_parameters is None:
|
|
security_parameters = {}
|
|
|
|
config_params = {self.SERVICE_PROPERTY_TYPE: 'wifi',
|
|
self.SERVICE_PROPERTY_HIDDEN: hidden_network,
|
|
self.SERVICE_PROPERTY_SSID: ssid,
|
|
self.SERVICE_PROPERTY_SECURITY_CLASS: security,
|
|
self.SERVICE_PROPERTY_MODE: mode}
|
|
if autoconnect is not None:
|
|
config_params[self.SERVICE_PROPERTY_AUTOCONNECT] = autoconnect
|
|
config_params.update(security_parameters)
|
|
if guid is not None:
|
|
config_params[self.SERVICE_PROPERTY_GUID] = guid
|
|
try:
|
|
self.configure_service(config_params)
|
|
except dbus.exceptions.DBusException as e:
|
|
logging.error('Caught an error while configuring a WiFi '
|
|
'service: %r', e)
|
|
return False
|
|
|
|
logging.info('Configured service: %s', ssid)
|
|
return True
|
|
|
|
|
|
def connect_to_wifi_network(self,
|
|
ssid,
|
|
security,
|
|
security_parameters,
|
|
save_credentials,
|
|
station_type=None,
|
|
hidden_network=False,
|
|
guid=None,
|
|
autoconnect=None,
|
|
discovery_timeout_seconds=15,
|
|
association_timeout_seconds=15,
|
|
configuration_timeout_seconds=15):
|
|
"""
|
|
Connect to a WiFi network with the given association parameters.
|
|
|
|
@param ssid string name of network to connect to.
|
|
@param security string type of security used in network (e.g. psk)
|
|
@param security_parameters dict of service property/value pairs that
|
|
make up the credentials and settings for the given security
|
|
type (e.g. the passphrase for psk security).
|
|
@param save_credentials bool True if we should save EAP credentials.
|
|
@param station_type string one of SUPPORTED_WIFI_STATION_TYPES.
|
|
@param hidden_network bool True when the SSID is not broadcasted.
|
|
@param guid string unique identifier for network.
|
|
@param discovery_timeout_seconds float timeout for service discovery.
|
|
@param association_timeout_seconds float timeout for service
|
|
association.
|
|
@param configuration_timeout_seconds float timeout for DHCP
|
|
negotiations.
|
|
@param autoconnect: bool or None. None indicates that this should not
|
|
be set one way or the other, while a boolean indicates a desired
|
|
value.
|
|
@return (successful, discovery_time, association_time,
|
|
configuration_time, reason)
|
|
where successful is True iff the operation succeeded, *_time is
|
|
the time spent waiting for each transition, and reason is a string
|
|
which may contain a meaningful description of failures.
|
|
|
|
"""
|
|
logging.info('Attempting to connect to %s', ssid)
|
|
start_time = time.time()
|
|
discovery_time = -1.0
|
|
association_time = -1.0
|
|
configuration_time = -1.0
|
|
if station_type not in self.SUPPORTED_WIFI_STATION_TYPES:
|
|
return (False, discovery_time, association_time,
|
|
configuration_time,
|
|
'FAIL(Invalid station type specified.)')
|
|
|
|
# |mode| is derived from the station type we're attempting to join. It
|
|
# does not refer to the 802.11x (802.11a/b/g/n) type. It refers to a
|
|
# shill connection mode.
|
|
mode = self.SUPPORTED_WIFI_STATION_TYPES[station_type]
|
|
|
|
if hidden_network:
|
|
logging.info('Configuring %s as a hidden network.', ssid)
|
|
if not self.configure_wifi_service(
|
|
ssid, security, save_credentials=save_credentials,
|
|
station_type=station_type, hidden_network=True,
|
|
autoconnect=autoconnect):
|
|
return (False, discovery_time, association_time,
|
|
configuration_time,
|
|
'FAIL(Failed to configure hidden SSID)')
|
|
|
|
logging.info('Configured hidden service: %s', ssid)
|
|
|
|
|
|
logging.info('Discovering...')
|
|
discovery_params = {self.SERVICE_PROPERTY_TYPE: 'wifi',
|
|
self.SERVICE_PROPERTY_NAME: ssid,
|
|
self.SERVICE_PROPERTY_SECURITY_CLASS: security,
|
|
self.SERVICE_PROPERTY_MODE: mode}
|
|
while time.time() - start_time < discovery_timeout_seconds:
|
|
discovery_time = time.time() - start_time
|
|
service_object = self.find_matching_service(discovery_params)
|
|
if service_object:
|
|
try:
|
|
service_properties = service_object.GetProperties(
|
|
utf8_strings=True)
|
|
except dbus.exceptions.DBusException:
|
|
# This usually means the service handle has become invalid.
|
|
# Which is sort of like not getting a handle back from
|
|
# find_matching_service in the first place.
|
|
continue
|
|
strength = self.dbus2primitive(
|
|
service_properties[self.SERVICE_PROPERTY_STRENGTH])
|
|
if strength > 0:
|
|
logging.info('Discovered service: %s. Strength: %r.',
|
|
ssid, strength)
|
|
break
|
|
|
|
# This is spammy, but shill handles that for us.
|
|
self.manager.RequestScan('wifi')
|
|
time.sleep(self.POLLING_INTERVAL_SECONDS)
|
|
else:
|
|
return (False, discovery_time, association_time,
|
|
configuration_time, 'FAIL(Discovery timed out)')
|
|
|
|
# At this point, we know |service| is in the service list. Attempt
|
|
# to connect it, and watch the states roll by.
|
|
logging.info('Connecting...')
|
|
try:
|
|
for service_property, value in security_parameters.iteritems():
|
|
service_object.SetProperty(service_property, value)
|
|
if guid is not None:
|
|
service_object.SetProperty(self.SERVICE_PROPERTY_GUID, guid)
|
|
if autoconnect is not None:
|
|
service_object.SetProperty(self.SERVICE_PROPERTY_AUTOCONNECT,
|
|
autoconnect)
|
|
service_object.Connect()
|
|
logging.info('Called connect on service')
|
|
except dbus.exceptions.DBusException, e:
|
|
logging.error('Caught an error while trying to connect: %s',
|
|
e.get_dbus_message())
|
|
return (False, discovery_time, association_time,
|
|
configuration_time, 'FAIL(Failed to call connect)')
|
|
|
|
logging.info('Associating...')
|
|
result = self.wait_for_property_in(
|
|
service_object,
|
|
self.SERVICE_PROPERTY_STATE,
|
|
self.SERVICE_CONNECTED_STATES + ['configuration'],
|
|
association_timeout_seconds)
|
|
(successful, _, association_time) = result
|
|
if not successful:
|
|
return (False, discovery_time, association_time,
|
|
configuration_time, 'FAIL(Association timed out)')
|
|
|
|
logging.info('Associated with service: %s', ssid)
|
|
|
|
logging.info('Configuring...')
|
|
result = self.wait_for_property_in(
|
|
service_object,
|
|
self.SERVICE_PROPERTY_STATE,
|
|
self.SERVICE_CONNECTED_STATES,
|
|
configuration_timeout_seconds)
|
|
(successful, _, configuration_time) = result
|
|
if not successful:
|
|
return (False, discovery_time, association_time,
|
|
configuration_time, 'FAIL(Configuration timed out)')
|
|
|
|
logging.info('Configured service: %s', ssid)
|
|
|
|
# Great success!
|
|
logging.info('Connected to WiFi service.')
|
|
return (True, discovery_time, association_time, configuration_time,
|
|
'SUCCESS(Connection successful)')
|
|
|
|
|
|
def disconnect_from_wifi_network(self, ssid, timeout=None):
|
|
"""Disconnect from the specified WiFi network.
|
|
|
|
Method will succeed if it observes the specified network in the idle
|
|
state after calling Disconnect.
|
|
|
|
@param ssid string name of network to disconnect.
|
|
@param timeout float number of seconds to wait for idle.
|
|
@return tuple(success, duration, reason) where:
|
|
success is a bool (True on success).
|
|
duration is a float number of seconds the operation took.
|
|
reason is a string containing an informative error on failure.
|
|
|
|
"""
|
|
if timeout is None:
|
|
timeout = self.SERVICE_DISCONNECT_TIMEOUT
|
|
service_description = {self.SERVICE_PROPERTY_TYPE: 'wifi',
|
|
self.SERVICE_PROPERTY_NAME: ssid}
|
|
service = self.find_matching_service(service_description)
|
|
if service is None:
|
|
return (False,
|
|
0.0,
|
|
'Failed to disconnect from %s, service not found.' % ssid)
|
|
|
|
service.Disconnect()
|
|
result = self.wait_for_property_in(service,
|
|
self.SERVICE_PROPERTY_STATE,
|
|
('idle',),
|
|
timeout)
|
|
(successful, final_state, duration) = result
|
|
message = 'Success.'
|
|
if not successful:
|
|
message = ('Failed to disconnect from %s, '
|
|
'timed out in state: %s.' % (ssid, final_state))
|
|
return (successful, duration, message)
|
|
|
|
|
|
def configure_bgscan(self, interface, method=None, short_interval=None,
|
|
long_interval=None, signal=None):
|
|
"""Configures bgscan parameters for shill and wpa_supplicant.
|
|
|
|
@param interface string name of interface to configure (e.g. 'mlan0').
|
|
@param method string bgscan method (e.g. 'none').
|
|
@param short_interval int short scanning interval.
|
|
@param long_interval int normal scanning interval.
|
|
@param signal int signal threshold.
|
|
|
|
"""
|
|
device = self.find_object('Device', {'Name': interface})
|
|
if device is None:
|
|
logging.error('No device found with name: %s', interface)
|
|
return False
|
|
|
|
attributes = {'ScanInterval': (dbus.UInt16, long_interval),
|
|
'BgscanMethod': (dbus.String, method),
|
|
'BgscanShortInterval': (dbus.UInt16, short_interval),
|
|
'BgscanSignalThreshold': (dbus.Int32, signal)}
|
|
for k, (type_cast, value) in attributes.iteritems():
|
|
if value is None:
|
|
continue
|
|
|
|
# 'default' is defined in:
|
|
# client/common_lib/cros/network/xmlrpc_datatypes.py
|
|
# but we don't have access to that file here.
|
|
if value == 'default':
|
|
device.ClearProperty(k)
|
|
else:
|
|
device.SetProperty(k, type_cast(value))
|
|
return True
|
|
|
|
|
|
def get_active_wifi_SSIDs(self):
|
|
"""@return list of string SSIDs with at least one BSS we've scanned."""
|
|
properties = self.manager.GetProperties(utf8_strings=True)
|
|
services = [self.get_dbus_object(self.DBUS_TYPE_SERVICE, path)
|
|
for path in properties[self.MANAGER_PROPERTY_SERVICES]]
|
|
wifi_services = []
|
|
for service in services:
|
|
try:
|
|
service_properties = self.dbus2primitive(service.GetProperties(
|
|
utf8_strings=True))
|
|
except dbus.exceptions.DBusException:
|
|
pass # Probably the service disappeared before GetProperties().
|
|
logging.debug('Considering service with properties: %r',
|
|
service_properties)
|
|
service_type = service_properties[self.SERVICE_PROPERTY_TYPE]
|
|
strength = service_properties[self.SERVICE_PROPERTY_STRENGTH]
|
|
if service_type == 'wifi' and strength > 0:
|
|
# Note that this may cause terrible things if the SSID
|
|
# is not a valid ASCII string.
|
|
ssid = service_properties[self.SERVICE_PROPERTY_HEX_SSID]
|
|
logging.info('Found active WiFi service: %s', ssid)
|
|
wifi_services.append(ssid.decode('hex'))
|
|
return wifi_services
|
|
|
|
|
|
def wait_for_service_states(self, ssid, states, timeout_seconds):
|
|
"""Wait for a service (ssid) to achieve one of a number of states.
|
|
|
|
@param ssid string name of network for whose state we're waiting.
|
|
@param states tuple states for which to wait.
|
|
@param timeout_seconds seconds to wait for property to be achieved
|
|
@return tuple(successful, final_value, duration)
|
|
where successful is True iff we saw one of |states|, final_value
|
|
is the final state we saw, and duration is how long we waited to
|
|
see that value.
|
|
|
|
"""
|
|
discovery_params = {self.SERVICE_PROPERTY_TYPE: 'wifi',
|
|
self.SERVICE_PROPERTY_NAME: ssid}
|
|
start_time = time.time()
|
|
try:
|
|
# Find a matching service in any state (only_visible=False) to
|
|
# make it possible to detect the state of services that are not
|
|
# visible because they're not in a connected state.
|
|
service_object = utils.poll_for_condition(
|
|
condition=lambda: self.find_matching_service(
|
|
discovery_params, only_visible=False),
|
|
timeout=timeout_seconds,
|
|
sleep_interval=self.POLLING_INTERVAL_SECONDS,
|
|
desc='Find a matching service to the discovery params')
|
|
|
|
return self.wait_for_property_in(
|
|
service_object,
|
|
self.SERVICE_PROPERTY_STATE,
|
|
states,
|
|
timeout_seconds - (time.time() - start_time))
|
|
|
|
# poll_for_condition timed out
|
|
except utils.TimeoutError:
|
|
logging.error('Timed out waiting for %s states', ssid)
|
|
return False, 'unknown', timeout_seconds
|