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