dynamic_ap_configurator.py revision 9f2cfccd01fe900c74b0fc43d93c2284b445bad5
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
5import binascii
6import copy
7import logging
8import os
9import pprint
10import re
11import time
12import xmlrpclib
13import json
14import urllib2
15import time
16
17import ap_spec
18import web_driver_core_helpers
19
20from autotest_lib.client.common_lib import error
21from autotest_lib.client.common_lib import global_config
22from autotest_lib.client.common_lib.cros.network import ap_constants
23from autotest_lib.client.common_lib.cros.network import xmlrpc_datatypes
24from autotest_lib.client.common_lib.cros.network import xmlrpc_security_types
25from autotest_lib.server.cros.ap_configurators import ap_configurator
26
27try:
28  from selenium import webdriver
29except ImportError:
30  raise ImportError('Could not locate the webdriver package. '
31                    'Did you emerge it into your chroot?')
32
33
34class 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