# Copyright (c) 2012 The Chromium OS 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 ctypes import datetime import logging import multiprocessing import os import pexpect import Queue import re import threading import time from config import rpm_config import dli_urllib import rpm_logging_config import common from autotest_lib.client.common_lib import error from autotest_lib.client.common_lib.cros import retry RPM_CALL_TIMEOUT_MINS = rpm_config.getint('RPM_INFRASTRUCTURE', 'call_timeout_mins') SET_POWER_STATE_TIMEOUT_SECONDS = rpm_config.getint( 'RPM_INFRASTRUCTURE', 'set_power_state_timeout_seconds') PROCESS_TIMEOUT_BUFFER = 30 class RPMController(object): """ This abstract class implements RPM request queueing and processes queued requests. The actual interaction with the RPM device will be implemented by the RPM specific subclasses. It assumes that you know the RPM hostname and that the device is on the specified RPM. This class also allows support for RPM devices that can be accessed directly or through a hydra serial concentrator device. Implementation details: This is an abstract class, subclasses must implement the methods listed here. You must not instantiate this class but should instantiate one of those leaf subclasses. Subclasses should also set TYPE class attribute to indicate device type. @var behind_hydra: boolean value to represent whether or not this RPM is behind a hydra device. @var hostname: hostname for this rpm device. @var is_running_lock: lock used to control access to _running. @var request_queue: queue used to store requested outlet state changes. @var queue_lock: lock used to control access to request_queue. @var _running: boolean value to represent if this controller is currently looping over queued requests. """ SSH_LOGIN_CMD = ('ssh -l %s -o StrictHostKeyChecking=no ' '-o ConnectTimeout=90 -o UserKnownHostsFile=/dev/null %s') USERNAME_PROMPT = 'Username:' HYRDA_RETRY_SLEEP_SECS = 10 HYDRA_MAX_CONNECT_RETRIES = 3 LOGOUT_CMD = 'logout' CLI_CMD = 'CLI' CLI_HELD = r'The administrator \[root\] has an active .* session.' CLI_KILL_PREVIOUS = 'cancel' CLI_PROMPT = 'cli>' HYDRA_PROMPT = '#' PORT_STATUS_CMD = 'portStatus' QUIT_CMD = 'quit' SESSION_KILL_CMD_FORMAT = 'administration sessions kill %s' HYDRA_CONN_HELD_MSG_FORMAT = 'is being used' CYCLE_SLEEP_TIME = 5 # Global Variables that will likely be changed by subclasses. DEVICE_PROMPT = '$' PASSWORD_PROMPT = 'Password:' # The state change command can be any string format but must accept 2 vars: # state followed by device/Plug name. SET_STATE_CMD = '%s %s' SUCCESS_MSG = None # Some RPM's may not return a success msg. NEW_STATE_ON = 'ON' NEW_STATE_OFF = 'OFF' NEW_STATE_CYCLE = 'CYCLE' TYPE = 'Should set TYPE in subclass.' def __init__(self, rpm_hostname, hydra_hostname=None): """ RPMController Constructor. To be called by subclasses. @param rpm_hostname: hostname of rpm device to be controlled. """ self._dns_zone = rpm_config.get('CROS', 'dns_zone') self.hostname = rpm_hostname self.request_queue = Queue.Queue() self._running = False self.is_running_lock = threading.Lock() # If a hydra name is provided by the subclass then we know we are # talking to an rpm behind a hydra device. self.hydra_hostname = hydra_hostname if hydra_hostname else None self.behind_hydra = hydra_hostname is not None def _start_processing_requests(self): """ Check if there is a thread processing requests. If not start one. """ with self.is_running_lock: if not self._running: self._running = True self._running_thread = threading.Thread(target=self._run) self._running_thread.start() def _stop_processing_requests(self): """ Called if the request request_queue is empty. Set running status to false. """ with self.is_running_lock: logging.debug('Request queue is empty. RPM Controller for %s' ' is terminating.', self.hostname) self._running = False if not self.request_queue.empty(): # This can occur if an item was pushed into the queue after we # exited the while-check and before the _stop_processing_requests # call was made. Therefore we need to start processing again. self._start_processing_requests() def _run(self): """ Processes all queued up requests for this RPM Controller. Callers should first request_queue up atleast one request and if this RPM Controller is not running then call run. Caller can either simply call run but then they will be blocked or can instantiate a new thread to process all queued up requests. For example: threading.Thread(target=rpm_controller.run).start() Requests are in the format of: [powerunit_info, new_state, condition_var, result] Run will set the result with the correct value. """ while not self.request_queue.empty(): try: result = multiprocessing.Value(ctypes.c_bool, False) request = self.request_queue.get() device_hostname = request['powerunit_info'].device_hostname if (datetime.datetime.utcnow() > (request['start_time'] + datetime.timedelta(minutes=RPM_CALL_TIMEOUT_MINS))): logging.error('The request was waited for too long to be ' "processed. It is timed out and won't be " 'processed.') request['result_queue'].put(False) continue is_timeout = multiprocessing.Value(ctypes.c_bool, False) process = multiprocessing.Process(target=self._process_request, args=(request, result, is_timeout)) process.start() process.join(SET_POWER_STATE_TIMEOUT_SECONDS + PROCESS_TIMEOUT_BUFFER) if process.is_alive(): logging.debug('%s: process (%s) still running, will be ' 'terminated!', device_hostname, process.pid) process.terminate() is_timeout.value = True if is_timeout.value: raise error.TimeoutException( 'Attempt to set power state is timed out after %s ' 'seconds.' % SET_POWER_STATE_TIMEOUT_SECONDS) if not result.value: logging.error('Request to change %s to state %s failed.', device_hostname, request['new_state']) except Exception as e: logging.error('Request to change %s to state %s failed: ' 'Raised exception: %s', device_hostname, request['new_state'], e) result.value = False # Put result inside the result Queue to allow the caller to resume. request['result_queue'].put(result.value) self._stop_processing_requests() def _process_request(self, request, result, is_timeout): """Process the request to change a device's outlet state. The call of set_power_state is made in a new running process. If it takes longer than SET_POWER_STATE_TIMEOUT_SECONDS, the request will be timed out. @param request: A request to change a device's outlet state. @param result: A Value object passed to the new process for the caller thread to retrieve the result. @param is_timeout: A Value object passed to the new process for the caller thread to retrieve the information about if the set_power_state call timed out. """ try: logging.getLogger().handlers = [] is_timeout_value, result_value = retry.timeout( rpm_logging_config.set_up_logging_to_server, timeout_sec=10) if is_timeout_value: raise Exception('Setup local log server handler timed out.') except Exception as e: # Fail over to log to a new file. LOG_FILENAME_FORMAT = rpm_config.get('GENERAL', 'dispatcher_logname_format') log_filename_format = LOG_FILENAME_FORMAT.replace( 'dispatcher', 'controller_%d' % os.getpid()) logging.getLogger().handlers = [] rpm_logging_config.set_up_logging_to_file( log_dir='./logs', log_filename_format=log_filename_format) logging.info('Failed to set up logging through log server: %s', e) kwargs = {'powerunit_info':request['powerunit_info'], 'new_state':request['new_state']} try: is_timeout_value, result_value = retry.timeout( self.set_power_state, args=(), kwargs=kwargs, timeout_sec=SET_POWER_STATE_TIMEOUT_SECONDS) result.value = result_value is_timeout.value = is_timeout_value except Exception as e: # This method runs in a subprocess. Must log the exception, # otherwise exceptions raised in set_power_state just get lost. # Need to convert e to a str type, because our logging server # code doesn't handle the conversion very well. logging.error('Request to change %s to state %s failed: ' 'Raised exception: %s', request['powerunit_info'].device_hostname, request['new_state'], str(e)) raise e def queue_request(self, powerunit_info, new_state): """ Queues up a requested state change for a device's outlet. Requests are in the format of: [powerunit_info, new_state, condition_var, result] Run will set the result with the correct value. @param powerunit_info: And PowerUnitInfo instance. @param new_state: ON/OFF/CYCLE - state or action we want to perform on the outlet. """ request = {} request['powerunit_info'] = powerunit_info request['new_state'] = new_state request['start_time'] = datetime.datetime.utcnow() # Reserve a spot for the result to be stored. request['result_queue'] = Queue.Queue() # Place in request_queue self.request_queue.put(request) self._start_processing_requests() # Block until the request is processed. result = request['result_queue'].get(block=True) return result def _kill_previous_connection(self): """ In case the port to the RPM through the hydra serial concentrator is in use, terminate the previous connection so we can log into the RPM. It logs into the hydra serial concentrator over ssh, launches the CLI command, gets the port number and then kills the current session. """ ssh = self._authenticate_with_hydra(admin_override=True) if not ssh: return ssh.expect(RPMController.PASSWORD_PROMPT, timeout=60) ssh.sendline(rpm_config.get('HYDRA', 'admin_password')) ssh.expect(RPMController.HYDRA_PROMPT) ssh.sendline(RPMController.CLI_CMD) cli_prompt_re = re.compile(RPMController.CLI_PROMPT) cli_held_re = re.compile(RPMController.CLI_HELD) response = ssh.expect_list([cli_prompt_re, cli_held_re], timeout=60) if response == 1: # Need to kill the previous adminstator's session. logging.error("Need to disconnect previous administrator's CLI " "session to release the connection to RPM device %s.", self.hostname) ssh.sendline(RPMController.CLI_KILL_PREVIOUS) ssh.expect(RPMController.CLI_PROMPT) ssh.sendline(RPMController.PORT_STATUS_CMD) ssh.expect(': %s' % self.hostname) ports_status = ssh.before port_number = ports_status.split(' ')[-1] ssh.expect(RPMController.CLI_PROMPT) ssh.sendline(RPMController.SESSION_KILL_CMD_FORMAT % port_number) ssh.expect(RPMController.CLI_PROMPT) self._logout(ssh, admin_logout=True) def _hydra_login(self, ssh): """ Perform the extra steps required to log into a hydra serial concentrator. @param ssh: pexpect.spawn object used to communicate with the hydra serial concentrator. @return: True if the login procedure is successful. False if an error occurred. The most common case would be if another user is logged into the device. """ try: response = ssh.expect_list( [re.compile(RPMController.PASSWORD_PROMPT), re.compile(RPMController.HYDRA_CONN_HELD_MSG_FORMAT)], timeout=15) except pexpect.TIMEOUT: # If there was a timeout, this ssh tunnel could be set up to # not require the hydra password. ssh.sendline('') try: ssh.expect(re.compile(RPMController.USERNAME_PROMPT)) logging.debug('Connected to rpm through hydra. Logging in.') return True except pexpect.ExceptionPexpect: return False if response == 0: try: ssh.sendline(rpm_config.get('HYDRA','password')) ssh.sendline('') response = ssh.expect_list( [re.compile(RPMController.USERNAME_PROMPT), re.compile(RPMController.HYDRA_CONN_HELD_MSG_FORMAT)], timeout=60) except pexpect.EOF: # Did not receive any of the expect responses, retry. return False except pexpect.TIMEOUT: logging.debug('Timeout occurred logging in to hydra.') return False # Send the username that the subclass will have set in its # construction. if response == 1: logging.debug('SSH Terminal most likely serving another' ' connection, retrying.') # Kill the connection for the next connection attempt. try: self._kill_previous_connection() except pexpect.ExceptionPexpect: logging.error('Failed to disconnect previous connection, ' 'retrying.') raise return False logging.debug('Connected to rpm through hydra. Logging in.') return True def _authenticate_with_hydra(self, admin_override=False): """ Some RPM's are behind a hydra serial concentrator and require their ssh connection to be tunneled through this device. This can fail if another user is logged in; therefore this will retry multiple times. This function also allows us to authenticate directly to the administrator interface of the hydra device. @param admin_override: Set to True if we are trying to access the administrator interface rather than tunnel through to the RPM. @return: The connected pexpect.spawn instance if the login procedure is successful. None if an error occurred. The most common case would be if another user is logged into the device. """ if admin_override: username = rpm_config.get('HYDRA', 'admin_username') else: username = '%s:%s' % (rpm_config.get('HYDRA','username'), self.hostname) cmd = RPMController.SSH_LOGIN_CMD % (username, self.hydra_hostname) num_attempts = 0 while num_attempts < RPMController.HYDRA_MAX_CONNECT_RETRIES: try: ssh = pexpect.spawn(cmd) except pexpect.ExceptionPexpect: return None if admin_override: return ssh if self._hydra_login(ssh): return ssh # Authenticating with hydra failed. Sleep then retry. time.sleep(RPMController.HYRDA_RETRY_SLEEP_SECS) num_attempts += 1 logging.error('Failed to connect to the hydra serial concentrator after' ' %d attempts.', RPMController.HYDRA_MAX_CONNECT_RETRIES) return None def _login(self): """ Log in into the RPM Device. The login process should be able to connect to the device whether or not it is behind a hydra serial concentrator. @return: ssh - a pexpect.spawn instance if the connection was successful or None if it was not. """ if self.behind_hydra: # Tunnel the connection through the hydra. ssh = self._authenticate_with_hydra() if not ssh: return None ssh.sendline(self._username) else: # Connect directly to the RPM over SSH. hostname = '%s.%s' % (self.hostname, self._dns_zone) cmd = RPMController.SSH_LOGIN_CMD % (self._username, hostname) try: ssh = pexpect.spawn(cmd) except pexpect.ExceptionPexpect: return None # Wait for the password prompt try: ssh.expect(self.PASSWORD_PROMPT, timeout=60) ssh.sendline(self._password) ssh.expect(self.DEVICE_PROMPT, timeout=60) except pexpect.ExceptionPexpect: return None return ssh def _logout(self, ssh, admin_logout=False): """ Log out of the RPM device. Send the device specific logout command and if the connection is through a hydra serial concentrator, kill the ssh connection. @param admin_logout: Set to True if we are trying to logout of the administrator interface of a hydra serial concentrator, rather than an RPM. @param ssh: pexpect.spawn instance to use to send the logout command. """ if admin_logout: ssh.sendline(RPMController.QUIT_CMD) ssh.expect(RPMController.HYDRA_PROMPT) ssh.sendline(self.LOGOUT_CMD) if self.behind_hydra and not admin_logout: # Terminate the hydra session. ssh.sendline('~.') # Wait a bit so hydra disconnects completely. Launching another # request immediately can cause a timeout. time.sleep(5) def set_power_state(self, powerunit_info, new_state): """ Set the state of the dut's outlet on this RPM. For ssh based devices, this will create the connection either directly or through a hydra tunnel and call the underlying _change_state function to be implemented by the subclass device. For non-ssh based devices, this method should be overloaded with the proper connection and state change code. And the subclass will handle accessing the RPM devices. @param powerunit_info: An instance of PowerUnitInfo. @param new_state: ON/OFF/CYCLE - state or action we want to perform on the outlet. @return: True if the attempt to change power state was successful, False otherwise. """ ssh = self._login() if not ssh: return False if new_state == self.NEW_STATE_CYCLE: logging.debug('Beginning Power Cycle for device: %s', powerunit_info.device_hostname) result = self._change_state(powerunit_info, self.NEW_STATE_OFF, ssh) if not result: return result time.sleep(RPMController.CYCLE_SLEEP_TIME) result = self._change_state(powerunit_info, self.NEW_STATE_ON, ssh) else: # Try to change the state of the device's power outlet. result = self._change_state(powerunit_info, new_state, ssh) # Terminate hydra connection if necessary. self._logout(ssh) ssh.close(force=True) return result def _change_state(self, powerunit_info, new_state, ssh): """ Perform the actual state change operation. Once we have established communication with the RPM this method is responsible for changing the state of the RPM outlet. @param powerunit_info: An instance of PowerUnitInfo. @param new_state: ON/OFF - state or action we want to perform on the outlet. @param ssh: The ssh connection used to execute the state change commands on the RPM device. @return: True if the attempt to change power state was successful, False otherwise. """ outlet = powerunit_info.outlet device_hostname = powerunit_info.device_hostname if not outlet: logging.error('Request to change outlet for device: %s to new ' 'state %s failed: outlet is unknown, please ' 'make sure POWERUNIT_OUTLET exist in the host\'s ' 'attributes in afe.', device_hostname, new_state) ssh.sendline(self.SET_STATE_CMD % (new_state, outlet)) if self.SUCCESS_MSG: # If this RPM device returns a success message check for it before # continuing. try: ssh.expect(self.SUCCESS_MSG, timeout=60) except pexpect.ExceptionPexpect: logging.error('Request to change outlet for device: %s to new ' 'state %s failed.', device_hostname, new_state) return False logging.debug('Outlet for device: %s set to %s', device_hostname, new_state) return True def type(self): """ Get the type of RPM device we are interacting with. Class attribute TYPE should be set by the subclasses. @return: string representation of RPM device type. """ return self.TYPE class SentryRPMController(RPMController): """ This class implements power control for Sentry Switched CDU http://www.servertech.com/products/switched-pdus/ Example usage: rpm = SentrySwitchedCDU('chromeos-rack1-rpm1') rpm.queue_request('chromeos-rack1-host1', 'ON') @var _username: username used to access device. @var _password: password used to access device. """ DEVICE_PROMPT = ['Switched CDU:', 'Switched PDU:'] SET_STATE_CMD = '%s %s' SUCCESS_MSG = 'Command successful' NUM_OF_OUTLETS = 17 TYPE = 'Sentry' def __init__(self, hostname, hydra_hostname=None): super(SentryRPMController, self).__init__(hostname, hydra_hostname) self._username = rpm_config.get('SENTRY', 'username') self._password = rpm_config.get('SENTRY', 'password') def _setup_test_user(self, ssh): """Configure the test user for the RPM @param ssh: Pexpect object to use to configure the RPM. """ # Create and configure the testing user profile. testing_user = rpm_config.get('SENTRY','testing_user') testing_password = rpm_config.get('SENTRY','testing_password') ssh.sendline('create user %s' % testing_user) response = ssh.expect_list([re.compile('not unique'), re.compile(self.PASSWORD_PROMPT)]) if not response: return # Testing user is not set up yet. ssh.sendline(testing_password) ssh.expect('Verify Password:') ssh.sendline(testing_password) ssh.expect(self.SUCCESS_MSG) ssh.expect(self.DEVICE_PROMPT) ssh.sendline('add outlettouser all %s' % testing_user) ssh.expect(self.SUCCESS_MSG) ssh.expect(self.DEVICE_PROMPT) def _clear_outlet_names(self, ssh): """ Before setting the outlet names, we need to clear out all the old names so there are no conflicts. For example trying to assign outlet 2 a name already assigned to outlet 9. """ for outlet in range(1, self.NUM_OF_OUTLETS): outlet_name = 'Outlet_%d' % outlet ssh.sendline(self.SET_OUTLET_NAME_CMD % (outlet, outlet_name)) ssh.expect(self.SUCCESS_MSG) ssh.expect(self.DEVICE_PROMPT) def setup(self, outlet_naming_map): """ Configure the RPM by adding the test user and setting up the outlet names. Note the rpm infrastructure does not rely on the outlet name to map a device to its outlet any more. We keep this method in case there is a need to label outlets for other reasons. We may deprecate this method if it has been proved the outlet names will not be used in any scenario. @param outlet_naming_map: Dictionary used to map the outlet numbers to host names. Keys must be ints. And names are in the format of 'hostX'. @return: True if setup completed successfully, False otherwise. """ ssh = self._login() if not ssh: logging.error('Could not connect to %s.', self.hostname) return False try: self._setup_test_user(ssh) # Set up the outlet names. # Hosts have the same name format as the RPM hostname except they # end in hostX instead of rpmX. dut_name_format = re.sub('-rpm[0-9]*', '', self.hostname) if self.behind_hydra: # Remove "chromeosX" from DUTs behind the hydra due to a length # constraint on the names we can store inside the RPM. dut_name_format = re.sub('chromeos[0-9]*-', '', dut_name_format) dut_name_format = dut_name_format + '-%s' self._clear_outlet_names(ssh) for outlet, name in outlet_naming_map.items(): dut_name = dut_name_format % name ssh.sendline(self.SET_OUTLET_NAME_CMD % (outlet, dut_name)) ssh.expect(self.SUCCESS_MSG) ssh.expect(self.DEVICE_PROMPT) except pexpect.ExceptionPexpect as e: logging.error('Setup failed. %s', e) return False finally: self._logout(ssh) return True class WebPoweredRPMController(RPMController): """ This class implements RPMController for the Web Powered units produced by Digital Loggers Inc. @var _rpm: dli_urllib.Powerswitch instance used to interact with RPM. """ TYPE = 'Webpowered' def __init__(self, hostname, powerswitch=None): username = rpm_config.get('WEBPOWERED', 'username') password = rpm_config.get('WEBPOWERED', 'password') # Call the constructor in RPMController. However since this is a web # accessible device, there should not be a need to tunnel through a # hydra serial concentrator. super(WebPoweredRPMController, self).__init__(hostname) self.hostname = '%s.%s' % (self.hostname, self._dns_zone) if not powerswitch: self._rpm = dli_urllib.Powerswitch(hostname=self.hostname, userid=username, password=password) else: # Should only be used in unit_testing self._rpm = powerswitch def _get_outlet_state(self, outlet): """ Look up the state for a given outlet on the RPM. @param outlet: the outlet to look up. @return state: the outlet's current state. """ status_list = self._rpm.statuslist() for outlet_name, _, state in status_list: if outlet_name == outlet: return state return None def set_power_state(self, powerunit_info, new_state): """ Since this does not utilize SSH in any manner, this will overload the set_power_state in RPMController and completes all steps of changing the device's outlet state. """ device_hostname = powerunit_info.device_hostname outlet = powerunit_info.outlet if not outlet: logging.error('Request to change outlet for device %s to ' 'new state %s failed: outlet is unknown. Make sure ' 'POWERUNIT_OUTLET exists in the host\'s ' 'attributes in afe' , device_hostname, new_state) return False expected_state = new_state if new_state == self.NEW_STATE_CYCLE: logging.debug('Beginning Power Cycle for device: %s', device_hostname) self._rpm.off(outlet) logging.debug('Outlet for device: %s set to OFF', device_hostname) # Pause for 5 seconds before restoring power. time.sleep(RPMController.CYCLE_SLEEP_TIME) self._rpm.on(outlet) logging.debug('Outlet for device: %s set to ON', device_hostname) expected_state = self.NEW_STATE_ON if new_state == self.NEW_STATE_OFF: self._rpm.off(outlet) logging.debug('Outlet for device: %s set to OFF', device_hostname) if new_state == self.NEW_STATE_ON: self._rpm.on(outlet) logging.debug('Outlet for device: %s set to ON', device_hostname) # Lookup the final state of the outlet return self._is_plug_state(powerunit_info, expected_state) def _is_plug_state(self, powerunit_info, expected_state): state = self._get_outlet_state(powerunit_info.outlet) if expected_state not in state: logging.error('Outlet for device: %s did not change to new state' ' %s', powerunit_info.device_hostname, expected_state) return False return True class CiscoPOEController(RPMController): """ This class implements power control for Cisco POE switch. Example usage: poe = CiscoPOEController('chromeos1-poe-switch1') poe.queue_request('chromeos1-rack5-host12-servo', 'ON') """ SSH_LOGIN_CMD = ('ssh -o StrictHostKeyChecking=no ' '-o UserKnownHostsFile=/dev/null %s') POE_USERNAME_PROMPT = 'User Name:' POE_PROMPT = '%s#' EXIT_CMD = 'exit' END_CMD = 'end' CONFIG = 'configure terminal' CONFIG_PROMPT = r'%s\(config\)#' CONFIG_IF = 'interface %s' CONFIG_IF_PROMPT = r'%s\(config-if\)#' SET_STATE_ON = 'power inline auto' SET_STATE_OFF = 'power inline never' CHECK_INTERFACE_STATE = 'show interface status %s' INTERFACE_STATE_MSG = r'Port\s+.*%s(\s+(\S+)){6,6}' CHECK_STATE_TIMEOUT = 60 CMD_TIMEOUT = 30 LOGIN_TIMEOUT = 60 PORT_UP = 'Up' PORT_DOWN = 'Down' TYPE = 'CiscoPOE' def __init__(self, hostname): """ Initialize controller class for a Cisco POE switch. @param hostname: the Cisco POE switch host name. """ super(CiscoPOEController, self).__init__(hostname) self._username = rpm_config.get('CiscoPOE', 'username') self._password = rpm_config.get('CiscoPOE', 'password') # For a switch, e.g. 'chromeos2-poe-switch8', # the device prompt looks like 'chromeos2-poe-sw8#'. short_hostname = self.hostname.replace('switch', 'sw') self.poe_prompt = self.POE_PROMPT % short_hostname self.config_prompt = self.CONFIG_PROMPT % short_hostname self.config_if_prompt = self.CONFIG_IF_PROMPT % short_hostname def _login(self): """ Log in into the Cisco POE switch. Overload _login in RPMController, as it always prompts for a user name. @return: ssh - a pexpect.spawn instance if the connection was successful or None if it was not. """ hostname = '%s.%s' % (self.hostname, self._dns_zone) cmd = self.SSH_LOGIN_CMD % (hostname) try: ssh = pexpect.spawn(cmd) except pexpect.ExceptionPexpect: logging.error('Could not connect to switch %s', hostname) return None # Wait for the username and password prompt. try: ssh.expect(self.POE_USERNAME_PROMPT, timeout=self.LOGIN_TIMEOUT) ssh.sendline(self._username) ssh.expect(self.PASSWORD_PROMPT, timeout=self.LOGIN_TIMEOUT) ssh.sendline(self._password) ssh.expect(self.poe_prompt, timeout=self.LOGIN_TIMEOUT) except pexpect.ExceptionPexpect: logging.error('Could not log into switch %s', hostname) return None return ssh def _enter_configuration_terminal(self, interface, ssh): """ Enter configuration terminal of |interface|. This function expects that we've already logged into the switch and the ssh is prompting the switch name. The work flow is chromeos1-poe-sw1# chromeos1-poe-sw1#configure terminal chromeos1-poe-sw1(config)#interface fa36 chromeos1-poe-sw1(config-if)# On success, the function exits with 'config-if' prompt. On failure, the function exits with device prompt, e.g. 'chromeos1-poe-sw1#' in the above case. @param interface: the name of the interface. @param ssh: pexpect.spawn instance to use. @return: True on success otherwise False. """ try: # Enter configure terminal. ssh.sendline(self.CONFIG) ssh.expect(self.config_prompt, timeout=self.CMD_TIMEOUT) # Enter configure terminal of the interface. ssh.sendline(self.CONFIG_IF % interface) ssh.expect(self.config_if_prompt, timeout=self.CMD_TIMEOUT) return True except pexpect.ExceptionPexpect, e: ssh.sendline(self.END_CMD) logging.exception(e) return False def _exit_configuration_terminal(self, ssh): """ Exit interface configuration terminal. On success, the function exits with device prompt, e.g. 'chromeos1-poe-sw1#' in the above case. On failure, the function exists with 'config-if' prompt. @param ssh: pexpect.spawn instance to use. @return: True on success otherwise False. """ try: ssh.sendline(self.END_CMD) ssh.expect(self.poe_prompt, timeout=self.CMD_TIMEOUT) return True except pexpect.ExceptionPexpect, e: logging.exception(e) return False def _verify_state(self, interface, expected_state, ssh): """ Check whehter the current state of |interface| matches expected state. This function tries to check the state of |interface| multiple times until its state matches the expected state or time is out. After the command of changing state has been executed, the state of an interface doesn't always change immediately to the expected state but requires some time. As such, we need a retry logic here. @param interface: the name of the interface. @param expect_state: the expected state, 'ON' or 'OFF' @param ssh: pexpect.spawn instance to use. @return: True if the state of |interface| swiches to |expected_state|, otherwise False. """ expected_state = (self.PORT_UP if expected_state == self.NEW_STATE_ON else self.PORT_DOWN) try: start = time.time() while((time.time() - start) < self.CHECK_STATE_TIMEOUT): ssh.sendline(self.CHECK_INTERFACE_STATE % interface) state_matcher = '.*'.join([self.INTERFACE_STATE_MSG % interface, self.poe_prompt]) ssh.expect(state_matcher, timeout=self.CMD_TIMEOUT) state = ssh.match.group(2) if state == expected_state: return True except pexpect.ExceptionPexpect, e: logging.exception(e) return False def _logout(self, ssh, admin_logout=False): """ Log out of the Cisco POE switch after changing state. Overload _logout in RPMController. @param admin_logout: ignored by this method. @param ssh: pexpect.spawn instance to use to send the logout command. """ ssh.sendline(self.EXIT_CMD) def _change_state(self, powerunit_info, new_state, ssh): """ Perform the actual state change operation. Overload _change_state in RPMController. @param powerunit_info: An PowerUnitInfo instance. @param new_state: ON/OFF - state or action we want to perform on the outlet. @param ssh: The ssh connection used to execute the state change commands on the POE switch. @return: True if the attempt to change power state was successful, False otherwise. """ interface = powerunit_info.outlet device_hostname = powerunit_info.device_hostname if not interface: logging.error('Could not change state: the interface on %s for %s ' 'was not given.', self.hostname, device_hostname) return False # Enter configuration terminal. if not self._enter_configuration_terminal(interface, ssh): logging.error('Could not enter configuration terminal for %s', interface) return False # Change the state. if new_state == self.NEW_STATE_ON: ssh.sendline(self.SET_STATE_ON) elif new_state == self.NEW_STATE_OFF: ssh.sendline(self.SET_STATE_OFF) else: logging.error('Unknown state request: %s', new_state) return False # Exit configuraiton terminal. if not self._exit_configuration_terminal(ssh): logging.error('Skipping verifying outlet state for device: %s, ' 'because could not exit configuration terminal.', device_hostname) return False # Verify if the state has changed successfully. if not self._verify_state(interface, new_state, ssh): logging.error('Could not verify state on interface %s', interface) return False logging.debug('Outlet for device: %s set to %s', device_hostname, new_state) return True