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