| # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import binascii |
| import copy |
| import datetime |
| import logging |
| import os |
| import re |
| import time |
| import xmlrpclib |
| |
| import ap_spec |
| import download_chromium_prebuilt as prebuilt |
| import web_driver_core_helpers |
| |
| from autotest_lib.client.common_lib import global_config |
| from autotest_lib.client.common_lib.cros.network import chaos_constants |
| from autotest_lib.client.common_lib.cros.network import xmlrpc_datatypes |
| from autotest_lib.client.common_lib.cros.network import xmlrpc_security_types |
| from autotest_lib.server.cros.chaos_ap_configurators import ap_configurator |
| |
| try: |
| from selenium import webdriver |
| except ImportError: |
| raise ImportError('Could not locate the webdriver package. ' |
| 'Did you emerge it into your chroot?') |
| |
| |
| class DynamicAPConfigurator(web_driver_core_helpers.WebDriverCoreHelpers, |
| ap_configurator.APConfiguratorAbstract): |
| """Base class for objects to configure access points using webdriver.""" |
| |
| |
| def __init__(self, ap_config): |
| """Construct a DynamicAPConfigurator. |
| |
| @param ap_config: information from the configuration file |
| @param set_ap_spec: APSpec object that when passed will set all |
| of the configuration options |
| |
| """ |
| super(DynamicAPConfigurator, self).__init__() |
| rpm_frontend_server = global_config.global_config.get_config_value( |
| 'CROS', 'rpm_frontend_uri') |
| self.rpm_client = xmlrpclib.ServerProxy( |
| rpm_frontend_server, verbose=False) |
| |
| # Load the data for the config file |
| self.admin_interface_url = ap_config.get_admin() |
| self.class_name = ap_config.get_class() |
| self._short_name = ap_config.get_model() |
| self.mac_address = ap_config.get_wan_mac() |
| self.host_name = ap_config.get_wan_host() |
| # Get corresponding PDU from host name. |
| self.pdu = re.sub('host\d+', 'rpm1', self.host_name) + '.cros' |
| self.config_data = ap_config |
| |
| self._name = ('Router name: %s, Controller class: %s,' |
| 'MAC Address: %s' % (self._short_name, self.class_name, |
| self.mac_address)) |
| |
| # Set a default band, this can be overriden by the subclasses |
| self.current_band = ap_spec.BAND_2GHZ |
| self._ssid = None |
| |
| # Diagnostic members |
| self._command_list = [] |
| self._screenshot_list = [] |
| self._traceback = None |
| |
| self.driver_connection_established = False |
| self.router_on = False |
| self._configuration_success = chaos_constants.CONFIG_SUCCESS |
| self._webdriver_port = 9515 |
| |
| self.ap_spec = None |
| |
| |
| def __del__(self): |
| """Cleanup webdriver connections""" |
| try: |
| self.driver.close() |
| except: |
| pass |
| |
| |
| def __str__(self): |
| """Prettier display of the object""" |
| return('AP Name: %s\n' |
| 'BSS: %s\n' |
| 'SSID: %s\n' |
| 'Short name: %s' % (self.name, self.get_bss(), |
| self._ssid, self.short_name)) |
| |
| |
| @property |
| def configurator_type(self): |
| """Returns the configurator type.""" |
| return ap_spec.CONFIGURATOR_DYNAMIC |
| |
| |
| @property |
| def ssid(self): |
| """Returns the SSID.""" |
| return self._ssid |
| |
| |
| def add_item_to_command_list(self, method, args, page, priority): |
| """ |
| Adds commands to be executed against the AP web UI. |
| |
| @param method: the method to run |
| @param args: the arguments for the method you want executed |
| @param page: the page on the web ui where to run the method against |
| @param priority: the priority of the method |
| |
| """ |
| self._command_list.append({'method': method, |
| 'args': copy.copy(args), |
| 'page': page, |
| 'priority': priority}) |
| |
| |
| def reset_command_list(self): |
| """Resets all internal command state.""" |
| logging.error('Dumping command list %s', self._command_list) |
| self._command_list = [] |
| self.destroy_driver_connection() |
| |
| |
| def save_screenshot(self): |
| """ |
| Stores and returns the screenshot as a base 64 encoded string. |
| |
| @returns the screenshot as a base 64 encoded string; if there was |
| an error saving the screenshot None is returned. |
| |
| """ |
| screenshot = None |
| if self.driver_connection_established: |
| try: |
| # driver.get_screenshot_as_base64 takes a screenshot that is |
| # whatever the size of the window is. That can be anything, |
| # forcing a size that will get everything we care about. |
| window_size = self.driver.get_window_size() |
| self.driver.set_window_size(2000, 5000) |
| screenshot = self.driver.get_screenshot_as_base64() |
| self.driver.set_window_size(window_size['width'], |
| window_size['height']) |
| except Exception as e: |
| # The messages differ based on the webdriver version |
| logging.error('Getting the screenshot failed. %s', e) |
| # TODO (krisr) this too can fail with an exception. |
| self._check_for_alert_in_message(str(e), |
| self._handler(None)) |
| logging.error('Alert was handled.') |
| screenshot = None |
| if screenshot: |
| self._screenshot_list.append(screenshot) |
| return screenshot |
| |
| |
| def get_all_screenshots(self): |
| """Returns a list of screenshots.""" |
| return self._screenshot_list |
| |
| |
| def clear_screenshot_list(self): |
| """Clear the list of currently stored screenshots.""" |
| self._screenshot_list = [] |
| |
| |
| def _save_all_pages(self): |
| """Iterate through AP pages, saving screenshots""" |
| self.establish_driver_connection() |
| if not self.driver_connection_established: |
| logging.error('Unable to establish webdriver connection to ' |
| 'retrieve screenshots.') |
| return |
| for page in range(1, self.get_number_of_pages() + 1): |
| self.navigate_to_page(page) |
| self.save_screenshot() |
| |
| |
| def _write_screenshots(self, filename, outputdir): |
| """ |
| Writes screenshots to filename in outputdir |
| |
| @param filename: a string prefix for screenshot filenames |
| @param outputdir: a string directory name to save screenshots |
| |
| """ |
| for (i, image) in enumerate(self.get_all_screenshots()): |
| path = os.path.join(outputdir, |
| str('%s_%d.png' % (filename, (i + 1)))) |
| with open(path, 'wb') as f: |
| f.write(image.decode('base64')) |
| |
| |
| @property |
| def traceback(self): |
| """ |
| Returns the traceback of a configuration error as a string. |
| |
| Note that if configuration_success returns CONFIG_SUCCESS this will |
| be none. |
| |
| """ |
| return self._traceback |
| |
| |
| @traceback.setter |
| def traceback(self, value): |
| """ |
| Set the traceback. |
| |
| If the APConfigurator crashes use this to store what the traceback |
| was as a string. It can be used later to debug configurator errors. |
| |
| @param value: a string representation of the exception traceback |
| |
| """ |
| self._traceback = value |
| |
| |
| @property |
| def webdriver_port(self): |
| """Returns the webdriver port.""" |
| return self._webdriver_port |
| |
| |
| @webdriver_port.setter |
| def webdriver_port(self, value): |
| """ |
| Set the webdriver server port. |
| |
| @param value: the port number of the webdriver server |
| |
| """ |
| self._webdriver_port = value |
| |
| |
| @property |
| def name(self): |
| """Returns a string to describe the router.""" |
| return self._name |
| |
| |
| @property |
| def short_name(self): |
| """Returns a short string to describe the router.""" |
| return self._short_name |
| |
| |
| def get_number_of_pages(self): |
| """Returns the number of web pages used to configure the router. |
| |
| Note: This is used internally by apply_settings, and this method must be |
| implemented by the derived class. |
| |
| Note: The derived class must implement this method. |
| |
| """ |
| raise NotImplementedError |
| |
| |
| def get_supported_bands(self): |
| """Returns a list of dictionaries describing the supported bands. |
| |
| Example: returned is a dictionary of band and a list of channels. The |
| band object returned must be one of those defined in the |
| __init___ of this class. |
| |
| supported_bands = [{'band' : self.band_2GHz, |
| 'channels' : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]}, |
| {'band' : ap_spec.BAND_5GHZ, |
| 'channels' : [26, 40, 44, 48, 149, 153, 165]}] |
| |
| Note: The derived class must implement this method. |
| |
| @return a list of dictionaries as described above |
| |
| """ |
| raise NotImplementedError |
| |
| |
| def get_bss(self): |
| """Returns the bss of the AP.""" |
| if self.current_band == ap_spec.BAND_2GHZ: |
| return self.config_data.get_bss() |
| else: |
| return self.config_data.get_bss5() |
| |
| |
| def _get_channel_popup_position(self, channel): |
| """Internal method that converts a channel value to a popup position.""" |
| supported_bands = self.get_supported_bands() |
| for band in supported_bands: |
| if band['band'] == self.current_band: |
| return band['channels'].index(channel) |
| raise RuntimeError('The channel passed %d to the band %s is not ' |
| 'supported.' % (channel, band)) |
| |
| |
| def get_supported_modes(self): |
| """ |
| Returns a list of dictionaries describing the supported modes. |
| |
| Example: returned is a dictionary of band and a list of modes. The band |
| and modes objects returned must be one of those defined in the |
| __init___ of this class. |
| |
| supported_modes = [{'band' : ap_spec.BAND_2GHZ, |
| 'modes' : [mode_b, mode_b | mode_g]}, |
| {'band' : ap_spec.BAND_5GHZ, |
| 'modes' : [mode_a, mode_n, mode_a | mode_n]}] |
| |
| Note: The derived class must implement this method. |
| |
| @return a list of dictionaries as described above |
| |
| """ |
| raise NotImplementedError |
| |
| |
| def is_visibility_supported(self): |
| """ |
| Returns if AP supports setting the visibility (SSID broadcast). |
| |
| @return True if supported; False otherwise. |
| |
| """ |
| return True |
| |
| |
| def is_band_and_channel_supported(self, band, channel): |
| """ |
| Returns if a given band and channel are supported. |
| |
| @param band: the band to check if supported |
| @param channel: the channel to check if supported |
| |
| @return True if combination is supported; False otherwise. |
| |
| """ |
| bands = self.get_supported_bands() |
| for current_band in bands: |
| if (current_band['band'] == band and |
| channel in current_band['channels']): |
| return True |
| return False |
| |
| |
| def is_security_mode_supported(self, security_mode): |
| """ |
| Returns if a given security_type is supported. |
| |
| Note: The derived class must implement this method. |
| |
| @param security_mode: one of the following modes: |
| self.security_disabled, |
| self.security_wep, |
| self.security_wpapsk, |
| self.security_wpa2psk |
| |
| @return True if the security mode is supported; False otherwise. |
| |
| """ |
| raise NotImplementedError |
| |
| |
| def navigate_to_page(self, page_number): |
| """ |
| Navigates to the page corresponding to the given page number. |
| |
| This method performs the translation between a page number and a url to |
| load. This is used internally by apply_settings. |
| |
| Note: The derived class must implement this method. |
| |
| @param page_number: page number of the page to load |
| |
| """ |
| raise NotImplementedError |
| |
| |
| def power_cycle_router_up(self): |
| """Queues the power cycle up command.""" |
| self.add_item_to_command_list(self._power_cycle_router_up, (), 1, 0) |
| |
| |
| def _power_cycle_router_up(self): |
| """Turns the ap off and then back on again.""" |
| self.rpm_client.queue_request(self.host_name, 'OFF') |
| self.router_on = False |
| self._power_up_router() |
| |
| |
| def power_down_router(self): |
| """Queues up the power down command.""" |
| self.add_item_to_command_list(self._power_down_router, (), 1, 999) |
| |
| |
| def _power_down_router(self): |
| """Turns off the power to the ap via the power strip.""" |
| self.check_pdu_status() |
| self.rpm_client.queue_request(self.host_name, 'OFF') |
| self.router_on = False |
| |
| |
| def power_up_router(self): |
| """Queues up the power up command.""" |
| self.add_item_to_command_list(self._power_up_router, (), 1, 0) |
| |
| |
| def _power_up_router(self): |
| """ |
| Turns on the power to the ap via the power strip. |
| |
| This method returns once it can navigate to a web page of the ap UI. |
| |
| """ |
| if self.router_on: |
| return |
| self.check_pdu_status() |
| self.rpm_client.queue_request(self.host_name, 'ON') |
| self.establish_driver_connection() |
| # Depending on the response of the webserver for the AP, or lack |
| # there of, the amount of time navigate_to_page and refresh take |
| # is indeterminate. Give the APs 5 minutes of real time and then |
| # give up. |
| timeout = time.time() + (5 * 60) |
| half_way = time.time() + (2.5 * 60) |
| performed_power_cycle = False |
| while time.time() < timeout: |
| try: |
| logging.info('Attempting to load page') |
| self.navigate_to_page(1) |
| logging.debug('Page navigation complete') |
| self.router_on = True |
| return |
| # Navigate to page may throw a Selemium error or its own |
| # RuntimeError depending on the implementation. Either way we are |
| # bringing a router back from power off, we need to be patient. |
| except: |
| logging.info('Forcing a page refresh') |
| self.driver.refresh() |
| logging.info('Waiting for router %s to come back up.', |
| self.name) |
| # Sometime the APs just don't come up right. |
| if not performed_power_cycle and time.time() > half_way: |
| logging.info('Cannot connect to AP, forcing cycle') |
| self.rpm_client.queue_request(self.host_name, 'CYCLE') |
| performed_power_cycle = True |
| logging.info('Power cycle complete') |
| raise RuntimeError('Unable to load admin page after powering on the ' |
| 'router: %s' % self.name) |
| |
| |
| def save_page(self, page_number): |
| """ |
| Saves the given page. |
| |
| Note: The derived class must implement this method. |
| |
| @param page_number: Page number of the page to save. |
| |
| """ |
| raise NotImplementedError |
| |
| |
| def set_using_ap_spec(self, set_ap_spec, power_up=True): |
| """ |
| Sets all configurator options. |
| |
| @param set_ap_spec: APSpec object |
| |
| """ |
| if power_up: |
| self.power_up_router() |
| if self.is_visibility_supported(): |
| self.set_visibility(set_ap_spec.visible) |
| if (set_ap_spec.security == ap_spec.SECURITY_TYPE_WPAPSK or |
| set_ap_spec.security == ap_spec.SECURITY_TYPE_WPA2PSK): |
| self.set_security_wpapsk(set_ap_spec.security, set_ap_spec.password) |
| else: |
| self.set_security_disabled() |
| self.set_band(set_ap_spec.band) |
| self.set_mode(set_ap_spec.mode) |
| self.set_channel(set_ap_spec.channel) |
| |
| # Update ssid |
| raw_ssid = '%s_%s_ch%d_%s' % ( |
| self.short_name, |
| ap_spec.mode_string_for_mode(set_ap_spec.mode), |
| set_ap_spec.channel, |
| set_ap_spec.security) |
| self._ssid = raw_ssid.replace(' ', '_').replace('.', '_')[:32] |
| self.set_ssid(self._ssid) |
| self.ap_spec = set_ap_spec |
| |
| |
| def set_mode(self, mode, band=None): |
| """ |
| Sets the mode. |
| |
| Note: The derived class must implement this method. |
| |
| @param mode: must be one of the modes listed in __init__() |
| @param band: the band to select |
| |
| """ |
| raise NotImplementedError |
| |
| |
| def set_radio(self, enabled=True): |
| """ |
| Turns the radio on and off. |
| |
| Note: The derived class must implement this method. |
| |
| @param enabled: True to turn on the radio; False otherwise |
| |
| """ |
| raise NotImplementedError |
| |
| |
| def set_ssid(self, ssid): |
| """ |
| Sets the SSID of the wireless network. |
| |
| Note: The derived class must implement this method. |
| |
| @param ssid: name of the wireless network |
| |
| """ |
| raise NotImplementedError |
| |
| |
| def set_channel(self, channel): |
| """ |
| Sets the channel of the wireless network. |
| |
| Note: The derived class must implement this method. |
| |
| @param channel: integer value of the channel |
| |
| """ |
| raise NotImplementedError |
| |
| |
| def set_band(self, band): |
| """ |
| Sets the band of the wireless network. |
| |
| Currently there are only two possible values for band: 2kGHz and 5kGHz. |
| Note: The derived class must implement this method. |
| |
| @param band: Constant describing the band type |
| |
| """ |
| raise NotImplementedError |
| |
| |
| def set_security_disabled(self): |
| """ |
| Disables the security of the wireless network. |
| |
| Note: The derived class must implement this method. |
| |
| """ |
| raise NotImplementedError |
| |
| |
| def set_security_wep(self, key_value, authentication): |
| """ |
| Enabled WEP security for the wireless network. |
| |
| Note: The derived class must implement this method. |
| |
| @param key_value: encryption key to use |
| @param authentication: one of two supported WEP authentication types: |
| open or shared. |
| """ |
| raise NotImplementedError |
| |
| |
| def set_security_wpapsk(self, security, shared_key, update_interval=1800): |
| """Enabled WPA using a private security key for the wireless network. |
| |
| Note: The derived class must implement this method. |
| |
| @param security: Required security for AP configuration |
| @param shared_key: shared encryption key to use |
| @param update_interval: number of seconds to wait before updating |
| |
| """ |
| raise NotImplementedError |
| |
| def set_visibility(self, visible=True): |
| """Set the visibility of the wireless network. |
| |
| Note: The derived class must implement this method. |
| |
| @param visible: True for visible; False otherwise |
| |
| """ |
| raise NotImplementedError |
| |
| |
| def establish_driver_connection(self): |
| """Makes a connection to the webdriver service.""" |
| if self.driver_connection_established: |
| return |
| # Load the Auth extension |
| webdriver_server = prebuilt.check_webdriver_ready(self._webdriver_port) |
| if webdriver_server is None: |
| raise RuntimeError('Unable to connect to webdriver locally or ' |
| 'via the lab service.') |
| extension_path = os.path.join(os.path.dirname(__file__), |
| 'basic_auth_extension.crx') |
| f = open(extension_path, 'rb') |
| base64_extensions = [] |
| base64_ext = (binascii.b2a_base64(f.read()).strip()) |
| base64_extensions.append(base64_ext) |
| f.close() |
| webdriver_url = ('http://%s:%d' % (webdriver_server, |
| self._webdriver_port)) |
| router_name = self.short_name.replace(' ', '_') |
| temp_dir = datetime.datetime.now().strftime('/tmp/chromedriver_' + |
| router_name + |
| '_' + '%H%M%S%m%d%Y') |
| capabilities = {'chromeOptions' : {'extensions' : base64_extensions, |
| 'args' : [str('--user-data-dir=%s' % temp_dir)]}} |
| self.driver = webdriver.Remote(webdriver_url, capabilities) |
| self.driver_connection_established = True |
| |
| |
| def destroy_driver_connection(self): |
| """Breaks the connection to the webdriver service.""" |
| try: |
| self.driver.close() |
| except Exception, e: |
| logging.debug('Webdriver is crashed, should be respawned') |
| finally: |
| self.driver_connection_established = False |
| |
| |
| def apply_settings(self): |
| """Apply all settings to the access point. |
| |
| @param skip_success_validation: Boolean to track if method was |
| executed successfully. |
| |
| """ |
| self.configuration_success = chaos_constants.CONFIG_FAIL |
| if len(self._command_list) == 0: |
| return |
| |
| # If all we are doing is powering down the router, don't mess with |
| # starting up webdriver. |
| if (len(self._command_list) == 1 and |
| self._command_list[0]['method'] == self._power_down_router): |
| self._command_list[0]['method'](*self._command_list[0]['args']) |
| self._command_list.pop() |
| self.destroy_driver_connection() |
| return |
| self.establish_driver_connection() |
| # Pull items by page and then sort |
| if self.get_number_of_pages() == -1: |
| self.fail(msg='Number of pages is not set.') |
| page_range = range(1, self.get_number_of_pages() + 1) |
| for i in page_range: |
| page_commands = [x for x in self._command_list if x['page'] == i] |
| sorted_page_commands = sorted(page_commands, |
| key=lambda k: k['priority']) |
| if sorted_page_commands: |
| first_command = sorted_page_commands[0]['method'] |
| # If the first command is bringing the router up or down, |
| # do that before navigating to a URL. |
| if (first_command == self._power_up_router or |
| first_command == self._power_cycle_router_up or |
| first_command == self._power_down_router): |
| direction = 'up' |
| if first_command == self._power_down_router: |
| direction = 'down' |
| logging.info('Powering %s %s', direction, self.name) |
| first_command(*sorted_page_commands[0]['args']) |
| sorted_page_commands.pop(0) |
| |
| # If the router is off, no point in navigating |
| if not self.router_on: |
| if len(sorted_page_commands) == 0: |
| # If all that was requested was to power off |
| # the router then abort here and do not set the |
| # configuration_success bit. The reason is |
| # because if we failed on the configuration that |
| # failure should remain since all tests power |
| # down the AP when they are done. |
| return |
| break |
| |
| self.navigate_to_page(i) |
| for command in sorted_page_commands: |
| command['method'](*command['args']) |
| self.save_page(i) |
| self._command_list = [] |
| self.configuration_success = chaos_constants.CONFIG_SUCCESS |
| self._traceback = None |
| self.destroy_driver_connection() |
| |
| |
| def get_association_parameters(self): |
| """ |
| Creates an AssociationParameters from the configured AP. |
| |
| @returns AssociationParameters for the configured AP. |
| |
| """ |
| security_config = None |
| if self.ap_spec.security in [ap_spec.SECURITY_TYPE_WPAPSK, |
| ap_spec.SECURITY_TYPE_WPA2PSK]: |
| # Not all of this is required but doing it just in case. |
| security_config = xmlrpc_security_types.WPAConfig( |
| psk=self.ap_spec.password, |
| wpa_mode=xmlrpc_security_types.WPAConfig.MODE_MIXED_WPA, |
| wpa_ciphers=[xmlrpc_security_types.WPAConfig.CIPHER_CCMP, |
| xmlrpc_security_types.WPAConfig.CIPHER_TKIP], |
| wpa2_ciphers=[xmlrpc_security_types.WPAConfig.CIPHER_CCMP]) |
| return xmlrpc_datatypes.AssociationParameters( |
| ssid=self._ssid, security_config=security_config, |
| discovery_timeout=45, association_timeout=30, |
| configuration_timeout=30, is_hidden=not self.ap_spec.visible) |
| |
| |
| def debug_last_failure(self, outputdir): |
| """ |
| Write debug information for last AP_CONFIG_FAIL |
| |
| @param outputdir: a string directory path for debug files |
| """ |
| logging.error('Traceback:\n %s', self.traceback) |
| self._write_screenshots('config_failure', outputdir) |
| self.clear_screenshot_list() |
| |
| |
| def debug_full_state(self, outputdir): |
| """ |
| Write debug information for full AP state |
| |
| @param outputdir: a string directory path for debug files |
| """ |
| if self.configuration_success != chaos_constants.PDU_FAIL: |
| self._save_all_pages() |
| self._write_screenshots('final_configuration', outputdir) |
| self.clear_screenshot_list() |
| self.reset_command_list() |
| |
| |
| def store_config_failure(self, trace): |
| """ |
| Store configuration failure for latter logging |
| |
| @param trace: a string traceback of config exception |
| """ |
| self.save_screenshot() |
| self._traceback = trace |