├── dnac_config.py ├── device-to-site.json ├── README.md ├── CONTRIBUTING.md ├── site-info.json ├── LICENSE ├── device-to-site.py └── create-site.py /dnac_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | DNAC=os.environ.get('DNAC','172.28.97.216') 3 | DNAC_PORT=os.environ.get('DNAC_PORT',443) 4 | DNAC_USER=os.environ.get('DNAC_USER','admin') 5 | DNAC_PASSWORD=os.environ.get('DNAC_PASSWORD','Cisco123') -------------------------------------------------------------------------------- /device-to-site.json: -------------------------------------------------------------------------------- 1 | { 2 | "device": [ 3 | { 4 | "serialNumber": "FOC1703V36B", 5 | "siteName": "Area1-BLD1" 6 | }, 7 | { 8 | "serialNumber": "FTX1842AHM1", 9 | "siteName": "Area1-BLD1" 10 | }, 11 | { 12 | "serialNumber": "FTX1842AHM2", 13 | "siteName": "Area1-BLD2" 14 | }, 15 | { 16 | "serialNumber": "FOX1525G5S1", 17 | "siteName": "Area1-BLD2" 18 | }, 19 | { 20 | "serialNumber": "FOX1524GV2Z", 21 | "siteName": "Area3-BLD1" 22 | }, 23 | { 24 | "serialNumber": "FXS1825Q1PA", 25 | "siteName": "Area3-BLD1" 26 | }, 27 | { 28 | "serialNumber": "FCW1630L0JG", 29 | "siteName": "Area3-BLD2" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cisco DNA Center - Assign Network Devices to Site 2 | 3 | Dynamically create and assign sites and devices using Cisco DNA Center APIs 4 | - Obtain the Device to Site Mapping 5 | - Source is a JSON file 6 | - Get Site and Device information 7 | - Assign Network Device to Site 8 | - Ready for Cisco DNA Center Automation and Assurance 9 | 10 | ## Getting Started 11 | Use the following command to create your sites in Cisco DNA Center. Sites are defined in [site-info.json](./site-info.json) file: 12 | 13 | ```bash 14 | python create-site.py site-info.json 15 | ``` 16 | 17 | After you've succesfully setup your sites, you can start assigning devices to sites. Device to site assignment is defined in [device-to-site.json](./device-to-site.json) file. Run the command below once ready: 18 | ```bash 19 | python device-to-site.py device-to-site.json 20 | ``` 21 | 22 | > Note: You change the controller credentials either through environment variables or by editing the dnac_config.py file 23 | 24 | ## Contributing 25 | 26 | Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Guidance on how to contribute 2 | 3 | Contributions to this code are welcome and appreciated. 4 | 5 | 6 | > All contributions to this code will be released under the terms of the [LICENSE](./LICENSE) of this code. By submitting a pull request or filing a bug, issue, or feature request, you are agreeing to comply with this waiver of copyright interest. Details can be found in our [LICENSE](./LICENSE). 7 | 8 | There are two primary ways to contribute: 9 | 10 | 1. Using the issue tracker 11 | 2. Changing the codebase 12 | 13 | 14 | ## Using the issue tracker 15 | 16 | Use the issue tracker to suggest feature requests, report bugs, and ask questions. This is also a great way to connect with the developers of the project as well as others who are interested in this solution. 17 | 18 | Use the issue tracker to find ways to contribute. Find a bug or a feature, mention in the issue that you will take on that effort, then follow the _Changing the codebase_ guidance below. 19 | 20 | 21 | ## Changing the codebase 22 | 23 | Generally speaking, you should fork this repository, make changes in your own fork, and then submit a pull request. All new code should have associated unit tests (if applicable) that validate implemented features and the presence or lack of defects. 24 | 25 | Additionally, the code should follow any stylistic and architectural guidelines prescribed by the project. In the absence of such guidelines, mimic the styles and patterns in the existing codebase. -------------------------------------------------------------------------------- /site-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "area": [ 3 | { 4 | "name": "US-East", 5 | "parentName": "Global" 6 | }, 7 | { 8 | "name": "Area1", 9 | "parentName": "US-East" 10 | }, 11 | { 12 | "name": "Area2", 13 | "parentName": "US-East" 14 | }, 15 | { 16 | "name": "Area3", 17 | "parentName": "US-East" 18 | } 19 | ], 20 | "building": [ 21 | { 22 | "area_name": "Area1", 23 | "area_parentName": "US-East", 24 | "bld_name": "Area1-BLD1", 25 | "bld_address": "170 West Tasman Drive, San Jose 95134" 26 | }, 27 | { 28 | "area_name": "Area1", 29 | "area_parentName": "US-East", 30 | "bld_name": "Area1-BLD2", 31 | "bld_address": "171 West Tasman Drive, San Jose 95134" 32 | }, 33 | { 34 | "area_name": "Area2", 35 | "area_parentName": "US-East", 36 | "bld_name": "Area2-BLD1", 37 | "bld_address": "180 West Tasman Drive, San Jose 95134" 38 | }, 39 | { 40 | "area_name": "Area3", 41 | "area_parentName": "US-East", 42 | "bld_name": "Area3-BLD1", 43 | "bld_address": "190 West Tasman Drive, San Jose 95134" 44 | }, 45 | { 46 | "area_name": "Area3", 47 | "area_parentName": "US-East", 48 | "bld_name": "Area3-BLD2", 49 | "bld_address": "191 West Tasman Drive, San Jose 95134" 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CISCO SAMPLE CODE LICENSE 2 | 3 | Version 1.1 4 | Copyright (c) 2019 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. 19 | -------------------------------------------------------------------------------- /device-to-site.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import sys 4 | import time 5 | import logging 6 | import time 7 | 8 | from dnac_config import DNAC, DNAC_PORT, DNAC_USER, DNAC_PASSWORD 9 | from requests.auth import HTTPBasicAuth 10 | requests.packages.urllib3.disable_warnings() 11 | 12 | # ------------------------------------------------------------------- 13 | # Custom exception definitions 14 | # ------------------------------------------------------------------- 15 | class TaskTimeoutError(Exception): 16 | pass 17 | 18 | class TaskError(Exception): 19 | pass 20 | 21 | # API ENDPOINTS 22 | ENDPOINT_TICKET = "ticket" 23 | ENDPOINT_TASK_SUMMARY ="task/%s" 24 | RETRY_INTERVAL=2 25 | 26 | # ------------------------------------------------------------------- 27 | # Site Create functions - Area, Building, Floor(to be added) 28 | # ------------------------------------------------------------------- 29 | 30 | def get_site_info(timestamp): 31 | return get_url("intent/api/v1/site-health?timestamp=" + str(timestamp)) 32 | 33 | def get_device_ip_address(serial_number): 34 | device_detail = get_url("intent/api/v1/network-device/serial-number/" + serial_number) 35 | return device_detail["response"]["managementIpAddress"] 36 | 37 | def add_device_to_site(site_id, device_ipAddress): 38 | payload = { 39 | "device": [ 40 | { 41 | "ip": device_ipAddress 42 | } 43 | ] 44 | } 45 | 46 | print(json.dumps(payload)) 47 | result = post_url("intent/api/v1/site/" + site_id + "/device", payload) 48 | return result 49 | 50 | # ------------------------------------------------------------------- 51 | # Helper functions 52 | # ------------------------------------------------------------------- 53 | 54 | def get_url(url): 55 | url = create_url(path=url) 56 | print(url) 57 | token = get_auth_token() 58 | headers = {'X-auth-token' : token['token']} 59 | try: 60 | response = requests.get(url, headers=headers, verify=False) 61 | except requests.exceptions.RequestException as cerror: 62 | print("Error processing request", cerror) 63 | sys.exit(1) 64 | 65 | return response.json() 66 | 67 | def create_url(path, controller_ip=DNAC): 68 | """ Helper function to create a DNAC API endpoint URL 69 | """ 70 | 71 | return "https://%s:%s/dna/%s" % (controller_ip, DNAC_PORT, path) 72 | 73 | def get_auth_token(controller_ip=DNAC, username=DNAC_USER, password=DNAC_PASSWORD): 74 | """ Authenticates with controller and returns a token to be used in subsequent API invocations 75 | """ 76 | 77 | login_url = "https://{0}:{1}/api/system/v1/auth/token".format(controller_ip, DNAC_PORT) 78 | result = requests.post(url=login_url, auth=HTTPBasicAuth(DNAC_USER, DNAC_PASSWORD), verify=False) 79 | result.raise_for_status() 80 | 81 | token = result.json()["Token"] 82 | return { 83 | "controller_ip": controller_ip, 84 | "token": token 85 | } 86 | 87 | def post_url(url, payload): 88 | token = get_auth_token() 89 | url = create_url(path=url) 90 | headers= { 91 | 'x-auth-token': token['token'], 92 | 'content-type' : 'application/json', 93 | '__runsync': "true", 94 | '__timeout': "30", 95 | '__persistbapioutput': "true" 96 | } 97 | 98 | try: 99 | response = requests.post(url, headers=headers, data=json.dumps(payload), verify=False) 100 | except requests.exceptions.RequestException as cerror: 101 | print ("Error processing request", cerror) 102 | sys.exit(1) 103 | 104 | return response.json() 105 | 106 | #----------------------------------------- 107 | # Main function 108 | #----------------------------------------- 109 | 110 | site_to_id = {} 111 | 112 | print "Input File Name: " + sys.argv[1] 113 | 114 | with open(sys.argv[1]) as f: 115 | data = json.load(f) 116 | 117 | print "Curent time: " + str(int(time.time() * 1000)) + " milliseconds" 118 | 119 | siteInfo = get_site_info(int(time.time() * 1000)) 120 | 121 | for site in siteInfo['response']: 122 | site_to_id[site["siteName"]] = site["siteId"] 123 | 124 | for key in data["device"]: 125 | print key["serialNumber"], key["siteName"] 126 | ip_address = get_device_ip_address(key["serialNumber"]) 127 | print "Site Id :" + site_to_id[key["siteName"]] 128 | print "Device IP Address :" + ip_address 129 | response = add_device_to_site(site_to_id[key["siteName"]], ip_address) 130 | print(json.dumps(response)) 131 | time.sleep(20) 132 | 133 | print "\n\n---------- Adding Devices to Site complete !! ----------\n\n" 134 | -------------------------------------------------------------------------------- /create-site.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import sys 4 | import time 5 | import logging 6 | 7 | from dnac_config import DNAC, DNAC_PORT, DNAC_USER, DNAC_PASSWORD 8 | from requests.auth import HTTPBasicAuth 9 | requests.packages.urllib3.disable_warnings() 10 | 11 | # ------------------------------------------------------------------- 12 | # Custom exception definitions 13 | # ------------------------------------------------------------------- 14 | class TaskTimeoutError(Exception): 15 | pass 16 | 17 | class TaskError(Exception): 18 | pass 19 | 20 | # API ENDPOINTS 21 | ENDPOINT_TICKET = "ticket" 22 | ENDPOINT_TASK_SUMMARY ="task/%s" 23 | RETRY_INTERVAL=2 24 | 25 | # ------------------------------------------------------------------- 26 | # Site Create functions - Area, Building, Floor(to be added) 27 | # ------------------------------------------------------------------- 28 | 29 | def create_area_request(area_name, area_parentName): 30 | payload = { 31 | "type": "area", 32 | "site": { 33 | "area": { 34 | "name": area_name, 35 | "parentName": area_parentName 36 | } 37 | } 38 | } 39 | 40 | print(json.dumps(payload)) 41 | result = post_url("system/api/v1/site", payload) 42 | return result 43 | 44 | def create_bld_request(area_name, area_parentName, bld_name, bld_address): 45 | payload = { 46 | "type": "building", 47 | "site": { 48 | "area": { 49 | "name": area_name, 50 | "parentName": area_parentName 51 | }, 52 | "building": { 53 | "name": bld_name, 54 | "address": bld_address 55 | } 56 | } 57 | } 58 | 59 | print(json.dumps(payload)) 60 | result = post_url("system/api/v1/site", payload) 61 | return result 62 | 63 | # ------------------------------------------------------------------- 64 | # Helper functions 65 | # ------------------------------------------------------------------- 66 | 67 | def get_url(url): 68 | url = create_url(path=url) 69 | print(url) 70 | token = get_auth_token() 71 | headers = {'X-auth-token' : token['token']} 72 | try: 73 | response = requests.get(url, headers=headers, verify=False) 74 | except requests.exceptions.RequestException as cerror: 75 | print("Error processing request", cerror) 76 | sys.exit(1) 77 | 78 | return response.json() 79 | 80 | def create_url(path, controller_ip=DNAC): 81 | """ Helper function to create a DNAC API endpoint URL 82 | """ 83 | 84 | return "https://%s:%s/dna/%s" % (controller_ip, DNAC_PORT, path) 85 | 86 | def get_auth_token(controller_ip=DNAC, username=DNAC_USER, password=DNAC_PASSWORD): 87 | """ Authenticates with controller and returns a token to be used in subsequent API invocations 88 | """ 89 | 90 | login_url = "https://{0}:{1}/api/system/v1/auth/token".format(controller_ip, DNAC_PORT) 91 | result = requests.post(url=login_url, auth=HTTPBasicAuth(DNAC_USER, DNAC_PASSWORD), verify=False) 92 | result.raise_for_status() 93 | 94 | token = result.json()["Token"] 95 | return { 96 | "controller_ip": controller_ip, 97 | "token": token 98 | } 99 | 100 | def post_url(url, payload): 101 | token = get_auth_token() 102 | url = create_url(path=url) 103 | headers= { 104 | 'x-auth-token': token['token'], 105 | 'content-type' : 'application/json', 106 | '__runsync': "true", 107 | '__timeout': "30", 108 | '__persistbapioutput': "true" 109 | } 110 | 111 | try: 112 | response = requests.post(url, headers=headers, data=json.dumps(payload), verify=False) 113 | except requests.exceptions.RequestException as cerror: 114 | print ("Error processing request", cerror) 115 | sys.exit(1) 116 | 117 | return response.json() 118 | 119 | #----------------------------------------- 120 | # Main function 121 | #----------------------------------------- 122 | 123 | print "File Name " + sys.argv[1] 124 | 125 | with open(sys.argv[1]) as f: 126 | data = json.load(f) 127 | 128 | for key in data["area"]: 129 | print key["name"], key["parentName"] 130 | response = create_area_request(key["name"], key["parentName"]) 131 | print(json.dumps(response)) 132 | 133 | print "\n\n---------- Area Creation complete !! ----------\n\n" 134 | 135 | for key in data["building"]: 136 | print key["area_name"], key["area_parentName"], key["bld_name"], key["bld_address"] 137 | response = create_bld_request(key["area_name"], key["area_parentName"], key["bld_name"], key["bld_address"]) 138 | print(json.dumps(response)) 139 | 140 | print "\n\n---------- Building Creation complete !! ----------\n\n" 141 | 142 | print "\n\n---------- Site Creation complete !! ----------\n\n" 143 | --------------------------------------------------------------------------------