├── node-red ├── serial-ip.csv └── flow.json ├── py-server ├── serial-ip.csv └── server.py ├── script ├── 01-base-ztp.py ├── 02-dynamic-ztp.py ├── 03-stack-ztd.py └── dynamic-restconf.py └── README.md /node-red/serial-ip.csv: -------------------------------------------------------------------------------- 1 | serial,ip,netmask,gw,config_url 2 | WWWWW,1.1.1.1,255.255.255.0,1.1.1.0,http://test/config_file 3 | BBBB,3.3.2.9,255.255.255.0,3.3.2.1, -------------------------------------------------------------------------------- /py-server/serial-ip.csv: -------------------------------------------------------------------------------- 1 | serial,ip,netmask,gw,config_url 2 | WWWWW,1.1.1.1,255.255.255.0,1.1.1.0,http://test/config_file 3 | BBBB,3.3.2.9,255.255.255.0,3.3.2.1, -------------------------------------------------------------------------------- /script/01-base-ztp.py: -------------------------------------------------------------------------------- 1 | from cli import configure, cli 2 | 3 | USER="cisco" 4 | PASSWORD="cisco" 5 | ENABLE="cisco" 6 | 7 | def base_config(): 8 | configure(['hostname adam-ztd']) 9 | configure(['username {} privilege 15 password {}'.format(USER,PASSWORD)]) 10 | configure(['enable secret {}'.format(ENABLE)]) 11 | configure(['line vty 0 4', 'login local']) 12 | 13 | base_config() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sample Python Scripts of ZTP 2 | This repo contains some sample scripts for Zero Touch Deployment on Cisco devices. Requires IOS-XE 16.6 or 3 | later. 4 | 5 | ### script 6 | contains examples for the python scripts 7 | 8 | ### Node-red 9 | contains a sample server for mapping serial number to atrtibutes (such as management IP address etc) for devices 10 | 11 | ### py-server 12 | A pure python version of this sample server 13 | 14 | More to come -------------------------------------------------------------------------------- /py-server/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | import csv 4 | from flask import Flask 5 | from flask import request, jsonify, make_response 6 | app = Flask(__name__) 7 | FILE ="serial-ip.csv" 8 | 9 | 10 | 11 | @app.route('/device', methods=["GET"]) 12 | def get_all(): 13 | #print (request.args) 14 | #print("Method {}".format(request.method)) 15 | if request.method == "GET": 16 | 17 | # this is a list 18 | serials = request.args.getlist('serial') 19 | print("serial:{}".format(serials)) 20 | 21 | if serials == []: 22 | return jsonify({"error:" : "No serial provided as query param (?serial=aaa)"}), 404 23 | for serial in serials: 24 | try: 25 | result = db[serial] 26 | print(result) 27 | return jsonify(result), 200 28 | except KeyError: 29 | pass 30 | 31 | return jsonify({"Error": "Cannot find serial:{}".format(serial)}), 404 32 | 33 | 34 | 35 | 36 | 37 | if __name__ == '__main__': 38 | global db 39 | db = {} 40 | with open(FILE,"r")as f: 41 | try: 42 | reader = csv.DictReader(f) 43 | for row in reader: 44 | print(row) 45 | db[row['serial']] = row 46 | except ValueError as e: 47 | pass 48 | 49 | app.run(host="0.0.0.0", port="1880") 50 | -------------------------------------------------------------------------------- /script/02-dynamic-ztp.py: -------------------------------------------------------------------------------- 1 | from cli import configure, cli 2 | from xml.dom import minidom 3 | import re 4 | import json 5 | import urllib2 6 | 7 | CONFIG_SERVER='10.10.10.151' 8 | 9 | USER="cisco" 10 | PASSWORD="cisco" 11 | ENABLE="cisco" 12 | 13 | def base_config(): 14 | configure(['hostname adam-ztd']) 15 | configure(['username {} privilege 15 password {}'.format(USER,PASSWORD)]) 16 | configure(['enable secret {}'.format(ENABLE)]) 17 | configure(['line vty 0 4', 'login local']) 18 | 19 | def get_serials(): 20 | # xml formatted 21 | inventory = cli('show inventory | format') 22 | # skip leading newline 23 | doc = minidom.parseString(inventory[1:]) 24 | serials =[] 25 | for node in doc.getElementsByTagName('InventoryEntry'): 26 | # router, what if there are several? 27 | chassis = node.getElementsByTagName('ChassisName')[0] 28 | if chassis.firstChild.data == "Chassis": 29 | serials.append(node.getElementsByTagName('SN')[0].firstChild.data) 30 | 31 | # switch 32 | match = re.match('"Switch ([0-9])"', chassis.firstChild.data) 33 | if match: 34 | serials.append(node.getElementsByTagName('SN')[0].firstChild.data) 35 | 36 | return serials 37 | 38 | def get_my_config(serials): 39 | # define your own REST API CALL 40 | base = 'http://{}:1880/device?serial='.format(CONFIG_SERVER) 41 | url = base + '&serial='.join(serials) 42 | print url 43 | configs = json.load(urllib2.urlopen(url)) 44 | return configs 45 | 46 | def configure_network(**kwargs): 47 | if 'ip' in kwargs and kwargs['ip'] is not None: 48 | print kwargs['ip'] 49 | configure(['int g0/0','ip address {} {}'.format(kwargs['ip'], kwargs['netmask'])]) 50 | configure(['ip route vrf Mgmt-vrf 0.0.0.0 0.0.0.0 {}'.format(kwargs['gw'])]) 51 | 52 | 53 | base_config() 54 | serials = get_serials() 55 | config = get_my_config(serials) 56 | configure_network(**config) 57 | -------------------------------------------------------------------------------- /script/03-stack-ztd.py: -------------------------------------------------------------------------------- 1 | from cli import configure, cli 2 | from xml.dom import minidom 3 | import re 4 | import json 5 | import urllib2 6 | import sys 7 | 8 | CONFIG_SERVER='10.10.10.151' 9 | 10 | USER="cisco" 11 | PASSWORD="cisco" 12 | ENABLE="cisco" 13 | 14 | def log(message, severity): 15 | cli('send log %d "%s"' % (severity, message)) 16 | 17 | def base_config(): 18 | configure(['hostname adam-ztd']) 19 | configure(['username {} privilege 15 password {}'.format(USER,PASSWORD)]) 20 | configure(['enable secret {}'.format(ENABLE)]) 21 | configure(['line vty 0 4', 'login local']) 22 | 23 | def get_serials(): 24 | # xml formatted 25 | inventory = cli('show inventory | format') 26 | # skip leading newline 27 | doc = minidom.parseString(inventory[1:]) 28 | serials =[] 29 | for node in doc.getElementsByTagName('InventoryEntry'): 30 | # router, what if there are several? 31 | chassis = node.getElementsByTagName('ChassisName')[0] 32 | if chassis.firstChild.data == "Chassis": 33 | serials.append(node.getElementsByTagName('SN')[0].firstChild.data) 34 | # switch 35 | match = re.match('"Switch ([0-9])"', chassis.firstChild.data) 36 | if match: 37 | serials.append(node.getElementsByTagName('SN')[0].firstChild.data) 38 | 39 | return serials 40 | 41 | def get_my_config(serials): 42 | # define your own REST API CALL 43 | base = 'http://{}:1880/device?serial='.format(CONFIG_SERVER) 44 | url = base + '&serial='.join(serials) 45 | log('Getting config from: {}'.format(url), 5) 46 | configs = json.load(urllib2.urlopen(url)) 47 | return configs 48 | 49 | def renumber_stack(serials, serial): 50 | # find position of the correct switch #1 in the list 51 | index = serials.index(serial) 52 | index +=1 53 | if index <> 1: 54 | log("Renumbering switch top of stack".format(index),5) 55 | cli('test pnpa service stack renumber-tos {}'.format(index)) 56 | cli('reload') 57 | sys.exit(1) 58 | else: 59 | log('No need to renumber stack') 60 | 61 | def configure_network(**kwargs): 62 | if 'ip' in kwargs and kwargs['ip'] is not None: 63 | log('Configuring IP address: {}'.format(kwargs['ip']),5) 64 | configure(['int g0/0','ip address {} {}'.format(kwargs['ip'], kwargs['netmask'])]) 65 | configure(['ip route vrf Mgmt-vrf 0.0.0.0 0.0.0.0 {}'.format(kwargs['gw'])]) 66 | 67 | 68 | base_config() 69 | serials = get_serials() 70 | log('platform serial numbers:{}'.format(','.join(serials)),5) 71 | config = get_my_config(serials) 72 | 73 | # check the renumber option 74 | renumber_stack(serials, config['serial']) 75 | configure_network(**config) 76 | -------------------------------------------------------------------------------- /script/dynamic-restconf.py: -------------------------------------------------------------------------------- 1 | from cli import configure, cli 2 | import urllib2, base64 3 | import socket, struct 4 | import json 5 | import time 6 | CONFIG_SERVER="10.66.104.91" 7 | 8 | USER="cisco" 9 | PASSWORD="cisco" 10 | ENABLE="cisco" 11 | 12 | def base_config(): 13 | configure(['hostname adam-ztd']) 14 | configure(['username {} privilege 15 password {}'.format(USER,PASSWORD)]) 15 | configure(['enable secret {}'.format(ENABLE)]) 16 | configure(['line vty 0 4', 'login local']) 17 | print "\n *** restconf *** \n" 18 | configure(['restconf']) 19 | print "\n *** sleeping 20 ** \n" 20 | time.sleep(20) 21 | 22 | 23 | def get_default_gateway_linux(): 24 | """Read the default gateway directly from /proc.""" 25 | with open("/proc/net/route") as fh: 26 | for line in fh: 27 | fields = line.strip().split() 28 | if fields[1] != '00000000' or not int(fields[3], 16) & 2: 29 | continue 30 | return socket.inet_ntoa(struct.pack("