Home | History | Annotate | Download | only in ap_configurators
      1 # Copyright 2016 The Chromium 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 binascii
      6 import copy
      7 import logging
      8 import os
      9 import pprint
     10 import re
     11 import time
     12 import xmlrpclib
     13 import json
     14 import urllib2
     15 import time
     16 
     17 import ap_spec
     18 import web_driver_core_helpers
     19 
     20 from autotest_lib.client.common_lib import error
     21 from autotest_lib.client.common_lib import global_config
     22 from autotest_lib.client.common_lib.cros.network import ap_constants
     23 from autotest_lib.client.common_lib.cros.network import xmlrpc_datatypes
     24 from autotest_lib.client.common_lib.cros.network import xmlrpc_security_types
     25 from autotest_lib.server.cros.ap_configurators import ap_configurator
     26 
     27 try:
     28   from selenium import webdriver
     29 except ImportError:
     30   raise ImportError('Could not locate the webdriver package. '
     31                     'Did you emerge it into your chroot?')
     32 
     33 
     34 class DynamicAPConfigurator(web_driver_core_helpers.WebDriverCoreHelpers,
     35                             ap_configurator.APConfiguratorAbstract):
     36     """Base class for objects to configure access points using webdriver."""
     37 
     38 
     39     def __init__(self, ap_config):
     40         """Construct a DynamicAPConfigurator.
     41 
     42         @param ap_config: information from the configuration file
     43         @param set_ap_spec: APSpec object that when passed will set all
     44                             of the configuration options
     45 
     46         """
     47         super(DynamicAPConfigurator, self).__init__()
     48         rpm_frontend_server = global_config.global_config.get_config_value(
     49                 'CROS', 'rpm_frontend_uri')
     50         self.rpm_client = xmlrpclib.ServerProxy(
     51                 rpm_frontend_server, verbose=False)
     52 
     53         # Load the data for the config file
     54         # The url is the actual IP.
     55         # TODO: Revert this after setting up local dns in chaos.
     56         self.admin_interface_url = ap_config.get_admin_ip()
     57         self.class_name = ap_config.get_class()
     58         self._short_name = ap_config.get_model()
     59         self.mac_address = ap_config.get_wan_mac()
     60         self.host_name = ap_config.get_wan_host()
     61         # Get corresponding PDU from host name.
     62         self.pdu = re.sub('host\d+', 'rpm1', self.host_name) + '.cros'
     63         self.config_data = ap_config
     64 
     65         name_dict = {'Router name': self._short_name,
     66                      'Controller class': self.class_name,
     67                      '2.4 GHz MAC Address': ap_config.get_bss(),
     68                      '5 GHz MAC Address': ap_config.get_bss5(),
     69                      'Hostname': ap_config.get_wan_host()}
     70 
     71         self._name = str('%s' % pprint.pformat(name_dict))
     72 
     73         # Set a default band, this can be overriden by the subclasses
     74         self.current_band = ap_spec.BAND_2GHZ
     75         self._ssid = None
     76 
     77         # Diagnostic members
     78         self._command_list = []
     79         self._screenshot_list = []
     80         self._traceback = None
     81 
     82         self.driver_connection_established = False
     83         self.router_on = False
     84         self._configuration_success = ap_constants.CONFIG_SUCCESS
     85         self._webdriver_port = 9515
     86 
     87         self.ap_spec = None
     88         self.webdriver_hostname = None
     89 
     90     def __del__(self):
     91         """Cleanup webdriver connections"""
     92         try:
     93             self.driver.close()
     94         except:
     95             pass
     96 
     97 
     98     def __str__(self):
     99         """Prettier display of the object"""
    100         return('AP Name: %s\n'
    101                'BSS: %s\n'
    102                'SSID: %s\n'
    103                'Short name: %s' % (self.name, self.get_bss(),
    104                self._ssid, self.short_name))
    105 
    106 
    107     @property
    108     def configurator_type(self):
    109         """Returns the configurator type."""
    110         return ap_spec.CONFIGURATOR_DYNAMIC
    111 
    112 
    113     @property
    114     def ssid(self):
    115         """Returns the SSID."""
    116         return self._ssid
    117 
    118 
    119     def add_item_to_command_list(self, method, args, page, priority):
    120         """
    121         Adds commands to be executed against the AP web UI.
    122 
    123         @param method: the method to run
    124         @param args: the arguments for the method you want executed
    125         @param page: the page on the web ui where to run the method against
    126         @param priority: the priority of the method
    127 
    128         """
    129         self._command_list.append({'method': method,
    130                                    'args': copy.copy(args),
    131                                    'page': page,
    132                                    'priority': priority})
    133 
    134 
    135     def reset_command_list(self):
    136         """Resets all internal command state."""
    137         logging.error('Dumping command list %s', self._command_list)
    138         self._command_list = []
    139         self.destroy_driver_connection()
    140 
    141 
    142     def save_screenshot(self):
    143         """
    144         Stores and returns the screenshot as a base 64 encoded string.
    145 
    146         @returns the screenshot as a base 64 encoded string; if there was
    147         an error saving the screenshot None is returned.
    148 
    149         """
    150         screenshot = None
    151         if self.driver_connection_established:
    152             try:
    153                 # driver.get_screenshot_as_base64 takes a screenshot that is
    154                 # whatever the size of the window is.  That can be anything,
    155                 # forcing a size that will get everything we care about.
    156                 window_size = self.driver.get_window_size()
    157                 self.driver.set_window_size(2000, 5000)
    158                 screenshot = self.driver.get_screenshot_as_base64()
    159                 self.driver.set_window_size(window_size['width'],
    160                                             window_size['height'])
    161             except Exception as e:
    162                 # The messages differ based on the webdriver version
    163                 logging.error('Getting the screenshot failed. %s', e)
    164                 # TODO (krisr) this too can fail with an exception.
    165                 self._check_for_alert_in_message(str(e),
    166                                                  self._handler(None))
    167                 logging.error('Alert was handled.')
    168                 screenshot = None
    169             if screenshot:
    170                 self._screenshot_list.append(screenshot)
    171         return screenshot
    172 
    173 
    174     def get_all_screenshots(self):
    175         """Returns a list of screenshots."""
    176         return self._screenshot_list
    177 
    178 
    179     def clear_screenshot_list(self):
    180         """Clear the list of currently stored screenshots."""
    181         self._screenshot_list = []
    182 
    183 
    184     def _save_all_pages(self):
    185         """Iterate through AP pages, saving screenshots"""
    186         self.establish_driver_connection()
    187         if not self.driver_connection_established:
    188             logging.error('Unable to establish webdriver connection to '
    189                           'retrieve screenshots.')
    190             return
    191         for page in range(1, self.get_number_of_pages() + 1):
    192             self.navigate_to_page(page)
    193             self.save_screenshot()
    194 
    195 
    196     def _write_screenshots(self, filename, outputdir):
    197         """
    198         Writes screenshots to filename in outputdir
    199 
    200         @param filename: a string prefix for screenshot filenames
    201         @param outputdir: a string directory name to save screenshots
    202 
    203         """
    204         for (i, image) in enumerate(self.get_all_screenshots()):
    205             path = os.path.join(outputdir,
    206                                 str('%s_%d.png' % (filename, (i + 1))))
    207             with open(path, 'wb') as f:
    208                 f.write(image.decode('base64'))
    209 
    210 
    211     @property
    212     def traceback(self):
    213         """
    214         Returns the traceback of a configuration error as a string.
    215 
    216         Note that if configuration_success returns CONFIG_SUCCESS this will
    217         be none.
    218 
    219         """
    220         return self._traceback
    221 
    222 
    223     @traceback.setter
    224     def traceback(self, value):
    225         """
    226         Set the traceback.
    227 
    228         If the APConfigurator crashes use this to store what the traceback
    229         was as a string.  It can be used later to debug configurator errors.
    230 
    231         @param value: a string representation of the exception traceback
    232 
    233         """
    234         self._traceback = value
    235 
    236 
    237     def check_webdriver_ready(self, webdriver_hostname, webdriver_port):
    238         """Checks if webdriver binary is installed and running.
    239 
    240         @param webdriver_hostname: locked webdriver instance
    241         @param webdriver_port: port of the webdriver server
    242 
    243         @returns a string: the webdriver instance running on port.
    244 
    245         @raises TestError: Webdriver is not running.
    246         """
    247         if webdriver_hostname is 'localhost':
    248             address = 'localhost'
    249         else:
    250             address = webdriver_hostname + '.cros'
    251         url = 'http://%s:%d/session' % (address, webdriver_port)
    252         req = urllib2.Request(url, '{"desiredCapabilities":{}}')
    253         try:
    254             time.sleep(20)
    255             response = urllib2.urlopen(req)
    256             json_dict = json.loads(response.read())
    257             if json_dict['status'] == 0:
    258                 # Connection was successful, close the session
    259                 session_url = os.path.join(url, json_dict['sessionId'])
    260                 req = urllib2.Request(session_url)
    261                 req.get_method = lambda: 'DELETE'
    262                 response = urllib2.urlopen(req)
    263                 logging.info('Webdriver connection established to server %s',
    264                             address)
    265                 return address
    266         except:
    267             err = 'Could not establish connection: %s', webdriver_hostname
    268             raise error.TestError(err)
    269 
    270 
    271     @property
    272     def webdriver_port(self):
    273         """Returns the webdriver port."""
    274         return self._webdriver_port
    275 
    276 
    277     @webdriver_port.setter
    278     def webdriver_port(self, value):
    279         """
    280         Set the webdriver server port.
    281 
    282         @param value: the port number of the webdriver server
    283 
    284         """
    285         self._webdriver_port = value
    286 
    287 
    288     @property
    289     def name(self):
    290         """Returns a string to describe the router."""
    291         return self._name
    292 
    293 
    294     @property
    295     def short_name(self):
    296         """Returns a short string to describe the router."""
    297         return self._short_name
    298 
    299 
    300     def get_number_of_pages(self):
    301         """Returns the number of web pages used to configure the router.
    302 
    303         Note: This is used internally by apply_settings, and this method must be
    304               implemented by the derived class.
    305 
    306         Note: The derived class must implement this method.
    307 
    308         """
    309         raise NotImplementedError
    310 
    311 
    312     def get_supported_bands(self):
    313         """Returns a list of dictionaries describing the supported bands.
    314 
    315         Example: returned is a dictionary of band and a list of channels. The
    316                  band object returned must be one of those defined in the
    317                  __init___ of this class.
    318 
    319         supported_bands = [{'band' : self.band_2GHz,
    320                             'channels' : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]},
    321                            {'band' : ap_spec.BAND_5GHZ,
    322                             'channels' : [26, 40, 44, 48, 149, 153, 165]}]
    323 
    324         Note: The derived class must implement this method.
    325 
    326         @return a list of dictionaries as described above
    327 
    328         """
    329         raise NotImplementedError
    330 
    331 
    332     def get_bss(self):
    333         """Returns the bss of the AP."""
    334         if self.current_band == ap_spec.BAND_2GHZ:
    335             return self.config_data.get_bss()
    336         else:
    337             return self.config_data.get_bss5()
    338 
    339 
    340     def _get_channel_popup_position(self, channel):
    341         """Internal method that converts a channel value to a popup position."""
    342         supported_bands = self.get_supported_bands()
    343         for band in supported_bands:
    344             if band['band'] == self.current_band:
    345                 return band['channels'].index(channel)
    346         raise RuntimeError('The channel passed %d to the band %s is not '
    347                            'supported.' % (channel, band))
    348 
    349 
    350     def get_supported_modes(self):
    351         """
    352         Returns a list of dictionaries describing the supported modes.
    353 
    354         Example: returned is a dictionary of band and a list of modes. The band
    355                  and modes objects returned must be one of those defined in the
    356                  __init___ of this class.
    357 
    358         supported_modes = [{'band' : ap_spec.BAND_2GHZ,
    359                             'modes' : [mode_b, mode_b | mode_g]},
    360                            {'band' : ap_spec.BAND_5GHZ,
    361                             'modes' : [mode_a, mode_n, mode_a | mode_n]}]
    362 
    363         Note: The derived class must implement this method.
    364 
    365         @return a list of dictionaries as described above
    366 
    367         """
    368         raise NotImplementedError
    369 
    370 
    371     def get_supported_channel_widths(self):
    372         """
    373         Returns a dictionary describing supported channel widths based on
    374         band and mode.
    375 
    376         Example:
    377         channel_width_2GHZ = [{ap_spec.MODE_B : [20]},
    378                               {ap_spec.MODE_B|ap_spec.MODE_G : [20,40]},
    379                               {ap_spec.MODE_N : [20]}]
    380 
    381         channel_width_5GHZ = [{ap_spec.MODE_N : [20,40]},
    382                               {ap_spec.MODE_AC : [20,40,80]}]
    383 
    384         channel_width = {ap_spec.BAND_2GHZ : channel_width_2GHZ,
    385                          ap_spec.BAND_5GHZ : channel_width_5GHZ}
    386 
    387         Note: The derived class implements this method and returns
    388         channel_width.
    389 
    390         @return a dictionary as described above.
    391         """
    392         raise NotImplementedError
    393 
    394 
    395     def is_visibility_supported(self):
    396         """
    397         Returns if AP supports setting the visibility (SSID broadcast).
    398 
    399         @return True if supported; False otherwise.
    400 
    401         """
    402         return True
    403 
    404 
    405     def is_radio_switchable(self):
    406         """
    407         Returns if AP supports setting the radio ON/OFF.
    408 
    409         @return True if supported; False otherwise.
    410         """
    411         return True
    412 
    413 
    414     def is_band_and_channel_supported(self, band, channel):
    415         """
    416         Returns if a given band and channel are supported.
    417 
    418         @param band: the band to check if supported
    419         @param channel: the channel to check if supported
    420 
    421         @return True if combination is supported; False otherwise.
    422 
    423         """
    424         bands = self.get_supported_bands()
    425         for current_band in bands:
    426             if (current_band['band'] == band and
    427                 channel in current_band['channels']):
    428                 return True
    429         return False
    430 
    431 
    432     def is_security_mode_supported(self, security_mode):
    433         """
    434         Returns if a given security_type is supported.
    435 
    436         Note: The derived class must implement this method.
    437 
    438         @param security_mode: one of the following modes:
    439                          self.security_disabled,
    440                          self.security_wep,
    441                          self.security_wpapsk,
    442                          self.security_wpa2psk
    443 
    444         @return True if the security mode is supported; False otherwise.
    445 
    446         """
    447         raise NotImplementedError
    448 
    449 
    450     def is_spec_supported(self, spec):
    451         """
    452         Returns if a given spec is supported by the router.
    453 
    454         @param spec: an instance of the
    455         autotest_lib.server.cros.ap_configurators.APSpec class.
    456 
    457         @return: True if supported. False otherwise.
    458         """
    459         return True
    460 
    461 
    462     def navigate_to_page(self, page_number):
    463         """
    464         Navigates to the page corresponding to the given page number.
    465 
    466         This method performs the translation between a page number and a url to
    467         load. This is used internally by apply_settings.
    468 
    469         Note: The derived class must implement this method.
    470 
    471         @param page_number: page number of the page to load
    472 
    473         """
    474         raise NotImplementedError
    475 
    476 
    477     def power_cycle_router_up(self):
    478         """Queues the power cycle up command."""
    479         self.add_item_to_command_list(self._power_cycle_router_up, (), 1, 0)
    480 
    481 
    482     def _power_cycle_router_up(self):
    483         """Turns the ap off and then back on again."""
    484         self.rpm_client.queue_request(self.host_name, 'OFF')
    485         self.router_on = False
    486         self._power_up_router()
    487 
    488 
    489     def power_down_router(self):
    490         """Queues up the power down command."""
    491         self.add_item_to_command_list(self._power_down_router, (), 1, 999)
    492 
    493 
    494     def _power_down_router(self):
    495         """Turns off the power to the ap via the power strip."""
    496         self.check_pdu_status()
    497         self.rpm_client.queue_request(self.host_name, 'OFF')
    498         self.router_on = False
    499 
    500 
    501     def power_up_router(self):
    502         """Queues up the power up command."""
    503         self.add_item_to_command_list(self._power_up_router, (), 1, 0)
    504 
    505 
    506     def _power_up_router(self):
    507         """
    508         Turns on the power to the ap via the power strip.
    509 
    510         This method returns once it can navigate to a web page of the ap UI.
    511 
    512         """
    513         if self.router_on:
    514             return
    515         self.check_pdu_status()
    516         self.rpm_client.queue_request(self.host_name, 'ON')
    517         self.establish_driver_connection()
    518         # Depending on the response of the webserver for the AP, or lack
    519         # there of, the amount of time navigate_to_page and refresh take
    520         # is indeterminate.  Give the APs 5 minutes of real time and then
    521         # give up.
    522         timeout = time.time() + (5 * 60)
    523         half_way = time.time() + (2.5 * 60)
    524         performed_power_cycle = False
    525         while time.time() < timeout:
    526             try:
    527                 logging.info('Attempting to load page')
    528                 self.navigate_to_page(1)
    529                 logging.debug('Page navigation complete')
    530                 self.router_on = True
    531                 return
    532             # Navigate to page may throw a Selemium error or its own
    533             # RuntimeError depending on the implementation.  Either way we are
    534             # bringing a router back from power off, we need to be patient.
    535             except:
    536                 logging.info('Forcing a page refresh')
    537                 self.driver.refresh()
    538                 logging.info('Waiting for router %s to come back up.',
    539                              self.name)
    540                 # Sometime the APs just don't come up right.
    541                 if not performed_power_cycle and time.time() > half_way:
    542                     logging.info('Cannot connect to AP, forcing cycle')
    543                     self.rpm_client.queue_request(self.host_name, 'CYCLE')
    544                     performed_power_cycle = True
    545                     logging.info('Power cycle complete')
    546         raise RuntimeError('Unable to load admin page after powering on the '
    547                            'router: %s' % self.name)
    548 
    549 
    550     def save_page(self, page_number):
    551         """
    552         Saves the given page.
    553 
    554         Note: The derived class must implement this method.
    555 
    556         @param page_number: Page number of the page to save.
    557 
    558         """
    559         raise NotImplementedError
    560 
    561 
    562     def set_using_ap_spec(self, set_ap_spec, power_up=True):
    563         """
    564         Sets all configurator options.
    565 
    566         @param set_ap_spec: APSpec object
    567 
    568         """
    569         if power_up:
    570             self.power_up_router()
    571         if self.is_visibility_supported():
    572             self.set_visibility(set_ap_spec.visible)
    573         if (set_ap_spec.security == ap_spec.SECURITY_TYPE_WPAPSK or
    574             set_ap_spec.security == ap_spec.SECURITY_TYPE_WPA2PSK or
    575             set_ap_spec.security == ap_spec.SECURITY_TYPE_MIXED):
    576             self.set_security_wpapsk(set_ap_spec.security, set_ap_spec.password)
    577         elif set_ap_spec.security == ap_spec.SECURITY_TYPE_WEP:
    578             self.set_security_wep(set_ap_spec.security, set_ap_spec.password)
    579         else:
    580             self.set_security_disabled()
    581         self.set_band(set_ap_spec.band)
    582         self.set_mode(set_ap_spec.mode)
    583         self.set_channel(set_ap_spec.channel)
    584 
    585         # Update ssid
    586         raw_ssid = '%s_%s_ch%d_%s' % (
    587                 self.short_name,
    588                 ap_spec.mode_string_for_mode(set_ap_spec.mode),
    589                 set_ap_spec.channel,
    590                 set_ap_spec.security)
    591         self._ssid = raw_ssid.replace(' ', '_').replace('.', '_')[:32]
    592         self.set_ssid(self._ssid)
    593         self.ap_spec = set_ap_spec
    594         self.webdriver_hostname = set_ap_spec.webdriver_hostname
    595 
    596     def set_mode(self, mode, band=None):
    597         """
    598         Sets the mode.
    599 
    600         Note: The derived class must implement this method.
    601 
    602         @param mode: must be one of the modes listed in __init__()
    603         @param band: the band to select
    604 
    605         """
    606         raise NotImplementedError
    607 
    608 
    609     def set_radio(self, enabled=True):
    610         """
    611         Turns the radio on and off.
    612 
    613         Note: The derived class must implement this method.
    614 
    615         @param enabled: True to turn on the radio; False otherwise
    616 
    617         """
    618         raise NotImplementedError
    619 
    620 
    621     def set_ssid(self, ssid):
    622         """
    623         Sets the SSID of the wireless network.
    624 
    625         Note: The derived class must implement this method.
    626 
    627         @param ssid: name of the wireless network
    628 
    629         """
    630         raise NotImplementedError
    631 
    632 
    633     def set_channel(self, channel):
    634         """
    635         Sets the channel of the wireless network.
    636 
    637         Note: The derived class must implement this method.
    638 
    639         @param channel: integer value of the channel
    640 
    641         """
    642         raise NotImplementedError
    643 
    644 
    645     def set_band(self, band):
    646         """
    647         Sets the band of the wireless network.
    648 
    649         Currently there are only two possible values for band: 2kGHz and 5kGHz.
    650         Note: The derived class must implement this method.
    651 
    652         @param band: Constant describing the band type
    653 
    654         """
    655         raise NotImplementedError
    656 
    657 
    658     def set_channel_width(self, channel_width):
    659         """
    660         Sets the channel width of the wireless network.
    661 
    662         Note: The derived class must implement this method.
    663 
    664         @param channel_width: integer value of the channel width.
    665         """
    666         raise NotImplementedError
    667 
    668 
    669     def set_security_disabled(self):
    670         """
    671         Disables the security of the wireless network.
    672 
    673         Note: The derived class must implement this method.
    674 
    675         """
    676         raise NotImplementedError
    677 
    678 
    679     def set_security_wep(self, key_value, authentication):
    680         """
    681         Enabled WEP security for the wireless network.
    682 
    683         Note: The derived class must implement this method.
    684 
    685         @param key_value: encryption key to use
    686         @param authentication: one of two supported WEP authentication types:
    687                                open or shared.
    688         """
    689         raise NotImplementedError
    690 
    691 
    692     def set_security_wpapsk(self, security, shared_key, update_interval=1800):
    693         """Enabled WPA using a private security key for the wireless network.
    694 
    695         Note: The derived class must implement this method.
    696 
    697         @param security: Required security for AP configuration
    698         @param shared_key: shared encryption key to use
    699         @param update_interval: number of seconds to wait before updating
    700 
    701         """
    702         raise NotImplementedError
    703 
    704     def set_visibility(self, visible=True):
    705         """Set the visibility of the wireless network.
    706 
    707         Note: The derived class must implement this method.
    708 
    709         @param visible: True for visible; False otherwise
    710 
    711         """
    712         raise NotImplementedError
    713 
    714 
    715     def establish_driver_connection(self):
    716         """Makes a connection to the webdriver service."""
    717         if self.driver_connection_established:
    718             return
    719         # Load the Auth extension
    720 
    721         webdriver_hostname = self.ap_spec.webdriver_hostname
    722         webdriver_address = self.check_webdriver_ready(webdriver_hostname,
    723                                                      self._webdriver_port)
    724         if webdriver_address is None:
    725             raise RuntimeError('Unable to connect to webdriver locally or '
    726                                'via the lab service.')
    727         extension_path = os.path.join(os.path.dirname(__file__),
    728                                       'basic_auth_extension.crx')
    729         f = open(extension_path, 'rb')
    730         base64_extensions = []
    731         base64_ext = (binascii.b2a_base64(f.read()).strip())
    732         base64_extensions.append(base64_ext)
    733         f.close()
    734         webdriver_url = ('http://%s:%d' % (webdriver_address,
    735                                            self._webdriver_port))
    736         capabilities = {'chromeOptions' : {'extensions' : base64_extensions}}
    737         self.driver = webdriver.Remote(webdriver_url, capabilities)
    738         self.driver_connection_established = True
    739 
    740 
    741     def destroy_driver_connection(self):
    742         """Breaks the connection to the webdriver service."""
    743         try:
    744             self.driver.close()
    745         except Exception, e:
    746             logging.debug('Webdriver is crashed, should be respawned %d',
    747                           time.time())
    748         finally:
    749             self.driver_connection_established = False
    750 
    751 
    752     def apply_settings(self):
    753         """Apply all settings to the access point.
    754 
    755         @param skip_success_validation: Boolean to track if method was
    756                                         executed successfully.
    757 
    758         """
    759         self.configuration_success = ap_constants.CONFIG_FAIL
    760         if len(self._command_list) == 0:
    761             return
    762         # If all we are doing is powering down the router, don't mess with
    763         # starting up webdriver.
    764         if (len(self._command_list) == 1 and
    765             self._command_list[0]['method'] == self._power_down_router):
    766             self._command_list[0]['method'](*self._command_list[0]['args'])
    767             self._command_list.pop()
    768             self.destroy_driver_connection()
    769             return
    770 
    771         self.establish_driver_connection()
    772         # Pull items by page and then sort
    773         if self.get_number_of_pages() == -1:
    774             self.fail(msg='Number of pages is not set.')
    775         page_range = range(1, self.get_number_of_pages() + 1)
    776         for i in page_range:
    777             page_commands = [x for x in self._command_list if x['page'] == i]
    778             sorted_page_commands = sorted(page_commands,
    779                                           key=lambda k: k['priority'])
    780             if sorted_page_commands:
    781                 first_command = sorted_page_commands[0]['method']
    782                 # If the first command is bringing the router up or down,
    783                 # do that before navigating to a URL.
    784                 if (first_command == self._power_up_router or
    785                     first_command == self._power_cycle_router_up or
    786                     first_command == self._power_down_router):
    787                     direction = 'up'
    788                     if first_command == self._power_down_router:
    789                         direction = 'down'
    790                     logging.info('Powering %s %s', direction, self.name)
    791                     first_command(*sorted_page_commands[0]['args'])
    792                     sorted_page_commands.pop(0)
    793 
    794                 # If the router is off, no point in navigating
    795                 if not self.router_on:
    796                     if len(sorted_page_commands) == 0:
    797                         # If all that was requested was to power off
    798                         # the router then abort here and do not set the
    799                         # configuration_success bit.  The reason is
    800                         # because if we failed on the configuration that
    801                         # failure should remain since all tests power
    802                         # down the AP when they are done.
    803                         return
    804                     break
    805 
    806                 self.navigate_to_page(i)
    807                 for command in sorted_page_commands:
    808                     command['method'](*command['args'])
    809                 self.save_page(i)
    810         self._command_list = []
    811         self.configuration_success = ap_constants.CONFIG_SUCCESS
    812         self._traceback = None
    813         self.destroy_driver_connection()
    814 
    815 
    816     def get_association_parameters(self):
    817         """
    818         Creates an AssociationParameters from the configured AP.
    819 
    820         @returns AssociationParameters for the configured AP.
    821 
    822         """
    823         security_config = None
    824         if self.ap_spec.security in [ap_spec.SECURITY_TYPE_WPAPSK,
    825                                      ap_spec.SECURITY_TYPE_WPA2PSK]:
    826             # Not all of this is required but doing it just in case.
    827             security_config = xmlrpc_security_types.WPAConfig(
    828                     psk=self.ap_spec.password,
    829                     wpa_mode=xmlrpc_security_types.WPAConfig.MODE_MIXED_WPA,
    830                     wpa_ciphers=[xmlrpc_security_types.WPAConfig.CIPHER_CCMP,
    831                                  xmlrpc_security_types.WPAConfig.CIPHER_TKIP],
    832                     wpa2_ciphers=[xmlrpc_security_types.WPAConfig.CIPHER_CCMP])
    833         return xmlrpc_datatypes.AssociationParameters(
    834                 ssid=self._ssid, security_config=security_config,
    835                 discovery_timeout=45, association_timeout=30,
    836                 configuration_timeout=30, is_hidden=not self.ap_spec.visible)
    837 
    838 
    839     def debug_last_failure(self, outputdir):
    840         """
    841         Write debug information for last AP_CONFIG_FAIL
    842 
    843         @param outputdir: a string directory path for debug files
    844         """
    845         logging.error('Traceback:\n %s', self.traceback)
    846         self._write_screenshots('config_failure', outputdir)
    847         self.clear_screenshot_list()
    848 
    849 
    850     def debug_full_state(self, outputdir):
    851         """
    852         Write debug information for full AP state
    853 
    854         @param outputdir: a string directory path for debug files
    855         """
    856         if self.configuration_success != ap_constants.PDU_FAIL:
    857             self._save_all_pages()
    858             self._write_screenshots('final_configuration', outputdir)
    859             self.clear_screenshot_list()
    860         self.reset_command_list()
    861 
    862 
    863     def store_config_failure(self, trace):
    864         """
    865         Store configuration failure for latter logging
    866 
    867         @param trace: a string traceback of config exception
    868         """
    869         self.save_screenshot()
    870         self._traceback = trace
    871