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/__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', diff --git a/netmiko/arista/__init__.py b/netmiko/arista/__init__.py index 7573444ff..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, AristaFileTransfer +from netmiko.arista.arista import AristaSSH, AristaTelnet, AristaFileTransfer -__all__ = ['AristaSSH', 'AristaFileTransfer'] +__all__ = ['AristaSSH', 'AristaTelnet', 'AristaFileTransfer'] diff --git a/netmiko/arista/arista_ssh.py b/netmiko/arista/arista.py similarity index 89% rename from netmiko/arista/arista_ssh.py rename to netmiko/arista/arista.py index 0e21e303a..d5f7218f1 100644 --- a/netmiko/arista/arista_ssh.py +++ b/netmiko/arista/arista.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'[>#]') @@ -43,6 +43,17 @@ def _return_cli(self): 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'): @@ -56,12 +67,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) @@ -77,7 +82,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 diff --git a/netmiko/base_connection.py b/netmiko/base_connection.py index 60dbc052c..baf4da046 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: 20) + """ delay_factor = self.select_delay_factor(delay_factor) time.sleep(1 * delay_factor) @@ -569,10 +603,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 @@ -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,7 +716,14 @@ 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: 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.""" main_delay = main_delay * increment @@ -725,7 +771,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 +786,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 +809,16 @@ 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 """ if not command: return "" @@ -770,8 +832,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 +841,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 +859,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 +909,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 +945,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 +1064,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 +1083,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 +1103,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 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(self.RESPONSE_RETURN, a_string) if self.RESPONSE_RETURN == '\n': @@ -1021,19 +1116,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: pattern 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 +1163,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,7 +1177,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: Pattern to terminate reading of channel + :type pattern: str + """ self.write_channel(self.RETURN) # You can encounter an issue here (on router name changes) prefer delay-based solution if not pattern: @@ -1070,7 +1194,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: Pattern to terminate reading of channel + :type pattern: str + """ output = '' if not self.check_config_mode(): self.write_channel(self.normalize_cmd(config_command)) @@ -1080,7 +1211,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: Pattern to terminate reading of channel + :type pattern: str + """ output = '' if self.check_config_mode(): self.write_channel(self.normalize_cmd(exit_config)) @@ -1098,6 +1236,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) @@ -1112,6 +1256,27 @@ 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 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 delays + :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 """ delay_factor = self.select_delay_factor(delay_factor) if config_commands is None: @@ -1163,6 +1328,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 to be processed to remove ANSI escape codes + :type string_buffer: str """ log.debug("In strip_ansi_escape_codes") log.debug("repr = {0}".format(repr(string_buffer))) 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 70% rename from netmiko/calix/calix_b6_ssh.py rename to netmiko/calix/calix_b6.py index da000c8b7..c8b9b1d40 100644 --- a/netmiko/calix/calix_b6_ssh.py +++ b/netmiko/calix/calix_b6.py @@ -1,22 +1,28 @@ """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): + """Set noauth when manually handling SSH authentication.""" def _auth(self, username, *args): self._transport.auth_none(username) return -class CalixB6SSH(CiscoSSHConnection): - """Calix B6 SSH driver +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) - 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 @@ -28,28 +34,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: @@ -77,18 +61,39 @@ def special_login_handler(self, delay_factor=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) + - def config_mode(self, config_command='config t', pattern=''): - """Enter configuration mode.""" - return super(CalixB6SSH, self).config_mode(config_command=config_command) +class CalixB6SSH(CalixB6Base): + """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.""" + # Create instance of SSHClient object + # If not using SSH keys, 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 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/cisco_base_connection.py b/netmiko/cisco_base_connection.py index 344faa611..d90f2a3b1 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 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) if match: diff --git a/netmiko/juniper/__init__.py b/netmiko/juniper/__init__.py index 22f8118c1..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, JuniperFileTransfer +from netmiko.juniper.juniper import JuniperSSH, JuniperTelnet, JuniperFileTransfer -__all__ = ['JuniperSSH', 'JuniperFileTransfer'] +__all__ = ['JuniperSSH', 'JuniperTelnet', 'JuniperFileTransfer'] diff --git a/netmiko/juniper/juniper_ssh.py b/netmiko/juniper/juniper.py similarity index 93% rename from netmiko/juniper/juniper_ssh.py rename to netmiko/juniper/juniper.py index 52bedd8ec..269b745d2 100644 --- a/netmiko/juniper/juniper_ssh.py +++ b/netmiko/juniper/juniper.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): @@ -194,6 +194,17 @@ def strip_context_items(self, a_string): 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'): 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/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/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) diff --git a/netmiko/ssh_dispatcher.py b/netmiko/ssh_dispatcher.py index 0737971f1..6309a9bef 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 @@ -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 @@ -36,9 +36,9 @@ 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.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 @@ -140,6 +141,9 @@ 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['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/conftest.py b/tests/conftest.py index f40bc8af9..7baa85f32 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -131,34 +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, - }, - } + platform_args = get_platform_args() # Create the files with open("test9.txt", "w") as f: @@ -206,35 +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, - }, - } - + 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] @@ -318,34 +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, - }, - } + platform_args = get_platform_args() # Create the files with open("test9.txt", "w") as f: @@ -385,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/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 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.') 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" \