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))