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