├── PnP-BulkConfig ├── .gitignore ├── work_files │ ├── .gitignore │ ├── test.csv │ └── bigtest.csv ├── utils.pyc ├── dnac_config.pyc ├── dnac_config.py ├── README.md ├── 11_show_config.py ├── 12_delete.py ├── utils.py └── 10_add_and_claim.py ├── PnP-BulkConfig-128 ├── .gitignore ├── work_files │ ├── .gitignore │ ├── test.csv │ ├── test-stack.csv │ ├── image_eg.csv │ └── bigtest.csv ├── dnac_config.py ├── 11_show_config.py ├── 12_delete.py ├── README.md ├── 00_pnp_devices.py ├── utils.py └── 10_add_and_claim.py ├── PnPNoSerialClaim ├── work_files │ ├── .gitignore │ ├── configs │ │ ├── .gitignore │ │ ├── access1.cfg │ │ ├── access2.cfg │ │ └── new-conf.cfg │ └── mapping ├── .gitignore ├── dnac_config.py ├── 01_workflows.py ├── 00_pnp_devices.py ├── utils.py ├── 00_file_sync.py ├── README.md └── no_serial_claim.py ├── PnPWatch ├── requirements.txt ├── src │ ├── utils.pyc │ ├── dnac_config.pyc │ ├── dnac_config.py │ ├── utils.py │ └── watch_provision.py └── README.md ├── .gitignore ├── README.md └── LICENSE /PnP-BulkConfig/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /PnP-BulkConfig-128/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | fake.py 3 | -------------------------------------------------------------------------------- /PnP-BulkConfig/work_files/.gitignore: -------------------------------------------------------------------------------- 1 | real.csv 2 | -------------------------------------------------------------------------------- /PnPNoSerialClaim/work_files/.gitignore: -------------------------------------------------------------------------------- 1 | mapping.adam 2 | -------------------------------------------------------------------------------- /PnPWatch/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.12.4 2 | uniq==1.3.2.30 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #[tests] 2 | tests/ 3 | __pycache__/ 4 | __init__.py 5 | 6 | -------------------------------------------------------------------------------- /PnPNoSerialClaim/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | fake_data.py 3 | __pycache__/ 4 | -------------------------------------------------------------------------------- /PnP-BulkConfig-128/work_files/.gitignore: -------------------------------------------------------------------------------- 1 | real.csv 2 | real-stack.csv 3 | t.csv 4 | -------------------------------------------------------------------------------- /PnPNoSerialClaim/work_files/configs/.gitignore: -------------------------------------------------------------------------------- 1 | new-perth.cfg 2 | perth.cfg 3 | pnp-9k.cfg 4 | pnp-stack.cfg 5 | -------------------------------------------------------------------------------- /PnPWatch/src/utils.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/DNAC-onboarding-tools/HEAD/PnPWatch/src/utils.pyc -------------------------------------------------------------------------------- /PnP-BulkConfig/utils.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/DNAC-onboarding-tools/HEAD/PnP-BulkConfig/utils.pyc -------------------------------------------------------------------------------- /PnPWatch/src/dnac_config.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/DNAC-onboarding-tools/HEAD/PnPWatch/src/dnac_config.pyc -------------------------------------------------------------------------------- /PnP-BulkConfig/dnac_config.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/DNAC-onboarding-tools/HEAD/PnP-BulkConfig/dnac_config.pyc -------------------------------------------------------------------------------- /PnP-BulkConfig/work_files/test.csv: -------------------------------------------------------------------------------- 1 | name,serial,pid,workflow,hostname 2 | auto_python,12345678901,WS-C3850,simpleTemplate,adam 3 | -------------------------------------------------------------------------------- /PnP-BulkConfig-128/work_files/test.csv: -------------------------------------------------------------------------------- 1 | name,serial,pid,siteName,templateName,hostname,loopback 2 | auto_python1,12345678901,WS-C3650,Global/EU/Barcelona,basic,pnp-stack,10.10.100.200 3 | -------------------------------------------------------------------------------- /PnPNoSerialClaim/work_files/configs/access1.cfg: -------------------------------------------------------------------------------- 1 | hostname access-1 2 | username cisco privilege 15 password 0 cisco123 3 | snmp-server community public RO 4 | line vty 0 15 5 | login local 6 | 7 | -------------------------------------------------------------------------------- /PnPNoSerialClaim/work_files/configs/access2.cfg: -------------------------------------------------------------------------------- 1 | hostname access-2 2 | username cisco privilege 15 password 0 cisco123 3 | snmp-server community public RO 4 | line vty 0 15 5 | login local 6 | 7 | -------------------------------------------------------------------------------- /PnPNoSerialClaim/work_files/configs/new-conf.cfg: -------------------------------------------------------------------------------- 1 | hostname pnp-stack 2 | username cisco privilege 15 password 0 cisco123 3 | snmp-server community public RO 4 | line vty 0 15 5 | login local 6 | 7 | -------------------------------------------------------------------------------- /PnPNoSerialClaim/work_files/mapping: -------------------------------------------------------------------------------- 1 | subnet,upLink,configFile 2 | 10.10.50.0/24,*,new-perth.cfg 3 | 10.10.14.0/24,GigabitEthernet1/0/5,access1.cfg 4 | 10.10.14.0/24,GigabitEthernet1/0/20,access2.cfg 5 | -------------------------------------------------------------------------------- /PnP-BulkConfig-128/work_files/test-stack.csv: -------------------------------------------------------------------------------- 1 | name,serial,pid,topOfStack,siteName,templateName,hostname,loopback 2 | auto_python1,12345678901,WS-C3650,12345678901,Global/EU/Barcelona,basic,pnp-stack,10.10.100.200 3 | -------------------------------------------------------------------------------- /PnP-BulkConfig/dnac_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | DNAC= os.getenv("DNAC") or "sandboxdnac.cisco.com" 3 | DNAC_USER= os.getenv("DNAC_USER") or "devnetuser" 4 | DNAC_PORT=os.getenv("DNAC_PORT") or 8080 5 | DNAC_PASSWORD= os.getenv("DNAC_PASSWORD") or "Cisco123!" -------------------------------------------------------------------------------- /PnPWatch/src/dnac_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | DNAC= os.getenv("DNAC") or "sandboxdnac.cisco.com" 3 | DNAC_USER= os.getenv("DNAC_USER") or "devnetuser" 4 | DNAC_PORT=os.getenv("DNAC_PORT") or 8080 5 | DNAC_PASSWORD= os.getenv("DNAC_PASSWORD") or "Cisco123!" -------------------------------------------------------------------------------- /PnPNoSerialClaim/dnac_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | DNAC= os.getenv("DNAC") or "sandboxdnac.cisco.com" 3 | DNAC_USER= os.getenv("DNAC_USER") or "devnetuser" 4 | DNAC_PORT=os.getenv("DNAC_PORT") or 8080 5 | DNAC_PASSWORD= os.getenv("DNAC_PASSWORD") or "Cisco123!" -------------------------------------------------------------------------------- /PnP-BulkConfig-128/dnac_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | DNAC= os.getenv("DNAC") or "sandboxdnac2.cisco.com" 3 | DNAC_USER= os.getenv("DNAC_USER") or "devnetuser" 4 | DNAC_PORT=os.getenv("DNAC_PORT") or 8080 5 | DNAC_PASSWORD= os.getenv("DNAC_PASSWORD") or "Cisco123!" 6 | -------------------------------------------------------------------------------- /PnP-BulkConfig-128/work_files/image_eg.csv: -------------------------------------------------------------------------------- 1 | name,serial,pid,siteName,image,templateName,hostname 2 | 3560_with_image,F002223E04t,WS-3560,Global/AUS/NSYD1,c3560cx-universalk9-mz.152-6.E.bin,switch-branch,9k-tor 3 | 3560_no_image,F002223E04r,WS-3560,Global/AUS/NSYD1,,switch-branch,9k-tor1 4 | -------------------------------------------------------------------------------- /PnP-BulkConfig/work_files/bigtest.csv: -------------------------------------------------------------------------------- 1 | name,serial,pid,workflow,hostname 2 | auto_python0,12345678910,WS-C3850,simpleTemplate,adam0 3 | auto_python1,12345678911,WS-C3850,simpleTemplate,adam1 4 | auto_python2,12345678912,WS-C3850,simpleTemplate,adam2 5 | auto_python3,12345678913,WS-C3850,simpleTemplate,adam3 6 | auto_python4,12345678914,WS-C3850,simpleTemplate,adam4 7 | auto_python5,12345678915,WS-C3850,simpleTemplate,adam5 8 | auto_python6,12345678916,WS-C3850,simpleTemplate,adam6 9 | auto_python7,12345678917,WS-C3850,simpleTemplate,adam7 10 | auto_python8,12345678918,WS-C3850,simpleTemplate,adam8 11 | auto_python9,12345678919,WS-C3850,simpleTemplate,adam9 12 | -------------------------------------------------------------------------------- /PnPWatch/README.md: -------------------------------------------------------------------------------- 1 | PnPWatch is a utility to show the status of a PnP device as it is provisioned. 2 | 3 | It requires python3 for the uniq library. 4 | 5 | Also recommend using virtualenv. Use the following commands as examples 6 | 7 | ``` 8 | virtualenv -p python3 env 9 | source env/bin/activate 10 | ``` 11 | To install: 12 | ``` 13 | pip install -r requirements.txt 14 | ``` 15 | 16 | You will need to edit the apic_config.py file to change your credentials. 17 | NOTE: You can also use environment variables for these parameters too. 18 | APIC, APIC_USER, APIC_PASSWORD will be looked at first. 19 | export APIC_PASSWORD="mysecrete" for example 20 | 21 | To run: 22 | 23 | ``` 24 | src/watch_provision.py 25 | ``` 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /PnP-BulkConfig-128/work_files/bigtest.csv: -------------------------------------------------------------------------------- 1 | name,serial,pid,siteName,templateName,hostname,loopback 2 | auto_python0,12345678910,WS-C3650,Global/AUS/NSYD5,basic,pnp-stack,10.10.100.200 3 | auto_python1,12345678911,WS-C3650,Global/AUS/NSYD5,basic,pnp-stack,10.10.100.201 4 | auto_python2,12345678912,WS-C3650,Global/AUS/NSYD5,basic,pnp-stack,10.10.100.202 5 | auto_python3,12345678913,WS-C3650,Global/AUS/NSYD5,basic,pnp-stack,10.10.100.203 6 | auto_python4,12345678914,WS-C3650,Global/AUS/NSYD5,basic,pnp-stack,10.10.100.204 7 | auto_python5,12345678915,WS-C3650,Global/AUS/NSYD5,basic,pnp-stack,10.10.100.205 8 | auto_python6,12345678916,WS-C3650,Global/AUS/NSYD5,basic,pnp-stack,10.10.100.206 9 | auto_python7,12345678917,WS-C3650,Global/AUS/NSYD5,basic,pnp-stack,10.10.100.207 10 | auto_python8,12345678918,WS-C3650,Global/AUS/NSYD5,basic,pnp-stack,10.10.100.208 11 | 12 | -------------------------------------------------------------------------------- /PnPNoSerialClaim/01_workflows.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | import json 4 | from utils import login, get, create_url 5 | 6 | import time 7 | 8 | def msec_to_time(msec): 9 | epoc = msec /1000 10 | return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(epoc)) 11 | 12 | 13 | def process(workflows): 14 | for workflow in workflows: 15 | print("Name:{}, Type:{}, InUse:{} updated {} id:{}".format(workflow['name'], 16 | workflow['type'], 17 | workflow['useState'], 18 | msec_to_time(workflow['lastupdateOn']), 19 | workflow['id'])) 20 | print (json.dumps(workflow['tasks'])) 21 | print() 22 | 23 | if __name__ == "__main__": 24 | dnac = login() 25 | 26 | data = get(dnac, 'dna/intent/api/v1/onboarding/pnp-workflow') 27 | process(data.json()) -------------------------------------------------------------------------------- /PnP-BulkConfig/README.md: -------------------------------------------------------------------------------- 1 | # PnP-BulkConfig 2 | Bulk upload of intent for onboarding network devices. 3 | 4 | The scripts in this directory take a CSV file of device serial number plus a workflow to be applied to them. 5 | A PnP rule is created for each device, linking the serialnumber of the device to a predefined workflow in DNAC 6 | 7 | When the device is connected to the network, it will discover DNA-C and begin the onboading process. 8 | 9 | DNA-C will recognise the serial number of the device, and run the pre-defined workflow. 10 | 11 | A simple example could include an initial configuration template, which contains varaibles. 12 | The script will recognise template variables and fill them in from the CSV file. 13 | 14 | ## work_files 15 | Contains the inventory files. These are CSV files with the device name, serial number, pid, workflow name and any variables required for the workflow template 16 | 17 | ## running the scripts 18 | ./10_add_and_claim.py work_files/test.csv 19 | 20 | creates a pnp device rule and then claims it with a workflow and template variables. 21 | 22 | you need to define the workflow and the template used. 23 | 24 | ./12_delete.py 25 | shows the rendered configuration for the device with serial number . 26 | 27 | ./12_delete.py work_files/test.csv 28 | cleans up afterwards. deleting pnp rules for all devices in the csv file 29 | -------------------------------------------------------------------------------- /PnP-BulkConfig/11_show_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | import csv 4 | import json 5 | 6 | from argparse import ArgumentParser 7 | from utils import login, get, put 8 | 9 | 10 | def get_config(dnac,templateId, params): 11 | body = { 12 | "templateId": templateId, 13 | "params": params 14 | } 15 | response = put(dnac, "template-programmer/template/preview", payload=json.dumps(body)) 16 | print (response.json()['cliPreview']) 17 | 18 | def get_device(dnac, serial): 19 | params = {} 20 | response = get(dnac, "onboarding/pnp-device?serialNumber={}".format(serial)) 21 | try: 22 | device = response.json()[0]['workflowParameters']['configList'] 23 | templateId = device[0]['configId'] 24 | for p in device[0]['configParameters']: 25 | params[p['key']] = p['value'] 26 | return templateId, params 27 | except KeyError: 28 | print("Cannot find tempate for device serial {}".format(serial)) 29 | raise KeyError 30 | 31 | if __name__ == "__main__": 32 | parser = ArgumentParser(description='Select options.') 33 | parser.add_argument( 'serial', type=str, 34 | help='device serial number') 35 | args = parser.parse_args() 36 | 37 | dnac = login() 38 | print ("looking for serial number:", args.serial) 39 | 40 | 41 | templateId, params = get_device(dnac, args.serial) 42 | get_config(dnac, templateId, params) -------------------------------------------------------------------------------- /PnP-BulkConfig-128/11_show_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | import csv 4 | import json 5 | 6 | from argparse import ArgumentParser 7 | from utils import login, get, put 8 | 9 | 10 | def get_config(dnac,templateId, params): 11 | body = { 12 | "templateId": templateId, 13 | "params": params 14 | } 15 | response = put(dnac, "template-programmer/template/preview", payload=json.dumps(body)) 16 | print (response.json()['cliPreview']) 17 | 18 | def get_device(dnac, serial): 19 | params = {} 20 | response = get(dnac, "onboarding/pnp-device?serialNumber={}".format(serial)) 21 | try: 22 | device = response.json()[0]['workflowParameters']['configList'] 23 | templateId = device[0]['configId'] 24 | for p in device[0]['configParameters']: 25 | params[p['key']] = p['value'] 26 | return templateId, params 27 | except KeyError: 28 | print("Cannot find tempate for device serial {}".format(serial)) 29 | raise KeyError 30 | 31 | if __name__ == "__main__": 32 | parser = ArgumentParser(description='Select options.') 33 | parser.add_argument( 'serial', type=str, 34 | help='device serial number') 35 | args = parser.parse_args() 36 | 37 | dnac = login() 38 | print ("looking for serial number:", args.serial) 39 | 40 | 41 | templateId, params = get_device(dnac, args.serial) 42 | get_config(dnac, templateId, params) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Onboarding Tools 2 | 3 | ## Intent 4 | Predefine an automated onbording workflow for new devices being connected to the network. 5 | 6 | ## PnP-BulkConfig 7 | Allows uploading of "predefined" rules to onboard network devices. 8 | variables can be supplied to fill in device specific parameters. 9 | 10 | The scripts in this directory take a CSV file of device serialNumber plus a workflow to be applied to them. 11 | A PnP rule is created for each device, linking the serialNumber of the device to a predefined workflow in Cisco DNA Center. 12 | 13 | When the device is connected to the network, it will discover Cisco DNA Center and begin the onboading process. 14 | 15 | Cisco DNA Center will recognise the serial number of the device, and run the pre-defined workflow. 16 | 17 | A simple example of a workflow would include an initial configuration template, which contains variables (such as hostname). 18 | The script will recognise template variables and fill them in from the CSV file. 19 | 20 | 21 | ## PnP-BulkConfig-128 22 | Modified for version 1.2.8 which integrates the onboarding process into provisioning. 23 | Instead of a workflow, uses a "site" and a day-0 template to configure the device. 24 | 25 | ## PnPNoSerialClaim 26 | A tool to allow auto-claiming of devices based on IP address and CDP neighbour uplink (as opposed to serialNumber). 27 | 28 | It is only possible to do this using unclaimed flow as you need to get the IP address and CDP neighbour information after the 29 | device contacts Cisco DNA Center. 30 | 31 | This approach uses a workflow (configured via API) and mapped into a static config file. 32 | -------------------------------------------------------------------------------- /PnP-BulkConfig/12_delete.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import csv 4 | import json 5 | import requests 6 | import os 7 | import os.path 8 | from argparse import ArgumentParser 9 | from utils import login, get, post, delete 10 | 11 | 12 | def find_device(dnac,deviceSerial): 13 | response = get (dnac, "onboarding/pnp-device?serialNumber={}".format(deviceSerial)) 14 | 15 | try: 16 | return response.json()[0]['id'] 17 | except IndexError as e: 18 | print "Cannot find serial:{}".format(deviceSerial) 19 | 20 | def delete_device(dnac, deviceId): 21 | response = delete(dnac, "onboarding/pnp-device/{}".format(deviceId)) 22 | return response.json()['deviceInfo']['state'] 23 | 24 | def find_and_delete(dnac, devices): 25 | 26 | f = open(devices, 'rt') 27 | try: 28 | reader = csv.DictReader(f) 29 | for device_row in reader: 30 | #print ("Variables:",device_row) 31 | 32 | deviceId = find_device(dnac, device_row['serial']) 33 | if deviceId is not None: 34 | print "deleting:{}: {} Status:".format(device_row['serial'], deviceId), 35 | status = delete_device(dnac, deviceId) 36 | print status 37 | 38 | finally: 39 | f.close() 40 | 41 | if __name__ == "__main__": 42 | parser = ArgumentParser(description='Select options.') 43 | parser.add_argument( 'devices', type=str, 44 | help='device inventory csv file') 45 | args = parser.parse_args() 46 | 47 | dnac = login() 48 | 49 | print ("Using device file:", args.devices) 50 | 51 | print ("##########################") 52 | find_and_delete(dnac, devices=args.devices) 53 | 54 | -------------------------------------------------------------------------------- /PnP-BulkConfig-128/12_delete.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | import csv 4 | import json 5 | import requests 6 | import os 7 | import os.path 8 | from argparse import ArgumentParser 9 | from utils import login, get, post, delete 10 | 11 | 12 | def find_device(dnac,deviceSerial): 13 | response = get (dnac, "onboarding/pnp-device?serialNumber={}".format(deviceSerial)) 14 | 15 | try: 16 | return response.json()[0]['id'] 17 | except IndexError as e: 18 | print ("Cannot find serial:{}".format(deviceSerial)) 19 | 20 | def delete_device(dnac, deviceId): 21 | response = delete(dnac, "onboarding/pnp-device/{}".format(deviceId)) 22 | if response.status_code == 400: 23 | message = json.loads(response.text) 24 | return message['response']['message'] 25 | return response.json()['deviceInfo']['state'] 26 | 27 | def find_and_delete(dnac, devices): 28 | 29 | f = open(devices, 'rt') 30 | try: 31 | reader = csv.DictReader(f) 32 | for device_row in reader: 33 | #print ("Variables:",device_row) 34 | 35 | deviceId = find_device(dnac, device_row['serial']) 36 | if deviceId is not None: 37 | print ("deleting:{}: {} Status:".format(device_row['serial'], deviceId),end='') 38 | status = delete_device(dnac, deviceId) 39 | print (status) 40 | 41 | finally: 42 | f.close() 43 | 44 | if __name__ == "__main__": 45 | parser = ArgumentParser(description='Select options.') 46 | parser.add_argument( 'devices', type=str, 47 | help='device inventory csv file') 48 | args = parser.parse_args() 49 | 50 | dnac = login() 51 | 52 | print ("Using device file:", args.devices) 53 | 54 | print ("##########################") 55 | find_and_delete(dnac, devices=args.devices) 56 | 57 | -------------------------------------------------------------------------------- /PnPWatch/src/utils.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from dnac_config import DNAC, DNAC_PORT, DNAC_USER, DNAC_PASSWORD 4 | from requests.auth import HTTPBasicAuth 5 | requests.packages.urllib3.disable_warnings() 6 | 7 | # ------------------------------------------------------------------- 8 | # Custom exception definitions 9 | # ------------------------------------------------------------------- 10 | class TaskTimeoutError(Exception): 11 | pass 12 | 13 | class TaskError(Exception): 14 | pass 15 | 16 | # API ENDPOINTS 17 | ENDPOINT_TICKET = "ticket" 18 | ENDPOINT_TASK_SUMMARY ="task/%s" 19 | RETRY_INTERVAL=2 20 | 21 | # ------------------------------------------------------------------- 22 | # Helper functions 23 | # ------------------------------------------------------------------- 24 | def create_url(path, controller_ip=DNAC): 25 | """ Helper function to create a DNAC API endpoint URL 26 | """ 27 | 28 | return "https://%s:%s/api/v1/%s" % (controller_ip, DNAC_PORT, path) 29 | 30 | 31 | def get_auth_token(controller_ip=DNAC, username=DNAC_USER, password=DNAC_PASSWORD): 32 | """ Authenticates with controller and returns a token to be used in subsequent API invocations 33 | """ 34 | 35 | login_url = "https://{0}:{1}/api/system/v1/auth/token".format(controller_ip, DNAC_PORT) 36 | result = requests.post(url=login_url, auth=HTTPBasicAuth(DNAC_USER, DNAC_PASSWORD), verify=False, timeout=5) 37 | result.raise_for_status() 38 | 39 | token = result.json()["Token"] 40 | return { 41 | "controller_ip": controller_ip, 42 | "token": token 43 | } 44 | 45 | def get(dnac, url): 46 | geturl = create_url(url) 47 | headers = {'x-auth-token': dnac['token']} 48 | response = requests.get(geturl, headers=headers, verify=False) 49 | response.raise_for_status() 50 | return response 51 | 52 | def post(dnac, url, payload): 53 | posturl = create_url(url) 54 | headers = {'x-auth-token': dnac['token'], "content-type" : "application/json"} 55 | response = requests.post(posturl, headers=headers, data=json.dumps(payload), verify=False) 56 | response.raise_for_status() 57 | return response 58 | 59 | def delete(dnac, url): 60 | deleteurl = create_url(url) 61 | headers = {'x-auth-token': dnac['token']} 62 | response = requests.delete(deleteurl, headers=headers, verify=False) 63 | response.raise_for_status() 64 | return response 65 | 66 | def login(): 67 | return get_auth_token() 68 | -------------------------------------------------------------------------------- /PnP-BulkConfig-128/README.md: -------------------------------------------------------------------------------- 1 | # PnP-BulkConfig 2 | Bulk upload of intent for onboarding network devices. 3 | 4 | ## Changes with 1.2.8 5 | In 1.2.8, there is a change where the PnP workflow is integrated into the provisioning process. 6 | This means a device can be onboarded into a site. 7 | 8 | There is a new API to do this, and in addition, you no longer require credentials in the day0 configuration file 9 | 10 | The other implication is that stack renumbering is part of the payload for the API call, 11 | rather than encapsulated in the workflow. 12 | 13 | The scripts in this directory take a CSV file of device serial number plus a siteName and a (day-0) template to be applied to them. 14 | A PnP rule is created for each device, linking the serialnumber of the device to a site and the onboarding template in DNAC 15 | 16 | When the device is connected to the network, it will discover DNA-C and begin the onboading process. 17 | 18 | DNA-C will recognise the serial number of the device, and assign the device to a site, and deploy the Day0 template. 19 | 20 | A simple example could include an initial configuration template, which contains variables. 21 | 22 | The script will recognise template variables and fill them in from the CSV file. 23 | 24 | NOTE: Make sure the template is not empty. 25 | 26 | ## Getting started 27 | I also recommend using virtualenv. Use the following commands as examples 28 | 29 | ```buildoutcfg 30 | virtualenv -p python3 env 31 | source env/bin/activate 32 | ``` 33 | 34 | To install: 35 | 36 | ```buildoutcfg 37 | pip install -r requirements.txt 38 | ``` 39 | 40 | You will need to edit the dnac_config.py file to change your credentials. 41 | NOTE: You can also use environment variables for these parameters too. 42 | DNAC, DNAC_USER, DNAC_PASSWORD will be looked at first. 43 | export DNAC_PASSWORD="mysecrete" for example 44 | 45 | 46 | ## work_files 47 | Contains the inventory files. These are CSV files with the device name, serial number, pid, Location, workflow 48 | and any variables required for the workflow template. 49 | 50 | I have recently updated these to support both stack renumbering and image upgrade. 51 | 52 | ## running the scripts 53 | ./10_add_and_claim.py work_files/test.csv 54 | 55 | creates a pnp device rule and then claims it with a workflow and template variables. 56 | 57 | you need to define the workflow and the template used. 58 | 59 | ./11_show_config.py 60 | shows the rendered configuration for the device with serial number . 61 | 62 | ./12_delete.py work_files/test.csv 63 | cleans up afterwards. deleting pnp rules for all devices in the csv file 64 | 65 | 66 | -------------------------------------------------------------------------------- /PnP-BulkConfig/utils.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from dnac_config import DNAC, DNAC_PORT, DNAC_USER, DNAC_PASSWORD 4 | from requests.auth import HTTPBasicAuth 5 | requests.packages.urllib3.disable_warnings() 6 | 7 | # ------------------------------------------------------------------- 8 | # Custom exception definitions 9 | # ------------------------------------------------------------------- 10 | class TaskTimeoutError(Exception): 11 | pass 12 | 13 | class TaskError(Exception): 14 | pass 15 | 16 | # API ENDPOINTS 17 | ENDPOINT_TICKET = "ticket" 18 | ENDPOINT_TASK_SUMMARY ="task/%s" 19 | RETRY_INTERVAL=2 20 | 21 | # ------------------------------------------------------------------- 22 | # Helper functions 23 | # ------------------------------------------------------------------- 24 | def create_url(path, controller_ip=DNAC): 25 | """ Helper function to create a DNAC API endpoint URL 26 | """ 27 | 28 | return "https://%s:%s/api/v1/%s" % (controller_ip, DNAC_PORT, path) 29 | 30 | 31 | def get_auth_token(controller_ip=DNAC, username=DNAC_USER, password=DNAC_PASSWORD): 32 | """ Authenticates with controller and returns a token to be used in subsequent API invocations 33 | """ 34 | 35 | login_url = "https://{0}:{1}/api/system/v1/auth/token".format(controller_ip, DNAC_PORT) 36 | result = requests.post(url=login_url, auth=HTTPBasicAuth(DNAC_USER, DNAC_PASSWORD), verify=False, timeout=20) 37 | result.raise_for_status() 38 | 39 | token = result.json()["Token"] 40 | return { 41 | "controller_ip": controller_ip, 42 | "token": token 43 | } 44 | 45 | def get(dnac, url): 46 | geturl = create_url(url) 47 | headers = {'x-auth-token': dnac['token']} 48 | response = requests.get(geturl, headers=headers, verify=False) 49 | response.raise_for_status() 50 | return response 51 | 52 | def post(dnac, url, payload): 53 | posturl = create_url(url) 54 | headers = {'x-auth-token': dnac['token'], "content-type" : "application/json"} 55 | response = requests.post(posturl, headers=headers, data=json.dumps(payload), verify=False) 56 | response.raise_for_status() 57 | return response 58 | 59 | def put(dnac, url, payload): 60 | puturl = create_url(url) 61 | 62 | headers = {'x-auth-token': dnac['token'], "Content-Type" : "application/json"} 63 | response = requests.put(puturl, headers=headers, data=payload, verify=False) 64 | response.raise_for_status() 65 | return response 66 | 67 | def delete(dnac, url): 68 | deleteurl = create_url(url) 69 | headers = {'x-auth-token': dnac['token']} 70 | response = requests.delete(deleteurl, headers=headers, verify=False) 71 | response.raise_for_status() 72 | return response 73 | 74 | def login(): 75 | return get_auth_token() 76 | -------------------------------------------------------------------------------- /PnP-BulkConfig-128/00_pnp_devices.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | import csv 4 | import json 5 | import requests 6 | import os 7 | import os.path 8 | import logging 9 | from argparse import ArgumentParser 10 | from utils import login, get, post 11 | #from fake import fake 12 | logger = logging.getLogger() 13 | 14 | def get_ip(host): 15 | try: 16 | ip = host['deviceInfo']['httpHeaders'][0]['value'] 17 | except KeyError: 18 | ip ="" 19 | return ip 20 | def get_neighbour(host): 21 | try: 22 | # change to list comprehension 23 | links = ",".join([ link['remoteInterfaceName'] for link in host['deviceInfo']['neighborLinks']]) 24 | except KeyError: 25 | links = '' 26 | return links 27 | def get_workflow_name(host): 28 | try: 29 | workflowname = host['deviceInfo']['workflowName'] 30 | except KeyError: 31 | workflowname = "" 32 | return workflowname 33 | 34 | def process(data): 35 | fmt="{:13s}{:17}{:12}{:8}{:16}{:26}{:26}" 36 | print(fmt.format("Name","PID","State","Source","IP Address","Neighbour Interface","Workflow Name")) 37 | for host in data: 38 | logging.debug('Host:{}'.format(json.dumps(host))) 39 | print(fmt.format(host['deviceInfo']['name'], 40 | host['deviceInfo']['pid'], 41 | host['deviceInfo']['state'], 42 | host['deviceInfo']['source'], 43 | get_ip(host), 44 | get_neighbour(host), 45 | get_workflow_name(host) 46 | )) 47 | def process_single(device): 48 | print(json.dumps(device, indent=2)) 49 | 50 | if __name__ == "__main__": 51 | parser = ArgumentParser(description='Select options.') 52 | parser.add_argument( '--device', type=str, 53 | help='serial number of device') 54 | parser.add_argument('-v', action='store_true', 55 | help="verbose") 56 | args = parser.parse_args() 57 | 58 | if args.v: 59 | handler = logging.StreamHandler() 60 | formatter = logging.Formatter( 61 | '%(asctime)s %(name)-12s %(levelname)-8s %(message)s') 62 | handler.setFormatter(formatter) 63 | logger.addHandler(handler) 64 | logger.setLevel(logging.DEBUG) 65 | 66 | # set logger 67 | logger.debug("Logging enabled") 68 | 69 | dnac = login() 70 | 71 | if args.device: 72 | # get the device 73 | device = response = get(dnac, "dna/intent/api/v1/onboarding/pnp-device?name={}".format(args.device)) 74 | process_single(device.json()) 75 | else: 76 | # show all devices 77 | response = get(dnac, "dna/intent/api/v1/onboarding/pnp-device") 78 | process(response.json()) 79 | -------------------------------------------------------------------------------- /PnPNoSerialClaim/00_pnp_devices.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | import csv 4 | import json 5 | import requests 6 | import os 7 | import os.path 8 | import logging 9 | from argparse import ArgumentParser 10 | from utils import login, get, post 11 | #from fake import fake 12 | logger = logging.getLogger() 13 | 14 | def get_ip(host): 15 | try: 16 | ip = host['deviceInfo']['httpHeaders'][0]['value'] 17 | except KeyError: 18 | ip ="" 19 | return ip 20 | def get_neighbour(host): 21 | try: 22 | # change to list comprehension 23 | links = ",".join([ link['remoteInterfaceName'] for link in host['deviceInfo']['neighborLinks']]) 24 | except KeyError: 25 | links = '' 26 | return links 27 | def get_workflow_name(host): 28 | try: 29 | workflowname = host['deviceInfo']['workflowName'] 30 | except KeyError: 31 | workflowname = "" 32 | return workflowname 33 | 34 | def process(data): 35 | fmt="{:13s}{:17}{:12}{:8}{:16}{:26}{:26}" 36 | print(fmt.format("Name","PID","State","Source","IP Address","Neighbour Interface","Workflow Name")) 37 | for host in data: 38 | logging.debug('Host:{}'.format(json.dumps(host))) 39 | print(fmt.format(host['deviceInfo']['name'], 40 | host['deviceInfo']['pid'], 41 | host['deviceInfo']['state'], 42 | host['deviceInfo']['source'], 43 | get_ip(host), 44 | get_neighbour(host), 45 | get_workflow_name(host) 46 | )) 47 | def process_single(device): 48 | print(json.dumps(device, indent=2)) 49 | 50 | if __name__ == "__main__": 51 | parser = ArgumentParser(description='Select options.') 52 | parser.add_argument( '--device', type=str, 53 | help='serial number of device') 54 | parser.add_argument('-v', action='store_true', 55 | help="verbose") 56 | args = parser.parse_args() 57 | 58 | if args.v: 59 | handler = logging.StreamHandler() 60 | formatter = logging.Formatter( 61 | '%(asctime)s %(name)-12s %(levelname)-8s %(message)s') 62 | handler.setFormatter(formatter) 63 | logger.addHandler(handler) 64 | logger.setLevel(logging.DEBUG) 65 | 66 | # set logger 67 | logger.debug("Logging enabled") 68 | 69 | dnac = login() 70 | 71 | if args.device: 72 | # get the device 73 | device = response = get(dnac, "dna/intent/api/v1/onboarding/pnp-device?name={}".format(args.device)) 74 | process_single(device.json()) 75 | else: 76 | # show all devices 77 | response = get(dnac, "dna/intent/api/v1/onboarding/pnp-device") 78 | process(response.json()) 79 | -------------------------------------------------------------------------------- /PnPNoSerialClaim/utils.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from dnac_config import DNAC, DNAC_PORT, DNAC_USER, DNAC_PASSWORD 4 | from requests.auth import HTTPBasicAuth 5 | requests.packages.urllib3.disable_warnings() 6 | 7 | # ------------------------------------------------------------------- 8 | # Custom exception definitions 9 | # ------------------------------------------------------------------- 10 | class TaskTimeoutError(Exception): 11 | pass 12 | 13 | class TaskError(Exception): 14 | pass 15 | 16 | # API ENDPOINTS 17 | ENDPOINT_TICKET = "ticket" 18 | ENDPOINT_TASK_SUMMARY ="task/%s" 19 | RETRY_INTERVAL=2 20 | 21 | # ------------------------------------------------------------------- 22 | # Helper functions 23 | # ------------------------------------------------------------------- 24 | def create_url(path, controller_ip=DNAC): 25 | """ Helper function to create a DNAC API endpoint URL 26 | """ 27 | if "dna/" in path: 28 | return "https://%s:%s/%s" % (controller_ip, DNAC_PORT, path) 29 | else: 30 | return "https://%s:%s/api/v1/%s" % (controller_ip, DNAC_PORT, path) 31 | 32 | 33 | def get_auth_token(controller_ip=DNAC, username=DNAC_USER, password=DNAC_PASSWORD): 34 | """ Authenticates with controller and returns a token to be used in subsequent API invocations 35 | """ 36 | 37 | login_url = "https://{0}:{1}/api/system/v1/auth/token".format(controller_ip, DNAC_PORT) 38 | result = requests.post(url=login_url, auth=HTTPBasicAuth(DNAC_USER, DNAC_PASSWORD), verify=False, timeout=20) 39 | result.raise_for_status() 40 | 41 | token = result.json()["Token"] 42 | return { 43 | "controller_ip": controller_ip, 44 | "token": token 45 | } 46 | 47 | def get(dnac, url): 48 | geturl = create_url(url) 49 | headers = {'x-auth-token': dnac['token']} 50 | response = requests.get(geturl, headers=headers, verify=False) 51 | response.raise_for_status() 52 | return response 53 | 54 | def post(dnac, url, payload): 55 | posturl = create_url(url) 56 | headers = {'x-auth-token': dnac['token'], "content-type" : "application/json", "__runsync" : "true"} 57 | response = requests.post(posturl, headers=headers, data=json.dumps(payload), verify=False) 58 | response.raise_for_status() 59 | return response 60 | 61 | def put(dnac, url, payload): 62 | puturl = create_url(url) 63 | 64 | headers = {'x-auth-token': dnac['token'], "Content-Type" : "application/json"} 65 | response = requests.put(puturl, headers=headers, data=payload, verify=False) 66 | response.raise_for_status() 67 | return response 68 | 69 | def delete(dnac, url): 70 | deleteurl = create_url(url) 71 | headers = {'x-auth-token': dnac['token']} 72 | response = requests.delete(deleteurl, headers=headers, verify=False) 73 | response.raise_for_status() 74 | return response 75 | 76 | def login(): 77 | return get_auth_token() 78 | -------------------------------------------------------------------------------- /PnPWatch/src/watch_provision.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from utils import login, get, post 3 | import logging 4 | import sys 5 | import json 6 | import time 7 | logging.captureWarnings(True) 8 | #logging.basicConfig(level=logging.DEBUG) 9 | 10 | def get_status(dnac, serial): 11 | url = "onboarding/pnp-device?serialNumber={}".format(serial) 12 | response = get(dnac,url) 13 | try: 14 | return response.json()[0]['deviceInfo']['onbState'] 15 | except IndexError: 16 | return None 17 | ''' 18 | { 19 | "lastStateTransitionTime": "2015-09-21 10:31:58.000418", 20 | "platformId": "ISR4451-X/K9", 21 | "hostName": "Router", 22 | "id": "e2d3ff68-87d7-412d-a2c7-1e30e726d5c1", 23 | "pnpProfileAutoCreated": false, 24 | "authType": "Unsupported", 25 | "pnpProfileUsedAddr": "10.10.10.140", 26 | "configReg": "0x2102", 27 | "fileDestination": "bootflash", 28 | "state": "LOCATION_TAG_FILLED", 29 | "deviceDetailsLastUpdate": "2015-09-21 10:31:58.384", 30 | "slotNumber": "1", 31 | "firstContact": "2015-09-21 10:31:54.000815", 32 | "attributeInfo": {}, 33 | "imageFile": "bootflash:/isr4400-universalk9.03.16.00c.S.155-3.S0c-ext.SPA.bi", 34 | "versionString": "15.5(3)S0c", 35 | "apCount": "0", 36 | "pkiEnabled": true, 37 | "stateDisplay": "Getting Device Info", 38 | "returnToRomReason": "reload", 39 | "serialNumber": "FTX1743ANS9", 40 | "sudiRequired": false, 41 | "filesystemInfo": "fileSystem: name=bootflash type=disk size=7451738112 freespace=5350199296;\nfileSystem: name=nvram type=nvram size=33554432 freespace=33348556;\n", 42 | "mainMemSize": "6295128", 43 | "authStatus": "None", 44 | "lastContact": "2015-09-21 10:31:58.000418", 45 | "isMobilityController": "false" 46 | } 47 | ''' 48 | def watch_status(dnac, serial, last_status): 49 | onbstate = get_status(dnac, serial) 50 | #print json.dumps(status, indent=2) 51 | 52 | if onbstate is None: 53 | print("No site status for device") 54 | sys.exit(1) 55 | 56 | this_status = onbstate 57 | 58 | return this_status 59 | 60 | def main(argv): 61 | 62 | 63 | if len(argv) != 1: 64 | print("Error: Usage %s serial" % sys.argv[0]) 65 | sys.exit(1) 66 | else: 67 | serial = argv[0] 68 | dnac = login() 69 | 70 | FINAL_STATUS="Provisioned" 71 | print("Watching unclaimed for serial:%s" % serial) 72 | starttime = time.time() 73 | status_detail = "EMPTY" 74 | last_status = "first" 75 | last_time = starttime 76 | while status_detail != FINAL_STATUS: 77 | status_detail = watch_status(dnac, serial, last_status) 78 | if status_detail != last_status: 79 | now = time.time() 80 | print("%s: Duration (%d) %s" % (time.strftime("%H:%M:%S"), now - starttime, status_detail)) 81 | last_status = status_detail 82 | last_time = now 83 | elif now - last_time > 300: 84 | print("waiting 5 mins") 85 | last_time = now 86 | time.sleep(2) 87 | 88 | 89 | print("%s: Completed (%d): %s" % (time.strftime("%H:%M:%S"), now - starttime, status_detail)) 90 | 91 | if status_detail == FINAL_STATUS: 92 | sys.exit(0) 93 | else: 94 | sys.exit(3) 95 | 96 | 97 | if __name__ == "__main__": 98 | main(sys.argv[1:]) 99 | -------------------------------------------------------------------------------- /PnP-BulkConfig-128/utils.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import logging 4 | from dnac_config import DNAC, DNAC_PORT, DNAC_USER, DNAC_PASSWORD 5 | from requests.auth import HTTPBasicAuth 6 | requests.packages.urllib3.disable_warnings() 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | # ------------------------------------------------------------------- 11 | # Custom exception definitions 12 | # ------------------------------------------------------------------- 13 | class TaskTimeoutError(Exception): 14 | pass 15 | 16 | class TaskError(Exception): 17 | pass 18 | 19 | # API ENDPOINTS 20 | ENDPOINT_TICKET = "ticket" 21 | ENDPOINT_TASK_SUMMARY ="task/%s" 22 | RETRY_INTERVAL=2 23 | 24 | # ------------------------------------------------------------------- 25 | # Helper functions 26 | # ------------------------------------------------------------------- 27 | def create_url(path, controller_ip=DNAC): 28 | """ Helper function to create a DNAC API endpoint URL 29 | """ 30 | 31 | if "dna/" in path: 32 | return "https://%s:%s/%s" % (controller_ip, DNAC_PORT, path) 33 | else: 34 | return "https://%s:%s/api/v1/%s" % (controller_ip, DNAC_PORT, path) 35 | 36 | 37 | def get_auth_token(controller_ip=DNAC, username=DNAC_USER, password=DNAC_PASSWORD): 38 | """ Authenticates with controller and returns a token to be used in subsequent API invocations 39 | """ 40 | 41 | #login_url = "https://{0}:{1}/api/system/v1/auth/token".format(controller_ip, DNAC_PORT) 42 | login_url = "https://{0}:{1}/dna/system/api/v1/auth/token".format(controller_ip, DNAC_PORT) 43 | result = requests.post(url=login_url, auth=HTTPBasicAuth(DNAC_USER, DNAC_PASSWORD), verify=False, timeout=20) 44 | result.raise_for_status() 45 | 46 | token = result.json()["Token"] 47 | return { 48 | "controller_ip": controller_ip, 49 | "token": token 50 | } 51 | 52 | def get(dnac, url): 53 | geturl = create_url(url) 54 | headers = {'x-auth-token': dnac['token']} 55 | response = requests.get(geturl, headers=headers, verify=False) 56 | response.raise_for_status() 57 | return response 58 | 59 | def post(dnac, url, payload): 60 | posturl = create_url(url) 61 | logger.debug(posturl) 62 | logger.debug(json.dumps(payload,indent=2)) 63 | headers = {'x-auth-token': dnac['token'], "content-type" : "application/json"} 64 | response = requests.post(posturl, headers=headers, data=json.dumps(payload), verify=False) 65 | response.raise_for_status() 66 | return response 67 | 68 | def put(dnac, url, payload): 69 | puturl = create_url(url) 70 | 71 | headers = {'x-auth-token': dnac['token'], "Content-Type" : "application/json"} 72 | response = requests.put(puturl, headers=headers, data=payload, verify=False) 73 | response.raise_for_status() 74 | return response 75 | 76 | def delete(dnac, url): 77 | deleteurl = create_url(url) 78 | headers = {'x-auth-token': dnac['token']} 79 | response = requests.delete(deleteurl, headers=headers, verify=False) 80 | 81 | # response.text has the error message for deleting a device in onboarding that is in the inventory. 82 | if response.status_code == 400 and response.text: 83 | return response 84 | response.raise_for_status() 85 | 86 | return response 87 | 88 | def login(): 89 | return get_auth_token() 90 | -------------------------------------------------------------------------------- /PnP-BulkConfig/10_add_and_claim.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | import csv 4 | import json 5 | import requests 6 | import os 7 | import os.path 8 | from argparse import ArgumentParser 9 | from utils import login, get, post 10 | 11 | 12 | 13 | def add_device(dnac, name, serial, pid): 14 | payload = [{ 15 | "deviceInfo": { 16 | "name": name, 17 | "serialNumber": serial, 18 | "pid": pid, 19 | "sudiRequired": False, 20 | "userSudiSerialNos": [], 21 | "stack": False, 22 | "aaaCredentials": { 23 | "username": "", 24 | "password": "" 25 | } 26 | } 27 | }] 28 | device = post(dnac, "onboarding/pnp-device/import", payload) 29 | try: 30 | deviceId = device.json()['successList'][0]['id'] 31 | except IndexError as e: 32 | print ('##SKIPPING device:{},{}:{}'.format(name, serial, device.json()['failureList'][0]['msg'])) 33 | deviceId = None 34 | 35 | return deviceId 36 | 37 | def claim_device(dnac,deviceId, configId, workflowId, params): 38 | payload = { 39 | "deviceClaimList": [{ 40 | "deviceId": deviceId, 41 | "configList": [{ 42 | "configId": configId, 43 | "configParameters": params 44 | }] 45 | }], 46 | "projectId": None, 47 | "workflowId": workflowId, 48 | "configId": None, 49 | "imageId": None, 50 | "populateInventory": True 51 | } 52 | #print json.dumps(payload, indent=2) 53 | 54 | claim = post(dnac,"onboarding/pnp-device/claim", payload) 55 | return claim.json()['message'] 56 | 57 | def get_workflow(dnac,workflowName): 58 | response = get (dnac, "onboarding/pnp-workflow") 59 | for workflow in response.json(): 60 | if workflow['name'] == workflowName: 61 | workflowId = workflow['id'] 62 | #print json.dumps(workflow, indent=4) 63 | for tasks in workflow['tasks']: 64 | configId = tasks['configInfo']['configId'] 65 | return workflowId, configId 66 | 67 | raise ValueError("Cannot find template:{}".format(workflowName)) 68 | 69 | def get_template(dnac, configId, supplied_params): 70 | params=[] 71 | response = get(dnac, "template-programmer/template/{}".format(configId)) 72 | for vars in response.json()['templateParams']: 73 | name = vars['parameterName'] 74 | params.append({"key": name, "value": supplied_params[name]}) 75 | #print params 76 | return params 77 | 78 | def create_and_upload(dnac, devices): 79 | 80 | f = open(devices, 'rt') 81 | try: 82 | reader = csv.DictReader(f) 83 | for device_row in reader: 84 | #print ("Variables:",device_row) 85 | 86 | try: 87 | workflowId, configId = get_workflow(dnac, device_row['workflow']) 88 | except ValueError as e: 89 | print("##ERROR {},{}: {}".format(device_row['name'],device_row['serial'], e)) 90 | continue 91 | 92 | params = get_template(dnac, configId, device_row) 93 | 94 | deviceId = add_device(dnac, device_row['name'], device_row['serial'], device_row['pid']) 95 | if deviceId is not None: 96 | #claim 97 | claim_status = claim_device(dnac, deviceId, configId, workflowId, params) 98 | if "Claimed" in claim_status: 99 | status = "PLANNED" 100 | else: 101 | status = "FAILED" 102 | print ('Device:{} name:{} workflow:{} Status:{}'.format(device_row['serial'], 103 | device_row['name'], 104 | device_row['workflow'], 105 | status)) 106 | finally: 107 | f.close() 108 | 109 | if __name__ == "__main__": 110 | parser = ArgumentParser(description='Select options.') 111 | parser.add_argument( 'devices', type=str, 112 | help='device inventory csv file') 113 | args = parser.parse_args() 114 | 115 | dnac = login() 116 | print ("Using device file:", args.devices) 117 | 118 | print ("##########################") 119 | create_and_upload(dnac, devices=args.devices) 120 | -------------------------------------------------------------------------------- /PnPNoSerialClaim/00_file_sync.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | import hashlib 4 | 5 | # file is sync 6 | import requests 7 | import os.path 8 | 9 | from utils import login, get, create_url 10 | import os 11 | DIR="work_files" 12 | 13 | class File(object): 14 | def __init__(self, dnac, name, namespace, path): 15 | self.dnac = dnac 16 | self.name = name 17 | self.namespace = namespace 18 | self.path = path + "/" + name 19 | self.fileid = None 20 | self.sha1 = None 21 | 22 | def update(self): 23 | try: 24 | f = open(self.path, "r") 25 | files = {'fileUpload': f} 26 | except: 27 | raise ValueError("Could not open file %s" % self.path) 28 | 29 | url = create_url(path="file/config/{}".format(self.fileid)) 30 | headers = {'x-auth-token': self.dnac['token']} 31 | file_result = requests.put(url, files=files, headers=headers, verify=False) 32 | print(file_result.json()) 33 | return file_result.json() 34 | 35 | def upload(self): 36 | try: 37 | f = open(self.path, "r") 38 | files = {'fileUpload': f} 39 | except: 40 | raise ValueError("Could not open file %s" % self.path) 41 | 42 | url = create_url(path="file/config") 43 | 44 | print("POST %s" % url) 45 | headers = {'x-auth-token': self.dnac['token']} 46 | 47 | try: 48 | response = requests.post(url, files=files, headers=headers, verify=False) 49 | except requests.exceptions.RequestException as cerror: 50 | print("Error processing request", cerror) 51 | 52 | return(response.json()) 53 | 54 | def delete(self): 55 | file_result = self.dnac.deleteFile(fileId=self.fileid) 56 | return file_result 57 | 58 | def present(self): 59 | files = get(self.dnac, "dna/intent/api/v1/file/namespace/{nameSpace}".format(nameSpace=self.namespace)) 60 | fileid_list = [(file['id'], file['sha1Checksum']) for file in files.json()['response'] if file['name'] == self.name] 61 | self.fileid = None if fileid_list == [] else fileid_list[0][0] 62 | self.sha1 = None if fileid_list == [] else fileid_list[0][1] 63 | return self.fileid 64 | 65 | 66 | def check_namespace(apic, namespace): 67 | names = apic.file.getNameSpaceList() 68 | if names is not None: 69 | return namespace in names.response 70 | else: 71 | return False 72 | 73 | def get_sha1(file): 74 | BLOCKSIZE = 65536 75 | hasher = hashlib.sha1() 76 | with open(file, 'rb') as afile: 77 | buf = afile.read(BLOCKSIZE) 78 | while len(buf) > 0: 79 | hasher.update(buf) 80 | buf = afile.read(BLOCKSIZE) 81 | return(hasher.hexdigest()) 82 | 83 | def process_namespace(dnac, namespace): 84 | 85 | # fix for windows 86 | #rootDir = DIR + "/" + namespace + "s" 87 | rootDir = os.path.join(DIR, namespace + "s") 88 | 89 | if not os.path.isdir(rootDir): 90 | print("No directory for {rootDir}, skipping".format(rootDir=rootDir)) 91 | return 92 | 93 | for filename in os.listdir(rootDir): 94 | f = File(dnac, filename, namespace, rootDir) 95 | 96 | if f.present() is None: 97 | result = f.upload() 98 | print("Uploaded File:{file} ({id})".format(file=result['response']['name'],id=result['response']['id'])) 99 | else: 100 | # need to look at checksum to see if need to update 101 | # fix for windows 102 | #sha1 = get_sha1(rootDir+ '/' + filename) 103 | sha1 = get_sha1(os.path.join(rootDir,filename)) 104 | 105 | #print (filename, sha1, f.sha1) 106 | if sha1 != f.sha1: 107 | result = f.update() 108 | print("Updated File:{file} ({id})".format(file=result['response']['name'], id=result['response']['id'])) 109 | else: 110 | print ("Skipping File:{file} ({id}) SHA1hash:{sha1}".format(file=filename, id=f.fileid, sha1=sha1)) 111 | 112 | def main(): 113 | dnac = login() 114 | process_namespace(dnac,"config") 115 | print() 116 | 117 | if __name__ == "__main__": 118 | main() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CISCO SAMPLE CODE LICENSE 2 | 3 | Version 1.1 4 | Copyright (c) 2017 Cisco and/or its affiliates 5 | 6 | These terms govern this Cisco Systems, Inc. (“Cisco”), example or demo source code and its associated documentation (together, the “Sample Code”). By downloading, copying, modifying, compiling, or redistributing the Sample Code, you accept and agree to be bound by the following terms and conditions (the “License”). If you are accepting the License on behalf of an entity, you represent that you have the authority to do so (either you or the entity, “you”). Sample Code is not supported by Cisco TAC and is not tested for quality or performance. This is your only license to the Sample Code and all rights not expressly granted are reserved. 7 | 8 | 1. LICENSE GRANT: Subject to the terms and conditions of this License, Cisco hereby grants to you a perpetual, worldwide, non-exclusive, non-transferable, non-sublicensable, royalty-free license to copy and modify the Sample Code in source code form, and compile and redistribute the Sample Code in binary/object code or other executable forms, in whole or in part, solely for use with Cisco products and services. For interpreted languages like Java and Python, the executable form of the software may include source code and compilation is not required. 9 | 10 | 2. CONDITIONS: You shall not use the Sample Code independent of, or to replicate or compete with, a Cisco product or service. Cisco products and services are licensed under their own separate terms and you shall not use the Sample Code in any way that violates or is inconsistent with those terms (for more information, please visit: www.cisco.com/go/terms ). 11 | 12 | 3. OWNERSHIP: Cisco retains sole and exclusive ownership of the Sample Code, including all intellectual property rights therein, except with respect to any third-party material that may be used in or by the Sample Code. Any such third-party material is licensed under its own separate terms (such as an open source license) and all use must be in full accordance with the applicable license. This License does not grant you permission to use any trade names, trademarks, service marks, or product names of Cisco. If you provide any feedback to Cisco regarding the Sample Code, you agree that Cisco, its partners, and its customers shall be free to use and incorporate such feedback into the Sample Code, and Cisco products and services, for any purpose, and without restriction, payment, or additional consideration of any kind. If you initiate or participate in any litigation against Cisco, its partners, or its customers (including cross-claims and counter-claims) alleging that the Sample Code and/or its use infringe any patent, copyright, or other intellectual property right, then all rights granted to you under this License shall terminate immediately without notice. 13 | 14 | 4. LIMITATION OF LIABILITY: CISCO SHALL HAVE NO LIABILITY IN CONNECTION WITH OR RELATING TO THIS LICENSE OR USE OF THE SAMPLE CODE, FOR DAMAGES OF ANY KIND, INCLUDING BUT NOT LIMITED TO DIRECT, INCIDENTAL, AND CONSEQUENTIAL DAMAGES, OR FOR ANY LOSS OF USE, DATA, INFORMATION, PROFITS, BUSINESS, OR GOODWILL, HOWEVER CAUSED, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 15 | 16 | 5. DISCLAIMER OF WARRANTY: SAMPLE CODE IS INTENDED FOR EXAMPLE PURPOSES ONLY AND IS PROVIDED BY CISCO “AS IS” WITH ALL FAULTS AND WITHOUT WARRANTY OR SUPPORT OF ANY KIND. TO THE MAXIMUM EXTENT PERMITTED BY LAW, ALL EXPRESS AND IMPLIED CONDITIONS, REPRESENTATIONS, AND WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OR CONDITION OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, SATISFACTORY QUALITY, NON-INTERFERENCE, AND ACCURACY, ARE HEREBY EXCLUDED AND EXPRESSLY DISCLAIMED BY CISCO. CISCO DOES NOT WARRANT THAT THE SAMPLE CODE IS SUITABLE FOR PRODUCTION OR COMMERCIAL USE, WILL OPERATE PROPERLY, IS ACCURATE OR COMPLETE, OR IS WITHOUT ERROR OR DEFECT. 17 | 18 | 6. GENERAL: This License shall be governed by and interpreted in accordance with the laws of the State of California, excluding its conflict of laws provisions. You agree to comply with all applicable United States export laws, rules, and regulations. If any provision of this License is judged illegal, invalid, or otherwise unenforceable, that provision shall be severed and the rest of the License shall remain in full force and effect. No failure by Cisco to enforce any of its rights related to the Sample Code or to a breach of this License in a particular situation will act as a waiver of such rights. In the event of any inconsistencies with any other terms, this License shall take precedence. -------------------------------------------------------------------------------- /PnPNoSerialClaim/README.md: -------------------------------------------------------------------------------- 1 | # Claim PnP devices without using serial number 2 | There are a set of use cases where devices are onbaorded using PnP, without knowing the serial number in advance. 3 | A common scenario is when a site has a specific IP subnet allocated to it and potentially we know the upstream interface on the parent device. 4 | 5 | The other consideration is a discrete configuration vs using a template. If using some off-box configuration creation tool, the requirement is 6 | to allocate the file to a specifc device. 7 | 8 | With the 1.2.8 PnP process, a workflow is no longer used (the API exist), and the only option is to use a template(rather than specific configuration file). 9 | 10 | These scripts address these requirements. Specifically, they: 11 | - provide a mechansm to sync off-box configuration files into DNAC 12 | - create a workflow mapped to a specific configuration file 13 | - a mapping betwen IP subnet and upstream CDP interface on parent device and a configration file. 14 | 15 | Note, as this is an "unclaimed" process, the device will have to contact Cisco DNA Center before being claimed. When the device 16 | contacts DNAC, information such as it's IP address and CDP information are available via API. I have used interface in this 17 | example, but hostname of the parent device could also be included. 18 | 19 | ## 00_file_sync.py 20 | This script will sync the files in work_files/configs with DNAC. If a file is modified it will be updated. 21 | If a file does not exist, it will be uploaded. 22 | 23 | ```buildoutcfg 24 | $ ./00_file_sync.py 25 | Skipping File:new-perth.cfg (a623c7e2-339e-4a96-9d96-b331e834e517) SHA1hash:4d545f3c6d1071a2402c223c82dcfc1536db1ad7 26 | Skipping File:perth.cfg (fbc8064b-6f22-42fc-ae76-afb7e012ae92) SHA1hash:607af7a1d858c3286402b6a97af33162e5e99f1c 27 | Skipping File:pnp-9k.cfg (dad21f4e-c8bf-4bc5-afc6-9b44f7ff01f1) SHA1hash:503904f43461aca7dcb8fb02314c9ec4ef84b932 28 | POST https://10.66.104.121:443/api/v1/file/config 29 | Uploaded File:.gitignore (87f4a176-12ce-4d14-ab1a-480137a3da91) 30 | POST https://10.66.104.121:443/api/v1/file/config 31 | Uploaded File:access1.cfg (4d492a0f-691e-4cff-92c6-4cc04cdcf1d1) 32 | POST https://10.66.104.121:443/api/v1/file/config 33 | Uploaded File:access2.cfg (a943e69f-b6bf-4053-a600-e0ff2a06916c) 34 | Skipping File:pnp-stack.cfg (4b5aac74-b9c9-434b-b764-de99cf0050e4) SHA1hash:dfc14e00899089ff4c579e948278198cf38e49c5 35 | {'response': {'nameSpace': 'config', 'name': 'new-conf.cfg', 'downloadPath': '/file/11f08606-60ac-422f-894e-7c694c10cf43', 'fileSize': '126', 'fileFormat': 'text/plain', 'md5Checksum': '7974529437dbc958511e5e4a2dc1b733', 'sha1Checksum': '6c7650c308de00e43ac1bf781b42905aacf2c00a', 'sftpServerList': [{'sftpserverid': '4903486e-d007-460e-ab99-7164a4cb3f96', 'downloadurl': '/config/11f08606-60ac-422f-894e-7c694c10cf43/new-conf.cfg', 'status': 'SUCCESS', 'createTimeStamp': '02/07/2019 22:41:24', 'updateTimeStamp': '02/07/2019 22:41:24', 'id': '595ba540-c57f-4deb-9fc2-90946cb98436'}], 'id': '11f08606-60ac-422f-894e-7c694c10cf43'}, 'version': '1.0'} 36 | Updated File:new-conf.cfg (11f08606-60ac-422f-894e-7c694c10cf43) 37 | ``` 38 | ## 00_pnp_devices.py 39 | formatted list of devices in the PnP database. 40 | 41 | The two devices at the bottom of the list are eligable for claiming. They are both in the same subnet, but have different uplinks. 42 | ```buildoutcfg 43 | $ ./00_pnp_devices.py 44 | Name PID State Source IP Address Neighbour Interface Workflow Name 45 | adam-9800-ap AIR-AP3802I-B-K9 Onboarding Network 10.10.32.3 Default_5c377767552b2e0007d009aa 46 | 11223344556 C9300-48P Planned User Default_5c52cde5552b2e0007d010df 47 | FOC2224Z000 C9300-24P Provisioned Network 10.10.50.2 GigabitEthernet0/0/0 10.10.50.2: 48 | FCW2220L000 C9300-48U Unclaimed Network 10.10.14.6 GigabitEthernet1/0/20 49 | FDO1732Q000 WS-C3650-48PQ-E Unclaimed Network 10.10.14.4 gi2,GigabitEthernet1/0/5 50 | ``` 51 | ## no_serial_claim.py 52 | this script takes a mapping file and looks for unclaimed devices in the PnP database. If a match on subnet and upstream interfaces is found, 53 | a new workflow will be create (matching the configuration file provided) and thte device will be claimed using the workflow. 54 | 55 | In the example below, the first rule applies to a device just using the IP address. Rules #2 and #3 are differentiating two devices in the same 56 | subnet, but with different uplinks 57 | ```buildoutcfg 58 | $ cat work_files/mapping.adam 59 | subnet,upLink,configFile 60 | 10.10.50.0/24,*,new-perth.cfg 61 | 10.10.14.0/24,GigabitEthernet1/0/5,pnp-stack.cfg 62 | 10.10.14.0/24,GigabitEthernet1/0/20,pnp-9k.cfg 63 | 64 | ``` 65 | 66 | Can now run the script and the workflows will be created (using the configuration files uploaded earlier) and the devices will be claimed 67 | ```buildoutcfg 68 | $ ./no_serial_claim.py work_files/mapping.adam 69 | Using device file: work_files/mapping.adam 70 | ########################## 71 | Trying to find mapping for 10.10.14.6 72 | No Link match upstream 73 | 5c5cf2ba552b2e0007d0480e:10.10.14.0/24:GigabitEthernet1/0/20:dad21f4e-c8bf-4bc5-afc6-9b44f7ff01f1 74 | Claiming 10.10.14.6 with file dad21f4e-c8bf-4bc5-afc6-9b44f7ff01f1 75 | creating workflow 76 | Deleting Old workflow 77 | Workflow created, id 5c5cf56e552b2e0007d04864 78 | claim device 79 | {"message": "Device(s) Claimed", "statusCode": 200} 80 | Trying to find mapping for 10.10.14.4 81 | 5c5cf3d3552b2e0007d0482a:10.10.14.0/24:GigabitEthernet1/0/5:4b5aac74-b9c9-434b-b764-de99cf0050e4 82 | Claiming 10.10.14.4 with file 4b5aac74-b9c9-434b-b764-de99cf0050e4 83 | creating workflow 84 | Deleting Old workflow 85 | Workflow created, id 5c5cf56e552b2e0007d04866 86 | claim device 87 | {"message": "Device(s) Claimed", "statusCode": 200} 88 | 89 | ``` -------------------------------------------------------------------------------- /PnPNoSerialClaim/no_serial_claim.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | import csv 4 | import json 5 | import requests 6 | import os 7 | import os.path 8 | import logging 9 | from netaddr import IPNetwork, IPAddress 10 | from argparse import ArgumentParser 11 | from utils import login, get, post, delete 12 | 13 | 14 | def get_file_id(dnac, file_name): 15 | logging.debug("looking for config file:{}".format(file_name)) 16 | response = get(dnac, "dna/intent/api/v1/file/namespace/config") 17 | for file in response.json()['response']: 18 | if file['name'] == file_name: 19 | logging.debug("found file:{}".format(file['id'])) 20 | return file['id'] 21 | raise ValueError("Cannot find configuration file:{}".format(file_name)) 22 | 23 | 24 | def parse_file(mappingfile): 25 | ''' 26 | takes a mapping file and parses it. ipsubnet -> configfile 27 | Validates the ip range, and also the config file exists in DNAC. 28 | :param mappingfile: 29 | :return: 30 | ''' 31 | mapping = {} 32 | f = open(mappingfile, 'rt') 33 | try: 34 | reader = csv.DictReader(f) 35 | for ip_row in reader: 36 | logging.debug((ip_row)) 37 | file_id = get_file_id (dnac, ip_row['configFile']) 38 | logging.debug("Checking presence of configfile{}".format(file_id)) 39 | # validate the IP 40 | ip = IPNetwork(ip_row['subnet']) 41 | mapping[ip_row['subnet']+","+ip_row['upLink']] = file_id 42 | finally: 43 | f.close() 44 | return mapping 45 | 46 | def find_workflow(dnac,name): 47 | response = get(dnac, 'dna/intent/api/v1/onboarding/pnp-workflow?name={}'.format(name)) 48 | return response.json() 49 | 50 | def create_workflow(dnac, device_ip, interface, file_id): 51 | 52 | 53 | 54 | if interface == "*": 55 | interface = "" 56 | safe_interface = interface.replace("/","-") 57 | name = '{}:{}'.format(device_ip, safe_interface) 58 | 59 | old_workflow = find_workflow(dnac, name) 60 | if old_workflow != []: 61 | response = delete(dnac, "dna/intent/api/v1/onboarding/pnp-workflow/{}".format(old_workflow[0]['id'])) 62 | print ("Deleting Old workflow:{}".format(name)) 63 | logging.debug(json.dumps(response.json())) 64 | payload = { 65 | "name": name, 66 | "description": "", 67 | "currTaskIdx": 0, 68 | "tasks": [ 69 | { 70 | "configInfo": { 71 | "saveToStartUp": True, 72 | "connLossRollBack": True, 73 | "fileServiceId": file_id 74 | }, 75 | "type": "Config", 76 | "currWorkItemIdx": 0, 77 | "name": "Config Download", 78 | "taskSeqNo": 0 79 | } 80 | ], 81 | "addToInventory": True # changed for testing ADAM 82 | #"addToInventory": False 83 | } 84 | logging.debug(json.dumps(payload)) 85 | response = post(dnac, "dna/intent/api/v1/onboarding/pnp-workflow", payload) 86 | logging.debug(json.dumps(response.json())) 87 | workflow_id=response.json()['id'] 88 | print ("Workflow:{} created, id:{}".format(name,workflow_id)) 89 | return workflow_id 90 | 91 | def claim_device(dnac, device_id, workflow_id): 92 | print('claiming device') 93 | 94 | payload = { 95 | "workflowId": workflow_id, 96 | "deviceClaimList": [ 97 | { 98 | "configList": [ 99 | { 100 | "configId": "", 101 | "configParameters": [ 102 | ] 103 | } 104 | ], 105 | "deviceId": device_id 106 | } 107 | ], 108 | "populateInventory": True, # changed for testing ADAM 109 | #"populateInventory": False, 110 | "imageId": None, 111 | "projectId": None, 112 | "configId": None 113 | } 114 | 115 | logging.debug(json.dumps(payload)) 116 | response = post(dnac, "dna/intent/api/v1/onboarding/pnp-device/claim", payload) 117 | return response.json() 118 | 119 | def claim(dnac, device_ip, interface, device_id, file_id): 120 | print ("Claiming {} with file {}".format(device_ip, file_id)) 121 | workflow_id = create_workflow(dnac, device_ip, interface, file_id) 122 | result = claim_device(dnac, device_id, workflow_id) 123 | print (json.dumps(result, indent=2)) 124 | 125 | def poll_and_wait(dnac, mapping): 126 | devices = get(dnac, "dna/intent/api/v1/onboarding/pnp-device?source=Network&onbState=Initialized") 127 | 128 | for device in devices.json(): 129 | ip_address = device['deviceInfo']['httpHeaders'][0]['value'] 130 | ip = IPAddress(ip_address) 131 | 132 | try: 133 | cdp_links = [ link['remoteInterfaceName'] for link in device['deviceInfo']['neighborLinks']] 134 | except KeyError: 135 | cdp_links = [] 136 | 137 | print("Trying to find mapping for {}/{}".format(ip,cdp_links)) 138 | for network_cdp in mapping.keys(): 139 | network = network_cdp.split(',')[0] 140 | cdp = network_cdp.split(',')[1] 141 | if IPAddress(ip_address) in IPNetwork(network): 142 | if cdp == "*" or cdp in cdp_links: 143 | print ("{}:{}:{}:{}".format(device['id'],network,cdp, mapping[network_cdp])) 144 | claim(dnac, ip_address, cdp, device['id'], mapping[network_cdp]) 145 | else: 146 | print("No Link match upstream link:{}".format(cdp)) 147 | 148 | if __name__ == "__main__": 149 | # can we use default workflow? No. that will keep old configfile. need to create ta new one. 150 | # do we put or just create new? 151 | parser = ArgumentParser(description='Select options.') 152 | parser.add_argument('-v', action='store_true', 153 | help="verbose") 154 | parser.add_argument( 'mapping', type=str, 155 | help='mapping file between IP subnet, configfile') 156 | args = parser.parse_args() 157 | 158 | if args.v: 159 | logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') 160 | 161 | dnac = login() 162 | print ("Using device file:", args.mapping) 163 | mapping = parse_file(args.mapping) 164 | print ("##########################") 165 | poll_and_wait(dnac, mapping) -------------------------------------------------------------------------------- /PnP-BulkConfig-128/10_add_and_claim.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | import csv 4 | import json 5 | import requests 6 | import os 7 | import os.path 8 | import logging 9 | from argparse import ArgumentParser 10 | from utils import login, get, post 11 | 12 | logger = logging.getLogger() 13 | 14 | class SiteCache: 15 | def __init__(self, dnac): 16 | self._cache = {} 17 | response = get(dnac, "group?groupType=SITE") 18 | sites = response.json()['response'] 19 | for s in sites: 20 | self._cache[s['groupNameHierarchy']] = s['id'] 21 | def lookup(self, fqSiteName): 22 | if fqSiteName in self._cache: 23 | return self._cache[fqSiteName] 24 | else: 25 | raise ValueError("Cannot find site:{}".format(fqSiteName)) 26 | 27 | class ImageCache: 28 | def __init__(self, dnac): 29 | self._cache = {} 30 | response = get(dnac, "image/importation") 31 | sites = response.json()['response'] 32 | for s in sites: 33 | self._cache[s['name']] = s['imageUuid'] 34 | def lookup(self, imagename): 35 | if imagename in self._cache: 36 | return self._cache[imagename] 37 | else: 38 | raise ValueError("Cannot find image:{}".format(imagename)) 39 | 40 | def add_device(dnac, name, serial, pid, top_of_stack): 41 | if top_of_stack is None: 42 | stack = False 43 | else: 44 | stack = True 45 | payload = [{ 46 | "deviceInfo": { 47 | "hostname": name, 48 | "serialNumber": serial, 49 | "pid": pid, 50 | "sudiRequired": False, 51 | "userSudiSerialNos": [], 52 | "stack": stack, 53 | "aaaCredentials": { 54 | "username": "", 55 | "password": "" 56 | } 57 | } 58 | }] 59 | logger.debug(json.dumps(payload)) 60 | device = post(dnac, "onboarding/pnp-device/import", payload) 61 | try: 62 | deviceId = device.json()['successList'][0]['id'] 63 | except IndexError as e: 64 | print ('##SKIPPING device:{},{}:{}'.format(name, serial, device.json()['failureList'][0]['msg'])) 65 | deviceId = None 66 | 67 | return deviceId 68 | 69 | # other options. 70 | # type="stackSwitch" 71 | # "licenseLevel":"", 72 | # "topOfStackSerialNumber":"", 73 | def claim_device(dnac,deviceId, configId, siteId, top_of_stack, imageId, params): 74 | # if image is not None and image is not "": 75 | # logging.debug("looking for imageid for {}".format(image)) 76 | # response = get(dnac, 'image/importation?name={0}'.format(image)) 77 | # try: 78 | # imageid = response.json()['response'][0]['imageUuid'] 79 | # except IndexError: 80 | # print("Image:{} not found".format(image)) 81 | # return {"Error" : "Imnage:{} nmot found".format(image)} 82 | # else: 83 | # imageid ='' 84 | 85 | payload = { 86 | "siteId": siteId, 87 | "deviceId": deviceId, 88 | "type": "Default", 89 | "imageInfo": {"imageId": imageId, "skip": False}, 90 | "configInfo": {"configId": configId, "configParameters": params} 91 | } 92 | if top_of_stack is not None: 93 | payload['type'] = "StackSwitch" 94 | payload['topOfStackSerialNumber'] = top_of_stack 95 | logger.debug(json.dumps(payload, indent=2)) 96 | 97 | claim = post(dnac,"onboarding/pnp-device/site-claim", payload) 98 | 99 | return claim.json()['response'] 100 | 101 | def find_template_name(data, templateName): 102 | # the order of attributes is random. need to search the children. 103 | for attr in data: 104 | logger.debug("ATTR:" +json.dumps(attr, indent=2) ) 105 | if 'key' in attr: 106 | if attr['key'] == 'day0.templates': 107 | for dev in attr['attribs']: 108 | # DeviceFamily/DeviceSeries/DeviceType 109 | #template = dev['attribs'][0]['attribs'][0]['attribs'][0] 110 | for template in dev['attribs'][0]['attribs'][0]['attribs']: 111 | logger.debug(json.dumps(template, indent=2)) 112 | if template['key'] == 'template.id': 113 | for templ_attrs in template['attribs']: 114 | if templ_attrs['key'] == 'template.name' and templ_attrs['value'] == templateName: 115 | return template['value'] 116 | 117 | #if template['attribs'][1]['value'] == templateName: 118 | #return template['value'] 119 | raise ValueError("Cannot find template named:{}".format(templateName)) 120 | 121 | def find_site_template(dnac, siteId, templateName): 122 | 123 | response = get(dnac,"siteprofile/site/{}".format(siteId)) 124 | 125 | logger.debug("siteprofile {} returned:".format(siteId)) 126 | logger.debug(json.dumps(response.json(), indent=2)) 127 | 128 | if response.json()['response'] == []: 129 | raise ValueError("Cannot find Network profile for siteId:{}".format(siteId)) 130 | 131 | # need to be careful here. WLAN profiles also apply to a site. need to filter them 132 | wired_site_profile = [ site for site in response.json()['response'] if site['namespace'] != "wlan" and site['namespace'] != "routing"] 133 | 134 | # now need to find the template 135 | data = wired_site_profile[0]['profileAttributes'] 136 | return find_template_name(data, templateName) 137 | 138 | 139 | def get_template(dnac, configId, supplied_params): 140 | params=[] 141 | response = get(dnac, "template-programmer/template/{}".format(configId)) 142 | logger.debug("template {} returned:".format(configId)) 143 | logger.debug(json.dumps(response.json(),indent=2)) 144 | for vars in response.json()['templateParams']: 145 | name = vars['parameterName'] 146 | params.append({"key": name, "value": supplied_params[name]}) 147 | #print params 148 | return params 149 | 150 | def create_and_upload(dnac, site_cache, image_cache, devices): 151 | 152 | f = open(devices, 'rt') 153 | try: 154 | reader = csv.DictReader(f) 155 | for device_row in reader: 156 | #print ("Variables:",device_row) 157 | 158 | try: 159 | siteId = site_cache.lookup(device_row['siteName']) 160 | #image is optional 161 | if 'image' in device_row and device_row['image'] != '': 162 | imageId = image_cache.lookup(device_row['image']) 163 | else: 164 | imageId = '' 165 | 166 | except ValueError as e: 167 | print("##ERROR {},{}: {}".format(device_row['name'],device_row['serial'], e)) 168 | continue 169 | # need to get templateId from Site.. 170 | configId = find_site_template(dnac, siteId, device_row['templateName']) 171 | params = get_template(dnac, configId, device_row) 172 | 173 | if 'topOfStack' in device_row: 174 | top_of_stack = device_row['topOfStack'] 175 | else: 176 | top_of_stack = None 177 | 178 | # add device to PnP 179 | deviceId = add_device(dnac, device_row['name'], device_row['serial'], device_row['pid'], top_of_stack) 180 | if deviceId is not None: 181 | #claim the device if sucessfully added 182 | claim_status = claim_device(dnac, deviceId, configId, siteId, top_of_stack, imageId, params) 183 | if "Claimed" in claim_status: 184 | status = "PLANNED" 185 | else: 186 | status = "FAILED" 187 | print ('Device:{} name:{} siteName:{} Status:{}'.format(device_row['serial'], 188 | device_row['name'], 189 | device_row['siteName'], 190 | status)) 191 | finally: 192 | f.close() 193 | 194 | if __name__ == "__main__": 195 | parser = ArgumentParser(description='Select options.') 196 | parser.add_argument( 'devices', type=str, 197 | help='device inventory csv file') 198 | parser.add_argument('-v', action='store_true', 199 | help="verbose") 200 | args = parser.parse_args() 201 | 202 | if args.v: 203 | handler = logging.StreamHandler() 204 | formatter = logging.Formatter( 205 | '%(asctime)s %(name)-12s %(levelname)-8s %(message)s') 206 | handler.setFormatter(formatter) 207 | logger.addHandler(handler) 208 | logger.setLevel(logging.DEBUG) 209 | 210 | # set logger 211 | logger.debug("Logging enabled") 212 | dnac = login() 213 | site_cache = SiteCache(dnac) 214 | image_cache = ImageCache(dnac) 215 | 216 | 217 | print ("Using device file:", args.devices) 218 | 219 | print ("##########################") 220 | create_and_upload(dnac, site_cache, image_cache, devices=args.devices) 221 | --------------------------------------------------------------------------------