From 4165c424828f267dcabb71c6acfb1671810b4cf5 Mon Sep 17 00:00:00 2001 From: tonypnode Date: Thu, 7 Dec 2017 21:11:03 -0500 Subject: [PATCH 01/25] Update docstrings Signed-off-by: tonypnode --- netmiko/base_connection.py | 240 +++++++++++++++++++++++++++++++------ 1 file changed, 204 insertions(+), 36 deletions(-) diff --git a/netmiko/base_connection.py b/netmiko/base_connection.py index 3595d434c..a1734ca7a 100644 --- a/netmiko/base_connection.py +++ b/netmiko/base_connection.py @@ -43,48 +43,67 @@ def __init__(self, ip='', host='', username='', password='', secret='', port=Non :param ip: IP address of target device. Not required if `host` is provided. :type ip: str + :param host: Hostname of target device. Not required if `ip` is provided. :type host: str + :param username: Username to authenticate against target device if required. :type username: str + :param password: Password to authenticate against target device if required. :type password: str + :param secret: The enable password if target device requires one. :type secret: str + :param port: The destination port used to connect to the target device. :type port: int or None + :param device_type: Class selection based on device type. :type device_type: str + :param verbose: Enable additional messages to standard output. :type verbose: bool + :param global_delay_factor: Multiplication factor affecting Netmiko delays (default: 1). :type global_delay_factor: int + :param use_keys: Connect to target device using SSH keys. :type use_keys: bool + :param key_file: Filename path of the SSH key file to use. :type key_file: str + :param allow_agent: Enable use of SSH key-agent. :type allow_agent: bool + :param ssh_strict: Automatically reject unknown SSH host keys (default: False, which means unknown SSH host keys will be accepted). :type ssh_strict: bool + :param system_host_keys: Load host keys from the user's 'known_hosts' file. :type system_host_keys: bool + :param alt_host_keys: If `True` host keys will be loaded from the file specified in 'alt_key_file'. :type alt_host_keys: bool + :param alt_key_file: SSH host key file to use (if alt_host_keys=True). :type alt_key_file: str + :param ssh_config_file: File name of OpenSSH configuration file. :type ssh_config_file: str + :param timeout: Connection timeout. :type timeout: float + :param session_timeout: Set a timeout for parallel requests. :type session_timeout: float + :param keepalive: Send SSH keepalive packets at a specific interval, in seconds. Currently defaults to 0, for backwards compatibility (it will not attempt to keep the connection alive). @@ -160,7 +179,18 @@ def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): - """Gracefully close connection on Context Manager exit.""" + """Gracefully close connection on Context Manager exit. + + :param exc_type: + :type exc_type: + + :param exc_value: + :type exc_value: + + :param traceback: + :type traceback: + + """ self.disconnect() if exc_type is not None: raise exc_type(exc_value) @@ -174,7 +204,7 @@ def _timeout_exceeded(self, start, msg='Timeout exceeded!'): Raise NetMikoTimeoutException if waiting too much in the serving queue. - :param start: time hack to determine timeout + :param start: Time hack to determine timeout :type start: float :param msg: Exception message if timeout was exceeded @@ -193,7 +223,7 @@ def _lock_netmiko_session(self, start=None): Try to acquire the Netmiko session lock. If not available, wait in the queue until the channel is available again. - :param start: initial time hack to measure the session timeout + :param start: Initial time hack to measure the session timeout :type start: float """ if not start: @@ -214,7 +244,7 @@ def _unlock_netmiko_session(self): def _write_channel(self, out_data): """Generic handler that will write to both SSH and telnet channel. - :param out_data: data to be written to the channel + :param out_data: Data to be written to the channel :type out_data: bytes """ if self.protocol == 'ssh': @@ -232,7 +262,7 @@ def _write_channel(self, out_data): def write_channel(self, out_data): """Generic handler that will write to both SSH and telnet channel. - :param out_data: data to be written to the channel + :param out_data: Data to be written to the channel :type out_data: bytes """ self._lock_netmiko_session() @@ -312,16 +342,14 @@ def _read_channel_expect(self, pattern='', re_flags=0, max_loops=None): There are dependencies here like determining whether in config_mode that are actually depending on reading beyond pattern. - :param pattern: the pattern used to identify device prompt + :param pattern: The pattern used to identify device prompt :type pattern: str - :param re_flags: regex flags used in conjunction with pattern to search for prompt + :param re_flags: Regex flags used in conjunction with pattern to search for prompt :type re_flags: re module flags - :param max_loops: max number of iterations to read the channel before raising exception + :param max_loops: Max number of iterations to read the channel before raising exception :type max_loops: int - - """ output = '' if not pattern: @@ -368,10 +396,10 @@ def _read_channel_timing(self, delay_factor=1, max_loops=150): Once data is encountered read channel for another two seconds (2 * delay_factor) to make sure reading of channel is complete. - :param delay_factor: factor to adjust delay when reading channel + :param delay_factor: Factor to adjust delay when reading the channel :type delay_factor: int - :param max_loops: maximum number of loops to iterate through before returning channel data + :param max_loops: Controls wait time in conjunction with delay_factor (default: 150) :type max_loops: int """ delay_factor = self.select_delay_factor(delay_factor) @@ -404,12 +432,11 @@ def read_until_pattern(self, *args, **kwargs): def read_until_prompt_or_pattern(self, pattern='', re_flags=0): """Read until either self.base_prompt or pattern is detected. Return ALL data available. - :param pattern: the pattern used to identify device prompt + :param pattern: The pattern used to identify device prompt :type pattern: str - :param re_flags: regex flags used in conjunction with pattern to search for prompt + :param re_flags: Regex flags used in conjunction with pattern to search for prompt :type re_flags: re module flags - """ combined_pattern = re.escape(self.base_prompt) if pattern: @@ -419,7 +446,23 @@ def read_until_prompt_or_pattern(self, pattern='', re_flags=0): def telnet_login(self, pri_prompt_terminator='#', alt_prompt_terminator='>', username_pattern=r"sername", pwd_pattern=r"assword", delay_factor=1, max_loops=60): - """Telnet login. Can be username/password or just password.""" + """Telnet login. Can be username/password or just password. + + :param pri_prompt_terminator: Primary trailing delimiter for identifying a device prompt + :type pri_prompt_terminator: str + + :param alt_prompt_terminator: Alternate trailing delimiter for identifying a device prompt + :type alt_prompt_terminator: str + + :param username_pattern: Pattern to identify a username prompt + :type username_pattern: str + + :param delay_factor: See __init__: global_delay_factor + :type delay_factor: int + + :param max_loops: Controls wait time in conjunction with delay_factor (default: 60) + :type max_loops: int + """ TELNET_RETURN = '\r\n' delay_factor = self.select_delay_factor(delay_factor) @@ -492,7 +535,7 @@ def session_preparation(self): def _use_ssh_config(self, dict_arg): """Update SSH connection parameters based on contents of SSH 'config' file. - :param dict_arg: + :param dict_arg: dictionary of connection parameters :type dict_arg: dict """ @@ -550,10 +593,16 @@ def _sanitize_output(self, output, strip_command=False, command_string=None, """Sanitize the output. :param output: The output of a command execution on the SSH channel - :type output: + :type output: str - :param strip_command: - :type strip_command: + :param strip_command: specifies that the initial command needs to be stripped from the output + :type strip_command: bool + + :param command_string: the command string sent to the device + :type command_string: str + + :param strip_prompt: specifies that the prompt needs to be stripped from the output + :type strip_prompt: bool """ if self.ansi_escape_codes: output = self.strip_ansi_escape_codes(output) @@ -572,6 +621,11 @@ def establish_connection(self, width=None, height=None): Authentication failure will generate a NetMikoAuthenticationException width and height are needed for Fortinet paging setting. + + :param width: Specified width of the terminal window for VT100 + :type width int + + : """ if self.protocol == 'telnet': self.remote_conn = telnetlib.Telnet(self.host, port=self.port, timeout=self.timeout) @@ -662,18 +716,33 @@ def _build_ssh_client(self): return remote_conn_pre def select_delay_factor(self, delay_factor): - """Choose the greater of delay_factor or self.global_delay_factor.""" + """Choose the greater of delay_factor or self.global_delay_factor. + + :param delay_factor: See __init__: global_delay_factor + :type delay_factor: int + """ if delay_factor >= self.global_delay_factor: return delay_factor else: return self.global_delay_factor def special_login_handler(self, delay_factor=1): - """Handler for devices like WLC, Avaya ERS that throw up characters prior to login.""" + """Handler for devices like WLC, Avaya ERS that throw up characters prior to login. + + :param delay_factor: Will probably do some really cool stuff, but nothing right now + :type delay_factor: int + """ pass def disable_paging(self, command="terminal length 0", delay_factor=1): - """Disable paging default to a Cisco CLI method.""" + """Disable paging default to a Cisco CLI method. + + :param command: Device command to disable pagination of output + :type command: str + + :param delay_factor: See __init__: global_delay_factor + :type delay_factor: int + """ delay_factor = self.select_delay_factor(delay_factor) time.sleep(delay_factor * .1) self.clear_buffer() @@ -694,6 +763,14 @@ def set_terminal_width(self, command="", delay_factor=1): This causes the output to get distorted when accessed programmatically. Set terminal width to 511 which works on a broad set of devices. + + :param command: Command string to send to device + :type command: str + + :param delay_factor: See __init__: global_delay_factor + :type delay_factor: int + + TODO: delay_factor doesn't seem to be used in this method """ if not command: return "" @@ -719,6 +796,15 @@ def set_base_prompt(self, pri_prompt_terminator='#', This will be set on entering user exec or privileged exec on Cisco, but not when entering/exiting config mode. + + :param pri_prompt_terminator: Primary trailing delimiter for identifying a device prompt + :type pri_prompt_terminator: str + + :param alt_prompt_terminator: Alternate trailing delimiter for identifying a device prompt + :type alt_prompt_terminator: str + + :param delay_factor: See __init__: global_delay_factor + :type delay_factor: int """ prompt = self.find_prompt(delay_factor=delay_factor) if not prompt[-1] in (pri_prompt_terminator, alt_prompt_terminator): @@ -728,7 +814,11 @@ def set_base_prompt(self, pri_prompt_terminator='#', return self.base_prompt def find_prompt(self, delay_factor=1): - """Finds the current network device prompt, last line only.""" + """Finds the current network device prompt, last line only. + + :param delay_factor: See __init__: global_delay_factor + :type delay_factor: int + """ delay_factor = self.select_delay_factor(delay_factor) self.clear_buffer() self.write_channel("\n") @@ -773,14 +863,19 @@ def send_command_timing(self, command_string, delay_factor=1, max_loops=150, :param command_string: The command to be executed on the remote device. :type command_string: str + :param delay_factor: Multiplying factor used to adjust delays (default: 1). :type delay_factor: int + :param max_loops: Controls wait time in conjunction with delay_factor (default: 150). :type max_loops: int + :param strip_prompt: Remove the trailing router prompt from the output (default: True). :type strip_prompt: bool + :param strip_command: Remove the echo of the command from the output (default: True). :type strip_command: bool + :param normalize: Ensure the proper enter is sent at end of command (default: True). :type normalize: bool """ @@ -797,7 +892,11 @@ def send_command_timing(self, command_string, delay_factor=1, max_loops=150, return output def strip_prompt(self, a_string): - """Strip the trailing router prompt from the output.""" + """Strip the trailing router prompt from the output. + + :param a_string: Returned output from device + :type a_string: str + """ response_list = a_string.split('\n') last_line = response_list[-1] if self.base_prompt in last_line: @@ -892,11 +991,11 @@ def send_command(self, command_string, expect_string=None, def send_command_expect(self, *args, **kwargs): """Support previous name of send_command method. - :param args: arguments to send to send_command() + :param args: Arguments to send to send_command() :type args: list - :param kwargs: keyword arguments to send to send_command() - :type kwargs: Dict + :param kwargs: Keyword arguments to send to send_command() + :type kwargs: dict """ return self.send_command(*args, **kwargs) @@ -904,7 +1003,7 @@ def send_command_expect(self, *args, **kwargs): def strip_backspaces(output): """Strip any backspace characters out of the output. - :param output: output returned from device that will have x08 replaced with '' + :param output: Output returned from device that will have x08 replaced with '' :type output: str """ backspace_char = '\x08' @@ -916,6 +1015,12 @@ def strip_command(command_string, output): Strip command_string from output string Cisco IOS adds backspaces into output for long commands (i.e. for commands that line wrap) + + :param command_string: Command string sent to the device. + :type command_string: str + + :param output: The returned output as a result of the command string sent to the device + :type output: str """ backspace_char = '\x08' @@ -931,7 +1036,12 @@ def strip_command(command_string, output): @staticmethod def normalize_linefeeds(a_string): - """Convert `\r\r\n`,`\r\n`, `\n\r` to `\n.`""" + """Convert `\r\r\n`,`\r\n`, `\n\r` to `\n.` + + :param a_string: A string that may have non-normalized line feeds + i.e. output returned from device, or a device prompt + :type a_string: str + """ newline = re.compile('(\r\r\r\n|\r\r\n|\r\n|\n\r)') a_string = newline.sub('\n', a_string) # Convert any remaining \r to \n @@ -939,13 +1049,22 @@ def normalize_linefeeds(a_string): @staticmethod def normalize_cmd(command): - """Normalize CLI commands to have a single trailing newline.""" + """Normalize CLI commands to have a single trailing newline. + + :param command: Command to be sent to the channel that may require line feed to be normalized + (No Default set) + :type command: str + """ command = command.rstrip("\n") command += '\n' return command def check_enable_mode(self, check_string=''): - """Check if in enable mode. Return boolean.""" + """Check if in enable mode. Return boolean. + + :param check_string: Identification of privilege mode from the device + :type check_string: str + """ self.write_channel('\n') output = self.read_until_prompt() log.debug("{0}".format(output)) @@ -966,7 +1085,11 @@ def enable(self, cmd='', pattern='ssword', re_flags=re.IGNORECASE): return output def exit_enable_mode(self, exit_command=''): - """Exit enable mode.""" + """Exit enable mode. + + :param exit_command: Command to send to the device that exits the session from privileged mode + :type exit_command: str + """ output = "" if self.check_enable_mode(): self.write_channel(self.normalize_cmd(exit_command)) @@ -976,7 +1099,14 @@ def exit_enable_mode(self, exit_command=''): return output def check_config_mode(self, check_string='', pattern=''): - """Checks if the device is in configuration mode or not.""" + """Checks if the device is in configuration mode or not. + + :param check_string: Identification of configuration mode from the device + :type check_string: str + + :param pattern: The pattern to identify the device prompt + :type pattern: str + """ log.debug("pattern: {0}".format(pattern)) self.write_channel('\n') output = self.read_until_pattern(pattern=pattern) @@ -984,7 +1114,14 @@ def check_config_mode(self, check_string='', pattern=''): return check_string in output def config_mode(self, config_command='', pattern=''): - """Enter into config_mode.""" + """Enter into config_mode. + + :param config_command: Configuration command to send to the device + :type config_command: str + + :param pattern: The pattern to identify the device prompt, signifying the config command completed + :type pattern: str + """ output = '' if not self.check_config_mode(): self.write_channel(self.normalize_cmd(config_command)) @@ -994,7 +1131,14 @@ def config_mode(self, config_command='', pattern=''): return output def exit_config_mode(self, exit_config='', pattern=''): - """Exit from configuration mode.""" + """Exit from configuration mode. + + :param exit_config: Command to exit configuration mode + :type exit_config: str + + :param pattern: The pattern to identify the device prompt, signifying the exit config mode command completed + :type pattern: str + """ output = '' if self.check_config_mode(): self.write_channel(self.normalize_cmd(exit_config)) @@ -1011,6 +1155,9 @@ def send_config_from_file(self, config_file=None, **kwargs): The file is processed line-by-line and each command is sent down the SSH channel. + :param config_file: Configuration file to be sent do the device + :type: + **kwargs are passed to send_config_set method. """ with io.open(config_file, "rt", encoding='utf-8') as cfg_file: @@ -1025,6 +1172,24 @@ def send_config_set(self, config_commands=None, exit_config_mode=True, delay_fac The commands will be executed one after the other. Automatically exits/enters configuration mode. + + :param config_commands: Multiple commands to be sent to the device + :type config_commands: iterable of strings + + :param exit_config_mode: Determines whether or not to exit config mode after all commands have been sent + :type exit_config_mode: bool + + :param delay_factor: Factor to adjust delay when reading the channel + :type delay_factor: int + + :param max_loops: Controls wait time in conjunction with delay_factor (default: 150) + :type max_loops: int + + :param strip_prompt: Determines whether or not to strip the prompt from the + :type strip_prompt: bool + + :param strip_command: + :type strip_command: bool """ delay_factor = self.select_delay_factor(delay_factor) if config_commands is None: @@ -1075,6 +1240,9 @@ def strip_ansi_escape_codes(string_buffer): ESC[\d\d;\d\dm and ESC[\d\d;\d\d;\d\dm HP ProCurve's, Cisco SG300, and F5 LTM's require this (possible others) + + :param string_buffer: the string that may require ansi escape chars to be removed + :type string_buffer: str """ log.debug("In strip_ansi_escape_codes") log.debug("repr = {0}".format(repr(string_buffer))) From 21e9d8fac320c533408ab4e1f98a1542477dddc3 Mon Sep 17 00:00:00 2001 From: tonypnode Date: Sun, 10 Dec 2017 17:01:51 -0500 Subject: [PATCH 02/25] Update docstrings Signed-off-by: tonypnode --- netmiko/base_connection.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/netmiko/base_connection.py b/netmiko/base_connection.py index a1734ca7a..a7919a96f 100644 --- a/netmiko/base_connection.py +++ b/netmiko/base_connection.py @@ -666,10 +666,28 @@ def establish_connection(self, width=None, height=None): return "" def _test_channel_read(self, count=40, pattern=""): - """Try to read the channel (generally post login) verify you receive data back.""" + """Try to read the channel (generally post login) verify you receive data back. + + :param count: the number of times to check the channel for data + :type count: int + + :param pattern: the pattern to search for signifying the device prompt has returned and to break out of the loop + :type: str + """ def _increment_delay(main_delay, increment=1.1, maximum=8): - """Increment sleep time to a maximum value.""" + """Increment sleep time to a maximum value. + + :param main_delay: Pri factor for calculating the sleep time while for data to return from the channel + :type int + + :param increment: Secondary factor for calculating sleep time while waiting for data to return from channel + :type increment: float + + :param maximum: The maximum amount of delay to sleep when waiting for data to return from the channel + :type int + : + """ main_delay = main_delay * increment if main_delay >= maximum: main_delay = maximum From fdd80a9f12c45a54023a1316af8ebd083b9f41a0 Mon Sep 17 00:00:00 2001 From: tonypnode Date: Sun, 10 Dec 2017 17:25:42 -0500 Subject: [PATCH 03/25] Update docstrings Signed-off-by: tonypnode --- netmiko/base_connection.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/netmiko/base_connection.py b/netmiko/base_connection.py index a7919a96f..d78cbeda7 100644 --- a/netmiko/base_connection.py +++ b/netmiko/base_connection.py @@ -595,7 +595,7 @@ def _sanitize_output(self, output, strip_command=False, command_string=None, :param output: The output of a command execution on the SSH channel :type output: str - :param strip_command: specifies that the initial command needs to be stripped from the output + :param strip_command: specifies that the initial command to be stripped from the output :type strip_command: bool :param command_string: the command string sent to the device @@ -671,20 +671,20 @@ def _test_channel_read(self, count=40, pattern=""): :param count: the number of times to check the channel for data :type count: int - :param pattern: the pattern to search for signifying the device prompt has returned and to break out of the loop + :param pattern: Pattern signifying the device prompt has returned and to break out of the loop :type: str """ def _increment_delay(main_delay, increment=1.1, maximum=8): """Increment sleep time to a maximum value. - :param main_delay: Pri factor for calculating the sleep time while for data to return from the channel + :param main_delay: Pri factor for sleep time while for data to return from the channel :type int - :param increment: Secondary factor for calculating sleep time while waiting for data to return from channel + :param increment: Sec factor for sleep time while waiting for data to return from channel :type increment: float - :param maximum: The maximum amount of delay to sleep when waiting for data to return from the channel + :param maximum: The max delay to sleep when waiting for data to return from the channel :type int : """ @@ -1069,7 +1069,7 @@ def normalize_linefeeds(a_string): def normalize_cmd(command): """Normalize CLI commands to have a single trailing newline. - :param command: Command to be sent to the channel that may require line feed to be normalized + :param command: Command that may require line feed to be normalized (No Default set) :type command: str """ @@ -1105,7 +1105,7 @@ def enable(self, cmd='', pattern='ssword', re_flags=re.IGNORECASE): def exit_enable_mode(self, exit_command=''): """Exit enable mode. - :param exit_command: Command to send to the device that exits the session from privileged mode + :param exit_command: Command that exits the session from privileged mode :type exit_command: str """ output = "" @@ -1122,7 +1122,7 @@ def check_config_mode(self, check_string='', pattern=''): :param check_string: Identification of configuration mode from the device :type check_string: str - :param pattern: The pattern to identify the device prompt + :param pattern: Pattern to identify the device prompt :type pattern: str """ log.debug("pattern: {0}".format(pattern)) @@ -1137,7 +1137,7 @@ def config_mode(self, config_command='', pattern=''): :param config_command: Configuration command to send to the device :type config_command: str - :param pattern: The pattern to identify the device prompt, signifying the config command completed + :param pattern: The pattern signifying the config command completed :type pattern: str """ output = '' @@ -1154,7 +1154,7 @@ def exit_config_mode(self, exit_config='', pattern=''): :param exit_config: Command to exit configuration mode :type exit_config: str - :param pattern: The pattern to identify the device prompt, signifying the exit config mode command completed + :param pattern: The pattern signifying the exit config mode command completed :type pattern: str """ output = '' @@ -1194,7 +1194,7 @@ def send_config_set(self, config_commands=None, exit_config_mode=True, delay_fac :param config_commands: Multiple commands to be sent to the device :type config_commands: iterable of strings - :param exit_config_mode: Determines whether or not to exit config mode after all commands have been sent + :param exit_config_mode: Determines exit config mode after all commands have been sent :type exit_config_mode: bool :param delay_factor: Factor to adjust delay when reading the channel From d669280f0287e84cdff7a84b94a3e4fb7bd3f671 Mon Sep 17 00:00:00 2001 From: tonypnode Date: Sun, 10 Dec 2017 17:28:36 -0500 Subject: [PATCH 04/25] Update docstrings Signed-off-by: tonypnode --- netmiko/base_connection.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/netmiko/base_connection.py b/netmiko/base_connection.py index d78cbeda7..bfee51034 100644 --- a/netmiko/base_connection.py +++ b/netmiko/base_connection.py @@ -671,20 +671,20 @@ def _test_channel_read(self, count=40, pattern=""): :param count: the number of times to check the channel for data :type count: int - :param pattern: Pattern signifying the device prompt has returned and to break out of the loop + :param pattern: Signifying the device prompt has returned and to break out of the loop :type: str """ def _increment_delay(main_delay, increment=1.1, maximum=8): """Increment sleep time to a maximum value. - :param main_delay: Pri factor for sleep time while for data to return from the channel + :param main_delay: Pri sleep factor for data to return from the channel :type int - :param increment: Sec factor for sleep time while waiting for data to return from channel + :param increment: Sec sleep factor for waiting for data to return from channel :type increment: float - :param maximum: The max delay to sleep when waiting for data to return from the channel + :param maximum: Max delay to sleep when waiting for data to return from the channel :type int : """ From 97d64617dc7833248cdafd29f38038c6dd225623 Mon Sep 17 00:00:00 2001 From: tonypnode Date: Sat, 10 Feb 2018 17:43:08 -0500 Subject: [PATCH 05/25] Fix docstring issues Signed-off-by: tonypnode --- netmiko/base_connection.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/netmiko/base_connection.py b/netmiko/base_connection.py index bfee51034..f3c340f95 100644 --- a/netmiko/base_connection.py +++ b/netmiko/base_connection.py @@ -346,7 +346,7 @@ def _read_channel_expect(self, pattern='', re_flags=0, max_loops=None): :type pattern: str :param re_flags: Regex flags used in conjunction with pattern to search for prompt - :type re_flags: re module flags + :type re_flags: int :param max_loops: Max number of iterations to read the channel before raising exception :type max_loops: int @@ -436,7 +436,7 @@ def read_until_prompt_or_pattern(self, pattern='', re_flags=0): :type pattern: str :param re_flags: Regex flags used in conjunction with pattern to search for prompt - :type re_flags: re module flags + :type re_flags: int """ combined_pattern = re.escape(self.base_prompt) if pattern: @@ -623,7 +623,7 @@ def establish_connection(self, width=None, height=None): width and height are needed for Fortinet paging setting. :param width: Specified width of the terminal window for VT100 - :type width int + :type width: int : """ @@ -672,21 +672,21 @@ def _test_channel_read(self, count=40, pattern=""): :type count: int :param pattern: Signifying the device prompt has returned and to break out of the loop - :type: str + :type: pattern: str """ def _increment_delay(main_delay, increment=1.1, maximum=8): """Increment sleep time to a maximum value. :param main_delay: Pri sleep factor for data to return from the channel - :type int + :type main_delay: int :param increment: Sec sleep factor for waiting for data to return from channel :type increment: float :param maximum: Max delay to sleep when waiting for data to return from the channel - :type int - : + :type maximum: int + """ main_delay = main_delay * increment if main_delay >= maximum: @@ -1173,8 +1173,8 @@ def send_config_from_file(self, config_file=None, **kwargs): The file is processed line-by-line and each command is sent down the SSH channel. - :param config_file: Configuration file to be sent do the device - :type: + :param config_file: Path to configuration file to be sent do the device + :type config_file: str **kwargs are passed to send_config_set method. """ @@ -1192,7 +1192,7 @@ def send_config_set(self, config_commands=None, exit_config_mode=True, delay_fac Automatically exits/enters configuration mode. :param config_commands: Multiple commands to be sent to the device - :type config_commands: iterable of strings + :type config_commands: list of strings :param exit_config_mode: Determines exit config mode after all commands have been sent :type exit_config_mode: bool From 7eb988175e266c6bf996a641bc4548794e937020 Mon Sep 17 00:00:00 2001 From: Xavier Hardy Date: Mon, 12 Feb 2018 22:20:36 +0100 Subject: [PATCH 06/25] Fix ProxyCommand case --- netmiko/base_connection.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/netmiko/base_connection.py b/netmiko/base_connection.py index 33c62b15b..f9a837973 100644 --- a/netmiko/base_connection.py +++ b/netmiko/base_connection.py @@ -569,10 +569,10 @@ def _use_ssh_config(self, dict_arg): else: source = {} - if source.get('proxycommand'): - proxy = paramiko.ProxyCommand(source['proxycommand']) - elif source.get('ProxyCommand'): + if "proxycommand" in source: proxy = paramiko.ProxyCommand(source['proxycommand']) + elif "ProxyCommand" in source: + proxy = paramiko.ProxyCommand(source['ProxyCommand']) else: proxy = None From d7d38ea36264f36cf45ea3e2609600c7682a46e4 Mon Sep 17 00:00:00 2001 From: tonypnode Date: Mon, 12 Feb 2018 22:23:05 -0500 Subject: [PATCH 07/25] rebase Signed-off-by: tonypnode --- .travis.yml | 4 +- README.md | 26 +- netmiko/__init__.py | 10 +- netmiko/_textfsm/__init__.py | 5 + netmiko/_textfsm/_clitable.py | 380 ++++++ netmiko/_textfsm/_terminal.py | 110 ++ netmiko/_textfsm/_texttable.py | 1103 +++++++++++++++++ netmiko/a10/a10_ssh.py | 11 +- netmiko/accedian/accedian_ssh.py | 9 +- netmiko/alcatel/alcatel_aos_ssh.py | 8 + netmiko/alcatel/alcatel_sros_ssh.py | 10 +- netmiko/arista/__init__.py | 4 +- netmiko/arista/arista_ssh.py | 100 +- netmiko/aruba/aruba_ssh.py | 3 + netmiko/avaya/avaya_ers_ssh.py | 10 +- netmiko/avaya/avaya_vsp_ssh.py | 10 +- netmiko/base_connection.py | 592 ++++----- netmiko/brocade/__init__.py | 6 +- netmiko/brocade/brocade_netiron.py | 19 + netmiko/brocade/brocade_netiron_ssh.py | 6 - netmiko/brocade/brocade_nos_ssh.py | 8 +- netmiko/calix/__init__.py | 4 + netmiko/calix/calix_b6_ssh.py | 94 ++ netmiko/checkpoint/checkpoint_gaia_ssh.py | 10 +- netmiko/ciena/ciena_saos_ssh.py | 10 +- netmiko/cisco/__init__.py | 12 +- netmiko/cisco/cisco_asa_ssh.py | 26 +- netmiko/cisco/cisco_ios.py | 184 ++- netmiko/cisco/cisco_nxos_ssh.py | 83 +- netmiko/cisco/cisco_s300.py | 8 +- netmiko/cisco/cisco_tp_tcce.py | 21 +- netmiko/cisco/cisco_wlc_ssh.py | 37 +- netmiko/cisco/cisco_xr_ssh.py | 30 +- netmiko/cisco_base_connection.py | 69 +- netmiko/coriant/__init__.py | 3 + netmiko/coriant/coriant_ssh.py | 42 + netmiko/dell/__init__.py | 4 +- netmiko/dell/dell_force10_ssh.py | 5 +- ...werconnect_ssh.py => dell_powerconnect.py} | 71 +- netmiko/dell/dell_powerconnect_telnet.py | 38 - netmiko/eltex/eltex_ssh.py | 13 +- netmiko/enterasys/enterasys_ssh.py | 10 +- netmiko/extreme/__init__.py | 5 +- .../{extreme_ssh.py => extreme_exos.py} | 31 +- netmiko/extreme/extreme_wing_ssh.py | 7 +- netmiko/f5/f5_ltm_ssh.py | 19 +- netmiko/fortinet/fortinet_ssh.py | 12 +- netmiko/hp/hp_comware_ssh.py | 23 +- netmiko/hp/hp_procurve_ssh.py | 27 +- netmiko/huawei/__init__.py | 4 +- netmiko/huawei/huawei_ssh.py | 49 +- netmiko/juniper/__init__.py | 4 +- netmiko/juniper/juniper_ssh.py | 168 ++- netmiko/linux/linux_ssh.py | 23 +- netmiko/mellanox/mellanox_ssh.py | 9 + netmiko/mrv/mrv_ssh.py | 8 + netmiko/netapp/__init__.py | 4 + netmiko/netapp/netapp_cdot_ssh.py | 38 + netmiko/paloalto/paloalto_panos_ssh.py | 28 +- netmiko/pluribus/pluribus_ssh.py | 4 + netmiko/quanta/quanta_mesh_ssh.py | 4 + netmiko/ruckus/__init__.py | 5 + .../ruckus_fastiron.py} | 30 +- netmiko/scp_handler.py | 198 +-- netmiko/ssh_autodetect.py | 25 +- netmiko/ssh_dispatcher.py | 81 +- netmiko/terminal_server/terminal_server.py | 7 +- netmiko/ubiquiti/edge_ssh.py | 8 +- netmiko/utilities.py | 68 + netmiko/vyos/vyos_ssh.py | 16 +- requirements-dev.txt | 3 +- requirements.txt | 4 +- setup.cfg | 2 +- setup.py | 38 +- tests/etc/test_devices.yml.example | 6 + tests/test_cisco_ios_serial.py | 38 + tests/test_import_netmiko.py | 4 + tests/test_suite_alt.sh | 6 +- tests/test_suite_tmp.sh | 68 + tox.ini | 4 +- 80 files changed, 3451 insertions(+), 835 deletions(-) create mode 100644 netmiko/_textfsm/__init__.py create mode 100644 netmiko/_textfsm/_clitable.py create mode 100644 netmiko/_textfsm/_terminal.py create mode 100644 netmiko/_textfsm/_texttable.py create mode 100644 netmiko/brocade/brocade_netiron.py delete mode 100644 netmiko/brocade/brocade_netiron_ssh.py create mode 100644 netmiko/calix/__init__.py create mode 100644 netmiko/calix/calix_b6_ssh.py create mode 100644 netmiko/coriant/__init__.py create mode 100644 netmiko/coriant/coriant_ssh.py rename netmiko/dell/{dell_powerconnect_ssh.py => dell_powerconnect.py} (67%) delete mode 100644 netmiko/dell/dell_powerconnect_telnet.py rename netmiko/extreme/{extreme_ssh.py => extreme_exos.py} (62%) create mode 100644 netmiko/netapp/__init__.py create mode 100644 netmiko/netapp/netapp_cdot_ssh.py create mode 100644 netmiko/ruckus/__init__.py rename netmiko/{brocade/brocade_fastiron_ssh.py => ruckus/ruckus_fastiron.py} (70%) create mode 100644 tests/test_cisco_ios_serial.py create mode 100644 tests/test_import_netmiko.py create mode 100755 tests/test_suite_tmp.sh diff --git a/.travis.yml b/.travis.yml index ae4864aa1..d074c0d49 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,12 +2,14 @@ language: python python: - "2.7" - - "3.4" - "3.5" + - "3.6" #before_install: # - openssl aes-256-cbc -K $encrypted_25c51ccabb0e_key -iv $encrypted_25c51ccabb0e_iv -in travis_test_env.tar.enc -out travis_test_env.tar -d # - tar xvf travis_test_env.tar install: - pip install -r requirements-dev.txt + - pip install tox-travis script: - pylama . + - tox diff --git a/README.md b/README.md index be740c173..580c41c3f 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,15 @@ Netmiko Multi-vendor library to simplify Paramiko SSH connections to network devices -Python 2.7, 3.4, 3.5 +Python 2.7, 3.5, 3.6 #### Requires: -Paramiko >= 1.13+ +Paramiko >= 2 scp >= 0.10.0 pyyaml -pytest (for unit tests) +pyserial +textfsm #### Supports: @@ -37,33 +38,38 @@ Alcatel AOS6/AOS8 Avaya ERS Avaya VSP Brocade VDX -Brocade ICX/FastIron Brocade MLX/NetIron +Calix B6 Cisco WLC -Dell-Force10 DNOS9 +Dell-Force10 Dell PowerConnect Huawei Mellanox +NetApp cDOT Palo Alto PAN-OS Pluribus -Ubiquiti EdgeOS +Ruckus ICX/FastIron +Ubiquiti EdgeSwitch Vyatta VyOS ###### Experimental A10 Accedian -Alcatel-Lucent SR-OS Aruba Ciena SAOS Cisco Telepresence -CheckPoint Gaia +Check Point GAiA +Coriant +Eltex Enterasys Extreme EXOS Extreme Wing F5 LTM Fortinet MRV Communications OptiSwitch +Nokia/Alcatel SR-OS +QuantaMesh ## Tutorials: @@ -75,6 +81,10 @@ https://pynet.twb-tech.com/blog/automation/netmiko.html https://pynet.twb-tech.com/blog/automation/netmiko-proxy.html +##### Documentation (Stable) + +http://netmiko.readthedocs.io/en/stable/index.html + ## Examples: #### Create a dictionary representing the device. diff --git a/netmiko/__init__.py b/netmiko/__init__.py index 8d1414fe5..e3fa88cd4 100644 --- a/netmiko/__init__.py +++ b/netmiko/__init__.py @@ -9,9 +9,9 @@ from netmiko.ssh_dispatcher import ssh_dispatcher from netmiko.ssh_dispatcher import redispatch from netmiko.ssh_dispatcher import platforms +from netmiko.ssh_dispatcher import FileTransfer from netmiko.scp_handler import SCPConn -from netmiko.scp_handler import FileTransfer -from netmiko.scp_handler import InLineTransfer +from netmiko.cisco.cisco_ios import InLineTransfer from netmiko.ssh_exception import NetMikoTimeoutException from netmiko.ssh_exception import NetMikoAuthenticationException from netmiko.ssh_autodetect import SSHDetect @@ -20,13 +20,13 @@ # Alternate naming NetmikoTimeoutError = NetMikoTimeoutException NetmikoAuthError = NetMikoAuthenticationException +Netmiko = ConnectHandler -__version__ = '1.4.3' - +__version__ = '2.0.2' __all__ = ('ConnectHandler', 'ssh_dispatcher', 'platforms', 'SCPConn', 'FileTransfer', 'NetMikoTimeoutException', 'NetMikoAuthenticationException', 'NetmikoTimeoutError', 'NetmikoAuthError', 'InLineTransfer', 'redispatch', - 'SSHDetect', 'BaseConnection') + 'SSHDetect', 'BaseConnection', 'Netmiko') # Cisco cntl-shift-six sequence CNTL_SHIFT_6 = chr(30) diff --git a/netmiko/_textfsm/__init__.py b/netmiko/_textfsm/__init__.py new file mode 100644 index 000000000..a9af36bc8 --- /dev/null +++ b/netmiko/_textfsm/__init__.py @@ -0,0 +1,5 @@ +from netmiko._textfsm import _terminal +from netmiko._textfsm import _texttable +from netmiko._textfsm import _clitable + +__all__ = ('_terminal', '_texttable', '_clitable') diff --git a/netmiko/_textfsm/_clitable.py b/netmiko/_textfsm/_clitable.py new file mode 100644 index 000000000..20286f995 --- /dev/null +++ b/netmiko/_textfsm/_clitable.py @@ -0,0 +1,380 @@ +""" +Google's clitable.py is inherently integrated to Linux: + +This is a workaround for that (basically include modified clitable code without anything +that is Linux-specific). + +_clitable.py is identical to Google's as of 2017-12-17 +_texttable.py is identical to Google's as of 2017-12-17 +_terminal.py is a highly stripped down version of Google's such that clitable.py works + +https://github.com/google/textfsm/blob/master/clitable.py +""" + +# Some of this code is from Google with the following license: +# +# Copyright 2012 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +import copy +import os +import re +import threading +import copyable_regex_object +import textfsm +from netmiko._textfsm import _texttable as texttable + + +class Error(Exception): + """Base class for errors.""" + + +class IndexTableError(Error): + """General INdexTable error.""" + + +class CliTableError(Error): + """General CliTable error.""" + + +class IndexTable(object): + """Class that reads and stores comma-separated values as a TextTable. + Stores a compiled regexp of the value for efficient matching. + Includes functions to preprocess Columns (both compiled and uncompiled). + Attributes: + index: TextTable, the index file parsed into a texttable. + compiled: TextTable, the table but with compiled regexp for each field. + """ + + def __init__(self, preread=None, precompile=None, file_path=None): + """Create new IndexTable object. + Args: + preread: func, Pre-processing, applied to each field as it is read. + precompile: func, Pre-compilation, applied to each field before compiling. + file_path: String, Location of file to use as input. + """ + self.index = None + self.compiled = None + if file_path: + self._index_file = file_path + self._index_handle = open(self._index_file, 'r') + self._ParseIndex(preread, precompile) + + def __del__(self): + """Close index handle.""" + if hasattr(self, '_index_handle'): + self._index_handle.close() + + def __len__(self): + """Returns number of rows in table.""" + return self.index.size + + def __copy__(self): + """Returns a copy of an IndexTable object.""" + clone = IndexTable() + if hasattr(self, '_index_file'): + # pylint: disable=protected-access + clone._index_file = self._index_file + clone._index_handle = self._index_handle + + clone.index = self.index + clone.compiled = self.compiled + return clone + + def __deepcopy__(self, memodict=None): + """Returns a deepcopy of an IndexTable object.""" + clone = IndexTable() + if hasattr(self, '_index_file'): + # pylint: disable=protected-access + clone._index_file = copy.deepcopy(self._index_file) + clone._index_handle = open(clone._index_file, 'r') + + clone.index = copy.deepcopy(self.index) + clone.compiled = copy.deepcopy(self.compiled) + return clone + + def _ParseIndex(self, preread, precompile): + """Reads index file and stores entries in TextTable. + For optimisation reasons, a second table is created with compiled entries. + Args: + preread: func, Pre-processing, applied to each field as it is read. + precompile: func, Pre-compilation, applied to each field before compiling. + Raises: + IndexTableError: If the column headers has illegal column labels. + """ + self.index = texttable.TextTable() + self.index.CsvToTable(self._index_handle) + + if preread: + for row in self.index: + for col in row.header: + row[col] = preread(col, row[col]) + + self.compiled = copy.deepcopy(self.index) + + for row in self.compiled: + for col in row.header: + if precompile: + row[col] = precompile(col, row[col]) + if row[col]: + row[col] = copyable_regex_object.CopyableRegexObject(row[col]) + + def GetRowMatch(self, attributes): + """Returns the row number that matches the supplied attributes.""" + for row in self.compiled: + try: + for key in attributes: + # Silently skip attributes not present in the index file. + # pylint: disable=E1103 + if key in row.header and row[key] and not row[key].match(attributes[key]): + # This line does not match, so break and try next row. + raise StopIteration() + return row.row + except StopIteration: + pass + return 0 + + +class CliTable(texttable.TextTable): + """Class that reads CLI output and parses into tabular format. + Reads an index file and uses it to map command strings to templates. It then + uses TextFSM to parse the command output (raw) into a tabular format. + The superkey is the set of columns that contain data that uniquely defines the + row, the key is the row number otherwise. This is typically gathered from the + templates 'Key' value but is extensible. + Attributes: + raw: String, Unparsed command string from device/command. + index_file: String, file where template/command mappings reside. + template_dir: String, directory where index file and templates reside. + """ + + # Parse each template index only once across all instances. + # Without this, the regexes are parsed at every call to CliTable(). + _lock = threading.Lock() + INDEX = {} + + # pylint: disable=C6409 + def synchronised(func): + """Synchronisation decorator.""" + + # pylint: disable=E0213 + def Wrapper(main_obj, *args, **kwargs): + main_obj._lock.acquire() # pylint: disable=W0212 + try: + return func(main_obj, *args, **kwargs) # pylint: disable=E1102 + finally: + main_obj._lock.release() # pylint: disable=W0212 + return Wrapper + # pylint: enable=C6409 + + @synchronised + def __init__(self, index_file=None, template_dir=None): + """Create new CLiTable object. + Args: + index_file: String, file where template/command mappings reside. + template_dir: String, directory where index file and templates reside. + """ + # pylint: disable=E1002 + super(CliTable, self).__init__() + self._keys = set() + self.raw = None + self.index_file = index_file + self.template_dir = template_dir + if index_file: + self.ReadIndex(index_file) + + def ReadIndex(self, index_file=None): + """Reads the IndexTable index file of commands and templates. + Args: + index_file: String, file where template/command mappings reside. + Raises: + CliTableError: A template column was not found in the table. + """ + + self.index_file = index_file or self.index_file + fullpath = os.path.join(self.template_dir, self.index_file) + if self.index_file and fullpath not in self.INDEX: + self.index = IndexTable(self._PreParse, self._PreCompile, fullpath) + self.INDEX[fullpath] = self.index + else: + self.index = self.INDEX[fullpath] + + # Does the IndexTable have the right columns. + if 'Template' not in self.index.index.header: # pylint: disable=E1103 + raise CliTableError("Index file does not have 'Template' column.") + + def _TemplateNamesToFiles(self, template_str): + """Parses a string of templates into a list of file handles.""" + template_list = template_str.split(':') + template_files = [] + try: + for tmplt in template_list: + template_files.append( + open(os.path.join(self.template_dir, tmplt), 'r')) + except: # noqa + for tmplt in template_files: + tmplt.close() + raise + + return template_files + + def ParseCmd(self, cmd_input, attributes=None, templates=None): + """Creates a TextTable table of values from cmd_input string. + Parses command output with template/s. If more than one template is found + subsequent tables are merged if keys match (dropped otherwise). + Args: + cmd_input: String, Device/command response. + attributes: Dict, attribute that further refine matching template. + templates: String list of templates to parse with. If None, uses index + Raises: + CliTableError: A template was not found for the given command. + """ + # Store raw command data within the object. + self.raw = cmd_input + + if not templates: + # Find template in template index. + row_idx = self.index.GetRowMatch(attributes) + if row_idx: + templates = self.index.index[row_idx]['Template'] + else: + raise CliTableError('No template found for attributes: "%s"' % + attributes) + + template_files = self._TemplateNamesToFiles(templates) + + try: + # Re-initialise the table. + self.Reset() + self._keys = set() + self.table = self._ParseCmdItem(self.raw, template_file=template_files[0]) + + # Add additional columns from any additional tables. + for tmplt in template_files[1:]: + self.extend(self._ParseCmdItem(self.raw, template_file=tmplt), + set(self._keys)) + finally: + for f in template_files: + f.close() + + def _ParseCmdItem(self, cmd_input, template_file=None): + """Creates Texttable with output of command. + Args: + cmd_input: String, Device response. + template_file: File object, template to parse with. + Returns: + TextTable containing command output. + Raises: + CliTableError: A template was not found for the given command. + """ + # Build FSM machine from the template. + fsm = textfsm.TextFSM(template_file) + if not self._keys: + self._keys = set(fsm.GetValuesByAttrib('Key')) + + # Pass raw data through FSM. + table = texttable.TextTable() + table.header = fsm.header + + # Fill TextTable from record entries. + for record in fsm.ParseText(cmd_input): + table.Append(record) + return table + + def _PreParse(self, key, value): + """Executed against each field of each row read from index table.""" + if key == 'Command': + return re.sub(r'(\[\[.+?\]\])', self._Completion, value) + else: + return value + + def _PreCompile(self, key, value): + """Executed against each field of each row before compiling as regexp.""" + if key == 'Template': + return + else: + return value + + def _Completion(self, match): + # pylint: disable=C6114 + r"""Replaces double square brackets with variable length completion. + Completion cannot be mixed with regexp matching or '\' characters + i.e. '[[(\n)]] would become (\(n)?)?.' + Args: + match: A regex Match() object. + Returns: + String of the format '(a(b(c(d)?)?)?)?'. + """ + # Strip the outer '[[' & ']]' and replace with ()? regexp pattern. + word = str(match.group())[2:-2] + return '(' + ('(').join(word) + ')?' * len(word) + + def LabelValueTable(self, keys=None): + """Return LabelValue with FSM derived keys.""" + keys = keys or self.superkey + # pylint: disable=E1002 + return super(CliTable, self).LabelValueTable(keys) + + # pylint: disable=W0622,C6409 + def sort(self, cmp=None, key=None, reverse=False): + """Overrides sort func to use the KeyValue for the key.""" + if not key and self._keys: + key = self.KeyValue + super(CliTable, self).sort(cmp=cmp, key=key, reverse=reverse) + # pylint: enable=W0622 + + def AddKeys(self, key_list): + """Mark additional columns as being part of the superkey. + Supplements the Keys already extracted from the FSM template. + Useful when adding new columns to existing tables. + Note: This will impact attempts to further 'extend' the table as the + superkey must be common between tables for successful extension. + Args: + key_list: list of header entries to be included in the superkey. + Raises: + KeyError: If any entry in list is not a valid header entry. + """ + + for keyname in key_list: + if keyname not in self.header: + raise KeyError("'%s'" % keyname) + + self._keys = self._keys.union(set(key_list)) + + @property + def superkey(self): + """Returns a set of column names that together constitute the superkey.""" + sorted_list = [] + for header in self.header: + if header in self._keys: + sorted_list.append(header) + return sorted_list + + def KeyValue(self, row=None): + """Returns the super key value for the row.""" + if not row: + if self._iterator: + # If we are inside an iterator use current row iteration. + row = self[self._iterator] + else: + row = self.row + # If no superkey then use row number. + if not self.superkey: + return ['%s' % row.row] + + sorted_list = [] + for header in self.header: + if header in self.superkey: + sorted_list.append(row[header]) + return sorted_list diff --git a/netmiko/_textfsm/_terminal.py b/netmiko/_textfsm/_terminal.py new file mode 100644 index 000000000..c94eb8997 --- /dev/null +++ b/netmiko/_textfsm/_terminal.py @@ -0,0 +1,110 @@ +""" +Google's clitable.py is inherently integrated to Linux. + +This is a workaround for that (basically include modified clitable code without anything +that is Linux-specific). + +_clitable.py is identical to Google's as of 2017-12-17 +_texttable.py is identical to Google's as of 2017-12-17 +_terminal.py is a highly stripped down version of Google's such that clitable.py works + +https://github.com/google/textfsm/blob/master/clitable.py +""" + +# Some of this code is from Google with the following license: +# +# Copyright 2012 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import re + + +__version__ = '0.1.1' + + +# ANSI, ISO/IEC 6429 escape sequences, SGR (Select Graphic Rendition) subset. +SGR = { + 'reset': 0, + 'bold': 1, + 'underline': 4, + 'blink': 5, + 'negative': 7, + 'underline_off': 24, + 'blink_off': 25, + 'positive': 27, + 'black': 30, + 'red': 31, + 'green': 32, + 'yellow': 33, + 'blue': 34, + 'magenta': 35, + 'cyan': 36, + 'white': 37, + 'fg_reset': 39, + 'bg_black': 40, + 'bg_red': 41, + 'bg_green': 42, + 'bg_yellow': 43, + 'bg_blue': 44, + 'bg_magenta': 45, + 'bg_cyan': 46, + 'bg_white': 47, + 'bg_reset': 49, + } + +# Provide a familar descriptive word for some ansi sequences. +FG_COLOR_WORDS = {'black': ['black'], + 'dark_gray': ['bold', 'black'], + 'blue': ['blue'], + 'light_blue': ['bold', 'blue'], + 'green': ['green'], + 'light_green': ['bold', 'green'], + 'cyan': ['cyan'], + 'light_cyan': ['bold', 'cyan'], + 'red': ['red'], + 'light_red': ['bold', 'red'], + 'purple': ['magenta'], + 'light_purple': ['bold', 'magenta'], + 'brown': ['yellow'], + 'yellow': ['bold', 'yellow'], + 'light_gray': ['white'], + 'white': ['bold', 'white']} + +BG_COLOR_WORDS = {'black': ['bg_black'], + 'red': ['bg_red'], + 'green': ['bg_green'], + 'yellow': ['bg_yellow'], + 'dark_blue': ['bg_blue'], + 'purple': ['bg_magenta'], + 'light_blue': ['bg_cyan'], + 'grey': ['bg_white']} + + +# Characters inserted at the start and end of ANSI strings +# to provide hinting for readline and other clients. +ANSI_START = '\001' +ANSI_END = '\002' + + +sgr_re = re.compile(r'(%s?\033\[\d+(?:;\d+)*m%s?)' % ( + ANSI_START, ANSI_END)) + + +def StripAnsiText(text): + """Strip ANSI/SGR escape sequences from text.""" + return sgr_re.sub('', text) diff --git a/netmiko/_textfsm/_texttable.py b/netmiko/_textfsm/_texttable.py new file mode 100644 index 000000000..1202b34de --- /dev/null +++ b/netmiko/_textfsm/_texttable.py @@ -0,0 +1,1103 @@ +""" +Google's clitable.py is inherently integrated to Linux: + +This is a workaround for that (basically include modified clitable code without anything +that is Linux-specific). + +_clitable.py is identical to Google's as of 2017-12-17 +_texttable.py is identical to Google's as of 2017-12-17 +_terminal.py is a highly stripped down version of Google's such that clitable.py works + +https://github.com/google/textfsm/blob/master/clitable.py + +A module to represent and manipulate tabular text data. + +A table of rows, indexed on row number. Each row is a ordered dictionary of row +elements that maintains knowledge of the parent table and column headings. + +Tables can be created from CSV input and in-turn supports a number of display +formats such as CSV and variable sized and justified rows. +""" + +# Some of this code is from Google with the following license: +# +# Copyright 2012 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +import copy +from functools import cmp_to_key +import textwrap +# pylint: disable=redefined-builtin +from six.moves import range +from netmiko._textfsm import _terminal as terminal + + +class Error(Exception): + """Base class for errors.""" + + +class TableError(Error): + """Error in TextTable.""" + + +class Row(dict): + """Represents a table row. We implement this as an ordered dictionary. + + The order is the chronological order of data insertion. Methods are supplied + to make it behave like a regular dict() and list(). + + Attributes: + row: int, the row number in the container table. 0 is the header row. + table: A TextTable(), the associated container table. + """ + + def __init__(self, *args, **kwargs): + super(Row, self).__init__(*args, **kwargs) + self._keys = list() + self._values = list() + self.row = None + self.table = None + self._color = None + self._index = {} + + def _BuildIndex(self): + """Recreate the key index.""" + self._index = {} + for i, k in enumerate(self._keys): + self._index[k] = i + + def __getitem__(self, column): + """Support for [] notation. + + Args: + column: Tuple of column names, or a (str) column name, or positional + column number, 0-indexed. + + Returns: + A list or string with column value(s). + + Raises: + IndexError: The given column(s) were not found. + """ + if isinstance(column, (list, tuple)): + ret = [] + for col in column: + ret.append(self[col]) + return ret + + try: + return self._values[self._index[column]] + except (KeyError, TypeError, ValueError): + pass + + # Perhaps we have a range like '1', ':-1' or '1:'. + try: + return self._values[column] + except (IndexError, TypeError): + pass + + raise IndexError('No such column "%s" in row.' % column) + + def __contains__(self, value): + return value in self._values + + def __setitem__(self, column, value): + for i in range(len(self)): + if self._keys[i] == column: + self._values[i] = value + return + # No column found, add a new one. + self._keys.append(column) + self._values.append(value) + self._BuildIndex() + + def __iter__(self): + return iter(self._values) + + def __len__(self): + return len(self._keys) + + def __str__(self): + ret = '' + for v in self._values: + ret += '%12s ' % v + ret += '\n' + return ret + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, str(self)) + + def get(self, column, default_value=None): + """Get an item from the Row by column name. + + Args: + column: Tuple of column names, or a (str) column name, or positional + column number, 0-indexed. + default_value: The value to use if the key is not found. + + Returns: + A list or string with column value(s) or default_value if not found. + """ + if isinstance(column, (list, tuple)): + ret = [] + for col in column: + ret.append(self.get(col, default_value)) + return ret + # Perhaps we have a range like '1', ':-1' or '1:'. + try: + return self._values[column] + except (IndexError, TypeError): + pass + try: + return self[column] + except IndexError: + return default_value + + def index(self, column): # pylint: disable=C6409 + """Fetches the column number (0 indexed). + + Args: + column: A string, column to fetch the index of. + + Returns: + An int, the row index number. + + Raises: + ValueError: The specified column was not found. + """ + for i, key in enumerate(self._keys): + if key == column: + return i + raise ValueError('Column "%s" not found.' % column) + + def iterkeys(self): + return iter(self._keys) + + def items(self): + # TODO(harro): self.get(k) should work here but didn't ? + return [(k, self.__getitem__(k)) for k in self._keys] + + def _GetValues(self): + """Return the row's values.""" + return self._values + + def _GetHeader(self): + """Return the row's header.""" + return self._keys + + def _SetHeader(self, values): + """Set the row's header from a list.""" + if self._values and len(values) != len(self._values): + raise ValueError('Header values not equal to existing data width.') + if not self._values: + for _ in range(len(values)): + self._values.append(None) + self._keys = list(values) + self._BuildIndex() + + def _SetColour(self, value_list): + """Sets row's colour attributes to a list of values in terminal.SGR.""" + if value_list is None: + self._color = None + return + colors = [] + for color in value_list: + if color in terminal.SGR: + colors.append(color) + elif color in terminal.FG_COLOR_WORDS: + colors += terminal.FG_COLOR_WORDS[color] + elif color in terminal.BG_COLOR_WORDS: + colors += terminal.BG_COLOR_WORDS[color] + else: + raise ValueError('Invalid colour specification.') + self._color = list(set(colors)) + + def _GetColour(self): + if self._color is None: + return None + return list(self._color) + + def _SetValues(self, values): + """Set values from supplied dictionary or list. + + Args: + values: A Row, dict indexed by column name, or list. + + Raises: + TypeError: Argument is not a list or dict, or list is not equal row + length or dictionary keys don't match. + """ + + def _ToStr(value): + """Convert individul list entries to string.""" + if isinstance(value, (list, tuple)): + result = [] + for val in value: + result.append(str(val)) + return result + else: + return str(value) + + # Row with identical header can be copied directly. + if isinstance(values, Row): + if self._keys != values.header: + raise TypeError('Attempt to append row with mismatched header.') + self._values = copy.deepcopy(values.values) + + elif isinstance(values, dict): + for key in self._keys: + if key not in values: + raise TypeError('Dictionary key mismatch with row.') + for key in self._keys: + self[key] = _ToStr(values[key]) + + elif isinstance(values, list) or isinstance(values, tuple): + if len(values) != len(self._values): + raise TypeError('Supplied list length != row length') + for (index, value) in enumerate(values): + self._values[index] = _ToStr(value) + + else: + raise TypeError('Supplied argument must be Row, dict or list, not %s', + type(values)) + + def Insert(self, key, value, row_index): + """Inserts new values at a specified offset. + + Args: + key: string for header value. + value: string for a data value. + row_index: Offset into row for data. + + Raises: + IndexError: If the offset is out of bands. + """ + if row_index < 0: + row_index += len(self) + + if not 0 <= row_index < len(self): + raise IndexError('Index "%s" is out of bounds.' % row_index) + + new_row = Row() + for idx in self.header: + if self.index(idx) == row_index: + new_row[key] = value + new_row[idx] = self[idx] + self._keys = new_row.header + self._values = new_row.values + del new_row + self._BuildIndex() + + color = property(_GetColour, _SetColour, doc='Colour spec of this row') + header = property(_GetHeader, _SetHeader, doc="List of row's headers.") + values = property(_GetValues, _SetValues, doc="List of row's values.") + + +class TextTable(object): + """Class that provides data methods on a tabular format. + + Data is stored as a list of Row() objects. The first row is always present as + the header row. + + Attributes: + row_class: class, A class to use for the Row object. + separator: str, field separator when printing table. + """ + + def __init__(self, row_class=Row): + """Initialises a new table. + + Args: + row_class: A class to use as the row object. This should be a + subclass of this module's Row() class. + """ + self.row_class = row_class + self.separator = ', ' + self.Reset() + + def Reset(self): + self._row_index = 1 + self._table = [[]] + self._iterator = 0 # While loop row index + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, str(self)) + + def __str__(self): + """Displays table with pretty formatting.""" + return self.table + + def __incr__(self, incr=1): + self._SetRowIndex(self._row_index + incr) + + def __contains__(self, name): + """Whether the given column header name exists.""" + return name in self.header + + def __getitem__(self, row): + """Fetches the given row number.""" + return self._table[row] + + def __iter__(self): + """Iterator that excludes the header row.""" + return self.next() + + def next(self): + # Maintain a counter so a row can know what index it is. + # Save the old value to support nested interations. + old_iter = self._iterator + try: + for r in self._table[1:]: + self._iterator = r.row + yield r + finally: + # Recover the original index after loop termination or exit with break. + self._iterator = old_iter + + def __add__(self, other): + """Merges two with identical columns.""" + + new_table = copy.copy(self) + for row in other: + new_table.Append(row) + + return new_table + + def __copy__(self): + """Copy table instance.""" + + new_table = self.__class__() + # pylint: disable=protected-access + new_table._table = [self.header] + for row in self[1:]: + new_table.Append(row) + return new_table + + def Filter(self, function=None): + """Construct Textable from the rows of which the function returns true. + + + Args: + function: A function applied to each row which returns a bool. If + function is None, all rows with empty column values are + removed. + Returns: + A new TextTable() + + Raises: + TableError: When an invalid row entry is Append()'d + """ + flat = lambda x: x if isinstance(x, str) else ''.join([flat(y) for y in x]) # noqa + if function is None: + function = lambda row: bool(flat(row.values)) # noqa + + new_table = self.__class__() + # pylint: disable=protected-access + new_table._table = [self.header] + for row in self: + if function(row) is True: + new_table.Append(row) + return new_table + + def Map(self, function): + """Applies the function to every row in the table. + + Args: + function: A function applied to each row. + + Returns: + A new TextTable() + + Raises: + TableError: When transform is not invalid row entry. The transform + must be compatible with Append(). + """ + new_table = self.__class__() + # pylint: disable=protected-access + new_table._table = [self.header] + for row in self: + filtered_row = function(row) + if filtered_row: + new_table.Append(filtered_row) + return new_table + + # pylint: disable=C6409 + # pylint: disable=W0622 + def sort(self, cmp=None, key=None, reverse=False): + """Sorts rows in the texttable. + + Args: + cmp: func, non default sort algorithm to use. + key: func, applied to each element before sorting. + reverse: bool, reverse order of sort. + """ + + def _DefaultKey(value): + """Default key func is to create a list of all fields.""" + result = [] + for key in self.header: + # Try sorting as numerical value if possible. + try: + result.append(float(value[key])) + except ValueError: + result.append(value[key]) + return result + + key = key or _DefaultKey + # Exclude header by copying table. + new_table = self._table[1:] + + if cmp is not None: + key = cmp_to_key(cmp) + + new_table.sort(key=key, reverse=reverse) + + # Regenerate the table with original header + self._table = [self.header] + self._table.extend(new_table) + # Re-write the 'row' attribute of each row + for index, row in enumerate(self._table): + row.row = index + # pylint: enable=W0622 + + def extend(self, table, keys=None): + """Extends all rows in the texttable. + + The rows are extended with the new columns from the table. + + Args: + table: A texttable, the table to extend this table by. + keys: A set, the set of columns to use as the key. If None, the + row index is used. + + Raises: + IndexError: If key is not a valid column name. + """ + if keys: + for k in keys: + if k not in self._Header(): + raise IndexError("Unknown key: '%s'", k) + + extend_with = [] + for column in table.header: + if column not in self.header: + extend_with.append(column) + + if not extend_with: + return + + for column in extend_with: + self.AddColumn(column) + + if not keys: + for row1, row2 in zip(self, table): + for column in extend_with: + row1[column] = row2[column] + return + + for row1 in self: + for row2 in table: + for k in keys: + if row1[k] != row2[k]: + break + else: + for column in extend_with: + row1[column] = row2[column] + break + + # pylint: enable=C6409 + def Remove(self, row): + """Removes a row from the table. + + Args: + row: int, the row number to delete. Must be >= 1, as the header + cannot be removed. + + Raises: + TableError: Attempt to remove nonexistent or header row. + """ + if row == 0 or row > self.size: + raise TableError('Attempt to remove header row') + new_table = [] + # pylint: disable=E1103 + for t_row in self._table: + if t_row.row != row: + new_table.append(t_row) + if t_row.row > row: + t_row.row -= 1 + self._table = new_table + + def _Header(self): + """Returns the header row.""" + return self._table[0] + + def _GetRow(self, columns=None): + """Returns the current row as a tuple.""" + + row = self._table[self._row_index] + if columns: + result = [] + for col in columns: + if col not in self.header: + raise TableError('Column header %s not known in table.' % col) + result.append(row[self.header.index(col)]) + row = result + return row + + def _SetRow(self, new_values, row=0): + """Sets the current row to new list. + + Args: + new_values: List|dict of new values to insert into row. + row: int, Row to insert values into. + + Raises: + TableError: If number of new values is not equal to row size. + """ + + if not row: + row = self._row_index + + if row > self.size: + raise TableError('Entry %s beyond table size %s.' % (row, self.size)) + + self._table[row].values = new_values + + def _SetHeader(self, new_values): + """Sets header of table to the given tuple. + + Args: + new_values: Tuple of new header values. + """ + row = self.row_class() + row.row = 0 + for v in new_values: + row[v] = v + self._table[0] = row + + def _SetRowIndex(self, row): + if not row or row > self.size: + raise TableError('Entry %s beyond table size %s.' % (row, self.size)) + self._row_index = row + + def _GetRowIndex(self): + return self._row_index + + def _GetSize(self): + """Returns number of rows in table.""" + + if not self._table: + return 0 + return len(self._table) - 1 + + def _GetTable(self): + """Returns table, with column headers and separators. + + Returns: + The whole table including headers as a string. Each row is + joined by a newline and each entry by self.separator. + """ + result = [] + # Avoid the global lookup cost on each iteration. + lstr = str + for row in self._table: + result.append( + '%s\n' % + self.separator.join(lstr(v) for v in row)) + + return ''.join(result) + + def _SetTable(self, table): + """Sets table, with column headers and separators.""" + if not isinstance(table, TextTable): + raise TypeError('Not an instance of TextTable.') + self.Reset() + self._table = copy.deepcopy(table._table) # pylint: disable=W0212 + # Point parent table of each row back ourselves. + for row in self: + row.table = self + + def _SmallestColSize(self, text): + """Finds the largest indivisible word of a string. + + ...and thus the smallest possible column width that can contain that + word unsplit over rows. + + Args: + text: A string of text potentially consisting of words. + + Returns: + Integer size of the largest single word in the text. + """ + if not text: + return 0 + stripped = terminal.StripAnsiText(text) + return max(len(word) for word in stripped.split()) + + def _TextJustify(self, text, col_size): + """Formats text within column with white space padding. + + A single space is prefixed, and a number of spaces are added as a + suffix such that the length of the resultant string equals the col_size. + + If the length of the text exceeds the column width available then it + is split into words and returned as a list of string, each string + contains one or more words padded to the column size. + + Args: + text: String of text to format. + col_size: integer size of column to pad out the text to. + + Returns: + List of strings col_size in length. + + Raises: + TableError: If col_size is too small to fit the words in the text. + """ + result = [] + if '\n' in text: + for paragraph in text.split('\n'): + result.extend(self._TextJustify(paragraph, col_size)) + return result + + wrapper = textwrap.TextWrapper(width=col_size-2, break_long_words=False, + expand_tabs=False) + try: + text_list = wrapper.wrap(text) + except ValueError: + raise TableError('Field too small (minimum width: 3)') + + if not text_list: + return [' '*col_size] + + for current_line in text_list: + stripped_len = len(terminal.StripAnsiText(current_line)) + ansi_color_adds = len(current_line) - stripped_len + # +2 for white space on either side. + if stripped_len + 2 > col_size: + raise TableError('String contains words that do not fit in column.') + + result.append(' %-*s' % (col_size - 1 + ansi_color_adds, current_line)) + + return result + + def FormattedTable(self, width=80, force_display=False, ml_delimiter=True, + color=True, display_header=True, columns=None): + """Returns whole table, with whitespace padding and row delimiters. + + Args: + width: An int, the max width we want the table to fit in. + force_display: A bool, if set to True will display table when the table + can't be made to fit to the width. + ml_delimiter: A bool, if set to False will not display the multi-line + delimiter. + color: A bool. If true, display any colours in row.colour. + display_header: A bool. If true, display header. + columns: A list of str, show only columns with these names. + + Returns: + A string. The tabled output. + + Raises: + TableError: Width too narrow to display table. + """ + + def _FilteredCols(): + """Returns list of column names to display.""" + if not columns: + return self._Header().values + return [col for col in self._Header().values if col in columns] + + # Largest is the biggest data entry in a column. + largest = {} + # Smallest is the same as above but with linewrap i.e. largest unbroken + # word in the data stream. + smallest = {} + # largest == smallest for a column with a single word of data. + # Initialise largest and smallest for all columns. + for key in _FilteredCols(): + largest[key] = 0 + smallest[key] = 0 + + # Find the largest and smallest values. + # Include Title line in equation. + # pylint: disable=E1103 + for row in self._table: + for key, value in row.items(): + if key not in _FilteredCols(): + continue + # Convert lists into a string. + if isinstance(value, list): + value = ', '.join(value) + value = terminal.StripAnsiText(value) + largest[key] = max(len(value), largest[key]) + smallest[key] = max(self._SmallestColSize(value), smallest[key]) + # pylint: enable=E1103 + + min_total_width = 0 + multi_word = [] + # Bump up the size of each column to include minimum pad. + # Find all columns that can be wrapped (multi-line). + # And the minimum width needed to display all columns (even if wrapped). + for key in _FilteredCols(): + # Each column is bracketed by a space on both sides. + # So increase size required accordingly. + largest[key] += 2 + smallest[key] += 2 + min_total_width += smallest[key] + # If column contains data that 'could' be split over multiple lines. + if largest[key] != smallest[key]: + multi_word.append(key) + + # Check if we have enough space to display the table. + if min_total_width > width and not force_display: + raise TableError('Width too narrow to display table.') + + # We have some columns that may need wrapping over several lines. + if multi_word: + # Find how much space is left over for the wrapped columns to use. + # Also find how much space we would need if they were not wrapped. + # These are 'spare_width' and 'desired_width' respectively. + desired_width = 0 + spare_width = width - min_total_width + for key in multi_word: + spare_width += smallest[key] + desired_width += largest[key] + + # Scale up the space we give each wrapped column. + # Proportional to its size relative to 'desired_width' for all columns. + # Rinse and repeat if we changed the wrap list in this iteration. + # Once done we will have a list of columns that definitely need wrapping. + done = False + while not done: + done = True + for key in multi_word: + # If we scale past the desired width for this particular column, + # then give it its desired width and remove it from the wrapped list. + if (largest[key] <= round((largest[key] / float(desired_width)) * spare_width)): + smallest[key] = largest[key] + multi_word.remove(key) + spare_width -= smallest[key] + desired_width -= largest[key] + done = False + # If we scale below the minimum width for this particular column, + # then leave it at its minimum and remove it from the wrapped list. + elif (smallest[key] >= + round((largest[key] / float(desired_width)) * spare_width)): + multi_word.remove(key) + spare_width -= smallest[key] + desired_width -= largest[key] + done = False + + # Repeat the scaling algorithm with the final wrap list. + # This time we assign the extra column space by increasing 'smallest'. + for key in multi_word: + smallest[key] = int(round((largest[key] / float(desired_width)) + * spare_width)) + + total_width = 0 + row_count = 0 + result_dict = {} + # Format the header lines and add to result_dict. + # Find what the total width will be and use this for the ruled lines. + # Find how many rows are needed for the most wrapped line (row_count). + for key in _FilteredCols(): + result_dict[key] = self._TextJustify(key, smallest[key]) + if len(result_dict[key]) > row_count: + row_count = len(result_dict[key]) + total_width += smallest[key] + + # Store header in header_list, working down the wrapped rows. + header_list = [] + for row_idx in range(row_count): + for key in _FilteredCols(): + try: + header_list.append(result_dict[key][row_idx]) + except IndexError: + # If no value than use whitespace of equal size. + header_list.append(' '*smallest[key]) + header_list.append('\n') + + # Format and store the body lines + result_dict = {} + body_list = [] + # We separate multi line rows with a single line delimiter. + prev_muli_line = False + # Unless it is the first line in which there is already the header line. + first_line = True + for row in self: + row_count = 0 + for key, value in row.items(): + if key not in _FilteredCols(): + continue + # Convert field contents to a string. + if isinstance(value, list): + value = ', '.join(value) + # Store results in result_dict and take note of wrapped line count. + result_dict[key] = self._TextJustify(value, smallest[key]) + if len(result_dict[key]) > row_count: + row_count = len(result_dict[key]) + + if row_count > 1: + prev_muli_line = True + # If current or prior line was multi-line then include delimiter. + if not first_line and prev_muli_line and ml_delimiter: + body_list.append('-'*total_width + '\n') + if row_count == 1: + # Our current line was not wrapped, so clear flag. + prev_muli_line = False + + row_list = [] + for row_idx in range(row_count): + for key in _FilteredCols(): + try: + row_list.append(result_dict[key][row_idx]) + except IndexError: + # If no value than use whitespace of equal size. + row_list.append(' '*smallest[key]) + row_list.append('\n') + + if color and row.color is not None: + # Don't care about colors + body_list.append(''.join(row_list)) + # body_list.append( + # terminal.AnsiText(''.join(row_list)[:-1], + # command_list=row.color)) + # body_list.append('\n') + else: + body_list.append(''.join(row_list)) + + first_line = False + + header = ''.join(header_list) + '='*total_width + if color and self._Header().color is not None: + pass + # header = terminal.AnsiText(header, command_list=self._Header().color) + # Add double line delimiter between header and main body. + if display_header: + return '%s\n%s' % (header, ''.join(body_list)) + return '%s' % ''.join(body_list) + + def LabelValueTable(self, label_list=None): + """Returns whole table as rows of name/value pairs. + + One (or more) column entries are used for the row prefix label. + The remaining columns are each displayed as a row entry with the + prefix labels appended. + + Use the first column as the label if label_list is None. + + Args: + label_list: A list of prefix labels to use. + + Returns: + Label/Value formatted table. + + Raises: + TableError: If specified label is not a column header of the table. + """ + label_list = label_list or self._Header()[0] + # Ensure all labels are valid. + for label in label_list: + if label not in self._Header(): + raise TableError('Invalid label prefix: %s.' % label) + + sorted_list = [] + for header in self._Header(): + if header in label_list: + sorted_list.append(header) + + label_str = '# LABEL %s\n' % '.'.join(sorted_list) + + body = [] + for row in self: + # Some of the row values are pulled into the label, stored in label_prefix. + label_prefix = [] + value_list = [] + for key, value in row.items(): + if key in sorted_list: + # Set prefix. + label_prefix.append(value) + else: + value_list.append('%s %s' % (key, value)) + + body.append(''.join( + ['%s.%s\n' % ('.'.join(label_prefix), v) for v in value_list])) + + return '%s%s' % (label_str, ''.join(body)) + + table = property(_GetTable, _SetTable, doc='Whole table') + row = property(_GetRow, _SetRow, doc='Current row') + header = property(_Header, _SetHeader, doc='List of header entries.') + row_index = property(_GetRowIndex, _SetRowIndex, doc='Current row.') + size = property(_GetSize, doc='Number of rows in table.') + + def RowWith(self, column, value): + """Retrieves the first non header row with the column of the given value. + + Args: + column: str, the name of the column to check. + value: str, The value of the column to check. + + Returns: + A Row() of the first row found, None otherwise. + + Raises: + IndexError: The specified column does not exist. + """ + for row in self._table[1:]: + if row[column] == value: + return row + return None + + def AddColumn(self, column, default='', col_index=-1): + """Appends a new column to the table. + + Args: + column: A string, name of the column to add. + default: Default value for entries. Defaults to ''. + col_index: Integer index for where to insert new column. + + Raises: + TableError: Column name already exists. + + """ + if column in self.table: + raise TableError('Column %r already in table.' % column) + if col_index == -1: + self._table[0][column] = column + for i in range(1, len(self._table)): + self._table[i][column] = default + else: + self._table[0].Insert(column, column, col_index) + for i in range(1, len(self._table)): + self._table[i].Insert(column, default, col_index) + + def Append(self, new_values): + """Adds a new row (list) to the table. + + Args: + new_values: Tuple, dict, or Row() of new values to append as a row. + + Raises: + TableError: Supplied tuple not equal to table width. + """ + newrow = self.NewRow() + newrow.values = new_values + self._table.append(newrow) + + def NewRow(self, value=''): + """Fetches a new, empty row, with headers populated. + + Args: + value: Initial value to set each row entry to. + + Returns: + A Row() object. + """ + newrow = self.row_class() + newrow.row = self.size + 1 + newrow.table = self + headers = self._Header() + for header in headers: + newrow[header] = value + return newrow + + def CsvToTable(self, buf, header=True, separator=','): + """Parses buffer into tabular format. + + Strips off comments (preceded by '#'). + Optionally parses and indexes by first line (header). + + Args: + buf: String file buffer containing CSV data. + header: Is the first line of buffer a header. + separator: String that CSV is separated by. + + Returns: + int, the size of the table created. + + Raises: + TableError: A parsing error occurred. + """ + self.Reset() + + header_row = self.row_class() + if header: + line = buf.readline() + header_str = '' + while not header_str: + # Remove comments. + header_str = line.split('#')[0].strip() + if not header_str: + line = buf.readline() + + header_list = header_str.split(separator) + header_length = len(header_list) + + for entry in header_list: + entry = entry.strip() + if entry in header_row: + raise TableError('Duplicate header entry %r.' % entry) + + header_row[entry] = entry + header_row.row = 0 + self._table[0] = header_row + + # xreadlines would be better but not supported by StringIO for testing. + for line in buf: + # Support commented lines, provide '#' is first character of line. + if line.startswith('#'): + continue + + lst = line.split(separator) + lst = [l.strip() for l in lst] + if header and len(lst) != header_length: + # Silently drop illegal line entries + continue + if not header: + header_row = self.row_class() + header_length = len(lst) + header_row.values = dict(zip(range(header_length), + range(header_length))) + self._table[0] = header_row + header = True + continue + + new_row = self.NewRow() + new_row.values = lst + header_row.row = self.size + 1 + self._table.append(new_row) + + return self.size + + def index(self, name=None): # pylint: disable=C6409 + """Returns index number of supplied column name. + + Args: + name: string of column name. + + Raises: + TableError: If name not found. + + Returns: + Index of the specified header entry. + """ + try: + return self.header.index(name) + except ValueError: + raise TableError('Unknown index name %s.' % name) diff --git a/netmiko/a10/a10_ssh.py b/netmiko/a10/a10_ssh.py index 2f721eb2f..dd9e752a0 100644 --- a/netmiko/a10/a10_ssh.py +++ b/netmiko/a10/a10_ssh.py @@ -1,5 +1,6 @@ """A10 support.""" from __future__ import unicode_literals +import time from netmiko.cisco_base_connection import CiscoSSHConnection @@ -10,7 +11,15 @@ def session_preparation(self): self._test_channel_read() self.set_base_prompt() self.enable() - self.disable_paging(command="terminal length 0\n") + self.disable_paging(command="terminal length 0") # Will not do anything without A10 specific command self.set_terminal_width() + + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() + + def save_config(self, cmd='', confirm=True, confirm_response=''): + """Not Implemented""" + raise NotImplementedError diff --git a/netmiko/accedian/accedian_ssh.py b/netmiko/accedian/accedian_ssh.py index aa0bbcc47..1ffabfa42 100644 --- a/netmiko/accedian/accedian_ssh.py +++ b/netmiko/accedian/accedian_ssh.py @@ -1,5 +1,5 @@ - from __future__ import unicode_literals +import time from netmiko.cisco_base_connection import CiscoSSHConnection @@ -7,6 +7,9 @@ class AccedianSSH(CiscoSSHConnection): def session_preparation(self): self._test_channel_read() self.set_base_prompt() + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def check_enable_mode(self, *args, **kwargs): raise AttributeError("Accedian devices do not support enable mode!") @@ -36,3 +39,7 @@ def set_base_prompt(self, pri_prompt_terminator=':', alt_prompt_terminator='#', alt_prompt_terminator=alt_prompt_terminator, delay_factor=delay_factor) return self.base_prompt + + def save_config(self, cmd='', confirm=True, confirm_response=''): + """Not Implemented""" + raise NotImplementedError diff --git a/netmiko/alcatel/alcatel_aos_ssh.py b/netmiko/alcatel/alcatel_aos_ssh.py index 5e1f0fcac..9c2c58c1f 100644 --- a/netmiko/alcatel/alcatel_aos_ssh.py +++ b/netmiko/alcatel/alcatel_aos_ssh.py @@ -1,6 +1,7 @@ """Alcatel-Lucent Enterprise AOS support (AOS6 and AOS8).""" from __future__ import print_function from __future__ import unicode_literals +import time from netmiko.cisco_base_connection import CiscoSSHConnection @@ -10,6 +11,9 @@ def session_preparation(self): # Prompt can be anything, but best practice is to end with > or # self._test_channel_read(pattern=r'[>#]') self.set_base_prompt() + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def check_enable_mode(self, *args, **kwargs): """No enable mode on AOS""" @@ -34,3 +38,7 @@ def config_mode(self, *args, **kwargs): def exit_config_mode(self, *args, **kwargs): """No config mode on AOS""" return '' + + def save_config(self, cmd='write memory flash-synchro', confirm=False): + """Save Config""" + return super(AlcatelAosSSH, self).save_config(cmd=cmd, confirm=confirm) diff --git a/netmiko/alcatel/alcatel_sros_ssh.py b/netmiko/alcatel/alcatel_sros_ssh.py index 8c7054c32..21ec9cf0b 100644 --- a/netmiko/alcatel/alcatel_sros_ssh.py +++ b/netmiko/alcatel/alcatel_sros_ssh.py @@ -2,6 +2,7 @@ from __future__ import print_function from __future__ import unicode_literals import re +import time from netmiko.cisco_base_connection import CiscoSSHConnection @@ -10,7 +11,10 @@ class AlcatelSrosSSH(CiscoSSHConnection): def session_preparation(self): self._test_channel_read() self.set_base_prompt() - self.disable_paging(command="environment no more\n") + self.disable_paging(command="environment no more") + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def set_base_prompt(self, *args, **kwargs): """Remove the > when navigating into the different config level.""" @@ -38,3 +42,7 @@ def check_config_mode(self, check_string='config', pattern='#'): """ Checks if the device is in configuration mode or not. """ return super(AlcatelSrosSSH, self).check_config_mode(check_string=check_string, pattern=pattern) + + def save_config(self, cmd='', confirm=True, confirm_response=''): + """Not Implemented""" + raise NotImplementedError diff --git a/netmiko/arista/__init__.py b/netmiko/arista/__init__.py index fdbc7f530..7573444ff 100644 --- a/netmiko/arista/__init__.py +++ b/netmiko/arista/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals -from netmiko.arista.arista_ssh import AristaSSH +from netmiko.arista.arista_ssh import AristaSSH, AristaFileTransfer -__all__ = ['AristaSSH'] +__all__ = ['AristaSSH', 'AristaFileTransfer'] diff --git a/netmiko/arista/arista_ssh.py b/netmiko/arista/arista_ssh.py index 5de156be4..f0605db34 100644 --- a/netmiko/arista/arista_ssh.py +++ b/netmiko/arista/arista_ssh.py @@ -1,5 +1,8 @@ from __future__ import unicode_literals +import time +import re from netmiko.cisco_base_connection import CiscoSSHConnection +from netmiko.cisco_base_connection import CiscoFileTransfer from netmiko import log @@ -10,6 +13,9 @@ def session_preparation(self): self.set_base_prompt() self.disable_paging() self.set_terminal_width(command='terminal width 511') + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def check_config_mode(self, check_string=')#', pattern=''): """ @@ -21,10 +27,102 @@ def check_config_mode(self, check_string=')#', pattern=''): Can also be (s2) """ log.debug("pattern: {0}".format(pattern)) - self.write_channel('\n') + self.write_channel(self.RETURN) output = self.read_until_pattern(pattern=pattern) log.debug("check_config_mode: {0}".format(repr(output))) output = output.replace("(s1)", "") output = output.replace("(s2)", "") log.debug("check_config_mode: {0}".format(repr(output))) return check_string in output + + +class AristaFileTransfer(CiscoFileTransfer): + """Arista SCP File Transfer driver.""" + def __init__(self, ssh_conn, source_file, dest_file, file_system=None, direction='put'): + msg = "Arista SCP Driver is under development and not fully implemented" + raise NotImplementedError(msg) + self.ssh_ctl_chan = ssh_conn + self.source_file = source_file + self.dest_file = dest_file + self.direction = direction + + if file_system: + self.file_system = file_system + else: + raise ValueError("Destination file system must be specified for Arista") + + # if direction == 'put': + # self.source_md5 = self.file_md5(source_file) + # self.file_size = os.stat(source_file).st_size + # elif direction == 'get': + # self.source_md5 = self.remote_md5(remote_file=source_file) + # self.file_size = self.remote_file_size(remote_file=source_file) + # else: + # raise ValueError("Invalid direction specified") + + def put_file(self): + """SCP copy the file from the local system to the remote device.""" + destination = "{}/{}".format(self.file_system, self.dest_file) + self.scp_conn.scp_transfer_file(self.source_file, destination) + # Must close the SCP connection to get the file written (flush) + self.scp_conn.close() + + def remote_space_available(self, search_pattern=r"(\d+) bytes free"): + """Return space available on remote device.""" + return super(AristaFileTransfer, self).remote_space_available( + search_pattern=search_pattern + ) + + def verify_space_available(self, search_pattern=r"(\d+) bytes free"): + """Verify sufficient space is available on destination file system (return boolean).""" + return super(AristaFileTransfer, self).verify_space_available( + search_pattern=search_pattern + ) + + def check_file_exists(self, remote_cmd=""): + """Check if the dest_file already exists on the file system (return boolean).""" + raise NotImplementedError + + def remote_file_size(self, remote_cmd="", remote_file=None): + """Get the file size of the remote file.""" + if remote_file is None: + if self.direction == 'put': + remote_file = self.dest_file + elif self.direction == 'get': + remote_file = self.source_file + + if not remote_cmd: + remote_cmd = "dir {}/{}".format(self.file_system, remote_file) + + remote_out = self.ssh_ctl_chan.send_command(remote_cmd) + # Match line containing file name + escape_file_name = re.escape(remote_file) + pattern = r".*({}).*".format(escape_file_name) + match = re.search(pattern, remote_out) + if match: + file_size = match.group(0) + file_size = file_size.split()[0] + + if 'No such file or directory' in remote_out: + raise IOError("Unable to find file on remote system") + else: + return int(file_size) + + @staticmethod + def process_md5(md5_output, pattern=r"= (.*)"): + raise NotImplementedError + + def remote_md5(self, base_cmd='show file', remote_file=None): + if remote_file is None: + if self.direction == 'put': + remote_file = self.dest_file + elif self.direction == 'get': + remote_file = self.source_file + remote_md5_cmd = "{} {}{} md5sum".format(base_cmd, self.file_system, remote_file) + return self.ssh_ctl_chan.send_command(remote_md5_cmd, delay_factor=3.0) + + def enable_scp(self, cmd=None): + raise NotImplementedError + + def disable_scp(self, cmd=None): + raise NotImplementedError diff --git a/netmiko/aruba/aruba_ssh.py b/netmiko/aruba/aruba_ssh.py index b1709b23e..98e929b29 100644 --- a/netmiko/aruba/aruba_ssh.py +++ b/netmiko/aruba/aruba_ssh.py @@ -15,6 +15,9 @@ def session_preparation(self): self.set_base_prompt() self.enable() self.disable_paging(command="no paging") + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def check_config_mode(self, check_string='(config) #', pattern=''): """ diff --git a/netmiko/avaya/avaya_ers_ssh.py b/netmiko/avaya/avaya_ers_ssh.py index 1fcc35fd1..f234b78dc 100644 --- a/netmiko/avaya/avaya_ers_ssh.py +++ b/netmiko/avaya/avaya_ers_ssh.py @@ -27,12 +27,16 @@ def special_login_handler(self, delay_factor=1): if 'Ctrl-Y' in output: self.write_channel(CTRL_Y) if 'sername' in output: - self.write_channel(self.username + '\n') + self.write_channel(self.username + self.RETURN) elif 'ssword' in output: - self.write_channel(self.password + '\n') + self.write_channel(self.password + self.RETURN) break time.sleep(.5 * delay_factor) else: - self.write_channel('\n') + self.write_channel(self.RETURN) time.sleep(1 * delay_factor) i += 1 + + def save_config(self, cmd='save config', confirm=False): + """Save Config""" + return super(AvayaErsSSH, self).save_config(cmd=cmd, confirm=confirm) diff --git a/netmiko/avaya/avaya_vsp_ssh.py b/netmiko/avaya/avaya_vsp_ssh.py index 1414fbf05..a5cade6c2 100644 --- a/netmiko/avaya/avaya_vsp_ssh.py +++ b/netmiko/avaya/avaya_vsp_ssh.py @@ -1,6 +1,7 @@ """Avaya Virtual Services Platform Support.""" from __future__ import print_function from __future__ import unicode_literals +import time from netmiko.cisco_base_connection import CiscoSSHConnection @@ -10,4 +11,11 @@ def session_preparation(self): """Prepare the session after the connection has been established.""" self._test_channel_read() self.set_base_prompt() - self.disable_paging(command="terminal more disable\n") + self.disable_paging(command="terminal more disable") + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() + + def save_config(self, cmd='save config', confirm=False): + """Save Config""" + return super(AvayaVspSSH, self).save_config(cmd=cmd, confirm=confirm) diff --git a/netmiko/base_connection.py b/netmiko/base_connection.py index f3c340f95..33c62b15b 100644 --- a/netmiko/base_connection.py +++ b/netmiko/base_connection.py @@ -21,9 +21,10 @@ from netmiko.netmiko_globals import MAX_BUFFER, BACKSPACE_CHAR from netmiko.ssh_exception import NetMikoTimeoutException, NetMikoAuthenticationException -from netmiko.utilities import write_bytes +from netmiko.utilities import write_bytes, check_serial_port, get_structured_data from netmiko.py23_compat import string_types from netmiko import log +import serial class BaseConnection(object): @@ -35,95 +36,86 @@ class BaseConnection(object): def __init__(self, ip='', host='', username='', password='', secret='', port=None, device_type='', verbose=False, global_delay_factor=1, use_keys=False, key_file=None, allow_agent=False, ssh_strict=False, system_host_keys=False, - alt_host_keys=False, alt_key_file='', ssh_config_file=None, timeout=8, - session_timeout=60, keepalive=0): + alt_host_keys=False, alt_key_file='', ssh_config_file=None, timeout=90, + session_timeout=60, blocking_timeout=8, keepalive=0, default_enter=None, + response_return=None, serial_settings=None): """ Initialize attributes for establishing connection to target device. :param ip: IP address of target device. Not required if `host` is provided. :type ip: str - :param host: Hostname of target device. Not required if `ip` is provided. :type host: str - :param username: Username to authenticate against target device if required. :type username: str - :param password: Password to authenticate against target device if required. :type password: str - :param secret: The enable password if target device requires one. :type secret: str - :param port: The destination port used to connect to the target device. :type port: int or None - :param device_type: Class selection based on device type. :type device_type: str - :param verbose: Enable additional messages to standard output. :type verbose: bool - :param global_delay_factor: Multiplication factor affecting Netmiko delays (default: 1). :type global_delay_factor: int - :param use_keys: Connect to target device using SSH keys. :type use_keys: bool - :param key_file: Filename path of the SSH key file to use. :type key_file: str - :param allow_agent: Enable use of SSH key-agent. :type allow_agent: bool - :param ssh_strict: Automatically reject unknown SSH host keys (default: False, which means unknown SSH host keys will be accepted). :type ssh_strict: bool - :param system_host_keys: Load host keys from the user's 'known_hosts' file. :type system_host_keys: bool - :param alt_host_keys: If `True` host keys will be loaded from the file specified in 'alt_key_file'. :type alt_host_keys: bool - :param alt_key_file: SSH host key file to use (if alt_host_keys=True). :type alt_key_file: str - :param ssh_config_file: File name of OpenSSH configuration file. :type ssh_config_file: str - :param timeout: Connection timeout. :type timeout: float - :param session_timeout: Set a timeout for parallel requests. :type session_timeout: float - :param keepalive: Send SSH keepalive packets at a specific interval, in seconds. Currently defaults to 0, for backwards compatibility (it will not attempt to keep the connection alive). :type keepalive: int + :param default_enter: Character(s) to send to correspond to enter key (default: '\n'). + :type default_enter: str + :param response_return: Character(s) to use in normalized return data to represent + enter key (default: '\n') + :type response_return: str """ self.remote_conn = None + self.RETURN = '\n' if default_enter is None else default_enter + self.TELNET_RETURN = '\r\n' + # Line Separator in response lines + self.RESPONSE_RETURN = '\n' if response_return is None else response_return if ip: self.host = ip self.ip = ip elif host: self.host = host - if not ip and not host: + if not ip and not host and 'serial' not in device_type: raise ValueError("Either ip or host must be set") if port is None: if 'telnet' in device_type: - self.port = 23 + port = 23 else: - self.port = 22 - else: - self.port = int(port) + port = 22 + self.port = int(port) + self.username = username self.password = password self.secret = secret @@ -132,21 +124,46 @@ def __init__(self, ip='', host='', username='', password='', secret='', port=Non self.verbose = verbose self.timeout = timeout self.session_timeout = session_timeout + self.blocking_timeout = blocking_timeout self.keepalive = keepalive + # Default values + self.serial_settings = { + 'port': 'COM1', + 'baudrate': 9600, + 'bytesize': serial.EIGHTBITS, + 'parity': serial.PARITY_NONE, + 'stopbits': serial.STOPBITS_ONE + } + if serial_settings is None: + serial_settings = {} + self.serial_settings.update(serial_settings) + + if 'serial' in device_type: + self.host = 'serial' + comm_port = self.serial_settings.pop('port') + # Get the proper comm port reference if a name was enterred + comm_port = check_serial_port(comm_port) + self.serial_settings.update({'port': comm_port}) + # Use the greater of global_delay_factor or delay_factor local to method self.global_delay_factor = global_delay_factor # set in set_base_prompt method self.base_prompt = '' - self._session_locker = Lock() + # determine if telnet or SSH if '_telnet' in device_type: self.protocol = 'telnet' self._modify_connection_params() self.establish_connection() self.session_preparation() + elif '_serial' in device_type: + self.protocol = 'serial' + self._modify_connection_params() + self.establish_connection() + self.session_preparation() else: self.protocol = 'ssh' @@ -170,42 +187,23 @@ def __init__(self, ip='', host='', username='', password='', secret='', port=Non self.establish_connection() self.session_preparation() - # Clear the read buffer - time.sleep(.3 * self.global_delay_factor) - self.clear_buffer() - def __enter__(self): """Establish a session using a Context Manager.""" return self def __exit__(self, exc_type, exc_value, traceback): - """Gracefully close connection on Context Manager exit. - - :param exc_type: - :type exc_type: - - :param exc_value: - :type exc_value: - - :param traceback: - :type traceback: - - """ + """Gracefully close connection on Context Manager exit.""" self.disconnect() - if exc_type is not None: - raise exc_type(exc_value) def _modify_connection_params(self): """Modify connection parameters prior to SSH connection.""" pass def _timeout_exceeded(self, start, msg='Timeout exceeded!'): - """ - Raise NetMikoTimeoutException if waiting too much in the - serving queue. + """Raise NetMikoTimeoutException if waiting too much in the serving queue. - :param start: Time hack to determine timeout - :type start: float + :param start: Initial start time to see if session lock timeout has been exceeded + :type start: float (from time.time() call i.e. epoch time) :param msg: Exception message if timeout was exceeded :type msg: str @@ -219,12 +217,11 @@ def _timeout_exceeded(self, start, msg='Timeout exceeded!'): return False def _lock_netmiko_session(self, start=None): - """ - Try to acquire the Netmiko session lock. If not available, wait in the queue until + """Try to acquire the Netmiko session lock. If not available, wait in the queue until the channel is available again. - :param start: Initial time hack to measure the session timeout - :type start: float + :param start: Initial start time to measure the session timeout + :type start: float (from time.time() call i.e. epoch time) """ if not start: start = time.time() @@ -244,13 +241,16 @@ def _unlock_netmiko_session(self): def _write_channel(self, out_data): """Generic handler that will write to both SSH and telnet channel. - :param out_data: Data to be written to the channel - :type out_data: bytes + :param out_data: data to be written to the channel + :type out_data: str (can be either unicode/byte string) """ if self.protocol == 'ssh': self.remote_conn.sendall(write_bytes(out_data)) elif self.protocol == 'telnet': self.remote_conn.write(write_bytes(out_data)) + elif self.protocol == 'serial': + self.remote_conn.write(write_bytes(out_data)) + self.remote_conn.flush() else: raise ValueError("Invalid protocol specified") try: @@ -262,8 +262,8 @@ def _write_channel(self, out_data): def write_channel(self, out_data): """Generic handler that will write to both SSH and telnet channel. - :param out_data: Data to be written to the channel - :type out_data: bytes + :param out_data: data to be written to the channel + :type out_data: str (can be either unicode/byte string) """ self._lock_netmiko_session() try: @@ -314,6 +314,10 @@ def _read_channel(self): break elif self.protocol == 'telnet': output = self.remote_conn.read_very_eager().decode('utf-8', 'ignore') + elif self.protocol == 'serial': + output = "" + while (self.remote_conn.in_waiting > 0): + output += self.remote_conn.read(self.remote_conn.in_waiting) log.debug("read_channel: {}".format(output)) return output @@ -328,9 +332,8 @@ def read_channel(self): self._unlock_netmiko_session() return output - def _read_channel_expect(self, pattern='', re_flags=0, max_loops=None): - """ - Function that reads channel until pattern is detected. + def _read_channel_expect(self, pattern='', re_flags=0, max_loops=150): + """Function that reads channel until pattern is detected. pattern takes a regular expression. @@ -342,24 +345,29 @@ def _read_channel_expect(self, pattern='', re_flags=0, max_loops=None): There are dependencies here like determining whether in config_mode that are actually depending on reading beyond pattern. - :param pattern: The pattern used to identify device prompt - :type pattern: str + :param pattern: Regular expression pattern used to identify the command is done \ + (defaults to self.base_prompt) + :type pattern: str (regular expression) - :param re_flags: Regex flags used in conjunction with pattern to search for prompt - :type re_flags: int + :param re_flags: regex flags used in conjunction with pattern to search for prompt \ + (defaults to no flags) + :type re_flags: re module flags - :param max_loops: Max number of iterations to read the channel before raising exception + :param max_loops: max number of iterations to read the channel before raising exception. + Will default to be based upon self.timeout. :type max_loops: int + """ output = '' if not pattern: pattern = re.escape(self.base_prompt) - log.debug("Pattern is: {0}".format(pattern)) + log.debug("Pattern is: {}".format(pattern)) - # Will loop for self.timeout time (override with max_loops argument) i = 1 loop_delay = .1 - if not max_loops: + # Default to making loop time be roughly equivalent to self.timeout (support old max_loops + # argument for backwards compatibility). + if max_loops != 150: max_loops = self.timeout / loop_delay while i < max_loops: if self.protocol == 'ssh': @@ -370,16 +378,16 @@ def _read_channel_expect(self, pattern='', re_flags=0, max_loops=None): if len(new_data) == 0: raise EOFError("Channel stream closed by remote device.") new_data = new_data.decode('utf-8', 'ignore') - log.debug("_read_channel_expect read_data: {0}".format(new_data)) + log.debug("_read_channel_expect read_data: {}".format(new_data)) output += new_data except socket.timeout: raise NetMikoTimeoutException("Timed-out reading channel, data not available.") finally: self._unlock_netmiko_session() - elif self.protocol == 'telnet': + elif self.protocol == 'telnet' or 'serial': output += self.read_channel() if re.search(pattern, output, flags=re_flags): - log.debug("Pattern found: {0} {1}".format(pattern, output)) + log.debug("Pattern found: {} {}".format(pattern, output)) return output time.sleep(loop_delay * self.global_delay_factor) i += 1 @@ -396,23 +404,34 @@ def _read_channel_timing(self, delay_factor=1, max_loops=150): Once data is encountered read channel for another two seconds (2 * delay_factor) to make sure reading of channel is complete. - :param delay_factor: Factor to adjust delay when reading the channel - :type delay_factor: int + :param delay_factor: multiplicative factor to adjust delay when reading channel (delays + get multiplied by this factor) + :type delay_factor: int or float - :param max_loops: Controls wait time in conjunction with delay_factor (default: 150) + :param max_loops: maximum number of loops to iterate through before returning channel data. + Will default to be based upon self.timeout. :type max_loops: int """ + # Time to delay in each read loop + loop_delay = .1 + final_delay = 2 + + # Default to making loop time be roughly equivalent to self.timeout (support old max_loops + # and delay_factor arguments for backwards compatibility). delay_factor = self.select_delay_factor(delay_factor) + if delay_factor == 1 and max_loops == 150: + max_loops = int(self.timeout / loop_delay) + channel_data = "" i = 0 while i <= max_loops: - time.sleep(.1 * delay_factor) + time.sleep(loop_delay * delay_factor) new_data = self.read_channel() if new_data: channel_data += new_data else: # Safeguard to make sure really done - time.sleep(2 * delay_factor) + time.sleep(final_delay * delay_factor) new_data = self.read_channel() if not new_data: break @@ -430,41 +449,33 @@ def read_until_pattern(self, *args, **kwargs): return self._read_channel_expect(*args, **kwargs) def read_until_prompt_or_pattern(self, pattern='', re_flags=0): - """Read until either self.base_prompt or pattern is detected. Return ALL data available. + """Read until either self.base_prompt or pattern is detected. - :param pattern: The pattern used to identify device prompt - :type pattern: str + :param pattern: the pattern used to identify that the output is complete (i.e. stop \ + reading when pattern is detected). pattern will be combined with self.base_prompt to \ + terminate output reading when the first of self.base_prompt or pattern is detected. + :type pattern: regular expression string + + :param re_flags: regex flags used in conjunction with pattern to search for prompt \ + (defaults to no flags) + :type re_flags: re module flags - :param re_flags: Regex flags used in conjunction with pattern to search for prompt - :type re_flags: int """ combined_pattern = re.escape(self.base_prompt) if pattern: combined_pattern = r"({}|{})".format(combined_pattern, pattern) return self._read_channel_expect(combined_pattern, re_flags=re_flags) - def telnet_login(self, pri_prompt_terminator='#', alt_prompt_terminator='>', - username_pattern=r"sername", pwd_pattern=r"assword", - delay_factor=1, max_loops=60): - """Telnet login. Can be username/password or just password. - - :param pri_prompt_terminator: Primary trailing delimiter for identifying a device prompt - :type pri_prompt_terminator: str - - :param alt_prompt_terminator: Alternate trailing delimiter for identifying a device prompt - :type alt_prompt_terminator: str - - :param username_pattern: Pattern to identify a username prompt - :type username_pattern: str - - :param delay_factor: See __init__: global_delay_factor - :type delay_factor: int - - :param max_loops: Controls wait time in conjunction with delay_factor (default: 60) - :type max_loops: int - """ - TELNET_RETURN = '\r\n' + def serial_login(self, pri_prompt_terminator=r'#\s*$', alt_prompt_terminator=r'>\s*$', + username_pattern=r"(?:[Uu]ser:|sername|ogin)", pwd_pattern=r"assword", + delay_factor=1, max_loops=20): + self.telnet_login(pri_prompt_terminator, alt_prompt_terminator, username_pattern, + pwd_pattern, delay_factor, max_loops) + def telnet_login(self, pri_prompt_terminator=r'#\s*$', alt_prompt_terminator=r'>\s*$', + username_pattern=r"(?:[Uu]ser:|sername|ogin)", pwd_pattern=r"assword", + delay_factor=1, max_loops=20): + """Telnet login. Can be username/password or just password.""" delay_factor = self.select_delay_factor(delay_factor) time.sleep(1 * delay_factor) @@ -478,40 +489,43 @@ def telnet_login(self, pri_prompt_terminator='#', alt_prompt_terminator='>', # Search for username pattern / send username if re.search(username_pattern, output): - self.write_channel(self.username + TELNET_RETURN) + self.write_channel(self.username + self.TELNET_RETURN) time.sleep(1 * delay_factor) output = self.read_channel() return_msg += output # Search for password pattern / send password if re.search(pwd_pattern, output): - self.write_channel(self.password + TELNET_RETURN) + self.write_channel(self.password + self.TELNET_RETURN) time.sleep(.5 * delay_factor) output = self.read_channel() return_msg += output - if pri_prompt_terminator in output or alt_prompt_terminator in output: + if (re.search(pri_prompt_terminator, output, flags=re.M) + or re.search(alt_prompt_terminator, output, flags=re.M)): return return_msg # Check if proper data received - if pri_prompt_terminator in output or alt_prompt_terminator in output: + if (re.search(pri_prompt_terminator, output, flags=re.M) + or re.search(alt_prompt_terminator, output, flags=re.M)): return return_msg - self.write_channel(TELNET_RETURN) + self.write_channel(self.TELNET_RETURN) time.sleep(.5 * delay_factor) i += 1 except EOFError: - msg = "Telnet login failed: {0}".format(self.host) + msg = "Telnet login failed: {}".format(self.host) raise NetMikoAuthenticationException(msg) # Last try to see if we already logged in - self.write_channel(TELNET_RETURN) + self.write_channel(self.TELNET_RETURN) time.sleep(.5 * delay_factor) output = self.read_channel() return_msg += output - if pri_prompt_terminator in output or alt_prompt_terminator in output: + if (re.search(pri_prompt_terminator, output, flags=re.M) + or re.search(alt_prompt_terminator, output, flags=re.M)): return return_msg - msg = "Telnet login failed: {0}".format(self.host) + msg = "Telnet login failed: {}".format(self.host) raise NetMikoAuthenticationException(msg) def session_preparation(self): @@ -526,19 +540,23 @@ def session_preparation(self): self.set_base_prompt() self.disable_paging() self.set_terminal_width() + self.clear_buffer() """ self._test_channel_read() self.set_base_prompt() self.disable_paging() self.set_terminal_width() + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() + def _use_ssh_config(self, dict_arg): """Update SSH connection parameters based on contents of SSH 'config' file. - :param dict_arg: dictionary of connection parameters + :param dict_arg: Dictionary of SSH connection parameters :type dict_arg: dict """ - connect_dict = dict_arg.copy() # Use SSHConfig to generate source content. @@ -590,24 +608,19 @@ def _connect_params_dict(self): def _sanitize_output(self, output, strip_command=False, command_string=None, strip_prompt=False): - """Sanitize the output. + """Strip out command echo, trailing router prompt and ANSI escape codes. - :param output: The output of a command execution on the SSH channel - :type output: str - - :param strip_command: specifies that the initial command to be stripped from the output - :type strip_command: bool + :param output: Output from a remote network device + :type output: unicode string - :param command_string: the command string sent to the device - :type command_string: str - - :param strip_prompt: specifies that the prompt needs to be stripped from the output - :type strip_prompt: bool + :param strip_command: + :type strip_command: """ if self.ansi_escape_codes: output = self.strip_ansi_escape_codes(output) output = self.normalize_linefeeds(output) if strip_command and command_string: + command_string = self.normalize_linefeeds(command_string) output = self.strip_command(command_string, output) if strip_prompt: output = self.strip_prompt(output) @@ -621,15 +634,13 @@ def establish_connection(self, width=None, height=None): Authentication failure will generate a NetMikoAuthenticationException width and height are needed for Fortinet paging setting. - - :param width: Specified width of the terminal window for VT100 - :type width: int - - : """ if self.protocol == 'telnet': self.remote_conn = telnetlib.Telnet(self.host, port=self.port, timeout=self.timeout) self.telnet_login() + elif self.protocol == 'serial': + self.remote_conn = serial.Serial(**self.serial_settings) + self.serial_login() elif self.protocol == 'ssh': ssh_connect_params = self._connect_params_dict() self.remote_conn_pre = self._build_ssh_client() @@ -644,7 +655,7 @@ def establish_connection(self, width=None, height=None): except paramiko.ssh_exception.AuthenticationException as auth_err: msg = "Authentication failure: unable to connect {device_type} {ip}:{port}".format( device_type=self.device_type, ip=self.host, port=self.port) - msg += '\n' + str(auth_err) + msg += self.RETURN + str(auth_err) raise NetMikoAuthenticationException(msg) if self.verbose: @@ -657,7 +668,7 @@ def establish_connection(self, width=None, height=None): else: self.remote_conn = self.remote_conn_pre.invoke_shell() - self.remote_conn.settimeout(self.timeout) + self.remote_conn.settimeout(self.blocking_timeout) if self.keepalive: self.remote_conn.transport.set_keepalive(self.keepalive) self.special_login_handler() @@ -666,28 +677,9 @@ def establish_connection(self, width=None, height=None): return "" def _test_channel_read(self, count=40, pattern=""): - """Try to read the channel (generally post login) verify you receive data back. - - :param count: the number of times to check the channel for data - :type count: int - - :param pattern: Signifying the device prompt has returned and to break out of the loop - :type: pattern: str - """ - + """Try to read the channel (generally post login) verify you receive data back.""" def _increment_delay(main_delay, increment=1.1, maximum=8): - """Increment sleep time to a maximum value. - - :param main_delay: Pri sleep factor for data to return from the channel - :type main_delay: int - - :param increment: Sec sleep factor for waiting for data to return from channel - :type increment: float - - :param maximum: Max delay to sleep when waiting for data to return from the channel - :type maximum: int - - """ + """Increment sleep time to a maximum value.""" main_delay = main_delay * increment if main_delay >= maximum: main_delay = maximum @@ -706,8 +698,7 @@ def _increment_delay(main_delay, increment=1.1, maximum=8): elif new_data: break else: - self.write_channel('\n') - + self.write_channel(self.RETURN) main_delay = _increment_delay(main_delay) time.sleep(main_delay) i += 1 @@ -734,33 +725,18 @@ def _build_ssh_client(self): return remote_conn_pre def select_delay_factor(self, delay_factor): - """Choose the greater of delay_factor or self.global_delay_factor. - - :param delay_factor: See __init__: global_delay_factor - :type delay_factor: int - """ + """Choose the greater of delay_factor or self.global_delay_factor.""" if delay_factor >= self.global_delay_factor: return delay_factor else: return self.global_delay_factor def special_login_handler(self, delay_factor=1): - """Handler for devices like WLC, Avaya ERS that throw up characters prior to login. - - :param delay_factor: Will probably do some really cool stuff, but nothing right now - :type delay_factor: int - """ + """Handler for devices like WLC, Avaya ERS that throw up characters prior to login.""" pass def disable_paging(self, command="terminal length 0", delay_factor=1): - """Disable paging default to a Cisco CLI method. - - :param command: Device command to disable pagination of output - :type command: str - - :param delay_factor: See __init__: global_delay_factor - :type delay_factor: int - """ + """Disable paging default to a Cisco CLI method.""" delay_factor = self.select_delay_factor(delay_factor) time.sleep(delay_factor * .1) self.clear_buffer() @@ -781,14 +757,6 @@ def set_terminal_width(self, command="", delay_factor=1): This causes the output to get distorted when accessed programmatically. Set terminal width to 511 which works on a broad set of devices. - - :param command: Command string to send to device - :type command: str - - :param delay_factor: See __init__: global_delay_factor - :type delay_factor: int - - TODO: delay_factor doesn't seem to be used in this method """ if not command: return "" @@ -798,8 +766,6 @@ def set_terminal_width(self, command="", delay_factor=1): output = self.read_until_prompt() if self.ansi_escape_codes: output = self.strip_ansi_escape_codes(output) - log.debug("{0}".format(output)) - log.debug("Exiting set_terminal_width") return output def set_base_prompt(self, pri_prompt_terminator='#', @@ -814,15 +780,6 @@ def set_base_prompt(self, pri_prompt_terminator='#', This will be set on entering user exec or privileged exec on Cisco, but not when entering/exiting config mode. - - :param pri_prompt_terminator: Primary trailing delimiter for identifying a device prompt - :type pri_prompt_terminator: str - - :param alt_prompt_terminator: Alternate trailing delimiter for identifying a device prompt - :type alt_prompt_terminator: str - - :param delay_factor: See __init__: global_delay_factor - :type delay_factor: int """ prompt = self.find_prompt(delay_factor=delay_factor) if not prompt[-1] in (pri_prompt_terminator, alt_prompt_terminator): @@ -832,14 +789,10 @@ def set_base_prompt(self, pri_prompt_terminator='#', return self.base_prompt def find_prompt(self, delay_factor=1): - """Finds the current network device prompt, last line only. - - :param delay_factor: See __init__: global_delay_factor - :type delay_factor: int - """ + """Finds the current network device prompt, last line only.""" delay_factor = self.select_delay_factor(delay_factor) self.clear_buffer() - self.write_channel("\n") + self.write_channel(self.RETURN) time.sleep(delay_factor * .1) # Initial attempt to get prompt @@ -856,13 +809,13 @@ def find_prompt(self, delay_factor=1): if self.ansi_escape_codes: prompt = self.strip_ansi_escape_codes(prompt).strip() else: - self.write_channel("\n") + self.write_channel(self.RETURN) time.sleep(delay_factor * .1) count += 1 # If multiple lines in the output take the last line prompt = self.normalize_linefeeds(prompt) - prompt = prompt.split('\n')[-1] + prompt = prompt.split(self.RESPONSE_RETURN)[-1] prompt = prompt.strip() if not prompt: raise ValueError("Unable to find prompt: {}".format(prompt)) @@ -875,27 +828,26 @@ def clear_buffer(self): self.read_channel() def send_command_timing(self, command_string, delay_factor=1, max_loops=150, - strip_prompt=True, strip_command=True, normalize=True): + strip_prompt=True, strip_command=True, normalize=True, + use_textfsm=False): """Execute command_string on the SSH channel using a delay-based mechanism. Generally used for show commands. :param command_string: The command to be executed on the remote device. :type command_string: str - :param delay_factor: Multiplying factor used to adjust delays (default: 1). - :type delay_factor: int - - :param max_loops: Controls wait time in conjunction with delay_factor (default: 150). + :type delay_factor: int or float + :param max_loops: Controls wait time in conjunction with delay_factor. Will default to be + based upon self.timeout. :type max_loops: int - :param strip_prompt: Remove the trailing router prompt from the output (default: True). :type strip_prompt: bool - :param strip_command: Remove the echo of the command from the output (default: True). :type strip_command: bool - :param normalize: Ensure the proper enter is sent at end of command (default: True). :type normalize: bool + :param use_textfsm: Process command output through TextFSM template (default: False). + :type normalize: bool """ output = '' delay_factor = self.select_delay_factor(delay_factor) @@ -907,24 +859,24 @@ def send_command_timing(self, command_string, delay_factor=1, max_loops=150, output = self._read_channel_timing(delay_factor=delay_factor, max_loops=max_loops) output = self._sanitize_output(output, strip_command=strip_command, command_string=command_string, strip_prompt=strip_prompt) + if use_textfsm: + output = get_structured_data(output, platform=self.device_type, + command=command_string.strip()) return output def strip_prompt(self, a_string): - """Strip the trailing router prompt from the output. - - :param a_string: Returned output from device - :type a_string: str - """ - response_list = a_string.split('\n') + """Strip the trailing router prompt from the output.""" + response_list = a_string.split(self.RESPONSE_RETURN) last_line = response_list[-1] if self.base_prompt in last_line: - return '\n'.join(response_list[:-1]) + return self.RESPONSE_RETURN.join(response_list[:-1]) else: return a_string def send_command(self, command_string, expect_string=None, delay_factor=1, max_loops=500, auto_find_prompt=True, - strip_prompt=True, strip_command=True, normalize=True): + strip_prompt=True, strip_command=True, normalize=True, + use_textfsm=False): """Execute command_string on the SSH channel using a pattern-based mechanism. Generally used for show commands. By default this method will keep waiting to receive data until the network device prompt is detected. The current network device prompt will be determined @@ -940,7 +892,8 @@ def send_command(self, command_string, expect_string=None, :param delay_factor: Multiplying factor used to adjust delays (default: 1). :type delay_factor: int - :param max_loops: Controls wait time in conjunction with delay_factor (default: 150). + :param max_loops: Controls wait time in conjunction with delay_factor. Will default to be + based upon self.timeout. :type max_loops: int :param strip_prompt: Remove the trailing router prompt from the output (default: True). @@ -951,8 +904,19 @@ def send_command(self, command_string, expect_string=None, :param normalize: Ensure the proper enter is sent at end of command (default: True). :type normalize: bool + + :param use_textfsm: Process command output through TextFSM template (default: False). + :type normalize: bool """ + # Time to delay in each read loop + loop_delay = .2 + + # Default to making loop time be roughly equivalent to self.timeout (support old max_loops + # and delay_factor arguments for backwards compatibility). delay_factor = self.select_delay_factor(delay_factor) + if delay_factor == 1 and max_loops == 500: + # Default arguments are being used; use self.timeout instead + max_loops = int(self.timeout / loop_delay) # Find the current router prompt if expect_string is None: @@ -970,19 +934,19 @@ def send_command(self, command_string, expect_string=None, if normalize: command_string = self.normalize_cmd(command_string) - time.sleep(delay_factor * .2) + time.sleep(delay_factor * loop_delay) self.clear_buffer() self.write_channel(command_string) - # Keep reading data until search_pattern is found (or max_loops) i = 1 output = '' + # Keep reading data until search_pattern is found or until max_loops is reached. while i <= max_loops: new_data = self.read_channel() if new_data: output += new_data try: - lines = output.split("\n") + lines = output.split(self.RETURN) first_line = lines[0] # First line is the echo line containing the command. In certain situations # it gets repainted and needs filtered @@ -990,30 +954,33 @@ def send_command(self, command_string, expect_string=None, pattern = search_pattern + r'.*$' first_line = re.sub(pattern, repl='', string=first_line) lines[0] = first_line - output = "\n".join(lines) + output = self.RETURN.join(lines) except IndexError: pass if re.search(search_pattern, output): break else: - time.sleep(delay_factor * .2) + time.sleep(delay_factor * loop_delay) i += 1 else: # nobreak - raise IOError("Search pattern never detected in send_command_expect: {0}".format( + raise IOError("Search pattern never detected in send_command_expect: {}".format( search_pattern)) output = self._sanitize_output(output, strip_command=strip_command, command_string=command_string, strip_prompt=strip_prompt) + if use_textfsm: + output = get_structured_data(output, platform=self.device_type, + command=command_string.strip()) return output def send_command_expect(self, *args, **kwargs): """Support previous name of send_command method. - :param args: Arguments to send to send_command() + :param args: Positional arguments to send to send_command() :type args: list :param kwargs: Keyword arguments to send to send_command() - :type kwargs: dict + :type kwargs: Dict """ return self.send_command(*args, **kwargs) @@ -1021,93 +988,69 @@ def send_command_expect(self, *args, **kwargs): def strip_backspaces(output): """Strip any backspace characters out of the output. - :param output: Output returned from device that will have x08 replaced with '' + :param output: Output obtained from a remote network device. :type output: str """ backspace_char = '\x08' return output.replace(backspace_char, '') - @staticmethod - def strip_command(command_string, output): + def strip_command(self, command_string, output): """ Strip command_string from output string Cisco IOS adds backspaces into output for long commands (i.e. for commands that line wrap) - - :param command_string: Command string sent to the device. - :type command_string: str - - :param output: The returned output as a result of the command string sent to the device - :type output: str """ backspace_char = '\x08' # Check for line wrap (remove backspaces) if backspace_char in output: output = output.replace(backspace_char, '') - output_lines = output.split("\n") + output_lines = output.split(self.RESPONSE_RETURN) new_output = output_lines[1:] - return "\n".join(new_output) + return self.RESPONSE_RETURN.join(new_output) else: command_length = len(command_string) return output[command_length:] - @staticmethod - def normalize_linefeeds(a_string): - """Convert `\r\r\n`,`\r\n`, `\n\r` to `\n.` - - :param a_string: A string that may have non-normalized line feeds - i.e. output returned from device, or a device prompt - :type a_string: str - """ + def normalize_linefeeds(self, a_string): + """Convert `\r\r\n`,`\r\n`, `\n\r` to `\n.`""" newline = re.compile('(\r\r\r\n|\r\r\n|\r\n|\n\r)') - a_string = newline.sub('\n', a_string) - # Convert any remaining \r to \n - return re.sub('\r', '\n', a_string) - - @staticmethod - def normalize_cmd(command): - """Normalize CLI commands to have a single trailing newline. - - :param command: Command that may require line feed to be normalized - (No Default set) - :type command: str - """ - command = command.rstrip("\n") - command += '\n' + a_string = newline.sub(self.RESPONSE_RETURN, a_string) + if self.RESPONSE_RETURN == '\n': + # Convert any remaining \r to \n + return re.sub('\r', self.RESPONSE_RETURN, a_string) + + def normalize_cmd(self, command): + """Normalize CLI commands to have a single trailing newline.""" + command = command.rstrip() + command += self.RETURN return command def check_enable_mode(self, check_string=''): - """Check if in enable mode. Return boolean. - - :param check_string: Identification of privilege mode from the device - :type check_string: str - """ - self.write_channel('\n') + """Check if in enable mode. Return boolean.""" + self.write_channel(self.RETURN) output = self.read_until_prompt() - log.debug("{0}".format(output)) return check_string in output def enable(self, cmd='', pattern='ssword', re_flags=re.IGNORECASE): """Enter enable mode.""" output = "" + msg = "Failed to enter enable mode. Please ensure you pass " \ + "the 'secret' argument to ConnectHandler." if not self.check_enable_mode(): self.write_channel(self.normalize_cmd(cmd)) - output += self.read_until_prompt_or_pattern(pattern=pattern, re_flags=re_flags) - self.write_channel(self.normalize_cmd(self.secret)) - output += self.read_until_prompt() + try: + output += self.read_until_prompt_or_pattern(pattern=pattern, re_flags=re_flags) + self.write_channel(self.normalize_cmd(self.secret)) + output += self.read_until_prompt() + except NetMikoTimeoutException: + raise ValueError(msg) if not self.check_enable_mode(): - msg = "Failed to enter enable mode. Please ensure you pass " \ - "the 'secret' argument to ConnectHandler." raise ValueError(msg) return output def exit_enable_mode(self, exit_command=''): - """Exit enable mode. - - :param exit_command: Command that exits the session from privileged mode - :type exit_command: str - """ + """Exit enable mode.""" output = "" if self.check_enable_mode(): self.write_channel(self.normalize_cmd(exit_command)) @@ -1117,29 +1060,13 @@ def exit_enable_mode(self, exit_command=''): return output def check_config_mode(self, check_string='', pattern=''): - """Checks if the device is in configuration mode or not. - - :param check_string: Identification of configuration mode from the device - :type check_string: str - - :param pattern: Pattern to identify the device prompt - :type pattern: str - """ - log.debug("pattern: {0}".format(pattern)) - self.write_channel('\n') + """Checks if the device is in configuration mode or not.""" + self.write_channel(self.RETURN) output = self.read_until_pattern(pattern=pattern) - log.debug("check_config_mode: {0}".format(repr(output))) return check_string in output def config_mode(self, config_command='', pattern=''): - """Enter into config_mode. - - :param config_command: Configuration command to send to the device - :type config_command: str - - :param pattern: The pattern signifying the config command completed - :type pattern: str - """ + """Enter into config_mode.""" output = '' if not self.check_config_mode(): self.write_channel(self.normalize_cmd(config_command)) @@ -1149,14 +1076,7 @@ def config_mode(self, config_command='', pattern=''): return output def exit_config_mode(self, exit_config='', pattern=''): - """Exit from configuration mode. - - :param exit_config: Command to exit configuration mode - :type exit_config: str - - :param pattern: The pattern signifying the exit config mode command completed - :type pattern: str - """ + """Exit from configuration mode.""" output = '' if self.check_config_mode(): self.write_channel(self.normalize_cmd(exit_config)) @@ -1173,16 +1093,14 @@ def send_config_from_file(self, config_file=None, **kwargs): The file is processed line-by-line and each command is sent down the SSH channel. - :param config_file: Path to configuration file to be sent do the device - :type config_file: str - **kwargs are passed to send_config_set method. """ with io.open(config_file, "rt", encoding='utf-8') as cfg_file: return self.send_config_set(cfg_file, **kwargs) def send_config_set(self, config_commands=None, exit_config_mode=True, delay_factor=1, - max_loops=150, strip_prompt=False, strip_command=False): + max_loops=150, strip_prompt=False, strip_command=False, + config_mode_command=None): """ Send configuration commands down the SSH channel. @@ -1190,24 +1108,6 @@ def send_config_set(self, config_commands=None, exit_config_mode=True, delay_fac The commands will be executed one after the other. Automatically exits/enters configuration mode. - - :param config_commands: Multiple commands to be sent to the device - :type config_commands: list of strings - - :param exit_config_mode: Determines exit config mode after all commands have been sent - :type exit_config_mode: bool - - :param delay_factor: Factor to adjust delay when reading the channel - :type delay_factor: int - - :param max_loops: Controls wait time in conjunction with delay_factor (default: 150) - :type max_loops: int - - :param strip_prompt: Determines whether or not to strip the prompt from the - :type strip_prompt: bool - - :param strip_command: - :type strip_command: bool """ delay_factor = self.select_delay_factor(delay_factor) if config_commands is None: @@ -1219,7 +1119,8 @@ def send_config_set(self, config_commands=None, exit_config_mode=True, delay_fac raise ValueError("Invalid argument passed into send_config_set") # Send config commands - output = self.config_mode() + cfg_mode_args = (config_mode_command,) if config_mode_command else tuple() + output = self.config_mode(*cfg_mode_args) for cmd in config_commands: self.write_channel(self.normalize_cmd(cmd)) time.sleep(delay_factor * .5) @@ -1229,11 +1130,10 @@ def send_config_set(self, config_commands=None, exit_config_mode=True, delay_fac if exit_config_mode: output += self.exit_config_mode() output = self._sanitize_output(output) - log.debug("{0}".format(output)) + log.debug("{}".format(output)) return output - @staticmethod - def strip_ansi_escape_codes(string_buffer): + def strip_ansi_escape_codes(self, string_buffer): """ Remove any ANSI (VT100) ESC codes from the output @@ -1256,11 +1156,9 @@ def strip_ansi_escape_codes(string_buffer): ESC[2J Code erase display ESC[00;32m Color Green (30 to 37 are different colors) more general pattern is ESC[\d\d;\d\dm and ESC[\d\d;\d\d;\d\dm + ESC[6n Get cursor position HP ProCurve's, Cisco SG300, and F5 LTM's require this (possible others) - - :param string_buffer: the string that may require ansi escape chars to be removed - :type string_buffer: str """ log.debug("In strip_ansi_escape_codes") log.debug("repr = {0}".format(repr(string_buffer))) @@ -1279,19 +1177,20 @@ def strip_ansi_escape_codes(string_buffer): code_erase_display = chr(27) + r'\[2J' code_graphics_mode = chr(27) + r'\[\d\d;\d\dm' code_graphics_mode2 = chr(27) + r'\[\d\d;\d\d;\d\dm' + code_get_cursor_position = chr(27) + r'\[6n' code_set = [code_position_cursor, code_show_cursor, code_erase_line, code_enable_scroll, code_erase_start_line, code_form_feed, code_carriage_return, code_disable_line_wrapping, code_erase_line_end, code_reset_mode_screen_options, code_erase_display, - code_graphics_mode, code_graphics_mode2] + code_graphics_mode, code_graphics_mode2, code_get_cursor_position] output = string_buffer for ansi_esc_code in code_set: output = re.sub(ansi_esc_code, '', output) - # CODE_NEXT_LINE must substitute with '\n' - output = re.sub(code_next_line, '\n', output) + # CODE_NEXT_LINE must substitute with return + output = re.sub(code_next_line, self.RETURN, output) log.debug("new_output = {0}".format(output)) log.debug("repr = {0}".format(repr(output))) @@ -1303,18 +1202,27 @@ def cleanup(self): pass def disconnect(self): - """Gracefully close the SSH connection.""" - self.cleanup() - if self.protocol == 'ssh': - self.remote_conn_pre.close() - elif self.protocol == 'telnet': - self.remote_conn.close() - self.remote_conn = None + """Try to gracefully close the SSH connection.""" + try: + self.cleanup() + if self.protocol == 'ssh': + self.remote_conn_pre.close() + elif self.protocol == 'telnet' or 'serial': + self.remote_conn.close() + except Exception: + # There have been race conditions observed on disconnect. + pass + finally: + self.remote_conn = None def commit(self): """Commit method for platforms that support this.""" raise AttributeError("Network device does not support 'commit()' method") + def save_config(self, cmd='', confirm=True, confirm_response=''): + """Not Implemented""" + raise NotImplementedError + class TelnetConnection(BaseConnection): pass diff --git a/netmiko/brocade/__init__.py b/netmiko/brocade/__init__.py index acfd870d6..6b3eb01ec 100644 --- a/netmiko/brocade/__init__.py +++ b/netmiko/brocade/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from netmiko.brocade.brocade_nos_ssh import BrocadeNosSSH -from netmiko.brocade.brocade_fastiron_ssh import BrocadeFastironSSH -from netmiko.brocade.brocade_netiron_ssh import BrocadeNetironSSH +from netmiko.brocade.brocade_netiron import BrocadeNetironSSH +from netmiko.brocade.brocade_netiron import BrocadeNetironTelnet -__all__ = ['BrocadeNosSSH', 'BrocadeFastironSSH', 'BrocadeNetironSSH'] +__all__ = ['BrocadeNosSSH', 'BrocadeNetironSSH', 'BrocadeNetironTelnet'] diff --git a/netmiko/brocade/brocade_netiron.py b/netmiko/brocade/brocade_netiron.py new file mode 100644 index 000000000..153879cdb --- /dev/null +++ b/netmiko/brocade/brocade_netiron.py @@ -0,0 +1,19 @@ +from __future__ import unicode_literals +from netmiko.cisco_base_connection import CiscoSSHConnection + + +class BrocadeNetironBase(CiscoSSHConnection): + def save_config(self, cmd='write memory', confirm=False): + """Save Config""" + return super(BrocadeNetironBase, self).save_config(cmd=cmd, confirm=confirm) + + +class BrocadeNetironSSH(BrocadeNetironBase): + pass + + +class BrocadeNetironTelnet(BrocadeNetironBase): + def __init__(self, *args, **kwargs): + default_enter = kwargs.get('default_enter') + kwargs['default_enter'] = '\r\n' if default_enter is None else default_enter + super(BrocadeNetironTelnet, self).__init__(*args, **kwargs) diff --git a/netmiko/brocade/brocade_netiron_ssh.py b/netmiko/brocade/brocade_netiron_ssh.py deleted file mode 100644 index cf65a56fd..000000000 --- a/netmiko/brocade/brocade_netiron_ssh.py +++ /dev/null @@ -1,6 +0,0 @@ -from __future__ import unicode_literals -from netmiko.cisco_base_connection import CiscoSSHConnection - - -class BrocadeNetironSSH(CiscoSSHConnection): - pass diff --git a/netmiko/brocade/brocade_nos_ssh.py b/netmiko/brocade/brocade_nos_ssh.py index 1d83d7937..4eb315b31 100644 --- a/netmiko/brocade/brocade_nos_ssh.py +++ b/netmiko/brocade/brocade_nos_ssh.py @@ -17,5 +17,11 @@ def exit_enable_mode(self, *args, **kwargs): def special_login_handler(self, delay_factor=1): """Adding a delay after login.""" delay_factor = self.select_delay_factor(delay_factor) - self.write_channel('\n') + self.write_channel(self.RETURN) time.sleep(1 * delay_factor) + + def save_config(self, cmd='copy running-config startup-config', confirm=True, + confirm_response='y'): + """Save Config for Brocade VDX.""" + return super(BrocadeNosSSH, self).save_config(cmd=cmd, confirm=confirm, + confirm_response=confirm_response) diff --git a/netmiko/calix/__init__.py b/netmiko/calix/__init__.py new file mode 100644 index 000000000..b3c6b06d1 --- /dev/null +++ b/netmiko/calix/__init__.py @@ -0,0 +1,4 @@ +from __future__ import unicode_literals +from netmiko.calix.calix_b6_ssh import CalixB6SSH + +__all__ = ['CalixB6SSH'] diff --git a/netmiko/calix/calix_b6_ssh.py b/netmiko/calix/calix_b6_ssh.py new file mode 100644 index 000000000..da000c8b7 --- /dev/null +++ b/netmiko/calix/calix_b6_ssh.py @@ -0,0 +1,94 @@ +"""Calix B6 SSH Driver for Netmiko""" +from __future__ import unicode_literals +import time +from os import path +from paramiko import SSHClient +from netmiko.cisco_base_connection import CiscoSSHConnection + + +class SSHClient_noauth(SSHClient): + def _auth(self, username, *args): + self._transport.auth_none(username) + return + + +class CalixB6SSH(CiscoSSHConnection): + """Calix B6 SSH driver + + These devices use SSH auth type (none) for cli user. Override SSH _auth method. + """ + def session_preparation(self): + """Prepare the session after the connection has been established.""" + self.ansi_escape_codes = True + self._test_channel_read() + self.set_base_prompt() + self.disable_paging() + self.set_terminal_width(command="terminal width 511") + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() + + def _build_ssh_client(self): + """Prepare for Paramiko SSH connection. + + See base_connection.py file for any updates. + """ + # Create instance of SSHClient object + # If username is cli, we use noauth + if not self.use_keys: + remote_conn_pre = SSHClient_noauth() + else: + remote_conn_pre = SSHClient() + + # Load host_keys for better SSH security + if self.system_host_keys: + remote_conn_pre.load_system_host_keys() + if self.alt_host_keys and path.isfile(self.alt_key_file): + remote_conn_pre.load_host_keys(self.alt_key_file) + + # Default is to automatically add untrusted hosts (make sure appropriate for your env) + remote_conn_pre.set_missing_host_key_policy(self.key_policy) + return remote_conn_pre + + def special_login_handler(self, delay_factor=1): + """ + Calix B6 presents with the following on login: + + login as: + Password: **** + """ + delay_factor = self.select_delay_factor(delay_factor) + i = 0 + time.sleep(delay_factor * .25) + output = "" + while i <= 12: + output = self.read_channel() + if output: + if 'login as:' in output: + self.write_channel(self.username + self.RETURN) + elif 'Password:' in output: + self.write_channel(self.password + self.RETURN) + break + time.sleep(delay_factor * 0.5) + else: + self.write_channel(self.RETURN) + time.sleep(delay_factor * 1) + i += 1 + + def check_config_mode(self, check_string=')#', pattern=''): + """Checks if the device is in configuration mode""" + return super(CalixB6SSH, self).check_config_mode(check_string=check_string) + + def config_mode(self, config_command='config t', pattern=''): + """Enter configuration mode.""" + return super(CalixB6SSH, self).config_mode(config_command=config_command) + + def exit_config_mode(self, exit_config=None, pattern=''): + """Exit from configuration mode.""" + if exit_config is None: + exit_config = 'exit' + self.RETURN + 'end' + return super(CalixB6SSH, self).exit_config_mode(exit_config=exit_config, pattern=pattern) + + def save_config(self, cmd='', confirm=True, confirm_response=''): + """Not Implemented""" + raise NotImplementedError diff --git a/netmiko/checkpoint/checkpoint_gaia_ssh.py b/netmiko/checkpoint/checkpoint_gaia_ssh.py index 591f3db09..48b5fdca6 100644 --- a/netmiko/checkpoint/checkpoint_gaia_ssh.py +++ b/netmiko/checkpoint/checkpoint_gaia_ssh.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +import time from netmiko.base_connection import BaseConnection @@ -15,7 +16,10 @@ def session_preparation(self): """ self._test_channel_read() self.set_base_prompt() - self.disable_paging(command="set clienv rows 0\n") + self.disable_paging(command="set clienv rows 0") + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def config_mode(self, config_command=''): """No config mode for Check Point devices.""" @@ -24,3 +28,7 @@ def config_mode(self, config_command=''): def exit_config_mode(self, exit_config=''): """No config mode for Check Point devices.""" return '' + + def save_config(self, cmd='', confirm=True, confirm_response=''): + """Not Implemented""" + raise NotImplementedError diff --git a/netmiko/ciena/ciena_saos_ssh.py b/netmiko/ciena/ciena_saos_ssh.py index 45e49b812..6d04fd20c 100644 --- a/netmiko/ciena/ciena_saos_ssh.py +++ b/netmiko/ciena/ciena_saos_ssh.py @@ -1,6 +1,7 @@ """Ciena SAOS support.""" from __future__ import print_function from __future__ import unicode_literals +import time from netmiko.cisco_base_connection import CiscoSSHConnection @@ -9,7 +10,14 @@ class CienaSaosSSH(CiscoSSHConnection): def session_preparation(self): self._test_channel_read() self.set_base_prompt() - self.disable_paging(command="system shell session set more off\n") + self.disable_paging(command="system shell session set more off") + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def enable(self, *args, **kwargs): pass + + def save_config(self, cmd='', confirm=True, confirm_response=''): + """Not Implemented""" + raise NotImplementedError diff --git a/netmiko/cisco/__init__.py b/netmiko/cisco/__init__.py index d34289aa3..d0ccda518 100644 --- a/netmiko/cisco/__init__.py +++ b/netmiko/cisco/__init__.py @@ -1,11 +1,15 @@ from __future__ import unicode_literals -from netmiko.cisco.cisco_ios import CiscoIosBase, CiscoIosSSH, CiscoIosTelnet -from netmiko.cisco.cisco_asa_ssh import CiscoAsaSSH -from netmiko.cisco.cisco_nxos_ssh import CiscoNxosSSH +from netmiko.cisco.cisco_ios import CiscoIosBase, CiscoIosSSH, CiscoIosTelnet, CiscoIosSerial +from netmiko.cisco.cisco_ios import CiscoIosFileTransfer +from netmiko.cisco.cisco_ios import InLineTransfer +from netmiko.cisco.cisco_asa_ssh import CiscoAsaSSH, CiscoAsaFileTransfer +from netmiko.cisco.cisco_nxos_ssh import CiscoNxosSSH, CiscoNxosFileTransfer from netmiko.cisco.cisco_xr_ssh import CiscoXrSSH from netmiko.cisco.cisco_wlc_ssh import CiscoWlcSSH from netmiko.cisco.cisco_s300 import CiscoS300SSH from netmiko.cisco.cisco_tp_tcce import CiscoTpTcCeSSH __all__ = ['CiscoIosSSH', 'CiscoIosTelnet', 'CiscoAsaSSH', 'CiscoNxosSSH', 'CiscoXrSSH', - 'CiscoWlcSSH', 'CiscoS300SSH', 'CiscoTpTcCeSSH', 'CiscoIosBase'] + 'CiscoWlcSSH', 'CiscoS300SSH', 'CiscoTpTcCeSSH', 'CiscoIosBase', + 'CiscoIosFileTransfer', 'InLineTransfer', 'CiscoAsaFileTransfer', + 'CiscoNxosFileTransfer', 'CiscoIosSerial'] diff --git a/netmiko/cisco/cisco_asa_ssh.py b/netmiko/cisco/cisco_asa_ssh.py index 71e22e085..ce46bf6c2 100644 --- a/netmiko/cisco/cisco_asa_ssh.py +++ b/netmiko/cisco/cisco_asa_ssh.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import re import time -from netmiko.cisco_base_connection import CiscoSSHConnection +from netmiko.cisco_base_connection import CiscoSSHConnection, CiscoFileTransfer class CiscoAsaSSH(CiscoSSHConnection): @@ -16,8 +16,11 @@ def session_preparation(self): self.enable() else: self.asa_login() - self.disable_paging(command="terminal pager 0\n") - self.set_terminal_width(command="terminal width 511\n") + self.disable_paging(command="terminal pager 0") + self.set_terminal_width(command="terminal width 511") + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def send_command_timing(self, *args, **kwargs): """ @@ -86,16 +89,25 @@ def asa_login(self): i = 1 max_attempts = 50 - self.write_channel("login\n") + self.write_channel("login" + self.RETURN) while i <= max_attempts: time.sleep(.5 * delay_factor) output = self.read_channel() if 'sername' in output: - self.write_channel(self.username + '\n') + self.write_channel(self.username + self.RETURN) elif 'ssword' in output: - self.write_channel(self.password + '\n') + self.write_channel(self.password + self.RETURN) elif '#' in output: break else: - self.write_channel("login\n") + self.write_channel("login" + self.RETURN) i += 1 + + def save_config(self, cmd='write mem', confirm=False): + """Saves Config""" + return super(CiscoAsaSSH, self).save_config(cmd=cmd, confirm=confirm) + + +class CiscoAsaFileTransfer(CiscoFileTransfer): + """Cisco ASA SCP File Transfer driver.""" + pass diff --git a/netmiko/cisco/cisco_ios.py b/netmiko/cisco/cisco_ios.py index 7f81b874f..34d30c615 100644 --- a/netmiko/cisco/cisco_ios.py +++ b/netmiko/cisco/cisco_ios.py @@ -1,6 +1,12 @@ from __future__ import unicode_literals -from netmiko.cisco_base_connection import CiscoBaseConnection +import time +import re +import os +import hashlib +import io + +from netmiko.cisco_base_connection import CiscoBaseConnection, CiscoFileTransfer class CiscoIosBase(CiscoBaseConnection): @@ -11,13 +17,185 @@ def session_preparation(self): self.set_base_prompt() self.disable_paging() self.set_terminal_width(command='terminal width 511') + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() + + def save_config(self, cmd='write mem', confirm=False): + """Saves Config Using Copy Run Start""" + return super(CiscoIosBase, self).save_config(cmd=cmd, confirm=confirm) -class CiscoIosSSH(CiscoBaseConnection): +class CiscoIosSSH(CiscoIosBase): """Cisco IOS SSH driver.""" pass -class CiscoIosTelnet(CiscoBaseConnection): +class CiscoIosTelnet(CiscoIosBase): """Cisco IOS Telnet driver.""" pass + + +class CiscoIosSerial(CiscoIosBase): + """Cisco IOS Serial driver.""" + pass + + +class CiscoIosFileTransfer(CiscoFileTransfer): + """Cisco IOS SCP File Transfer driver.""" + pass + + +class InLineTransfer(CiscoIosFileTransfer): + """Use TCL on Cisco IOS to directly transfer file.""" + def __init__(self, ssh_conn, source_file=None, dest_file=None, file_system=None, + direction='put', source_config=None): + if source_file and source_config: + msg = "Invalid call to InLineTransfer both source_file and source_config specified." + raise ValueError(msg) + if direction != 'put': + raise ValueError("Only put operation supported by InLineTransfer.") + + self.ssh_ctl_chan = ssh_conn + if source_file: + self.source_file = source_file + self.source_config = None + self.source_md5 = self.file_md5(source_file) + self.file_size = os.stat(source_file).st_size + elif source_config: + self.source_file = None + self.source_config = source_config + self.source_md5 = self.config_md5(source_config) + self.file_size = len(source_config.encode('UTF-8')) + self.dest_file = dest_file + self.direction = direction + + if not file_system: + self.file_system = self.ssh_ctl_chan._autodetect_fs() + else: + self.file_system = file_system + + @staticmethod + def _read_file(file_name): + with io.open(file_name, "rt", encoding='utf-8') as f: + return f.read() + + @staticmethod + def _tcl_newline_rationalize(tcl_string): + """ + When using put inside a TCL {} section the newline is considered a new TCL + statement and causes a missing curly-brace message. Convert "\n" to "\r". TCL + will convert the "\r" to a "\n" i.e. you will see a "\n" inside the file on the + Cisco IOS device. + """ + NEWLINE = r"\n" + CARRIAGE_RETURN = r"\r" + tmp_string = re.sub(NEWLINE, CARRIAGE_RETURN, tcl_string) + if re.search(r"[{}]", tmp_string): + msg = "Curly brace detected in string; TCL requires this be escaped." + raise ValueError(msg) + return tmp_string + + def __enter__(self): + self._enter_tcl_mode() + return self + + def __exit__(self, exc_type, exc_value, traceback): + _ = self._exit_tcl_mode() # noqa + + def _enter_tcl_mode(self): + TCL_ENTER = 'tclsh' + cmd_failed = ['Translating "tclsh"', '% Unknown command', '% Bad IP address'] + output = self.ssh_ctl_chan.send_command(TCL_ENTER, expect_string=r"\(tcl\)#", + strip_prompt=False, strip_command=False) + for pattern in cmd_failed: + if pattern in output: + raise ValueError("Failed to enter tclsh mode on router: {}".format(output)) + return output + + def _exit_tcl_mode(self): + TCL_EXIT = 'tclquit' + self.ssh_ctl_chan.write_channel("\r") + time.sleep(1) + output = self.ssh_ctl_chan.read_channel() + if '(tcl)' in output: + self.ssh_ctl_chan.write_channel(TCL_EXIT + "\r") + time.sleep(1) + output += self.ssh_ctl_chan.read_channel() + return output + + def establish_scp_conn(self): + raise NotImplementedError + + def close_scp_chan(self): + raise NotImplementedError + + def local_space_available(self): + raise NotImplementedError + + def file_md5(self, file_name): + """Compute MD5 hash of file.""" + file_contents = self._read_file(file_name) + file_contents = file_contents + '\n' # Cisco IOS automatically adds this + file_contents = file_contents.encode('UTF-8') + return hashlib.md5(file_contents).hexdigest() + + def config_md5(self, source_config): + """Compute MD5 hash of file.""" + file_contents = source_config + '\n' # Cisco IOS automatically adds this + file_contents = file_contents.encode('UTF-8') + return hashlib.md5(file_contents).hexdigest() + + def put_file(self): + curlybrace = r'{' + TCL_FILECMD_ENTER = 'puts [open "{}{}" w+] {}'.format(self.file_system, + self.dest_file, curlybrace) + TCL_FILECMD_EXIT = '}' + + if self.source_file: + file_contents = self._read_file(self.source_file) + elif self.source_config: + file_contents = self.source_config + file_contents = self._tcl_newline_rationalize(file_contents) + + # Try to remove any existing data + self.ssh_ctl_chan.clear_buffer() + + self.ssh_ctl_chan.write_channel(TCL_FILECMD_ENTER) + time.sleep(.25) + self.ssh_ctl_chan.write_channel(file_contents) + self.ssh_ctl_chan.write_channel(TCL_FILECMD_EXIT + "\r") + + # This operation can be slow (depends on the size of the file) + max_loops = 400 + sleep_time = 4 + if self.file_size >= 2500: + max_loops = 1500 + sleep_time = 12 + elif self.file_size >= 7500: + max_loops = 3000 + sleep_time = 25 + + # Initial delay + time.sleep(sleep_time) + + # File paste and TCL_FILECMD_exit should be indicated by "router(tcl)#" + output = self.ssh_ctl_chan._read_channel_expect(pattern=r"\(tcl\)", max_loops=max_loops) + + # The file doesn't write until tclquit + TCL_EXIT = 'tclquit' + self.ssh_ctl_chan.write_channel(TCL_EXIT + "\r") + + time.sleep(1) + # Read all data remaining from the TCLSH session + output += self.ssh_ctl_chan._read_channel_expect(max_loops=max_loops) + return output + + def get_file(self): + raise NotImplementedError + + def enable_scp(self, cmd=None): + raise NotImplementedError + + def disable_scp(self, cmd=None): + raise NotImplementedError diff --git a/netmiko/cisco/cisco_nxos_ssh.py b/netmiko/cisco/cisco_nxos_ssh.py index f243789ec..0e75a7ff6 100644 --- a/netmiko/cisco/cisco_nxos_ssh.py +++ b/netmiko/cisco/cisco_nxos_ssh.py @@ -1,7 +1,10 @@ from __future__ import print_function from __future__ import unicode_literals import re +import time +import os from netmiko.cisco_base_connection import CiscoSSHConnection +from netmiko.cisco_base_connection import CiscoFileTransfer class CiscoNxosSSH(CiscoSSHConnection): @@ -12,9 +15,83 @@ def session_preparation(self): self.ansi_escape_codes = True self.set_base_prompt() self.disable_paging() + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() - @staticmethod - def normalize_linefeeds(a_string): + def normalize_linefeeds(self, a_string): """Convert '\r\n' or '\r\r\n' to '\n, and remove extra '\r's in the text.""" newline = re.compile(r'(\r\r\n|\r\n)') - return newline.sub('\n', a_string).replace('\r', '') + return newline.sub(self.RESPONSE_RETURN, a_string).replace('\r', '') + + +class CiscoNxosFileTransfer(CiscoFileTransfer): + """Cisco NXOS SCP File Transfer driver.""" + def __init__(self, ssh_conn, source_file, dest_file, file_system='bootflash:', direction='put'): + self.ssh_ctl_chan = ssh_conn + self.source_file = source_file + self.dest_file = dest_file + self.direction = direction + + if file_system: + self.file_system = file_system + else: + raise ValueError("Destination file system must be specified for NX-OS") + + if direction == 'put': + self.source_md5 = self.file_md5(source_file) + self.file_size = os.stat(source_file).st_size + elif direction == 'get': + self.source_md5 = self.remote_md5(remote_file=source_file) + self.file_size = self.remote_file_size(remote_file=source_file) + else: + raise ValueError("Invalid direction specified") + + def check_file_exists(self, remote_cmd=""): + """Check if the dest_file already exists on the file system (return boolean).""" + raise NotImplementedError + + def remote_file_size(self, remote_cmd="", remote_file=None): + """Get the file size of the remote file.""" + if remote_file is None: + if self.direction == 'put': + remote_file = self.dest_file + elif self.direction == 'get': + remote_file = self.source_file + + if not remote_cmd: + remote_cmd = "dir {}/{}".format(self.file_system, remote_file) + + remote_out = self.ssh_ctl_chan.send_command(remote_cmd) + # Match line containing file name + escape_file_name = re.escape(remote_file) + pattern = r".*({}).*".format(escape_file_name) + match = re.search(pattern, remote_out) + if match: + file_size = match.group(0) + file_size = file_size.split()[0] + + if 'No such file or directory' in remote_out: + raise IOError("Unable to find file on remote system") + else: + return int(file_size) + + @staticmethod + def process_md5(md5_output, pattern=r"= (.*)"): + """Not needed on NX-OS.""" + raise NotImplementedError + + def remote_md5(self, base_cmd='show file', remote_file=None): + if remote_file is None: + if self.direction == 'put': + remote_file = self.dest_file + elif self.direction == 'get': + remote_file = self.source_file + remote_md5_cmd = "{} {}{} md5sum".format(base_cmd, self.file_system, remote_file) + return self.ssh_ctl_chan.send_command(remote_md5_cmd, delay_factor=3.0) + + def enable_scp(self, cmd=None): + raise NotImplementedError + + def disable_scp(self, cmd=None): + raise NotImplementedError diff --git a/netmiko/cisco/cisco_s300.py b/netmiko/cisco/cisco_s300.py index 771a9cde5..f183f8a3b 100644 --- a/netmiko/cisco/cisco_s300.py +++ b/netmiko/cisco/cisco_s300.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +import time from netmiko.cisco_base_connection import CiscoSSHConnection @@ -16,5 +17,10 @@ def session_preparation(self): self.ansi_escape_codes = True self._test_channel_read() self.set_base_prompt() - self.disable_paging(command="terminal datadump\n") + self.disable_paging(command="terminal datadump") self.set_terminal_width(command='terminal width 511') + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + + def save_config(self, cmd='write memory', confirm=True, confirm_response='Y'): + return super(CiscoS300SSH, self).save_config(cmd=cmd, confirm=confirm) diff --git a/netmiko/cisco/cisco_tp_tcce.py b/netmiko/cisco/cisco_tp_tcce.py index 2424af573..e779dac50 100644 --- a/netmiko/cisco/cisco_tp_tcce.py +++ b/netmiko/cisco/cisco_tp_tcce.py @@ -6,13 +6,16 @@ Written by Ahmad Barrin """ from __future__ import unicode_literals - +import time import re - from netmiko.cisco_base_connection import CiscoSSHConnection class CiscoTpTcCeSSH(CiscoSSHConnection): + def __init__(self, *args, **kwargs): + default_enter = kwargs.get('default_enter') + kwargs['default_enter'] = '\r\n' if default_enter is None else default_enter + super(CiscoTpTcCeSSH, self).__init__(*args, **kwargs) def disable_paging(self, *args, **kwargs): """Paging is disabled by default.""" @@ -34,6 +37,9 @@ def session_preparation(self): self.set_base_prompt() self.disable_paging() self.set_terminal_width() + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def set_base_prompt(self, *args, **kwargs): """Use 'OK' as base_prompt.""" @@ -47,10 +53,10 @@ def find_prompt(self, *args, **kwargs): def strip_prompt(self, a_string): """Strip the trailing router prompt from the output.""" expect_string = r'^(OK|ERROR|Command not recognized\.)$' - response_list = a_string.split('\n') + response_list = a_string.split(self.RESPONSE_RETURN) last_line = response_list[-1] if re.search(expect_string, last_line): - return '\n'.join(response_list[:-1]) + return self.RESPONSE_RETURN.join(response_list[:-1]) else: return a_string @@ -73,8 +79,13 @@ def send_command(self, *args, **kwargs): else: expect_string = kwargs.get('expect_string') if expect_string is None: - expect_string = r'\r\n(OK|ERROR|Command not recognized\.)\r\n' + expect_string = r'(OK|ERROR|Command not recognized\.)' + expect_string = self.RETURN + expect_string + self.RETURN kwargs.setdefault('expect_string', expect_string) output = super(CiscoSSHConnection, self).send_command(*args, **kwargs) return output + + def save_config(self): + """Not Implemented""" + raise NotImplementedError diff --git a/netmiko/cisco/cisco_wlc_ssh.py b/netmiko/cisco/cisco_wlc_ssh.py index c1a9fb1b2..2e65241bd 100644 --- a/netmiko/cisco/cisco_wlc_ssh.py +++ b/netmiko/cisco/cisco_wlc_ssh.py @@ -13,8 +13,7 @@ class CiscoWlcSSH(BaseConnection): """Netmiko Cisco WLC support.""" def special_login_handler(self, delay_factor=1): - ''' - WLC presents with the following on login (in certain OS versions) + """WLC presents with the following on login (in certain OS versions) login as: user @@ -23,7 +22,7 @@ def special_login_handler(self, delay_factor=1): User: user Password:**** - ''' + """ delay_factor = self.select_delay_factor(delay_factor) i = 0 time.sleep(delay_factor * .5) @@ -32,13 +31,13 @@ def special_login_handler(self, delay_factor=1): output = self.read_channel() if output: if 'login as' in output or 'User' in output: - self.write_channel(self.username + '\n') + self.write_channel(self.username + self.RETURN) elif 'Password' in output: - self.write_channel(self.password + '\n') + self.write_channel(self.password + self.RETURN) break time.sleep(delay_factor * 1) else: - self.write_channel('\n') + self.write_channel(self.RETURN) time.sleep(delay_factor * 1.5) i += 1 @@ -48,7 +47,7 @@ def send_command_w_enter(self, *args, **kwargs): Even though pagination is disabled show run-config also has excessive delays in the output which requires special handling. - Arguments are the same as send_command() method + Arguments are the same as send_command_timing() method ''' if len(args) > 1: raise ValueError("Must pass in delay_factor as keyword argument") @@ -56,19 +55,19 @@ def send_command_w_enter(self, *args, **kwargs): # If no delay_factor use 1 for default value delay_factor = kwargs.get('delay_factor', 1) kwargs['delay_factor'] = self.select_delay_factor(delay_factor) - output = self.send_command(*args, **kwargs) + output = self.send_command_timing(*args, **kwargs) if 'Press Enter to' in output: new_args = list(args) if len(args) == 1: - new_args[0] = '\n' + new_args[0] = self.RETURN else: - kwargs['command_string'] = '\n' + kwargs['command_string'] = self.RETURN if not kwargs.get('max_loops'): kwargs['max_loops'] = 150 # Send an 'enter' - output = self.send_command(*new_args, **kwargs) + output = self.send_command_timing(*new_args, **kwargs) # WLC has excessive delay after this appears on screen if '802.11b Advanced Configuration' in output: @@ -102,11 +101,14 @@ def session_preparation(self): ''' self._test_channel_read() self.set_base_prompt() - self.disable_paging(command="config paging disable\n") + self.disable_paging(command="config paging disable") + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def cleanup(self): """Reset WLC back to normal paging.""" - self.send_command("config paging enable\n") + self.send_command_timing("config paging enable") def check_config_mode(self, check_string='config', pattern=''): """Checks if the device is in configuration mode or not.""" @@ -127,7 +129,8 @@ def exit_config_mode(self, exit_config='exit', pattern=''): return super(CiscoWlcSSH, self).exit_config_mode(exit_config, pattern) def send_config_set(self, config_commands=None, exit_config_mode=True, delay_factor=1, - max_loops=150, strip_prompt=False, strip_command=False): + max_loops=150, strip_prompt=False, strip_command=False, + config_mode_command=None): """ Send configuration commands down the SSH channel. @@ -153,5 +156,9 @@ def send_config_set(self, config_commands=None, exit_config_mode=True, delay_fac # Gather output output = self._read_channel_timing(delay_factor=delay_factor, max_loops=max_loops) output = self._sanitize_output(output) - log.debug("{0}".format(output)) + log.debug("{}".format(output)) return output + + def save_config(self, cmd='save config', confirm=True, confirm_response='y'): + return super(CiscoWlcSSH, self).save_config(cmd=cmd, confirm=confirm, + confirm_response=confirm_response) diff --git a/netmiko/cisco/cisco_xr_ssh.py b/netmiko/cisco/cisco_xr_ssh.py index 2589129a9..2c2df3ba5 100644 --- a/netmiko/cisco/cisco_xr_ssh.py +++ b/netmiko/cisco/cisco_xr_ssh.py @@ -1,8 +1,6 @@ from __future__ import print_function from __future__ import unicode_literals - -import re - +import time from netmiko.cisco_base_connection import CiscoSSHConnection @@ -14,6 +12,9 @@ def session_preparation(self): self.set_base_prompt() self.disable_paging() self.set_terminal_width(command='terminal width 511') + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def send_config_set(self, config_commands=None, exit_config_mode=True, **kwargs): """IOS-XR requires you not exit from configuration mode.""" @@ -101,19 +102,30 @@ def commit(self, confirm=False, confirm_delay=None, comment='', label='', delay_ return output + def check_config_mode(self, check_string=')#', pattern=r"[#\$]"): + """Checks if the device is in configuration mode or not. + + IOS-XR, unfortunately, does this: + RP/0/RSP0/CPU0:BNG(admin)# + """ + self.write_channel(self.RETURN) + output = self.read_until_pattern(pattern=pattern) + # Strip out (admin) so we don't get a false positive with (admin)# + # (admin-config)# would still match. + output = output.replace("(admin)", "") + return check_string in output + def exit_config_mode(self, exit_config='end'): """Exit configuration mode.""" output = '' if self.check_config_mode(): output = self.send_command_timing(exit_config, strip_prompt=False, strip_command=False) if "Uncommitted changes found" in output: - output += self.send_command_timing('no\n', strip_prompt=False, strip_command=False) + output += self.send_command_timing('no', strip_prompt=False, strip_command=False) if self.check_config_mode(): raise ValueError("Failed to exit configuration mode") return output - @staticmethod - def normalize_linefeeds(a_string): - """Convert '\r\n','\r\r\n', '\n\r', or '\r' to '\n.""" - newline = re.compile(r'(\r\r\n|\r\n|\n\r|\r)') - return newline.sub('\n', a_string) + def save_config(self): + """Not Implemented (use commit() method)""" + raise NotImplementedError diff --git a/netmiko/cisco_base_connection.py b/netmiko/cisco_base_connection.py index c5e8f5bf5..31ad1eb53 100644 --- a/netmiko/cisco_base_connection.py +++ b/netmiko/cisco_base_connection.py @@ -1,6 +1,7 @@ """CiscoBaseConnection is netmiko SSH class for Cisco and Cisco-like platforms.""" from __future__ import unicode_literals from netmiko.base_connection import BaseConnection +from netmiko.scp_handler import BaseFileTransfer from netmiko.ssh_exception import NetMikoAuthenticationException import re import time @@ -49,12 +50,23 @@ def exit_config_mode(self, exit_config='end', pattern=''): return super(CiscoBaseConnection, self).exit_config_mode(exit_config=exit_config, pattern=pattern) - def telnet_login(self, pri_prompt_terminator='#', alt_prompt_terminator='>', - username_pattern=r"sername", pwd_pattern=r"assword", - delay_factor=1, max_loops=60): + def serial_login(self, pri_prompt_terminator=r'#\s*$', alt_prompt_terminator=r'>\s*$', + username_pattern=r"(?:[Uu]ser:|sername|ogin)", pwd_pattern=r"assword", + delay_factor=1, max_loops=20): + self.write_channel(self.TELNET_RETURN) + output = self.read_channel() + if (re.search(pri_prompt_terminator, output, flags=re.M) + or re.search(alt_prompt_terminator, output, flags=re.M)): + return output + else: + return self.telnet_login(pri_prompt_terminator, alt_prompt_terminator, + username_pattern, pwd_pattern, delay_factor, max_loops) + + def telnet_login(self, pri_prompt_terminator=r'#\s*$', alt_prompt_terminator=r'>\s*$', + username_pattern=r"(?:[Uu]ser:|sername|ogin|User Name)", + pwd_pattern=r"assword", + delay_factor=1, max_loops=20): """Telnet login. Can be username/password or just password.""" - TELNET_RETURN = '\r\n' - delay_factor = self.select_delay_factor(delay_factor) time.sleep(1 * delay_factor) @@ -68,23 +80,24 @@ def telnet_login(self, pri_prompt_terminator='#', alt_prompt_terminator='>', # Search for username pattern / send username if re.search(username_pattern, output): - self.write_channel(self.username + TELNET_RETURN) + self.write_channel(self.username + self.TELNET_RETURN) time.sleep(1 * delay_factor) output = self.read_channel() return_msg += output # Search for password pattern / send password if re.search(pwd_pattern, output): - self.write_channel(self.password + TELNET_RETURN) + self.write_channel(self.password + self.TELNET_RETURN) time.sleep(.5 * delay_factor) output = self.read_channel() return_msg += output - if pri_prompt_terminator in output or alt_prompt_terminator in output: + if (re.search(pri_prompt_terminator, output, flags=re.M) + or re.search(alt_prompt_terminator, output, flags=re.M)): return return_msg # Support direct telnet through terminal server if re.search(r"initial configuration dialog\? \[yes/no\]: ", output): - self.write_channel("no" + TELNET_RETURN) + self.write_channel("no" + self.TELNET_RETURN) time.sleep(.5 * delay_factor) count = 0 while count < 15: @@ -98,30 +111,32 @@ def telnet_login(self, pri_prompt_terminator='#', alt_prompt_terminator='>', # Check for device with no password configured if re.search(r"assword required, but none set", output): - msg = "Telnet login failed - Password required, but none set: {0}".format( + msg = "Telnet login failed - Password required, but none set: {}".format( self.host) raise NetMikoAuthenticationException(msg) # Check if proper data received - if pri_prompt_terminator in output or alt_prompt_terminator in output: + if (re.search(pri_prompt_terminator, output, flags=re.M) + or re.search(alt_prompt_terminator, output, flags=re.M)): return return_msg - self.write_channel(TELNET_RETURN) + self.write_channel(self.TELNET_RETURN) time.sleep(.5 * delay_factor) i += 1 except EOFError: - msg = "Telnet login failed: {0}".format(self.host) + msg = "Telnet login failed: {}".format(self.host) raise NetMikoAuthenticationException(msg) # Last try to see if we already logged in - self.write_channel(TELNET_RETURN) + self.write_channel(self.TELNET_RETURN) time.sleep(.5 * delay_factor) output = self.read_channel() return_msg += output - if pri_prompt_terminator in output or alt_prompt_terminator in output: + if (re.search(pri_prompt_terminator, output, flags=re.M) + or re.search(alt_prompt_terminator, output, flags=re.M)): return return_msg - msg = "Telnet login failed: {0}".format(self.host) + msg = "Telnet login failed: {}".format(self.host) raise NetMikoAuthenticationException(msg) def cleanup(self): @@ -131,7 +146,7 @@ def cleanup(self): except Exception: # Always try to send 'exit' regardless of whether exit_config_mode works or not. pass - self.write_channel("exit\n") + self.write_channel("exit" + self.RETURN) def _autodetect_fs(self, cmd='dir', pattern=r'Directory of (.*)/'): """Autodetect the file system on the remote device. Used by SCP operations.""" @@ -148,6 +163,26 @@ def _autodetect_fs(self, cmd='dir', pattern=r'Directory of (.*)/'): raise ValueError("An error occurred in dynamically determining remote file " "system: {} {}".format(cmd, output)) + def save_config(self, cmd='copy running-config startup-config', confirm=False, + confirm_response=''): + """Saves Config.""" + self.enable() + if confirm: + output = self.send_command_timing(command_string=cmd) + if confirm_response: + output += self.send_command_timing(confirm_response) + else: + # Send enter by default + output += self.send_command_timing(self.RETURN) + else: + # Some devices are slow so match on trailing-prompt if you can + output = self.send_command(command_string=cmd) + return output + class CiscoSSHConnection(CiscoBaseConnection): pass + + +class CiscoFileTransfer(BaseFileTransfer): + pass diff --git a/netmiko/coriant/__init__.py b/netmiko/coriant/__init__.py new file mode 100644 index 000000000..4cfd0163e --- /dev/null +++ b/netmiko/coriant/__init__.py @@ -0,0 +1,3 @@ +from netmiko.coriant.coriant_ssh import CoriantSSH + +__all__ = ['CoriantSSH'] diff --git a/netmiko/coriant/coriant_ssh.py b/netmiko/coriant/coriant_ssh.py new file mode 100644 index 000000000..9fa901960 --- /dev/null +++ b/netmiko/coriant/coriant_ssh.py @@ -0,0 +1,42 @@ + +from __future__ import unicode_literals +from netmiko.cisco_base_connection import CiscoSSHConnection + + +class CoriantSSH(CiscoSSHConnection): + def session_preparation(self): + self._test_channel_read() + self.set_base_prompt() + + def check_enable_mode(self, *args, **kwargs): + raise AttributeError("Coriant devices do not support enable mode!") + + def enable(self, *args, **kwargs): + raise AttributeError("Coriant devices do not support enable mode!") + + def exit_enable_mode(self, *args, **kwargs): + raise AttributeError("Coriant devices do not support enable mode!") + + def check_config_mode(self): + """Coriant devices do not have a config mode.""" + return False + + def config_mode(self): + """Coriant devices do not have a config mode.""" + return '' + + def exit_config_mode(self): + """Coriant devices do not have a config mode.""" + return '' + + def set_base_prompt(self, pri_prompt_terminator=':', alt_prompt_terminator='>', + delay_factor=2): + """Sets self.base_prompt: used as delimiter for stripping of trailing prompt in output.""" + super(CoriantSSH, self).set_base_prompt(pri_prompt_terminator=pri_prompt_terminator, + alt_prompt_terminator=alt_prompt_terminator, + delay_factor=delay_factor) + return self.base_prompt + + def save_config(self, cmd='', confirm=True, confirm_response=''): + """Not Implemented""" + raise NotImplementedError diff --git a/netmiko/dell/__init__.py b/netmiko/dell/__init__.py index 15868b82b..40d788850 100644 --- a/netmiko/dell/__init__.py +++ b/netmiko/dell/__init__.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from netmiko.dell.dell_force10_ssh import DellForce10SSH -from netmiko.dell.dell_powerconnect_ssh import DellPowerConnectSSH -from netmiko.dell.dell_powerconnect_telnet import DellPowerConnectTelnet +from netmiko.dell.dell_powerconnect import DellPowerConnectSSH +from netmiko.dell.dell_powerconnect import DellPowerConnectTelnet __all__ = ['DellForce10SSH', 'DellPowerConnectSSH', 'DellPowerConnectTelnet'] diff --git a/netmiko/dell/dell_force10_ssh.py b/netmiko/dell/dell_force10_ssh.py index 3646908fe..0481b9ab1 100644 --- a/netmiko/dell/dell_force10_ssh.py +++ b/netmiko/dell/dell_force10_ssh.py @@ -5,4 +5,7 @@ class DellForce10SSH(CiscoSSHConnection): """Dell Force10 Driver - supports DNOS9.""" - pass + + def save_config(self, cmd='copy running-configuration startup-configuration', confirm=False): + """Saves Config""" + return super(DellForce10SSH, self).save_config(cmd=cmd, confirm=confirm) diff --git a/netmiko/dell/dell_powerconnect_ssh.py b/netmiko/dell/dell_powerconnect.py similarity index 67% rename from netmiko/dell/dell_powerconnect_ssh.py rename to netmiko/dell/dell_powerconnect.py index 12a497db0..f85ea22a4 100644 --- a/netmiko/dell/dell_powerconnect_ssh.py +++ b/netmiko/dell/dell_powerconnect.py @@ -1,9 +1,9 @@ """Dell PowerConnect Driver.""" from __future__ import unicode_literals -from netmiko.cisco_base_connection import CiscoSSHConnection from paramiko import SSHClient import time from os import path +from netmiko.cisco_base_connection import CiscoSSHConnection class SSHClient_noauth(SSHClient): @@ -12,13 +12,44 @@ def _auth(self, username, *args): return -class DellPowerConnectSSH(CiscoSSHConnection): +class DellPowerConnectBase(CiscoSSHConnection): + """Dell PowerConnect Driver.""" + def session_preparation(self): + """Prepare the session after the connection has been established.""" + self.ansi_escape_codes = True + self._test_channel_read() + self.set_base_prompt() + self.disable_paging(command="terminal datadump") + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() + + def set_base_prompt(self, pri_prompt_terminator='>', alt_prompt_terminator='#', + delay_factor=1): + """Sets self.base_prompt: used as delimiter for stripping of trailing prompt in output.""" + prompt = super(DellPowerConnectBase, self).set_base_prompt( + pri_prompt_terminator=pri_prompt_terminator, + alt_prompt_terminator=alt_prompt_terminator, + delay_factor=delay_factor) + prompt = prompt.strip() + self.base_prompt = prompt + return self.base_prompt + + def check_config_mode(self, check_string='(config)#'): + """Checks if the device is in configuration mode""" + return super(DellPowerConnectBase, self).check_config_mode(check_string=check_string) + + def config_mode(self, config_command='config'): + """Enter configuration mode.""" + return super(DellPowerConnectBase, self).config_mode(config_command=config_command) + + +class DellPowerConnectSSH(DellPowerConnectBase): """Dell PowerConnect Driver. To make it work, we have to override the SSHClient _auth method. If we use login/password, the ssh server use the (none) auth mechanism. """ - def _build_ssh_client(self): """Prepare for Paramiko SSH connection. @@ -57,38 +88,26 @@ def special_login_handler(self, delay_factor=1): output = self.read_channel() if output: if 'User Name:' in output: - self.write_channel(self.username + '\n') + self.write_channel(self.username + self.RETURN) elif 'Password:' in output: - self.write_channel(self.password + '\n') + self.write_channel(self.password + self.RETURN) break time.sleep(delay_factor * 1) else: - self.write_channel('\n') + self.write_channel(self.RETURN) time.sleep(delay_factor * 1.5) i += 1 + +class DellPowerConnectTelnet(DellPowerConnectBase): def session_preparation(self): """Prepare the session after the connection has been established.""" self.ansi_escape_codes = True self._test_channel_read() self.set_base_prompt() - self.disable_paging(command="terminal datadump") - - def set_base_prompt(self, pri_prompt_terminator='>', alt_prompt_terminator='#', - delay_factor=1): - """Sets self.base_prompt: used as delimiter for stripping of trailing prompt in output.""" - prompt = super(DellPowerConnectSSH, self).set_base_prompt( - pri_prompt_terminator=pri_prompt_terminator, - alt_prompt_terminator=alt_prompt_terminator, - delay_factor=delay_factor) - prompt = prompt.strip() - self.base_prompt = prompt - return self.base_prompt - - def check_config_mode(self, check_string='(config)#'): - """Checks if the device is in configuration mode""" - return super(DellPowerConnectSSH, self).check_config_mode(check_string=check_string) - - def config_mode(self, config_command='config'): - """Enter configuration mode.""" - return super(DellPowerConnectSSH, self).config_mode(config_command=config_command) + self.enable() + self.disable_paging(command="terminal length 0") + self.set_terminal_width() + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() diff --git a/netmiko/dell/dell_powerconnect_telnet.py b/netmiko/dell/dell_powerconnect_telnet.py deleted file mode 100644 index e9e28cb27..000000000 --- a/netmiko/dell/dell_powerconnect_telnet.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Dell Telnet Driver.""" -from __future__ import unicode_literals - -import time -from netmiko.cisco_base_connection import CiscoBaseConnection -from netmiko import log - - -class DellPowerConnectTelnet(CiscoBaseConnection): - def disable_paging(self, command="terminal length 0", delay_factor=1): - """Must be in enable mode to disable paging.""" - - self.enable() - delay_factor = self.select_delay_factor(delay_factor) - time.sleep(delay_factor * .1) - self.clear_buffer() - command = self.normalize_cmd(command) - log.debug("In disable_paging") - log.debug("Command: {0}".format(command)) - self.write_channel(command) - output = self.read_until_prompt() - if self.ansi_escape_codes: - output = self.strip_ansi_escape_codes(output) - log.debug("{0}".format(output)) - log.debug("Exiting disable_paging") - return output - - def telnet_login(self, pri_prompt_terminator='#', alt_prompt_terminator='>', - username_pattern=r"User:", pwd_pattern=r"assword", - delay_factor=1, max_loops=60): - """Telnet login. Can be username/password or just password.""" - super(DellPowerConnectTelnet, self).telnet_login( - pri_prompt_terminator=pri_prompt_terminator, - alt_prompt_terminator=alt_prompt_terminator, - username_pattern=username_pattern, - pwd_pattern=pwd_pattern, - delay_factor=delay_factor, - max_loops=max_loops) diff --git a/netmiko/eltex/eltex_ssh.py b/netmiko/eltex/eltex_ssh.py index 85344db24..8c3b1beff 100644 --- a/netmiko/eltex/eltex_ssh.py +++ b/netmiko/eltex/eltex_ssh.py @@ -1,14 +1,21 @@ from __future__ import print_function from __future__ import unicode_literals +import time from netmiko.cisco_base_connection import CiscoSSHConnection class EltexSSH(CiscoSSHConnection): def session_preparation(self): - """ - Prepare the session after the connection has been established - """ + """Prepare the session after the connection has been established.""" self.ansi_escape_codes = True self._test_channel_read() self.set_base_prompt() self.disable_paging(command='terminal datadump') + + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() + + def save_config(self, cmd='', confirm=True, confirm_response=''): + """Not Implemented""" + raise NotImplementedError diff --git a/netmiko/enterasys/enterasys_ssh.py b/netmiko/enterasys/enterasys_ssh.py index 65bf98315..9a1b2c83b 100644 --- a/netmiko/enterasys/enterasys_ssh.py +++ b/netmiko/enterasys/enterasys_ssh.py @@ -1,5 +1,6 @@ """Enterasys support.""" from __future__ import unicode_literals +import time from netmiko.cisco_base_connection import CiscoSSHConnection @@ -9,4 +10,11 @@ def session_preparation(self): """Enterasys requires enable mode to disable paging.""" self._test_channel_read() self.set_base_prompt() - self.disable_paging(command="set length 0\n") + self.disable_paging(command="set length 0") + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() + + def save_config(self, cmd='', confirm=True, confirm_response=''): + """Not Implemented""" + raise NotImplementedError diff --git a/netmiko/extreme/__init__.py b/netmiko/extreme/__init__.py index 150eff194..1074d9428 100644 --- a/netmiko/extreme/__init__.py +++ b/netmiko/extreme/__init__.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals -from netmiko.extreme.extreme_ssh import ExtremeSSH +from netmiko.extreme.extreme_exos import ExtremeSSH +from netmiko.extreme.extreme_exos import ExtremeTelnet from netmiko.extreme.extreme_wing_ssh import ExtremeWingSSH -__all__ = ['ExtremeSSH', 'ExtremeWingSSH'] +__all__ = ['ExtremeSSH', 'ExtremeWingSSH', 'ExtremeTelnet'] diff --git a/netmiko/extreme/extreme_ssh.py b/netmiko/extreme/extreme_exos.py similarity index 62% rename from netmiko/extreme/extreme_ssh.py rename to netmiko/extreme/extreme_exos.py index f20bf56be..94e406a81 100644 --- a/netmiko/extreme/extreme_ssh.py +++ b/netmiko/extreme/extreme_exos.py @@ -1,11 +1,11 @@ """Extreme support.""" from __future__ import unicode_literals - +import time import re from netmiko.cisco_base_connection import CiscoSSHConnection -class ExtremeSSH(CiscoSSHConnection): +class ExtremeBase(CiscoSSHConnection): """Extreme support. Designed for EXOS >= 15.0 @@ -13,7 +13,11 @@ class ExtremeSSH(CiscoSSHConnection): def session_preparation(self): self._test_channel_read() self.set_base_prompt() - self.disable_paging(command="disable clipaging\n") + self.disable_paging(command="disable clipaging") + self.send_command_timing("disable cli prompting") + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def set_base_prompt(self, *args, **kwargs): """ @@ -30,7 +34,7 @@ def set_base_prompt(self, *args, **kwargs): * testhost.4 # * testhost.5 # """ - cur_base_prompt = super(ExtremeSSH, self).set_base_prompt(*args, **kwargs) + cur_base_prompt = super(ExtremeBase, self).set_base_prompt(*args, **kwargs) # Strip off any leading * or whitespace chars; strip off trailing period and digits match = re.search(r'[\*\s]*(.*)\.\d+', cur_base_prompt) if match: @@ -47,7 +51,7 @@ def send_command(self, *args, **kwargs): # refresh self.base_prompt self.set_base_prompt() - return super(ExtremeSSH, self).send_command(*args, **kwargs) + return super(ExtremeBase, self).send_command(*args, **kwargs) def config_mode(self, config_command=''): """No configuration mode on Extreme.""" @@ -55,8 +59,23 @@ def config_mode(self, config_command=''): def check_config_mode(self, check_string='#'): """Checks whether in configuration mode. Returns a boolean.""" - return super(ExtremeSSH, self).check_config_mode(check_string=check_string) + return super(ExtremeBase, self).check_config_mode(check_string=check_string) def exit_config_mode(self, exit_config=''): """No configuration mode on Extreme.""" return '' + + def save_config(self, cmd='save configuration primary', confirm=False): + """Saves configuration.""" + return super(ExtremeBase, self).save_config(cmd=cmd, confirm=confirm) + + +class ExtremeSSH(ExtremeBase): + pass + + +class ExtremeTelnet(ExtremeBase): + def __init__(self, *args, **kwargs): + default_enter = kwargs.get('default_enter') + kwargs['default_enter'] = '\r\n' if default_enter is None else default_enter + super(ExtremeTelnet, self).__init__(*args, **kwargs) diff --git a/netmiko/extreme/extreme_wing_ssh.py b/netmiko/extreme/extreme_wing_ssh.py index b4f0364e6..c026d7e21 100644 --- a/netmiko/extreme/extreme_wing_ssh.py +++ b/netmiko/extreme/extreme_wing_ssh.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals - +import time from netmiko.cisco_base_connection import CiscoSSHConnection @@ -9,5 +9,8 @@ def session_preparation(self): self.set_base_prompt(pri_prompt_terminator='>', alt_prompt_terminator='#', delay_factor=2) - self.disable_paging(command="no page\n") + self.disable_paging(command="no page") self.set_terminal_width(command='terminal width 512') + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() diff --git a/netmiko/f5/f5_ltm_ssh.py b/netmiko/f5/f5_ltm_ssh.py index 205b816b3..8d9abb498 100644 --- a/netmiko/f5/f5_ltm_ssh.py +++ b/netmiko/f5/f5_ltm_ssh.py @@ -1,8 +1,5 @@ from __future__ import unicode_literals - import time -import re - from netmiko.base_connection import BaseConnection @@ -10,25 +7,21 @@ class F5LtmSSH(BaseConnection): def session_preparation(self): """Prepare the session after the connection has been established.""" - delay_factor = self.select_delay_factor(delay_factor=0) self._test_channel_read() self.set_base_prompt() - self.disable_paging(command="\nset length 0\n") - time.sleep(1 * delay_factor) self.tmsh_mode() self.set_base_prompt() + self.disable_paging(command="modify cli preference pager disabled") + self.clear_buffer() + cmd = 'run /util bash -c "stty cols 255"' + self.set_terminal_width(command=cmd) def tmsh_mode(self, delay_factor=1): """tmsh command is equivalent to config command on F5.""" delay_factor = self.select_delay_factor(delay_factor) self.clear_buffer() - self.write_channel("\ntmsh\n") + command = "{}tmsh{}".format(self.RETURN, self.RETURN) + self.write_channel(command) time.sleep(1 * delay_factor) self.clear_buffer() return None - - @staticmethod - def normalize_linefeeds(a_string): - """Convert '\r\n' or '\r\r\n' to '\n, and remove '\r's in the text.""" - newline = re.compile(r'(\r\n|\r\n\r\n|\r\r\n|\n\r|\r)') - return newline.sub('\n', a_string) diff --git a/netmiko/fortinet/fortinet_ssh.py b/netmiko/fortinet/fortinet_ssh.py index 4cfa0084d..793d28311 100644 --- a/netmiko/fortinet/fortinet_ssh.py +++ b/netmiko/fortinet/fortinet_ssh.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals import paramiko +import time from netmiko.cisco_base_connection import CiscoSSHConnection @@ -17,10 +18,13 @@ def session_preparation(self): self._test_channel_read() self.set_base_prompt(alt_prompt_terminator='$') self.disable_paging() + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def disable_paging(self, delay_factor=1): """Disable paging is only available with specific roles so it may fail.""" - check_command = "get system status | grep Virtual\n" + check_command = "get system status | grep Virtual" output = self.send_command_timing(check_command) self.allow_disable_global = True self.vdoms = False @@ -43,7 +47,7 @@ def disable_paging(self, delay_factor=1): outputlist = [self.send_command_timing(command, delay_factor=2) for command in disable_paging_commands] # Should test output is valid - new_output = "\n".join(outputlist) + new_output = self.RETURN.join(outputlist) return output + new_output @@ -64,3 +68,7 @@ def config_mode(self, config_command=''): def exit_config_mode(self, exit_config=''): """No config mode for Fortinet devices.""" return '' + + def save_config(self, cmd='', confirm=True, confirm_response=''): + """Not Implemented""" + raise NotImplementedError diff --git a/netmiko/hp/hp_comware_ssh.py b/netmiko/hp/hp_comware_ssh.py index 8642a3c75..76d867084 100644 --- a/netmiko/hp/hp_comware_ssh.py +++ b/netmiko/hp/hp_comware_ssh.py @@ -1,6 +1,6 @@ from __future__ import print_function from __future__ import unicode_literals - +import time from netmiko.cisco_base_connection import CiscoSSHConnection @@ -11,9 +11,24 @@ def session_preparation(self): Prepare the session after the connection has been established. Extra time to read HP banners. """ + delay_factor = self.select_delay_factor(delay_factor=0) + i = 1 + while i <= 4: + # Comware can have a banner that prompts you to continue + # 'Press Y or ENTER to continue, N to exit.' + time.sleep(.5 * delay_factor) + self.write_channel("\n") + i += 1 + + time.sleep(.3 * delay_factor) + self.clear_buffer() self._test_channel_read(pattern=r'[>\]]') self.set_base_prompt() - self.disable_paging(command="\nscreen-length disable\n") + command = self.RETURN + "screen-length disable" + self.disable_paging(command=command) + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def config_mode(self, config_command='system-view'): """Enter configuration mode.""" @@ -61,3 +76,7 @@ def exit_enable_mode(self, exit_command='return'): def check_enable_mode(self, check_string=']'): """enable mode on Comware is system-view.""" return self.check_config_mode(check_string=check_string) + + def save_config(self, cmd='save force', confirm=False): + """Save Config.""" + return super(HPComwareSSH, self).save_config(cmd=cmd, confirm=confirm) diff --git a/netmiko/hp/hp_procurve_ssh.py b/netmiko/hp/hp_procurve_ssh.py index df8cea25e..fadaa6a59 100644 --- a/netmiko/hp/hp_procurve_ssh.py +++ b/netmiko/hp/hp_procurve_ssh.py @@ -20,50 +20,59 @@ def session_preparation(self): while count <= 30: output += self.read_channel() if 'any key to continue' in output: - self.write_channel("\n") + self.write_channel(self.RETURN) break else: time.sleep(.33 * delay_factor) count += 1 # Try one last time to past "Press any key to continue - self.write_channel("\n") + self.write_channel(self.RETURN) # HP output contains VT100 escape codes self.ansi_escape_codes = True self._test_channel_read(pattern=r'[>#]') self.set_base_prompt() - self.disable_paging(command="\nno page\n") + command = self.RETURN + "no page" + self.disable_paging(command=command) self.set_terminal_width(command='terminal width 511') + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def enable(self, cmd='enable', pattern='password', re_flags=re.IGNORECASE, default_username='manager'): """Enter enable mode""" output = self.send_command_timing(cmd) - if 'username' in output.lower(): + if 'username' in output.lower() or 'login name' in output.lower() or \ + 'user name' in output.lower(): output += self.send_command_timing(default_username) if 'password' in output.lower(): output += self.send_command_timing(self.secret) - log.debug("{0}".format(output)) + log.debug("{}".format(output)) self.clear_buffer() return output def cleanup(self): """Gracefully exit the SSH session.""" self.exit_config_mode() - self.write_channel("logout\n") + self.write_channel("logout" + self.RETURN) count = 0 while count <= 5: time.sleep(.5) output = self.read_channel() if 'Do you want to log out' in output: - self.write_channel("y\n") + self.write_channel("y" + self.RETURN) # Don't automatically save the config (user's responsibility) elif 'Do you want to save the current' in output: - self.write_channel("n\n") + self.write_channel("n" + self.RETURN) try: - self.write_channel("\n") + self.write_channel(self.RETURN) except socket.error: break count += 1 + + def save_config(self, cmd='write memory', confirm=False): + """Save Config.""" + return super(HPProcurveSSH, self).save_config(cmd=cmd, confirm=confirm) diff --git a/netmiko/huawei/__init__.py b/netmiko/huawei/__init__.py index 5e219b01f..9a9a938e4 100644 --- a/netmiko/huawei/__init__.py +++ b/netmiko/huawei/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals -from netmiko.huawei.huawei_ssh import HuaweiSSH +from netmiko.huawei.huawei_ssh import HuaweiSSH, HuaweiVrpv8SSH -__all__ = ['HuaweiSSH'] +__all__ = ['HuaweiSSH', 'HuaweiVrpv8SSH'] diff --git a/netmiko/huawei/huawei_ssh.py b/netmiko/huawei/huawei_ssh.py index a5285de44..2c584a762 100644 --- a/netmiko/huawei/huawei_ssh.py +++ b/netmiko/huawei/huawei_ssh.py @@ -12,7 +12,10 @@ def session_preparation(self): """Prepare the session after the connection has been established.""" self._test_channel_read() self.set_base_prompt() - self.disable_paging(command="screen-length 0 temporary\n") + self.disable_paging(command="screen-length 0 temporary") + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def config_mode(self, config_command='system-view'): """Enter configuration mode.""" @@ -53,14 +56,14 @@ def set_base_prompt(self, pri_prompt_terminator='>', alt_prompt_terminator=']', log.debug("In set_base_prompt") delay_factor = self.select_delay_factor(delay_factor) self.clear_buffer() - self.write_channel("\n") + self.write_channel(self.RETURN) time.sleep(.5 * delay_factor) prompt = self.read_channel() prompt = self.normalize_linefeeds(prompt) # If multiple lines in the output take the last line - prompt = prompt.split('\n')[-1] + prompt = prompt.split(self.RESPONSE_RETURN)[-1] prompt = prompt.strip() # Check that ends with a valid terminator character @@ -77,3 +80,43 @@ def set_base_prompt(self, pri_prompt_terminator='>', alt_prompt_terminator=']', log.debug("prompt: {0}".format(self.base_prompt)) return self.base_prompt + + def save_config(self, cmd='save', confirm=False, confirm_response=''): + """ Save Config for HuaweiSSH""" + return super(HuaweiSSH, self).save_config(cmd=cmd, confirm=confirm) + + +class HuaweiVrpv8SSH(HuaweiSSH): + + def commit(self, comment='', delay_factor=1): + """ + Commit the candidate configuration. + + Commit the entered configuration. Raise an error and return the failure + if the commit fails. + + default: + command_string = commit + comment: + command_string = commit comment + + """ + delay_factor = self.select_delay_factor(delay_factor) + error_marker = 'Failed to generate committed config' + command_string = 'commit' + + if comment: + command_string += ' comment "{}"'.format(comment) + + output = self.config_mode() + output += self.send_command_expect(command_string, strip_prompt=False, + strip_command=False, delay_factor=delay_factor) + output += self.exit_config_mode() + + if error_marker in output: + raise ValueError('Commit failed with following errors:\n\n{}'.format(output)) + return output + + def save_config(self, cmd='', confirm=True, confirm_response=''): + """Not Implemented""" + raise NotImplementedError diff --git a/netmiko/juniper/__init__.py b/netmiko/juniper/__init__.py index 631865ca0..22f8118c1 100644 --- a/netmiko/juniper/__init__.py +++ b/netmiko/juniper/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals -from netmiko.juniper.juniper_ssh import JuniperSSH +from netmiko.juniper.juniper_ssh import JuniperSSH, JuniperFileTransfer -__all__ = ['JuniperSSH'] +__all__ = ['JuniperSSH', 'JuniperFileTransfer'] diff --git a/netmiko/juniper/juniper_ssh.py b/netmiko/juniper/juniper_ssh.py index ba6227b9d..b46f95d1e 100644 --- a/netmiko/juniper/juniper_ssh.py +++ b/netmiko/juniper/juniper_ssh.py @@ -2,8 +2,10 @@ import re import time +import os from netmiko.base_connection import BaseConnection +from netmiko.scp_handler import BaseFileTransfer, SCPConn class JuniperSSH(BaseConnection): @@ -23,8 +25,11 @@ def session_preparation(self): self._test_channel_read() self.enter_cli_mode() self.set_base_prompt() - self.disable_paging(command="set cli screen-length 0\n") + self.disable_paging(command="set cli screen-length 0") self.set_terminal_width(command='set cli screen-width 511') + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def enter_cli_mode(self): """Check if at shell prompt root@ and go into CLI.""" @@ -32,11 +37,11 @@ def enter_cli_mode(self): count = 0 cur_prompt = '' while count < 50: - self.write_channel("\n") + self.write_channel(self.RETURN) time.sleep(.1 * delay_factor) cur_prompt = self.read_channel() - if re.search(r'root@', cur_prompt): - self.write_channel("cli\n") + if re.search(r'root@', cur_prompt) or re.search(r"^%$", cur_prompt.strip()): + self.write_channel("cli" + self.RETURN) time.sleep(.3 * delay_factor) self.clear_buffer() break @@ -153,8 +158,7 @@ def strip_prompt(self, *args, **kwargs): a_string = super(JuniperSSH, self).strip_prompt(*args, **kwargs) return self.strip_context_items(a_string) - @staticmethod - def strip_context_items(a_string): + def strip_context_items(self, a_string): """Strip Juniper-specific output. Juniper will also put a configuration context: @@ -174,11 +178,157 @@ def strip_context_items(a_string): r'\{secondary.*\}', ] - response_list = a_string.split('\n') + response_list = a_string.split(self.RESPONSE_RETURN) last_line = response_list[-1] for pattern in strings_to_strip: if re.search(pattern, last_line): - return "\n".join(response_list[:-1]) - + return self.RESPONSE_RETURN.join(response_list[:-1]) return a_string + + +class JuniperFileTransfer(BaseFileTransfer): + """Juniper SCP File Transfer driver.""" + def __init__(self, ssh_conn, source_file, dest_file, file_system="/var/tmp", direction='put'): + msg = "Juniper SCP Driver is under development and not fully implemented" + raise NotImplementedError(msg) + self.ssh_ctl_chan = ssh_conn + self.dest_file = dest_file + self.direction = direction + + self.file_system = file_system + + if direction == 'put': + self.source_file = source_file + # self.source_md5 = self.file_md5(source_file) + self.file_size = os.stat(self.source_file).st_size + elif direction == 'get': + self.source_file = "{}/{}".format(file_system, source_file) + # self.source_md5 = self.remote_md5(remote_file=source_file) + self.file_size = self.remote_file_size(remote_file=self.source_file) + else: + raise ValueError("Invalid direction specified") + + def __enter__(self): + """Context manager setup""" + self.establish_scp_conn() + return self + + def __exit__(self, exc_type, exc_value, traceback): + """Context manager cleanup.""" + self.close_scp_chan() + + def establish_scp_conn(self): + """Establish SCP connection.""" + self.scp_conn = SCPConn(self.ssh_ctl_chan) + + def close_scp_chan(self): + """Close the SCP connection to the remote network device.""" + self.scp_conn.close() + self.scp_conn = None + + def remote_space_available(self, search_pattern=""): + """Return space available on remote device.""" + # Ensure at BSD prompt + self.ssh_ctl_chan.send_command('start shell sh', expect_string=r"[\$#]") + remote_cmd = "/bin/df -k {}".format(self.file_system) + remote_output = self.ssh_ctl_chan.send_command(remote_cmd, expect_string=r"[\$#]") + + # Try to ensure parsing is correct: + # Filesystem 512-blocks Used Avail Capacity Mounted on + # /dev/bo0s3f 1264808 16376 1147248 1% /cf/var + remote_output = remote_output.strip() + fields = remote_output.splitlines() + + # First line is the header; second is the actual file system info + header_line = fields[0] + filesystem_line = fields[1] + + if 'Filesystem' not in header_line or 'Avail' not in header_line.split()[3]: + # Filesystem 512-blocks Used Avail Capacity Mounted on + msg = "Parsing error, unexpected output from {}:\n{}".format(remote_cmd, + remote_output) + raise ValueError(msg) + + space_available = filesystem_line.split()[3] + if not re.search(r"^\d+$", space_available): + msg = "Parsing error, unexpected output from {}:\n{}".format(remote_cmd, + remote_output) + raise ValueError(msg) + + # Ensure back at CLI prompt + self.ssh_ctl_chan.send_command('cli', expect_string=r">") + return int(space_available) * 1024 + + def check_file_exists(self, remote_cmd=""): + """Check if the dest_file already exists on the file system (return boolean).""" + if self.direction == 'put': + self.ssh_ctl_chan.send_command('start shell sh', expect_string=r"[\$#]") + remote_cmd = "ls {}/{}".format(self.file_system, self.dest_file) + remote_out = self.ssh_ctl_chan.send_command(remote_cmd, expect_string=r"[\$#]") + + # Ensure back at CLI prompt + self.ssh_ctl_chan.send_command('cli', expect_string=r">") + return self.dest_file in remote_out + + elif self.direction == 'get': + return os.path.exists(self.dest_file) + + def remote_file_size(self, remote_cmd="", remote_file=None): + """Get the file size of the remote file.""" + if remote_file is None: + if self.direction == 'put': + remote_file = self.dest_file + elif self.direction == 'get': + remote_file = self.source_file + if not remote_cmd: + remote_cmd = "ls -l {}".format(remote_file) + # Ensure at BSD prompt + self.ssh_ctl_chan.send_command('start shell sh', expect_string=r"[\$#]") + remote_out = self.ssh_ctl_chan.send_command(remote_cmd, expect_string=r"[\$#]") + escape_file_name = re.escape(remote_file) + pattern = r".*({}).*".format(escape_file_name) + match = re.search(pattern, remote_out) + if match: + # Format: -rw-r--r-- 1 pyclass wheel 12 Nov 5 19:07 /var/tmp/test3.txt + line = match.group(0) + file_size = line.split()[4] + + # Ensure back at CLI prompt + self.ssh_ctl_chan.send_command('cli', expect_string=r">") + return int(file_size) + + @staticmethod + def process_md5(md5_output, pattern=r"= (.*)"): + """ + Process the string to retrieve the MD5 hash + + Output from Cisco IOS (ASA is similar) + .MD5 of flash:file_name Done! + verify /md5 (flash:file_name) = 410db2a7015eaa42b1fe71f1bf3d59a2 + """ + raise NotImplementedError + + def compare_md5(self): + """Compare md5 of file on network device to md5 of local file""" + raise NotImplementedError + + def remote_md5(self, base_cmd='verify /md5', remote_file=None): + raise NotImplementedError + + def put_file(self): + """SCP copy the file from the local system to the remote device.""" + destination = "{}/{}".format(self.file_system, self.dest_file) + self.scp_conn.scp_transfer_file(self.source_file, destination) + # Must close the SCP connection to get the file written (flush) + self.scp_conn.close() + + def verify_file(self): + """Verify the file has been transferred correctly.""" + raise NotImplementedError + + def enable_scp(self, cmd=None): + raise NotImplementedError + + def disable_scp(self, cmd=None): + raise NotImplementedError diff --git a/netmiko/linux/linux_ssh.py b/netmiko/linux/linux_ssh.py index 124463cf3..4f51110c0 100644 --- a/netmiko/linux/linux_ssh.py +++ b/netmiko/linux/linux_ssh.py @@ -10,6 +10,11 @@ class LinuxSSH(CiscoSSHConnection): + def session_preparation(self): + """Prepare the session after the connection has been established.""" + self.ansi_escape_codes = True + return super(LinuxSSH, self).session_preparation() + def disable_paging(self, *args, **kwargs): """Linux doesn't have paging by default.""" return "" @@ -17,7 +22,7 @@ def disable_paging(self, *args, **kwargs): def set_base_prompt(self, pri_prompt_terminator='$', alt_prompt_terminator='#', delay_factor=1): """Determine base prompt.""" - return super(CiscoSSHConnection, self).set_base_prompt( + return super(LinuxSSH, self).set_base_prompt( pri_prompt_terminator=pri_prompt_terminator, alt_prompt_terminator=alt_prompt_terminator, delay_factor=delay_factor) @@ -26,9 +31,9 @@ def send_config_set(self, config_commands=None, exit_config_mode=True, **kwargs) """Can't exit from root (if root)""" if self.username == "root": exit_config_mode = False - return super(CiscoSSHConnection, self).send_config_set(config_commands=config_commands, - exit_config_mode=exit_config_mode, - **kwargs) + return super(LinuxSSH, self).send_config_set(config_commands=config_commands, + exit_config_mode=exit_config_mode, + **kwargs) def check_config_mode(self, check_string='#'): """Verify root""" @@ -43,7 +48,7 @@ def exit_config_mode(self, exit_config='exit'): def check_enable_mode(self, check_string='#'): """Verify root""" - return super(CiscoSSHConnection, self).check_enable_mode(check_string=check_string) + return super(LinuxSSH, self).check_enable_mode(check_string=check_string) def exit_enable_mode(self, exit_command='exit'): """Exit enable mode.""" @@ -76,3 +81,11 @@ def enable(self, cmd='sudo su', pattern='ssword', re_flags=re.IGNORECASE): "the 'secret' argument to ConnectHandler." raise ValueError(msg) return output + + def cleanup(self): + """Try to Gracefully exit the SSH session.""" + self.write_channel("exit" + self.RETURN) + + def save_config(self, cmd='', confirm=True, confirm_response=''): + """Not Implemented""" + raise NotImplementedError diff --git a/netmiko/mellanox/mellanox_ssh.py b/netmiko/mellanox/mellanox_ssh.py index a817b2f45..3f9aa5f59 100644 --- a/netmiko/mellanox/mellanox_ssh.py +++ b/netmiko/mellanox/mellanox_ssh.py @@ -47,3 +47,12 @@ def exit_config_mode(self, exit_config='exit', pattern='#'): raise ValueError("Failed to exit configuration mode") log.debug("exit_config_mode: {0}".format(output)) return output + + def save_config(self, cmd='configuration write', confirm=False, + confirm_response=''): + """Save Config on Mellanox devices Enters and Leaves Config Mode""" + output = self.enable() + output += self.config_mode() + output += self.send_command(cmd) + output += self.exit_config_mode() + return output diff --git a/netmiko/mrv/mrv_ssh.py b/netmiko/mrv/mrv_ssh.py index da7290f92..dddbe7412 100644 --- a/netmiko/mrv/mrv_ssh.py +++ b/netmiko/mrv/mrv_ssh.py @@ -1,5 +1,6 @@ """MRV Communications Driver (OptiSwitch).""" from __future__ import unicode_literals +import time import re from netmiko.cisco_base_connection import CiscoSSHConnection @@ -13,6 +14,9 @@ def session_preparation(self): self.enable() self.set_base_prompt() self.disable_paging(command="no cli-paging") + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def enable(self, cmd='enable', pattern=r'#', re_flags=re.IGNORECASE): """Enable mode on MRV uses no password.""" @@ -25,3 +29,7 @@ def enable(self, cmd='enable', pattern=r'#', re_flags=re.IGNORECASE): "the 'secret' argument to ConnectHandler." raise ValueError(msg) return output + + def save_config(self, cmd='save config flash', confirm=False): + """Saves configuration.""" + return super(MrvOptiswitchSSH, self).save_config(cmd=cmd, confirm=confirm) diff --git a/netmiko/netapp/__init__.py b/netmiko/netapp/__init__.py new file mode 100644 index 000000000..ea0eeb2a7 --- /dev/null +++ b/netmiko/netapp/__init__.py @@ -0,0 +1,4 @@ +from __future__ import unicode_literals +from netmiko.netapp.netapp_cdot_ssh import NetAppcDotSSH + +__all__ = ['NetAppcDotSSH'] diff --git a/netmiko/netapp/netapp_cdot_ssh.py b/netmiko/netapp/netapp_cdot_ssh.py new file mode 100644 index 000000000..6415ff1c1 --- /dev/null +++ b/netmiko/netapp/netapp_cdot_ssh.py @@ -0,0 +1,38 @@ +from __future__ import unicode_literals + +from netmiko.base_connection import BaseConnection + + +class NetAppcDotSSH(BaseConnection): + + def session_preparation(self): + """Prepare the session after the connection has been established.""" + self.set_base_prompt() + cmd = self.RETURN + "rows 0" + self.RETURN + self.disable_paging(command=cmd) + + def send_command_with_y(self, *args, **kwargs): + output = self.send_command_timing(*args, **kwargs) + if '{y|n}' in output: + output += self.send_command_timing('y', strip_prompt=False, + strip_command=False) + return output + + def check_config_mode(self, check_string='*>'): + return super(NetAppcDotSSH, self).check_config_mode(check_string=check_string) + + def config_mode(self, config_command='set -privilege diagnostic -confirmations off'): + return super(NetAppcDotSSH, self).config_mode(config_command=config_command) + + def exit_config_mode(self, exit_config='set -privilege admin -confirmations off'): + return super(NetAppcDotSSH, self).exit_config_mode(exit_config=exit_config) + + def enable(self, *args, **kwargs): + """No enable mode on NetApp.""" + pass + + def check_enable_mode(self, *args, **kwargs): + pass + + def exit_enable_mode(self, *args, **kwargs): + pass diff --git a/netmiko/paloalto/paloalto_panos_ssh.py b/netmiko/paloalto/paloalto_panos_ssh.py index 4768926e1..8dc0b07e2 100644 --- a/netmiko/paloalto/paloalto_panos_ssh.py +++ b/netmiko/paloalto/paloalto_panos_ssh.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals +import time import re - from netmiko.base_connection import BaseConnection @@ -20,7 +20,10 @@ def session_preparation(self): """ self._test_channel_read() self.set_base_prompt(delay_factor=20) - self.disable_paging(command="set cli pager off\n") + self.disable_paging(command="set cli pager off") + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def check_enable_mode(self, *args, **kwargs): """No enable mode on PaloAlto.""" @@ -100,27 +103,22 @@ def commit(self, force=False, partial=False, device_and_network=False, return output def strip_command(self, command_string, output): - """ - Strip command_string from output string - """ + """Strip command_string from output string.""" output_list = output.split(command_string) - return '\n'.join(output_list) + return self.RESPONSE_RETURN.join(output_list) def strip_prompt(self, a_string): - ''' - Strip the trailing router prompt from the output - ''' - response_list = a_string.split('\n') + """Strip the trailing router prompt from the output.""" + response_list = a_string.split(self.RESPONSE_RETURN) new_response_list = [] for line in response_list: if self.base_prompt not in line: new_response_list.append(line) - output = '\n'.join(new_response_list) + output = self.RESPONSE_RETURN.join(new_response_list) return self.strip_context_items(output) - @staticmethod - def strip_context_items(a_string): + def strip_context_items(self, a_string): """Strip PaloAlto-specific output. PaloAlto will also put a configuration context: @@ -132,12 +130,12 @@ def strip_context_items(a_string): r'\[edit.*\]', ] - response_list = a_string.split('\n') + response_list = a_string.split(self.RESPONSE_RETURN) last_line = response_list[-1] for pattern in strings_to_strip: if re.search(pattern, last_line): - return "\n".join(response_list[:-1]) + return self.RESPONSE_RETURN.join(response_list[:-1]) return a_string diff --git a/netmiko/pluribus/pluribus_ssh.py b/netmiko/pluribus/pluribus_ssh.py index fdf0ee35c..fed5fdce5 100644 --- a/netmiko/pluribus/pluribus_ssh.py +++ b/netmiko/pluribus/pluribus_ssh.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +import time from netmiko.base_connection import BaseConnection @@ -18,6 +19,9 @@ def session_preparation(self): self._test_channel_read() self.set_base_prompt() self.disable_paging() + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def check_config_mode(self, *args, **kwargs): ''' diff --git a/netmiko/quanta/quanta_mesh_ssh.py b/netmiko/quanta/quanta_mesh_ssh.py index c1b64cae6..ddca0961e 100644 --- a/netmiko/quanta/quanta_mesh_ssh.py +++ b/netmiko/quanta/quanta_mesh_ssh.py @@ -10,3 +10,7 @@ def disable_paging(self, command="no pager", delay_factor=1): def config_mode(self, config_command='configure'): """Enter configuration mode.""" return super(QuantaMeshSSH, self).config_mode(config_command=config_command) + + def save_config(self, cmd='', confirm=True, confirm_response=''): + """Not Implemented""" + raise NotImplementedError diff --git a/netmiko/ruckus/__init__.py b/netmiko/ruckus/__init__.py new file mode 100644 index 000000000..e11e17cdf --- /dev/null +++ b/netmiko/ruckus/__init__.py @@ -0,0 +1,5 @@ +from __future__ import unicode_literals +from netmiko.ruckus.ruckus_fastiron import RuckusFastironSSH +from netmiko.ruckus.ruckus_fastiron import RuckusFastironTelnet + +__all__ = ['RuckusFastironSSH', 'RuckusFastironTelnet'] diff --git a/netmiko/brocade/brocade_fastiron_ssh.py b/netmiko/ruckus/ruckus_fastiron.py similarity index 70% rename from netmiko/brocade/brocade_fastiron_ssh.py rename to netmiko/ruckus/ruckus_fastiron.py index 1c5a54c68..79819c689 100644 --- a/netmiko/brocade/brocade_fastiron_ssh.py +++ b/netmiko/ruckus/ruckus_fastiron.py @@ -4,26 +4,21 @@ from netmiko.cisco_base_connection import CiscoSSHConnection -class BrocadeFastironSSH(CiscoSSHConnection): - """Brocade FastIron aka ICX support.""" +class RuckusFastironBase(CiscoSSHConnection): + """Ruckus FastIron aka ICX support.""" def session_preparation(self): """FastIron requires to be enable mode to disable paging.""" self._test_channel_read() self.set_base_prompt() self.enable() self.disable_paging(command="skip-page-display") - - @staticmethod - def normalize_linefeeds(a_string): - """Convert '\r\n\r\n', '\r\r\n','\r\n', '\n\r' to '\n.""" - newline = re.compile(r'(\r\n\r\n|\r\r\n|\r\n|\n\r|\r)') - return newline.sub('\n', a_string) + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def enable(self, cmd='enable', pattern=r'(ssword|User Name)', re_flags=re.IGNORECASE): """Enter enable mode. - With RADIUS can prompt for User Name - SSH@Lab-ICX7250>en User Name:service_netmiko Password: @@ -52,3 +47,18 @@ def enable(self, cmd='enable', pattern=r'(ssword|User Name)', re_flags=re.IGNORE msg = "Failed to enter enable mode. Please ensure you pass " \ "the 'secret' argument to ConnectHandler." raise ValueError(msg) + + def save_config(self, cmd='write mem', confirm=False): + """Saves configuration.""" + return super(RuckusFastironBase, self).save_config(cmd=cmd, confirm=confirm) + + +class RuckusFastironTelnet(RuckusFastironBase): + def __init__(self, *args, **kwargs): + default_enter = kwargs.get('default_enter') + kwargs['default_enter'] = '\r\n' if default_enter is None else default_enter + super(RuckusFastironTelnet, self).__init__(*args, **kwargs) + + +class RuckusFastironSSH(RuckusFastironBase): + pass diff --git a/netmiko/scp_handler.py b/netmiko/scp_handler.py index 6a2649812..5e6c2ca53 100644 --- a/netmiko/scp_handler.py +++ b/netmiko/scp_handler.py @@ -13,8 +13,6 @@ import re import os import hashlib -import time -import io import scp @@ -53,7 +51,7 @@ def close(self): self.scp_conn.close() -class FileTransfer(object): +class BaseFileTransfer(object): """Class to manage SCP file transfer and associated SSH control channel.""" def __init__(self, ssh_conn, source_file, dest_file, file_system=None, direction='put'): self.ssh_ctl_chan = ssh_conn @@ -83,8 +81,6 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, traceback): """Context manager cleanup.""" self.close_scp_chan() - if exc_type is not None: - raise exc_type(exc_value) def establish_scp_conn(self): """Establish SCP connection.""" @@ -95,9 +91,9 @@ def close_scp_chan(self): self.scp_conn.close() self.scp_conn = None - def remote_space_available(self, search_pattern=r"bytes total \((.*) bytes free\)"): + def remote_space_available(self, search_pattern=r"(\d+) bytes free"): """Return space available on remote device.""" - remote_cmd = "dir {0}".format(self.file_system) + remote_cmd = "dir {}".format(self.file_system) remote_output = self.ssh_ctl_chan.send_command_expect(remote_cmd) match = re.search(search_pattern, remote_output) return int(match.group(1)) @@ -107,7 +103,7 @@ def local_space_available(self): destination_stats = os.statvfs(".") return destination_stats.f_bsize * destination_stats.f_bavail - def verify_space_available(self, search_pattern=r"bytes total \((.*) bytes free\)"): + def verify_space_available(self, search_pattern=r"(\d+) bytes free"): """Verify sufficient space is available on destination file system (return boolean).""" if self.direction == 'put': space_avail = self.remote_space_available(search_pattern=search_pattern) @@ -136,16 +132,19 @@ def check_file_exists(self, remote_cmd=""): def remote_file_size(self, remote_cmd="", remote_file=None): """Get the file size of the remote file.""" if remote_file is None: - remote_file = self.dest_file + if self.direction == 'put': + remote_file = self.dest_file + elif self.direction == 'get': + remote_file = self.source_file if not remote_cmd: - remote_cmd = "dir {0}/{1}".format(self.file_system, remote_file) - remote_out = self.ssh_ctl_chan.send_command_expect(remote_cmd) + remote_cmd = "dir {}/{}".format(self.file_system, remote_file) + remote_out = self.ssh_ctl_chan.send_command(remote_cmd) # Strip out "Directory of flash:/filename line remote_out = re.split(r"Directory of .*", remote_out) remote_out = "".join(remote_out) # Match line containing file name escape_file_name = re.escape(remote_file) - pattern = r".*({0}).*".format(escape_file_name) + pattern = r".*({}).*".format(escape_file_name) match = re.search(pattern, remote_out) if match: line = match.group(0) @@ -178,25 +177,27 @@ def process_md5(md5_output, pattern=r"= (.*)"): else: raise ValueError("Invalid output from MD5 command: {0}".format(md5_output)) - def compare_md5(self, base_cmd='verify /md5'): - """Compare md5 of file on network device to md5 of local file""" + def compare_md5(self): + """Compare md5 of file on network device to md5 of local file.""" if self.direction == 'put': - remote_md5 = self.remote_md5(base_cmd=base_cmd) + remote_md5 = self.remote_md5() return self.source_md5 == remote_md5 elif self.direction == 'get': local_md5 = self.file_md5(self.dest_file) return self.source_md5 == local_md5 def remote_md5(self, base_cmd='verify /md5', remote_file=None): - """ - Calculate remote MD5 and return the checksum. + """Calculate remote MD5 and returns the hash. This command can be CPU intensive on the remote device. """ if remote_file is None: - remote_file = self.dest_file + if self.direction == 'put': + remote_file = self.dest_file + elif self.direction == 'get': + remote_file = self.source_file remote_md5_cmd = "{0} {1}{2}".format(base_cmd, self.file_system, remote_file) - dest_md5 = self.ssh_ctl_chan.send_command_expect(remote_md5_cmd, delay_factor=3.0) + dest_md5 = self.ssh_ctl_chan.send_command(remote_md5_cmd, delay_factor=3.0) dest_md5 = self.process_md5(dest_md5) return dest_md5 @@ -214,7 +215,7 @@ def get_file(self): def put_file(self): """SCP copy the file from the local system to the remote device.""" - destination = "{0}{1}".format(self.file_system, self.dest_file) + destination = "{}{}".format(self.file_system, self.dest_file) if ':' not in destination: raise ValueError("Invalid destination file system specified") self.scp_conn.scp_transfer_file(self.source_file, destination) @@ -248,160 +249,3 @@ def disable_scp(self, cmd=None): elif not hasattr(cmd, '__iter__'): cmd = [cmd] self.ssh_ctl_chan.send_config_set(cmd) - - -class InLineTransfer(FileTransfer): - """Use TCL on Cisco IOS to directly transfer file.""" - def __init__(self, ssh_conn, source_file=None, dest_file=None, file_system=None, - direction='put', source_config=None): - if source_file and source_config: - msg = "Invalid call to InLineTransfer both source_file and source_config specified." - raise ValueError(msg) - if direction != 'put': - raise ValueError("Only put operation supported by InLineTransfer.") - - self.ssh_ctl_chan = ssh_conn - if source_file: - self.source_file = source_file - self.source_config = None - self.source_md5 = self.file_md5(source_file) - self.file_size = os.stat(source_file).st_size - elif source_config: - self.source_file = None - self.source_config = source_config - self.source_md5 = self.config_md5(source_config) - self.file_size = len(source_config.encode('UTF-8')) - self.dest_file = dest_file - self.direction = direction - - if not file_system: - self.file_system = self.ssh_ctl_chan._autodetect_fs() - else: - self.file_system = file_system - - @staticmethod - def _read_file(file_name): - with io.open(file_name, "rt", encoding='utf-8') as f: - return f.read() - - @staticmethod - def _tcl_newline_rationalize(tcl_string): - """ - When using put inside a TCL {} section the newline is considered a new TCL - statement and causes a missing curly-brace message. Convert "\n" to "\r". TCL - will convert the "\r" to a "\n" i.e. you will see a "\n" inside the file on the - Cisco IOS device. - """ - NEWLINE = r"\n" - CARRIAGE_RETURN = r"\r" - tmp_string = re.sub(NEWLINE, CARRIAGE_RETURN, tcl_string) - if re.search(r"[{}]", tmp_string): - msg = "Curly brace detected in string; TCL requires this be escaped." - raise ValueError(msg) - return tmp_string - - def __enter__(self): - self._enter_tcl_mode() - return self - - def __exit__(self, exc_type, exc_value, traceback): - _ = self._exit_tcl_mode() # noqa - if exc_type is not None: - raise exc_type(exc_value) - - def _enter_tcl_mode(self): - TCL_ENTER = 'tclsh' - cmd_failed = ['Translating "tclsh"', '% Unknown command', '% Bad IP address'] - output = self.ssh_ctl_chan.send_command(TCL_ENTER, expect_string=r"\(tcl\)#", - strip_prompt=False, strip_command=False) - for pattern in cmd_failed: - if pattern in output: - raise ValueError("Failed to enter tclsh mode on router: {}".format(output)) - return output - - def _exit_tcl_mode(self): - TCL_EXIT = 'tclquit' - self.ssh_ctl_chan.write_channel("\r") - time.sleep(1) - output = self.ssh_ctl_chan.read_channel() - if '(tcl)' in output: - self.ssh_ctl_chan.write_channel(TCL_EXIT + "\r") - time.sleep(1) - output += self.ssh_ctl_chan.read_channel() - return output - - def establish_scp_conn(self): - raise NotImplementedError - - def close_scp_chan(self): - raise NotImplementedError - - def local_space_available(self): - raise NotImplementedError - - def file_md5(self, file_name): - """Compute MD5 hash of file.""" - file_contents = self._read_file(file_name) - file_contents = file_contents + '\n' # Cisco IOS automatically adds this - file_contents = file_contents.encode('UTF-8') - return hashlib.md5(file_contents).hexdigest() - - def config_md5(self, source_config): - """Compute MD5 hash of file.""" - file_contents = source_config + '\n' # Cisco IOS automatically adds this - file_contents = file_contents.encode('UTF-8') - return hashlib.md5(file_contents).hexdigest() - - def put_file(self): - curlybrace = r'{' - TCL_FILECMD_ENTER = 'puts [open "{}{}" w+] {}'.format(self.file_system, - self.dest_file, curlybrace) - TCL_FILECMD_EXIT = '}' - - if self.source_file: - file_contents = self._read_file(self.source_file) - elif self.source_config: - file_contents = self.source_config - file_contents = self._tcl_newline_rationalize(file_contents) - - # Try to remove any existing data - self.ssh_ctl_chan.clear_buffer() - - self.ssh_ctl_chan.write_channel(TCL_FILECMD_ENTER) - time.sleep(.25) - self.ssh_ctl_chan.write_channel(file_contents) - self.ssh_ctl_chan.write_channel(TCL_FILECMD_EXIT + "\r") - - # This operation can be slow (depends on the size of the file) - max_loops = 400 - sleep_time = 4 - if self.file_size >= 2500: - max_loops = 1500 - sleep_time = 12 - elif self.file_size >= 7500: - max_loops = 3000 - sleep_time = 25 - - # Initial delay - time.sleep(sleep_time) - - # File paste and TCL_FILECMD_exit should be indicated by "router(tcl)#" - output = self.ssh_ctl_chan._read_channel_expect(pattern=r"\(tcl\)", max_loops=max_loops) - - # The file doesn't write until tclquit - TCL_EXIT = 'tclquit' - self.ssh_ctl_chan.write_channel(TCL_EXIT + "\r") - - time.sleep(1) - # Read all data remaining from the TCLSH session - output += self.ssh_ctl_chan._read_channel_expect(max_loops=max_loops) - return output - - def get_file(self): - raise NotImplementedError - - def enable_scp(self, cmd=None): - raise NotImplementedError - - def disable_scp(self, cmd=None): - raise NotImplementedError diff --git a/netmiko/ssh_autodetect.py b/netmiko/ssh_autodetect.py index 88c909342..89d0a4cf4 100644 --- a/netmiko/ssh_autodetect.py +++ b/netmiko/ssh_autodetect.py @@ -48,6 +48,7 @@ # 'dispatch' key is the SSHDetect method to call. dispatch key will be popped off dictionary # remaining keys indicate kwargs that will be passed to dispatch method. +# Note, the 'cmd' needs to avoid output paging. SSH_MAPPER_BASE = { 'alcatel_aos': { "cmd": "show system", @@ -56,8 +57,11 @@ "dispatch": "_autodetect_std", }, 'alcatel_sros': { - "cmd": "show version | match ALCATEL", - "search_patterns": ["TiMOS"], + "cmd": "show version | match TiMOS", + "search_patterns": [ + "Nokia", + "Alcatel", + ], "priority": 99, "dispatch": "_autodetect_std", }, @@ -96,13 +100,26 @@ }, 'huawei': { "cmd": "display version | inc Huawei", - "search_patterns": ["Huawei Technologies", "Huawei Versatile Routing Platform Software"], + "search_patterns": [ + "Huawei Technologies", + "Huawei Versatile Routing Platform Software" + ], "priority": 99, "dispatch": "_autodetect_std", }, 'juniper_junos': { "cmd": "show version | match JUNOS", - "search_patterns": ["JUNOS Software Release", "JUNOS .+ Software"], + "search_patterns": [ + "JUNOS Software Release", + "JUNOS .+ Software", + "JUNOS OS Kernel", + ], + "priority": 99, + "dispatch": "_autodetect_std", + }, + 'dell_force10': { + "cmd": "show version | grep Type", + "search_patterns": ["S4048-ON"], "priority": 99, "dispatch": "_autodetect_std", }, diff --git a/netmiko/ssh_dispatcher.py b/netmiko/ssh_dispatcher.py index fadbcccfc..de3b5b58e 100644 --- a/netmiko/ssh_dispatcher.py +++ b/netmiko/ssh_dispatcher.py @@ -6,21 +6,24 @@ from netmiko.alcatel import AlcatelAosSSH from netmiko.alcatel import AlcatelSrosSSH from netmiko.arista import AristaSSH +# from netmiko.arista import AristaFileTransfer from netmiko.aruba import ArubaSSH from netmiko.avaya import AvayaErsSSH from netmiko.avaya import AvayaVspSSH -from netmiko.brocade import BrocadeFastironSSH from netmiko.brocade import BrocadeNetironSSH +from netmiko.brocade import BrocadeNetironTelnet from netmiko.brocade import BrocadeNosSSH +from netmiko.calix import CalixB6SSH from netmiko.checkpoint import CheckPointGaiaSSH from netmiko.ciena import CienaSaosSSH -from netmiko.cisco import CiscoAsaSSH -from netmiko.cisco import CiscoIosBase -from netmiko.cisco import CiscoNxosSSH +from netmiko.cisco import CiscoAsaSSH, CiscoAsaFileTransfer +from netmiko.cisco import CiscoIosSSH, CiscoIosFileTransfer, CiscoIosTelnet, CiscoIosSerial +from netmiko.cisco import CiscoNxosSSH, CiscoNxosFileTransfer from netmiko.cisco import CiscoS300SSH from netmiko.cisco import CiscoTpTcCeSSH from netmiko.cisco import CiscoWlcSSH from netmiko.cisco import CiscoXrSSH +from netmiko.coriant import CoriantSSH from netmiko.dell import DellForce10SSH from netmiko.dell import DellPowerConnectSSH from netmiko.dell import DellPowerConnectTelnet @@ -28,18 +31,23 @@ from netmiko.enterasys import EnterasysSSH from netmiko.extreme import ExtremeSSH from netmiko.extreme import ExtremeWingSSH +from netmiko.extreme import ExtremeTelnet from netmiko.f5 import F5LtmSSH from netmiko.fortinet import FortinetSSH from netmiko.hp import HPProcurveSSH, HPComwareSSH -from netmiko.huawei import HuaweiSSH +from netmiko.huawei import HuaweiSSH, HuaweiVrpv8SSH from netmiko.juniper import JuniperSSH +# from netmiko.juniper import JuniperFileTransfer from netmiko.linux import LinuxSSH from netmiko.mellanox import MellanoxSSH from netmiko.mrv import MrvOptiswitchSSH +from netmiko.netapp import NetAppcDotSSH from netmiko.ovs import OvsLinuxSSH from netmiko.paloalto import PaloAltoPanosSSH from netmiko.pluribus import PluribusSSH from netmiko.quanta import QuantaMeshSSH +from netmiko.ruckus import RuckusFastironSSH +from netmiko.ruckus import RuckusFastironTelnet from netmiko.terminal_server import TerminalServerSSH from netmiko.terminal_server import TerminalServerTelnet from netmiko.ubiquiti import UbiquitiEdgeSSH @@ -56,21 +64,23 @@ 'aruba_os': ArubaSSH, 'avaya_ers': AvayaErsSSH, 'avaya_vsp': AvayaVspSSH, - 'brocade_fastiron': BrocadeFastironSSH, + 'brocade_fastiron': RuckusFastironSSH, 'brocade_netiron': BrocadeNetironSSH, 'brocade_nos': BrocadeNosSSH, 'brocade_vdx': BrocadeNosSSH, 'brocade_vyos': VyOSSSH, 'checkpoint_gaia': CheckPointGaiaSSH, + 'calix_b6': CalixB6SSH, 'ciena_saos': CienaSaosSSH, 'cisco_asa': CiscoAsaSSH, - 'cisco_ios': CiscoIosBase, + 'cisco_ios': CiscoIosSSH, 'cisco_nxos': CiscoNxosSSH, 'cisco_s300': CiscoS300SSH, 'cisco_tp': CiscoTpTcCeSSH, 'cisco_wlc': CiscoWlcSSH, - 'cisco_xe': CiscoIosBase, + 'cisco_xe': CiscoIosSSH, 'cisco_xr': CiscoXrSSH, + 'coriant': CoriantSSH, 'dell_force10': DellForce10SSH, 'dell_powerconnect': DellPowerConnectSSH, 'eltex': EltexSSH, @@ -83,20 +93,33 @@ 'hp_comware': HPComwareSSH, 'hp_procurve': HPProcurveSSH, 'huawei': HuaweiSSH, + 'huawei_vrpv8': HuaweiVrpv8SSH, 'juniper': JuniperSSH, 'juniper_junos': JuniperSSH, 'linux': LinuxSSH, - 'mellanox_ssh': MellanoxSSH, + 'mellanox': MellanoxSSH, 'mrv_optiswitch': MrvOptiswitchSSH, + 'netapp_cdot': NetAppcDotSSH, 'ovs_linux': OvsLinuxSSH, 'paloalto_panos': PaloAltoPanosSSH, 'pluribus': PluribusSSH, 'quanta_mesh': QuantaMeshSSH, + 'ruckus_fastiron': RuckusFastironSSH, 'ubiquiti_edge': UbiquitiEdgeSSH, + 'ubiquiti_edgeswitch': UbiquitiEdgeSSH, 'vyatta_vyos': VyOSSSH, 'vyos': VyOSSSH, } +FILE_TRANSFER_MAP = { + # 'arista_eos': AristaFileTransfer, + 'cisco_asa': CiscoAsaFileTransfer, + 'cisco_ios': CiscoIosFileTransfer, + 'cisco_xe': CiscoIosFileTransfer, + 'cisco_nxos': CiscoNxosFileTransfer, + # 'juniper_junos': JuniperFileTransfer, +} + # Also support keys that end in _ssh new_mapper = {} for k, v in CLASS_MAPPER_BASE.items(): @@ -105,10 +128,24 @@ new_mapper[alt_key] = v CLASS_MAPPER = new_mapper +new_mapper = {} +for k, v in FILE_TRANSFER_MAP.items(): + new_mapper[k] = v + alt_key = k + u"_ssh" + new_mapper[alt_key] = v +FILE_TRANSFER_MAP = new_mapper + # Add telnet drivers -CLASS_MAPPER['cisco_ios_telnet'] = CiscoIosBase +CLASS_MAPPER['brocade_fastiron_telnet'] = RuckusFastironTelnet +CLASS_MAPPER['brocade_netiron_telnet'] = BrocadeNetironTelnet +CLASS_MAPPER['cisco_ios_telnet'] = CiscoIosTelnet CLASS_MAPPER['dell_powerconnect_telnet'] = DellPowerConnectTelnet CLASS_MAPPER['generic_termserver_telnet'] = TerminalServerTelnet +CLASS_MAPPER['extreme_telnet'] = ExtremeTelnet +CLASS_MAPPER['ruckus_fastiron_telnet'] = RuckusFastironTelnet + +# Add serial drivers +CLASS_MAPPER['cisco_ios_serial'] = CiscoIosSerial # Add general terminal_server driver and autodetect CLASS_MAPPER['terminal_server'] = TerminalServerSSH @@ -118,15 +155,20 @@ platforms.sort() platforms_base = list(CLASS_MAPPER_BASE.keys()) platforms_base.sort() -platforms_str = u"\n".join(platforms_base) -platforms_str = u"\n" + platforms_str +platforms_str = "\n".join(platforms_base) +platforms_str = "\n" + platforms_str + +scp_platforms = list(FILE_TRANSFER_MAP.keys()) +scp_platforms.sort() +scp_platforms_str = "\n".join(scp_platforms) +scp_platforms_str = "\n" + scp_platforms_str def ConnectHandler(*args, **kwargs): """Factory function selects the proper class and creates object based on device_type.""" if kwargs['device_type'] not in platforms: raise ValueError('Unsupported device_type: ' - 'currently supported platforms are: {0}'.format(platforms_str)) + 'currently supported platforms are: {}'.format(platforms_str)) ConnectionClass = ssh_dispatcher(kwargs['device_type']) return ConnectionClass(*args, **kwargs) @@ -147,3 +189,16 @@ def redispatch(obj, device_type, session_prep=True): obj.__class__ = new_class if session_prep: obj.session_preparation() + + +def FileTransfer(*args, **kwargs): + """Factory function selects the proper SCP class and creates object based on device_type.""" + if len(args) >= 1: + device_type = args[0].device_type + else: + device_type = kwargs['ssh_conn'].device_type + if device_type not in scp_platforms: + raise ValueError('Unsupported SCP device_type: ' + 'currently supported platforms are: {}'.format(scp_platforms_str)) + FileTransferClass = FILE_TRANSFER_MAP[device_type] + return FileTransferClass(*args, **kwargs) diff --git a/netmiko/terminal_server/terminal_server.py b/netmiko/terminal_server/terminal_server.py index 8f4e02233..63e897639 100644 --- a/netmiko/terminal_server/terminal_server.py +++ b/netmiko/terminal_server/terminal_server.py @@ -21,4 +21,9 @@ class TerminalServerSSH(TerminalServer): class TerminalServerTelnet(TerminalServer): """Generic Terminal Server driver telnet.""" - pass + def telnet_login(self, *args, **kwargs): + # Disable automatic handling of username and password when using terminal server driver + pass + + def std_login(self, *args, **kwargs): + return super(TerminalServerTelnet, self).telnet_login(*args, **kwargs) diff --git a/netmiko/ubiquiti/edge_ssh.py b/netmiko/ubiquiti/edge_ssh.py index 5db3067f1..b4cf5631c 100644 --- a/netmiko/ubiquiti/edge_ssh.py +++ b/netmiko/ubiquiti/edge_ssh.py @@ -4,9 +4,11 @@ class UbiquitiEdgeSSH(CiscoSSHConnection): """ - Implements support for Ubiquity Edge devices. + Implements support for Ubiquity EdgeSwitch devices. Mostly conforms to Cisco IOS style syntax with a few minor changes. + + This is NOT for EdgeRouter devices. """ def check_config_mode(self, check_string=')#'): """Checks if the device is in configuration mode or not.""" @@ -23,3 +25,7 @@ def exit_config_mode(self, exit_config='exit'): def exit_enable_mode(self, exit_command='exit'): """Exit enable mode.""" return super(UbiquitiEdgeSSH, self).exit_enable_mode(exit_command=exit_command) + + def save_config(self, cmd='write memory', confirm=False): + """Saves configuration.""" + return super(UbiquitiEdgeSSH, self).save_config(cmd=cmd, confirm=confirm) diff --git a/netmiko/utilities.py b/netmiko/utilities.py index a38fdac2c..fd7d3896d 100644 --- a/netmiko/utilities.py +++ b/netmiko/utilities.py @@ -5,6 +5,10 @@ import sys import io import os +import serial.tools.list_ports +from netmiko._textfsm import _clitable as clitable +from netmiko._textfsm._clitable import CliTableError + # Dictionary mapping 'show run' for vendors with different command SHOW_RUN_MAPPER = { @@ -160,3 +164,67 @@ def write_bytes(out_data): return out_data msg = "Invalid value for out_data neither unicode nor byte string: {0}".format(out_data) raise ValueError(msg) + + +def check_serial_port(name): + """returns valid COM Port.""" + try: + cdc = next(serial.tools.list_ports.grep(name)) + return cdc.split()[0] + except StopIteration: + msg = "device {} not found. ".format(name) + msg += "available devices are: " + ports = list(serial.tools.list_ports.comports()) + for p in ports: + msg += "{},".format(str(p)) + raise ValueError(msg) + + +def get_template_dir(): + """Find and return the ntc-templates/templates dir.""" + try: + template_dir = os.environ['NET_TEXTFSM'] + index = os.path.join(template_dir, 'index') + if not os.path.isfile(index): + # Assume only base ./ntc-templates specified + template_dir = os.path.join(template_dir, 'templates') + except KeyError: + # Construct path ~/ntc-templates/templates + home_dir = os.path.expanduser("~") + template_dir = os.path.join(home_dir, 'ntc-templates', 'templates') + + index = os.path.join(template_dir, 'index') + if not os.path.isdir(template_dir) or not os.path.isfile(index): + msg = """ +Valid ntc-templates not found, please install https://github.com/networktocode/ntc-templates +and then set the NET_TEXTFSM environment variable to point to the ./ntc-templates/templates +directory.""" + raise ValueError(msg) + return template_dir + + +def clitable_to_dict(cli_table): + """Converts TextFSM cli_table object to list of dictionaries.""" + objs = [] + for row in cli_table: + temp_dict = {} + for index, element in enumerate(row): + temp_dict[cli_table.header[index].lower()] = element + objs.append(temp_dict) + return objs + + +def get_structured_data(raw_output, platform, command): + """Convert raw CLI output to structured data using TextFSM template.""" + template_dir = get_template_dir() + index_file = os.path.join(template_dir, 'index') + textfsm_obj = clitable.CliTable(index_file, template_dir) + attrs = {'Command': command, 'Platform': platform} + try: + # Parse output through template + textfsm_obj.ParseCmd(raw_output, attrs) + structured_data = clitable_to_dict(textfsm_obj) + output = raw_output if structured_data == [] else structured_data + return output + except CliTableError: + return raw_output diff --git a/netmiko/vyos/vyos_ssh.py b/netmiko/vyos/vyos_ssh.py index 6a6bd50cb..f44b6e265 100644 --- a/netmiko/vyos/vyos_ssh.py +++ b/netmiko/vyos/vyos_ssh.py @@ -1,5 +1,6 @@ from __future__ import print_function from __future__ import unicode_literals +import time from netmiko.cisco_base_connection import CiscoSSHConnection @@ -10,7 +11,10 @@ def session_preparation(self): """Prepare the session after the connection has been established.""" self._test_channel_read() self.set_base_prompt() - self.disable_paging(command="set terminal length 0\n") + self.disable_paging(command="set terminal length 0") + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def check_enable_mode(self, *args, **kwargs): """No enable mode on VyOS.""" @@ -83,10 +87,16 @@ def set_base_prompt(self, pri_prompt_terminator='$', alt_prompt_terminator='#', return self.base_prompt def send_config_set(self, config_commands=None, exit_config_mode=False, delay_factor=1, - max_loops=150, strip_prompt=False, strip_command=False): + max_loops=150, strip_prompt=False, strip_command=False, + config_mode_command=None): """Remain in configuration mode.""" return super(VyOSSSH, self).send_config_set(config_commands=config_commands, exit_config_mode=exit_config_mode, delay_factor=delay_factor, max_loops=max_loops, strip_prompt=strip_prompt, - strip_command=strip_command) + strip_command=strip_command, + config_mode_command=config_mode_command) + + def save_config(self, cmd='', confirm=True, confirm_response=''): + """Not Implemented""" + raise NotImplementedError diff --git a/requirements-dev.txt b/requirements-dev.txt index 004b249aa..c298a0b5e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,5 @@ -pytest>=2.6.0 +pytest>=3.2.5 pylama +tox pysnmp -r requirements.txt diff --git a/requirements.txt b/requirements.txt index c06b34a33..0a7f52f20 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ -paramiko>=1.13.0 +paramiko>=2.0.0 scp>=0.10.0 pyyaml +pyserial +textfsm diff --git a/setup.cfg b/setup.cfg index 285413570..ee8cc0028 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [pylama] linters = mccabe,pep8,pyflakes ignore = D203,C901 -skip = tests/*,examples/*,build/*,.tox/* +skip = tests/*,examples/*,build/*,.tox/*,netmiko/_textfsm/* [pylama:pep8] max_line_length = 100 diff --git a/setup.py b/setup.py index 9e2757d55..6330dd553 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ from setuptools import setup +from setuptools import find_packages import os import re @@ -35,41 +36,12 @@ def find_version(*file_paths): 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', ], - packages=['netmiko', - 'netmiko/a10', - 'netmiko/accedian', - 'netmiko/alcatel', - 'netmiko/arista', - 'netmiko/aruba', - 'netmiko/avaya', - 'netmiko/brocade', - 'netmiko/ciena', - 'netmiko/cisco', - 'netmiko/dell', - 'netmiko/eltex', - 'netmiko/enterasys', - 'netmiko/extreme', - 'netmiko/f5', - 'netmiko/fortinet', - 'netmiko/checkpoint', - 'netmiko/hp', - 'netmiko/huawei', - 'netmiko/juniper', - 'netmiko/linux', - 'netmiko/mellanox', - 'netmiko/mrv', - 'netmiko/ovs', - 'netmiko/paloalto', - 'netmiko/pluribus', - 'netmiko/quanta', - 'netmiko/terminal_server', - 'netmiko/ubiquiti', - 'netmiko/vyos'], - install_requires=['paramiko>=1.13.0', 'scp>=0.10.0', 'pyyaml'], + packages=find_packages(exclude=("test*", )), + install_requires=['paramiko>=2.0.0', 'scp>=0.10.0', 'pyyaml', 'pyserial', 'textfsm'], extras_require={ - 'test': ['pytest>=2.6.0', ] + 'test': ['pytest>=3.2.5', ] }, ) diff --git a/tests/etc/test_devices.yml.example b/tests/etc/test_devices.yml.example index f423a89a3..d396f0f11 100644 --- a/tests/etc/test_devices.yml.example +++ b/tests/etc/test_devices.yml.example @@ -77,6 +77,12 @@ ubiquiti_edge: username: admin password: password +ubiquiti_edgeswitch: + device_type: ubiquiti_edgeswitch + ip: 172.16.51.102 + username: admin + password: password + dell_powerconnect: device_type: dell_powerconnect ip: 192.168.1.254 diff --git a/tests/test_cisco_ios_serial.py b/tests/test_cisco_ios_serial.py new file mode 100644 index 000000000..f5c8930d6 --- /dev/null +++ b/tests/test_cisco_ios_serial.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +''' +This will run an command via serial on an cisco ios switch and so +serial cable must be attached to the device +''' + +from __future__ import print_function +from netmiko import ConnectHandler +import serial + +def main(): + ''' + This will run an command via serial on an cisco ios switch and so + serial cable must be attached to the device + ''' + serialhandle = { + 'device_type':'cisco_ios_serial', + 'port': 'USB Serial', # can be COM or any line you can get from + # serial.tools.list_ports.comports() + 'username':'', + 'password':'', + 'secret':'', + 'serial_settings':{ # this are the default values + 'baudrate': 9600, + 'bytesize': serial.EIGHTBITS, + 'parity': serial.PARITY_NONE, + 'stopbits': serial.STOPBITS_ONE + } + } + net_connect = ConnectHandler(**serialhandle) + net_connect.enable() + output = net_connect.send_command('show run') + net_connect.disconnect() + + print(output) + +if __name__ == "__main__": + main() diff --git a/tests/test_import_netmiko.py b/tests/test_import_netmiko.py new file mode 100644 index 000000000..074055326 --- /dev/null +++ b/tests/test_import_netmiko.py @@ -0,0 +1,4 @@ +from netmiko import ConnectHandler + +def test_placeholder(): + assert True diff --git a/tests/test_suite_alt.sh b/tests/test_suite_alt.sh index 5b4f81bd7..77a48fc92 100755 --- a/tests/test_suite_alt.sh +++ b/tests/test_suite_alt.sh @@ -4,9 +4,6 @@ RETURN_CODE=0 # Exit on the first test failure and set RETURN_CODE = 1 echo "Starting tests...good luck:" \ -&& echo "Linux SSH (using keys)" \ -&& py.test -s -v test_netmiko_show.py --test_device linux_srv1 \ -\ && echo "Cisco IOS SSH (including SCP) using key auth" \ && py.test -v test_netmiko_scp.py --test_device cisco881_key \ && py.test -v test_netmiko_tcl.py --test_device cisco881_key \ @@ -63,6 +60,9 @@ echo "Starting tests...good luck:" \ && py.test -v test_netmiko_show.py --test_device nxos1 \ && py.test -v test_netmiko_config.py --test_device nxos1 \ \ +&& echo "Linux SSH (using keys)" \ +&& py.test -s -v test_netmiko_show.py --test_device linux_srv1 \ +\ && echo "Autodetect tests" \ && py.test -s -v test_netmiko_autodetect.py --test_device cisco881 \ && py.test -s -v test_netmiko_autodetect.py --test_device arista_sw4 \ diff --git a/tests/test_suite_tmp.sh b/tests/test_suite_tmp.sh new file mode 100755 index 000000000..ac70110a5 --- /dev/null +++ b/tests/test_suite_tmp.sh @@ -0,0 +1,68 @@ +#!/bin/sh + +RETURN_CODE=0 + +# Exit on the first test failure and set RETURN_CODE = 1 +echo "Starting tests...good luck:" \ +&& echo "Cisco IOS SSH (including SCP) using key auth" \ +&& py.test -v test_netmiko_scp.py --test_device cisco881_key \ +&& py.test -v test_netmiko_tcl.py --test_device cisco881_key \ +&& py.test -v test_netmiko_show.py --test_device cisco881_key \ +&& py.test -v test_netmiko_config.py --test_device cisco881_key \ +\ +&& echo "Cisco IOS SSH (including SCP)" \ +&& py.test -v test_netmiko_scp.py --test_device cisco881 \ +&& py.test -v test_netmiko_tcl.py --test_device cisco881 \ +&& py.test -v test_netmiko_show.py --test_device cisco881 \ +&& py.test -v test_netmiko_config.py --test_device cisco881 \ +\ +&& echo "Cisco IOS telnet" \ +&& py.test -v test_netmiko_show.py --test_device cisco881_telnet \ +&& py.test -v test_netmiko_config.py --test_device cisco881_telnet \ +\ +&& echo "Cisco SG300" \ +&& py.test -v test_netmiko_show.py --test_device cisco_s300 \ +&& py.test -v test_netmiko_config.py --test_device cisco_s300 \ +\ +&& echo "Arista" \ +&& py.test -v test_netmiko_show.py --test_device arista_sw4 \ +&& py.test -v test_netmiko_config.py --test_device arista_sw4 \ +\ +&& echo "HP ProCurve" \ +&& py.test -v test_netmiko_show.py --test_device hp_procurve \ +&& py.test -v test_netmiko_config.py --test_device hp_procurve \ +\ +&& echo "Juniper" \ +&& py.test -v test_netmiko_show.py --test_device juniper_srx \ +&& py.test -v test_netmiko_config.py --test_device juniper_srx \ +&& py.test -v test_netmiko_commit.py --test_device juniper_srx \ +\ +&& echo "Cisco ASA" \ +&& py.test -v test_netmiko_show.py --test_device cisco_asa \ +&& py.test -v test_netmiko_config.py --test_device cisco_asa \ +&& py.test -v test_netmiko_show.py --test_device cisco_asa_login \ +&& py.test -v test_netmiko_config.py --test_device cisco_asa_login \ +\ +&& echo "Cisco IOS-XR" \ +&& py.test -v test_netmiko_show.py --test_device cisco_xrv \ +&& py.test -v test_netmiko_config.py --test_device cisco_xrv \ +&& py.test -v test_netmiko_commit.py --test_device cisco_xrv \ +\ +&& echo "Cisco NXOS" \ +&& py.test -v test_netmiko_show.py --test_device nxos1 \ +&& py.test -v test_netmiko_config.py --test_device nxos1 \ +\ +&& echo "Autodetect tests" \ +&& py.test -s -v test_netmiko_autodetect.py --test_device cisco881 \ +&& py.test -s -v test_netmiko_autodetect.py --test_device arista_sw4 \ +&& py.test -s -v test_netmiko_autodetect.py --test_device juniper_srx \ +&& py.test -s -v test_netmiko_autodetect.py --test_device cisco_asa \ +&& py.test -s -v test_netmiko_autodetect.py --test_device cisco_xrv \ +\ +|| RETURN_CODE=1 + +exit $RETURN_CODE + +#&& echo "Linux SSH (using keys)" \ +#&& py.test -s -v test_netmiko_show.py --test_device linux_srv1 \ +#\ diff --git a/tox.ini b/tox.ini index 9062aec2f..c7d5c005b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,9 @@ [tox] -envlist = py27,py34,py35 +envlist = py27,py35,py36 [testenv] deps = -rrequirements-dev.txt commands= - pylama . + py.test -v -s tests/test_import_netmiko.py From 256c562ce789cbf62883017a7c9f11abc129df61 Mon Sep 17 00:00:00 2001 From: tonypnode Date: Sun, 18 Feb 2018 15:35:01 -0500 Subject: [PATCH 08/25] Fix PR conflicts for docstring updates Signed-off-by: tonypnode --- netmiko/base_connection.py | 236 ++++++++++++++++++++++++++++++++----- 1 file changed, 209 insertions(+), 27 deletions(-) diff --git a/netmiko/base_connection.py b/netmiko/base_connection.py index 33c62b15b..033146ebc 100644 --- a/netmiko/base_connection.py +++ b/netmiko/base_connection.py @@ -45,54 +45,74 @@ def __init__(self, ip='', host='', username='', password='', secret='', port=Non :param ip: IP address of target device. Not required if `host` is provided. :type ip: str + :param host: Hostname of target device. Not required if `ip` is provided. :type host: str + :param username: Username to authenticate against target device if required. :type username: str + :param password: Password to authenticate against target device if required. :type password: str + :param secret: The enable password if target device requires one. :type secret: str + :param port: The destination port used to connect to the target device. :type port: int or None + :param device_type: Class selection based on device type. :type device_type: str + :param verbose: Enable additional messages to standard output. :type verbose: bool + :param global_delay_factor: Multiplication factor affecting Netmiko delays (default: 1). :type global_delay_factor: int + :param use_keys: Connect to target device using SSH keys. :type use_keys: bool + :param key_file: Filename path of the SSH key file to use. :type key_file: str + :param allow_agent: Enable use of SSH key-agent. :type allow_agent: bool + :param ssh_strict: Automatically reject unknown SSH host keys (default: False, which means unknown SSH host keys will be accepted). :type ssh_strict: bool + :param system_host_keys: Load host keys from the user's 'known_hosts' file. :type system_host_keys: bool :param alt_host_keys: If `True` host keys will be loaded from the file specified in 'alt_key_file'. :type alt_host_keys: bool + :param alt_key_file: SSH host key file to use (if alt_host_keys=True). :type alt_key_file: str + :param ssh_config_file: File name of OpenSSH configuration file. :type ssh_config_file: str + :param timeout: Connection timeout. :type timeout: float + :param session_timeout: Set a timeout for parallel requests. :type session_timeout: float + :param keepalive: Send SSH keepalive packets at a specific interval, in seconds. Currently defaults to 0, for backwards compatibility (it will not attempt to keep the connection alive). :type keepalive: int + :param default_enter: Character(s) to send to correspond to enter key (default: '\n'). :type default_enter: str + :param response_return: Character(s) to use in normalized return data to represent enter key (default: '\n') :type response_return: str @@ -351,12 +371,11 @@ def _read_channel_expect(self, pattern='', re_flags=0, max_loops=150): :param re_flags: regex flags used in conjunction with pattern to search for prompt \ (defaults to no flags) - :type re_flags: re module flags + :type re_flags: int :param max_loops: max number of iterations to read the channel before raising exception. Will default to be based upon self.timeout. :type max_loops: int - """ output = '' if not pattern: @@ -395,8 +414,7 @@ def _read_channel_expect(self, pattern='', re_flags=0, max_loops=150): .format(pattern)) def _read_channel_timing(self, delay_factor=1, max_loops=150): - """ - Read data on the channel based on timing delays. + """Read data on the channel based on timing delays. Attempt to read channel max_loops number of times. If no data this will cause a 15 second delay. @@ -458,7 +476,7 @@ def read_until_prompt_or_pattern(self, pattern='', re_flags=0): :param re_flags: regex flags used in conjunction with pattern to search for prompt \ (defaults to no flags) - :type re_flags: re module flags + :type re_flags: int """ combined_pattern = re.escape(self.base_prompt) @@ -475,7 +493,23 @@ def serial_login(self, pri_prompt_terminator=r'#\s*$', alt_prompt_terminator=r'> def telnet_login(self, pri_prompt_terminator=r'#\s*$', alt_prompt_terminator=r'>\s*$', username_pattern=r"(?:[Uu]ser:|sername|ogin)", pwd_pattern=r"assword", delay_factor=1, max_loops=20): - """Telnet login. Can be username/password or just password.""" + """Telnet login. Can be username/password or just password. + + :param pri_prompt_terminator: Primary trailing delimiter for identifying a device prompt + :type pri_prompt_terminator: str + + :param alt_prompt_terminator: Alternate trailing delimiter for identifying a device prompt + :type alt_prompt_terminator: str + + :param username_pattern: Pattern used to identify the username prompt + :type username_pattern: str + + :param delay_factor: See __init__: global_delay_factor + :type delay_factor: int + + :param max_loops: Controls the wait time in conjunction with the delay_factor + (default: 60) + """ delay_factor = self.select_delay_factor(delay_factor) time.sleep(1 * delay_factor) @@ -627,13 +661,18 @@ def _sanitize_output(self, output, strip_command=False, command_string=None, return output def establish_connection(self, width=None, height=None): - """ - Establish SSH connection to the network device + """Establish SSH connection to the network device Timeout will generate a NetMikoTimeoutException Authentication failure will generate a NetMikoAuthenticationException width and height are needed for Fortinet paging setting. + + :param width: Specified width of the VT100 terminal window + :type width: int + + :param height: Specified height of the VT100 terminal window + :type height: int """ if self.protocol == 'telnet': self.remote_conn = telnetlib.Telnet(self.host, port=self.port, timeout=self.timeout) @@ -677,9 +716,26 @@ def establish_connection(self, width=None, height=None): return "" def _test_channel_read(self, count=40, pattern=""): - """Try to read the channel (generally post login) verify you receive data back.""" + """Try to read the channel (generally post login) verify you receive data back. + + :param count: the number of times to check the channel for data + :type count: int + + :param pattern: Signifying the device prompt has returned and to break out of the loop + :type pattern: str + """ def _increment_delay(main_delay, increment=1.1, maximum=8): - """Increment sleep time to a maximum value.""" + """Increment sleep time to a maximum value. + + :param main_delay: Pri sleep factor for data to return from the channel + :type int + + :param increment: Sec sleep factor for waiting for data to return from the channel + :type increment: float + + :param maximum: Max delay to sleep when waiting for data to return from the channel + :type maximum: int + """ main_delay = main_delay * increment if main_delay >= maximum: main_delay = maximum @@ -725,7 +781,11 @@ def _build_ssh_client(self): return remote_conn_pre def select_delay_factor(self, delay_factor): - """Choose the greater of delay_factor or self.global_delay_factor.""" + """Choose the greater of delay_factor or self.global_delay_factor. + + :param delay_factor: See __init__: global_delay_factor + :type delay_factor: int + """ if delay_factor >= self.global_delay_factor: return delay_factor else: @@ -736,7 +796,14 @@ def special_login_handler(self, delay_factor=1): pass def disable_paging(self, command="terminal length 0", delay_factor=1): - """Disable paging default to a Cisco CLI method.""" + """Disable paging default to a Cisco CLI method. + + :param command: Device command to disable pagination of output + :type command: str + + :param delay_factor: See __init__: global_delay_factor + :type delay_factor: int + """ delay_factor = self.select_delay_factor(delay_factor) time.sleep(delay_factor * .1) self.clear_buffer() @@ -752,11 +819,18 @@ def disable_paging(self, command="terminal length 0", delay_factor=1): return output def set_terminal_width(self, command="", delay_factor=1): - """ - CLI terminals try to automatically adjust the line based on the width of the terminal. + """CLI terminals try to automatically adjust the line based on the width of the terminal. This causes the output to get distorted when accessed programmatically. Set terminal width to 511 which works on a broad set of devices. + + :param command: Command string to send to the device + :type command: str + + :param delay_factor: See __init__: global_delay_factor + :type delay_factor: int + + TODO: delay_factor doesn't seem to be used in this method """ if not command: return "" @@ -770,8 +844,7 @@ def set_terminal_width(self, command="", delay_factor=1): def set_base_prompt(self, pri_prompt_terminator='#', alt_prompt_terminator='>', delay_factor=1): - """ - Sets self.base_prompt + """Sets self.base_prompt Used as delimiter for stripping of trailing prompt in output. @@ -780,6 +853,15 @@ def set_base_prompt(self, pri_prompt_terminator='#', This will be set on entering user exec or privileged exec on Cisco, but not when entering/exiting config mode. + + :param pri_prompt_terminator: Primary trailing delimiter for identifying a device prompt + :type pri_prompt_terminator: str + + :param alt_prompt_terminator: Alternate trailing delimiter for identifying a device prompt + :type alt_prompt_terminator: str + + :param delay_factor: See __init__: global_delay_factor + :type delay_factor: int """ prompt = self.find_prompt(delay_factor=delay_factor) if not prompt[-1] in (pri_prompt_terminator, alt_prompt_terminator): @@ -789,7 +871,11 @@ def set_base_prompt(self, pri_prompt_terminator='#', return self.base_prompt def find_prompt(self, delay_factor=1): - """Finds the current network device prompt, last line only.""" + """Finds the current network device prompt, last line only. + + :param delay_factor: See __init__: global_delay_factor + :type delay_factor: int + """ delay_factor = self.select_delay_factor(delay_factor) self.clear_buffer() self.write_channel(self.RETURN) @@ -835,17 +921,23 @@ def send_command_timing(self, command_string, delay_factor=1, max_loops=150, :param command_string: The command to be executed on the remote device. :type command_string: str + :param delay_factor: Multiplying factor used to adjust delays (default: 1). :type delay_factor: int or float + :param max_loops: Controls wait time in conjunction with delay_factor. Will default to be based upon self.timeout. :type max_loops: int + :param strip_prompt: Remove the trailing router prompt from the output (default: True). :type strip_prompt: bool + :param strip_command: Remove the echo of the command from the output (default: True). :type strip_command: bool + :param normalize: Ensure the proper enter is sent at end of command (default: True). :type normalize: bool + :param use_textfsm: Process command output through TextFSM template (default: False). :type normalize: bool """ @@ -865,7 +957,11 @@ def send_command_timing(self, command_string, delay_factor=1, max_loops=150, return output def strip_prompt(self, a_string): - """Strip the trailing router prompt from the output.""" + """Strip the trailing router prompt from the output. + + :param a_string: Returned string from device + :type a_string: str + """ response_list = a_string.split(self.RESPONSE_RETURN) last_line = response_list[-1] if self.base_prompt in last_line: @@ -980,7 +1076,7 @@ def send_command_expect(self, *args, **kwargs): :type args: list :param kwargs: Keyword arguments to send to send_command() - :type kwargs: Dict + :type kwargs: dict """ return self.send_command(*args, **kwargs) @@ -999,6 +1095,12 @@ def strip_command(self, command_string, output): Strip command_string from output string Cisco IOS adds backspaces into output for long commands (i.e. for commands that line wrap) + + :param command_string: The command string sent to the device + :type command_string: str + + :param output: The returned output as a result of the command string sent to the device + :type output: str """ backspace_char = '\x08' @@ -1013,7 +1115,12 @@ def strip_command(self, command_string, output): return output[command_length:] def normalize_linefeeds(self, a_string): - """Convert `\r\r\n`,`\r\n`, `\n\r` to `\n.`""" + """Convert `\r\r\n`,`\r\n`, `\n\r` to `\n.` + + :param a_string: A string that may hove non-normalized line feeds + i.e. output returned from device, or a device prompt + :type a_string: str + """ newline = re.compile('(\r\r\r\n|\r\r\n|\r\n|\n\r)') a_string = newline.sub(self.RESPONSE_RETURN, a_string) if self.RESPONSE_RETURN == '\n': @@ -1021,19 +1128,37 @@ def normalize_linefeeds(self, a_string): return re.sub('\r', self.RESPONSE_RETURN, a_string) def normalize_cmd(self, command): - """Normalize CLI commands to have a single trailing newline.""" + """Normalize CLI commands to have a single trailing newline. + + :param command: Command that may require line feed to be normalized + :type command: str + """ command = command.rstrip() command += self.RETURN return command def check_enable_mode(self, check_string=''): - """Check if in enable mode. Return boolean.""" + """Check if in enable mode. Return boolean. + + :param check_string: Identification of privilege mode from device + :type check_string: str + """ self.write_channel(self.RETURN) output = self.read_until_prompt() return check_string in output def enable(self, cmd='', pattern='ssword', re_flags=re.IGNORECASE): - """Enter enable mode.""" + """Enter enable mode. + + :param cmd: Device command to enter enable mode + :type cmd: str + + :param pattern: patter to search for indicating device is waiting for password + :type pattern: str + + :param re_flags: Regular expression flags used in conjunction with pattern + :type re_flags: int + """ output = "" msg = "Failed to enter enable mode. Please ensure you pass " \ "the 'secret' argument to ConnectHandler." @@ -1050,7 +1175,11 @@ def enable(self, cmd='', pattern='ssword', re_flags=re.IGNORECASE): return output def exit_enable_mode(self, exit_command=''): - """Exit enable mode.""" + """Exit enable mode. + + :param exit_command: Command that exits the session from privileged mode + :type exit_command: str + """ output = "" if self.check_enable_mode(): self.write_channel(self.normalize_cmd(exit_command)) @@ -1060,13 +1189,27 @@ def exit_enable_mode(self, exit_command=''): return output def check_config_mode(self, check_string='', pattern=''): - """Checks if the device is in configuration mode or not.""" + """Checks if the device is in configuration mode or not. + + :param check_string: Identification of configuration mode from the device + :type check_string: str + + :param pattern: Pattern to identify the device prompt + :type pattern: str + """ self.write_channel(self.RETURN) output = self.read_until_pattern(pattern=pattern) return check_string in output def config_mode(self, config_command='', pattern=''): - """Enter into config_mode.""" + """Enter into config_mode. + + :param config_command: Configuration command to send to the device + :type config_command: str + + :param pattern: The pattern signifying the config command was completed + :type pattern: str + """ output = '' if not self.check_config_mode(): self.write_channel(self.normalize_cmd(config_command)) @@ -1076,7 +1219,14 @@ def config_mode(self, config_command='', pattern=''): return output def exit_config_mode(self, exit_config='', pattern=''): - """Exit from configuration mode.""" + """Exit from configuration mode. + + :param exit_config: Command to exit configuration mode + :type exit_config: str + + :param pattern: The pattern signifying the exit config mode command completed + :type pattern: str + """ output = '' if self.check_config_mode(): self.write_channel(self.normalize_cmd(exit_config)) @@ -1094,6 +1244,12 @@ def send_config_from_file(self, config_file=None, **kwargs): SSH channel. **kwargs are passed to send_config_set method. + + :param config_file: Path to configuration file to be sent to the device + :type config_file: str + + :param kwargs: params to be sent to send_config_set method + :type kwargs: dict """ with io.open(config_file, "rt", encoding='utf-8') as cfg_file: return self.send_config_set(cfg_file, **kwargs) @@ -1108,6 +1264,29 @@ def send_config_set(self, config_commands=None, exit_config_mode=True, delay_fac The commands will be executed one after the other. Automatically exits/enters configuration mode. + + :param config_commands: Multiple commands to be sent to the device + :type config_commands: list of strings + + :param exit_config_mode: Determines whether or not to exit config mode after complete + :type exit_config_mode: bool + + :param delay_factor: Factor to adjust delay when reading the channel + :type delay_factor: int + + :param max_loops: Controls wait time in conjunction with delay_factor (default: 150) + :type max_loops: int + + :param strip_prompt: Determines whether or not to strip the prompt + :type strip_prompt: bool + + :param strip_command: Determines whether or not to strip the command + :type strip_command: bool + + :param config_mode_command: The command to enter into config mode + :type config_mode_command: str + + TODO: strip_prompt and strip_command not used in the method """ delay_factor = self.select_delay_factor(delay_factor) if config_commands is None: @@ -1159,6 +1338,9 @@ def strip_ansi_escape_codes(self, string_buffer): ESC[6n Get cursor position HP ProCurve's, Cisco SG300, and F5 LTM's require this (possible others) + + :param string_buffer: The string that may require ansi escape chars to be removed + :type string_buffer: str """ log.debug("In strip_ansi_escape_codes") log.debug("repr = {0}".format(repr(string_buffer))) From bc437fa9240a4e889304526878d7f38dfe481819 Mon Sep 17 00:00:00 2001 From: tonypnode Date: Sun, 18 Feb 2018 15:43:01 -0500 Subject: [PATCH 09/25] Fix PR conflicts for docstring updates Signed-off-by: tonypnode --- netmiko/base_connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netmiko/base_connection.py b/netmiko/base_connection.py index 033146ebc..68fdc2219 100644 --- a/netmiko/base_connection.py +++ b/netmiko/base_connection.py @@ -728,7 +728,7 @@ def _increment_delay(main_delay, increment=1.1, maximum=8): """Increment sleep time to a maximum value. :param main_delay: Pri sleep factor for data to return from the channel - :type int + :type main_delay: int :param increment: Sec sleep factor for waiting for data to return from the channel :type increment: float From 8ab4d4e4c0905518d68fee3f13c79fa4c2067ccb Mon Sep 17 00:00:00 2001 From: Norihito Misaki Date: Wed, 28 Feb 2018 17:48:51 +0900 Subject: [PATCH 10/25] add arista_eos_telnet --- netmiko/arista/__init__.py | 4 ++-- netmiko/arista/arista_ssh.py | 10 +++++++++- netmiko/ssh_dispatcher.py | 3 ++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/netmiko/arista/__init__.py b/netmiko/arista/__init__.py index 7573444ff..07831d149 100644 --- a/netmiko/arista/__init__.py +++ b/netmiko/arista/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals -from netmiko.arista.arista_ssh import AristaSSH, AristaFileTransfer +from netmiko.arista.arista_ssh import AristaSSH, AristaTelnet, AristaFileTransfer -__all__ = ['AristaSSH', 'AristaFileTransfer'] +__all__ = ['AristaSSH', 'AristaTelnet', 'AristaFileTransfer'] diff --git a/netmiko/arista/arista_ssh.py b/netmiko/arista/arista_ssh.py index db9405d71..87f0409c3 100644 --- a/netmiko/arista/arista_ssh.py +++ b/netmiko/arista/arista_ssh.py @@ -5,7 +5,7 @@ from netmiko import log -class AristaSSH(CiscoSSHConnection): +class AristaBase(CiscoSSHConnection): def session_preparation(self): """Prepare the session after the connection has been established.""" self._test_channel_read(pattern=r'[>#]') @@ -42,6 +42,14 @@ def _return_cli(self): """Return to the CLI.""" return self.send_command('exit', expect_string=r"[#>]") +class AristaSSH(AristaBase): + pass + +class AristaTelnet(AristaBase): + def __init__(self, *args, **kwargs): + default_enter = kwargs.get('default_enter') + kwargs['default_enter'] = '\r\n' if default_enter is None else default_enter + super(AristaTelnet, self).__init__(*args, **kwargs) class AristaFileTransfer(CiscoFileTransfer): """Arista SCP File Transfer driver.""" diff --git a/netmiko/ssh_dispatcher.py b/netmiko/ssh_dispatcher.py index 2d2577449..8ffa58b69 100644 --- a/netmiko/ssh_dispatcher.py +++ b/netmiko/ssh_dispatcher.py @@ -5,7 +5,7 @@ from netmiko.accedian import AccedianSSH from netmiko.alcatel import AlcatelAosSSH from netmiko.alcatel import AlcatelSrosSSH -from netmiko.arista import AristaSSH +from netmiko.arista import AristaSSH, AristaTelnet from netmiko.arista import AristaFileTransfer from netmiko.aruba import ArubaSSH from netmiko.avaya import AvayaErsSSH @@ -139,6 +139,7 @@ CLASS_MAPPER['brocade_fastiron_telnet'] = RuckusFastironTelnet CLASS_MAPPER['brocade_netiron_telnet'] = BrocadeNetironTelnet CLASS_MAPPER['cisco_ios_telnet'] = CiscoIosTelnet +CLASS_MAPPER['arista_eos_telnet'] = AristaTelnet CLASS_MAPPER['dell_powerconnect_telnet'] = DellPowerConnectTelnet CLASS_MAPPER['generic_termserver_telnet'] = TerminalServerTelnet CLASS_MAPPER['extreme_telnet'] = ExtremeTelnet From bdf0d78760b2a2c57d10387c2380e4c631314bd4 Mon Sep 17 00:00:00 2001 From: Norihito Misaki Date: Wed, 28 Feb 2018 17:58:28 +0900 Subject: [PATCH 11/25] add juniper_junos_telnet --- netmiko/juniper/__init__.py | 4 ++-- netmiko/juniper/juniper_ssh.py | 16 ++++++++++++---- netmiko/ssh_dispatcher.py | 3 ++- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/netmiko/juniper/__init__.py b/netmiko/juniper/__init__.py index 22f8118c1..e4905f179 100644 --- a/netmiko/juniper/__init__.py +++ b/netmiko/juniper/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals -from netmiko.juniper.juniper_ssh import JuniperSSH, JuniperFileTransfer +from netmiko.juniper.juniper_ssh import JuniperSSH, JuniperTelnet, JuniperFileTransfer -__all__ = ['JuniperSSH', 'JuniperFileTransfer'] +__all__ = ['JuniperSSH', 'JuniperTelnet', 'JuniperFileTransfer'] diff --git a/netmiko/juniper/juniper_ssh.py b/netmiko/juniper/juniper_ssh.py index 52bedd8ec..a4ec9193f 100644 --- a/netmiko/juniper/juniper_ssh.py +++ b/netmiko/juniper/juniper_ssh.py @@ -7,7 +7,7 @@ from netmiko.scp_handler import BaseFileTransfer -class JuniperSSH(BaseConnection): +class JuniperBase(BaseConnection): """ Implement methods for interacting with Juniper Networks devices. @@ -70,11 +70,11 @@ def exit_enable_mode(self, *args, **kwargs): def check_config_mode(self, check_string=']'): """Checks if the device is in configuration mode or not.""" - return super(JuniperSSH, self).check_config_mode(check_string=check_string) + return super(JuniperBase, self).check_config_mode(check_string=check_string) def config_mode(self, config_command='configure'): """Enter configuration mode.""" - return super(JuniperSSH, self).config_mode(config_command=config_command) + return super(JuniperBase, self).config_mode(config_command=config_command) def exit_config_mode(self, exit_config='exit configuration-mode'): """Exit configuration mode.""" @@ -162,7 +162,7 @@ def commit(self, confirm=False, confirm_delay=None, check=False, comment='', def strip_prompt(self, *args, **kwargs): """Strip the trailing router prompt from the output.""" - a_string = super(JuniperSSH, self).strip_prompt(*args, **kwargs) + a_string = super(JuniperBase, self).strip_prompt(*args, **kwargs) return self.strip_context_items(a_string) def strip_context_items(self, a_string): @@ -193,6 +193,14 @@ def strip_context_items(self, a_string): return self.RESPONSE_RETURN.join(response_list[:-1]) return a_string +class JuniperSSH(JuniperBase): + pass + +class JuniperTelnet(JuniperBase): + def __init__(self, *args, **kwargs): + default_enter = kwargs.get('default_enter') + kwargs['default_enter'] = '\r\n' if default_enter is None else default_enter + super(JuniperTelnet, self).__init__(*args, **kwargs) class JuniperFileTransfer(BaseFileTransfer): """Juniper SCP File Transfer driver.""" diff --git a/netmiko/ssh_dispatcher.py b/netmiko/ssh_dispatcher.py index 8ffa58b69..07c7ca2f8 100644 --- a/netmiko/ssh_dispatcher.py +++ b/netmiko/ssh_dispatcher.py @@ -36,7 +36,7 @@ from netmiko.fortinet import FortinetSSH from netmiko.hp import HPProcurveSSH, HPComwareSSH from netmiko.huawei import HuaweiSSH, HuaweiVrpv8SSH -from netmiko.juniper import JuniperSSH +from netmiko.juniper import JuniperSSH, JuniperTelnet from netmiko.juniper import JuniperFileTransfer from netmiko.linux import LinuxSSH from netmiko.mellanox import MellanoxSSH @@ -140,6 +140,7 @@ CLASS_MAPPER['brocade_netiron_telnet'] = BrocadeNetironTelnet CLASS_MAPPER['cisco_ios_telnet'] = CiscoIosTelnet CLASS_MAPPER['arista_eos_telnet'] = AristaTelnet +CLASS_MAPPER['juniper_junos_telnet'] = JuniperTelnet CLASS_MAPPER['dell_powerconnect_telnet'] = DellPowerConnectTelnet CLASS_MAPPER['generic_termserver_telnet'] = TerminalServerTelnet CLASS_MAPPER['extreme_telnet'] = ExtremeTelnet From ab9a4f4f2f974f0c05c4c21b5a1cbc945e09ad07 Mon Sep 17 00:00:00 2001 From: Norihito Misaki Date: Fri, 2 Mar 2018 17:55:00 +0900 Subject: [PATCH 12/25] add blank lines --- netmiko/arista/arista_ssh.py | 3 +++ netmiko/juniper/juniper_ssh.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/netmiko/arista/arista_ssh.py b/netmiko/arista/arista_ssh.py index 87f0409c3..dd0e4f0ab 100644 --- a/netmiko/arista/arista_ssh.py +++ b/netmiko/arista/arista_ssh.py @@ -42,15 +42,18 @@ def _return_cli(self): """Return to the CLI.""" return self.send_command('exit', expect_string=r"[#>]") + class AristaSSH(AristaBase): pass + class AristaTelnet(AristaBase): def __init__(self, *args, **kwargs): default_enter = kwargs.get('default_enter') kwargs['default_enter'] = '\r\n' if default_enter is None else default_enter super(AristaTelnet, self).__init__(*args, **kwargs) + class AristaFileTransfer(CiscoFileTransfer): """Arista SCP File Transfer driver.""" def __init__(self, ssh_conn, source_file, dest_file, file_system="/mnt/flash", direction='put'): diff --git a/netmiko/juniper/juniper_ssh.py b/netmiko/juniper/juniper_ssh.py index a4ec9193f..269b745d2 100644 --- a/netmiko/juniper/juniper_ssh.py +++ b/netmiko/juniper/juniper_ssh.py @@ -193,15 +193,18 @@ def strip_context_items(self, a_string): return self.RESPONSE_RETURN.join(response_list[:-1]) return a_string + class JuniperSSH(JuniperBase): pass + class JuniperTelnet(JuniperBase): def __init__(self, *args, **kwargs): default_enter = kwargs.get('default_enter') kwargs['default_enter'] = '\r\n' if default_enter is None else default_enter super(JuniperTelnet, self).__init__(*args, **kwargs) + class JuniperFileTransfer(BaseFileTransfer): """Juniper SCP File Transfer driver.""" def __init__(self, ssh_conn, source_file, dest_file, file_system="/var/tmp", direction='put'): From 8a9eba26c3b1ae6942c6e1222dd161b47eba0024 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 9 Mar 2018 12:31:07 -0800 Subject: [PATCH 13/25] Add netmiko Linux secure file copy (mainly for testing) --- netmiko/arista/arista_ssh.py | 6 ---- netmiko/linux/__init__.py | 4 +-- netmiko/linux/linux_ssh.py | 56 ++++++++++++++++++++++++++++++++++++ netmiko/ssh_dispatcher.py | 3 +- tests/conftest.py | 15 ++++++++++ 5 files changed, 75 insertions(+), 9 deletions(-) diff --git a/netmiko/arista/arista_ssh.py b/netmiko/arista/arista_ssh.py index 0e21e303a..92cc59d32 100644 --- a/netmiko/arista/arista_ssh.py +++ b/netmiko/arista/arista_ssh.py @@ -56,12 +56,6 @@ def remote_space_available(self, search_pattern=""): """Return space available on remote device.""" return self._remote_space_available_unix(search_pattern=search_pattern) - def verify_space_available(self, search_pattern=r"(\d+) bytes free"): - """Verify sufficient space is available on destination file system (return boolean).""" - return super(AristaFileTransfer, self).verify_space_available( - search_pattern=search_pattern - ) - def check_file_exists(self, remote_cmd=""): """Check if the dest_file already exists on the file system (return boolean).""" return self._check_file_exists_unix(remote_cmd=remote_cmd) diff --git a/netmiko/linux/__init__.py b/netmiko/linux/__init__.py index 3ec318d6c..5dcf67176 100644 --- a/netmiko/linux/__init__.py +++ b/netmiko/linux/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals -from netmiko.linux.linux_ssh import LinuxSSH +from netmiko.linux.linux_ssh import LinuxSSH, LinuxFileTransfer -__all__ = ['LinuxSSH'] +__all__ = ['LinuxSSH', 'LinuxFileTransfer'] diff --git a/netmiko/linux/linux_ssh.py b/netmiko/linux/linux_ssh.py index 4f51110c0..b08425e99 100644 --- a/netmiko/linux/linux_ssh.py +++ b/netmiko/linux/linux_ssh.py @@ -5,6 +5,7 @@ import time from netmiko.cisco_base_connection import CiscoSSHConnection +from netmiko.cisco_base_connection import CiscoFileTransfer from netmiko.ssh_exception import NetMikoTimeoutException @@ -15,6 +16,14 @@ def session_preparation(self): self.ansi_escape_codes = True return super(LinuxSSH, self).session_preparation() + def _enter_shell(self): + """Already in shell.""" + return '' + + def _return_cli(self): + """The shell is the CLI.""" + return '' + def disable_paging(self, *args, **kwargs): """Linux doesn't have paging by default.""" return "" @@ -89,3 +98,50 @@ def cleanup(self): def save_config(self, cmd='', confirm=True, confirm_response=''): """Not Implemented""" raise NotImplementedError + + +class LinuxFileTransfer(CiscoFileTransfer): + """ + Linux SCP File Transfer driver. + + Mostly for testing purposes. + """ + def __init__(self, ssh_conn, source_file, dest_file, file_system="/var/tmp", direction='put'): + return super(LinuxFileTransfer, self).__init__(ssh_conn=ssh_conn, + source_file=source_file, + dest_file=dest_file, + file_system=file_system, + direction=direction) + + def remote_space_available(self, search_pattern=""): + """Return space available on remote device.""" + return self._remote_space_available_unix(search_pattern=search_pattern) + + def check_file_exists(self, remote_cmd=""): + """Check if the dest_file already exists on the file system (return boolean).""" + return self._check_file_exists_unix(remote_cmd=remote_cmd) + + def remote_file_size(self, remote_cmd="", remote_file=None): + """Get the file size of the remote file.""" + return self._remote_file_size_unix(remote_cmd=remote_cmd, remote_file=remote_file) + + def remote_md5(self, base_cmd='md5sum', remote_file=None): + if remote_file is None: + if self.direction == 'put': + remote_file = self.dest_file + elif self.direction == 'get': + remote_file = self.source_file + remote_md5_cmd = "{} {}/{}".format(base_cmd, self.file_system, remote_file) + dest_md5 = self.ssh_ctl_chan.send_command(remote_md5_cmd, max_loops=750, delay_factor=2) + dest_md5 = self.process_md5(dest_md5) + return dest_md5 + + @staticmethod + def process_md5(md5_output, pattern=r"^(\S+)\s+"): + return super(LinuxFileTransfer, LinuxFileTransfer).process_md5(md5_output, pattern=pattern) + + def enable_scp(self, cmd=None): + raise NotImplementedError + + def disable_scp(self, cmd=None): + raise NotImplementedError diff --git a/netmiko/ssh_dispatcher.py b/netmiko/ssh_dispatcher.py index 0737971f1..935d25ed9 100644 --- a/netmiko/ssh_dispatcher.py +++ b/netmiko/ssh_dispatcher.py @@ -38,7 +38,7 @@ from netmiko.huawei import HuaweiSSH, HuaweiVrpv8SSH from netmiko.juniper import JuniperSSH from netmiko.juniper import JuniperFileTransfer -from netmiko.linux import LinuxSSH +from netmiko.linux import LinuxSSH, LinuxFileTransfer from netmiko.mellanox import MellanoxSSH from netmiko.mrv import MrvOptiswitchSSH from netmiko.netapp import NetAppcDotSSH @@ -119,6 +119,7 @@ 'cisco_xe': CiscoIosFileTransfer, 'cisco_xr': CiscoXrFileTransfer, 'juniper_junos': JuniperFileTransfer, + 'linux': LinuxFileTransfer, } # Also support keys that end in _ssh diff --git a/tests/conftest.py b/tests/conftest.py index f40bc8af9..9ca08fe1e 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -158,6 +158,11 @@ def scp_fixture(request): # Delete pattern is the same on IOS-XR 'delete_file': delete_file_ios, }, + 'linux': { + 'file_system': '/var/tmp', + 'enable_scp': False, + 'delete_file': delete_file_generic, + }, } # Create the files @@ -233,6 +238,11 @@ def scp_fixture_get(request): # Delete pattern is the same on IOS-XR 'delete_file': delete_file_ios, }, + 'linux': { + 'file_system': '/var/tmp', + 'enable_scp': False, + 'delete_file': delete_file_generic, + }, } device_under_test = request.config.getoption('test_device') @@ -345,6 +355,11 @@ def scp_file_transfer(request): # Delete pattern is the same on IOS-XR 'delete_file': delete_file_ios, }, + 'linux': { + 'file_system': '/var/tmp', + 'enable_scp': False, + 'delete_file': delete_file_generic, + }, } # Create the files From eef2662572f9e37403922a98c5ee4edc12da43f1 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 9 Mar 2018 12:53:37 -0800 Subject: [PATCH 14/25] Fixing file system bug in file_transfer function --- netmiko/scp_functions.py | 12 ++++++++++-- netmiko/scp_handler.py | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/netmiko/scp_functions.py b/netmiko/scp_functions.py index 8f4310c45..4d0315d04 100644 --- a/netmiko/scp_functions.py +++ b/netmiko/scp_functions.py @@ -55,10 +55,18 @@ def file_transfer(ssh_conn, source_file, dest_file, file_system=None, direction= if not cisco_ios and inline_transfer: raise ValueError("Inline Transfer only supported for Cisco IOS/Cisco IOS-XE") + scp_args = { + 'ssh_conn': ssh_conn, + 'source_file': source_file, + 'dest_file': dest_file, + 'direction': direction, + } + if file_system is not None: + scp_args['file_system'] = file_system + TransferClass = InLineTransfer if inline_transfer else FileTransfer - with TransferClass(ssh_conn, source_file=source_file, dest_file=dest_file, - file_system=file_system, direction=direction) as scp_transfer: + with TransferClass(**scp_args) as scp_transfer: if scp_transfer.check_file_exists(): if overwrite_file: if not disable_md5: diff --git a/netmiko/scp_handler.py b/netmiko/scp_handler.py index bf05f091b..ccbff7559 100644 --- a/netmiko/scp_handler.py +++ b/netmiko/scp_handler.py @@ -111,7 +111,7 @@ def _remote_space_available_unix(self, search_pattern=""): remote_output = self.ssh_ctl_chan.send_command(remote_cmd, expect_string=r"[\$#]") # Try to ensure parsing is correct: - # Filesystem 512-blocks Used Avail Capacity Mounted on + # Filesystem 1K-blocks Used Avail Capacity Mounted on # /dev/bo0s3f 1264808 16376 1147248 1% /cf/var remote_output = remote_output.strip() output_lines = remote_output.splitlines() @@ -121,7 +121,7 @@ def _remote_space_available_unix(self, search_pattern=""): filesystem_line = output_lines[1] if 'Filesystem' not in header_line or 'Avail' not in header_line.split()[3]: - # Filesystem 512-blocks Used Avail Capacity Mounted on + # Filesystem 1K-blocks Used Avail Capacity Mounted on msg = "Parsing error, unexpected output from {}:\n{}".format(remote_cmd, remote_output) raise ValueError(msg) From aff1131a303e6f84774a625ed0545a4638c47fc4 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 9 Mar 2018 13:27:36 -0800 Subject: [PATCH 15/25] Cleaning up tests --- tests/conftest.py | 139 +++++++++++----------------------------- tests/test_suite_alt.sh | 1 + 2 files changed, 40 insertions(+), 100 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9ca08fe1e..7baa85f32 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -131,39 +131,7 @@ def scp_fixture(request): Return a tuple (ssh_conn, scp_handle) """ - platform_args = { - 'cisco_ios': { - 'file_system': 'flash:', - 'enable_scp': True, - 'delete_file': delete_file_ios, - }, - 'juniper_junos': { - 'file_system': '/var/tmp', - 'enable_scp': False, - 'delete_file': delete_file_generic, - }, - 'arista_eos': { - 'file_system': '/mnt/flash', - 'enable_scp': False, - 'delete_file': delete_file_generic, - }, - 'cisco_nxos': { - 'file_system': 'bootflash:', - 'enable_scp': False, - 'delete_file': delete_file_nxos, - }, - 'cisco_xr': { - 'file_system': 'disk0:', - 'enable_scp': False, - # Delete pattern is the same on IOS-XR - 'delete_file': delete_file_ios, - }, - 'linux': { - 'file_system': '/var/tmp', - 'enable_scp': False, - 'delete_file': delete_file_generic, - }, - } + platform_args = get_platform_args() # Create the files with open("test9.txt", "w") as f: @@ -211,40 +179,7 @@ def scp_fixture_get(request): Return a tuple (ssh_conn, scp_handle) """ - platform_args = { - 'cisco_ios': { - 'file_system': 'flash:', - 'enable_scp': True, - 'delete_file': delete_file_ios, - }, - 'juniper_junos': { - 'file_system': '/var/tmp', - 'enable_scp': False, - 'delete_file': delete_file_generic, - }, - 'arista_eos': { - 'file_system': '/mnt/flash', - 'enable_scp': False, - 'delete_file': delete_file_generic, - }, - 'cisco_nxos': { - 'file_system': 'bootflash:', - 'enable_scp': False, - 'delete_file': delete_file_nxos, - }, - 'cisco_xr': { - 'file_system': 'disk0:', - 'enable_scp': False, - # Delete pattern is the same on IOS-XR - 'delete_file': delete_file_ios, - }, - 'linux': { - 'file_system': '/var/tmp', - 'enable_scp': False, - 'delete_file': delete_file_generic, - }, - } - + platform_args = get_platform_args() device_under_test = request.config.getoption('test_device') test_devices = parse_yaml(PWD + "/etc/test_devices.yml") device = test_devices[device_under_test] @@ -328,39 +263,7 @@ def scp_file_transfer(request): Return the netmiko connection object """ - platform_args = { - 'cisco_ios': { - 'file_system': 'flash:', - 'enable_scp': True, - 'delete_file': delete_file_ios, - }, - 'juniper_junos': { - 'file_system': '/var/tmp', - 'enable_scp': False, - 'delete_file': delete_file_generic, - }, - 'arista_eos': { - 'file_system': '/mnt/flash', - 'enable_scp': False, - 'delete_file': delete_file_generic, - }, - 'cisco_nxos': { - 'file_system': 'bootflash:', - 'enable_scp': False, - 'delete_file': delete_file_nxos, - }, - 'cisco_xr': { - 'file_system': 'disk0:', - 'enable_scp': False, - # Delete pattern is the same on IOS-XR - 'delete_file': delete_file_ios, - }, - 'linux': { - 'file_system': '/var/tmp', - 'enable_scp': False, - 'delete_file': delete_file_generic, - }, - } + platform_args = get_platform_args() # Create the files with open("test9.txt", "w") as f: @@ -400,3 +303,39 @@ def scp_file_transfer(request): os.remove(alt_file) return (ssh_conn, file_system) + +def get_platform_args(): + return { + 'cisco_ios': { + 'file_system': 'flash:', + 'enable_scp': True, + 'delete_file': delete_file_ios, + }, + 'juniper_junos': { + 'file_system': '/var/tmp', + 'enable_scp': False, + 'delete_file': delete_file_generic, + }, + 'arista_eos': { + 'file_system': '/mnt/flash', + 'enable_scp': False, + 'delete_file': delete_file_generic, + }, + 'cisco_nxos': { + 'file_system': 'bootflash:', + 'enable_scp': False, + 'delete_file': delete_file_nxos, + }, + 'cisco_xr': { + 'file_system': 'disk0:', + 'enable_scp': False, + # Delete pattern is the same on IOS-XR + 'delete_file': delete_file_ios, + }, + 'linux': { + 'file_system': '/var/tmp', + 'enable_scp': False, + 'delete_file': delete_file_generic, + }, + } + diff --git a/tests/test_suite_alt.sh b/tests/test_suite_alt.sh index ed5f43986..d81d8ef9d 100755 --- a/tests/test_suite_alt.sh +++ b/tests/test_suite_alt.sh @@ -65,6 +65,7 @@ echo "Starting tests...good luck:" \ && py.test -v test_netmiko_config.py --test_device nxos1 \ \ && echo "Linux SSH (using keys)" \ +&& py.test -v test_netmiko_scp.py --test_device linux_srv1 \ && py.test -s -v test_netmiko_show.py --test_device linux_srv1 \ \ && echo "Autodetect tests" \ From 4eef1fd38a97879c24b4853844a41807672925db Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sat, 10 Mar 2018 07:49:23 -0800 Subject: [PATCH 16/25] Increasing Arista default delay for SCP --- netmiko/arista/arista_ssh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netmiko/arista/arista_ssh.py b/netmiko/arista/arista_ssh.py index 92cc59d32..1793b8584 100644 --- a/netmiko/arista/arista_ssh.py +++ b/netmiko/arista/arista_ssh.py @@ -71,7 +71,7 @@ def remote_md5(self, base_cmd='verify /md5', remote_file=None): elif self.direction == 'get': remote_file = self.source_file remote_md5_cmd = "{} file:{}/{}".format(base_cmd, self.file_system, remote_file) - dest_md5 = self.ssh_ctl_chan.send_command(remote_md5_cmd, max_loops=750, delay_factor=2) + dest_md5 = self.ssh_ctl_chan.send_command(remote_md5_cmd, max_loops=750, delay_factor=4) dest_md5 = self.process_md5(dest_md5) return dest_md5 From 4ca46bf990421c5217fe77d6f0005562ea567e04 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sat, 10 Mar 2018 07:53:39 -0800 Subject: [PATCH 17/25] Renaming juniper and arista files --- netmiko/arista/__init__.py | 2 +- netmiko/arista/{arista_ssh.py => arista.py} | 0 netmiko/juniper/__init__.py | 2 +- netmiko/juniper/{juniper_ssh.py => juniper.py} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename netmiko/arista/{arista_ssh.py => arista.py} (100%) rename netmiko/juniper/{juniper_ssh.py => juniper.py} (100%) diff --git a/netmiko/arista/__init__.py b/netmiko/arista/__init__.py index 07831d149..274f1acb2 100644 --- a/netmiko/arista/__init__.py +++ b/netmiko/arista/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals -from netmiko.arista.arista_ssh import AristaSSH, AristaTelnet, AristaFileTransfer +from netmiko.arista.arista import AristaSSH, AristaTelnet, AristaFileTransfer __all__ = ['AristaSSH', 'AristaTelnet', 'AristaFileTransfer'] diff --git a/netmiko/arista/arista_ssh.py b/netmiko/arista/arista.py similarity index 100% rename from netmiko/arista/arista_ssh.py rename to netmiko/arista/arista.py diff --git a/netmiko/juniper/__init__.py b/netmiko/juniper/__init__.py index e4905f179..1a17023b3 100644 --- a/netmiko/juniper/__init__.py +++ b/netmiko/juniper/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals -from netmiko.juniper.juniper_ssh import JuniperSSH, JuniperTelnet, JuniperFileTransfer +from netmiko.juniper.juniper import JuniperSSH, JuniperTelnet, JuniperFileTransfer __all__ = ['JuniperSSH', 'JuniperTelnet', 'JuniperFileTransfer'] diff --git a/netmiko/juniper/juniper_ssh.py b/netmiko/juniper/juniper.py similarity index 100% rename from netmiko/juniper/juniper_ssh.py rename to netmiko/juniper/juniper.py From de5ae0bea4aa7c334cad0a3f7b920274a8ffb527 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sat, 10 Mar 2018 08:06:06 -0800 Subject: [PATCH 18/25] Minor PY3 cleanup --- _release.sh | 8 +++--- netmiko/py23_compat.py | 2 +- tests/test_cisco_simple.py | 55 -------------------------------------- tests/test_pluribus.py | 31 --------------------- 4 files changed, 5 insertions(+), 91 deletions(-) delete mode 100755 tests/test_cisco_simple.py delete mode 100755 tests/test_pluribus.py diff --git a/_release.sh b/_release.sh index 9f3a2b63d..beb61d50a 100755 --- a/_release.sh +++ b/_release.sh @@ -68,8 +68,8 @@ else echo echo echo "Netmiko Installed Version" - python -c "import netmiko; print netmiko.__version__" - TEST_VERSION=`python -c "import netmiko; print netmiko.__version__"` + python -c "import netmiko; print(netmiko.__version__)" + TEST_VERSION=`python -c "import netmiko; print(netmiko.__version__)"` echo fi @@ -139,7 +139,7 @@ else echo echo echo "Netmiko Installed Version (from pypi)" - python -c "import netmiko; print netmiko.__version__" - TEST_VERSION=`python -c "import netmiko; print netmiko.__version__"` + python -c "import netmiko; print(netmiko.__version__)" + TEST_VERSION=`python -c "import netmiko; print(netmiko.__version__)"` echo fi diff --git a/netmiko/py23_compat.py b/netmiko/py23_compat.py index fb753e176..570092e4b 100644 --- a/netmiko/py23_compat.py +++ b/netmiko/py23_compat.py @@ -7,7 +7,7 @@ PY2 = sys.version_info.major == 2 PY3 = sys.version_info.major == 3 -if sys.version_info.major == 3: +if PY3: string_types = (str,) text_type = str else: diff --git a/tests/test_cisco_simple.py b/tests/test_cisco_simple.py deleted file mode 100755 index 2d44e2705..000000000 --- a/tests/test_cisco_simple.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python -from netmiko import ConnectHandler -from getpass import getpass - -#ip_addr = raw_input("Enter IP Address: ") -pwd = getpass() -ip_addr = '184.105.247.70' - -telnet_device = { - 'device_type': 'cisco_ios_telnet', - 'ip': ip_addr, - 'username': 'pyclass', - 'password': pwd, - 'port': 23, -} - -ssh_device = { - 'device_type': 'cisco_ios_ssh', - 'ip': ip_addr, - 'username': 'pyclass', - 'password': pwd, - 'port': 22, -} - -print "telnet" -net_connect1 = ConnectHandler(**telnet_device) -print "telnet prompt: {}".format(net_connect1.find_prompt()) -print "send_command: " -print '-' * 50 -print net_connect1.send_command_timing("show arp") -print '-' * 50 -print '-' * 50 -print net_connect1.send_command("show run") -print '-' * 50 -print - -print "SSH" -net_connect2 = ConnectHandler(**ssh_device) -print "SSH prompt: {}".format(net_connect2.find_prompt()) -print "send_command: " -print '-' * 50 -print net_connect2.send_command("show arp") -print '-' * 50 -print '-' * 50 -print net_connect1.send_command("show run") -print '-' * 50 -print - -#output = net_connect.send_command_expect("show version") - -#print -#print '#' * 50 -#print output -#print '#' * 50 -#print diff --git a/tests/test_pluribus.py b/tests/test_pluribus.py deleted file mode 100755 index aa9d24114..000000000 --- a/tests/test_pluribus.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python -from __future__ import print_function -from netmiko import ConnectHandler -from getpass import getpass - -#ip_addr = raw_input("Enter IP Address: ") -pwd = getpass() -ip_addr = 'sw05.bjm01' - -pluribus_ssh_device = { - 'device_type': 'pluribus', - 'ip': ip_addr, - 'username': 'pluriusr', - 'password': pwd, - 'port': 22, -} - -print('Opening SSH connection with', ip_addr) -net_connect = ConnectHandler(**pluribus_ssh_device) -print('SSH prompt: {}'.format(net_connect.find_prompt())) -print('Sending l2-table-show') -print('-' * 50) -print(net_connect.send_command('l2-table-show')) -print('-' * 50) -print('Sending lldp-show') -print('-' * 50) -print(net_connect.send_command('lldp-show')) -print('-' * 50) -print('Closing connection...') -net_connect.disconnect() -print('Connection closed.') From 346429188f9686caa0b03304379acfd50a526456 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sat, 10 Mar 2018 08:18:34 -0800 Subject: [PATCH 19/25] Force _autodetect_fs to be in enable mode --- netmiko/cisco_base_connection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netmiko/cisco_base_connection.py b/netmiko/cisco_base_connection.py index 344faa611..cc318c33b 100644 --- a/netmiko/cisco_base_connection.py +++ b/netmiko/cisco_base_connection.py @@ -148,6 +148,8 @@ def cleanup(self): def _autodetect_fs(self, cmd='dir', pattern=r'Directory of (.*)/'): """Autodetect the file system on the remote device. Used by SCP operations.""" + if not ssh_conn.check_enable_mode(): + raise ValueError('Must be in enable mode to auto-detect the file-system.') output = self.send_command_expect(cmd) match = re.search(pattern, output) if match: From 4af290b7e4ed2c3bd9aa06f6d1a56a3cdaad1712 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sat, 10 Mar 2018 08:21:12 -0800 Subject: [PATCH 20/25] Fixing invalid reference --- netmiko/cisco_base_connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netmiko/cisco_base_connection.py b/netmiko/cisco_base_connection.py index cc318c33b..d90f2a3b1 100644 --- a/netmiko/cisco_base_connection.py +++ b/netmiko/cisco_base_connection.py @@ -148,7 +148,7 @@ def cleanup(self): def _autodetect_fs(self, cmd='dir', pattern=r'Directory of (.*)/'): """Autodetect the file system on the remote device. Used by SCP operations.""" - if not ssh_conn.check_enable_mode(): + if not self.check_enable_mode(): raise ValueError('Must be in enable mode to auto-detect the file-system.') output = self.send_command_expect(cmd) match = re.search(pattern, output) From a552253c2978132846ff569a1a4e198fe3c3e073 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sat, 10 Mar 2018 08:45:54 -0800 Subject: [PATCH 21/25] Documentation strings. --- netmiko/base_connection.py | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/netmiko/base_connection.py b/netmiko/base_connection.py index f088be21e..29c62eaf2 100644 --- a/netmiko/base_connection.py +++ b/netmiko/base_connection.py @@ -508,7 +508,7 @@ def telnet_login(self, pri_prompt_terminator=r'#\s*$', alt_prompt_terminator=r'> :type delay_factor: int :param max_loops: Controls the wait time in conjunction with the delay_factor - (default: 60) + (default: 20) """ delay_factor = self.select_delay_factor(delay_factor) time.sleep(1 * delay_factor) @@ -721,21 +721,11 @@ def _test_channel_read(self, count=40, pattern=""): :param count: the number of times to check the channel for data :type count: int - :param pattern: Signifying the device prompt has returned and to break out of the loop + :param pattern: Regular expression pattern used to determine end of channel read :type pattern: str """ def _increment_delay(main_delay, increment=1.1, maximum=8): - """Increment sleep time to a maximum value. - - :param main_delay: Pri sleep factor for data to return from the channel - :type main_delay: int - - :param increment: Sec sleep factor for waiting for data to return from the channel - :type increment: float - - :param maximum: Max delay to sleep when waiting for data to return from the channel - :type maximum: int - """ + """Increment sleep time to a maximum value.""" main_delay = main_delay * increment if main_delay >= maximum: main_delay = maximum @@ -829,8 +819,6 @@ def set_terminal_width(self, command="", delay_factor=1): :param delay_factor: See __init__: global_delay_factor :type delay_factor: int - - TODO: delay_factor doesn't seem to be used in this method """ if not command: return "" @@ -1117,7 +1105,7 @@ def strip_command(self, command_string, output): def normalize_linefeeds(self, a_string): """Convert `\r\r\n`,`\r\n`, `\n\r` to `\n.` - :param a_string: A string that may hove non-normalized line feeds + :param a_string: A string that may have non-normalized line feeds i.e. output returned from device, or a device prompt :type a_string: str """ @@ -1153,7 +1141,7 @@ def enable(self, cmd='', pattern='ssword', re_flags=re.IGNORECASE): :param cmd: Device command to enter enable mode :type cmd: str - :param pattern: patter to search for indicating device is waiting for password + :param pattern: pattern to search for indicating device is waiting for password :type pattern: str :param re_flags: Regular expression flags used in conjunction with pattern @@ -1194,7 +1182,7 @@ def check_config_mode(self, check_string='', pattern=''): :param check_string: Identification of configuration mode from the device :type check_string: str - :param pattern: Pattern to identify the device prompt + :param pattern: Pattern to terminate reading of channel :type pattern: str """ self.write_channel(self.RETURN) @@ -1211,7 +1199,7 @@ def config_mode(self, config_command='', pattern=''): :param config_command: Configuration command to send to the device :type config_command: str - :param pattern: The pattern signifying the config command was completed + :param pattern: Pattern to terminate reading of channel :type pattern: str """ output = '' @@ -1228,7 +1216,7 @@ def exit_config_mode(self, exit_config='', pattern=''): :param exit_config: Command to exit configuration mode :type exit_config: str - :param pattern: The pattern signifying the exit config mode command completed + :param pattern: Pattern to terminate reading of channel :type pattern: str """ output = '' @@ -1269,13 +1257,13 @@ def send_config_set(self, config_commands=None, exit_config_mode=True, delay_fac Automatically exits/enters configuration mode. - :param config_commands: Multiple commands to be sent to the device - :type config_commands: list of strings + :param config_commands: Multiple configuration commands to be sent to the device + :type config_commands: list or string :param exit_config_mode: Determines whether or not to exit config mode after complete :type exit_config_mode: bool - :param delay_factor: Factor to adjust delay when reading the channel + :param delay_factor: Factor to adjust delays :type delay_factor: int :param max_loops: Controls wait time in conjunction with delay_factor (default: 150) @@ -1289,8 +1277,6 @@ def send_config_set(self, config_commands=None, exit_config_mode=True, delay_fac :param config_mode_command: The command to enter into config mode :type config_mode_command: str - - TODO: strip_prompt and strip_command not used in the method """ delay_factor = self.select_delay_factor(delay_factor) if config_commands is None: @@ -1343,7 +1329,7 @@ def strip_ansi_escape_codes(self, string_buffer): HP ProCurve's, Cisco SG300, and F5 LTM's require this (possible others) - :param string_buffer: The string that may require ansi escape chars to be removed + :param string_buffer: The string to be processed to remove ANSI escape codes :type string_buffer: str """ log.debug("In strip_ansi_escape_codes") From 6e086d18d9b75084a29eb08f632656f44ec2c3bf Mon Sep 17 00:00:00 2001 From: Ray Tetzloff Date: Wed, 14 Mar 2018 18:52:22 -0500 Subject: [PATCH 22/25] Add Calix B6 save_config and telnet support; correct exit_config_mode; add to device templates --- netmiko/calix/__init__.py | 4 +- .../calix/{calix_b6_ssh.py => calix_b6.py} | 83 ++++++++++--------- netmiko/ssh_dispatcher.py | 3 +- tests/etc/commands.yml.example | 10 +++ tests/etc/responses.yml.example | 10 +++ tests/etc/test_devices.yml.example | 7 ++ 6 files changed, 73 insertions(+), 44 deletions(-) rename netmiko/calix/{calix_b6_ssh.py => calix_b6.py} (71%) diff --git a/netmiko/calix/__init__.py b/netmiko/calix/__init__.py index b3c6b06d1..eda503df1 100644 --- a/netmiko/calix/__init__.py +++ b/netmiko/calix/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals -from netmiko.calix.calix_b6_ssh import CalixB6SSH +from netmiko.calix.calix_b6 import CalixB6SSH, CalixB6Telnet -__all__ = ['CalixB6SSH'] +__all__ = ['CalixB6SSH', 'CalixB6Telnet'] diff --git a/netmiko/calix/calix_b6_ssh.py b/netmiko/calix/calix_b6.py similarity index 71% rename from netmiko/calix/calix_b6_ssh.py rename to netmiko/calix/calix_b6.py index da000c8b7..cb3a08917 100644 --- a/netmiko/calix/calix_b6_ssh.py +++ b/netmiko/calix/calix_b6.py @@ -1,22 +1,23 @@ """Calix B6 SSH Driver for Netmiko""" from __future__ import unicode_literals + import time from os import path + from paramiko import SSHClient + from netmiko.cisco_base_connection import CiscoSSHConnection class SSHClient_noauth(SSHClient): + """Override _auth method for noauth""" def _auth(self, username, *args): self._transport.auth_none(username) return -class CalixB6SSH(CiscoSSHConnection): - """Calix B6 SSH driver - - These devices use SSH auth type (none) for cli user. Override SSH _auth method. - """ +class CalixB6Base(CiscoSSHConnection): + """Common methods for Calix B6, both SSH and Telnet.""" def session_preparation(self): """Prepare the session after the connection has been established.""" self.ansi_escape_codes = True @@ -28,28 +29,6 @@ def session_preparation(self): time.sleep(.3 * self.global_delay_factor) self.clear_buffer() - def _build_ssh_client(self): - """Prepare for Paramiko SSH connection. - - See base_connection.py file for any updates. - """ - # Create instance of SSHClient object - # If username is cli, we use noauth - if not self.use_keys: - remote_conn_pre = SSHClient_noauth() - else: - remote_conn_pre = SSHClient() - - # Load host_keys for better SSH security - if self.system_host_keys: - remote_conn_pre.load_system_host_keys() - if self.alt_host_keys and path.isfile(self.alt_key_file): - remote_conn_pre.load_host_keys(self.alt_key_file) - - # Default is to automatically add untrusted hosts (make sure appropriate for your env) - remote_conn_pre.set_missing_host_key_policy(self.key_policy) - return remote_conn_pre - def special_login_handler(self, delay_factor=1): """ Calix B6 presents with the following on login: @@ -69,26 +48,48 @@ def special_login_handler(self, delay_factor=1): elif 'Password:' in output: self.write_channel(self.password + self.RETURN) break - time.sleep(delay_factor * 0.5) else: self.write_channel(self.RETURN) - time.sleep(delay_factor * 1) i += 1 def check_config_mode(self, check_string=')#', pattern=''): """Checks if the device is in configuration mode""" - return super(CalixB6SSH, self).check_config_mode(check_string=check_string) + return super(CalixB6Base, self).check_config_mode( + check_string=check_string) + + def save_config(self, cmd='copy run start', confirm=False): + return super(CalixB6Base, self).save_config(cmd=cmd, confirm=confirm) + + +class CalixB6SSH(CalixB6Base): + """ + Calix B6 SSH Driver. + To make it work, we have to override the SSHClient _auth method. + If we use login/password, the ssh server use the (none) auth mechanism. + """ + def _build_ssh_client(self): + """Prepare for Paramiko SSH connection. + See base_connection.py file for any updates. + """ + # Create instance of SSHClient object + # If username is cli, we use noauth + if not self.use_keys: + remote_conn_pre = SSHClient_noauth() + else: + remote_conn_pre = SSHClient() + + # Load host_keys for better SSH security + if self.system_host_keys: + remote_conn_pre.load_system_host_keys() + if self.alt_host_keys and path.isfile(self.alt_key_file): + remote_conn_pre.load_host_keys(self.alt_key_file) - def config_mode(self, config_command='config t', pattern=''): - """Enter configuration mode.""" - return super(CalixB6SSH, self).config_mode(config_command=config_command) + # Default is to automatically add untrusted hosts (make sure + # appropriate for your env) + remote_conn_pre.set_missing_host_key_policy(self.key_policy) + return remote_conn_pre - def exit_config_mode(self, exit_config=None, pattern=''): - """Exit from configuration mode.""" - if exit_config is None: - exit_config = 'exit' + self.RETURN + 'end' - return super(CalixB6SSH, self).exit_config_mode(exit_config=exit_config, pattern=pattern) - def save_config(self, cmd='', confirm=True, confirm_response=''): - """Not Implemented""" - raise NotImplementedError +class CalixB6Telnet(CalixB6Base): + """Calix B6 Telnet Driver.""" + pass diff --git a/netmiko/ssh_dispatcher.py b/netmiko/ssh_dispatcher.py index 67be9225a..6309a9bef 100644 --- a/netmiko/ssh_dispatcher.py +++ b/netmiko/ssh_dispatcher.py @@ -13,7 +13,7 @@ from netmiko.brocade import BrocadeNetironSSH from netmiko.brocade import BrocadeNetironTelnet from netmiko.brocade import BrocadeNosSSH -from netmiko.calix import CalixB6SSH +from netmiko.calix import CalixB6SSH, CalixB6Telnet from netmiko.checkpoint import CheckPointGaiaSSH from netmiko.ciena import CienaSaosSSH from netmiko.cisco import CiscoAsaSSH, CiscoAsaFileTransfer @@ -143,6 +143,7 @@ CLASS_MAPPER['cisco_ios_telnet'] = CiscoIosTelnet CLASS_MAPPER['arista_eos_telnet'] = AristaTelnet CLASS_MAPPER['juniper_junos_telnet'] = JuniperTelnet +CLASS_MAPPER['calix_b6_telnet'] = CalixB6Telnet CLASS_MAPPER['dell_powerconnect_telnet'] = DellPowerConnectTelnet CLASS_MAPPER['generic_termserver_telnet'] = TerminalServerTelnet CLASS_MAPPER['extreme_telnet'] = ExtremeTelnet diff --git a/tests/etc/commands.yml.example b/tests/etc/commands.yml.example index 065438165..e3ef91cb1 100644 --- a/tests/etc/commands.yml.example +++ b/tests/etc/commands.yml.example @@ -154,3 +154,13 @@ alcatel_aos: - 'VLAN 666' config_verification: "show vlan" +calix_b6_ssh: + version: "show version" + basic: "show interface bvi 1" + extended_output: "show hardware" + config: + - "access-list ethernet 999 permit 0x8863" + - "access-list ethernet 999 permit 0x8864" + - "access-list ethernet 999 permit ip" + - "access-list ethernet 999 permit arp" + config_verification: "find running-config 999" diff --git a/tests/etc/responses.yml.example b/tests/etc/responses.yml.example index b5b4b334a..12ffc6b82 100644 --- a/tests/etc/responses.yml.example +++ b/tests/etc/responses.yml.example @@ -103,3 +103,13 @@ alcatel_aos: interface_ip: 192.168.1.154 version_banner: "Alcatel-Lucent OS6250-24 6.7.1.108.R04 Service Release, January 04, 2017" multiple_line_output: "FC - ForcedCopper PC - PreferredCopper C - Copper" + +calix_b6_ssh: + base_prompt: CALIX-B6-TEST + router_prompt: CALIX-B6-TEST> + enable_prompt: CALIX-B6-TEST# + interface_ip: 192.168.99.99 + version_banner: "Kernel build id 8.0.30.0" + multiple_line_output: "rtcPwrUptimeTotal" + cmd_response_init: "Building configuration... Done" + cmd_response_final: "access-list ethernet 999 permit ip" \ No newline at end of file diff --git a/tests/etc/test_devices.yml.example b/tests/etc/test_devices.yml.example index d396f0f11..5c7a04133 100644 --- a/tests/etc/test_devices.yml.example +++ b/tests/etc/test_devices.yml.example @@ -100,3 +100,10 @@ alcatel_aos: ip: 192.168.1.154 username: admin password: switch + +calix_b6_ssh: + device_type: calix_b6_ssh + ip: 192.168.99.99 + username: cli + password: occam + secret: razor \ No newline at end of file From 5f218ed8c7efcc2fe4174965a481b2e65157f5ef Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sat, 17 Mar 2018 09:52:49 -0700 Subject: [PATCH 23/25] Calix B6 return character --- netmiko/calix/calix_b6.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/netmiko/calix/calix_b6.py b/netmiko/calix/calix_b6.py index cb3a08917..f5b9b92cb 100644 --- a/netmiko/calix/calix_b6.py +++ b/netmiko/calix/calix_b6.py @@ -10,7 +10,7 @@ class SSHClient_noauth(SSHClient): - """Override _auth method for noauth""" + """Set noauth when manually handling SSH authentication.""" def _auth(self, username, *args): self._transport.auth_none(username) return @@ -18,6 +18,11 @@ def _auth(self, username, *args): class CalixB6Base(CiscoSSHConnection): """Common methods for Calix B6, both SSH and Telnet.""" + def __init__(self, *args, **kwargs): + default_enter = kwargs.get('default_enter') + kwargs['default_enter'] = '\r\n' if default_enter is None else default_enter + super(CalixB6SSH, self).__init__(*args, **kwargs) + def session_preparation(self): """Prepare the session after the connection has been established.""" self.ansi_escape_codes = True @@ -48,8 +53,10 @@ def special_login_handler(self, delay_factor=1): elif 'Password:' in output: self.write_channel(self.password + self.RETURN) break + time.sleep(delay_factor * 0.5) else: self.write_channel(self.RETURN) + time.sleep(delay_factor * 1) i += 1 def check_config_mode(self, check_string=')#', pattern=''): @@ -60,19 +67,23 @@ def check_config_mode(self, check_string=')#', pattern=''): def save_config(self, cmd='copy run start', confirm=False): return super(CalixB6Base, self).save_config(cmd=cmd, confirm=confirm) + def exit_config_mode(self, exit_config=None, pattern=''): + """Exit from configuration mode.""" + if exit_config is None: + exit_config = 'exit{}{}end'.format(self.RETURN, self.RETURN) + return super(CalixB6SSH, self).exit_config_mode(exit_config=exit_config, pattern=pattern) + class CalixB6SSH(CalixB6Base): - """ - Calix B6 SSH Driver. - To make it work, we have to override the SSHClient _auth method. - If we use login/password, the ssh server use the (none) auth mechanism. + """Calix B6 SSH Driver. + + To make it work, we have to override the SSHClient _auth method and manually handle + the username/password. """ def _build_ssh_client(self): - """Prepare for Paramiko SSH connection. - See base_connection.py file for any updates. - """ + """Prepare for Paramiko SSH connection.""" # Create instance of SSHClient object - # If username is cli, we use noauth + # If not using SSH keys, we use noauth if not self.use_keys: remote_conn_pre = SSHClient_noauth() else: @@ -84,8 +95,7 @@ def _build_ssh_client(self): if self.alt_host_keys and path.isfile(self.alt_key_file): remote_conn_pre.load_host_keys(self.alt_key_file) - # Default is to automatically add untrusted hosts (make sure - # appropriate for your env) + # Default is to automatically add untrusted hosts (make sure appropriate for your env) remote_conn_pre.set_missing_host_key_policy(self.key_policy) return remote_conn_pre From 1f29337ba37a2508b89770e37bcea267f85c1668 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Tue, 20 Mar 2018 19:57:19 -0700 Subject: [PATCH 24/25] Updates to calix B6 --- netmiko/calix/calix_b6.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/netmiko/calix/calix_b6.py b/netmiko/calix/calix_b6.py index f5b9b92cb..c8b9b1d40 100644 --- a/netmiko/calix/calix_b6.py +++ b/netmiko/calix/calix_b6.py @@ -67,12 +67,6 @@ def check_config_mode(self, check_string=')#', pattern=''): def save_config(self, cmd='copy run start', confirm=False): return super(CalixB6Base, self).save_config(cmd=cmd, confirm=confirm) - def exit_config_mode(self, exit_config=None, pattern=''): - """Exit from configuration mode.""" - if exit_config is None: - exit_config = 'exit{}{}end'.format(self.RETURN, self.RETURN) - return super(CalixB6SSH, self).exit_config_mode(exit_config=exit_config, pattern=pattern) - class CalixB6SSH(CalixB6Base): """Calix B6 SSH Driver. From 27aea6e5ede5392769be67c4a3490da85dfadfa2 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Thu, 22 Mar 2018 12:07:47 -0700 Subject: [PATCH 25/25] Rolling version --- netmiko/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netmiko/__init__.py b/netmiko/__init__.py index a041da3a4..f2f600d9c 100644 --- a/netmiko/__init__.py +++ b/netmiko/__init__.py @@ -23,7 +23,7 @@ NetmikoAuthError = NetMikoAuthenticationException Netmiko = ConnectHandler -__version__ = '2.1.0' +__version__ = '2.1.1' __all__ = ('ConnectHandler', 'ssh_dispatcher', 'platforms', 'SCPConn', 'FileTransfer', 'NetMikoTimeoutException', 'NetMikoAuthenticationException', 'NetmikoTimeoutError', 'NetmikoAuthError', 'InLineTransfer', 'redispatch',