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..a421583eb 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Huawei Mellanox Palo Alto PAN-OS Pluribus -Ubiquiti EdgeOS +Ubiquiti EdgeSwitch Vyatta VyOS ###### Experimental @@ -75,6 +75,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..f16d65093 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 @@ -21,8 +21,7 @@ NetmikoTimeoutError = NetMikoTimeoutException NetmikoAuthError = NetMikoAuthenticationException -__version__ = '1.4.3' - +__version__ = '2.0.0' __all__ = ('ConnectHandler', 'ssh_dispatcher', 'platforms', 'SCPConn', 'FileTransfer', 'NetMikoTimeoutException', 'NetMikoAuthenticationException', 'NetmikoTimeoutError', 'NetmikoAuthError', 'InLineTransfer', 'redispatch', diff --git a/netmiko/a10/a10_ssh.py b/netmiko/a10/a10_ssh.py index 2f721eb2f..a9bb43f48 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,11 @@ 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() diff --git a/netmiko/accedian/accedian_ssh.py b/netmiko/accedian/accedian_ssh.py index aa0bbcc47..9d742601e 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!") diff --git a/netmiko/alcatel/alcatel_aos_ssh.py b/netmiko/alcatel/alcatel_aos_ssh.py index 5e1f0fcac..4c9432f83 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""" diff --git a/netmiko/alcatel/alcatel_sros_ssh.py b/netmiko/alcatel/alcatel_sros_ssh.py index 8c7054c32..0fcf40d91 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.""" 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..281a8c029 100644 --- a/netmiko/avaya/avaya_ers_ssh.py +++ b/netmiko/avaya/avaya_ers_ssh.py @@ -27,12 +27,12 @@ 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 diff --git a/netmiko/avaya/avaya_vsp_ssh.py b/netmiko/avaya/avaya_vsp_ssh.py index 1414fbf05..fd68f940c 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,7 @@ 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() diff --git a/netmiko/base_connection.py b/netmiko/base_connection.py index fc2a628bc..6c392cd51 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,8 +36,9 @@ 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. @@ -89,22 +91,31 @@ def __init__(self, ip='', host='', username='', password='', secret='', port=Non 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 @@ -113,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' @@ -151,10 +187,6 @@ 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 @@ -162,17 +194,19 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, 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: 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 """ if not start: # Must provide a comparison time @@ -183,9 +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 start time to measure the session timeout + :type start: float (from time.time() call i.e. epoch time) """ if not start: start = time.time() @@ -203,11 +239,18 @@ def _unlock_netmiko_session(self): self._session_locker.release() def _write_channel(self, out_data): - """Generic handler that will write to both SSH and telnet channel.""" + """Generic handler that will write to both SSH and telnet channel. + + :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: @@ -217,7 +260,11 @@ def _write_channel(self, out_data): pass def write_channel(self, out_data): - """Generic handler that will write to both SSH and telnet channel.""" + """Generic handler that will write to both SSH and telnet channel. + + :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: self._write_channel(out_data) @@ -267,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 @@ -281,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. @@ -294,16 +344,30 @@ 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: 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 \ + (defaults to no flags) + :type re_flags: re module flags + + :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': @@ -314,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 @@ -339,18 +403,35 @@ 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: multiplicative factor to adjust delay when reading channel (delays + get multiplied by this factor) + :type delay_factor: int or float + + :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 @@ -368,18 +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 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 + + """ 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.""" - 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) @@ -393,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): @@ -441,15 +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.""" + """Update SSH connection parameters based on contents of SSH 'config' file. + :param dict_arg: Dictionary of SSH connection parameters + :type dict_arg: dict + """ connect_dict = dict_arg.copy() # Use SSHConfig to generate source content. @@ -501,11 +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: Output from a remote network device + :type output: unicode string + + :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) @@ -523,6 +638,9 @@ def establish_connection(self, width=None, height=None): 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() @@ -537,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: @@ -550,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() @@ -560,7 +678,6 @@ def establish_connection(self, width=None, height=None): def _test_channel_read(self, count=40, pattern=""): """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.""" main_delay = main_delay * increment @@ -581,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 @@ -650,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='#', @@ -678,7 +792,7 @@ def find_prompt(self, delay_factor=1): """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 @@ -695,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)) @@ -714,15 +828,17 @@ 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 @@ -730,6 +846,8 @@ def send_command_timing(self, command_string, delay_factor=1, max_loops=150, :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) @@ -741,20 +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.""" - response_list = a_string.split('\n') + 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 @@ -762,21 +884,39 @@ def send_command(self, command_string, expect_string=None, :param command_string: The command to be executed on the remote device. :type command_string: str + :param expect_string: Regular expression pattern to use for determining end of output. If left blank will default to being based on router prompt. - :type expect_str: str + :type expect_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). + + :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 """ + # 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: @@ -794,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 @@ -814,34 +954,47 @@ 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.""" + """Support previous name of send_command method. + + :param args: Positional arguments to send to send_command() + :type args: list + + :param kwargs: Keyword arguments to send to send_command() + :type kwargs: Dict + """ return self.send_command(*args, **kwargs) @staticmethod def strip_backspaces(output): - """Strip any backspace characters out of the output.""" + """Strip any backspace characters out of the output. + + :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 @@ -852,46 +1005,47 @@ def strip_command(command_string, output): # 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): + 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) + 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) - @staticmethod - def normalize_cmd(command): + def normalize_cmd(self, command): """Normalize CLI commands to have a single trailing newline.""" - command = command.rstrip("\n") - command += '\n' + command = command.rstrip() + command += self.RETURN return command def check_enable_mode(self, check_string=''): """Check if in enable mode. Return boolean.""" - self.write_channel('\n') + 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 @@ -907,10 +1061,8 @@ def exit_enable_mode(self, exit_command=''): def check_config_mode(self, check_string='', pattern=''): """Checks if the device is in configuration mode or not.""" - 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))) return check_string in output def config_mode(self, config_command='', pattern=''): @@ -947,7 +1099,8 @@ def send_config_from_file(self, config_file=None, **kwargs): 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. @@ -966,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) @@ -976,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 @@ -1003,6 +1156,7 @@ 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) """ @@ -1023,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))) @@ -1047,13 +1202,18 @@ 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.""" 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..22e795f86 --- /dev/null +++ b/netmiko/brocade/brocade_netiron.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals +from netmiko.cisco_base_connection import CiscoSSHConnection + + +class BrocadeNetironBase(CiscoSSHConnection): + pass + + +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..70a45842d 100644 --- a/netmiko/brocade/brocade_nos_ssh.py +++ b/netmiko/brocade/brocade_nos_ssh.py @@ -17,5 +17,5 @@ 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) 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..f55228972 --- /dev/null +++ b/netmiko/calix/calix_b6_ssh.py @@ -0,0 +1,90 @@ +"""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) diff --git a/netmiko/checkpoint/checkpoint_gaia_ssh.py b/netmiko/checkpoint/checkpoint_gaia_ssh.py index 591f3db09..9c19b47a1 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.""" diff --git a/netmiko/ciena/ciena_saos_ssh.py b/netmiko/ciena/ciena_saos_ssh.py index 45e49b812..06b91a4fe 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,10 @@ 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 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..f0df10c6c 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,21 @@ 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 + + +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..b774269a7 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,181 @@ 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() -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..f4effc620 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,95 @@ 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 remote_space_available(self, search_pattern=r"(\d+) bytes free"): + """Return space available on remote device.""" + return super(CiscoNxosFileTransfer, 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(CiscoNxosFileTransfer, 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"= (.*)"): + """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..8d31e717c 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,8 @@ 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) + self.clear_buffer() diff --git a/netmiko/cisco/cisco_tp_tcce.py b/netmiko/cisco/cisco_tp_tcce.py index 2424af573..0bd20714d 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,7 +79,8 @@ 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) diff --git a/netmiko/cisco/cisco_wlc_ssh.py b/netmiko/cisco/cisco_wlc_ssh.py index c1a9fb1b2..95d65e54b 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,5 @@ 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 diff --git a/netmiko/cisco/cisco_xr_ssh.py b/netmiko/cisco/cisco_xr_ssh.py index 2589129a9..4cce75fdf 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,26 @@ 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) diff --git a/netmiko/cisco_base_connection.py b/netmiko/cisco_base_connection.py index c5e8f5bf5..567774d41 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,22 @@ 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)", 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 +79,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 +110,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 +145,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.""" @@ -151,3 +165,7 @@ def _autodetect_fs(self, cmd='dir', pattern=r'Directory of (.*)/'): 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..11b602cd4 --- /dev/null +++ b/netmiko/coriant/coriant_ssh.py @@ -0,0 +1,38 @@ + +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 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_powerconnect_ssh.py b/netmiko/dell/dell_powerconnect.py similarity index 78% rename from netmiko/dell/dell_powerconnect_ssh.py rename to netmiko/dell/dell_powerconnect.py index 12a497db0..a464e3d3d 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(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) + + +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..d6bc47964 100644 --- a/netmiko/eltex/eltex_ssh.py +++ b/netmiko/eltex/eltex_ssh.py @@ -1,14 +1,17 @@ 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() diff --git a/netmiko/enterasys/enterasys_ssh.py b/netmiko/enterasys/enterasys_ssh.py index 65bf98315..69e0d42a9 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,7 @@ 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() diff --git a/netmiko/extreme/extreme_ssh.py b/netmiko/extreme/extreme_ssh.py index f20bf56be..01a09cfb5 100644 --- a/netmiko/extreme/extreme_ssh.py +++ b/netmiko/extreme/extreme_ssh.py @@ -1,6 +1,6 @@ """Extreme support.""" from __future__ import unicode_literals - +import time import re from netmiko.cisco_base_connection import CiscoSSHConnection @@ -13,7 +13,10 @@ 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") + # Clear the read buffer + time.sleep(.3 * self.global_delay_factor) + self.clear_buffer() def set_base_prompt(self, *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..79c0f8405 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 diff --git a/netmiko/hp/hp_comware_ssh.py b/netmiko/hp/hp_comware_ssh.py index 8642a3c75..b115bb1c1 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.""" diff --git a/netmiko/hp/hp_procurve_ssh.py b/netmiko/hp/hp_procurve_ssh.py index df8cea25e..d92367914 100644 --- a/netmiko/hp/hp_procurve_ssh.py +++ b/netmiko/hp/hp_procurve_ssh.py @@ -20,50 +20,55 @@ 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 diff --git a/netmiko/huawei/huawei_ssh.py b/netmiko/huawei/huawei_ssh.py index a5285de44..67474afc7 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 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..9f34bcfe0 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,7 @@ 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) diff --git a/netmiko/mrv/mrv_ssh.py b/netmiko/mrv/mrv_ssh.py index da7290f92..ed72174ff 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.""" 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/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 75% rename from netmiko/brocade/brocade_fastiron_ssh.py rename to netmiko/ruckus/ruckus_fastiron.py index 1c5a54c68..377028bb7 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,14 @@ 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) + + +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..962a2016f 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.""" @@ -97,7 +93,7 @@ def close_scp_chan(self): def remote_space_available(self, search_pattern=r"bytes total \((.*) 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)) @@ -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_dispatcher.py b/netmiko/ssh_dispatcher.py index fadbcccfc..bd9a7cdd0 100644 --- a/netmiko/ssh_dispatcher.py +++ b/netmiko/ssh_dispatcher.py @@ -5,22 +5,24 @@ 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, 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 @@ -32,14 +34,17 @@ from netmiko.fortinet import FortinetSSH from netmiko.hp import HPProcurveSSH, HPComwareSSH from netmiko.huawei import HuaweiSSH -from netmiko.juniper import JuniperSSH +from netmiko.juniper import JuniperSSH, 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 +61,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, @@ -86,17 +93,28 @@ '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_nxos': CiscoNxosFileTransfer, + 'juniper_junos': JuniperFileTransfer, +} + # Also support keys that end in _ssh new_mapper = {} for k, v in CLASS_MAPPER_BASE.items(): @@ -105,10 +123,23 @@ 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['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 +149,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(platforms_base) +scp_platforms_str = "\n" + 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 +183,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..9bc275a17 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.""" diff --git a/netmiko/utilities.py b/netmiko/utilities.py index a38fdac2c..4893dfbd4 100644 --- a/netmiko/utilities.py +++ b/netmiko/utilities.py @@ -5,6 +5,10 @@ import sys import io import os +import serial.tools.list_ports +import clitable +from 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..ce8807362 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,12 @@ 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) 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.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