From 421d7bc4eca79a114bc88bb08f5c644e6e301efa Mon Sep 17 00:00:00 2001 From: arsakthi <51338554+arsakthi@users.noreply.github.com> Date: Wed, 29 Sep 2021 15:11:53 -0700 Subject: [PATCH] Initial Commit --- .gitignore | 82 +++++++++++++ CHANGELOG.md | 23 ++++ LICENSE | 23 ++++ cisconet.cfg | 75 ++++++++++++ ztp.py | 330 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 533 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 cisconet.cfg create mode 100644 ztp.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4eda8e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,82 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux +# Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# sphinx build folder +build + +# Sphinx +.doctrees + +# Visual Studio Code + +# End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2748f5e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [V1.0] + +### Added + +- Added support for additional XE models apart from C9300 (C9500's,C9800's,ASR's). +- Enhanced error handling. +- Ability to log to console when an unhandled exception is caught during execution. + +### Changed + +- None + +### Fixed + +- None + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..77228e0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +Cisco Systems, Inc End User License Agreement 2020. + +As a general rule, software is not included in our Services and must be purchased separately. For Technical Support Services +that expressly include Software Updates, your right to use the Software is covered under your original license. For any other +Software and Scripts that Cisco provides as part of the Services, it is provided as a convenience to you and incidental to the +provision of Services (“Incidental Software and Scriptsâ€). + +All Incidental Software and Scripts, in whatever form provided, are licensed to you solely for the express purposes of the +Services and in accordance with our EULA located at http://www.cisco.com/go/eula. If we provide you with Source Code for +any Incidental Software and Scripts, then the Source Code, including any copies, modifications, enhancements and derivative +works of the Source Code, is Cisco Confidential Information that you must keep secure with access given only to your +personnel who must access the Source Code to accomplish the purposes of the Services. Unless we state otherwise, the +Source Code license we grant to you for Incidental Software and Scripts includes the limited license to modify and enhance +the provided Source Code solely for your internal use and only to the extent we expressly permit. Upon our request, you must +remove and substitute, or allow us to remove and substitute, this Source Code with functionally equivalent object code, +provided the object code substitution will only occur if you continue to have an applicable license to the Incidental Software +and Scripts. + +Disclaimer of Warranty and Support: Your warranties for the overall Service are provided in your Master Agreement. For +Incidental Software and Scripts, except as we otherwise expressly grant, these items are provided “AS-IS,†“With All Faults,†+and without warranties) of any kind (whether they are express, implied, or statutory). We have no obligations with respect +to support or maintenance, including without limitation, upgrades, updates, maintenance releases, or modifications, of the +Incidental Software and Scripts. diff --git a/cisconet.cfg b/cisconet.cfg new file mode 100644 index 0000000..fd17ddd --- /dev/null +++ b/cisconet.cfg @@ -0,0 +1,75 @@ +service nagle +no service pad +service tcp-keepalives-in +service tcp-keepalives-out +service timestamps debug datetime msec localtime show-timezone +service timestamps log datetime msec localtime show-timezone +service password-encryption +! +hostname autoinstall_config +! +! +aaa new-model +aaa authorization exec default local none +! +ip domain name amazon.com +! +enable secret 0 Cisco.123 +username admin privilege 15 secret 0 Cisco.123 +! +snmp-server community cisco RO +! +! +!### Generate RSA keys #### +crypto key generate rsa modulus 4096 +ip ssh version 2 +! +! +! +!### Enable Netconf #### +netconf-yang +! +!### Configure MGMT interface ### +interface GigabitEthernet0/0 + ip address dhcp +! +interface GigabitEthernet0 + ip address dhcp +! +! +!### change ip block size for faster tftp transfers +ip tftp blocksize 8192 +! +!### Enable LLDP #### +lldp run +lldp holdtime 60 +! +! +!### Enable mst #### +spanning-tree mode mst +spanning-tree extend system-id +! +! +logging buffered 51200 +logging console informational +! +!### Enable iox service ### +iox +! +line con 0 + login authentication default + transport preferred none + stopbits 1 + password seedoflife +line vty 0 15 + privilege level 15 + login authentication default + transport input ssh +! +! +! +no banner exec +no banner login +! +! +end \ No newline at end of file diff --git a/ztp.py b/ztp.py new file mode 100644 index 0000000..bfbe613 --- /dev/null +++ b/ztp.py @@ -0,0 +1,330 @@ + +# Importing cli module +from cli import configure, cli, configurep, executep +import re +import time +import urllib +import sys +import logging +import os +from logging.handlers import RotatingFileHandler +import subprocess + +software_mappings = { + 'C9300-24P': { + 'software_image': 'cat9k_iosxe.17.06.01.SPA.bin', + 'software_version': '17.06.01', + 'software_md5_checksum': 'fdb9c92bae37f9130d0ee6761afe2919' + }, + 'C9500-24Q': { + 'software_image': 'cat9k_iosxe.17.06.01.SPA.bin', + 'software_version': '17.06.01', + 'software_md5_checksum': 'fdb9c92bae37f9130d0ee6761afe2919' + }, + 'ASR1001-HX': { + 'software_image': 'asr1000-universalk9.17.05.01a.SPA.bin', + 'software_version': '17.05.01a', + 'software_md5_checksum': '0e4b1fc1448f8ee289634a41f75dc215' + } +} + +http_server = '10.85.134.66' +log_tofile = True +release_set = ['16.06', '16.07'] + +def main(): + + try: + print ('###### STARTING ZTP SCRIPT ######\n') + # switch to enable/disbale persistent logger + if(log_tofile == True): + filepath = create_logfile() + configure_logger(filepath) + + log_info('###### STARTING ZTP SCRIPT ######\n') + print ('**** Determining Device Model ****\n') + log_info('**** Determining Device Model ****\n') + model = get_model() + + software_image = software_mappings[model]['software_image'] + software_version = software_mappings[model]['software_version'] + software_md5_checksum = software_mappings[model]['software_md5_checksum'] + + print ('**** Checking if upgrade is required or not ***** \n') + log_info('**** Checking if upgrade is required or not ***** \n') + update_status, current_version = upgrade_required(software_version) + if update_status: + #check if image transfer needed + if check_file_exists(software_image): + print(current_version) + if current_version[0:5] not in release_set: + if not verify_dst_image_md5(software_image, software_md5_checksum): + print ('*** Attempting to transfer image to switch.. ***') + log_info('*** Attempting to transfer image to switch.. ***') + file_transfer(http_server, software_image) + if not verify_dst_image_md5(software_image, software_md5_checksum): + log_critical('*** Failed Xfer mds hash mismatch ***') + raise ValueError('Failed Xfer') + else: + file_transfer(http_server, software_image) + if current_version[0:5] not in release_set: + if not verify_dst_image_md5(software_image, software_md5_checksum): + log_critical('XXX Failed Xfer XXX') + raise ValueError('XXX Failed Xfer XXX') + + print ('*** Deploying EEM upgrade script ***') + log_info('*** Deploying EEM upgrade script ***') + deploy_eem_upgrade_script(software_image) + print ('*** Performing the upgrade - switch will reboot ***\n') + log_info('*** Performing the upgrade - switch will reboot ***\n') + cli('event manager run upgrade') + time.sleep(600) + print('*** EEM upgrade took more than 600 seconds to complete..Increase the sleep time by few minutes before retrying ***\n') + log_info('*** EEM upgrade took more than 600 seconds to reload the device..Increase the sleep time by few minutes before retrying ***\n') + else: + print ('*** No upgrade is required!!! *** \n') + log_info('*** No upgrade is required!!! *** \n') + + # Cleanup any leftover install files + print ('*** Deploying Cleanup EEM Script ***') + log_info('*** Deploying Cleanup EEM Script ***') + deploy_eem_cleanup_script() + print ('*** Running Cleanup EEM Script ***') + log_info('*** Running Cleanup EEM Script ***') + cli('event manager run cleanup') + time.sleep(30) + + #print config file name to download + config_file = '%s.cfg' % model + print('**** Downloading config file ****\n') + log_info('**** Downloading config file ****\n') + file_transfer(http_server, config_file) + print ('*** Trying to perform Day 0 configuration push **** \n') + log_info('*** Trying to perform Day 0 configuration push **** \n') + #configure_replace(config_file) + configure_merge(config_file) + configure('crypto key generate rsa modulus 4096') + print ('###### END OF ZTP SCRIPT ######\n') + log_info('###### END OF ZTP SCRIPT ######\n') + + except Exception as e: + print('*** Failure encountered during day 0 provisioning . Aborting ZTP script execution. Error details below ***\n') + log_critical('*** Failure encountered during day 0 provisioning . Aborting ZTP script execution. Error details below ***\n' + e) + print(e) + sys.exit(e) + + +def configure_replace(file,file_system='flash:/' ): + config_command = 'configure replace %s%s force' % (file_system, file) + print("************************Replacing configuration************************\n") + log_info('************************Replacing configuration************************\n') + config_repl = executep(config_command) + time.sleep(120) + +def configure_merge(file,file_system='flash:/'): + print("************************Merging running config with given config file************************\n") + log_info('************************Merging running config with given config file************************\n') + config_command = 'copy %s%s running-config' %(file_system,file) + config_repl = executep(config_command) + time.sleep(120) + +def check_file_exists(file, file_system='flash:/'): + dir_check = 'dir ' + file_system + file + print ('*** Checking to see if %s exists on %s ***' % (file, file_system)) + log_info('*** Checking to see if %s exists on %s ***' % (file, file_system)) + results = cli(dir_check) + if 'No such file or directory' in results: + print ('*** The %s does NOT exist on %s ***' % (file, file_system)) + log_info('*** The %s does NOT exist on %s ***' % (file, file_system)) + return False + elif 'Directory of %s%s' % (file_system, file) in results: + print ('*** The %s DOES exist on %s ***' % (file, file_system)) + log_info('*** The %s DOES exist on %s ***' % (file, file_system)) + return True + elif 'Directory of %s%s' % ('bootflash:/', file) in results: + print ('*** The %s DOES exist on %s ***' % (file, 'bootflash:/')) + log_info('*** The %s DOES exist on %s ***' % (file, 'bootflash:/')) + return True + else: + log_critical('************************Unexpected output from check_file_exists************************\n') + raise ValueError("Unexpected output from check_file_exists") + + +def deploy_eem_cleanup_script(): + install_command = 'install remove inactive' + eem_commands = ['event manager applet cleanup', + 'event none maxrun 600', + 'action 1.0 cli command "enable"', + 'action 2.0 cli command "%s" pattern "\[y\/n\]"' % install_command, + 'action 2.1 cli command "y" pattern "proceed"', + 'action 2.2 cli command "y"' + ] + results = configurep(eem_commands) + print ('*** Successfully configured cleanup EEM script on device! ***') + log_info('*** Successfully configured cleanup EEM script on device! ***') + +def deploy_eem_upgrade_script(image): + install_command = 'install add file flash://' + image + ' activate commit' + eem_commands = ['event manager applet upgrade', + 'event none maxrun 600', + 'action 1.0 cli command "enable"', + 'action 2.0 cli command "%s" pattern "\[y\/n\/q\]"' % install_command, + 'action 2.1 cli command "n" pattern "proceed"', + 'action 2.2 cli command "y"' + ] + results = configurep(eem_commands) + print ('*** Successfully configured upgrade EEM script on device! ***') + log_info('*** Successfully configured upgrade EEM script on device! ***') + +def file_transfer(http_server, file): + print('**** Start transferring file *******\n') + log_info('**** Start transferring file *******\n') + res = cli('copy http://%s/%s flash:%s' % (http_server,file,file)) + print(res) + log_info(res) + print("\n") + print('**** Finished transferring device configuration file *******\n') + log_info('**** Finished transferring device configuration file *******\n') + +def find_certs(): + certs = cli('show run | include crypto pki') + if certs: + certs_split = certs.splitlines() + certs_split.remove('') + for cert in certs_split: + command = 'no %s' % (cert) + configure(command) + +def get_serial(): + print ("******** Trying to get Serial# *********** ") + log_info("******** Trying to get Serial# *********** ") + try: + show_version = cli('show version') + except Exception as e: + time.sleep(90) + show_version = cli('show version') + try: + serial = re.search(r"System Serial Number\s+:\s+(\S+)", show_version).group(1) + except AttributeError: + serial = re.search(r"Processor board ID\s+(\S+)", show_version).group(1) + return serial + +def get_model(): + print ("******** Trying to get Model *********** ") + log_info("******** Trying to get Model *********** ") + try: + show_version = cli('show version') + except Exception as e: + time.sleep(90) + show_version = cli('show version') + model = re.search(r"Model Number\s+:\s+(\S+)", show_version) + if model != None: + model = model.group(1) + else: + model = re.search(r"cisco\s(\w+-.*?)\s", show_version) + if model != None: + model = model.group(1) + return model + +def get_file_system(): + pass + +def update_config(file,file_system='flash:/'): + update_running_config = 'copy %s%s running-config' % (file_system, file) + save_to_startup = 'write memory' + print("************************Copying to startup-config************************\n") + running_config = executep(update_running_config) + startup_config = executep(save_to_startup) + +def upgrade_required(target_version): + # Obtains show version output + sh_version = cli('show version') + current_version = re.search(r"Cisco IOS XE Software, Version\s+(\S+)", sh_version).group(1) + print('**** Current Code Version is %s ****** \n' % current_version) + print('**** Target Code Version is %s ****** \n' % target_version) + log_info('**** Current Code Version is %s ****** \n' % current_version) + log_info('**** Target Code Version is %s ****** \n' % target_version) + # Returns False if on approved version or True if upgrade is required + + if (target_version == current_version): + return False, current_version + else: + return True, current_version + +def verify_dst_image_md5(image, src_md5, file_system='flash:/'): + verify_md5 = 'verify /md5 ' + file_system + image + print ('Verifying MD5 for ' + file_system + image) + # + try: + dst_md5 = cli(verify_md5) + if src_md5 in dst_md5: + print ('*** MD5 hashes match!! ***\n') + log_info('*** MD5 hashes match!! ***\n') + return True + else: + print ('**** Failed transfer due to MD5 checksum mismatch *****') + log_info('**** Failed transfer due to MD5 checksum mismatch *****') + return False + except Exception as e: + print ('**** MD5 checksum failed due to an exception *****') + print(e) + log_info('**** MD5 checksum failed due to an exception *****') + log_info(e) + return True + #output = subprocess.Popen(['md5sum', '/flash/'+image],stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) + #stdout_data, stderr_data = output.communicate() + #output.wait() + #outputdata = (stdout_data.decode('utf-8')).split() + #md5_returned = outputdata[0] + #if src_md5 == md5_returned: + # return True + #else: + # return False + +def create_logfile(): + try: + print ("******** Creating a persistent log file *********** ") + path = '/flash/guest-share/ztp.log' + #file_exists = os.path.isfile(path) + #if(file_exists == False): + #print ("******** ztp.log file dont exist . *********** ") + with open(path, 'a+') as fp: + pass + return path + except IOError: + print("Couldnt create a log file at guset-share .Trying to use /flash/ztp.log as an alternate log path") + path = '/flash/ztp.log' + #file_exists = os.path.isfile(path) + #if(file_exists == False): + # print ("******** ztp.log file dont exist . *********** ") + with open(path, 'a+') as fp: + pass + return path + except Exception as e: + print("Couldnt create a log file to proceed") + + +def configure_logger(path): + log_formatter = logging.Formatter('%(asctime)s :: %(levelname)s :: %(message)s') + logFile = path + #create a new file > 5 mb size + log_handler = RotatingFileHandler(logFile, mode='a', maxBytes=5*1024*1024, backupCount=10, encoding=None, delay=0) + log_handler.setFormatter(log_formatter) + log_handler.setLevel(logging.INFO) + ztp_log = logging.getLogger('root') + ztp_log.setLevel(logging.INFO) + ztp_log.addHandler(log_handler) + +def log_info(message ): + if(log_tofile == True): + ztp_log = logging.getLogger('root') + ztp_log.info(message) + +def log_critical(message ): + if(log_tofile == True): + ztp_log = logging.getLogger('root') + ztp_log.critical(message) + + +if __name__ == "__main__": + main() -- GitLab