#!/bin/env python #md5sum="1399d8ca81cf1c7b565d13a003be4c0f" # The above is the (embedded) md5sum of this file taken without this line, # can be # created this way if using a bash shell: # f=poap_ng.py ; cat $f | sed '/^#md5sum/d' > $f.md5 ; sed -i "s/^#md5sum=.*/#md5sum=\"$(md5sum $f.md5 | sed 's/ .*//')\"/" $f # This way this script's integrity can be checked in case you do not trust # tftp's ip checksum. This integrity check is done by /isan/bin/poap.bin). # The integrity of the files downloaded later (images, config) is checked # by downloading the corresponding file with the .md5 extension and is # done by this script itself. # Version History # --------------------------------------------------------------------------- # Version 1.1 - Integrating both static mode and serial_number mode for POAP # to obtain the config file. This may be required for Smart Licensing token # config automation requirement. # User can now simultaneously define serial_number based config and static # config download. POAP will try to download by default, # and if the file does not exists in the tftp server, then it will retry # again specified by serial_number_retry value. If still config downlaod # fails POAP will fallback to downlaod the static config(golden.cfg). # User to set retry count as per their network conditions. # Please also check the accompanying new script sl_token_preprovision.py, # which can be used for token config automation in smart licensing feature. import os import time import re import sys import syslog import shutil import signal import string import traceback # **** Here are all variables that parametrize this script **** # ************************************************************* # system and kickstart images, configuration: location on server (src) and target (dst) image_version = "6.2.1.23" n3k_image_version = "5.0.3.U5.1" n5k_image_version = "5.2.1.N1.2a" image_dir_src = "/tftpb" # part of path to remove during copy n3k_system_image_src = "n3000-uk9.%s.bin" % n3k_image_version n7k_system_image_src = "n7000-s1-dk9.%s.bin" % image_version n5k_system_image_src = "n5000-uk9.%s.bin" % n5k_image_version n3k_kickstart_image_src = "n3000-uk9-kickstart.%s.bin" % n3k_image_version n7k_kickstart_image_src = "n7000-s1-kickstart.%s.bin" % image_version n5k_kickstart_image_src = "n5000-uk9-kickstart.%s.bin" % n5k_image_version config_file_src = "/tftpb/poap.cfg" config_file_src_static = "/tftpb/golden.cfg" image_dir_dst = "bootflash:" system_image_dst = "%s/system.img" % image_dir_dst kickstart_image_dst = "%s/kickstart.img" % image_dir_dst # the copy scheduled-config command will copy to persistent location config_file_dst = "volatile:poap.cfg" md5sum_ext_src = "md5" # extension of file containing md5sum of the one without ext. # there is no md5sum_ext_dst because one the target it is a temp file required_space = 250000 # Required space on /bootflash (for config and kick/system images) # Protocols available to download are scp/tftp/ftp/sftp protocol="scp" # protocol to use to download images/config # Host name and user credentials username = "user" # tftp server account ftp_username = "user" # ftp server account password = "password" hostname = "192.168.1.1" # vrf info vrf = "management" if os.environ.has_key('POAP_VRF'): vrf=os.environ['POAP_VRF'] # Timeout info (from biggest to smallest image, should be f(image-size, protocol)) system_timeout = 2100 kickstart_timeout = 900 config_timeout = 120 md5sum_timeout = 120 # POAP can use 3 modes to obtain the config file. # - 'static' - filename is static # - 'serial_number' - switch serial number is part of the filename # - 'location' - CDP neighbor of interface on which DHCPDISCOVER arrived # is part of filename # serial_number case: # if serial-number is abc123, filename for config should be abc123.cfg # file is present in location /tftp/ # the above requires populating config_file_src = "/tftp/.cfg" # location case: # if cdp neighbor's device_id=abc and port_id=111, then filename is config_file_src.abc_111 # Note: the next line can be overwritten by command-line arg processing later # static case: # POAP will try to copy from config_file_src_static. This variable should be changed # to reflect name of any static config file for POAP.(Not mandatory for serial_number case). # serial_number_retry signifies the number of retries for serial_number case. # 0 means no retry. # Warning: Setting serial_number_retry to a high value can potentially cause # lag in bootup due to large number of copy retries. serial_number_retry = 0 config_file_type = "serial_number" # parameters passed through environment: pid="" if os.environ.has_key('POAP_PID'): pid=os.environ['POAP_PID'] serial_number=None if os.environ.has_key('POAP_SERIAL'): serial_number=os.environ['POAP_SERIAL'] cdp_interface=None if os.environ.has_key('POAP_INTF'): cdp_interface=os.environ['POAP_INTF'] # will append date/timespace into the name later log_filename = "/bootflash/poap.log" t=time.localtime() now="%d_%d_%d" % (t.tm_hour, t.tm_min, t.tm_sec) # **** end of parameters **** # ************************************************************* # ***** argv parsing and online help (for test through cli) ****** # **************************************************************** cl_cdp_interface=None # Command Line version of cdp-interface cl_serial_number=None # can overwrite the corresp. env var cl_protocol=None # can overwride the script's default cl_download_only=None # dont write boot variables def parse_args(argv, help=None): global cl_cdp_interface, cl_serial_number, cl_protocol, protocol, cl_download_only while argv: x = argv.pop(0) # not handling duplicate matches... if cmp('cdp-interface'[0:len(x)], x) == 0: try: cl_cdp_interface = argv.pop(0) except: if help: cl_cdp_interface=-1 if len(x) != len('cdp-interface') and help: cl_cdp_interface=None continue if cmp('serial-number'[0:len(x)], x) == 0: try: cl_serial_number = argv.pop(0) except: if help: cl_serial_number=-1 if len(x) != len('serial-number') and help: cl_serial_number=None continue if cmp('protocol'[0:len(x)], x) == 0: try: cl_protocol = argv.pop(0); except: if help: cl_protocol=-1 if len(x) != len('protocol') and help: cl_protocol=None if cl_protocol: protocol=cl_protocol continue if cmp('download-only'[0:len(x)], x) == 0: cl_download_only = 1 continue print "Syntax Error|invalid token:", x exit(-1) ########### display online help (if asked for) ################# nb_args = len(sys.argv) if nb_args > 1: m = re.match('__cli_script.*help', sys.argv[1]) if m: # first level help: display script description if sys.argv[1] == "__cli_script_help": print "loads system/kickstart images and config file for POAP\n" exit(0) # argument help argv = sys.argv[2:] # dont count last arg if it was partial help (no-space-question-mark) if sys.argv[1] == "__cli_script_args_help_partial": argv = argv[:-1] parse_args(argv, "help") if cl_serial_number==-1: print "WORD|Enter the serial number" exit(0) if cl_cdp_interface==-1: print "WORD|Enter the CDP interface instance" exit(0) if cl_protocol==-1: print "tftp|Use tftp for file transfer protocol" print "ftp|Use ftp for file transfer protocol" print "scp|Use scp for file transfer protocol" exit(0) if not cl_serial_number: print "serial-number|The serial number to use for the config filename" if not cl_cdp_interface: print "cdp-interface|The CDP interface to use for the config filename" if not cl_protocol: print "protocol|The file transfer protocol" if not cl_download_only: print "download-only|stop after download, dont write boot variables" print "|Run it (use static name for config file)" # we are done exit(0) # *** now overwrite env vars with command line vars (if any given) # *** this can be used for testing the script using the command line argv = sys.argv[1:] parse_args(argv) if cl_serial_number: serial_number=cl_serial_number config_file_type = "serial_number" if cl_cdp_interface: cdp_interface=cl_cdp_interface config_file_type = "location" if cl_protocol: protocol=cl_protocol # figure out what kind of box we have (to download the correct images) from cisco import cli r=cli("show version") # n3k, n5k and n6k cli returns a two element list, second one is the result string if len(r)==2: lines=r[1].split("\n") else: lines=r.split("\n") idx = [i for i, line in enumerate(lines) if re.search('^.*cisco.*Chassis.*$', line)] if re.match(".*Nexus7.*",lines[idx[0]]): box="n7k" elif re.match(".*Nexus3.*",lines[idx[0]]): box="n3k" elif re.match(".*Nexus5.*",lines[idx[0]]): box="n5k" elif re.match(".*Nexus6.*",lines[idx[0]]): box="n6k" elif re.match(".*Nexus 7.*",lines[idx[0]]): box="titan" print "box is", box if box!="n7k": from cisco import transfer # setup log file and associated utils try: log_filename = "%s.%s" % (log_filename, now) except Exception as inst: print inst poap_log_file = open(log_filename, "w+") def poap_log (info): poap_log_file.write(info) poap_log_file.write("\n") poap_log_file.flush() print info syslog.syslog(9, info) sys.stdout.flush() def poap_log_close (): poap_log_file.close() def abort_cleanup_exit () : poap_log("INFO: cleaning up") poap_log_close() exit(-1) # some argument sanity checks: # sanity check for serial number is removed for smart-licensing requirement if config_file_type == "location" and cdp_interface == None: poap_log("ERR: interface required (to derive config name) but none given") exit(-1) # Get final image name based on actual box # The variable box is the box platform, like n7k, n3k, n5k, n6k. # This is to generate the kickstart or system image src variable names defined # in the beginning of the file, e.g., n3k_system_image_src # For different sup, e.g., sup1, sup2, assign the correct image name to image # src variable system_image_src = eval("%s_%s" %(box , "system_image_src"), globals()) kickstart_image_src = eval("%s_%s" %(box , "kickstart_image_src"), globals()) # images are copied to temporary location first (dont want to # overwrite good images with bad ones). system_image_dst_tmp = "%s/system.img%s" % (image_dir_dst, ".new") kickstart_image_dst_tmp = "%s/kickstart.img%s" % (image_dir_dst, ".new") system_image_src = "%s/%s" % (image_dir_src, system_image_src) kickstart_image_src = "%s/%s" % (image_dir_src, kickstart_image_src) # setup the cli session cli("no terminal color persist"); cli("terminal dont-ask"); # utility functions def run_cli (cmd): if("password" not in cmd): poap_log("CLI : %s" % cmd) if box=="n7k": r=cli(cmd) else: r=cli(cmd)[1] return r def rm_rf (filename): try: cli("delete %s" % filename) except: pass # signal handling def sig_handler_no_exit (signum, frame) : poap_log("INFO: SIGTERM Handler while configuring boot variables") def sigterm_handler (signum, frame): poap_log("INFO: SIGTERM Handler") abort_cleanup_exit() exit(1) signal.signal(signal.SIGTERM, sigterm_handler) # transfers file, return True on success; on error exits unless 'fatal' is False in which case we return False def doCopy(protocol = "", host = "", source = "", dest = "", vrf = "management", login_timeout=10, user = "", password = "", fatal=True): rm_rf(dest) # modify source paths (tftp does not like full paths) global username, ftp_username if protocol=="tftp": source=source[len(image_dir_src):] if protocol=="ftp": username=ftp_username source=source[len(image_dir_src):] if box=="n7k": cmd="terminal password %s ; copy %s://%s@%s/%s %s vrf %s" % (password, protocol, username, host, source, dest, vrf) poap_log("Command executed: terminal password ****** ; copy %s://%s@%s/%s %s vrf %s" % (protocol, username, host, source, dest, vrf)) #print cmd try: run_cli(cmd) except: poap_log("WARN: Copy Failed: %s" % str(sys.exc_value).strip('\n\r') ) if fatal: poap_log("ERR : aborting") abort_cleanup_exit() exit(1) return False return True else: try: transfer(protocol, host, source, dest, vrf, login_timeout, username, password) except Exception as inst: poap_log("WARN: Copy Failed: %s" % inst) if fatal: poap_log("ERR : aborting") abort_cleanup_exit() exit(1) return False return True def get_md5sum_src (file_name): md5_file_name_src = "%s.%s" % (file_name, md5sum_ext_src) md5_file_name_dst = "volatile:%s.poap_md5" % os.path.basename(md5_file_name_src) rm_rf(md5_file_name_dst) ret=doCopy(protocol, hostname, md5_file_name_src, md5_file_name_dst, vrf, md5sum_timeout, username, password, False) if ret == True: r=run_cli("show file %s" % md5_file_name_dst) match = re.search("^([0-9a-f]+)\s+.*", r, re.MULTILINE) if match: sum = match.group(1) else: poap_log("INFO: Unable to get the md5 sum") return None poap_log("INFO: md5sum %s (.md5 file)" % sum) rm_rf(md5_file_name_dst) return sum return None # if no .md5 file, and text file, could try to look for an embedded checksum (see below) def check_embedded_md5sum (filename): # extract the embedded checksum sum_emb=run_cli("show file %s | grep '^#md5sum' | head lines 1 | sed 's/.*=//'" % filename).strip('\n') if sum_emb == "": poap_log("INFO: no embedded checksum") return None poap_log("INFO: md5sum %s (embedded)" % sum_emb) # remove the embedded checksum (create temp file) before we recalculate cmd="show file %s exact | sed '/^#md5sum=/d' > volatile:poap_md5" % filename run_cli(cmd) # calculate checksum (using temp file without md5sum line) sum_dst=run_cli("show file volatile:poap_md5 md5sum").strip('\n') poap_log("INFO: md5sum %s (recalculated)" % sum_dst) try: run_cli("delete volatile:poap_md5") except: pass if sum_emb != sum_dst: poap_log("ERR : MD5 verification failed for %s" % filename) abort_cleanup_exit() return None def get_md5sum_dst (filename): sum=run_cli("show file %s md5sum" % filename).strip('\n') poap_log("INFO: md5sum %s (recalculated)" % sum) return sum def check_md5sum (filename_src, filename_dst, lname): md5sum_src = get_md5sum_src(filename_src) if md5sum_src: # we found a .md5 file on the server md5sum_dst = get_md5sum_dst(filename_dst) if md5sum_dst != md5sum_src: poap_log("ERR : MD5 verification failed for %s! (%s)" % (lname, filename_dst)) abort_cleanup_exit() # Will run our CLI command to test MD5 checksum and if files are valid images # This check is also performed while setting the boot variables, but this is an # additional check def get_md5_status (msg): lines=msg.split("\n") for line in lines: index=line.find("MD5") if (index!=-1): status=line[index+17:] return status def get_version (msg): lines=msg.split("\n") for line in lines: index=line.find("MD5") if (index!=-1): status=line[index+17:] index=line.find("kickstart:") if (index!=-1): index=line.find("version") ver=line[index:] return ver index=line.find("system:") if (index!=-1): index=line.find("version") ver=line[index:] return ver def get_image_version(image): version = "Unable to retrieve" try: out = cli("show version image %s" % image) out = out[1] # This is a Hack; once the "plugin based. failed to get image swid" # is fixed this expect part can be removed. except SyntaxError, err: out = err.msg match = re.search("image name:\s+([a-zA-Z\.\-0-9]+)\n", out, re.MULTILINE) if match: split_image = match.group(1).split('.') version = ".".join(split_image[1:len(split_image) -1]) return version def verify_images (): if box=="n5k": # This is to fix the n5k system image bug kick_v=get_image_version(kickstart_image_dst) sys_v=get_image_version(system_image_dst) if kick_v != sys_v: poap_log("ERR : Image version mismatch. (kickstart : %s) (system : %s)" % (kick_v, sys_v)) abort_cleanup_exit() return True kick_cmd="show version image %s" % kickstart_image_dst sys_cmd="show version image %s" % system_image_dst kick_msg=run_cli(kick_cmd) sys_msg=run_cli(sys_cmd) # n3k, n6k images do not provide md5 information if box=="n7k": kick_s=get_md5_status(kick_msg) sys_s=get_md5_status(sys_msg) kick_v=get_version(kick_msg) sys_v=get_version(sys_msg) if box=="n7k": print "MD5 status: %s and %s" % (kick_s, sys_s) if (kick_s == "Passed" and sys_s == "Passed"): # MD5 verification passed if(kick_v != sys_v): poap_log("ERR : Image version mismatch. (kickstart : %s) (system : %s)" % (kick_v, sys_v)) abort_cleanup_exit() else: poap_log("ERR : MD5 verification failed!") poap_log("%s\n%s" % (kick_msg, sys_msg)) abort_cleanup_exit() poap_log("INFO: Verification passed. (kickstart : %s) (system : %s)" % (kick_v, sys_v)) return True else: if kick_v != sys_v: poap_log("ERR : Image version mismatch. (kickstart : %s) (system : %s)" % (kick_v, sys_v)) abort_cleanup_exit() return True # get config file from server def get_config (config_file_type, serial_number_retry): if config_file_type == "serial_number": if (serial_number_retry > 25): poap_log("WARNING: High serial_number_retry detected. Retries may take few minutes.") rc = doCopy (protocol, hostname, config_file_src, config_file_dst, vrf, config_timeout, username, password, False) # here the retries will start if initial copy failed. if (rc == True): poap_log("INFO: Completed Copy of Config File") else: while (serial_number_retry > 0): rc = doCopy (protocol, hostname, config_file_src, config_file_dst, vrf, config_timeout, username, password, False) if (rc == True): poap_log("INFO: Completed Copy of Config File") serial_number_retry = 1 #this is to terminate the while loop. serial_number_retry = serial_number_retry - 1 if (rc != True): poap_log("INFO: Serial number case failed. Trying static case.") rc = doCopy(protocol, hostname, config_file_src_static, config_file_dst, vrf, config_timeout, username, password) if (rc == True): poap_log("INFO: Completed copy of config file through static") config_file_type = "static" #config_file_type is changed for md5 checksum to be picked up else: rc = doCopy(protocol, hostname, config_file_src_static, config_file_dst, vrf, config_timeout, username, password) if (rc == True): poap_log("INFO: Completed copy of config file through static") # get file's md5 from server (if any) and verify it, failure is fatal (exit) if config_file_type == "static": check_md5sum (config_file_src_static, config_file_dst, "config file") else: check_md5sum (config_file_src, config_file_dst, "config file") # get system image file from server def get_system_image (): doCopy (protocol, hostname, system_image_src, system_image_dst_tmp, vrf, system_timeout, username, password) poap_log("INFO: Completed Copy of System Image" ) # get file's md5 from server (if any) and verify it, failure is fatal (exit) check_md5sum (system_image_src, system_image_dst_tmp, "system image") run_cli ("move %s %s" % (system_image_dst_tmp, system_image_dst)) # get kickstart image file from server def get_kickstart_image (): doCopy (protocol, hostname, kickstart_image_src, kickstart_image_dst_tmp, vrf, kickstart_timeout, username, password) poap_log("INFO: Completed Copy of Kickstart Image") # get file's md5 from server (if any) and verify it, failure is fatal (exit) check_md5sum (kickstart_image_src, kickstart_image_dst_tmp, "kickstart image") run_cli ("move %s %s" % (kickstart_image_dst_tmp, kickstart_image_dst)) def wait_box_online (): while 1: # r=run_cli("show system internal ascii-cfg event-history | grep BOX_ONLINE") r=int(run_cli("show system internal platform internal info | grep box_online | sed 's/[^0-9]*//g'").strip('\n')) if r: break else: time.sleep(5) poap_log("INFO: Waiting for box online...") # install (make persistent) images and config def install_it (): global cl_download_only if cl_download_only: exit(0) timeout = -1 # make sure box is online if box=="n7k": wait_box_online() poap_log("INFO: Setting the boot variables") try: run_cli ("config terminal ; boot kickstart %s" % kickstart_image_dst) run_cli ("config terminal ; boot system %s" % system_image_dst) run_cli ("copy running-config startup-config") run_cli ('copy %s scheduled-config' % config_file_dst) except: poap_log("ERR : setting bootvars or copy run start failed!") poap_log("ERR: msg: %s" % str(sys.exc_value).strip('\n\r')) traceback.print_exc(file=sys.stdout) sys.stdout.flush() abort_cleanup_exit() poap_log("INFO: Configuration successful") # Verify if free space is available to download config, kickstart and system images def verify_freespace (): out=run_cli("dir bootflash:") match = re.search("^\s+([0-9]+)\s+bytes\s+free$", out, re.MULTILINE) if match: freespace=int(match.group(1))/1024 else: poap_log("ERR: Unable to get free space") poap_log_handle.close() abort_cleanup_exit() poap_log("INFO: free space is %s kB" % freespace ) if required_space > freespace: poap_log("ERR : Not enough space to copy the config, kickstart image and system image, aborting!") abort_cleanup_exit() # figure out config filename to download based on serial-number def set_config_file_src_serial_number (): global config_file_src match=re.search("^(.*)\.cfg",config_file_src,re.MULTILINE) if match: config_file_src = "%s%s.cfg" % (match.group(1), serial_number) poap_log("INFO: Selected config filename (serial-nb) : %s" % config_file_src) # figure out config filename to download based on cdp neighbor info # sample output: # switch# show cdp neig # Capability Codes: R - Router, T - Trans-Bridge, B - Source-Route-Bridge # S - Switch, H - Host, I - IGMP, r - Repeater, # V - VoIP-Phone, D - Remotely-Managed-Device, # s - Supports-STP-Dispute, M - Two-port Mac Relay # # Device ID Local Intrfce Hldtme Capability Platform Port ID # Switch mgmt0 148 S I WS-C2960G-24T Gig0/2 # switch(Nexus-Switch) Eth1/1 150 R S I s Nexus-Switch Eth2/1 # switch(Nexus-Switch) Eth1/2 150 R S I s Nexus-Switch Eth2/2 # in xml: # # 83886080 # Switch # mgmt0 # 137 # switch # IGMP_cnd_filtering # cisco WS-C2960G-24TC-L # GigabitEthernet0/4 # def set_config_file_src_location(): global config_file_src cmd = "show cdp neighbors interface %s" % cdp_interface poap_log("CLI: %s" % cmd) try: r = run_cli(cmd); except: poap_log("ERR: cant get neighbor info on %s", cdp_interface) exit(-1) lines=r.split("\n") try: idx = [i for i, line in enumerate(lines) if re.search('^.*Device-ID.*$', line)] ix=idx[0]+1 line=lines[ix] words=line.split() # Check if there is a wrap due to long output string, which breaks one # line output into multiple lines while len(words)<6: ix=ix+1 if ix