From e2c8ecd218280c1cd7869fa8040ad02bf0f6b02b Mon Sep 17 00:00:00 2001 From: Pradeep Sathasivam Date: Fri, 7 Aug 2015 14:49:48 +0530 Subject: [PATCH] Support for enable password configuration When enable password is configured in device, ML2 plugin will not be able to configure the device via SSH or TELNET. Now this is supported by reading the response of each command and identifying the prompt. Change-Id: I3344f63b2f076809046fdbe92b9c9227bc0f1e1c Closes-Bug: 1481161 --- etc/neutron/plugins/brocade/brocade_mlx.ini | 4 + .../plugins/ml2/ml2_conf_brocade_fi_ni.ini | 6 + .../mlx/ml2/device_connector.py | 60 +++------ .../mlx/ml2/fi_ni/brcd_config.py | 8 ++ networking_brocade/mlx/ml2/ssh_connector.py | 114 ++++++++++++------ .../mlx/ml2/telnet_connector.py | 83 +++++++++++-- .../test_discover/test_discover.py | 5 +- 7 files changed, 182 insertions(+), 98 deletions(-) diff --git a/etc/neutron/plugins/brocade/brocade_mlx.ini b/etc/neutron/plugins/brocade/brocade_mlx.ini index 3e1e338..8165bb4 100644 --- a/etc/neutron/plugins/brocade/brocade_mlx.ini +++ b/etc/neutron/plugins/brocade/brocade_mlx.ini @@ -9,6 +9,8 @@ # password = The SSH password to use to connect to device # physical_networks = Allowed physical networks for VLAN configuration # ports = Comma separated list of ports on the switch which needs to be tagged to VLAN +# enable_username = The username for the enable configuration prompt, if any. +# enable_password = The password for the enable configuration prompt, if any. # # Example: # [mlx] @@ -17,3 +19,5 @@ # password = password # physical_networks = physnet1 # ports = 3/3, 3/9 +# enable_username = admin +# enable_password = password diff --git a/etc/neutron/plugins/ml2/ml2_conf_brocade_fi_ni.ini b/etc/neutron/plugins/ml2/ml2_conf_brocade_fi_ni.ini index b37fa43..6207e9b 100644 --- a/etc/neutron/plugins/ml2/ml2_conf_brocade_fi_ni.ini +++ b/etc/neutron/plugins/ml2/ml2_conf_brocade_fi_ni.ini @@ -11,6 +11,8 @@ # ports = Ports on the switch which needs to tagged to VLAN. Multiple ports can be separated by a comma. # transport = Protocol to use for device connection(SSH or Telnet). Default is SSH. This is an optional parameter # ostype = Optional parameter, which will identify the firmware version(FI/NI) +# enable_username = The username for the enable configuration prompt, if any. +# enable_password = The password for the enable configuration prompt, if any. # # Example: # [icx-1] @@ -21,6 +23,8 @@ # ports = 1/1/1, 1/1/2 # transport = SSH # ostype = FI +# enable_username = admin +# enable_password = password # Example: # [mlx] @@ -31,3 +35,5 @@ # ports = 3/3, 3/9 # transport = SSH # ostype = NI +# enable_username = admin +# enable_password = password diff --git a/networking_brocade/mlx/ml2/device_connector.py b/networking_brocade/mlx/ml2/device_connector.py index 8012d81..d027e25 100644 --- a/networking_brocade/mlx/ml2/device_connector.py +++ b/networking_brocade/mlx/ml2/device_connector.py @@ -18,7 +18,7 @@ Decides which connector to use - TELNET or SSH, based on the argument passed """ -import networking_brocade.mlx.ml2.commands as Commands +from networking_brocade.mlx.ml2 import commands from oslo_log import log as logging LOG = logging.getLogger(__name__) @@ -36,31 +36,10 @@ class DeviceConnector(object): self.host = device_info.get('address') self.username = device_info.get('username') self.password = device_info.get('password') + self.enable_username = device_info.get('enable_username') + self.enable_password = device_info.get('enable_password') self.transport = device_info.get('transport') - def enter_configuration_mode(self): - """ - Enter configuration mode. First it enters enable mode - and then to configuration mode. There should be no - Enable password. - """ - - self.write(Commands.ENABLE_TERMINAL_CMD) - self.write(Commands.CONFIGURE_TERMINAL_CMD) - - def exit_configuration_mode(self): - """ - Exit Configuration mode. - """ - self.send_exit(2) - - def exit_from_device(self): - """ - Exit Configuration mode and device. - """ - self.exit_configuration_mode() - self.send_exit(1) - def create_vlan(self, vlanid, ports): """ Creates VLAN and tags the ports to the created VLAN @@ -72,7 +51,7 @@ class DeviceConnector(object): "device %(host)s", {'vlanid': vlanid, 'host': self.host}) self.enter_configuration_mode() self.write( - Commands.CONFIGURE_VLAN.format( + commands.CONFIGURE_VLAN.format( vlan_id=vlanid)) LOG.debug( "Created VLAN with id %(vlanid)s on device %(host)s", { @@ -80,8 +59,6 @@ class DeviceConnector(object): for port in ports: self.tag_port(port) LOG.debug("tagged port %(port)s", {'port': port}) - self.send_exit(1) - self.exit_from_device() return self.read_response() def tag_port(self, port): @@ -92,7 +69,7 @@ class DeviceConnector(object): LOG.debug("DeviceConnector:tag_port:Tagging port %(portid)s on device" " %(host)s", {'portid': port, 'host': self.host}) self.write( - Commands.CONFIGURE_ETHERNET.format( + commands.CONFIGURE_ETHERNET.format( port_number=port)) def delete_vlan(self, vlan_id): @@ -106,12 +83,11 @@ class DeviceConnector(object): 'host': self.host}) self.enter_configuration_mode() self.write( - Commands.DELETE_CONFIGURED_VLAN.format( + commands.DELETE_CONFIGURED_VLAN.format( vlan_id=vlan_id)) LOG.debug( "Deleted VLAN with id %(vlan_id)s on device %(host)s", { 'vlan_id': vlan_id, 'host': self.host}) - self.exit_from_device() return self.read_response() def get_version(self): @@ -121,10 +97,9 @@ class DeviceConnector(object): """ LOG.debug("DeviceConnector:get_version:Executing show version for " "device %(host)s", {'host': self.host}) - self.write(Commands.SHOW_VERSION) - self.write(Commands.CTRL_C) - self.send_exit(1) - return self.read_response(read_lines=False) + self.write(commands.SHOW_VERSION) + self.write(commands.CTRL_C) + return self.read_response() def create_l3_router(self, vlan_id, gateway_ip_cidr): """Configures a Router Interface interface @@ -144,7 +119,6 @@ class DeviceConnector(object): LOG.debug(("DeviceConnector:create_l3_router:Configured router " "interface with id %(vlanid)s on device %(host)s"), {'vlanid': vlan_id, 'host': self.host}) - self.exit_from_device() return self.read_response() def _create_router_interface(self, vlan_id): @@ -158,16 +132,15 @@ class DeviceConnector(object): "%(host)s", {'vlanid': vlan_id, 'host': self.host}) self.write( - Commands.CONFIGURE_VLAN.format( + commands.CONFIGURE_VLAN.format( vlan_id=vlan_id)) self.write( - Commands.CONFIGURE_ROUTER_INTERFACE.format( + commands.CONFIGURE_ROUTER_INTERFACE.format( vlan_id=vlan_id)) LOG.debug("DeviceConnector:_create_router_interface:Created l3 " "router interface %(vlanid)s on device " "%(host)s", {'vlanid': vlan_id, 'host': self.host}) - self.send_exit(1) def _configure_ipaddress(self, vlan_id, gateway_ip_cidr): """Assigns Gateway ip for the configured vlan @@ -182,17 +155,16 @@ class DeviceConnector(object): 'vlanid': vlan_id, 'host': self.host}) self.write( - Commands.CONFIGURE_INTERFACE.format( + commands.CONFIGURE_INTERFACE.format( vlan_id=vlan_id)) self.write( - Commands.CONFIGURE_GATEWAY_IP.format( + commands.CONFIGURE_GATEWAY_IP.format( gateway_ip_addr=gateway_ip_cidr)) LOG.debug("DeviceConnector:_configure_ipaddress:Assigned IP address" " %(ipaddr)s to the router interface %(vlanid)s on device" " %(host)s", {'ipaddr': gateway_ip_cidr, 'vlanid': vlan_id, 'host': self.host}) - self.send_exit(1) def delete_l3_router(self, vlan_id): """ @@ -209,15 +181,13 @@ class DeviceConnector(object): " %(vlan_id)s from vlan %(vlan_id)s on device %(host)s"), {'vlan_id': vlan_id, 'host': self.host}) self.write( - Commands.CONFIGURE_VLAN.format(vlan_id=vlan_id)) + commands.CONFIGURE_VLAN.format(vlan_id=vlan_id)) self.write( - Commands.DELETE_ROUTER_INTERFACE.format( + commands.DELETE_ROUTER_INTERFACE.format( vlan_id=vlan_id)) - self.send_exit(1) LOG.debug(("DeviceConnector:delete_l3_router:Deleted router interface" " %(vlan_id)s from vlan %(vlan_id)s on device %(host)s"), {'vlan_id': vlan_id, 'host': self.host}) - self.exit_from_device() return self.read_response() diff --git a/networking_brocade/mlx/ml2/fi_ni/brcd_config.py b/networking_brocade/mlx/ml2/fi_ni/brcd_config.py index c982b79..06c8ba3 100644 --- a/networking_brocade/mlx/ml2/fi_ni/brcd_config.py +++ b/networking_brocade/mlx/ml2/fi_ni/brcd_config.py @@ -45,6 +45,10 @@ ML2_BROCADE = [cfg.StrOpt('address', default='', help=('OS type of the device. NI is NetIron ' 'for MLX switches. FI is FastIron for ' 'ICX switches.')), + cfg.StrOpt('enable_username', default='admin', + help=('Username of the enable config prompt')), + cfg.StrOpt('enable_password', default='password', secret=True, + help=('Password of the enable config prompt')), ] L3_BROCADE = [cfg.StrOpt('address', default='', help=('The IP address of the MLX switch')), @@ -58,6 +62,10 @@ L3_BROCADE = [cfg.StrOpt('address', default='', cfg.StrOpt('ports', default='', help=('Ports to be tagged in the VLAN being ' 'configured on the switch')), + cfg.StrOpt('enable_username', default='admin', + help=('Username of the enable config prompt')), + cfg.StrOpt('enable_password', default='password', secret=True, + help=('Password of the enable config prompt')), ] cfg.CONF.register_opts(SWITCHES, 'ml2_brocade_fi_ni') cfg.CONF.register_opts(SWITCHES, 'l3_brocade_mlx') diff --git a/networking_brocade/mlx/ml2/ssh_connector.py b/networking_brocade/mlx/ml2/ssh_connector.py index 8d1ed1d..03d83e2 100644 --- a/networking_brocade/mlx/ml2/ssh_connector.py +++ b/networking_brocade/mlx/ml2/ssh_connector.py @@ -15,19 +15,29 @@ """ Implementation of SSH Connector""" -import networking_brocade.mlx.ml2.commands as Commands -from networking_brocade.mlx.ml2.device_connector import ( - DeviceConnector as DevConn) +from networking_brocade.mlx.ml2 import device_connector as dev_conn from neutron.i18n import _LE from oslo_log import log as logging + import paramiko +import time LOG = logging.getLogger(__name__) -WRITE = "wb" -READ = "rb" +PROMPT = '>' +ENABLE_PROMPT = '#' +ENABLE_USERNAME_PROMPT = 'User Name:' +ENABLE_PASSWORD_PROMPT = 'Password:' +CONFIG_MODE = '(config)#' + +CONFIG_COMMAND = "conf t\n" +ENABLE_COMMAND = "en\n" +NEW_LINE = "\n" + +TIMEOUT = 30.0 +SLEEP_TIME = 1 -class SSHConnector(DevConn): +class SSHConnector(dev_conn.DeviceConnector): """ Uses SSH to connect to device @@ -46,10 +56,10 @@ class SSHConnector(DevConn): username=self.username, password=self.password) - channel = self.connector.invoke_shell() - self.stdin_stream = channel.makefile(WRITE) - self.stdout_stream = channel.makefile(READ) - self.stderr_stream = channel.makefile(READ) + self.channel = self.connector.invoke_shell() + self.channel.settimeout(TIMEOUT) + self.channel_data = str() + self._enter_prompt(False) except Exception as e: LOG.exception(_LE("Connect failed to switch %(host)s with error" @@ -57,45 +67,75 @@ class SSHConnector(DevConn): {'host': self.host, 'error': e.args}) raise Exception(_("Connection Failed")) + def _send_command(self, prompt, command): + """ + Executes the command passed, if the response matches the prompt + """ + execution_state = False + if self.channel_data.endswith(prompt): + self.channel.send(command) + execution_state = True + return execution_state + + def _enter_prompt(self, is_config_mode): + """ + Enters enable prompt mode or config mode based on the parameter + + param:is_config_mode: if this is True, will enter config mode, else + it will make sure it is in enable prompt. + """ + commands = [] + prompt_command = {} + if is_config_mode: + prompts = [ENABLE_PROMPT, CONFIG_MODE] + prompt_command = {ENABLE_PROMPT: CONFIG_COMMAND, + CONFIG_MODE: NEW_LINE} + else: + prompts = [PROMPT, ENABLE_USERNAME_PROMPT, + ENABLE_PASSWORD_PROMPT, ENABLE_PROMPT] + prompt_command = {PROMPT: ENABLE_COMMAND, + ENABLE_USERNAME_PROMPT: self.enable_username + + NEW_LINE, + ENABLE_PASSWORD_PROMPT: self.enable_password + + NEW_LINE, + ENABLE_PROMPT: NEW_LINE} + cmd_executed = True + # Send new line so that channel will have something to read for the + # first time. + self.channel.send(NEW_LINE) + index = 0 + while index < len(commands): + prompt = prompts[index] + if cmd_executed: + self.channel_data += self.channel.recv(9999) + command = prompt_command.get(prompt) + cmd_executed = self._send_command(prompt, command) + index += 1 + time.sleep(SLEEP_TIME) + + def enter_configuration_mode(self): + """ + This method will ensure the session is in configuration mode + """ + self._enter_prompt(True) + def write(self, command): """ Write from input stream to device :param:command: Command to be executed on the device """ - self.stdin_stream.write(command) - self.stdin_stream.flush() + self.channel.send(command) + time.sleep(SLEEP_TIME) + self.channel_data += self.channel.recv(9999) def read_response(self, read_lines=True): """Read the response from the output stream. - - :param:read_lines: Boolean value which indicated to read multiple line - or single line. It is true by default. - :returns: Response from the device as list when read_lines is True or - string when read_lines is false. """ - response = None - if read_lines: - response = self.stdout_stream.readlines() - else: - response = self.stdout_stream.read() - return response - - def send_exit(self, count): - """Send Exit command. - - :param:count: Indicates number of times to execute exit command - """ - index = 0 - while index < count: - self.stdin_stream.write(Commands.EXIT) - self.stdin_stream.flush() - index += 1 + return self.channel_data def close_session(self): """Close SSH session.""" if self.connector: - self.stdin_stream.close() - self.stdout_stream.close() - self.stderr_stream.close() + self.channel.close() self.connector.close() diff --git a/networking_brocade/mlx/ml2/telnet_connector.py b/networking_brocade/mlx/ml2/telnet_connector.py index 730687d..4f61bc5 100644 --- a/networking_brocade/mlx/ml2/telnet_connector.py +++ b/networking_brocade/mlx/ml2/telnet_connector.py @@ -16,9 +16,9 @@ """ Implementation of Telnet Connector """ import telnetlib +import time -from networking_brocade.mlx.ml2.device_connector import ( - DeviceConnector as DevConn) +from networking_brocade.mlx.ml2 import device_connector as dev_conn from neutron.i18n import _LE from oslo_log import log as logging @@ -32,15 +32,23 @@ SUPER_USER_AUTH = '^Password\\:$' TERMINAL_LENGTH = "terminal length 0" END_OF_LINE = "\r" -TELNET_TERMINAL = ">" +PROMPT = ">" +ENABLE_PROMPT = '#' CONFIGURE_TERMINAL = "#" +ENABLE_USERNAME_PROMPT = 'User Name:' +ENABLE_PASSWORD_PROMPT = 'Password:' +CONFIG_MODE = "(config)#" + +RETURN_COMMAND = "\r" +ENABLE_COMMAND = "en\r" +CONFIG_COMMAND = "conf t\r" MIN_TIMEOUT = 2 AVG_TIMEOUT = 4 MAX_TIMEOUT = 8 -class TelnetConnector(DevConn): +class TelnetConnector(dev_conn.DeviceConnector): """ Uses Telnet to connect to device @@ -56,13 +64,66 @@ class TelnetConnector(DevConn): self.connector.write(self.username + END_OF_LINE) self.connector.read_until(LOGIN_PASS_TOKEN, AVG_TIMEOUT) self.connector.write(self.password + END_OF_LINE) - self.connector.read_until(TELNET_TERMINAL, MAX_TIMEOUT) + self.response = str() + self._enter_prompt(False) except Exception as e: LOG.exception(_LE("Connect failed to switch %(host)s with error" " %(error)s"), {'host': self.host, 'error': e.args}) raise Exception(_("Connection Failed")) + def _send_command(self, prompt, command): + """ + Executes the command passed, if the response matches the prompt + """ + execution_state = False + if self.response.endswith(prompt): + self.connector.write(command) + execution_state = True + return execution_state + + def _enter_prompt(self, is_config_mode): + """ + Enters enable prompt mode or config mode based on the parameter + + param:is_config_mode: if this is True, will enter config mode, else + it will make sure it is in enable prompt. + """ + commands = [] + prompt_command = {} + if is_config_mode: + prompts = [ENABLE_PROMPT, CONFIG_MODE] + prompt_command = {ENABLE_PROMPT: CONFIG_COMMAND, + CONFIG_MODE: RETURN_COMMAND} + else: + prompts = [PROMPT, ENABLE_USERNAME_PROMPT, + ENABLE_PASSWORD_PROMPT, ENABLE_PROMPT] + prompt_command = {PROMPT: ENABLE_COMMAND, + ENABLE_USERNAME_PROMPT: self.enable_username + + RETURN_COMMAND, + ENABLE_PASSWORD_PROMPT: self.enable_password + + RETURN_COMMAND, + ENABLE_PROMPT: RETURN_COMMAND} + cmd_executed = True + # Send new line so that channel will have something to read for the + # first time. + self.connector.write(RETURN_COMMAND) + index = 0 + while index < len(commands): + prompt = prompts[index] + if cmd_executed: + self.response += self.connector.read_until(prompt, AVG_TIMEOUT) + command = prompt_command.get(prompt) + cmd_executed = self._send_command(prompt, command) + index += 1 + time.sleep(MIN_TIMEOUT) + + def enter_configuration_mode(self): + """ + This method will ensure the session is in configuration mode + """ + self._enter_prompt(True) + def write(self, command): """ Write from input stream to device @@ -70,21 +131,17 @@ class TelnetConnector(DevConn): :param:command: Command to be executed on the device """ self.connector.write(command) + self.response += self.connector.read_until(PROMPT, MAX_TIMEOUT) - def read_response(self, read_lines=True): + def read_response(self): """Read the response from the output stream. - :param:read_lines: This is used only by the SSH connector. - :returns: Response from the device as list. + :returns: Response from the device as string. """ - return self.connector.read_until(CONFIGURE_TERMINAL, MIN_TIMEOUT) + return self.response def close_session(self): """Close TELNET session.""" if self.connector: self.connector.close() self.connector = None - - def send_exit(self, count): - """No operation. Used by SSH connector only.""" - pass diff --git a/networking_brocade/test_discover/test_discover.py b/networking_brocade/test_discover/test_discover.py index fa4a9d9..9da6ff6 100644 --- a/networking_brocade/test_discover/test_discover.py +++ b/networking_brocade/test_discover/test_discover.py @@ -28,10 +28,9 @@ def load_tests(loader, tests, pattern): base_path = os.path.split(os.path.dirname(os.path.abspath(__file__)))[0] base_path = os.path.split(base_path)[0] test_dirs = {'./networking_brocade/tests', -# './networking_brocade/vdx/tests/unit/ml2/drivers/brocade', + './networking_brocade/vdx/tests/unit/ml2/drivers/brocade', MLX_TEST_BASE_PATH + '/unit/ml2/drivers/brocade', - MLX_TEST_BASE_PATH + '/unit/services/l3_router/brocade', - } + MLX_TEST_BASE_PATH + '/unit/services/l3_router/brocade'} for test_dir in test_dirs: if not pattern: suite.addTests(loader.discover(test_dir, top_level_dir=base_path))