Home | History | Annotate | Download | only in networking
      1 #!/usr/bin/python2.7
      2 
      3 # Copyright (c) 2015 The Chromium OS Authors. All rights reserved.
      4 # Use of this source code is governed by a BSD-style license that can be
      5 # found in the LICENSE file.
      6 
      7 import argparse
      8 import contextlib2
      9 import errno
     10 import logging
     11 import Queue
     12 import select
     13 import shutil
     14 import signal
     15 import subprocess
     16 import threading
     17 import time
     18 
     19 from SimpleXMLRPCServer import SimpleXMLRPCServer
     20 
     21 from acts import logger
     22 from acts import utils
     23 from acts.controllers import android_device
     24 from acts.controllers import attenuator
     25 from acts.test_utils.wifi import wifi_test_utils as wutils
     26 
     27 
     28 class Map(dict):
     29     """A convenience class that makes dictionary values accessible via dot
     30     operator.
     31 
     32     Example:
     33         >> m = Map({"SSID": "GoogleGuest"})
     34         >> m.SSID
     35         GoogleGuest
     36     """
     37     def __init__(self, *args, **kwargs):
     38         super(Map, self).__init__(*args, **kwargs)
     39         for arg in args:
     40             if isinstance(arg, dict):
     41                 for k, v in arg.items():
     42                     self[k] = v
     43         if kwargs:
     44             for k, v in kwargs.items():
     45                 self[k] = v
     46 
     47 
     48     def __getattr__(self, attr):
     49         return self.get(attr)
     50 
     51 
     52     def __setattr__(self, key, value):
     53         self.__setitem__(key, value)
     54 
     55 
     56 # This is copied over from client/cros/xmlrpc_server.py so that this
     57 # daemon has no autotest dependencies.
     58 class XmlRpcServer(threading.Thread):
     59     """Simple XMLRPC server implementation.
     60 
     61     In theory, Python should provide a sane XMLRPC server implementation as
     62     part of its standard library.  In practice the provided implementation
     63     doesn't handle signals, not even EINTR.  As a result, we have this class.
     64 
     65     Usage:
     66 
     67     server = XmlRpcServer(('localhost', 43212))
     68     server.register_delegate(my_delegate_instance)
     69     server.run()
     70 
     71     """
     72 
     73     def __init__(self, host, port):
     74         """Construct an XmlRpcServer.
     75 
     76         @param host string hostname to bind to.
     77         @param port int port number to bind to.
     78 
     79         """
     80         super(XmlRpcServer, self).__init__()
     81         logging.info('Binding server to %s:%d', host, port)
     82         self._server = SimpleXMLRPCServer((host, port), allow_none=True)
     83         self._server.register_introspection_functions()
     84         self._keep_running = True
     85         self._delegates = []
     86         # Gracefully shut down on signals.  This is how we expect to be shut
     87         # down by autotest.
     88         signal.signal(signal.SIGTERM, self._handle_signal)
     89         signal.signal(signal.SIGINT, self._handle_signal)
     90 
     91 
     92     def register_delegate(self, delegate):
     93         """Register delegate objects with the server.
     94 
     95         The server will automagically look up all methods not prefixed with an
     96         underscore and treat them as potential RPC calls.  These methods may
     97         only take basic Python objects as parameters, as noted by the
     98         SimpleXMLRPCServer documentation.  The state of the delegate is
     99         persisted across calls.
    100 
    101         @param delegate object Python object to be exposed via RPC.
    102 
    103         """
    104         self._server.register_instance(delegate)
    105         self._delegates.append(delegate)
    106 
    107 
    108     def run(self):
    109         """Block and handle many XmlRpc requests."""
    110         logging.info('XmlRpcServer starting...')
    111         with contextlib2.ExitStack() as stack:
    112             for delegate in self._delegates:
    113                 stack.enter_context(delegate)
    114             while self._keep_running:
    115                 try:
    116                     self._server.handle_request()
    117                 except select.error as v:
    118                     # In a cruel twist of fate, the python library doesn't
    119                     # handle this kind of error.
    120                     if v[0] != errno.EINTR:
    121                         raise
    122                 except Exception as e:
    123                     logging.error("Error in handle request: %s", e)
    124         logging.info('XmlRpcServer exited.')
    125 
    126 
    127     def _handle_signal(self, _signum, _frame):
    128         """Handle a process signal by gracefully quitting.
    129 
    130         SimpleXMLRPCServer helpfully exposes a method called shutdown() which
    131         clears a flag similar to _keep_running, and then blocks until it sees
    132         the server shut down.  Unfortunately, if you call that function from
    133         a signal handler, the server will just hang, since the process is
    134         paused for the signal, causing a deadlock.  Thus we are reinventing the
    135         wheel with our own event loop.
    136 
    137         """
    138         self._server.server_close()
    139         self._keep_running = False
    140 
    141 
    142 class XmlRpcServerError(Exception):
    143     """Raised when an error is encountered in the XmlRpcServer."""
    144 
    145 
    146 class AndroidXmlRpcDelegate(object):
    147     """Exposes methods called remotely during WiFi autotests.
    148 
    149     All instance methods of this object without a preceding '_' are exposed via
    150     an XMLRPC server.
    151     """
    152 
    153     WEP40_HEX_KEY_LEN = 10
    154     WEP104_HEX_KEY_LEN = 26
    155     SHILL_DISCONNECTED_STATES = ['idle']
    156     SHILL_CONNECTED_STATES =  ['portal', 'online', 'ready']
    157     DISCONNECTED_SSID = '0x'
    158     DISCOVERY_POLLING_INTERVAL = 1
    159     NUM_ATTEN = 4
    160 
    161 
    162     def __init__(self, serial_number, log_dir, test_station):
    163         """Initializes the ACTS library components.
    164 
    165         @test_station string represting teststation's hostname.
    166         @param serial_number Serial number of the android device to be tested,
    167                None if there is only one device connected to the host.
    168         @param log_dir Path to store output logs of this run.
    169 
    170         """
    171         # Cleanup all existing logs for this device when starting.
    172         shutil.rmtree(log_dir, ignore_errors=True)
    173         logger.setup_test_logger(log_path=log_dir, prefix="ANDROID_XMLRPC")
    174         if not serial_number:
    175             ads = android_device.get_all_instances()
    176             if not ads:
    177                 msg = "No android device found, abort!"
    178                 logging.error(msg)
    179                 raise XmlRpcServerError(msg)
    180             self.ad = ads[0]
    181         elif serial_number in android_device.list_adb_devices():
    182             self.ad = android_device.AndroidDevice(serial_number)
    183         else:
    184             msg = ("Specified Android device %s can't be found, abort!"
    185                    ) % serial_number
    186             logging.error(msg)
    187             raise XmlRpcServerError(msg)
    188         # Even if we find one attenuator assume the rig has attenuators for now.
    189         # With the single IP attenuator, this will be a easy check.
    190         rig_has_attenuator = False
    191         count = 0
    192         for i in range(1, self.NUM_ATTEN + 1):
    193             atten_addr = test_station+'-attenuator-'+'%d' %i
    194             if subprocess.Popen(['ping', '-c', '2', atten_addr],
    195                                  stdout=subprocess.PIPE).communicate()[0]:
    196                 rig_has_attenuator = True
    197                 count = count + 1
    198         if rig_has_attenuator and count == self.NUM_ATTEN:
    199             atten = attenuator.create([{"Address":test_station+'-attenuator-1',
    200                                         "Port":23,
    201                                         "Model":"minicircuits",
    202                                         "InstrumentCount": 1,
    203                                         "Paths":["Attenuator-1"]},
    204                                         {"Address":test_station+'-attenuator-2',
    205                                         "Port":23,
    206                                         "Model":"minicircuits",
    207                                         "InstrumentCount": 1,
    208                                         "Paths":["Attenuator-2"]},
    209                                         {"Address":test_station+'-attenuator-3',
    210                                         "Port":23,
    211                                         "Model":"minicircuits",
    212                                         "InstrumentCount": 1,
    213                                         "Paths":["Attenuator-3"]},
    214                                         {"Address":test_station+'-attenuator-4',
    215                                         "Port":23,
    216                                         "Model":"minicircuits",
    217                                         "InstrumentCount": 1,
    218                                         "Paths":["Attenuator-4"]}])
    219             device = 0
    220             # Set attenuation on all attenuators to 0.
    221             for device in range(len(atten)):
    222                atten[device].set_atten(0)
    223             attenuator.destroy(atten)
    224         elif rig_has_attenuator and count < self.NUM_ATTEN:
    225             msg = 'One or more attenuators are down.'
    226             logging.error(msg)
    227             raise XmlRpcServerError(msg)
    228 
    229 
    230     def __enter__(self):
    231         logging.debug('Bringing up AndroidXmlRpcDelegate.')
    232         self.ad.get_droid()
    233         self.ad.ed.start()
    234         self.ad.start_adb_logcat()
    235         return self
    236 
    237 
    238     def __exit__(self, exception, value, traceback):
    239         logging.debug('Tearing down AndroidXmlRpcDelegate.')
    240         self.ad.terminate_all_sessions()
    241         self.ad.stop_adb_logcat()
    242 
    243 
    244     # Commands start.
    245     def ready(self):
    246         """Confirm that the XMLRPC server is up and ready to serve.
    247 
    248         @return True (always).
    249 
    250         """
    251         logging.debug('ready()')
    252         return True
    253 
    254 
    255     def collect_debug_info(self, test_name):
    256         """Collects appropriate debug information on DUT.
    257 
    258         @param test_name: string name of the test to collect debug information
    259                           for.
    260         """
    261         self.ad.cat_adb_log(test_name, self.test_begin_time)
    262         self.ad.take_bug_report(test_name, self.test_begin_time)
    263 
    264 
    265     def list_controlled_wifi_interfaces(self):
    266         """List all controlled wifi interfaces (just wlan0 for Android). """
    267         return ['wlan0']
    268 
    269 
    270     def set_device_enabled(self, wifi_interface, enabled):
    271         """Enable or disable the WiFi device.
    272 
    273         @param wifi_interface: string name of interface being modified.
    274         @param enabled: boolean; true if this device should be enabled,
    275                 false if this device should be disabled.
    276         @return True if it worked; false, otherwise
    277 
    278         """
    279         return wutils.wifi_toggle_state(self.ad, enabled)
    280 
    281 
    282     def sync_time_to(self, epoch_seconds):
    283         """Sync time on the DUT to |epoch_seconds| from the epoch.
    284 
    285         @param epoch_seconds: float number of seconds from the epoch.
    286 
    287         """
    288         # The adb_host is already doing this; just return True.
    289         return True
    290 
    291 
    292     def clean_profiles(self):
    293         """ Not applicable for Android.
    294         @param profile_name: Ignored.
    295         """
    296         return True
    297 
    298 
    299     def create_profile(self, profile_name):
    300         """ Not applicable for Android.
    301         @param profile_name: Ignored.
    302         """
    303         return True
    304 
    305 
    306     def push_profile(self, profile_name):
    307         """ Not applicable for Android.
    308         @param profile_name: Ignored.
    309         """
    310         return True
    311 
    312 
    313     def remove_profile(self, profile_name):
    314         """ Not applicable for Android.
    315         @param profile_name: Ignored.
    316         """
    317         return True
    318 
    319 
    320     def pop_profile(self, profile_name):
    321         """ Not applicable for Android.
    322         @param profile_name: Ignored.
    323         """
    324         return True
    325 
    326 
    327     def disconnect(self, ssid):
    328         """Attempt to disconnect from the given ssid.
    329 
    330         Blocks until disconnected or operation has timed out.  Returns True iff
    331         disconnect was successful.
    332 
    333         @param ssid string network to disconnect from.
    334         @return bool True on success, False otherwise.
    335 
    336         """
    337         # Android had no explicit disconnect, so let's just forget the network.
    338         return self.delete_entries_for_ssid(ssid)
    339 
    340 
    341     def get_active_wifi_SSIDs(self):
    342         """Get the list of all SSIDs in the current scan results.
    343 
    344         @return list of string SSIDs with at least one BSS we've scanned.
    345 
    346         """
    347         ssids = []
    348         try:
    349             self.ad.droid.wifiStartScan()
    350             self.ad.ed.pop_event('WifiManagerScanResultsAvailable')
    351             scan_results = self.ad.droid.wifiGetScanResults()
    352             for result in scan_results:
    353                 if wutils.WifiEnums.SSID_KEY in result:
    354                     ssids.append(result[wutils.WifiEnums.SSID_KEY])
    355         except Queue.Empty:
    356             logging.error("Scan results available event timed out!")
    357         except Exception as e:
    358             logging.error("Scan results error: %s", e)
    359         finally:
    360             logging.debug("Scan Results: %r", ssids)
    361             return ssids
    362 
    363 
    364     def wait_for_service_states(self, ssid, states, timeout_seconds):
    365         """Wait for SSID to reach one state out of a list of states.
    366 
    367         @param ssid string the network to connect to (e.g. 'GoogleGuest').
    368         @param states tuple the states for which to wait
    369         @param timeout_seconds int seconds to wait for a state
    370 
    371         @return (result, final_state, wait_time) tuple of the result for the
    372                 wait.
    373         """
    374         current_con = self.ad.droid.wifiGetConnectionInfo()
    375         # Check the current state to see if we're connected/disconnected.
    376         if set(states).intersection(set(self.SHILL_CONNECTED_STATES)):
    377             if current_con[wutils.WifiEnums.SSID_KEY] == ssid:
    378                 return True, '', 0
    379             wait_event = 'WifiNetworkConnected'
    380         elif set(states).intersection(set(self.SHILL_DISCONNECTED_STATES)):
    381             if current_con[wutils.WifiEnums.SSID_KEY] == self.DISCONNECTED_SSID:
    382                 return True, '', 0
    383             wait_event = 'WifiNetworkDisconnected'
    384         else:
    385             assert 0, "Unhandled wait states received: %r" % states
    386         final_state = ""
    387         wait_time = -1
    388         result = False
    389         logging.debug(current_con)
    390         try:
    391             self.ad.droid.wifiStartTrackingStateChange()
    392             start_time = utils.get_current_epoch_time()
    393             wait_result = self.ad.ed.pop_event(wait_event, timeout_seconds)
    394             end_time = utils.get_current_epoch_time()
    395             wait_time = (end_time - start_time) / 1000
    396             if wait_event == 'WifiNetworkConnected':
    397                 actual_ssid = wait_result['data'][wutils.WifiEnums.SSID_KEY]
    398                 assert actual_ssid == ssid, ("Expected to connect to %s, but "
    399                         "connected to %s") % (ssid, actual_ssid)
    400             result = True
    401         except Queue.Empty:
    402             logging.error("No state change available yet!")
    403         except Exception as e:
    404             logging.error("State change error: %s", e)
    405         finally:
    406             logging.debug((result, final_state, wait_time))
    407             self.ad.droid.wifiStopTrackingStateChange()
    408             return result, final_state, wait_time
    409 
    410 
    411     def delete_entries_for_ssid(self, ssid):
    412         """Delete all saved entries for an SSID.
    413 
    414         @param ssid string of SSID for which to delete entries.
    415         @return True on success, False otherwise.
    416 
    417         """
    418         try:
    419             wutils.wifi_forget_network(self.ad, ssid)
    420         except Exception as e:
    421             logging.error(e)
    422             return False
    423         return True
    424 
    425 
    426     def connect_wifi(self, raw_params):
    427         """Block and attempt to connect to wifi network.
    428 
    429         @param raw_params serialized AssociationParameters.
    430         @return serialized AssociationResult
    431 
    432         """
    433         # Prepare data objects.
    434         params = Map(raw_params)
    435         params.security_config = Map(raw_params['security_config'])
    436         params.bgscan_config = Map(raw_params['bgscan_config'])
    437         logging.debug('connect_wifi(). Params: %r', params)
    438         network_config = {
    439             "SSID": params.ssid,
    440             "hiddenSSID":  True if params.is_hidden else False
    441         }
    442         assoc_result = {
    443             "discovery_time" : 0,
    444             "association_time" : 0,
    445             "configuration_time" : 0,
    446             "failure_reason" : "None",
    447             "xmlrpc_struct_type_key" : "AssociationResult"
    448         }
    449         duration = lambda: (utils.get_current_epoch_time() - start_time) / 1000
    450         try:
    451             # Verify that the network was found, if the SSID is not hidden.
    452             if not params.is_hidden:
    453                 start_time = utils.get_current_epoch_time()
    454                 found = False
    455                 while duration() < params.discovery_timeout and not found:
    456                     active_ssids = self.get_active_wifi_SSIDs()
    457                     found = params.ssid in active_ssids
    458                     if not found:
    459                         time.sleep(self.DISCOVERY_POLLING_INTERVAL)
    460                 assoc_result["discovery_time"] = duration()
    461                 assert found, ("Could not find %s in scan results: %r") % (
    462                         params.ssid, active_ssids)
    463             result = False
    464             if params.security_config.security == "psk":
    465                 network_config["password"] = params.security_config.psk
    466             elif params.security_config.security == "wep":
    467                 network_config["wepTxKeyIndex"] = params.security_config.wep_default_key
    468                 # Convert all ASCII keys to Hex
    469                 wep_hex_keys = []
    470                 for key in params.security_config.wep_keys:
    471                     if len(key) == self.WEP40_HEX_KEY_LEN or \
    472                        len(key) == self.WEP104_HEX_KEY_LEN:
    473                         wep_hex_keys.append(key)
    474                     else:
    475                         hex_key = ""
    476                         for byte in bytearray(key, 'utf-8'):
    477                             hex_key += '%x' % byte
    478                         wep_hex_keys.append(hex_key)
    479                 network_config["wepKeys"] = wep_hex_keys
    480             # Associate to the network.
    481             self.ad.droid.wifiStartTrackingStateChange()
    482             start_time = utils.get_current_epoch_time()
    483             result = self.ad.droid.wifiConnect(network_config)
    484             assert result, "wifiConnect call failed."
    485             # Verify connection successful and correct.
    486             logging.debug('wifiConnect result: %s. Waiting for connection', result);
    487             timeout = params.association_timeout + params.configuration_timeout
    488             connect_result = self.ad.ed.pop_event(
    489                 wutils.WifiEventNames.WIFI_CONNECTED, timeout)
    490             assoc_result["association_time"] = duration()
    491             actual_ssid = connect_result['data'][wutils.WifiEnums.SSID_KEY]
    492             logging.debug('Connected to SSID: %s', actual_ssid);
    493             assert actual_ssid == params.ssid, ("Expected to connect to %s, "
    494                 "connected to %s") % (params.ssid, actual_ssid)
    495             result = True
    496         except Queue.Empty:
    497             msg = "Failed to connect to %s with %s" % (params.ssid,
    498                 params.security_config.security)
    499             logging.error(msg)
    500             assoc_result["failure_reason"] = msg
    501             result = False
    502         except Exception as e:
    503             msg = e
    504             logging.error(msg)
    505             assoc_result["failure_reason"] = msg
    506             result = False
    507         finally:
    508             assoc_result["success"] = result
    509             logging.debug(assoc_result)
    510             self.ad.droid.wifiStopTrackingStateChange()
    511             return assoc_result
    512 
    513 
    514     def init_test_network_state(self):
    515         """Create a clean slate for tests with respect to remembered networks.
    516 
    517         @return True iff operation succeeded, False otherwise.
    518         """
    519         self.test_begin_time = logger.get_log_line_timestamp()
    520         try:
    521             wutils.wifi_test_device_init(self.ad)
    522             self.ad.ed.clear_all_events()
    523         except AssertionError as e:
    524             logging.error(e)
    525             return False
    526         return True
    527 
    528 
    529 if __name__ == '__main__':
    530     parser = argparse.ArgumentParser(description='Cros Wifi Xml RPC server.')
    531     parser.add_argument('-s', '--serial-number', action='store', default=None,
    532                          help='Serial Number of the device to test.')
    533     parser.add_argument('-l', '--log-dir', action='store', default=None,
    534                          help='Path to store output logs.')
    535     parser.add_argument('-t', '--test-station', action='store', default=None,
    536                          help='The accompaning teststion hostname.')
    537     parser.add_argument('-p', '--port', action='store', default=9989,
    538                         type=int, help='The port number to listen on.')
    539     args = parser.parse_args()
    540     listen_port = args.port
    541     logging.basicConfig(level=logging.DEBUG)
    542     logging.debug("android_xmlrpc_server main...")
    543     logging.debug('xmlrpc instance on port %d' % listen_port)
    544     server = XmlRpcServer('localhost', listen_port)
    545     server.register_delegate(
    546             AndroidXmlRpcDelegate(args.serial_number, args.log_dir,
    547                                   args.test_station))
    548     server.run()
    549