From 01a81db05a064a7cf09e5c66f332f0900441bf17 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 1 Dec 2017 23:15:37 +0800 Subject: [PATCH 01/29] Add JUNOS SCP Checksum --- netmiko/juniper/juniper_ssh.py | 36 ++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/netmiko/juniper/juniper_ssh.py b/netmiko/juniper/juniper_ssh.py index fc7e63c88..b703cfec6 100644 --- a/netmiko/juniper/juniper_ssh.py +++ b/netmiko/juniper/juniper_ssh.py @@ -198,11 +198,11 @@ def __init__(self, ssh_conn, source_file, dest_file, file_system="/var/tmp", dir if direction == 'put': self.source_file = source_file -# self.source_md5 = self.file_md5(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.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") @@ -301,18 +301,34 @@ 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 + Output from JUNOS + MD5 (file_name) = 3a608f654faf9afa0d8368984de1a5ce """ - raise NotImplementedError + match = re.search(pattern, md5_output) + if match: + return match.group(1) + else: + raise ValueError("Invalid output from MD5 command: {0}".format(md5_output)) def compare_md5(self): """Compare md5 of file on network device to md5 of local file""" - raise NotImplementedError + if self.direction == 'put': + 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): - raise NotImplementedError + def remote_md5(self, base_cmd='file checksum md5', 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 = "{0} {1}{2}".format(base_cmd, self.file_system, remote_file) + 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 def put_file(self): """SCP copy the file from the local system to the remote device.""" @@ -323,7 +339,7 @@ def put_file(self): def verify_file(self): """Verify the file has been transferred correctly.""" - raise NotImplementedError + return self.compare_md5() def enable_scp(self, cmd=None): raise NotImplementedError From 6000ec802aad3977ee6c7338af26e9536d39f7d6 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 9 Feb 2018 13:28:49 -0800 Subject: [PATCH 02/29] Minor doc update --- release_process.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release_process.txt b/release_process.txt index 12ec4a936..1586c9014 100644 --- a/release_process.txt +++ b/release_process.txt @@ -3,7 +3,7 @@ # Make sure you have rolled the version in __init__.py -# Merge into master / checkout master +# Merge into master / checkout master (use PR in GitHub for this) # Check FIX issues in _release.sh @@ -11,5 +11,5 @@ # Create a tag for the version $ git tag -a v1.4.1 -m "Version 1.4.1 Release" -$ git push --tags +$ git push origin From 1ee79d2c185ff9893eb5d9cd5cd3b4a7ae790e16 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Wed, 21 Feb 2018 13:01:29 -0800 Subject: [PATCH 03/29] Updating Netmiko examples --- examples/DEVICE_CREDS.py | 98 ---------------- examples/adding_delay/add_delay.py | 21 ++++ examples/asa_upgrade.py | 55 +++++---- examples/ciscoTP_example.py | 29 ----- examples/cisco_logging.txt | 2 - .../configuration_changes/change_file.txt | 2 + .../configuration_changes/config_changes.py | 22 ++++ .../config_changes_from_file.py | 21 ++++ .../connect_multiple.py | 37 ++++++ examples/enable/enable.py | 20 ++++ examples/handle_prompts/handle_prompts.py | 34 ++++++ examples/multiprocess_example.py | 111 ------------------ examples/scp_example.py | 48 -------- examples/show_command/show_command.py | 20 ++++ examples/show_command/show_command_textfsm.py | 18 +++ examples/simple_connection/simple_conn.py | 12 ++ .../simple_connection/simple_conn_dict.py | 17 +++ examples/troubleshooting/enable_logging.py | 23 ++++ setup.cfg | 2 +- 19 files changed, 278 insertions(+), 314 deletions(-) delete mode 100644 examples/DEVICE_CREDS.py create mode 100755 examples/adding_delay/add_delay.py delete mode 100644 examples/ciscoTP_example.py delete mode 100644 examples/cisco_logging.txt create mode 100644 examples/configuration_changes/change_file.txt create mode 100755 examples/configuration_changes/config_changes.py create mode 100755 examples/configuration_changes/config_changes_from_file.py create mode 100755 examples/connect_multiple_devices/connect_multiple.py create mode 100755 examples/enable/enable.py create mode 100755 examples/handle_prompts/handle_prompts.py delete mode 100644 examples/multiprocess_example.py delete mode 100755 examples/scp_example.py create mode 100755 examples/show_command/show_command.py create mode 100755 examples/show_command/show_command_textfsm.py create mode 100755 examples/simple_connection/simple_conn.py create mode 100755 examples/simple_connection/simple_conn_dict.py create mode 100755 examples/troubleshooting/enable_logging.py diff --git a/examples/DEVICE_CREDS.py b/examples/DEVICE_CREDS.py deleted file mode 100644 index edf29e4fe..000000000 --- a/examples/DEVICE_CREDS.py +++ /dev/null @@ -1,98 +0,0 @@ -cisco_881 = { - 'device_type': 'cisco_ios', - 'ip': '10.10.10.227', - 'username': 'test1', - 'password': 'password', - 'secret': 'secret', - 'verbose': False, -} - -cisco_asa = { - 'device_type': 'cisco_asa', - 'ip': '10.10.10.226', - 'username': 'admin', - 'password': 'password', - 'secret': 'secret', - 'verbose': False, -} - -arista_veos_sw1 = { - 'device_type': 'arista_eos', - 'ip': '10.10.10.227', - 'username': 'admin1', - 'password': 'password', - 'secret': '', - 'port': 8222, - 'verbose': False, -} - -arista_veos_sw2 = { - 'device_type': 'arista_eos', - 'ip': '10.10.10.227', - 'username': 'admin1', - 'password': 'password', - 'secret': '', - 'port': 8322, - 'verbose': False, -} - -arista_veos_sw3 = { - 'device_type': 'arista_eos', - 'ip': '10.10.10.227', - 'username': 'admin1', - 'password': 'password', - 'secret': '', - 'port': 8422, - 'verbose': False, -} - -arista_veos_sw4 = { - 'device_type': 'arista_eos', - 'ip': '10.10.10.227', - 'username': 'admin1', - 'password': 'password', - 'secret': '', - 'port': 8522, - 'verbose': False, -} - -hp_procurve = { - 'device_type': 'hp_procurve', - 'ip': '10.10.10.227', - 'username': 'admin', - 'password': 'password', - 'secret': '', - 'port': 9922, - 'verbose': False, -} - -hp_comware = { - 'device_type': 'hp_comware', - 'ip': '192.168.112.11', - 'username': 'admin', - 'password': 'admin', - 'port': 22, - 'verbose': False, -} - -brocade_vdx = { - 'device_type': 'brocade_vdx', - 'ip': '10.254.8.8', - 'username': 'admin', - 'password': 'password', - 'port': 22, - 'verbose': False, -} - - -all_devices = [ - cisco_881, - cisco_asa, - arista_veos_sw1, - arista_veos_sw2, - arista_veos_sw3, - arista_veos_sw4, - hp_procurve, - hp_comware, - brocade_vdx, -] diff --git a/examples/adding_delay/add_delay.py b/examples/adding_delay/add_delay.py new file mode 100755 index 000000000..75f6d275c --- /dev/null +++ b/examples/adding_delay/add_delay.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +from __future__ import print_function, unicode_literals + +# Netmiko is the same as ConnectHandler +from netmiko import Netmiko +from getpass import getpass + +my_device = { + 'host': "host.domain.com", + 'username': 'pyclass', + 'password': getpass(), + 'device_type': 'cisco_ios', + # Increase (essentially) all sleeps by a factor of 2 + 'global_delay_factor': 2, +} + +net_connect = Netmiko(**my_device) +# Increase the sleeps for just send_command by a factor of 2 +output = net_connect.send_command("show ip int brief", delay_factor=2) +print(output) +net_connect.disconnect() diff --git a/examples/asa_upgrade.py b/examples/asa_upgrade.py index 5406902cb..62c48853d 100755 --- a/examples/asa_upgrade.py +++ b/examples/asa_upgrade.py @@ -1,22 +1,27 @@ #!/usr/bin/env python """Script to upgrade a Cisco ASA.""" -import sys +from __future__ import print_function from datetime import datetime from getpass import getpass from netmiko import ConnectHandler, FileTransfer + def asa_scp_handler(ssh_conn, cmd='ssh scopy enable', mode='enable'): """Enable/disable SCP on Cisco ASA.""" if mode == 'disable': cmd = 'no ' + cmd return ssh_conn.send_config_set([cmd]) + def main(): """Script to upgrade a Cisco ASA.""" - ip_addr = raw_input("Enter ASA IP address: ") + try: + ip_addr = raw_input("Enter ASA IP address: ") + except NameError: + ip_addr = input("Enter ASA IP address: ") my_pass = getpass() start_time = datetime.now() - print ">>>> {}".format(start_time) + print(">>>> {}".format(start_time)) net_device = { 'device_type': 'cisco_asa', @@ -27,16 +32,15 @@ def main(): 'port': 22, } - print "\nLogging in to ASA" + print("\nLogging in to ASA") ssh_conn = ConnectHandler(**net_device) - print + print() # ADJUST TO TRANSFER IMAGE FILE dest_file_system = 'disk0:' source_file = 'test1.txt' dest_file = 'test1.txt' alt_dest_file = 'asa825-59-k8.bin' - scp_changed = False with FileTransfer(ssh_conn, source_file=source_file, dest_file=dest_file, file_system=dest_file_system) as scp_transfer: @@ -45,42 +49,43 @@ def main(): if not scp_transfer.verify_space_available(): raise ValueError("Insufficient space available on remote device") - print "Enabling SCP" + print("Enabling SCP") output = asa_scp_handler(ssh_conn, mode='enable') - print output + print(output) - print "\nTransferring file\n" + print("\nTransferring file\n") scp_transfer.transfer_file() - print "Disabling SCP" + print("Disabling SCP") output = asa_scp_handler(ssh_conn, mode='disable') - print output + print(output) - print "\nVerifying file" + print("\nVerifying file") if scp_transfer.verify_file(): - print "Source and destination MD5 matches" + print("Source and destination MD5 matches") else: raise ValueError("MD5 failure between source and destination files") - print "\nSending boot commands" + print("\nSending boot commands") full_file_name = "{}/{}".format(dest_file_system, alt_dest_file) boot_cmd = 'boot system {}'.format(full_file_name) output = ssh_conn.send_config_set([boot_cmd]) - print output + print(output) - print "\nVerifying state" + print("\nVerifying state") output = ssh_conn.send_command('show boot') - print output + print(output) # UNCOMMENT TO PERFORM WR MEM AND RELOAD - #print "\nWrite mem and reload" - #output = ssh_conn.send_command_expect('write mem') - #output += ssh_conn.send_command('reload') - #output += ssh_conn.send_command('y') - #print output - - print "\n>>>> {}".format(datetime.now() - start_time) - print + # print("\nWrite mem and reload") + # output = ssh_conn.send_command_expect('write mem') + # output += ssh_conn.send_command('reload') + # output += ssh_conn.send_command('y') + # print(output) + + print("\n>>>> {}".format(datetime.now() - start_time)) + print() + if __name__ == "__main__": main() diff --git a/examples/ciscoTP_example.py b/examples/ciscoTP_example.py deleted file mode 100644 index 3667e31f0..000000000 --- a/examples/ciscoTP_example.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python -''' -Test Netmiko on Cisco TelePresence Video device (C, SX, DX, EX, MX) -''' - -from netmiko import ConnectHandler, cisco - -mydevice = { -'device_type': 'cisco_tp', -'ip': '192.168.105.1', -'username': 'admin', -'password': 'Tandberg', -'verbose':True -} - -ssh_conn = ConnectHandler(**mydevice) -print( "\n\n") - - -command_list = ['help', 'whoami', 'whoaami', 'echo test', 'xconfig'] -#command_list = ['help', 'whoami', 'whoaami', 'echo test'] - -ssh_conn = ConnectHandler(**mydevice) -print( "\n\n") - -for command in command_list: - print('>>> running command : ' + command) - output = ssh_conn.send_command(command) - print('Result = ' + output + '\n') \ No newline at end of file diff --git a/examples/cisco_logging.txt b/examples/cisco_logging.txt deleted file mode 100644 index c237e53a8..000000000 --- a/examples/cisco_logging.txt +++ /dev/null @@ -1,2 +0,0 @@ -logging buffered 8111 -no logging console diff --git a/examples/configuration_changes/change_file.txt b/examples/configuration_changes/change_file.txt new file mode 100644 index 000000000..b331444e6 --- /dev/null +++ b/examples/configuration_changes/change_file.txt @@ -0,0 +1,2 @@ +logging buffered 8000 +logging console diff --git a/examples/configuration_changes/config_changes.py b/examples/configuration_changes/config_changes.py new file mode 100755 index 000000000..fbe18f52a --- /dev/null +++ b/examples/configuration_changes/config_changes.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +from __future__ import print_function, unicode_literals + +# Netmiko is the same as ConnectHandler +from netmiko import Netmiko +from getpass import getpass + +my_device = { + 'host': "host.domain.com", + 'username': 'pyclass', + 'password': getpass(), + 'device_type': 'cisco_ios', +} + +net_connect = Netmiko(**my_device) +cfg_commands = ['logging buffered 10000', 'no logging console'] + +# send_config_set() will automatically enter/exit config mode +output = net_connect.send_config_set(cfg_commands) +print(output) + +net_connect.disconnect() diff --git a/examples/configuration_changes/config_changes_from_file.py b/examples/configuration_changes/config_changes_from_file.py new file mode 100755 index 000000000..c5b0eaa88 --- /dev/null +++ b/examples/configuration_changes/config_changes_from_file.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +from __future__ import print_function, unicode_literals + +# Netmiko is the same as ConnectHandler +from netmiko import Netmiko +from getpass import getpass + +my_device = { + 'host': "host.domain.com", + 'username': 'pyclass', + 'password': getpass(), + 'device_type': 'cisco_ios', +} + +net_connect = Netmiko(**my_device) + +# Make configuration changes using an external file +output = net_connect.send_config_from_file("change_file.txt") +print(output) + +net_connect.disconnect() diff --git a/examples/connect_multiple_devices/connect_multiple.py b/examples/connect_multiple_devices/connect_multiple.py new file mode 100755 index 000000000..ba67218a7 --- /dev/null +++ b/examples/connect_multiple_devices/connect_multiple.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +""" +This example is serial (i.e. no concurrency). Connect to one device, after the other, +after the other. +""" +from __future__ import print_function, unicode_literals + +# Netmiko is the same as ConnectHandler +from netmiko import Netmiko +from getpass import getpass + +password = getpass() + +cisco1 = { + 'host': "host1.domain.com", + 'username': 'pyclass', + 'password': password, + 'device_type': 'cisco_ios', +} + +arista1 = { + 'host': "host2.domain.com", + 'username': 'pyclass', + 'password': password, + 'device_type': 'arista_eos', +} + +srx1 = { + 'host': "host3.domain.com", + 'username': 'pyclass', + 'password': password, + 'device_type': 'juniper_junos', +} + +for device in (cisco1, arista1, srx1): + net_connect = Netmiko(**device) + print(net_connect.find_prompt()) diff --git a/examples/enable/enable.py b/examples/enable/enable.py new file mode 100755 index 000000000..8c14df8b6 --- /dev/null +++ b/examples/enable/enable.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +from __future__ import print_function, unicode_literals + +# Netmiko is the same as ConnectHandler +from netmiko import Netmiko +from getpass import getpass + +my_device = { + 'host': "host.domain.com", + 'username': 'pyclass', + 'password': getpass(), + 'device_type': 'cisco_ios', +} + +net_connect = Netmiko(**my_device) +# Ensure in enable mode +net_connect.enable() +print(net_connect.find_prompt()) + +net_connect.disconnect() diff --git a/examples/handle_prompts/handle_prompts.py b/examples/handle_prompts/handle_prompts.py new file mode 100755 index 000000000..409c95d04 --- /dev/null +++ b/examples/handle_prompts/handle_prompts.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +"""Handling commands that prompt for additional information.""" +from __future__ import print_function, unicode_literals + +# Netmiko is the same as ConnectHandler +from netmiko import Netmiko +from getpass import getpass + +my_device = { + 'host': "host.domain.com", + 'username': 'pyclass', + 'password': getpass(), + 'device_type': 'cisco_ios', +} + +""" +Cisco IOS behavior on file delete: + +pynet-rtr1# delete flash:/small_file_bim.txt +Delete flash:/test1.txt? [confirm]y +pynet-rtr1 +""" + +net_connect = Netmiko(**my_device) +filename = "text1234.txt" +cmd = "delete flash:{}".format(filename) + +# send_command_timing as the router prompt is not returned +output = net_connect.send_command_timing(cmd, strip_command=False, strip_prompt=False) +if 'confirm' in output: + output += net_connect.send_command_timing("\n", strip_command=False, strip_prompt=False) + +net_connect.disconnect() +print(output) diff --git a/examples/multiprocess_example.py b/examples/multiprocess_example.py deleted file mode 100644 index 3007806d0..000000000 --- a/examples/multiprocess_example.py +++ /dev/null @@ -1,111 +0,0 @@ -''' -Requires paramiko >=1.8.0 (paramiko had an issue with multiprocessing prior -to this) - -Example code showing how to use netmiko for multiprocessing. Create a -separate process for each ssh connection. Each subprocess executes a -'show version' command on the remote device. Use a multiprocessing.queue to -pass data from subprocess to parent process. - -Only supports Python2 -''' - -# Catch Paramiko warnings about libgmp and RandomPool -import warnings -with warnings.catch_warnings(record=True) as w: - import paramiko - -import multiprocessing -import time -from datetime import datetime - -import netmiko -from netmiko.ssh_exception import NetMikoTimeoutException, NetMikoAuthenticationException - -# DEVICE_CREDS contains the devices to connect to -from DEVICE_CREDS import all_devices - - -def print_output(results): - - print "\nSuccessful devices:" - for a_dict in results: - for identifier,v in a_dict.iteritems(): - (success, out_string) = v - if success: - print '\n\n' - print '#' * 80 - print 'Device = {0}\n'.format(identifier) - print out_string - print '#' * 80 - - print "\n\nFailed devices:\n" - for a_dict in results: - for identifier,v in a_dict.iteritems(): - (success, out_string) = v - if not success: - print 'Device failed = {0}'.format(identifier) - - print "\nEnd time: " + str(datetime.now()) - print - - -def worker_show_version(a_device, mp_queue): - ''' - Return a dictionary where the key is the device identifier - Value is (success|fail(boolean), return_string) - ''' - - try: - a_device['port'] - except KeyError: - a_device['port'] = 22 - - identifier = '{ip}:{port}'.format(**a_device) - return_data = {} - - show_ver_command = 'show version' - SSHClass = netmiko.ssh_dispatcher(a_device['device_type']) - - try: - net_connect = SSHClass(**a_device) - show_version = net_connect.send_command(show_ver_command) - except (NetMikoTimeoutException, NetMikoAuthenticationException) as e: - return_data[identifier] = (False, e) - - # Add data to the queue (for parent process) - mp_queue.put(return_data) - return None - - return_data[identifier] = (True, show_version) - mp_queue.put(return_data) - - -def main(): - mp_queue = multiprocessing.Queue() - processes = [] - - print "\nStart time: " + str(datetime.now()) - - for a_device in all_devices: - p = multiprocessing.Process(target=worker_show_version, args=(a_device, mp_queue)) - processes.append(p) - # start the work process - p.start() - - # retrieve all the data from the queue - results = [] - while any(p.is_alive() for p in processes): - time.sleep(0.1) - while not mp_queue.empty(): - results.append(mp_queue.get()) - - # wait until the child processes have completed - for p in processes: - p.join() - - print_output(results) - - -if __name__ == '__main__': - main() diff --git a/examples/scp_example.py b/examples/scp_example.py deleted file mode 100755 index cd5dc919a..000000000 --- a/examples/scp_example.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python -''' -Cisco IOS only - -Requires scp https://github.com/jbardin/scp.py -''' - -from netmiko import ConnectHandler, SCPConn -from SECRET_DEVICE_CREDS import cisco_881 - -def main(): - ''' - SCP transfer cisco_logging.txt to network device - - Use ssh_conn as ssh channel into network device - scp_conn must be closed after file transfer - ''' - ssh_conn = ConnectHandler(**cisco_881) - scp_conn = SCPConn(ssh_conn) - s_file = 'cisco_logging.txt' - d_file = 'cisco_logging.txt' - - print "\n\n" - - scp_conn.scp_transfer_file(s_file, d_file) - scp_conn.close() - - output = ssh_conn.send_command("show flash: | inc cisco_logging") - print ">> " + output + '\n' - - # Disable file copy confirmation - output = ssh_conn.send_config_set(["file prompt quiet"]) - - # Execute config merge - print "Performing config merge\n" - output = ssh_conn.send_command("copy flash:cisco_logging.txt running-config") - - # Verify change - print "Verifying logging buffer change" - output = ssh_conn.send_command("show run | inc logging buffer") - print ">> " + output + '\n' - - # Restore copy confirmation - output = ssh_conn.send_config_set(["file prompt alert"]) - - -if __name__ == "__main__": - main() diff --git a/examples/show_command/show_command.py b/examples/show_command/show_command.py new file mode 100755 index 000000000..51fc25d4e --- /dev/null +++ b/examples/show_command/show_command.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +from __future__ import print_function, unicode_literals + +# Netmiko is the same as ConnectHandler +from netmiko import Netmiko +from getpass import getpass + +my_device = { + 'host': 'host.domain.com', + 'username': 'pyclass', + 'password': getpass(), + 'device_type': 'cisco_ios', +} + +net_connect = Netmiko(**my_device) + +output = net_connect.send_command("show ip int brief") +print(output) + +net_connect.disconnect() diff --git a/examples/show_command/show_command_textfsm.py b/examples/show_command/show_command_textfsm.py new file mode 100755 index 000000000..6a3e9c783 --- /dev/null +++ b/examples/show_command/show_command_textfsm.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +from __future__ import print_function, unicode_literals + +# Netmiko is the same as ConnectHandler +from netmiko import Netmiko +from getpass import getpass + +my_device = { + 'host': 'host.domain.com', + 'username': 'pyclass', + 'password': getpass(), + 'device_type': 'cisco_ios', +} + +net_connect = Netmiko(**my_device) +# Requires ntc-templates to be installed in ~/ntc-templates/templates +output = net_connect.send_command("show ip int brief", use_textfsm=True) +print(output) diff --git a/examples/simple_connection/simple_conn.py b/examples/simple_connection/simple_conn.py new file mode 100755 index 000000000..535a73095 --- /dev/null +++ b/examples/simple_connection/simple_conn.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +from __future__ import print_function, unicode_literals + +# Netmiko is the same as ConnectHandler +from netmiko import Netmiko +from getpass import getpass + +net_connect = Netmiko(host='host.domain.com', username='pyclass', + password=getpass(), device_type='cisco_ios') + +print(net_connect.find_prompt()) +net_connect.disconnect() diff --git a/examples/simple_connection/simple_conn_dict.py b/examples/simple_connection/simple_conn_dict.py new file mode 100755 index 000000000..5ece4dd95 --- /dev/null +++ b/examples/simple_connection/simple_conn_dict.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python +from __future__ import print_function, unicode_literals + +# Netmiko is the same as ConnectHandler +from netmiko import Netmiko +from getpass import getpass + +my_device = { + 'host': 'host.domain.com', + 'username': 'pyclass', + 'password': getpass(), + 'device_type': 'cisco_ios', +} + +net_connect = Netmiko(**my_device) +print(net_connect.find_prompt()) +net_connect.disconnect() diff --git a/examples/troubleshooting/enable_logging.py b/examples/troubleshooting/enable_logging.py new file mode 100755 index 000000000..ca695f581 --- /dev/null +++ b/examples/troubleshooting/enable_logging.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +from __future__ import print_function, unicode_literals + +import logging +from netmiko import Netmiko +from getpass import getpass + +# This will create a file named 'test.log' in your current directory. +# It will log all reads and writes on the SSH channel. +logging.basicConfig(filename='test.log', level=logging.DEBUG) +logger = logging.getLogger("netmiko") + +my_device = { + 'host': 'host.domain.com', + 'username': 'pyclass', + 'password': getpass(), + 'device_type': 'cisco_ios', +} + +net_connect = Netmiko(**my_device) +output = net_connect.send_command("show ip int brief") +print(output) +net_connect.disconnect() diff --git a/setup.cfg b/setup.cfg index ee8cc0028..8ba5463ba 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [pylama] linters = mccabe,pep8,pyflakes ignore = D203,C901 -skip = tests/*,examples/*,build/*,.tox/*,netmiko/_textfsm/* +skip = tests/*,build/*,.tox/*,netmiko/_textfsm/* [pylama:pep8] max_line_length = 100 From 7b163fc27a5300ff380c8d0e64e7f3adee7141e6 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 23 Feb 2018 15:12:05 -0800 Subject: [PATCH 04/29] Improving check_config_mode behavior to better handle hostname changes --- netmiko/base_connection.py | 6 +++++- netmiko/cisco_base_connection.py | 2 -- tests/test_suite_tmp.sh | 11 +++++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/netmiko/base_connection.py b/netmiko/base_connection.py index 33c62b15b..549814893 100644 --- a/netmiko/base_connection.py +++ b/netmiko/base_connection.py @@ -1062,7 +1062,11 @@ 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.""" self.write_channel(self.RETURN) - output = self.read_until_pattern(pattern=pattern) + # You can encounter an issue here (on router name changes) prefer delay-based solution + if not pattern: + output = self._read_channel_timing() + else: + output = self.read_until_pattern(pattern=pattern) return check_string in output def config_mode(self, config_command='', pattern=''): diff --git a/netmiko/cisco_base_connection.py b/netmiko/cisco_base_connection.py index 31ad1eb53..1dea9d77b 100644 --- a/netmiko/cisco_base_connection.py +++ b/netmiko/cisco_base_connection.py @@ -27,8 +27,6 @@ def check_config_mode(self, check_string=')#', pattern=''): Cisco IOS devices abbreviate the prompt at 20 chars in config mode """ - if not pattern: - pattern = re.escape(self.base_prompt[:16]) return super(CiscoBaseConnection, self).check_config_mode(check_string=check_string, pattern=pattern) diff --git a/tests/test_suite_tmp.sh b/tests/test_suite_tmp.sh index ac70110a5..950d91e67 100755 --- a/tests/test_suite_tmp.sh +++ b/tests/test_suite_tmp.sh @@ -16,6 +16,10 @@ echo "Starting tests...good luck:" \ && py.test -v test_netmiko_show.py --test_device cisco881 \ && py.test -v test_netmiko_config.py --test_device cisco881 \ \ +&& echo "Cisco IOS using SSH config with SSH Proxy" \ +&& py.test -v test_netmiko_show.py --test_device cisco881_ssh_config \ +&& py.test -v test_netmiko_config.py --test_device cisco881_ssh_config \ +\ && 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 \ @@ -52,6 +56,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 \ @@ -62,7 +69,3 @@ echo "Starting tests...good luck:" \ || RETURN_CODE=1 exit $RETURN_CODE - -#&& echo "Linux SSH (using keys)" \ -#&& py.test -s -v test_netmiko_show.py --test_device linux_srv1 \ -#\ From e840e5c8615d72c3fa8e0b6c7ec6eb1718196d83 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sat, 24 Feb 2018 14:22:02 -0800 Subject: [PATCH 05/29] Juniper SCP support --- netmiko/juniper/juniper_ssh.py | 9 ++++----- netmiko/scp_handler.py | 2 -- netmiko/ssh_dispatcher.py | 4 ++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/netmiko/juniper/juniper_ssh.py b/netmiko/juniper/juniper_ssh.py index 7878403b9..00944b016 100644 --- a/netmiko/juniper/juniper_ssh.py +++ b/netmiko/juniper/juniper_ssh.py @@ -199,7 +199,7 @@ 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) + # raise NotImplementedError(msg) self.ssh_ctl_chan = ssh_conn self.source_file = source_file self.dest_file = dest_file @@ -257,11 +257,10 @@ 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._enter_shell() - remote_cmd = "ls {}/{}".format(self.file_system, self.dest_file) + remote_cmd = "ls {}".format(self.file_system) remote_out = self.ssh_ctl_chan.send_command(remote_cmd, expect_string=r"[\$#]") self.ssh_ctl_chan._return_cli() return self.dest_file in remote_out - elif self.direction == 'get': return os.path.exists(self.dest_file) @@ -278,8 +277,8 @@ def remote_file_size(self, remote_cmd="", remote_file=None): self.ssh_ctl_chan._enter_shell() remote_out = self.ssh_ctl_chan.send_command(remote_cmd, expect_string=r"[\$#]") escape_file_name = re.escape(remote_file) - pattern = r"\s+({}).*".format(escape_file_name) - match = re.search(pattern, remote_out) + pattern = r"^.* ({}).*$".format(escape_file_name) + match = re.search(pattern, remote_out, flags=re.M) if match: # Format: -rw-r--r-- 1 pyclass wheel 12 Nov 5 19:07 /var/tmp/test3.txt line = match.group(0) diff --git a/netmiko/scp_handler.py b/netmiko/scp_handler.py index 146d23e79..3d829d3aa 100644 --- a/netmiko/scp_handler.py +++ b/netmiko/scp_handler.py @@ -216,8 +216,6 @@ def get_file(self): def put_file(self): """SCP copy the file from the local system to the remote device.""" 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) # Must close the SCP connection to get the file written (flush) self.scp_conn.close() diff --git a/netmiko/ssh_dispatcher.py b/netmiko/ssh_dispatcher.py index de3b5b58e..d97ca3abb 100644 --- a/netmiko/ssh_dispatcher.py +++ b/netmiko/ssh_dispatcher.py @@ -37,7 +37,7 @@ from netmiko.hp import HPProcurveSSH, HPComwareSSH from netmiko.huawei import HuaweiSSH, HuaweiVrpv8SSH from netmiko.juniper import JuniperSSH -# from netmiko.juniper import JuniperFileTransfer +from netmiko.juniper import JuniperFileTransfer from netmiko.linux import LinuxSSH from netmiko.mellanox import MellanoxSSH from netmiko.mrv import MrvOptiswitchSSH @@ -117,7 +117,7 @@ 'cisco_ios': CiscoIosFileTransfer, 'cisco_xe': CiscoIosFileTransfer, 'cisco_nxos': CiscoNxosFileTransfer, - # 'juniper_junos': JuniperFileTransfer, + 'juniper_junos': JuniperFileTransfer, } # Also support keys that end in _ssh From 947010b55a5afd1dec4f434696fba289996ca8ea Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sat, 24 Feb 2018 19:42:36 -0800 Subject: [PATCH 06/29] Adding SCP unit tests for Juniper --- netmiko/juniper/juniper_ssh.py | 5 ++- netmiko/scp_handler.py | 3 +- tests/conftest.py | 55 +++++++++++++++++++++++++++------ tests/test_netmiko_scp.py | 56 ++++++++++++++++++---------------- 4 files changed, 79 insertions(+), 40 deletions(-) diff --git a/netmiko/juniper/juniper_ssh.py b/netmiko/juniper/juniper_ssh.py index 00944b016..a7a1150fe 100644 --- a/netmiko/juniper/juniper_ssh.py +++ b/netmiko/juniper/juniper_ssh.py @@ -198,8 +198,6 @@ def strip_context_items(self, 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.source_file = source_file self.dest_file = dest_file @@ -211,7 +209,7 @@ def __init__(self, ssh_conn, source_file, dest_file, file_system="/var/tmp", dir 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_file = 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: @@ -271,6 +269,7 @@ def remote_file_size(self, remote_cmd="", remote_file=None): remote_file = self.dest_file elif self.direction == 'get': remote_file = self.source_file + remote_file = "{}/{}".format(self.file_system, remote_file) if not remote_cmd: remote_cmd = "ls -l {}".format(remote_file) diff --git a/netmiko/scp_handler.py b/netmiko/scp_handler.py index 3d829d3aa..6f5a7170e 100644 --- a/netmiko/scp_handler.py +++ b/netmiko/scp_handler.py @@ -210,7 +210,8 @@ def transfer_file(self): def get_file(self): """SCP copy the file from the remote device to local system.""" - self.scp_conn.scp_get_file(self.source_file, self.dest_file) + source_file = "{}/{}".format(self.file_system, self.source_file) + self.scp_conn.scp_get_file(source_file, self.dest_file) self.scp_conn.close() def put_file(self): diff --git a/tests/conftest.py b/tests/conftest.py index 9dfee05aa..1fd75d775 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -74,13 +74,11 @@ def commands(request): def delete_file_ios(ssh_conn, dest_file_system, dest_file): """Delete a remote file for a Cisco IOS device.""" - if not dest_file_system: raise ValueError("Invalid file system specified") if not dest_file: raise ValueError("Invalid dest file specified") - # Check if the dest_file already exists full_file_name = "{0}/{1}".format(dest_file_system, dest_file) cmd = "delete {0}".format(full_file_name) @@ -96,6 +94,16 @@ def delete_file_ios(ssh_conn, dest_file_system, dest_file): raise ValueError("An error happened deleting file on Cisco IOS") +def delete_file_junos(ssh_conn, dest_file_system, dest_file): + """Delete a remote file for a Junos device.""" + full_file_name = "{}/{}".format(dest_file_system, dest_file) + cmd = "rm {}".format(full_file_name) + output = ssh_conn._enter_shell() + output += ssh_conn.send_command_timing(cmd, strip_command=False, strip_prompt=False) + output += ssh_conn._return_cli() + return output + + @pytest.fixture(scope='module') def scp_fixture(request): """ @@ -103,13 +111,27 @@ def scp_fixture(request): Return a tuple (ssh_conn, scp_handle) """ + platform_args = { + 'cisco_ios': { + 'file_system': 'flash:', + 'enable_scp': True, + 'delete_file': delete_file_ios, + }, + 'juniper_junos': { + 'file_system': '/var/tmp', + 'enable_scp': False, + 'delete_file': delete_file_junos, + }, + } + device_under_test = request.config.getoption('test_device') test_devices = parse_yaml(PWD + "/etc/test_devices.yml") device = test_devices[device_under_test] device['verbose'] = False ssh_conn = ConnectHandler(**device) - dest_file_system = 'flash:' + platform = device['device_type'] + dest_file_system = platform_args[platform]['file_system'] source_file = 'test9.txt' dest_file = 'test9.txt' local_file = 'testx.txt' @@ -120,14 +142,15 @@ def scp_fixture(request): scp_transfer.establish_scp_conn() # Make sure SCP is enabled - scp_transfer.enable_scp() + if platform_args[platform]['enable_scp']: + scp_transfer.enable_scp() # Delete the test transfer files if scp_transfer.check_file_exists(): - delete_file_ios(ssh_conn, dest_file_system, dest_file) + func = platform_args[platform]['delete_file'] + func(ssh_conn, dest_file_system, dest_file) if os.path.exists(local_file): os.remove(local_file) - return (ssh_conn, scp_transfer) @pytest.fixture(scope='module') @@ -137,13 +160,27 @@ def scp_fixture_get(request): Return a tuple (ssh_conn, scp_handle) """ + platform_args = { + 'cisco_ios': { + 'file_system': 'flash:', + 'enable_scp': True, + 'delete_file': delete_file_ios, + }, + 'juniper_junos': { + 'file_system': '/var/tmp', + 'enable_scp': False, + 'delete_file': delete_file_junos, + }, + } + device_under_test = request.config.getoption('test_device') test_devices = parse_yaml(PWD + "/etc/test_devices.yml") device = test_devices[device_under_test] device['verbose'] = False ssh_conn = ConnectHandler(**device) - dest_file_system = 'flash:' + platform = device['device_type'] + dest_file_system = platform_args[platform]['file_system'] source_file = 'test9.txt' local_file = 'testx.txt' dest_file = local_file @@ -154,12 +191,12 @@ def scp_fixture_get(request): scp_transfer.establish_scp_conn() # Make sure SCP is enabled - scp_transfer.enable_scp() + if platform_args[platform]['enable_scp']: + scp_transfer.enable_scp() # Delete the test transfer files if os.path.exists(local_file): os.remove(local_file) - return (ssh_conn, scp_transfer) @pytest.fixture(scope='module') diff --git a/tests/test_netmiko_scp.py b/tests/test_netmiko_scp.py index 3356ab75a..c75bf55aa 100755 --- a/tests/test_netmiko_scp.py +++ b/tests/test_netmiko_scp.py @@ -8,30 +8,29 @@ from getpass import getpass from netmiko import ConnectHandler, FileTransfer -def test_enable_scp(scp_fixture): - ssh_conn, scp_transfer = scp_fixture - - scp_transfer.disable_scp() - output = ssh_conn.send_command_expect("show run | inc scp") - assert 'ip scp server enable' not in output - - scp_transfer.enable_scp() - output = ssh_conn.send_command_expect("show run | inc scp") - assert 'ip scp server enable' in output +###def test_enable_scp(scp_fixture): +### ssh_conn, scp_transfer = scp_fixture +### +### scp_transfer.disable_scp() +### output = ssh_conn.send_command_expect("show run | inc scp") +### assert 'ip scp server enable' not in output +### +### scp_transfer.enable_scp() +### output = ssh_conn.send_command_expect("show run | inc scp") +### assert 'ip scp server enable' in output def test_scp_put(scp_fixture): ssh_conn, scp_transfer = scp_fixture - if scp_transfer.check_file_exists(): assert False else: scp_transfer.put_file() assert scp_transfer.check_file_exists() == True -def test_remote_space_available(scp_fixture): +def test_remote_space_available(scp_fixture, expected_responses): ssh_conn, scp_transfer = scp_fixture remote_space = scp_transfer.remote_space_available() - assert remote_space >= 30000000 + assert remote_space >= expected_responses["scp_remote_space"] def test_local_space_available(scp_fixture): ssh_conn, scp_transfer = scp_fixture @@ -45,15 +44,10 @@ def test_verify_space_available_put(scp_fixture): scp_transfer.file_size = 1000000000 assert scp_transfer.verify_space_available() == False -def test_verify_space_available_get(scp_fixture_get): - ssh_conn, scp_transfer = scp_fixture_get - assert scp_transfer.verify_space_available() == True - # intentional make there not be enough space available - scp_transfer.file_size = 100000000000 - assert scp_transfer.verify_space_available() == False - def test_remote_file_size(scp_fixture): ssh_conn, scp_transfer = scp_fixture + if not scp_transfer.check_file_exists(): + scp_transfer.put_file() remote_file_size = scp_transfer.remote_file_size() assert remote_file_size == 19 @@ -65,6 +59,19 @@ def test_md5_methods(scp_fixture): assert remote_md5 == md5_value assert scp_transfer.compare_md5() == True +def test_disconnect(scp_fixture): + """Terminate the SSH session.""" + ssh_conn, scp_transfer = scp_fixture + ssh_conn.disconnect() + + +def test_verify_space_available_get(scp_fixture_get): + ssh_conn, scp_transfer = scp_fixture_get + assert scp_transfer.verify_space_available() == True + # intentional make there not be enough space available + scp_transfer.file_size = 100000000000 + assert scp_transfer.verify_space_available() == False + def test_scp_get(scp_fixture_get): ssh_conn, scp_transfer = scp_fixture_get @@ -74,9 +81,9 @@ def test_scp_get(scp_fixture_get): else: scp_transfer.get_file() if scp_transfer.check_file_exists(): - assert True == True + assert True else: - assert False == True + assert False def test_md5_methods_get(scp_fixture_get): ssh_conn, scp_transfer = scp_fixture_get @@ -85,11 +92,6 @@ def test_md5_methods_get(scp_fixture_get): assert local_md5 == md5_value assert scp_transfer.compare_md5() == True -def test_disconnect(scp_fixture): - """Terminate the SSH session.""" - ssh_conn, scp_transfer = scp_fixture - ssh_conn.disconnect() - def test_disconnect_get(scp_fixture_get): """Terminate the SSH session.""" ssh_conn, scp_transfer = scp_fixture_get From d0df2e05e49edb417e0a93dcef34e4f7c659296f Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sat, 24 Feb 2018 19:44:44 -0800 Subject: [PATCH 07/29] Updating test suite --- tests/test_suite_alt.sh | 1 + tests/test_suite_tmp.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/test_suite_alt.sh b/tests/test_suite_alt.sh index 77a48fc92..041673737 100755 --- a/tests/test_suite_alt.sh +++ b/tests/test_suite_alt.sh @@ -41,6 +41,7 @@ echo "Starting tests...good luck:" \ && py.test -v test_netmiko_config.py --test_device hp_comware \ \ && echo "Juniper" \ +&& py.test -v test_netmiko_scp.py --test_device juniper_srx \ && 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 \ diff --git a/tests/test_suite_tmp.sh b/tests/test_suite_tmp.sh index 950d91e67..8889137bc 100755 --- a/tests/test_suite_tmp.sh +++ b/tests/test_suite_tmp.sh @@ -37,6 +37,7 @@ echo "Starting tests...good luck:" \ && py.test -v test_netmiko_config.py --test_device hp_procurve \ \ && echo "Juniper" \ +&& py.test -v test_netmiko_scp.py --test_device juniper_srx \ && 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 \ From 720b4f3e6e81bb1ed3c5dd2bd8f7d6a84fc7fe39 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sun, 25 Feb 2018 11:06:27 -0800 Subject: [PATCH 08/29] Adding Arista SCP support --- netmiko/arista/arista_ssh.py | 81 ++++++++++++++++++++++------------ netmiko/juniper/juniper_ssh.py | 27 +----------- netmiko/scp_handler.py | 30 ++++++++++++- netmiko/ssh_dispatcher.py | 4 +- 4 files changed, 85 insertions(+), 57 deletions(-) diff --git a/netmiko/arista/arista_ssh.py b/netmiko/arista/arista_ssh.py index f0605db34..fe7ec6206 100644 --- a/netmiko/arista/arista_ssh.py +++ b/netmiko/arista/arista_ssh.py @@ -35,30 +35,54 @@ def check_config_mode(self, check_string=')#', pattern=''): log.debug("check_config_mode: {0}".format(repr(output))) return check_string in output + def _enter_shell(self): + """Enter the Bourne Shell.""" + return self.send_command('bash', expect_string=r"[\$#]") + + def _return_cli(self): + """Return to the CLI.""" + return self.send_command('exit', expect_string=r"[#>]") + 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 __init__(self, ssh_conn, source_file, dest_file, file_system="/mnt/flash", direction='put'): + return super(AristaFileTransfer, self).__init__(ssh_conn=ssh_conn, + source_file=source_file, + dest_file=dest_file, + file_system=file_system, + direction=direction) + + def remote_space_available(self, search_pattern=""): + """Return space available on remote device.""" + self.ssh_ctl_chan._enter_shell() + 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 1K-blocks Used Available Use% Mounted on + # /dev/sda1 3933548 510808 3422740 13% /mnt/flash + remote_output = remote_output.strip() + output_lines = remote_output.splitlines() + + # First line is the header; second is the actual file system info + header_line = output_lines[0] + filesystem_line = output_lines[1] + + if 'Filesystem' not in header_line or 'Avail' not in header_line.split()[3]: + # Filesystem 1K-blocks Used Available Use% 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) + + self.ssh_ctl_chan._return_cli() + return int(space_available) * 1024 def put_file(self): """SCP copy the file from the local system to the remote device.""" @@ -67,12 +91,6 @@ def put_file(self): # 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( @@ -81,7 +99,14 @@ def verify_space_available(self, search_pattern=r"(\d+) bytes free"): def check_file_exists(self, remote_cmd=""): """Check if the dest_file already exists on the file system (return boolean).""" - raise NotImplementedError + if self.direction == 'put': + self.ssh_ctl_chan._enter_shell() + remote_cmd = "ls {}".format(self.file_system) + remote_out = self.ssh_ctl_chan.send_command(remote_cmd, expect_string=r"[\$#]") + self.ssh_ctl_chan._return_cli() + 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.""" diff --git a/netmiko/juniper/juniper_ssh.py b/netmiko/juniper/juniper_ssh.py index a7a1150fe..23cf19836 100644 --- a/netmiko/juniper/juniper_ssh.py +++ b/netmiko/juniper/juniper_ssh.py @@ -215,11 +215,6 @@ def __init__(self, ssh_conn, source_file, dest_file, file_system="/var/tmp", dir else: raise ValueError("Invalid direction specified") - 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.""" self.ssh_ctl_chan._enter_shell() @@ -264,27 +259,7 @@ 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: - if self.direction == 'put': - remote_file = self.dest_file - elif self.direction == 'get': - remote_file = self.source_file - remote_file = "{}/{}".format(self.file_system, remote_file) - if not remote_cmd: - remote_cmd = "ls -l {}".format(remote_file) - - self.ssh_ctl_chan._enter_shell() - 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, flags=re.M) - 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] - - self.ssh_ctl_chan._return_cli() - return int(file_size) + self._remote_file_size_unix(remote_cmd=remote_cmd, remote_file=remote_file) def remote_md5(self, base_cmd='file checksum md5', remote_file=None): return super(JuniperFileTransfer, self).remote_md5(base_cmd=base_cmd, diff --git a/netmiko/scp_handler.py b/netmiko/scp_handler.py index 6f5a7170e..ee6307c24 100644 --- a/netmiko/scp_handler.py +++ b/netmiko/scp_handler.py @@ -59,8 +59,12 @@ def __init__(self, ssh_conn, source_file, dest_file, file_system=None, direction self.dest_file = dest_file self.direction = direction + ios_flag = 'cisco_ios' in ssh_conn.device_type or 'cisco_xe' in ssh_conn.device_type if not file_system: - self.file_system = self.ssh_ctl_chan._autodetect_fs() + if ios_flag: + self.file_system = self.ssh_ctl_chan._autodetect_fs() + else: + raise ValueError("Destination file system not specified") else: self.file_system = file_system @@ -155,6 +159,30 @@ def remote_file_size(self, remote_cmd="", remote_file=None): else: return int(file_size) + def _remote_file_size_unix(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 + remote_file = "{}/{}".format(self.file_system, remote_file) + if not remote_cmd: + remote_cmd = "ls -l {}".format(remote_file) + + self.ssh_ctl_chan._enter_shell() + 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, flags=re.M) + 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] + + self.ssh_ctl_chan._return_cli() + return int(file_size) + def file_md5(self, file_name): """Compute MD5 hash of file.""" with open(file_name, "rb") as f: diff --git a/netmiko/ssh_dispatcher.py b/netmiko/ssh_dispatcher.py index d97ca3abb..2d2577449 100644 --- a/netmiko/ssh_dispatcher.py +++ b/netmiko/ssh_dispatcher.py @@ -6,7 +6,7 @@ from netmiko.alcatel import AlcatelAosSSH from netmiko.alcatel import AlcatelSrosSSH from netmiko.arista import AristaSSH -# from netmiko.arista import AristaFileTransfer +from netmiko.arista import AristaFileTransfer from netmiko.aruba import ArubaSSH from netmiko.avaya import AvayaErsSSH from netmiko.avaya import AvayaVspSSH @@ -112,7 +112,7 @@ } FILE_TRANSFER_MAP = { - # 'arista_eos': AristaFileTransfer, + 'arista_eos': AristaFileTransfer, 'cisco_asa': CiscoAsaFileTransfer, 'cisco_ios': CiscoIosFileTransfer, 'cisco_xe': CiscoIosFileTransfer, From 334d8d5cbfe62c07d544a8e8eb01a4366c704106 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sun, 25 Feb 2018 11:06:41 -0800 Subject: [PATCH 09/29] Adding Arista SCP support --- tests/conftest.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1fd75d775..c967d37a0 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -94,7 +94,7 @@ def delete_file_ios(ssh_conn, dest_file_system, dest_file): raise ValueError("An error happened deleting file on Cisco IOS") -def delete_file_junos(ssh_conn, dest_file_system, dest_file): +def delete_file_generic(ssh_conn, dest_file_system, dest_file): """Delete a remote file for a Junos device.""" full_file_name = "{}/{}".format(dest_file_system, dest_file) cmd = "rm {}".format(full_file_name) @@ -120,7 +120,12 @@ def scp_fixture(request): 'juniper_junos': { 'file_system': '/var/tmp', 'enable_scp': False, - 'delete_file': delete_file_junos, + 'delete_file': delete_file_generic, + }, + 'arista_eos': { + 'file_system': '/mnt/flash', + 'enable_scp': False, + 'delete_file': delete_file_generic, }, } From df1620f822740a39c217998862e4ddff10a32aaf Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sun, 25 Feb 2018 12:17:31 -0800 Subject: [PATCH 10/29] Implementing Arista SCP including unit tests --- netmiko/arista/arista_ssh.py | 81 ++++------------------------------ netmiko/juniper/juniper_ssh.py | 62 ++++---------------------- netmiko/scp_handler.py | 42 ++++++++++++++++++ tests/conftest.py | 7 ++- tests/test_netmiko_scp.py | 2 +- 5 files changed, 65 insertions(+), 129 deletions(-) diff --git a/netmiko/arista/arista_ssh.py b/netmiko/arista/arista_ssh.py index fe7ec6206..db9405d71 100644 --- a/netmiko/arista/arista_ssh.py +++ b/netmiko/arista/arista_ssh.py @@ -1,6 +1,5 @@ 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 @@ -55,41 +54,7 @@ def __init__(self, ssh_conn, source_file, dest_file, file_system="/mnt/flash", d def remote_space_available(self, search_pattern=""): """Return space available on remote device.""" - self.ssh_ctl_chan._enter_shell() - 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 1K-blocks Used Available Use% Mounted on - # /dev/sda1 3933548 510808 3422740 13% /mnt/flash - remote_output = remote_output.strip() - output_lines = remote_output.splitlines() - - # First line is the header; second is the actual file system info - header_line = output_lines[0] - filesystem_line = output_lines[1] - - if 'Filesystem' not in header_line or 'Avail' not in header_line.split()[3]: - # Filesystem 1K-blocks Used Available Use% 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) - - self.ssh_ctl_chan._return_cli() - return int(space_available) * 1024 - - 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() + return self._remote_space_available_unix(search_pattern=search_pattern) def verify_space_available(self, search_pattern=r"(\d+) bytes free"): """Verify sufficient space is available on destination file system (return boolean).""" @@ -99,52 +64,22 @@ def verify_space_available(self, search_pattern=r"(\d+) bytes free"): 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._enter_shell() - remote_cmd = "ls {}".format(self.file_system) - remote_out = self.ssh_ctl_chan.send_command(remote_cmd, expect_string=r"[\$#]") - self.ssh_ctl_chan._return_cli() - return self.dest_file in remote_out - elif self.direction == 'get': - return os.path.exists(self.dest_file) + return self._check_file_exists_unix(remote_cmd=remote_cmd) def remote_file_size(self, remote_cmd="", remote_file=None): """Get the file size of the remote file.""" - 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 + return self._remote_file_size_unix(remote_cmd=remote_cmd, remote_file=remote_file) - def remote_md5(self, base_cmd='show file', remote_file=None): + def remote_md5(self, base_cmd='verify /md5', 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) + remote_md5_cmd = "{} file:{}/{}".format(base_cmd, self.file_system, remote_file) + 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 def enable_scp(self, cmd=None): raise NotImplementedError diff --git a/netmiko/juniper/juniper_ssh.py b/netmiko/juniper/juniper_ssh.py index 23cf19836..52bedd8ec 100644 --- a/netmiko/juniper/juniper_ssh.py +++ b/netmiko/juniper/juniper_ssh.py @@ -2,7 +2,6 @@ import re import time -import os from netmiko.base_connection import BaseConnection from netmiko.scp_handler import BaseFileTransfer @@ -198,68 +197,23 @@ def strip_context_items(self, 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'): - self.ssh_ctl_chan = ssh_conn - self.source_file = source_file - self.dest_file = dest_file - self.direction = direction - - self.file_system = file_system - - if direction == 'put': - self.source_md5 = self.file_md5(source_file) - self.file_size = os.stat(self.source_file).st_size - elif direction == 'get': - self.source_file = 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") + return super(JuniperFileTransfer, self).__init__(ssh_conn=ssh_conn, + source_file=source_file, + dest_file=dest_file, + file_system=file_system, + direction=direction) def remote_space_available(self, search_pattern=""): """Return space available on remote device.""" - self.ssh_ctl_chan._enter_shell() - 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() - output_lines = remote_output.splitlines() - - # First line is the header; second is the actual file system info - header_line = output_lines[0] - filesystem_line = output_lines[1] - - if 'Filesystem' not in header_line or 'Avail' not in header_line.split()[3]: - # Filesystem 512-blocks Used Avail Capacity Mounted on - 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) - - self.ssh_ctl_chan._return_cli() - return int(space_available) * 1024 + return self._remote_space_available_unix(search_pattern=search_pattern) def check_file_exists(self, remote_cmd=""): """Check if the dest_file already exists on the file system (return boolean).""" - if self.direction == 'put': - self.ssh_ctl_chan._enter_shell() - remote_cmd = "ls {}".format(self.file_system) - remote_out = self.ssh_ctl_chan.send_command(remote_cmd, expect_string=r"[\$#]") - self.ssh_ctl_chan._return_cli() - return self.dest_file in remote_out - elif self.direction == 'get': - return os.path.exists(self.dest_file) + return self._check_file_exists_unix(remote_cmd=remote_cmd) def remote_file_size(self, remote_cmd="", remote_file=None): """Get the file size of the remote file.""" - self._remote_file_size_unix(remote_cmd=remote_cmd, remote_file=remote_file) + return self._remote_file_size_unix(remote_cmd=remote_cmd, remote_file=remote_file) def remote_md5(self, base_cmd='file checksum md5', remote_file=None): return super(JuniperFileTransfer, self).remote_md5(base_cmd=base_cmd, diff --git a/netmiko/scp_handler.py b/netmiko/scp_handler.py index ee6307c24..caa6c5fe2 100644 --- a/netmiko/scp_handler.py +++ b/netmiko/scp_handler.py @@ -102,6 +102,37 @@ def remote_space_available(self, search_pattern=r"(\d+) bytes free"): match = re.search(search_pattern, remote_output) return int(match.group(1)) + def _remote_space_available_unix(self, search_pattern=""): + """Return space available on *nix system (BSD/Linux).""" + self.ssh_ctl_chan._enter_shell() + 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() + output_lines = remote_output.splitlines() + + # First line is the header; second is the actual file system info + header_line = output_lines[0] + filesystem_line = output_lines[1] + + if 'Filesystem' not in header_line or 'Avail' not in header_line.split()[3]: + # Filesystem 512-blocks Used Avail Capacity Mounted on + 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) + + self.ssh_ctl_chan._return_cli() + return int(space_available) * 1024 + def local_space_available(self): """Return space available on local filesystem.""" destination_stats = os.statvfs(".") @@ -133,6 +164,17 @@ def check_file_exists(self, remote_cmd=""): elif self.direction == 'get': return os.path.exists(self.dest_file) + def _check_file_exists_unix(self, remote_cmd=""): + """Check if the dest_file already exists on the file system (return boolean).""" + if self.direction == 'put': + self.ssh_ctl_chan._enter_shell() + remote_cmd = "ls {}".format(self.file_system) + remote_out = self.ssh_ctl_chan.send_command(remote_cmd, expect_string=r"[\$#]") + self.ssh_ctl_chan._return_cli() + 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: diff --git a/tests/conftest.py b/tests/conftest.py index c967d37a0..c6eb62563 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -174,7 +174,12 @@ def scp_fixture_get(request): 'juniper_junos': { 'file_system': '/var/tmp', 'enable_scp': False, - 'delete_file': delete_file_junos, + 'delete_file': delete_file_generic, + }, + 'arista_eos': { + 'file_system': '/mnt/flash', + 'enable_scp': False, + 'delete_file': delete_file_generic, }, } diff --git a/tests/test_netmiko_scp.py b/tests/test_netmiko_scp.py index c75bf55aa..885b3a8ff 100755 --- a/tests/test_netmiko_scp.py +++ b/tests/test_netmiko_scp.py @@ -41,7 +41,7 @@ def test_verify_space_available_put(scp_fixture): ssh_conn, scp_transfer = scp_fixture assert scp_transfer.verify_space_available() == True # intentional make there not be enough space available - scp_transfer.file_size = 1000000000 + scp_transfer.file_size = 10000000000 assert scp_transfer.verify_space_available() == False def test_remote_file_size(scp_fixture): From e6d7c140a082685666dd6cb0b8b7ba681de4b6b1 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sun, 25 Feb 2018 12:19:11 -0800 Subject: [PATCH 11/29] Updating test suite --- tests/test_suite_alt.sh | 1 + tests/test_suite_tmp.sh | 72 ----------------------------------------- 2 files changed, 1 insertion(+), 72 deletions(-) delete mode 100755 tests/test_suite_tmp.sh diff --git a/tests/test_suite_alt.sh b/tests/test_suite_alt.sh index 041673737..04751b0b4 100755 --- a/tests/test_suite_alt.sh +++ b/tests/test_suite_alt.sh @@ -29,6 +29,7 @@ echo "Starting tests...good luck:" \ && py.test -v test_netmiko_config.py --test_device cisco_s300 \ \ && echo "Arista" \ +&& py.test -v test_netmiko_scp.py --test_device arista_sw4 \ && py.test -v test_netmiko_show.py --test_device arista_sw4 \ && py.test -v test_netmiko_config.py --test_device arista_sw4 \ \ diff --git a/tests/test_suite_tmp.sh b/tests/test_suite_tmp.sh deleted file mode 100755 index 8889137bc..000000000 --- a/tests/test_suite_tmp.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/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 using SSH config with SSH Proxy" \ -&& py.test -v test_netmiko_show.py --test_device cisco881_ssh_config \ -&& py.test -v test_netmiko_config.py --test_device cisco881_ssh_config \ -\ -&& 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_scp.py --test_device juniper_srx \ -&& 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 "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 \ -&& 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 From 2a0ce80e2f9e64a495ae5a013ba1d2541c6187c8 Mon Sep 17 00:00:00 2001 From: Daniel DiSisto Date: Tue, 27 Feb 2018 10:25:16 +1100 Subject: [PATCH 12/29] fix base_connection is_alive() to work with telnet connections --- netmiko/base_connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netmiko/base_connection.py b/netmiko/base_connection.py index 549814893..9c9489482 100644 --- a/netmiko/base_connection.py +++ b/netmiko/base_connection.py @@ -283,7 +283,7 @@ def is_alive(self): # Try sending IAC + NOP (IAC is telnet way of sending command # IAC = Interpret as Command (it comes before the NOP) log.debug("Sending IAC + NOP") - self.device.write_channel(telnetlib.IAC + telnetlib.NOP) + self.write_channel(telnetlib.IAC + telnetlib.NOP) return True except AttributeError: return False From 60991287c1b95041acc0b782b6740502496a833b Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Tue, 27 Feb 2018 21:07:20 -0800 Subject: [PATCH 13/29] Fixing SCP unit tests for NXOS --- netmiko/cisco/cisco_nxos_ssh.py | 14 +++++++++++++- tests/conftest.py | 30 ++++++++++++++++++++++++++++++ tests/test_netmiko_scp.py | 1 - tests/test_suite_alt.sh | 1 + 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/netmiko/cisco/cisco_nxos_ssh.py b/netmiko/cisco/cisco_nxos_ssh.py index 0e75a7ff6..9a29740a4 100644 --- a/netmiko/cisco/cisco_nxos_ssh.py +++ b/netmiko/cisco/cisco_nxos_ssh.py @@ -49,7 +49,19 @@ def __init__(self, ssh_conn, source_file, dest_file, file_system='bootflash:', d def check_file_exists(self, remote_cmd=""): """Check if the dest_file already exists on the file system (return boolean).""" - raise NotImplementedError + if self.direction == 'put': + if not remote_cmd: + remote_cmd = "dir {}{}".format(self.file_system, self.dest_file) + remote_out = self.ssh_ctl_chan.send_command_expect(remote_cmd) + search_string = r"{}.*Usage for".format(self.dest_file) + if 'No such file or directory' in remote_out: + return False + elif re.search(search_string, remote_out, flags=re.DOTALL): + return True + else: + raise ValueError("Unexpected output from check_file_exists") + 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.""" diff --git a/tests/conftest.py b/tests/conftest.py index c6eb62563..e4effbca8 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -71,6 +71,26 @@ def commands(request): commands_yml = parse_yaml(PWD + "/etc/commands.yml") return commands_yml[test_platform] +def delete_file_nxos(ssh_conn, dest_file_system, dest_file): + """ + nxos1# delete bootflash:test2.txt + Do you want to delete "/test2.txt" ? (yes/no/abort) [y] y + """ + if not dest_file_system: + raise ValueError("Invalid file system specified") + if not dest_file: + raise ValueError("Invalid dest file specified") + + full_file_name = "{}{}".format(dest_file_system, dest_file) + + cmd = "delete {}".format(full_file_name) + output = ssh_conn.send_command_timing(cmd) + if 'yes/no/abort' in output and dest_file in output: + output += ssh_conn.send_command_timing("y", strip_command=False, strip_prompt=False) + return output + else: + output += ssh_conn.send_command_timing("abort") + raise ValueError("An error happened deleting file on Cisco NX-OS") def delete_file_ios(ssh_conn, dest_file_system, dest_file): """Delete a remote file for a Cisco IOS device.""" @@ -127,6 +147,11 @@ def scp_fixture(request): 'enable_scp': False, 'delete_file': delete_file_generic, }, + 'cisco_nxos': { + 'file_system': 'bootflash:', + 'enable_scp': False, + 'delete_file': delete_file_nxos, + }, } device_under_test = request.config.getoption('test_device') @@ -181,6 +206,11 @@ def scp_fixture_get(request): 'enable_scp': False, 'delete_file': delete_file_generic, }, + 'cisco_nxos': { + 'file_system': 'bootflash:', + 'enable_scp': False, + 'delete_file': delete_file_nxos, + }, } device_under_test = request.config.getoption('test_device') diff --git a/tests/test_netmiko_scp.py b/tests/test_netmiko_scp.py index 885b3a8ff..efe5c59d8 100755 --- a/tests/test_netmiko_scp.py +++ b/tests/test_netmiko_scp.py @@ -64,7 +64,6 @@ def test_disconnect(scp_fixture): ssh_conn, scp_transfer = scp_fixture ssh_conn.disconnect() - def test_verify_space_available_get(scp_fixture_get): ssh_conn, scp_transfer = scp_fixture_get assert scp_transfer.verify_space_available() == True diff --git a/tests/test_suite_alt.sh b/tests/test_suite_alt.sh index 04751b0b4..8ed74ac8d 100755 --- a/tests/test_suite_alt.sh +++ b/tests/test_suite_alt.sh @@ -59,6 +59,7 @@ echo "Starting tests...good luck:" \ && py.test -v test_netmiko_commit.py --test_device cisco_xrv \ \ && echo "Cisco NXOS" \ +&& py.test -v test_netmiko_scp.py --test_device nxos1 \ && py.test -v test_netmiko_show.py --test_device nxos1 \ && py.test -v test_netmiko_config.py --test_device nxos1 \ \ From 6e1c02687dcd8c3d91115736c6f0f4534f6b2fd5 Mon Sep 17 00:00:00 2001 From: Octopus zhang <1004988384@qq.com> Date: Thu, 1 Mar 2018 17:11:02 +0800 Subject: [PATCH 14/29] add lost parm in cisco_300 confirm_response is necessary here because lost --- netmiko/cisco/cisco_s300.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netmiko/cisco/cisco_s300.py b/netmiko/cisco/cisco_s300.py index f183f8a3b..ee95f8af1 100644 --- a/netmiko/cisco/cisco_s300.py +++ b/netmiko/cisco/cisco_s300.py @@ -23,4 +23,5 @@ def session_preparation(self): time.sleep(.3 * self.global_delay_factor) def save_config(self, cmd='write memory', confirm=True, confirm_response='Y'): - return super(CiscoS300SSH, self).save_config(cmd=cmd, confirm=confirm) + return super(CiscoS300SSH, self).save_config(cmd=cmd, confirm=confirm, + confirm_response=confirm_response) From 942261388ddea910848b288052b6c404353917f3 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sat, 3 Mar 2018 09:59:36 -0800 Subject: [PATCH 15/29] Initial implementation of IOS-XR file transfer SCP --- netmiko/cisco/__init__.py | 4 ++-- netmiko/cisco/cisco_xr_ssh.py | 40 +++++++++++++++++++++++++++++++++++ netmiko/scp_handler.py | 8 ++++--- netmiko/ssh_dispatcher.py | 5 +++-- 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/netmiko/cisco/__init__.py b/netmiko/cisco/__init__.py index d0ccda518..61a7d5989 100644 --- a/netmiko/cisco/__init__.py +++ b/netmiko/cisco/__init__.py @@ -4,7 +4,7 @@ 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_xr_ssh import CiscoXrSSH, CiscoXrFileTransfer from netmiko.cisco.cisco_wlc_ssh import CiscoWlcSSH from netmiko.cisco.cisco_s300 import CiscoS300SSH from netmiko.cisco.cisco_tp_tcce import CiscoTpTcCeSSH @@ -12,4 +12,4 @@ __all__ = ['CiscoIosSSH', 'CiscoIosTelnet', 'CiscoAsaSSH', 'CiscoNxosSSH', 'CiscoXrSSH', 'CiscoWlcSSH', 'CiscoS300SSH', 'CiscoTpTcCeSSH', 'CiscoIosBase', 'CiscoIosFileTransfer', 'InLineTransfer', 'CiscoAsaFileTransfer', - 'CiscoNxosFileTransfer', 'CiscoIosSerial'] + 'CiscoNxosFileTransfer', 'CiscoIosSerial', 'CiscoXrFileTransfer'] diff --git a/netmiko/cisco/cisco_xr_ssh.py b/netmiko/cisco/cisco_xr_ssh.py index 2c2df3ba5..23cc92f05 100644 --- a/netmiko/cisco/cisco_xr_ssh.py +++ b/netmiko/cisco/cisco_xr_ssh.py @@ -129,3 +129,43 @@ def exit_config_mode(self, exit_config='end'): def save_config(self): """Not Implemented (use commit() method)""" raise NotImplementedError + + +class CiscoXrFileTransfer(CiscoFileTransfer): + """Cisco IOS-XR SCP File Transfer driver.""" + def process_md5(md5_output, pattern=r"^([a-fA-F0-9]+)$"): + """ + IOS-XR defaults with timestamps enabled + + # show md5 file /bootflash:/boot/grub/grub.cfg + Sat Mar 3 17:49:03.596 UTC + c84843f0030efd44b01343fdb8c2e801 + """ + match = re.search(pattern, md5_output) + if match: + return match.group(1) + else: + raise ValueError("Invalid output from MD5 command: {}".format(md5_output)) + + def remote_md5(self, base_cmd='show md5 file', remote_file=None): + """ + IOS-XR for MD5 requires this extra leading / + + show md5 file /bootflash:/boot/grub/grub.cfg + """ + if remote_file is None: + if self.direction == 'put': + remote_file = self.dest_file + elif self.direction == 'get': + remote_file = self.source_file + remote_md5_cmd = "{} /{}{}".format(base_cmd, self.file_system, remote_file) + print(remote_md5_cmd) +# 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 + + def enable_scp(self, cmd=None): + raise NotImplementedError + + def disable_scp(self, cmd=None): + raise NotImplementedError diff --git a/netmiko/scp_handler.py b/netmiko/scp_handler.py index caa6c5fe2..9f98fd8a4 100644 --- a/netmiko/scp_handler.py +++ b/netmiko/scp_handler.py @@ -59,9 +59,11 @@ def __init__(self, ssh_conn, source_file, dest_file, file_system=None, direction self.dest_file = dest_file self.direction = direction - ios_flag = 'cisco_ios' in ssh_conn.device_type or 'cisco_xe' in ssh_conn.device_type + auto_flag = 'cisco_ios' in ssh_conn.device_type or \ + 'cisco_xe' in ssh_conn.device_type or \ + 'cisco_xr' in ssh_conn.device_type: if not file_system: - if ios_flag: + if auto_flag: self.file_system = self.ssh_ctl_chan._autodetect_fs() else: raise ValueError("Destination file system not specified") @@ -196,7 +198,7 @@ def remote_file_size(self, remote_cmd="", remote_file=None): line = match.group(0) # Format will be 26 -rw- 6738 Jul 30 2016 19:49:50 -07:00 filename file_size = line.split()[2] - if 'Error opening' in remote_out: + if 'Error opening' in remote_out or 'No such file or directory' in remote_out: raise IOError("Unable to find file on remote system") else: return int(file_size) diff --git a/netmiko/ssh_dispatcher.py b/netmiko/ssh_dispatcher.py index 2d2577449..0737971f1 100644 --- a/netmiko/ssh_dispatcher.py +++ b/netmiko/ssh_dispatcher.py @@ -22,7 +22,7 @@ from netmiko.cisco import CiscoS300SSH from netmiko.cisco import CiscoTpTcCeSSH from netmiko.cisco import CiscoWlcSSH -from netmiko.cisco import CiscoXrSSH +from netmiko.cisco import CiscoXrSSH, CiscoXrFileTransfer from netmiko.coriant import CoriantSSH from netmiko.dell import DellForce10SSH from netmiko.dell import DellPowerConnectSSH @@ -115,8 +115,9 @@ 'arista_eos': AristaFileTransfer, 'cisco_asa': CiscoAsaFileTransfer, 'cisco_ios': CiscoIosFileTransfer, - 'cisco_xe': CiscoIosFileTransfer, 'cisco_nxos': CiscoNxosFileTransfer, + 'cisco_xe': CiscoIosFileTransfer, + 'cisco_xr': CiscoXrFileTransfer, 'juniper_junos': JuniperFileTransfer, } From 5d91e037237fc0b79f6fee1c4d1435473f76d6ba Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sat, 3 Mar 2018 09:59:45 -0800 Subject: [PATCH 16/29] Initial implementation of IOS-XR file transfer SCP --- netmiko/cisco_base_connection.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/netmiko/cisco_base_connection.py b/netmiko/cisco_base_connection.py index 1dea9d77b..344faa611 100644 --- a/netmiko/cisco_base_connection.py +++ b/netmiko/cisco_base_connection.py @@ -155,7 +155,10 @@ def _autodetect_fs(self, cmd='dir', pattern=r'Directory of (.*)/'): # Test file_system cmd = "dir {}".format(file_system) output = self.send_command_expect(cmd) - if '% Invalid' not in output: + if '% Invalid' in output or '%Error:' in output: + raise ValueError("An error occurred in dynamically determining remote file " + "system: {} {}".format(cmd, output)) + else: return file_system raise ValueError("An error occurred in dynamically determining remote file " From 3c43f814752dec8c0c1bee72ee5337bca1bbdc15 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sat, 3 Mar 2018 12:56:13 -0800 Subject: [PATCH 17/29] Adding IOS-XR SCP support --- netmiko/cisco/cisco_xr_ssh.py | 16 +++++---- netmiko/scp_handler.py | 6 ++-- tests/conftest.py | 11 +++++++ tests/test_netmiko_scp.py | 62 +++++++++++++++++------------------ tests/test_suite_alt.sh | 1 + 5 files changed, 55 insertions(+), 41 deletions(-) diff --git a/netmiko/cisco/cisco_xr_ssh.py b/netmiko/cisco/cisco_xr_ssh.py index 23cc92f05..d404044c7 100644 --- a/netmiko/cisco/cisco_xr_ssh.py +++ b/netmiko/cisco/cisco_xr_ssh.py @@ -1,7 +1,8 @@ from __future__ import print_function from __future__ import unicode_literals import time -from netmiko.cisco_base_connection import CiscoSSHConnection +import re +from netmiko.cisco_base_connection import CiscoSSHConnection, CiscoFileTransfer class CiscoXrSSH(CiscoSSHConnection): @@ -133,7 +134,7 @@ def save_config(self): class CiscoXrFileTransfer(CiscoFileTransfer): """Cisco IOS-XR SCP File Transfer driver.""" - def process_md5(md5_output, pattern=r"^([a-fA-F0-9]+)$"): + def process_md5(self, md5_output, pattern=r"^([a-fA-F0-9]+)$"): """ IOS-XR defaults with timestamps enabled @@ -141,7 +142,7 @@ def process_md5(md5_output, pattern=r"^([a-fA-F0-9]+)$"): Sat Mar 3 17:49:03.596 UTC c84843f0030efd44b01343fdb8c2e801 """ - match = re.search(pattern, md5_output) + match = re.search(pattern, md5_output, flags=re.M) if match: return match.group(1) else: @@ -158,10 +159,11 @@ def remote_md5(self, base_cmd='show md5 file', remote_file=None): remote_file = self.dest_file elif self.direction == 'get': remote_file = self.source_file - remote_md5_cmd = "{} /{}{}".format(base_cmd, self.file_system, remote_file) - print(remote_md5_cmd) -# dest_md5 = self.ssh_ctl_chan.send_command(remote_md5_cmd, delay_factor=3.0) - #dest_md5 = self.process_md5(dest_md5) + # IOS-XR requires both the leading slash and the slash between file-system and file here + remote_md5_cmd = "{} /{}/{}".format(base_cmd, self.file_system, remote_file) + dest_md5 = self.ssh_ctl_chan.send_command(remote_md5_cmd, delay_factor=3.0) + dest_md5 = self.process_md5(dest_md5) + print(dest_md5) return dest_md5 def enable_scp(self, cmd=None): diff --git a/netmiko/scp_handler.py b/netmiko/scp_handler.py index 9f98fd8a4..cddb0a97f 100644 --- a/netmiko/scp_handler.py +++ b/netmiko/scp_handler.py @@ -61,7 +61,7 @@ def __init__(self, ssh_conn, source_file, dest_file, file_system=None, direction auto_flag = 'cisco_ios' in ssh_conn.device_type or \ 'cisco_xe' in ssh_conn.device_type or \ - 'cisco_xr' in ssh_conn.device_type: + 'cisco_xr' in ssh_conn.device_type if not file_system: if auto_flag: self.file_system = self.ssh_ctl_chan._autodetect_fs() @@ -157,9 +157,9 @@ def check_file_exists(self, remote_cmd=""): remote_cmd = "dir {}/{}".format(self.file_system, self.dest_file) remote_out = self.ssh_ctl_chan.send_command_expect(remote_cmd) search_string = r"Directory of .*{0}".format(self.dest_file) - if 'Error opening' in remote_out: + if 'Error opening' in remote_out or 'No such file or directory' in remote_out: return False - elif re.search(search_string, remote_out): + elif re.search(search_string, remote_out, flags=re.DOTALL): return True else: raise ValueError("Unexpected output from check_file_exists") diff --git a/tests/conftest.py b/tests/conftest.py index e4effbca8..0b089683a 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -152,6 +152,12 @@ def scp_fixture(request): 'enable_scp': False, 'delete_file': delete_file_nxos, }, + 'cisco_xr': { + 'file_system': 'disk0:', + 'enable_scp': False, + # Delete pattern is the same on IOS-XR + 'delete_file': delete_file_ios, + }, } device_under_test = request.config.getoption('test_device') @@ -211,6 +217,11 @@ def scp_fixture_get(request): 'enable_scp': False, 'delete_file': delete_file_nxos, }, + 'cisco_xr': { + 'file_system': 'disk0:', + 'enable_scp': False, + 'delete_file': delete_file_ios, + }, } device_under_test = request.config.getoption('test_device') diff --git a/tests/test_netmiko_scp.py b/tests/test_netmiko_scp.py index efe5c59d8..9d62af8dc 100755 --- a/tests/test_netmiko_scp.py +++ b/tests/test_netmiko_scp.py @@ -64,34 +64,34 @@ def test_disconnect(scp_fixture): ssh_conn, scp_transfer = scp_fixture ssh_conn.disconnect() -def test_verify_space_available_get(scp_fixture_get): - ssh_conn, scp_transfer = scp_fixture_get - assert scp_transfer.verify_space_available() == True - # intentional make there not be enough space available - scp_transfer.file_size = 100000000000 - assert scp_transfer.verify_space_available() == False - -def test_scp_get(scp_fixture_get): - ssh_conn, scp_transfer = scp_fixture_get - - if scp_transfer.check_file_exists(): - # File should not already exist - assert False - else: - scp_transfer.get_file() - if scp_transfer.check_file_exists(): - assert True - else: - assert False - -def test_md5_methods_get(scp_fixture_get): - ssh_conn, scp_transfer = scp_fixture_get - md5_value = 'd8df36973ff832b564ad84642d07a261' - local_md5 = scp_transfer.file_md5("test9.txt") - assert local_md5 == md5_value - assert scp_transfer.compare_md5() == True - -def test_disconnect_get(scp_fixture_get): - """Terminate the SSH session.""" - ssh_conn, scp_transfer = scp_fixture_get - ssh_conn.disconnect() +###def test_verify_space_available_get(scp_fixture_get): +### ssh_conn, scp_transfer = scp_fixture_get +### assert scp_transfer.verify_space_available() == True +### # intentional make there not be enough space available +### scp_transfer.file_size = 100000000000 +### assert scp_transfer.verify_space_available() == False +### +###def test_scp_get(scp_fixture_get): +### ssh_conn, scp_transfer = scp_fixture_get +### +### if scp_transfer.check_file_exists(): +### # File should not already exist +### assert False +### else: +### scp_transfer.get_file() +### if scp_transfer.check_file_exists(): +### assert True +### else: +### assert False +### +###def test_md5_methods_get(scp_fixture_get): +### ssh_conn, scp_transfer = scp_fixture_get +### md5_value = 'd8df36973ff832b564ad84642d07a261' +### local_md5 = scp_transfer.file_md5("test9.txt") +### assert local_md5 == md5_value +### assert scp_transfer.compare_md5() == True +### +###def test_disconnect_get(scp_fixture_get): +### """Terminate the SSH session.""" +### ssh_conn, scp_transfer = scp_fixture_get +### ssh_conn.disconnect() diff --git a/tests/test_suite_alt.sh b/tests/test_suite_alt.sh index 8ed74ac8d..ed5f43986 100755 --- a/tests/test_suite_alt.sh +++ b/tests/test_suite_alt.sh @@ -54,6 +54,7 @@ echo "Starting tests...good luck:" \ && py.test -v test_netmiko_config.py --test_device cisco_asa_login \ \ && echo "Cisco IOS-XR" \ +&& py.test -v test_netmiko_scp.py --test_device cisco_xrv \ && 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 \ From 6705c5981f17793565e74add6df8c6b772d61551 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sat, 3 Mar 2018 15:41:34 -0800 Subject: [PATCH 18/29] Adding reliable file transfer function --- netmiko/__init__.py | 5 ++- netmiko/scp_functions.py | 93 ++++++++++++++++++++++++++++++++++++++++ tests/test_suite_tmp.sh | 75 ++++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 netmiko/scp_functions.py create mode 100755 tests/test_suite_tmp.sh diff --git a/netmiko/__init__.py b/netmiko/__init__.py index e3fa88cd4..4341f58fe 100644 --- a/netmiko/__init__.py +++ b/netmiko/__init__.py @@ -16,17 +16,18 @@ from netmiko.ssh_exception import NetMikoAuthenticationException from netmiko.ssh_autodetect import SSHDetect from netmiko.base_connection import BaseConnection +from netmiko.scp_functions import reliable_file_transfer # Alternate naming NetmikoTimeoutError = NetMikoTimeoutException NetmikoAuthError = NetMikoAuthenticationException Netmiko = ConnectHandler -__version__ = '2.0.2' +__version__ = '2.0.3' __all__ = ('ConnectHandler', 'ssh_dispatcher', 'platforms', 'SCPConn', 'FileTransfer', 'NetMikoTimeoutException', 'NetMikoAuthenticationException', 'NetmikoTimeoutError', 'NetmikoAuthError', 'InLineTransfer', 'redispatch', - 'SSHDetect', 'BaseConnection', 'Netmiko') + 'SSHDetect', 'BaseConnection', 'Netmiko', 'reliable_file_transfer') # Cisco cntl-shift-six sequence CNTL_SHIFT_6 = chr(30) diff --git a/netmiko/scp_functions.py b/netmiko/scp_functions.py new file mode 100644 index 000000000..6947faffc --- /dev/null +++ b/netmiko/scp_functions.py @@ -0,0 +1,93 @@ +""" +Netmiko SCP operations. + +Supports file get and file put operations. + +SCP requires a separate SSH connection for a control channel. + +Currently only supports Cisco IOS and Cisco ASA. +""" +from __future__ import print_function +from __future__ import unicode_literals + +from netmiko import FileTransfer, InLineTransfer + + +def verifyspace_and_transferfile(scp_transfer): + """Verify space and transfer file.""" + if not scp_transfer.verify_space_available(): + raise ValueError("Insufficient space available on remote device") + scp_transfer.transfer_file() + + +def reliable_file_transfer(ssh_conn, source_file, dest_file, file_system=None, direction='put', + disable_md5=False, inline_transfer=False, overwrite_file=False): + """Use Secure Copy or Inline (IOS-only) to transfer files to/from network devices. + + return { + 'file_exists': boolean, + 'file_transferred': boolean, + 'file_verified': boolean, + } + """ + transferred_and_verified = { + 'file_exists': True, + 'file_transferred': True, + 'file_verified': True, + } + transferred_and_notverified = { + 'file_exists': True, + 'file_transferred': True, + 'file_verified': False, + } + nottransferred_but_verified = { + 'file_exists': True, + 'file_transferred': False, + 'file_verified': True, + } + + if 'cisco_ios' in ssh_conn.device_type or 'cisco_xe' in ssh_conn.device_type: + cisco_ios = True + else: + cisco_ios = False + if not cisco_ios and inline_transfer: + raise ValueError("Inline Transfer only supported for Cisco IOS/Cisco IOS-XE") + + TransferClass = InLineTransfer if inline_transfer else FileTransfer + with TransferClass(ssh_conn, source_file=source_file, dest_file=dest_file, + file_system=file_system, direction=direction) as scp_transfer: + + if scp_transfer.check_file_exists(): + if overwrite_file: + if not disable_md5: + if scp_transfer.compare_md5(): + return nottransferred_but_verified + else: + # File exists, you can overwrite it, MD5 is wrong (transfer file) + verifyspace_and_transferfile(scp_transfer) + if scp_transfer.compare_md5(): + return transferred_and_verified + else: + raise ValueError("MD5 failure between source and destination files") + else: + # File exists, you can overwrite it, but MD5 not allowed (transfer file) + verifyspace_and_transferfile(scp_transfer) + return transferred_and_notverified + else: + # File exists, but you can't overwrite it. + if not disable_md5: + if scp_transfer.compare_md5(): + return nottransferred_but_verified + else: + msg = "File already exists and overwrite_file is disabled" + raise ValueError(msg) + else: + verifyspace_and_transferfile(scp_transfer) + # File doesn't exist + if not disable_md5: + if scp_transfer.compare_md5(): + return transferred_and_verified + else: + raise ValueError("MD5 failure between source and destination files") + else: + return transferred_and_notverified diff --git a/tests/test_suite_tmp.sh b/tests/test_suite_tmp.sh new file mode 100755 index 000000000..a283e368c --- /dev/null +++ b/tests/test_suite_tmp.sh @@ -0,0 +1,75 @@ +#!/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 using SSH config with SSH Proxy" \ +&& py.test -v test_netmiko_show.py --test_device cisco881_ssh_config \ +&& py.test -v test_netmiko_config.py --test_device cisco881_ssh_config \ +\ +&& 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_scp.py --test_device arista_sw4 \ +&& 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_scp.py --test_device juniper_srx \ +&& 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_scp.py --test_device cisco_xrv \ +&& 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_scp.py --test_device nxos1 \ +&& 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 \ +&& 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 From 752f9d97710941f8ed4be5b50907f9582a8e072b Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sun, 4 Mar 2018 10:09:19 -0800 Subject: [PATCH 19/29] Adding unit tests for file transfer functions --- netmiko/scp_functions.py | 5 +- tests/conftest.py | 76 ++++++++++++++++++ tests/test2_src.txt | 1 + tests/test9.txt | 2 +- tests/test_netmiko_scp.py | 163 ++++++++++++++++++++++++++++++-------- 5 files changed, 212 insertions(+), 35 deletions(-) create mode 100644 tests/test2_src.txt diff --git a/netmiko/scp_functions.py b/netmiko/scp_functions.py index 6947faffc..0b9faef61 100644 --- a/netmiko/scp_functions.py +++ b/netmiko/scp_functions.py @@ -78,9 +78,8 @@ def reliable_file_transfer(ssh_conn, source_file, dest_file, file_system=None, d if not disable_md5: if scp_transfer.compare_md5(): return nottransferred_but_verified - else: - msg = "File already exists and overwrite_file is disabled" - raise ValueError(msg) + msg = "File already exists and overwrite_file is disabled" + raise ValueError(msg) else: verifyspace_and_transferfile(scp_transfer) # File doesn't exist diff --git a/tests/conftest.py b/tests/conftest.py index 0b089683a..b3ceec74a 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -220,6 +220,7 @@ def scp_fixture_get(request): 'cisco_xr': { 'file_system': 'disk0:', 'enable_scp': False, + # Delete pattern is the same on IOS-XR 'delete_file': delete_file_ios, }, } @@ -294,3 +295,78 @@ def ssh_autodetect(request): device['device_type'] = 'autodetect' conn = SSHDetect(**device) return (conn, my_device_type) + +@pytest.fixture(scope='module') +def scp_file_transfer(request): + """ + Testing reliable_file_transfer + + Return the netmiko connection object + """ + platform_args = { + 'cisco_ios': { + 'file_system': 'flash:', + 'enable_scp': True, + 'delete_file': delete_file_ios, + }, + 'juniper_junos': { + 'file_system': '/var/tmp', + 'enable_scp': False, + 'delete_file': delete_file_generic, + }, + 'arista_eos': { + 'file_system': '/mnt/flash', + 'enable_scp': False, + 'delete_file': delete_file_generic, + }, + 'cisco_nxos': { + 'file_system': 'bootflash:', + 'enable_scp': False, + 'delete_file': delete_file_nxos, + }, + 'cisco_xr': { + 'file_system': 'disk0:', + 'enable_scp': False, + # Delete pattern is the same on IOS-XR + 'delete_file': delete_file_ios, + }, + } + + # Create the files + with open("test9.txt", "w") as f: + # Not important what it is in the file + f.write("no logging console") + + with open("test2_src.txt", "w") as f: + # Not important what it is in the file + f.write("no logging console") + f.write("logging buffered 10000") + + device_under_test = request.config.getoption('test_device') + test_devices = parse_yaml(PWD + "/etc/test_devices.yml") + device = test_devices[device_under_test] + device['verbose'] = False + ssh_conn = ConnectHandler(**device) + + platform = device['device_type'] + file_system = platform_args[platform]['file_system'] + source_file = 'test9.txt' + dest_file = 'test9.txt' + local_file = 'testx.txt' + alt_file = 'test2.txt' + direction = 'put' + + scp_transfer = FileTransfer(ssh_conn, source_file=source_file, dest_file=dest_file, + file_system=file_system, direction=direction) + scp_transfer.establish_scp_conn() + + # Delete the test transfer files + if scp_transfer.check_file_exists(): + func = platform_args[platform]['delete_file'] + func(ssh_conn, file_system, dest_file) + if os.path.exists(local_file): + os.remove(local_file) + if os.path.exists(alt_file): + os.remove(alt_file) + + return (ssh_conn, file_system) diff --git a/tests/test2_src.txt b/tests/test2_src.txt new file mode 100644 index 000000000..6c0ea5c2e --- /dev/null +++ b/tests/test2_src.txt @@ -0,0 +1 @@ +no logging consolelogging buffered 10000 \ No newline at end of file diff --git a/tests/test9.txt b/tests/test9.txt index acc8e3dd5..b24025041 100644 --- a/tests/test9.txt +++ b/tests/test9.txt @@ -1 +1 @@ -no logging console +no logging console \ No newline at end of file diff --git a/tests/test_netmiko_scp.py b/tests/test_netmiko_scp.py index 9d62af8dc..ccc626441 100755 --- a/tests/test_netmiko_scp.py +++ b/tests/test_netmiko_scp.py @@ -4,9 +4,11 @@ import time import sys import os +import pytest from datetime import datetime from getpass import getpass from netmiko import ConnectHandler, FileTransfer +from netmiko import reliable_file_transfer ###def test_enable_scp(scp_fixture): ### ssh_conn, scp_transfer = scp_fixture @@ -64,34 +66,133 @@ def test_disconnect(scp_fixture): ssh_conn, scp_transfer = scp_fixture ssh_conn.disconnect() -###def test_verify_space_available_get(scp_fixture_get): -### ssh_conn, scp_transfer = scp_fixture_get -### assert scp_transfer.verify_space_available() == True -### # intentional make there not be enough space available -### scp_transfer.file_size = 100000000000 -### assert scp_transfer.verify_space_available() == False -### -###def test_scp_get(scp_fixture_get): -### ssh_conn, scp_transfer = scp_fixture_get -### -### if scp_transfer.check_file_exists(): -### # File should not already exist -### assert False -### else: -### scp_transfer.get_file() -### if scp_transfer.check_file_exists(): -### assert True -### else: -### assert False -### -###def test_md5_methods_get(scp_fixture_get): -### ssh_conn, scp_transfer = scp_fixture_get -### md5_value = 'd8df36973ff832b564ad84642d07a261' -### local_md5 = scp_transfer.file_md5("test9.txt") -### assert local_md5 == md5_value -### assert scp_transfer.compare_md5() == True -### -###def test_disconnect_get(scp_fixture_get): -### """Terminate the SSH session.""" -### ssh_conn, scp_transfer = scp_fixture_get -### ssh_conn.disconnect() +def test_verify_space_available_get(scp_fixture_get): + ssh_conn, scp_transfer = scp_fixture_get + assert scp_transfer.verify_space_available() == True + # intentional make there not be enough space available + scp_transfer.file_size = 100000000000 + assert scp_transfer.verify_space_available() == False + +def test_scp_get(scp_fixture_get): + ssh_conn, scp_transfer = scp_fixture_get + + if scp_transfer.check_file_exists(): + # File should not already exist + assert False + else: + scp_transfer.get_file() + if scp_transfer.check_file_exists(): + assert True + else: + assert False + +def test_md5_methods_get(scp_fixture_get): + ssh_conn, scp_transfer = scp_fixture_get + md5_value = 'd8df36973ff832b564ad84642d07a261' + local_md5 = scp_transfer.file_md5("test9.txt") + assert local_md5 == md5_value + assert scp_transfer.compare_md5() == True + +def test_disconnect_get(scp_fixture_get): + """Terminate the SSH session.""" + ssh_conn, scp_transfer = scp_fixture_get + ssh_conn.disconnect() + + +def test_file_transfer(scp_file_transfer): + """Test Netmiko reliable file transfer function.""" + ssh_conn, file_system = scp_file_transfer + source_file = 'test9.txt' + dest_file = 'test9.txt' + direction = 'put' + print(file_system) + + transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + dest_file=dest_file, + file_system=file_system, direction=direction, + overwrite_file=True) + + # No file on device at the beginning + assert transfer_dict['file_exists'] and transfer_dict['file_transferred'] and transfer_dict['file_verified'] + + # File exists on device at this point + transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + dest_file=dest_file, + file_system=file_system, direction=direction, + overwrite_file=True) + assert transfer_dict['file_exists'] and not transfer_dict['file_transferred'] and transfer_dict['file_verified'] + + # Don't allow a file overwrite (switch the source file, but same dest file name) + source_file = 'test2_src.txt' + with pytest.raises(Exception): + transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + dest_file=dest_file, + file_system=file_system, direction=direction, + overwrite_file=False) + + # Don't allow MD5 and file overwrite not allowed + source_file = 'test9.txt' + with pytest.raises(Exception): + transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + dest_file=dest_file, disable_md5=True, + file_system=file_system, direction=direction, + overwrite_file=False) + + # Don't allow MD5 (this will force a re-transfer) + transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + dest_file=dest_file, disable_md5=True, + file_system=file_system, direction=direction, + overwrite_file=True) + assert transfer_dict['file_exists'] and transfer_dict['file_transferred'] and not transfer_dict['file_verified'] + + # Transfer 'test2.txt' in preparation for get operations + source_file = 'test2_src.txt' + dest_file = 'test2.txt' + transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + dest_file=dest_file, + file_system=file_system, direction=direction, + overwrite_file=True) + assert transfer_dict['file_exists'] + + # GET Operations + direction = 'get' + source_file = 'test9.txt' + dest_file = 'testx.txt' + transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + dest_file=dest_file, disable_md5=False, + file_system=file_system, direction=direction, + overwrite_file=True) + # File get should occur here + assert transfer_dict['file_exists'] and transfer_dict['file_transferred'] and transfer_dict['file_verified'] + + # File should exist now + transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + dest_file=dest_file, disable_md5=False, + file_system=file_system, direction=direction, + overwrite_file=True) + assert transfer_dict['file_exists'] and not transfer_dict['file_transferred'] and transfer_dict['file_verified'] + + # Don't allow a file overwrite (switch the file, but same dest file name) + source_file = 'test2.txt' + dest_file = 'testx.txt' + with pytest.raises(Exception): + transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + dest_file=dest_file, + file_system=file_system, direction=direction, + overwrite_file=False) + + # Don't allow MD5 and file overwrite not allowed + source_file = 'test9.txt' + dest_file = 'testx.txt' + with pytest.raises(Exception): + transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + dest_file=dest_file, disable_md5=True, + file_system=file_system, direction=direction, + overwrite_file=False) + + # Don't allow MD5 (this will force a re-transfer) + transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + dest_file=dest_file, disable_md5=True, + file_system=file_system, direction=direction, + overwrite_file=True) + assert transfer_dict['file_exists'] and transfer_dict['file_transferred'] and not transfer_dict['file_verified'] From 69601d7d9dccfd10d6604884668443ebda6c4f76 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sun, 4 Mar 2018 12:07:29 -0800 Subject: [PATCH 20/29] Adding file_transfer function and associated unit tests --- netmiko/__init__.py | 4 ++-- netmiko/cisco/cisco_xr_ssh.py | 1 - netmiko/scp_functions.py | 4 ++-- tests/conftest.py | 2 +- tests/test_netmiko_scp.py | 26 +++++++++++++------------- tests/test_scp.sh | 14 ++++++++++++++ 6 files changed, 32 insertions(+), 19 deletions(-) create mode 100755 tests/test_scp.sh diff --git a/netmiko/__init__.py b/netmiko/__init__.py index 4341f58fe..63722e9d0 100644 --- a/netmiko/__init__.py +++ b/netmiko/__init__.py @@ -16,7 +16,7 @@ from netmiko.ssh_exception import NetMikoAuthenticationException from netmiko.ssh_autodetect import SSHDetect from netmiko.base_connection import BaseConnection -from netmiko.scp_functions import reliable_file_transfer +from netmiko.scp_functions import file_transfer # Alternate naming NetmikoTimeoutError = NetMikoTimeoutException @@ -27,7 +27,7 @@ __all__ = ('ConnectHandler', 'ssh_dispatcher', 'platforms', 'SCPConn', 'FileTransfer', 'NetMikoTimeoutException', 'NetMikoAuthenticationException', 'NetmikoTimeoutError', 'NetmikoAuthError', 'InLineTransfer', 'redispatch', - 'SSHDetect', 'BaseConnection', 'Netmiko', 'reliable_file_transfer') + 'SSHDetect', 'BaseConnection', 'Netmiko', 'file_transfer') # Cisco cntl-shift-six sequence CNTL_SHIFT_6 = chr(30) diff --git a/netmiko/cisco/cisco_xr_ssh.py b/netmiko/cisco/cisco_xr_ssh.py index d404044c7..efd81776d 100644 --- a/netmiko/cisco/cisco_xr_ssh.py +++ b/netmiko/cisco/cisco_xr_ssh.py @@ -163,7 +163,6 @@ def remote_md5(self, base_cmd='show md5 file', remote_file=None): remote_md5_cmd = "{} /{}/{}".format(base_cmd, self.file_system, remote_file) dest_md5 = self.ssh_ctl_chan.send_command(remote_md5_cmd, delay_factor=3.0) dest_md5 = self.process_md5(dest_md5) - print(dest_md5) return dest_md5 def enable_scp(self, cmd=None): diff --git a/netmiko/scp_functions.py b/netmiko/scp_functions.py index 0b9faef61..f5abd0a6d 100644 --- a/netmiko/scp_functions.py +++ b/netmiko/scp_functions.py @@ -20,8 +20,8 @@ def verifyspace_and_transferfile(scp_transfer): scp_transfer.transfer_file() -def reliable_file_transfer(ssh_conn, source_file, dest_file, file_system=None, direction='put', - disable_md5=False, inline_transfer=False, overwrite_file=False): +def file_transfer(ssh_conn, source_file, dest_file, file_system=None, direction='put', + disable_md5=False, inline_transfer=False, overwrite_file=False): """Use Secure Copy or Inline (IOS-only) to transfer files to/from network devices. return { diff --git a/tests/conftest.py b/tests/conftest.py index b3ceec74a..c8f5f3baa 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -299,7 +299,7 @@ def ssh_autodetect(request): @pytest.fixture(scope='module') def scp_file_transfer(request): """ - Testing reliable_file_transfer + Testing file_transfer Return the netmiko connection object """ diff --git a/tests/test_netmiko_scp.py b/tests/test_netmiko_scp.py index ccc626441..ad5c938fb 100755 --- a/tests/test_netmiko_scp.py +++ b/tests/test_netmiko_scp.py @@ -8,7 +8,7 @@ from datetime import datetime from getpass import getpass from netmiko import ConnectHandler, FileTransfer -from netmiko import reliable_file_transfer +from netmiko import file_transfer ###def test_enable_scp(scp_fixture): ### ssh_conn, scp_transfer = scp_fixture @@ -100,14 +100,14 @@ def test_disconnect_get(scp_fixture_get): def test_file_transfer(scp_file_transfer): - """Test Netmiko reliable file transfer function.""" + """Test Netmiko file_transfer function.""" ssh_conn, file_system = scp_file_transfer source_file = 'test9.txt' dest_file = 'test9.txt' direction = 'put' print(file_system) - transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + transfer_dict = file_transfer(ssh_conn, source_file=source_file, dest_file=dest_file, file_system=file_system, direction=direction, overwrite_file=True) @@ -116,7 +116,7 @@ def test_file_transfer(scp_file_transfer): assert transfer_dict['file_exists'] and transfer_dict['file_transferred'] and transfer_dict['file_verified'] # File exists on device at this point - transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + transfer_dict = file_transfer(ssh_conn, source_file=source_file, dest_file=dest_file, file_system=file_system, direction=direction, overwrite_file=True) @@ -125,7 +125,7 @@ def test_file_transfer(scp_file_transfer): # Don't allow a file overwrite (switch the source file, but same dest file name) source_file = 'test2_src.txt' with pytest.raises(Exception): - transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + transfer_dict = file_transfer(ssh_conn, source_file=source_file, dest_file=dest_file, file_system=file_system, direction=direction, overwrite_file=False) @@ -133,13 +133,13 @@ def test_file_transfer(scp_file_transfer): # Don't allow MD5 and file overwrite not allowed source_file = 'test9.txt' with pytest.raises(Exception): - transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + transfer_dict = file_transfer(ssh_conn, source_file=source_file, dest_file=dest_file, disable_md5=True, file_system=file_system, direction=direction, overwrite_file=False) # Don't allow MD5 (this will force a re-transfer) - transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + transfer_dict = file_transfer(ssh_conn, source_file=source_file, dest_file=dest_file, disable_md5=True, file_system=file_system, direction=direction, overwrite_file=True) @@ -148,7 +148,7 @@ def test_file_transfer(scp_file_transfer): # Transfer 'test2.txt' in preparation for get operations source_file = 'test2_src.txt' dest_file = 'test2.txt' - transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + transfer_dict = file_transfer(ssh_conn, source_file=source_file, dest_file=dest_file, file_system=file_system, direction=direction, overwrite_file=True) @@ -158,7 +158,7 @@ def test_file_transfer(scp_file_transfer): direction = 'get' source_file = 'test9.txt' dest_file = 'testx.txt' - transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + transfer_dict = file_transfer(ssh_conn, source_file=source_file, dest_file=dest_file, disable_md5=False, file_system=file_system, direction=direction, overwrite_file=True) @@ -166,7 +166,7 @@ def test_file_transfer(scp_file_transfer): assert transfer_dict['file_exists'] and transfer_dict['file_transferred'] and transfer_dict['file_verified'] # File should exist now - transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + transfer_dict = file_transfer(ssh_conn, source_file=source_file, dest_file=dest_file, disable_md5=False, file_system=file_system, direction=direction, overwrite_file=True) @@ -176,7 +176,7 @@ def test_file_transfer(scp_file_transfer): source_file = 'test2.txt' dest_file = 'testx.txt' with pytest.raises(Exception): - transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + transfer_dict = file_transfer(ssh_conn, source_file=source_file, dest_file=dest_file, file_system=file_system, direction=direction, overwrite_file=False) @@ -185,13 +185,13 @@ def test_file_transfer(scp_file_transfer): source_file = 'test9.txt' dest_file = 'testx.txt' with pytest.raises(Exception): - transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + transfer_dict = file_transfer(ssh_conn, source_file=source_file, dest_file=dest_file, disable_md5=True, file_system=file_system, direction=direction, overwrite_file=False) # Don't allow MD5 (this will force a re-transfer) - transfer_dict = reliable_file_transfer(ssh_conn, source_file=source_file, + transfer_dict = file_transfer(ssh_conn, source_file=source_file, dest_file=dest_file, disable_md5=True, file_system=file_system, direction=direction, overwrite_file=True) diff --git a/tests/test_scp.sh b/tests/test_scp.sh new file mode 100755 index 000000000..0cebead60 --- /dev/null +++ b/tests/test_scp.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +RETURN_CODE=0 + +echo "Starting tests...good luck:" \ +&& echo "SCP Tests" \ +&& py.test test_netmiko_scp.py::test_file_transfer --test_device cisco881 \ +&& py.test test_netmiko_scp.py::test_file_transfer --test_device nxos1 \ +&& py.test -s -v test_netmiko_scp.py::test_file_transfer --test_device arista_sw4 \ +&& py.test -s -v test_netmiko_scp.py::test_file_transfer --test_device juniper_srx \ +&& py.test -s -v test_netmiko_scp.py::test_file_transfer --test_device cisco_xrv \ +|| RETURN_CODE=1 + +exit $RETURN_CODE From daef963122a418d7faf5adf25059b6bb55670d15 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Sun, 4 Mar 2018 12:24:52 -0800 Subject: [PATCH 21/29] Fixing minor unit tests issue --- tests/test_netmiko_scp.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_netmiko_scp.py b/tests/test_netmiko_scp.py index ad5c938fb..d3eb58871 100755 --- a/tests/test_netmiko_scp.py +++ b/tests/test_netmiko_scp.py @@ -51,11 +51,11 @@ def test_remote_file_size(scp_fixture): if not scp_transfer.check_file_exists(): scp_transfer.put_file() remote_file_size = scp_transfer.remote_file_size() - assert remote_file_size == 19 + assert remote_file_size == 18 def test_md5_methods(scp_fixture): ssh_conn, scp_transfer = scp_fixture - md5_value = 'd8df36973ff832b564ad84642d07a261' + md5_value = '679a22fcceef1b6bd3da4944519086b5' remote_md5 = scp_transfer.remote_md5() assert remote_md5 == md5_value @@ -88,7 +88,7 @@ def test_scp_get(scp_fixture_get): def test_md5_methods_get(scp_fixture_get): ssh_conn, scp_transfer = scp_fixture_get - md5_value = 'd8df36973ff832b564ad84642d07a261' + md5_value = '679a22fcceef1b6bd3da4944519086b5' local_md5 = scp_transfer.file_md5("test9.txt") assert local_md5 == md5_value assert scp_transfer.compare_md5() == True From dbf712142f159da2baa8e2169e3df9ee1825075f Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Tue, 6 Mar 2018 13:07:50 -0800 Subject: [PATCH 22/29] Standardizing files for file transfer testing --- tests/conftest.py | 10 ++++++++++ tests/test2_src.txt | 3 ++- tests/test9.txt | 2 +- tests/test_netmiko_scp.py | 6 +++--- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c8f5f3baa..a13927034 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -160,6 +160,16 @@ def scp_fixture(request): }, } + # Create the files + with open("test9.txt", "w") as f: + # Not important what it is in the file + f.write("no logging console\n") + + with open("test2_src.txt", "w") as f: + # Not important what it is in the file + f.write("no logging console\n") + f.write("logging buffered 10000\n") + device_under_test = request.config.getoption('test_device') test_devices = parse_yaml(PWD + "/etc/test_devices.yml") device = test_devices[device_under_test] diff --git a/tests/test2_src.txt b/tests/test2_src.txt index 6c0ea5c2e..d2ddd3cb8 100644 --- a/tests/test2_src.txt +++ b/tests/test2_src.txt @@ -1 +1,2 @@ -no logging consolelogging buffered 10000 \ No newline at end of file +no logging console +logging buffered 10000 diff --git a/tests/test9.txt b/tests/test9.txt index b24025041..acc8e3dd5 100644 --- a/tests/test9.txt +++ b/tests/test9.txt @@ -1 +1 @@ -no logging console \ No newline at end of file +no logging console diff --git a/tests/test_netmiko_scp.py b/tests/test_netmiko_scp.py index d3eb58871..ad5c938fb 100755 --- a/tests/test_netmiko_scp.py +++ b/tests/test_netmiko_scp.py @@ -51,11 +51,11 @@ def test_remote_file_size(scp_fixture): if not scp_transfer.check_file_exists(): scp_transfer.put_file() remote_file_size = scp_transfer.remote_file_size() - assert remote_file_size == 18 + assert remote_file_size == 19 def test_md5_methods(scp_fixture): ssh_conn, scp_transfer = scp_fixture - md5_value = '679a22fcceef1b6bd3da4944519086b5' + md5_value = 'd8df36973ff832b564ad84642d07a261' remote_md5 = scp_transfer.remote_md5() assert remote_md5 == md5_value @@ -88,7 +88,7 @@ def test_scp_get(scp_fixture_get): def test_md5_methods_get(scp_fixture_get): ssh_conn, scp_transfer = scp_fixture_get - md5_value = '679a22fcceef1b6bd3da4944519086b5' + md5_value = 'd8df36973ff832b564ad84642d07a261' local_md5 = scp_transfer.file_md5("test9.txt") assert local_md5 == md5_value assert scp_transfer.compare_md5() == True From c515207eff3631f5f43041f8b2f3223e375d9a8c Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Tue, 6 Mar 2018 15:23:15 -0800 Subject: [PATCH 23/29] Fixing max_loops bug --- netmiko/base_connection.py | 2 +- netmiko/scp_functions.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/netmiko/base_connection.py b/netmiko/base_connection.py index 9c9489482..60dbc052c 100644 --- a/netmiko/base_connection.py +++ b/netmiko/base_connection.py @@ -367,7 +367,7 @@ def _read_channel_expect(self, pattern='', re_flags=0, max_loops=150): loop_delay = .1 # Default to making loop time be roughly equivalent to self.timeout (support old max_loops # argument for backwards compatibility). - if max_loops != 150: + if max_loops == 150: max_loops = self.timeout / loop_delay while i < max_loops: if self.protocol == 'ssh': diff --git a/netmiko/scp_functions.py b/netmiko/scp_functions.py index f5abd0a6d..8f4310c45 100644 --- a/netmiko/scp_functions.py +++ b/netmiko/scp_functions.py @@ -24,6 +24,8 @@ def file_transfer(ssh_conn, source_file, dest_file, file_system=None, direction= disable_md5=False, inline_transfer=False, overwrite_file=False): """Use Secure Copy or Inline (IOS-only) to transfer files to/from network devices. + inline_transfer ONLY SUPPORTS TEXT FILES and will not support binary file transfers. + return { 'file_exists': boolean, 'file_transferred': boolean, From e9af867a70e487f595b94f6483536b63f953c6c0 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Tue, 6 Mar 2018 15:23:32 -0800 Subject: [PATCH 24/29] Improving tests for file copy code --- tests/conftest.py | 5 +++++ tests/test2_src.txt | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a13927034..e1dd4d9e8 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -268,6 +268,11 @@ def tcl_fixture(request): Return a tuple (ssh_conn, tcl_handle) """ + # Create the files + with open("test9.txt", "w") as f: + # Not important what it is in the file + f.write("no logging console\n") + device_under_test = request.config.getoption('test_device') test_devices = parse_yaml(PWD + "/etc/test_devices.yml") device = test_devices[device_under_test] diff --git a/tests/test2_src.txt b/tests/test2_src.txt index d2ddd3cb8..6c0ea5c2e 100644 --- a/tests/test2_src.txt +++ b/tests/test2_src.txt @@ -1,2 +1 @@ -no logging console -logging buffered 10000 +no logging consolelogging buffered 10000 \ No newline at end of file From 00ca6734518d082f418b549545211134dc0beb22 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Tue, 6 Mar 2018 15:36:31 -0800 Subject: [PATCH 25/29] Converting over to use max_loops for file transfer (500 seconds max wait) --- netmiko/arista/arista_ssh.py | 2 +- netmiko/cisco/cisco_nxos_ssh.py | 2 +- netmiko/cisco/cisco_xr_ssh.py | 2 +- netmiko/scp_handler.py | 2 +- tests/test9.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/netmiko/arista/arista_ssh.py b/netmiko/arista/arista_ssh.py index db9405d71..e6b11af4c 100644 --- a/netmiko/arista/arista_ssh.py +++ b/netmiko/arista/arista_ssh.py @@ -77,7 +77,7 @@ def remote_md5(self, base_cmd='verify /md5', remote_file=None): elif self.direction == 'get': remote_file = self.source_file remote_md5_cmd = "{} file:{}/{}".format(base_cmd, self.file_system, remote_file) - dest_md5 = self.ssh_ctl_chan.send_command(remote_md5_cmd, delay_factor=3.0) + dest_md5 = self.ssh_ctl_chan.send_command(remote_md5_cmd, max_loops=1500) dest_md5 = self.process_md5(dest_md5) return dest_md5 diff --git a/netmiko/cisco/cisco_nxos_ssh.py b/netmiko/cisco/cisco_nxos_ssh.py index 9a29740a4..6b820177a 100644 --- a/netmiko/cisco/cisco_nxos_ssh.py +++ b/netmiko/cisco/cisco_nxos_ssh.py @@ -100,7 +100,7 @@ def remote_md5(self, base_cmd='show file', remote_file=None): 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) + return self.ssh_ctl_chan.send_command(remote_md5_cmd, max_loops=1500) def enable_scp(self, cmd=None): raise NotImplementedError diff --git a/netmiko/cisco/cisco_xr_ssh.py b/netmiko/cisco/cisco_xr_ssh.py index efd81776d..7ada09150 100644 --- a/netmiko/cisco/cisco_xr_ssh.py +++ b/netmiko/cisco/cisco_xr_ssh.py @@ -161,7 +161,7 @@ def remote_md5(self, base_cmd='show md5 file', remote_file=None): remote_file = self.source_file # IOS-XR requires both the leading slash and the slash between file-system and file here remote_md5_cmd = "{} /{}/{}".format(base_cmd, self.file_system, remote_file) - dest_md5 = self.ssh_ctl_chan.send_command(remote_md5_cmd, delay_factor=3.0) + dest_md5 = self.ssh_ctl_chan.send_command(remote_md5_cmd, max_loops=1500) dest_md5 = self.process_md5(dest_md5) return dest_md5 diff --git a/netmiko/scp_handler.py b/netmiko/scp_handler.py index cddb0a97f..bf05f091b 100644 --- a/netmiko/scp_handler.py +++ b/netmiko/scp_handler.py @@ -269,7 +269,7 @@ def remote_md5(self, base_cmd='verify /md5', remote_file=None): elif self.direction == 'get': remote_file = self.source_file remote_md5_cmd = "{} {}/{}".format(base_cmd, self.file_system, remote_file) - dest_md5 = self.ssh_ctl_chan.send_command(remote_md5_cmd, delay_factor=3.0) + dest_md5 = self.ssh_ctl_chan.send_command(remote_md5_cmd, max_loops=1500) dest_md5 = self.process_md5(dest_md5) return dest_md5 diff --git a/tests/test9.txt b/tests/test9.txt index acc8e3dd5..b24025041 100644 --- a/tests/test9.txt +++ b/tests/test9.txt @@ -1 +1 @@ -no logging console +no logging console \ No newline at end of file From 5bcb81b5c88eb85a1c0e7750ea225e199a52e141 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Tue, 6 Mar 2018 15:37:11 -0800 Subject: [PATCH 26/29] Test file change --- tests/test9.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test9.txt b/tests/test9.txt index b24025041..acc8e3dd5 100644 --- a/tests/test9.txt +++ b/tests/test9.txt @@ -1 +1 @@ -no logging console \ No newline at end of file +no logging console From 448fafcd65af7cd2b31c6d967828ca6c342d5068 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Tue, 6 Mar 2018 15:39:11 -0800 Subject: [PATCH 27/29] Fixing minor bug on test text files --- tests/conftest.py | 6 +++--- tests/test2_src.txt | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e1dd4d9e8..f40bc8af9 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -350,12 +350,12 @@ def scp_file_transfer(request): # Create the files with open("test9.txt", "w") as f: # Not important what it is in the file - f.write("no logging console") + f.write("no logging console\n") with open("test2_src.txt", "w") as f: # Not important what it is in the file - f.write("no logging console") - f.write("logging buffered 10000") + f.write("no logging console\n") + f.write("logging buffered 10000\n") device_under_test = request.config.getoption('test_device') test_devices = parse_yaml(PWD + "/etc/test_devices.yml") diff --git a/tests/test2_src.txt b/tests/test2_src.txt index 6c0ea5c2e..d2ddd3cb8 100644 --- a/tests/test2_src.txt +++ b/tests/test2_src.txt @@ -1 +1,2 @@ -no logging consolelogging buffered 10000 \ No newline at end of file +no logging console +logging buffered 10000 From a478143d7f94922bba6a97adbf5a171857d198c4 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Wed, 7 Mar 2018 09:40:18 -0800 Subject: [PATCH 28/29] Minor Arista SCP timing fix --- netmiko/arista/arista_ssh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netmiko/arista/arista_ssh.py b/netmiko/arista/arista_ssh.py index e6b11af4c..0e21e303a 100644 --- a/netmiko/arista/arista_ssh.py +++ b/netmiko/arista/arista_ssh.py @@ -77,7 +77,7 @@ def remote_md5(self, base_cmd='verify /md5', remote_file=None): elif self.direction == 'get': remote_file = self.source_file remote_md5_cmd = "{} file:{}/{}".format(base_cmd, self.file_system, remote_file) - dest_md5 = self.ssh_ctl_chan.send_command(remote_md5_cmd, max_loops=1500) + dest_md5 = self.ssh_ctl_chan.send_command(remote_md5_cmd, max_loops=750, delay_factor=2) dest_md5 = self.process_md5(dest_md5) return dest_md5 From 729798ce9048190187f2c622e8731c6fb8cacb4a Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Wed, 7 Mar 2018 09:47:25 -0800 Subject: [PATCH 29/29] Netmiko release 2.1.0 --- netmiko/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netmiko/__init__.py b/netmiko/__init__.py index 63722e9d0..a041da3a4 100644 --- a/netmiko/__init__.py +++ b/netmiko/__init__.py @@ -23,7 +23,7 @@ NetmikoAuthError = NetMikoAuthenticationException Netmiko = ConnectHandler -__version__ = '2.0.3' +__version__ = '2.1.0' __all__ = ('ConnectHandler', 'ssh_dispatcher', 'platforms', 'SCPConn', 'FileTransfer', 'NetMikoTimeoutException', 'NetMikoAuthenticationException', 'NetmikoTimeoutError', 'NetmikoAuthError', 'InLineTransfer', 'redispatch',