├── .gitignore ├── .idea ├── dictionaries │ └── rsayle.xml ├── encodings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml └── vcs.xml ├── HEADER ├── LICENSE ├── MANIFEST.in ├── README.md ├── dnac ├── __init__.py ├── basicauth.py ├── client.py ├── commandrunner.py ├── commandrunner_task.py ├── config_archive.py ├── config_archive_settings.py ├── crud.py ├── ctype.py ├── deployment.py ├── device_archive.py ├── device_archive_task.py ├── dnac_config.py ├── dnacapi.py ├── file.py ├── networkdevice.py ├── project.py ├── site.py ├── site_hierarchy.py ├── task.py ├── template.py ├── timestamp.py ├── version.py └── xauthtoken.py ├── docs ├── Cisco DNAC Wrapper UML.pdf ├── Cisco DNAC Wrapper UML.vsdx ├── Dnac.html ├── DnacError.html ├── basicauth.html ├── client.html ├── commandrunner.html ├── commandrunner_task.html ├── config_archive.html ├── config_archive_settings.html ├── config_archiver.pdf ├── config_archiver.vsdx ├── crud.html ├── ctype.html ├── deployment.html ├── device_archive.html ├── device_archive_task.html ├── dnac.html ├── dnac_config.html ├── dnacapi.html ├── file.html ├── intercluster_template_manager.pdf ├── intercluster_template_manager.vsdx ├── mkdocs ├── network_hierarchy_replicator.pdf ├── network_hierarchy_replicator.vsdx ├── networkdevice.html ├── project.html ├── site.html ├── task.html ├── template.html ├── template_replicator.pdf ├── template_replicator.vsdx ├── timestamp.html ├── version.html └── xauthtoken.html ├── examples ├── .gitignore ├── commandrunner_example.py ├── config_archiver │ ├── config_archiver.py │ └── views │ │ ├── add_device_archive_version.tpl │ │ ├── add_new_device_archive_version.tpl │ │ ├── change_settings.tpl │ │ ├── config_archiver.tpl │ │ ├── create_new_archive.tpl │ │ ├── create_new_device_archive.tpl │ │ ├── delete_archives.tpl │ │ ├── delete_config.tpl │ │ ├── delete_device_archive_versions.tpl │ │ ├── manage_archive.tpl │ │ ├── manage_archive_configs.tpl │ │ ├── manage_settings.tpl │ │ └── view_config.tpl ├── device_finder │ ├── device_finder.py │ ├── dnac_clusters.json │ └── views │ │ ├── results.tpl │ │ └── select_device.tpl ├── export_project.py ├── export_template.py ├── import_project.py ├── import_template.py ├── network_hierarchy_replicator │ ├── dnac_clusters.json │ ├── network_hierarchy_replicator.py │ └── views │ │ ├── replicate_sites.tpl │ │ ├── select_clusters.tpl │ │ └── select_sites.tpl ├── networkdevice_example.py ├── site_hierarchy_replicator │ ├── dnac_clusters.json │ ├── site_hierarchy_replicator.py │ └── views │ │ ├── replicate_sites.tpl │ │ ├── select_clusters.tpl │ │ └── select_sites.tpl ├── template_example.py └── template_replicator │ ├── dnac_clusters.json │ ├── template_replicator.py │ └── views │ ├── replicate_projects.tpl │ ├── replicate_projects_and_templates.tpl │ ├── replicate_templates.tpl │ ├── select_clusters.tpl │ ├── select_projects.tpl │ └── select_templates.tpl └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled python modules 2 | *.pyc 3 | 4 | # setuptools distribution folder 5 | /dist 6 | 7 | # setuptools build folder 8 | /build 9 | 10 | # python egg metadata 11 | /*.egg-info 12 | 13 | # pycharm cache 14 | /.idea 15 | 16 | -------------------------------------------------------------------------------- /.idea/dictionaries/rsayle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cisco 5 | dnac 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /HEADER: -------------------------------------------------------------------------------- 1 | Copyright (c) {{current_year}} Cisco and/or its affiliates. 2 | 3 | This software is licensed to you under the terms of the Cisco Sample 4 | Code License, Version 1.0 (the "License"). You may obtain a copy of the 5 | License at 6 | 7 | https://developer.cisco.com/docs/licenses 8 | 9 | All use of the material herein must be in accordance with the terms of 10 | the License. All rights not expressly granted by the License are 11 | reserved. Unless required by applicable law or agreed to separately in 12 | writing, software distributed under the License is distributed on an "AS 13 | IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 14 | or implied. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CISCO SAMPLE CODE LICENSE 2 | Version 1.0 3 | Copyright (c) 2017 Cisco and/or its affiliates 4 | 5 | These terms govern this Cisco example or demo source code and its 6 | associated documentation (together, the "Sample Code"). By downloading, 7 | copying, modifying, compiling, or redistributing the Sample Code, you 8 | accept and agree to be bound by the following terms and conditions (the 9 | "License"). If you are accepting the License on behalf of an entity, you 10 | represent that you have the authority to do so (either you or the entity, 11 | "you"). Sample Code is not supported by Cisco TAC and is not tested for 12 | quality or performance. This is your only license to the Sample Code and 13 | all rights not expressly granted are reserved. 14 | 15 | 1. LICENSE GRANT: Subject to the terms and conditions of this License, 16 | Cisco hereby grants to you a perpetual, worldwide, non-exclusive, non- 17 | transferable, non-sublicensable, royalty-free license to copy and 18 | modify the Sample Code in source code form, and compile and 19 | redistribute the Sample Code in binary/object code or other executable 20 | forms, in whole or in part, solely for use with Cisco products and 21 | services. For interpreted languages like Java and Python, the 22 | executable form of the software may include source code and 23 | compilation is not required. 24 | 25 | 2. CONDITIONS: You shall not use the Sample Code independent of, or to 26 | replicate or compete with, a Cisco product or service. Cisco products 27 | and services are licensed under their own separate terms and you shall 28 | not use the Sample Code in any way that violates or is inconsistent 29 | with those terms (for more information, please visit: 30 | www.cisco.com/go/terms. 31 | 32 | 3. OWNERSHIP: Cisco retains sole and exclusive ownership of the Sample 33 | Code, including all intellectual property rights therein, except with 34 | respect to any third-party material that may be used in or by the 35 | Sample Code. Any such third-party material is licensed under its own 36 | separate terms (such as an open source license) and all use must be in 37 | full accordance with the applicable license. This License does not 38 | grant you permission to use any trade names, trademarks, service 39 | marks, or product names of Cisco. If you provide any feedback to Cisco 40 | regarding the Sample Code, you agree that Cisco, its partners, and its 41 | customers shall be free to use and incorporate such feedback into the 42 | Sample Code, and Cisco products and services, for any purpose, and 43 | without restriction, payment, or additional consideration of any kind. 44 | If you initiate or participate in any litigation against Cisco, its 45 | partners, or its customers (including cross-claims and counter-claims) 46 | alleging that the Sample Code and/or its use infringe any patent, 47 | copyright, or other intellectual property right, then all rights 48 | granted to you under this License shall terminate immediately without 49 | notice. 50 | 51 | 4. LIMITATION OF LIABILITY: CISCO SHALL HAVE NO LIABILITY IN CONNECTION 52 | WITH OR RELATING TO THIS LICENSE OR USE OF THE SAMPLE CODE, FOR 53 | DAMAGES OF ANY KIND, INCLUDING BUT NOT LIMITED TO DIRECT, INCIDENTAL, 54 | AND CONSEQUENTIAL DAMAGES, OR FOR ANY LOSS OF USE, DATA, INFORMATION, 55 | PROFITS, BUSINESS, OR GOODWILL, HOWEVER CAUSED, EVEN IF ADVISED OF THE 56 | POSSIBILITY OF SUCH DAMAGES. 57 | 58 | 5. DISCLAIMER OF WARRANTY: SAMPLE CODE IS INTENDED FOR EXAMPLE PURPOSES 59 | ONLY AND IS PROVIDED BY CISCO "AS IS" WITH ALL FAULTS AND WITHOUT 60 | WARRANTY OR SUPPORT OF ANY KIND. TO THE MAXIMUM EXTENT PERMITTED BY 61 | LAW, ALL EXPRESS AND IMPLIED CONDITIONS, REPRESENTATIONS, AND 62 | WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OR 63 | CONDITION OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON- 64 | INFRINGEMENT, SATISFACTORY QUALITY, NON-INTERFERENCE, AND ACCURACY, 65 | ARE HEREBY EXCLUDED AND EXPRESSLY DISCLAIMED BY CISCO. CISCO DOES NOT 66 | WARRANT THAT THE SAMPLE CODE IS SUITABLE FOR PRODUCTION OR COMMERCIAL 67 | USE, WILL OPERATE PROPERLY, IS ACCURATE OR COMPLETE, OR IS WITHOUT 68 | ERROR OR DEFECT. 69 | 70 | 6. GENERAL: This License shall be governed by and interpreted in 71 | accordance with the laws of the State of California, excluding its 72 | conflict of laws provisions. You agree to comply with all applicable 73 | United States export laws, rules, and regulations. If any provision of 74 | this License is judged illegal, invalid, or otherwise unenforceable, 75 | that provision shall be severed and the rest of the License shall 76 | remain in full force and effect. No failure by Cisco to enforce any of 77 | its rights related to the Sample Code or to a breach of this License 78 | in a particular situation will act as a waiver of such rights. In the 79 | event of any inconsistencies with any other terms, this License shall 80 | take precedence. 81 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include HEADER 4 | -------------------------------------------------------------------------------- /dnac/basicauth.py: -------------------------------------------------------------------------------- 1 | 2 | from base64 import b64encode 3 | 4 | 5 | class BasicAuth(object): 6 | """ 7 | Class BasicAuth stores the username and password for basic authorization of an HTTP request and then encodes it as 8 | "username:password" in base64 format. The encoded string is used to authenticate connections to an API server. 9 | """ 10 | 11 | def __init__(self, user, passwd): 12 | """ 13 | The __init__ method initializes a BasicAuth object. It takes in a username and password, sets the __user and 14 | __passwd, respectively, and then uses those values to create a base64 encoded string for the __creds attribute 15 | and a header dictionary for __hdrs. 16 | 17 | Usage: b = BasicAuth(user, passwd) 18 | 19 | :param user: The username for logging into an HTTP server 20 | type: str 21 | required: yes 22 | default: none 23 | :param passwd: The password for logging into an HTTP server 24 | type: str 25 | required: yes 26 | default: none 27 | """ 28 | self.__user = user 29 | self.__passwd = passwd 30 | self.__creds = self.make_creds() 31 | self.__hdrs = {'Authorization': ('Basic %s' % self.__creds)} 32 | 33 | def __str__(self): 34 | """ 35 | String handler for a BasicAuth object. 36 | :return: base64 encoded string ":" 37 | """ 38 | return self.__creds 39 | 40 | def make_creds(self): 41 | """ 42 | Builds the base64 encoded string : for basic authentication to an API server. 43 | :return: base64 encoded str 44 | """ 45 | credstr = '%s:%s' % (self.__user, self.__passwd) 46 | cred64 = b64encode(credstr.encode()) 47 | return cred64.decode() 48 | 49 | @property 50 | def creds(self): 51 | """ 52 | Getter method to retrieve the credentials stored in the object instance. 53 | :return: base64 encoded string 54 | """ 55 | return self.__creds 56 | 57 | @property 58 | def hdrs(self): 59 | """ 60 | Getter method that returns the format of a basic auth request for an API request. 61 | :return: dict of the form {'Authorization': 'Basic ' 62 | """ 63 | return self.__hdrs 64 | 65 | # end class BasicAuth() 66 | 67 | -------------------------------------------------------------------------------- /dnac/client.py: -------------------------------------------------------------------------------- 1 | 2 | from dnac import DnacError, \ 3 | SUPPORTED_DNAC_VERSIONS, \ 4 | UNSUPPORTED_DNAC_VERSION 5 | from dnac.dnacapi import DnacApi, \ 6 | DnacApiError 7 | from dnac.crud import OK, \ 8 | REQUEST_NOT_OK, \ 9 | ERROR_MSGS 10 | from dnac.timestamp import TimeStamp 11 | 12 | # globals 13 | 14 | MODULE = 'client.py' 15 | 16 | CLIENT_RESOURCE_PATH = { 17 | '1.2.10': '/dna/intent/api/v1/client-detail', 18 | '1.3.0.2': '/dna/intent/api/v1/client-detail', 19 | '1.3.0.3': '/dna/intent/api/v1/client-detail', 20 | '1.3.1.3': '/dna/intent/api/v1/client-detail', 21 | '1.3.1.4': '/dna/intent/api/v1/client-detail' 22 | } 23 | 24 | NULL_MAC = '00:00:00:00:00:00' 25 | BCAST_MAC = 'FF:FF:FF:FF:FF:FF' 26 | ILLEGAL_MAC_ADDRS = [ 27 | NULL_MAC, 28 | BCAST_MAC 29 | ] 30 | 31 | # error messages 32 | ILLEGAL_MAC = 'Illegal MAC address' 33 | SET_CLIENT_MAC = 'Set the client\'s MAC address to a legal value' 34 | CLIENT_NOT_FOUND = 'Requested client could not be found' 35 | 36 | class Client(DnacApi): 37 | """ 38 | The Client class represents a host or end-point as represented in Cisco DNA Center. 39 | In this API's current form, client's can only be searched using their MAC address. 40 | Once located, the API returns the client's state according to a given time stamp. At 41 | the moment, this class pulls the client's state for the current time only. 42 | 43 | Attributes: 44 | dnac: A reference to the Dnac instance that contains a Client object 45 | type: Dnac object 46 | default: none 47 | scope: protected 48 | name: A user friendly name for the Client object used as a key for finding it in a Dnac.api attribute. 49 | type: str 50 | default: none 51 | scope: protected 52 | mac: The end-point's MAC address 53 | type: str 54 | default: 00:00:00:00:00:00 55 | scope: public 56 | client_detail: The host's current state 57 | type: dict 58 | scope: protected 59 | verify: A flag used to check Cisco DNAC's certificate. 60 | type: boolean 61 | default: False 62 | required: no 63 | timeout: The number of seconds to wait for Cisco DNAC's 64 | response. 65 | type: int 66 | default: 5 67 | required: no 68 | 69 | Usage: 70 | d = Dnac() 71 | host = Client(d, 'myPC', mac='a1:b2:c3:d4:e5:f6') 72 | """ 73 | 74 | def __init__(self, 75 | dnac, 76 | name, 77 | mac=NULL_MAC, 78 | verify=False, 79 | timeout=5): 80 | """ 81 | Creates a new client object. 82 | :param dnac: A reference to the program's Dnac instance. 83 | type: Dnac object 84 | required: yes 85 | default: None 86 | :param name: The client's name. 87 | type: str 88 | required: yes 89 | default: None 90 | :param mac: The client's MAC address 91 | type: str 92 | required: no 93 | default: NULL_MAC (00:00:00:00:00:00) 94 | :param verify: A flag to determine whether or not to verify Cisco DNA Center's certificate. 95 | type: bool 96 | required: no 97 | default: False 98 | :param timeout: The time in seconds to wait for Cisco DNAC's response. 99 | type: int 100 | required: no 101 | default: 5 102 | """ 103 | if dnac.version in SUPPORTED_DNAC_VERSIONS: 104 | path = CLIENT_RESOURCE_PATH[dnac.version] 105 | else: 106 | raise DnacError( 107 | '__init__: %s: %s' % 108 | (UNSUPPORTED_DNAC_VERSION, dnac.version) 109 | ) 110 | self.__mac = mac 111 | self.__client_detail = {} 112 | super(Client, self).__init__(dnac, 113 | name, 114 | resource=path, 115 | verify=verify, 116 | timeout=timeout) 117 | 118 | # end __init__() 119 | 120 | @property 121 | def mac(self): 122 | """ 123 | Get method mac return's the Client's assigned MAC address. 124 | :return: str 125 | """ 126 | return self.__mac 127 | 128 | # end mac getter 129 | 130 | @mac.setter 131 | def mac(self, mac): 132 | """ 133 | Set method mac changes the Client object's mac attribute. 134 | :param mac: The new MAC address. 135 | type: str 136 | default: none 137 | required yes 138 | :return: 139 | """ 140 | self.__mac = mac 141 | 142 | # end mac setter 143 | 144 | @property 145 | def client_detail(self): 146 | """ 147 | Get method client_detail return's the Client's state information 148 | :return: dict 149 | """ 150 | return self.__client_detail 151 | 152 | # end client_detail getter 153 | 154 | def get_client_detail(self): 155 | """ 156 | Get method get_client_detail makes a call to Cisco DNAC, retrieves the Client's state information, stores it in 157 | the client_detail attribute, and also returns the results for further processing. 158 | :return: dict 159 | """ 160 | if self.__mac in ILLEGAL_MAC_ADDRS: 161 | raise DnacApiError( 162 | MODULE, 'get_client_detail', ILLEGAL_MAC, '', 163 | '', self.__mac, '', '' 164 | ) 165 | time = TimeStamp() 166 | query = '?timestamp=%s&macAddress=%s' % (time, self.__mac) 167 | url = self.dnac.url + self.resource + query 168 | detail, status = self.crud.get(url, 169 | headers=self.dnac.hdrs, 170 | verify=self.verify, 171 | timeout=self.timeout) 172 | if status != OK: 173 | raise DnacApiError( 174 | MODULE, 'get_client_detail', REQUEST_NOT_OK, url, 175 | OK, status, ERROR_MSGS[status], str(detail) 176 | ) 177 | self.__client_detail = detail 178 | return self.__client_detail 179 | 180 | # end get_client_detail() 181 | 182 | # end class Client() 183 | 184 | -------------------------------------------------------------------------------- /dnac/commandrunner.py: -------------------------------------------------------------------------------- 1 | 2 | from dnac import DnacError, \ 3 | SUPPORTED_DNAC_VERSIONS, \ 4 | UNSUPPORTED_DNAC_VERSION 5 | from dnac.dnacapi import DnacApi, \ 6 | DnacApiError 7 | from dnac.crud import ACCEPTED, \ 8 | REQUEST_NOT_ACCEPTED, \ 9 | ERROR_MSGS 10 | from dnac.commandrunner_task import CommandRunnerTask 11 | import json 12 | import time 13 | 14 | MODULE = 'commandrunner.py' 15 | 16 | COMMANDRUNNER_RESOURCE_PATH = { 17 | '1.2.8': '/api/v1/network-device-poller/cli/read-request', 18 | '1.2.10': '/api/v1/network-device-poller/cli/read-request', 19 | '1.3.0.2': '/api/v1/network-device-poller/cli/read-request', 20 | '1.3.0.3': '/api/v1/network-device-poller/cli/read-request', 21 | '1.3.1.3': '/api/v1/network-device-poller/cli/read-request', 22 | '1.3.1.4': '/api/v1/network-device-poller/cli/read-request' 23 | } 24 | 25 | 26 | class CommandRunner(DnacApi): 27 | """ 28 | The CommandRunner class provides the interface for running CLI commands on DNA Center. Note that the command 29 | runner API only allows read-only commands, i.e. show commands. 30 | 31 | Command sets must be formatted as a dictionary with two lists. The first item uses "commands" as its key and then 32 | has a list of the actual CLI commands to run. The second value's key is "deviceUuids" and its values are the 33 | device IDs where the commands will be run. CommandRunner provides two functions to help produce the dictionary 34 | used as the API call's body: formatCmd and formatCmds. 35 | 36 | To execute the commands, CommandRunner provdes two different methods. run() issues the commands but does not wait 37 | for the task to complete. runSync() on the other hand, waits for the task to finish and then collects the results. 38 | 39 | Attributes: 40 | dnac: A pointer to the Dnac object containing the CommandRunner instance. 41 | type: Dnac object 42 | default: none 43 | scope: protected 44 | name: A user-friendly name for accessing the CommandRunner object in a Dnac.api{}. 45 | type: str 46 | default: none 47 | scope: protected 48 | task: A Task object associated with the commands being run. 49 | type: Task object 50 | default: none 51 | scope: protected 52 | cmds: The CLI commands to be run on the target devices. 53 | type: dict 54 | default: none 55 | scope: public 56 | resource: The URI for running commands within Cisco DNAC. 57 | type: str 58 | default: Cisco DNA Center version dependent 59 | scope: protected 60 | verify: A flag indicating whether or not to verify Cisco DNA Center's certificate. 61 | type: bool 62 | default: False 63 | scope: protected 64 | timeout: The number of seconds to wait for Cisco DNAC to respond before timing out. 65 | type: int 66 | default: 5 67 | scope: protected 68 | 69 | Usage: 70 | d = Dnac() 71 | cmds = {'commands': ['show version', 'show module'], 72 | 'deviceUuids': ['', ']} 73 | cmd = CommandRunner(d, "aName", cmds=cmds) 74 | progress = cmd.run() 75 | results = cmd.runSync() 76 | """ 77 | 78 | def __init__(self, 79 | dnac, 80 | name, 81 | cmds=None, 82 | verify=False, 83 | timeout=5): 84 | """ 85 | The __init__ method creates a CommandRunner object. As with all classes that inherit from DnacApi, a minimum 86 | of a Dnac container and a name must be given. Optionally, a dictionary of the CLI commands to run and the 87 | UUIDs of devices to run them on may be specified. 88 | :param dnac: A reference to the containing Dnac object. 89 | type: Dnac object 90 | default: none 91 | required: yes 92 | :param name: A user friendly name for finding this object in a Dnac instance. 93 | type: str 94 | default: none 95 | required: yes 96 | :param cmds: A dict with the commands and target devices. 97 | type: dict 98 | default: none 99 | required: no 100 | :param verify: A flag used to check Cisco DNAC's certificate. 101 | type: boolean 102 | default: False 103 | required: no 104 | :param timeout: The number of seconds to wait for Cisco DNAC's response. 105 | type: int 106 | default: 5 107 | required: no 108 | """ 109 | # check Cisco DNA Center's version and set the resourece path 110 | if cmds is None: 111 | cmds = {} 112 | if dnac.version in SUPPORTED_DNAC_VERSIONS: 113 | path = COMMANDRUNNER_RESOURCE_PATH[dnac.version] 114 | else: 115 | raise DnacError( 116 | '__init__: %s: %s' % 117 | (UNSUPPORTED_DNAC_VERSION, dnac.version) 118 | ) 119 | # setup the attributes 120 | self.__cmds = cmds # commands to run 121 | self.__task = None # CommandRunnerTask object created after running cmds 122 | super(CommandRunner, self).__init__(dnac, 123 | name, 124 | resource=path, 125 | verify=verify, 126 | timeout=timeout) 127 | 128 | # end __init__() 129 | 130 | @property 131 | def cmds(self): 132 | """ 133 | Get method cmds returns the __cmds body to be sent to Cisco DNAC. 134 | :return: dict 135 | """ 136 | return self.__cmds 137 | 138 | # end cmds getter 139 | 140 | @cmds.setter 141 | def cmds(self, cmds): 142 | """ 143 | Method cmds sets its __cmds attribute to the dictionary of commands given. 144 | :param cmds: A dict of commands and device UUIDs to run the commands against. 145 | type: dict 146 | default: None 147 | required: Yes 148 | :return: none 149 | """ 150 | self.__cmds = cmds 151 | 152 | # end cmds setter 153 | 154 | @property 155 | def task(self): 156 | """ 157 | The task get function returns the CommandRunnerTask object stored in __task. 158 | :return: CommandRunnerTask object 159 | """ 160 | return self.__task 161 | 162 | # end cmds getter 163 | 164 | def format_cmd(self, cmd, uuid): 165 | """ 166 | The formatCmd method takes a single CLI command and runs it against the UUID of a network device in Cisco DNA 167 | Center. It converts the command and the UUID into a dict stored in the __cmds attribute and returns __cmds' 168 | value. 169 | :param cmd: A CLI command. 170 | type: str 171 | default: none 172 | required: yes 173 | :param uuid: A network device UUID. 174 | type: str 175 | default: none 176 | required: yes 177 | :return: dict 178 | """ 179 | c = [cmd] 180 | u = [uuid] 181 | cmds = {'commands': c, 'deviceUuids': u} 182 | self.__cmds = json.dumps(cmds) 183 | return self.__cmds 184 | 185 | # end format_cmd() 186 | 187 | def format_cmds(self, cmd_list, uuid_list): 188 | """ 189 | The format_cmds method accepts a list of CLI commands to run against a list of UUIDs for the target network 190 | devices in Cisco DNA Center. It converts the two lists into a dict stored in the __cmds attribute and returns 191 | __cmds' value. 192 | :param cmd_list: A list of CLI commands. 193 | type: list of str 194 | default: none 195 | required: yes 196 | :param uuid_list: A list of network device UUIDs. 197 | type: list of str 198 | default: none 199 | required: yes 200 | :return: 201 | """ 202 | cmds = {'commands': cmd_list, 'deviceUuids': uuid_list} 203 | self.__cmds = json.dumps(cmds) 204 | return self.__cmds 205 | 206 | # end format_cmds() 207 | 208 | def run(self, wait=3): 209 | """ 210 | Method run instructs Cisco DNAC to execute the command set stored in the CommandRunner object. It 211 | waits for the task to complete on Cisco DNA Center. Specifically, it creates a CommandRunnerTask object 212 | and stores it in its __task attribute. When the task completes, it returns the results from the file 213 | where Cisco DNA Center stored them. They can also be retrieved from the task's file getter method. 214 | :return: str 215 | """ 216 | url = self.dnac.url + self.resource 217 | results, status = self.crud.post(url, 218 | headers=self.dnac.hdrs, 219 | body=self.__cmds, 220 | verify=self.verify, 221 | timeout=self.timeout) 222 | if status != ACCEPTED: 223 | raise DnacApiError(MODULE, 'run', REQUEST_NOT_ACCEPTED, url, 224 | ACCEPTED, status, ERROR_MSGS[status], 225 | str(results)) 226 | task_id = results['response']['taskId'] 227 | self.__task = CommandRunnerTask(self.dnac, task_id) 228 | return self.__task.get_task_results(wait) 229 | 230 | # end run() 231 | 232 | # end class CommandRunner() 233 | 234 | 235 | -------------------------------------------------------------------------------- /dnac/commandrunner_task.py: -------------------------------------------------------------------------------- 1 | from dnac.task import Task 2 | from dnac.file import File 3 | import json 4 | 5 | MODULE = 'commandrunner_task.py' 6 | 7 | # globals 8 | 9 | NO_FILE = None 10 | NO_FILE_ID = '' 11 | 12 | 13 | class CommandRunnerTask(Task): 14 | """ 15 | CommandRunnerTask extends the Task class by providing the means to monitor the task and return the results that 16 | Cisco DNAC stores in a file. A CommandRunner instance automatically manages the use of a CommandRunnerTask. 17 | It is unnecessary for users to create objects from this class. 18 | 19 | ROADMAP: This class may eventually be generalized to any task that produces a file for the task's results. 20 | 21 | Usage: 22 | d = Dnac() 23 | task = CommandRunnerTask(d, a_task_id) 24 | pprint.PrettyPrinter(task.get_task_results()) 25 | """ 26 | def __init__(self, 27 | dnac, 28 | id, 29 | verify=False, 30 | timeout=5): 31 | """ 32 | Instantiates a new CommandRunnerTask object. 33 | :param dnac: A reference to the master Dnac object. 34 | type: Dnac object 35 | required: yes 36 | default: None 37 | :param id: The object's UUID from Cisco DNA Center. 38 | type: str 39 | required: yes 40 | default: None 41 | :param verify: A flag that determines whether or not Cisco DNAC's certificate should be authenticated. 42 | type: bool 43 | required: no 44 | default: None 45 | :param timeout: The number of seconds to wait for a response from Cisco DNAC. 46 | type: int 47 | required: no 48 | default: 5 49 | """ 50 | super(CommandRunnerTask, self).__init__(dnac, 51 | id, 52 | verify=verify, 53 | timeout=timeout) 54 | self.__file = NO_FILE 55 | self.__file_id = NO_FILE_ID 56 | 57 | # end __init__() 58 | 59 | @property 60 | def file(self): 61 | """ 62 | Get method file returns the File object associated with the task referenced by this object. If the task has not 63 | finished, None is returned. 64 | :return: File object 65 | """ 66 | return self.__file 67 | 68 | # end file getter 69 | 70 | @property 71 | def file_id(self): 72 | """ 73 | The file_id get method retrieves the object current value for __file_id, which can be used to generate a new 74 | File object. 75 | :return: str 76 | """ 77 | return self.__file_id 78 | 79 | # end file_id getter 80 | 81 | def get_task_results(self, wait=3): 82 | """ 83 | Retrieves the command runner task's results from the file where Cisco DNAC stores the CLI output. 84 | :param wait: Number of seconds to wait for Cisco DNAC to finish the command. 85 | type: int 86 | required: no 87 | default: 3 88 | :return: dict 89 | """ 90 | # run the CLI command 91 | super(CommandRunnerTask, self).get_task_results(wait) 92 | # task completed - get the fileId in the progress dict 93 | #self.__progress = json.loads(self.__progress) 94 | progress = json.loads(self.progress) 95 | self.__file_id = progress['fileId'] 96 | # create the task results 97 | self.__file = File(self.dnac, self.__file_id) 98 | # retrieve the results, which are automatically saved in 99 | # File's __results attribute 100 | return self.__file.get_results() 101 | 102 | # end get_task_results() 103 | 104 | # end class CommandrunnerTask() 105 | 106 | -------------------------------------------------------------------------------- /dnac/config_archive.py: -------------------------------------------------------------------------------- 1 | from dnac import DnacError, \ 2 | SUPPORTED_DNAC_VERSIONS, \ 3 | UNSUPPORTED_DNAC_VERSION 4 | from dnac.dnacapi import DnacApi, \ 5 | DnacApiError 6 | from dnac.crud import OK, \ 7 | REQUEST_NOT_OK, \ 8 | ERROR_MSGS 9 | from dnac.device_archive import DeviceArchive 10 | 11 | MODULE = 'config_archive.py' 12 | 13 | ARCHIVE_RESOURCE_PATH = { 14 | '1.2.10': '/api/v1/archive-config', 15 | '1.3.0.2': '/api/v1/archive-config', 16 | '1.3.0.3': '/api/v1/archive-config', 17 | '1.3.1.3': '/api/v1/archive-config', 18 | '1.3.1.4': '/api/v1/archive-config' 19 | } 20 | 21 | # globals 22 | 23 | ARCHIVE_ALREADY_EXISTS_ERROR = 'A device archive already exists for the requested host' 24 | 25 | 26 | class ConfigArchive(DnacApi): 27 | """ 28 | The ConfigArchive class represents the device configuration archive in Cisco DNA Center. For any given Cisco DNAC 29 | cluster, there is one and only one configuration archive. For this implementation, ConfigArchive is a container 30 | that holds a set of DeviceArchive objects. 31 | 32 | Attributes: 33 | dnac: A pointer to the Dnac object containing the ConfigArchive instance. 34 | type: Dnac object 35 | default: none 36 | scope: protected 37 | name: A user-friendly name for accessing the ConfigArchive object in a Dnac.api{}. 38 | type: str 39 | default: none 40 | scope: protected 41 | archive: a dict that holds DeviceArchive instances. Use a device's UUID as the key to access it's 42 | associated DeviceArchive. 43 | type: dict 44 | default: {} 45 | scope: protected 46 | resource: The URI for running commands within Cisco DNAC. 47 | type: str 48 | default: Cisco DNA Center version dependent 49 | scope: protected 50 | verify: A flag indicating whether or not to verify Cisco DNA Center's certificate. 51 | type: bool 52 | default: False 53 | scope: protected 54 | timeout: The number of seconds to wait for Cisco DNAC to respond before timing out. 55 | type: int 56 | default: 5 57 | scope: protected 58 | 59 | Usage: 60 | d = Dnac() 61 | dnac_archive = ConfigArchive(d, 'archive') 62 | dnac_archive.load_all_archives() 63 | """ 64 | 65 | def __init__(self, 66 | dnac, 67 | name, 68 | verify=False, 69 | timeout=5): 70 | """ 71 | ConfigArchive's __init__ method initializes the object with an empty archive. 72 | :param dnac: A reference to the containing Dnac object. 73 | type: Dnac object 74 | default: none 75 | required: yes 76 | :param name: A user friendly name for finding this object in a Dnac instance. 77 | type: str 78 | default: none 79 | required: yes 80 | :param verify: A flag used to check Cisco DNAC's certificate. 81 | type: boolean 82 | default: False 83 | required: no 84 | :param timeout: The number of seconds to wait for Cisco DNAC's response. 85 | type: int 86 | default: 5 87 | required: no 88 | """ 89 | if dnac.version in SUPPORTED_DNAC_VERSIONS: 90 | path = ARCHIVE_RESOURCE_PATH[dnac.version] 91 | else: 92 | raise DnacError('%s: __init__: %s: %s' % (MODULE, UNSUPPORTED_DNAC_VERSION, dnac.version)) 93 | self.__archive = {} # key = deviceId, value = DeviceArchive 94 | super(ConfigArchive, self).__init__(dnac, 95 | '%s_archive' % name, 96 | resource=path, 97 | verify=verify, 98 | timeout=timeout) 99 | 100 | # end __init__() 101 | 102 | @property 103 | def archive(self): 104 | """ 105 | Get method archive returns the __archive dictionary. 106 | :return: dict 107 | """ 108 | return self.__archive 109 | 110 | # end archive getter 111 | 112 | def load_all_archives(self): 113 | """ 114 | ConfigArchive uses its load_all_archives method to retrieve the entire configuration archive from a Cisco 115 | DNA Center cluster. 116 | :return: dict 117 | """ 118 | url = self.dnac.url + self.resource 119 | archives, status = self.crud.get(url, 120 | headers=self.dnac.hdrs, 121 | verify=self.verify, 122 | timeout=self.timeout) 123 | if status != OK: 124 | raise DnacApiError( 125 | MODULE, 'load_all_archives', REQUEST_NOT_OK, url, 126 | OK, status, ERROR_MSGS[status], str(archives) 127 | ) 128 | for archive in archives['archiveResultlist']: 129 | device_archive = DeviceArchive(self.dnac, archive['deviceId']) 130 | device_archive.load_versions() 131 | self.__archive[archive['deviceId']] = device_archive 132 | return self.__archive 133 | 134 | # end load_all_archives() 135 | 136 | def load_device_archive(self, device): 137 | """ 138 | The load_device_archive instructs a ConfigArchive to pull the configuration archive of a single device. 139 | If the archive already contains the device's config, ConfigArchive first deletes the existing archive and 140 | then it reloads the information from Cisco DNA Center. 141 | :param device: The target device's UUID. 142 | type: str 143 | default: none 144 | required: yes 145 | :return: dict 146 | """ 147 | if device in self.__archive.keys(): 148 | del self.__archive[device] 149 | device_archive = DeviceArchive(d, device) 150 | device_archive.load_versions() 151 | self.__archive[device] = device_archive 152 | return self.__archive[device] 153 | 154 | # end load_device_archive 155 | 156 | def add_new_archive(self, device): 157 | """ 158 | The add_new_archive method creates a new DeviceArchive from a device's UUID and then stores it in the 159 | ConfigArchive's archive attribute. 160 | :param device: A device's UUID. 161 | type: str 162 | default: none 163 | required: yes 164 | :return: DeviceArchive object 165 | """ 166 | if device in self.__archive.keys(): 167 | raise DnacApiError(MODULE, 'add_new_device_archive', ARCHIVE_ALREADY_EXISTS_ERROR, '', 168 | '', device, '', '') 169 | new_archive = DeviceArchive(self.dnac, device) 170 | self.__archive[device] = new_archive 171 | return new_archive 172 | 173 | # end add_new_device_archive() 174 | 175 | # end class ConfigArchive() 176 | 177 | -------------------------------------------------------------------------------- /dnac/config_archive_settings.py: -------------------------------------------------------------------------------- 1 | from dnac import DnacError, \ 2 | SUPPORTED_DNAC_VERSIONS, \ 3 | UNSUPPORTED_DNAC_VERSION 4 | from dnac.dnacapi import DnacApi, \ 5 | DnacApiError 6 | from dnac.crud import OK, \ 7 | REQUEST_NOT_OK, \ 8 | ERROR_MSGS 9 | import json 10 | 11 | MODULE = 'config_archive_settings.py' 12 | 13 | ARCHIVE_SETTINGS_RESOURCE_PATH = { 14 | '1.2.10': '/api/v1/archive-config/setting', 15 | '1.3.0.2': '/api/v1/archive-config/setting', 16 | '1.3.0.3': '/api/v1/archive-config/setting', 17 | '1.3.1.3': '/api/v1/archive-config/setting', 18 | '1.3.1.4': '/api/v1/archive-config/setting' 19 | } 20 | 21 | # globals 22 | 23 | SUCCESSFUL_ARCHIVE_SETTINGS_UPDATE = 'SUCCESS' 24 | 25 | 26 | class ConfigArchiveSettings(DnacApi): 27 | """ 28 | The ConfigArchiveSettings class stores and modifies Cisco DNA Center's archive settings. Archive settings are 29 | global across a given Cisco DNAC cluster. The available settings are: 30 | 31 | noOfDays - the number of days to keep a device's configuration archive 32 | noOfVersion - the maximum number of archive versions for a given device 33 | timeout - timeout value for constructing a new device archive 34 | 35 | Cisco DNAC's API uses these as the keys of a dict when returning the existing settings or receiving a request 36 | to change the settings. 37 | 38 | Attributes: 39 | dnac: A pointer to the Dnac object containing the ConfigArchiveSettings instance. 40 | type: Dnac object 41 | default: none 42 | scope: protected 43 | name: A user-friendly name for accessing the ConfigArchiveSettings object in a Dnac.api{}. 44 | type: str 45 | default: none 46 | scope: protected 47 | settings: The archive's settings. 48 | type: dict 49 | default: {} 50 | scope: public 51 | resource: The URI for running commands within Cisco DNAC. 52 | type: str 53 | default: Cisco DNA Center version dependent 54 | scope: protected 55 | verify: A flag indicating whether or not to verify Cisco DNA Center's certificate. 56 | type: bool 57 | default: False 58 | scope: protected 59 | timeout: The number of seconds to wait for Cisco DNAC to respond before timing out. 60 | type: int 61 | default: 5 62 | scope: protected 63 | 64 | Usage: 65 | d = Dnac() 66 | archive_settings = ConfigArchiveSettings(d, d.name) 67 | print(archive_settings.settings) 68 | """ 69 | 70 | def __init__(self, 71 | dnac, 72 | name, 73 | verify=False, 74 | timeout=5): 75 | """ 76 | The ConfigArchiveSettings __init__ method creates a new object with blank settings. 77 | :param dnac: A reference to the containing Dnac object. 78 | type: Dnac object 79 | default: none 80 | required: yes 81 | :param name: A user friendly name for finding this object in a Dnac instance. 82 | type: str 83 | default: none 84 | required: yes 85 | :param verify: A flag used to check Cisco DNAC's certificate. 86 | type: boolean 87 | default: False 88 | required: no 89 | :param timeout: The number of seconds to wait for Cisco DNAC's response. 90 | type: int 91 | default: 5 92 | required: no 93 | """ 94 | if dnac.version in SUPPORTED_DNAC_VERSIONS: 95 | path = ARCHIVE_SETTINGS_RESOURCE_PATH[dnac.version] 96 | else: 97 | raise DnacError( 98 | '__init__: %s: %s' % 99 | (UNSUPPORTED_DNAC_VERSION, dnac.version) 100 | ) 101 | self.__settings = {} # global DNA Center archive settings 102 | super(ConfigArchiveSettings, self).__init__(dnac, 103 | '%s_archive_settings' % name, 104 | resource=path, 105 | verify=verify, 106 | timeout=timeout) 107 | 108 | # end __init__() 109 | 110 | @property 111 | def settings(self): 112 | """ 113 | The settings getter method makes a call to the Cisco DNAC cluster, stores the results in the __settings 114 | attribute and then returns value to the calling program. 115 | :return: dict 116 | """ 117 | # make a GET call to DNAC for the current settings 118 | url = self.dnac.url + ARCHIVE_SETTINGS_RESOURCE_PATH[self.dnac.version] 119 | settings, status = self.crud.get(url, 120 | headers=self.dnac.hdrs, 121 | verify=self.verify, 122 | timeout=self.timeout) 123 | if status != OK: 124 | raise DnacApiError( 125 | MODULE, 'settings getter', REQUEST_NOT_OK, url, 126 | OK, status, ERROR_MSGS[status], str(settings) 127 | ) 128 | self.__settings = settings 129 | return self.__settings 130 | 131 | # end settings getter 132 | 133 | @settings.setter 134 | def settings(self, settings): 135 | """ 136 | The settings setter method changes the objects value for its __settings attribute. 137 | :param settings: The new archive settings for the Cisco DNA Center instance. 138 | type: dict 139 | default: none 140 | required: yes 141 | :return: none 142 | """ 143 | self.__settings = settings 144 | url = self.dnac.url + ARCHIVE_SETTINGS_RESOURCE_PATH[self.dnac.version] 145 | result, status = self.crud.post(url, 146 | headers=self.dnac.hdrs, 147 | body=json.dumps(self.__settings), 148 | verify=self.verify, 149 | timeout=self.timeout) 150 | if status != OK: 151 | raise DnacApiError( 152 | MODULE, 'settings setter', REQUEST_NOT_OK, url, 153 | OK, status, ERROR_MSGS[status], str(self.__settings) 154 | ) 155 | if result['status'] != SUCCESSFUL_ARCHIVE_SETTINGS_UPDATE: 156 | print('Did not work') 157 | 158 | # end settings setter 159 | 160 | # end class ConfigArchiveSettings 161 | -------------------------------------------------------------------------------- /dnac/ctype.py: -------------------------------------------------------------------------------- 1 | 2 | # globals 3 | 4 | JSON = 'application/json' 5 | XML = 'application/xml' 6 | 7 | 8 | class CType(object): 9 | """ 10 | Class CType stores the content type a user wants a CRUD API to return. 11 | 12 | Attributes: 13 | ctype: The content type to return. Valid types include: 14 | application/json 15 | application/xml 16 | type: str 17 | scope: protected 18 | default: application/json 19 | hdrs: a dict for constructing CRUD request headers 20 | type: dict 21 | scope: public 22 | default: {'Content-Type': 'application/json'} 23 | """ 24 | 25 | def __init__(self, content_type=JSON): 26 | """ 27 | Method __init__ initializes a CType object. 28 | :param content_type: content type to return. Valid types include: 29 | application/json 30 | application/xml 31 | type: str 32 | default: application/json 33 | required: no 34 | """ 35 | self.__ctype = content_type 36 | self.__hdrs = {'Content-Type': self.__ctype} 37 | 38 | def __str__(self): 39 | """ 40 | String handler for a CType object. 41 | :return: str 42 | """ 43 | return self.__ctype 44 | 45 | @property 46 | def ctype(self): 47 | """ 48 | Get method ctype returns the string value of __ctype. 49 | :return: str 50 | """ 51 | return self.__ctype 52 | 53 | @property 54 | def hdrs(self): 55 | """ 56 | Get method hdrs returns the string value of __hdrs. 57 | :return: dict 58 | """ 59 | return self.__hdrs 60 | 61 | # end class CType() 62 | 63 | -------------------------------------------------------------------------------- /dnac/deployment.py: -------------------------------------------------------------------------------- 1 | 2 | from dnac import DnacError, \ 3 | SUPPORTED_DNAC_VERSIONS, \ 4 | UNSUPPORTED_DNAC_VERSION 5 | from dnac.dnacapi import DnacApi, \ 6 | DnacApiError 7 | from dnac.crud import ACCEPTED, \ 8 | REQUEST_NOT_ACCEPTED, \ 9 | ERROR_MSGS 10 | 11 | MODULE = 'deployment.py' 12 | 13 | # globals 14 | 15 | DEPLOYMENT_RESOURCE_PATH = { 16 | '1.2.8': '/api/v1/template-programmer/template/deploy/status', 17 | '1.2.10': '/api/v1/template-programmer/template/deploy/status', 18 | '1.3.0.2': '/api/v1/template-programmer/template/deploy/status', 19 | '1.3.0.3': '/api/v1/template-programmer/template/deploy/status', 20 | '1.3.1.3': '/api/v1/template-programmer/template/deploy/status', 21 | '1.3.1.4': '/dna/intent/api/v1/template-programmer/template/deploy/status/' 22 | } 23 | 24 | STATUS_KEY = 'status' 25 | 26 | NO_STATUS = '' 27 | 28 | class Deployment(DnacApi): 29 | """ 30 | The Deployment class monitors Cisco DNA Center's progress when pushing 31 | a template to a set of devices. When using a Template object to 32 | apply changes, the Template automatically creates and stores a 33 | corresponding Deployment instance. Use the Template to reference the 34 | Deployment for the job's results. 35 | 36 | Usage: 37 | d = Dnac() 38 | template = Template(d, 'Set VLAN') 39 | d.api['Set VLAN'].deploy() 40 | print d.api['Set VLAN'].deployment.check_deployment() 41 | """ 42 | 43 | def __init__(self, 44 | dnac, 45 | deployment_id, 46 | verify=False, 47 | timeout=5): 48 | """ 49 | Creates a deployment object and sets the deployment job's UUID. 50 | 51 | :param dnac: A reference to the master Dnac instance. 52 | type: Dnac object 53 | required: yes 54 | default: None 55 | :param deployment_id: The deployment job's UUID. 56 | type: str 57 | required: yes 58 | default: None 59 | :param verify: A flag that sets whether or not Cisco DNA Center's certificated should be authenticated. 60 | type: bool 61 | required: no 62 | default: False 63 | :param timeout: The number of seconds to wait for a response from Cisco DNAC. 64 | type: int 65 | required: no 66 | default: 5 67 | """ 68 | if dnac.version in SUPPORTED_DNAC_VERSIONS: 69 | path = DEPLOYMENT_RESOURCE_PATH[dnac.version] 70 | else: 71 | raise DnacError( 72 | '__init__: %s: %s' % 73 | (UNSUPPORTED_DNAC_VERSION, dnac.version) 74 | ) 75 | self.__deployment = {} 76 | self.__deployment_id = deployment_id 77 | super(Deployment, self).__init__(dnac, 78 | ('deployment_%s' % deployment_id), 79 | resource=path, 80 | verify=verify, 81 | timeout=timeout) 82 | 83 | # end __init__() 84 | 85 | @property 86 | def deployment_id(self): 87 | """ 88 | Provides the deployment job's UUID. 89 | :return: str 90 | """ 91 | return self.__deployment_id 92 | 93 | # end id getter 94 | 95 | @property 96 | def status(self): 97 | """ 98 | When available, reports the deployment job's status: SUCCESS or FAILURE. Returns an empty string if the job 99 | has not yet completed. 100 | :return: str 101 | """ 102 | if STATUS_KEY in self.__deployment.keys(): 103 | return self.__deployment[STATUS_KEY] 104 | else: 105 | return NO_STATUS 106 | 107 | # end status getter 108 | 109 | def check_deployment(self): 110 | """ 111 | Makes an API call to Cisco DNA Center for the deployment job's results. 112 | :return: dict 113 | """ 114 | # prepare the API call 115 | url = '%s%s/%s' % (self.dnac.url, self.resource, self.__deployment_id) 116 | # make the call 117 | results, status = self.crud.get(url, 118 | headers=self.dnac.hdrs, 119 | verify=self.verify, 120 | timeout=self.timeout) 121 | # return the results 122 | if status != ACCEPTED: 123 | raise DnacApiError( 124 | MODULE, 'check_deployment', REQUEST_NOT_ACCEPTED, url, 125 | ACCEPTED, status, ERROR_MSGS[status], str(results) 126 | ) 127 | self.__deployment = results 128 | return self.__deployment 129 | 130 | # end check_deployment() 131 | 132 | # end class Deployment() 133 | -------------------------------------------------------------------------------- /dnac/device_archive.py: -------------------------------------------------------------------------------- 1 | from dnac import DnacError, \ 2 | SUPPORTED_DNAC_VERSIONS, \ 3 | UNSUPPORTED_DNAC_VERSION 4 | from dnac.dnacapi import DnacApi, \ 5 | DnacApiError 6 | from dnac.crud import OK, \ 7 | REQUEST_NOT_OK, \ 8 | ERROR_MSGS, \ 9 | ACCEPTED, \ 10 | REQUEST_NOT_ACCEPTED 11 | from dnac.version import Version 12 | from dnac.device_archive_task import DeviceArchiveTask 13 | import json 14 | 15 | MODULE = 'device_archive.py' 16 | 17 | # globals 18 | 19 | ARCHIVE_RESOURCE_PATH = { 20 | '1.2.10': '/api/v1/archive-config', 21 | '1.3.0.2': '/api/v1/archive-config', 22 | '1.3.0.3': '/api/v1/archive-config', 23 | '1.3.1.3': '/api/v1/archive-config', 24 | '1.3.1.4': '/api/v1/archive-config' 25 | } 26 | 27 | DEVICE_ARCHIVE_RESOURCE_PATH = { 28 | '1.2.10': '%s/network-device' % ARCHIVE_RESOURCE_PATH['1.2.10'], 29 | '1.3.0.2': '%s/network-device' % ARCHIVE_RESOURCE_PATH['1.3.0.2'], 30 | '1.3.0.3': '%s/network-device' % ARCHIVE_RESOURCE_PATH['1.3.0.3'], 31 | '1.3.1.3': '%s/network-device' % ARCHIVE_RESOURCE_PATH['1.3.1.3'], 32 | '1.3.1.4': '%s/network-device' % ARCHIVE_RESOURCE_PATH['1.3.1.4'] 33 | } 34 | 35 | # error conditions 36 | 37 | NO_ARCHIVED_CONFIGS = [] 38 | SINGLE_DEVICE = 1 39 | 40 | # error messages and resolutions 41 | 42 | ILLEGAL_DEVICE_LIST = 'Illegal device ID list' 43 | LEGAL_DEVICE_LIST = 'dict' 44 | DEVICE_LIST_RESOLUTION = 'Only one device allowed. Device list is empty or has multiple entries.' 45 | ILLEGAL_DEVICE_TYPE = 'Illegal device type' 46 | LEGAL_DEVICE_TYPE = 'class NetworkDevice' 47 | DEVICE_TYPE_RESOLUTION = 'Create the config archive with a valid NetworkDevice object' 48 | SINGLE_DEVICE_ERROR = 'Archive requested includes multiple devices' 49 | SINGLE_DEVICE_ERROR_RESOLUTION = 'Modify the request\'s scope to one device only.' 50 | 51 | 52 | class DeviceArchive(DnacApi): 53 | """ 54 | The DeviceArchive class manages the configuration archive for a device. It can be used to create new versions 55 | and delete old ones. 56 | 57 | Usage: 58 | d = Dnac() 59 | device = NetworkDevice(d, 'aDevice') 60 | archive = DeviceArchive(d, device.devices['id']) 61 | archive.add_configs_to_archive(running=True, startup=True) 62 | for version in archive.versions: 63 | for file in version.config_files: 64 | pprint.PrettyPrinter(file) 65 | archive.delete_version(version) 66 | """ 67 | 68 | def __init__(self, 69 | dnac, 70 | device_id, 71 | verify=False, 72 | timeout=5): 73 | """ 74 | Creates a new DeviceArchive object and sets the target network device's ID. 75 | :param dnac: Reference to the program's Dnac object. 76 | type: Dnac object 77 | required: yes 78 | default: None 79 | :param device_id: The UUID in Cisco DNAC for the device to be managed. 80 | type: str 81 | required: yes 82 | default: None 83 | :param verify: Flag indicating whether or not to validate Cisco DNA Center's certificate. 84 | type: bool 85 | required: no 86 | default: False 87 | :param timeout: Number of seconds to wait for Cisco DNA Center to respond to an API call. 88 | type: int 89 | required: no 90 | default: 5 91 | """ 92 | if dnac.version in SUPPORTED_DNAC_VERSIONS: 93 | path = DEVICE_ARCHIVE_RESOURCE_PATH[dnac.version] 94 | else: 95 | raise DnacError('%s: __init__: %s: %s' % (MODULE, UNSUPPORTED_DNAC_VERSION, dnac.version)) 96 | self.__device = device_id 97 | self.__versions = [] # list of Version objects that contain the config files 98 | super(DeviceArchive, self).__init__(dnac, 99 | '%s_archive' % self.__device, 100 | resource=path, 101 | verify=verify, 102 | timeout=timeout) 103 | 104 | # end __init__() 105 | 106 | @property 107 | def device(self): 108 | """ 109 | Yields the target devices' UUID. 110 | :return: str 111 | """ 112 | return self.__device 113 | 114 | # end device getter 115 | 116 | @property 117 | def versions(self): 118 | """ 119 | Provides the list of archive versions available for the device. 120 | :return: list 121 | """ 122 | return self.__versions 123 | 124 | # end versions getter 125 | 126 | def load_versions(self): 127 | """ 128 | Instructs the DeviceArchive instance to load all the device's configs available in Cisco DNAC's archive and 129 | then return them. 130 | :return: list 131 | """ 132 | # make a GET call to DNAC for the latest versions list 133 | url = '%s%s/%s/version' % (self.dnac.url, self.resource, self.__device) 134 | versions, status = self.crud.get(url, 135 | headers=self.dnac.hdrs, 136 | verify=self.verify, 137 | timeout=self.timeout) 138 | if status != OK: 139 | raise DnacApiError( 140 | MODULE, 'versions', REQUEST_NOT_OK, url, 141 | OK, status, ERROR_MSGS[status], str(versions) 142 | ) 143 | # construct the object's versions list 144 | vers = versions['versions'] 145 | if vers != NO_ARCHIVED_CONFIGS: 146 | for version in vers: 147 | ver = Version(self.dnac, self.__device, version['id']) 148 | self.__versions.append(ver) 149 | return self.__versions 150 | 151 | # end load_versions() 152 | 153 | def delete_version(self, version): 154 | """ 155 | Deletes the indicated version and its configs from DNA Center's archive. 156 | :param version: The version to be removed. 157 | type: Version object 158 | required: yes 159 | default: None 160 | :return: None 161 | """ 162 | self.__versions.remove(version) 163 | version.delete() 164 | 165 | # end delete_version() 166 | 167 | def add_configs_to_archive(self, 168 | running=False, 169 | startup=False): 170 | """ 171 | Instructs the DeviceArchive instance to add a configuration file to Cisco DNA Center's archive and then 172 | refresh all available versions. 173 | :param running: A flag indicating whether or not the device's running config should be archived. 174 | type: bool 175 | required: no 176 | default: False 177 | :param startup: A flag indicating whether or not the device's startup config should be archived. 178 | type: bool 179 | required: no 180 | default: False 181 | :return: None 182 | """ 183 | device_id_list = [self.__device] 184 | # format the request body 185 | # 'vlan' and 'all' keys currently cause task failure; default these to False 186 | requested_configs = { 187 | 'startupconfig': startup, 188 | 'runningconfig': running, 189 | 'vlan': False, 190 | 'all': False 191 | } 192 | request_body = { 193 | 'deviceIds': device_id_list, 194 | 'configFileType': requested_configs 195 | } 196 | body = json.dumps(request_body) 197 | # issue the request to add configs to the archive 198 | url = self.dnac.url + ARCHIVE_RESOURCE_PATH[self.dnac.version] 199 | results, status = self.crud.post(url, 200 | headers=self.dnac.hdrs, 201 | body=body, 202 | verify=self.verify, 203 | timeout=self.timeout) 204 | if status != ACCEPTED: 205 | raise DnacApiError( 206 | MODULE, 'add_configs_to_archive', REQUEST_NOT_ACCEPTED, url, 207 | ACCEPTED, status, ERROR_MSGS[status], str(results) 208 | ) 209 | # monitor the task 210 | device_archive_task = DeviceArchiveTask(self.dnac, results['response']['taskId']) 211 | device_archive_task.get_task_results() 212 | # return the object's new versions list 213 | return self.load_versions() 214 | 215 | # end add_configs_to_archive() 216 | 217 | # end class DeviceArchive() 218 | 219 | -------------------------------------------------------------------------------- /dnac/device_archive_task.py: -------------------------------------------------------------------------------- 1 | 2 | from dnac.task import Task 3 | 4 | MODULE = 'device_archive_task.py' 5 | 6 | 7 | class DeviceArchiveTask(Task): 8 | """ 9 | The DeviceArchiveTask is a subclass of Task. As of this version of the wrapper package, DeviceArchiveTask does 10 | not modify Task's behavior. This class exists for stronger class typing and in the event that archiving tasks 11 | behavior changes and needs additional processing beyond what Task offers. 12 | 13 | When a DeviceArchive instance performs CRUD operations for a device, e.g. create a new version, it creates a 14 | new DeviceArchiveTask to handle monitoring the transaction and reporting its results. 15 | 16 | Attributes: 17 | dnac: A pointer to the Dnac object containing the ConfigArchive instance. 18 | type: Dnac object 19 | default: none 20 | scope: protected 21 | name: A user-friendly name for accessing the ConfigArchive object in a Dnac.api{}. 22 | type: str 23 | default: none 24 | scope: protected 25 | resource: The URI for running commands within Cisco DNAC. 26 | type: str 27 | default: Cisco DNA Center version dependent 28 | scope: protected 29 | verify: A flag indicating whether or not to verify Cisco DNA Center's certificate. 30 | type: bool 31 | default: False 32 | scope: protected 33 | timeout: The number of seconds to wait for Cisco DNAC to respond before timing out. 34 | type: int 35 | default: 5 36 | scope: protected 37 | 38 | Usage: 39 | d = Dnac() 40 | device_archive_task = DeviceArchiveTask(d, ) 41 | """ 42 | 43 | def __init__(self, 44 | dnac, 45 | id, 46 | verify=False, 47 | timeout=5): 48 | """ 49 | The __init__ method creates a new device archive task instance. Use the task ID returned by Cisco DNA Center 50 | as the id for the new object. 51 | :param dnac: A reference to the containing Dnac object. 52 | type: Dnac object 53 | default: none 54 | required: yes 55 | :param id: The UUID for monitoring the task in Cisco DNAC. 56 | type: str 57 | default: none 58 | required: yes 59 | :param verify: A flag used to check Cisco DNAC's certificate. 60 | type: boolean 61 | default: False 62 | required: no 63 | :param timeout: The number of seconds to wait for Cisco DNAC's response. 64 | type: int 65 | default: 5 66 | required: no 67 | """ 68 | super(DeviceArchiveTask, self).__init__(dnac, 69 | id=id, 70 | verify=verify, 71 | timeout=timeout) 72 | 73 | # end __init__() 74 | 75 | # end class Task() 76 | 77 | -------------------------------------------------------------------------------- /dnac/dnac_config.py: -------------------------------------------------------------------------------- 1 | '''Configuration parameters for a Dnac object.''' 2 | 3 | # DNAC_NAME: FQDN of a Cisco DNA Center cluster 4 | # A Dnac instance prefers this value over an IP address (DNAC_IP) 5 | # To use an IP address instead, set this constant = '' 6 | DNAC_NAME = 'denlab-en-dnac.cisco.com' 7 | 8 | # 9 | # DNAC_IP: IP address of a Cisco DNA Center cluster 10 | # 11 | DNAC_IP = '' 12 | 13 | # 14 | # DNAC_VERSION: Used for setting the resource path of API calls based upon 15 | # the version of the Cisco DNAC cluster. 16 | # 17 | DNAC_VERSION = '1.3.1.4' 18 | 19 | # 20 | # DNAC_PORT: TCP port used to communicate with Cisco DNAC's API 21 | # 22 | DNAC_PORT = '443' 23 | 24 | # 25 | # DNAC_USER: Name of a Cisco DNAC account with administrative privileges 26 | # 27 | DNAC_USER = 'admin' 28 | 29 | # 30 | # DNAC_PASSWD: Password for the Cisco DNAC_USER 31 | # 32 | DNAC_PASSWD = 'C!sco123' 33 | 34 | # 35 | # DNAC_CONTENT_TYPE: Data format with Cisco DNAC should respond with 36 | # Valid values include: 'application/json' 37 | # 'application/xml' 38 | # 39 | DNAC_CONTENT_TYPE = 'application/json' 40 | -------------------------------------------------------------------------------- /dnac/file.py: -------------------------------------------------------------------------------- 1 | 2 | from dnac import DnacError, \ 3 | SUPPORTED_DNAC_VERSIONS, \ 4 | UNSUPPORTED_DNAC_VERSION 5 | from dnac.dnacapi import DnacApi, \ 6 | DnacApiError 7 | from dnac.crud import OK, \ 8 | REQUEST_NOT_OK, \ 9 | ERROR_MSGS 10 | 11 | MODULE = 'file.py' 12 | 13 | FILE_RESOURCE_PATH = { 14 | '1.2.8': '/api/v1/file', 15 | '1.2.10': '/dna/intent/api/v1/file', 16 | '1.3.0.2': '/api/v1/file', 17 | '1.3.0.3': '/api/v1/file', 18 | '1.3.1.3': '/api/v1/file', 19 | '1.3.1.4': '/api/v1/file' 20 | } 21 | 22 | 23 | class File(DnacApi): 24 | """ 25 | Class File gets and stores the results from a task run on Cisco DNA Center. Cisco DNAC delivers the results from 26 | a file whose contents are a string, which this class converts to a list. 27 | 28 | Like all entities in Cisco DNAC, files are referenced via a UUID. Set the file UUID either during initialization 29 | before attempting to retrieve the file's data with the get_results() method. 30 | 31 | Although this class may be instantiated independently, the Task class automatically creates and stores an instance 32 | of File to simplify handling tasks. 33 | 34 | File inherits from class DnacApi for its base attributes and for inclusion in a Dnac object's api store. 35 | 36 | Usage: 37 | d = Dnac() 38 | task = Task(d, 'aTask') 39 | task.checkTask() 40 | # task.file below is a File object 41 | results = task.file.get_results() 42 | """ 43 | 44 | def __init__(self, 45 | dnac, 46 | id, 47 | verify=False, 48 | timeout=5): 49 | """ 50 | Class method __init__ creates a new File instance. When making a File object, pass it a Dnac object and the 51 | UUID of the file in Cisco DNAC that this object represents. 52 | :param dnac: A reference to the containing Dnac object. 53 | type: Dnac object 54 | default: none 55 | required: yes 56 | :param id: The UUID of the file in Cisco DNAC this object represents. If included as part of calling __init__, 57 | the new object sets its name and url based on id's value: 58 | name = 'task_' 59 | url = 'dnac.url/self.respath/' 60 | type: str 61 | default: none 62 | required: yes 63 | :param verify: A flag used to check Cisco DNAC's certificate. 64 | type: boolean 65 | default: False 66 | required: no 67 | :param timeout: The number of seconds to wait for Cisco DNAC's response. 68 | type: int 69 | default: 5 70 | required: no 71 | """ 72 | # check Cisco DNA Center's version and set the resource path 73 | if dnac.version in SUPPORTED_DNAC_VERSIONS: 74 | path = FILE_RESOURCE_PATH[dnac.version] 75 | else: 76 | raise DnacError( 77 | '__init__: %s: %s' % 78 | (UNSUPPORTED_DNAC_VERSION, dnac.version) 79 | ) 80 | # setup the attributes 81 | self.__id = id # use the fileId in the task's progress 82 | self.__results = [] # raw data in case further processing needed 83 | super(File, self).__init__(dnac, 84 | ('file_%s' % self.__id), 85 | resource=path, 86 | verify=verify, 87 | timeout=timeout) 88 | 89 | # end __init__() 90 | 91 | @property 92 | def id(self): 93 | """ 94 | Get method id returns __id, the UUID of the file on Cisco DNA Center that contains the task's results. 95 | :return: str 96 | """ 97 | return self.__id 98 | 99 | # end id getter 100 | 101 | @property 102 | def results(self): 103 | """ 104 | Get method results returns __results, a list containing the task's results stored in the file. 105 | :return: list 106 | """ 107 | return self.__results 108 | 109 | # end results getter 110 | 111 | def get_results(self, is_json=True): 112 | """ 113 | get_results makes an API call to Cisco DNA Center and retrieves the task results contained in the file 114 | identified by this object's __file_id, i.e. the file's UUID. 115 | :param is_json: Flag indicating whether or not the results are in JSON format. 116 | type: bool 117 | required: no 118 | default: True 119 | :return: list 120 | """ 121 | url = self.dnac.url + self.resource + ('/%s' % self.__id) 122 | results, status = self.crud.get(url, 123 | headers=self.dnac.hdrs, 124 | verify=self.verify, 125 | timeout=self.timeout, 126 | is_json=is_json) 127 | if status != OK: 128 | raise DnacApiError( 129 | MODULE, 'get_results', REQUEST_NOT_OK, url, 130 | OK, status, ERROR_MSGS[status], str(results) 131 | ) 132 | self.__results = results 133 | return self.__results 134 | 135 | # end get_results() 136 | 137 | # end class File() 138 | 139 | -------------------------------------------------------------------------------- /dnac/task.py: -------------------------------------------------------------------------------- 1 | 2 | from dnac import DnacError, \ 3 | SUPPORTED_DNAC_VERSIONS, \ 4 | UNSUPPORTED_DNAC_VERSION 5 | from dnac.dnacapi import DnacApi, \ 6 | DnacApiError 7 | from dnac.crud import OK, \ 8 | REQUEST_NOT_OK, \ 9 | ERROR_MSGS 10 | import time 11 | 12 | # globals 13 | 14 | MODULE = 'task.py' 15 | 16 | TASK_RESOURCE_PATH = { 17 | '1.2.8': '/api/v1/task', 18 | '1.2.10': '/api/v1/task', 19 | '1.3.0.2': '/api/v1/task', 20 | '1.3.0.3': '/api/v1/task', 21 | '1.3.1.3': '/api/v1/task', 22 | '1.3.1.4': '/dna/intent/api/v1/task' 23 | } 24 | 25 | PROGRESS_KEY = 'progress' 26 | END_TIME_KEY = 'endTime' 27 | START_TIME_KEY = 'startTime' 28 | IS_ERROR_KEY = 'isError' 29 | FAILURE_REASON_KEY = 'failureReason' 30 | 31 | # error conditions 32 | 33 | NO_PROGRESS = '' 34 | NO_START_TIME = -1 35 | NO_END_TIME = -1 36 | NO_IS_ERROR = '' 37 | NO_FAILURE_REASON = '' 38 | 39 | class Task(DnacApi): 40 | """ 41 | The Task class manages and monitors Cisco DNA Center jobs. Many actions Cisco DNAC performs happen asychronously 42 | and are scheduled using a Task, for example, running show commands (CommandRunner) or applying templates (Template). 43 | A task's state and the format of its results varies by job type. Consequently, it may be necessary to subtype 44 | Task and extend its behavior. The base class monitors itself by looking for an endTime parameter in its results. 45 | Until endTime appears, the Task remains in some form of a running state that is usually reflected in the progress 46 | field. Upon job completion, Task populates the results. If the Task's isError field indicates a problem 47 | (isError == True), then check the progress and failureReason for fault indicators. If not, then the Task results 48 | should be loaded into the __task attribute. This last action depends upon the task subtype; for example, a 49 | CommandRunner task points to a file with the command's output, but a Template job simply ends in success or failure. 50 | 51 | Usage: 52 | d = Dnac() 53 | t = Task(d, ) 54 | results = t.getTaskResults() 55 | """ 56 | def __init__(self, 57 | dnac, 58 | id, 59 | verify=False, 60 | timeout=5): 61 | # check Cisco DNA Center's version and set the resource path 62 | if dnac.version in SUPPORTED_DNAC_VERSIONS: 63 | path = TASK_RESOURCE_PATH[dnac.version] 64 | else: 65 | raise DnacError('__init__: %s: %s' % (UNSUPPORTED_DNAC_VERSION, dnac.version)) 66 | # setup the attributes 67 | self.__task = {} 68 | self.__id = id 69 | super(Task, self).__init__(dnac, 70 | ('task_%s' % self.__id), 71 | resource=path, 72 | verify=verify, 73 | timeout=timeout) 74 | 75 | # end __init__() 76 | 77 | @property 78 | def id(self): 79 | """ 80 | Provides the task's UUID. 81 | :return: str 82 | """ 83 | return self.__id 84 | 85 | # end id getter 86 | 87 | @property 88 | def progress(self): 89 | """ 90 | Gives the task's current run state. 91 | :return: str 92 | """ 93 | if PROGRESS_KEY in self.__task.keys(): 94 | return self.__task[PROGRESS_KEY] 95 | else: 96 | return NO_PROGRESS 97 | 98 | # end progress getter 99 | 100 | @property 101 | def start_time(self): 102 | """ 103 | Indicates when the task started using epoch time. 104 | :return: int 105 | """ 106 | if START_TIME_KEY in self.__task.keys(): 107 | return self.__task[START_TIME_KEY] 108 | else: 109 | return NO_START_TIME 110 | 111 | # end start_time getter 112 | 113 | @property 114 | def end_time(self): 115 | """ 116 | Indicates when the task ended using epoch time. 117 | :return: int 118 | """ 119 | if END_TIME_KEY in self.__task.keys(): 120 | return self.__task[END_TIME_KEY] 121 | else: 122 | return NO_END_TIME 123 | 124 | # end end_time getter 125 | 126 | @property 127 | def is_error(self): 128 | """ 129 | Returns the task's error state. True, if error; otherwise, false, if succeeded. 130 | :return: bool 131 | """ 132 | if IS_ERROR_KEY in self.__task.keys(): 133 | return self.__task[IS_ERROR_KEY] 134 | else: 135 | return NO_IS_ERROR 136 | 137 | # end is_error getter 138 | 139 | @property 140 | def failure_reason(self): 141 | """ 142 | If the task ended with an error, this method provides the reason it failed. 143 | :return: str 144 | """ 145 | if FAILURE_REASON_KEY in self.__task.keys(): 146 | return self.__task[FAILURE_REASON_KEY] 147 | else: 148 | return NO_FAILURE_REASON 149 | 150 | # end failure_reason getter 151 | 152 | def __check_task__(self): 153 | """ 154 | Hidden method used to retrieve the task results from Cisco DNAC. 155 | :return: dict 156 | """ 157 | url = '%s%s/%s' % (self.dnac.url, self.resource, self.id) 158 | results, status = self.crud.get(url, 159 | headers=self.dnac.hdrs, 160 | verify=self.verify, 161 | timeout=self.timeout) 162 | if status != OK: 163 | raise DnacApiError( 164 | MODULE, '__check_task__', REQUEST_NOT_OK, url, OK, status, ERROR_MSGS[status], str(results) 165 | ) 166 | self.__task = results['response'] 167 | return self.__task 168 | 169 | # end check_task() 170 | 171 | def get_task_results(self, wait=3): 172 | """ 173 | Checks for the task's endTime. If endTime does not exist, then the method waits for the number of seconds 174 | given and checks again. If endTime exists, then the results get loaded into the object's __task attribute. 175 | :param wait: Number of seconds to sleep before checking again. 176 | type: int 177 | required: no 178 | default: 3 179 | :return: dict 180 | """ 181 | while END_TIME_KEY not in self.__task.keys(): 182 | time.sleep(wait) 183 | self.__check_task__() 184 | return self.__task 185 | 186 | # end get_task_results() 187 | 188 | # end class Task() 189 | -------------------------------------------------------------------------------- /dnac/timestamp.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import time 3 | 4 | # globals 5 | 6 | MODULE = 'timestamp.py' 7 | 8 | NO_TIME = -1 9 | GET_CURRENT_TIME = 0 10 | 11 | class TimeStamp(object): 12 | """ 13 | The TimeStamp class takes the system's time in UTC and converts it to epoch time given in milliseconds. 14 | Remember that the TimeStamp's value is set when it is created and is not updated whenever it is referenced. 15 | Future enhancements to this class may change this behavior if needed. 16 | 17 | Attributes: 18 | timestamp: Current system epoch time in milliseconds 19 | type: int 20 | default: current epoch time 21 | scope: protected 22 | 23 | Usage: 24 | time = TimeStamp() 25 | print(time) 26 | """ 27 | 28 | def __init__(self, time=GET_CURRENT_TIME): 29 | """ 30 | The TimeStamp's __init__ method sets its value to the current number of milliseconds in epoch time. 31 | :param time: Epoch time with which to initialize the new object. 32 | type: int 33 | required: no 34 | default: retrieves the current time 35 | """ 36 | if time == GET_CURRENT_TIME: 37 | self.get_current_time() 38 | else: 39 | self.__timestamp = int(time) 40 | 41 | # end __init__ 42 | 43 | def __str__(self): 44 | """ 45 | TimeStamp class' __str__ function converts its timestamp attribute, an int, into a string. 46 | :return: The epoch time in milliseconds when the TimeStamp object was created. 47 | """ 48 | 49 | # end __str__ 50 | 51 | @property 52 | def timestamp(self): 53 | """ 54 | The timestamp getter method returns the object's epoch time in milliseconds. 55 | :return: The epoch time in milliseconds when the TimeStamp object was created. 56 | """ 57 | return self.__timestamp 58 | 59 | # end timestamp getter 60 | 61 | @timestamp.setter 62 | def timestamp(self, time): 63 | """ 64 | TimeStamp's timestamp setter method resets its time. The method automatically converts whatever time is 65 | passed to an int. Use an epoch time in milliseconds. 66 | :param time: Epoch time in milliseconds. 67 | type: int or str 68 | default: none 69 | required: yes 70 | :return: none 71 | """ 72 | self.__timestamp = int(time) 73 | 74 | # end timestamp setter 75 | 76 | def utc_timestamp(self): 77 | """ 78 | The utc_timestamp method returns the object's current timestamp value as a formatted string in UTC time. 79 | :return: The timestamp in UTC time formatted as %Y-%m-%d %H:%M:%S 80 | """ 81 | t = self.__timestamp / 1000 82 | t = time.gmtime(t) 83 | return time.strftime('%Y-%m-%d %H:%M:%S', t) 84 | 85 | # end utc_timestamp() 86 | 87 | def local_timestamp(self): 88 | """ 89 | The local_timestamp method returns the object's current timestamp value as a formatted string in the Cisco 90 | DNA Center instance's local timezone. 91 | :return: The timestamp in local time formatted as %Y-%m-%d %H:%M:%S 92 | """ 93 | t = self.__timestamp / 1000 94 | t = time.localtime(t) 95 | return time.strftime('%Y-%m-%d %H:%M:%S', t) 96 | 97 | # end local_timestamp() 98 | 99 | def sleep(self, seconds): 100 | """ 101 | Instructs the TimeStamp object to wait the number of seconds given. 102 | :param seconds: The number of seconds to halt program activity. 103 | type: int 104 | required: yes 105 | default: none 106 | :return: none 107 | """ 108 | time.sleep(seconds) 109 | 110 | # end sleep() 111 | 112 | def get_current_time(self): 113 | """ 114 | Resets the TimeStamp to the current time. 115 | :return: The current time as an epoch integer. 116 | """ 117 | self.__timestamp = int((datetime.utcnow() - datetime(1970, 1, 1)).total_seconds() * 1000) 118 | return self.__timestamp 119 | 120 | # end get_current_time() 121 | 122 | # end class TimeStamp() 123 | 124 | -------------------------------------------------------------------------------- /dnac/version.py: -------------------------------------------------------------------------------- 1 | from dnac import DnacError, \ 2 | SUPPORTED_DNAC_VERSIONS, \ 3 | UNSUPPORTED_DNAC_VERSION 4 | from dnac.dnacapi import DnacApi, \ 5 | DnacApiError 6 | from dnac.crud import OK, \ 7 | REQUEST_NOT_OK, \ 8 | ERROR_MSGS 9 | from dnac.file import File 10 | from dnac.task import Task 11 | 12 | # globals 13 | 14 | MODULE = 'version.py' 15 | 16 | VERSION_RESOURCE_PATH = { 17 | '1.2.10': '/api/v1/archive-config/network-device', 18 | '1.3.0.2': '/api/v1/archive-config/network-device', 19 | '1.3.0.3': '/api/v1/archive-config/network-device', 20 | '1.3.1.3': '/api/v1/archive-config/network-device' 21 | } 22 | 23 | VERSION_SUB_RESOURCE_PATH = { 24 | '1.2.10': '/version', 25 | '1.3.0.2': '/version', 26 | '1.3.0.3': '/version', 27 | '1.3.1.3': '/version' 28 | } 29 | 30 | CONFIG_FILE_SUB_RESOURCE_PATH = { 31 | '1.2.10': '/file', 32 | '1.3.0.2': '/file', 33 | '1.3.0.3': '/file', 34 | '1.3.1.3': '/file' 35 | } 36 | 37 | RUNNING_CONFIG = 'RUNNINGCONFIG' 38 | STARTUP_CONFIG = 'STARTUPCONFIG' 39 | VLAN = 'VLAN' 40 | 41 | CONFIG_FILE_TYPES = [ 42 | RUNNING_CONFIG, 43 | STARTUP_CONFIG 44 | ] 45 | 46 | # error conditions 47 | 48 | NO_SYNC = '' 49 | NO_CONFIG = {} 50 | 51 | # error messages and resolutions 52 | 53 | ILLEGAL_CONFIG_FILE_TYPE = 'Illegal config file type' 54 | VERSION_DELETE_FAILED = 'Version deletion failed' 55 | 56 | 57 | class Version(DnacApi): 58 | """ 59 | The Version class represents a configuration archive version created in Cisco DNA Center. A DeviceArchive wraps 60 | Version instances, and in turn, ConfigArchive objects wrap DeviceArchives. It is unnecessary for users to manage 61 | their own Version instances. 62 | """ 63 | def __init__(self, 64 | dnac, 65 | device_id, 66 | version_id, 67 | verify=False, 68 | timeout=5): 69 | """ 70 | Instantiates a new Version object. 71 | :param dnac: A reference to the master script's Dnac object. 72 | type: Dnac object 73 | required: yes 74 | default: None 75 | :param device_id: The UUID for the device whose archive is being managed. 76 | type: str 77 | required: yes 78 | default: none 79 | :param version_id: The UUID of the version being queried. 80 | type: str 81 | required: yes 82 | default: None 83 | :param verify: A flag that determines whether or not Cisco DNA Center's certificate should be authenticated. 84 | type: bool 85 | required: no 86 | default: False 87 | :param timeout: The number of seconds to wait for Cisco DNAC to respond to a query. 88 | type: int 89 | required: no 90 | default: 5 91 | """ 92 | if dnac.version in SUPPORTED_DNAC_VERSIONS: 93 | path = '%s/%s%s/%s' % (VERSION_RESOURCE_PATH[dnac.version], 94 | device_id, 95 | VERSION_SUB_RESOURCE_PATH[dnac.version], 96 | version_id) 97 | else: 98 | raise DnacError('__init__: %s: %s' % (UNSUPPORTED_DNAC_VERSION, dnac.version)) 99 | self.__id = version_id 100 | self.__device_id = device_id 101 | self.__config_files = {} # key = fileType, value = File object 102 | name = 'version_%s' % self.__id 103 | super(Version, self).__init__(dnac, 104 | name, 105 | resource=path, 106 | verify=verify, 107 | timeout=timeout) 108 | # load the version 109 | url = self.dnac.url + self.resource 110 | version, status = self.crud.get(url, 111 | headers=self.dnac.hdrs, 112 | verify=self.verify, 113 | timeout=self.timeout) 114 | if status != OK: 115 | raise DnacApiError( 116 | MODULE, '__init__', REQUEST_NOT_OK, url, OK, status, ERROR_MSGS[status], str(version) 117 | ) 118 | self.__created_time = version['versions'][0]['createdTime'] 119 | self.__sync_status = version['versions'][0]['startupRunningStatus'] 120 | for file in version['versions'][0]['files']: 121 | # iterate through all the archive's versions and load the config files 122 | if file['fileType'] not in CONFIG_FILE_TYPES: 123 | # ignore VLAN data files 124 | if file['fileType'] == VLAN: 125 | continue 126 | raise DnacApiError( 127 | MODULE, '__init__', ILLEGAL_CONFIG_FILE_TYPE, '', 128 | str(CONFIG_FILE_TYPES), '', file['fileType'], '' 129 | ) 130 | config_file = File(dnac, file['fileId']) 131 | config_file.get_results(is_json=False) 132 | self.__config_files[file['fileType']] = config_file 133 | 134 | # end __init__() 135 | 136 | @property 137 | def id(self): 138 | """ 139 | The id get method returns the version's UUID. 140 | :return: str 141 | """ 142 | return self.__id 143 | 144 | # end id getter 145 | 146 | @property 147 | def device_id(self): 148 | """ 149 | Provides the device's UUID being queried. 150 | :return: str 151 | """ 152 | return self.__device_id 153 | 154 | # end device_id getter 155 | 156 | @property 157 | def created_time(self): 158 | """ 159 | Returns the epoch time the version was created. 160 | :return: int 161 | """ 162 | return self.__created_time 163 | 164 | # end created_time getter 165 | 166 | @property 167 | def sync_status(self): 168 | """ 169 | Indicates whether or not the device's running config has been sychronized with the startup config for this 170 | version. 171 | :return: bool 172 | """ 173 | return self.__sync_status 174 | 175 | # end sync_status getter 176 | 177 | @property 178 | def config_files(self): 179 | """ 180 | Returns the configuration files associated with the Version object. 181 | :return: dict 182 | """ 183 | return self.__config_files 184 | 185 | # end config_files getter 186 | 187 | def delete(self): 188 | """ 189 | Removes the Version object from Cisco DNA Center as well as from the program's Dnac object. 190 | :return: None 191 | """ 192 | url = self.dnac.url + self.resource 193 | results, status = self.crud.delete(url, headers=self.dnac.hdrs) 194 | if status != OK: 195 | raise DnacApiError(MODULE, 'delete', REQUEST_NOT_OK, url, OK, status, ERROR_MSGS[status], '') 196 | task = Task(self.dnac, results['response']['taskId']) 197 | task.get_task_results() 198 | if task.is_error: 199 | raise DnacApiError(MODULE, 'delete', task.progress, '', '', '', task.failure_reason, '') 200 | else: 201 | # remove self from Dnac.api{} 202 | del self.dnac.api[self.name] 203 | 204 | # end delete() 205 | 206 | def delete_config_file(self, file_id): 207 | """ 208 | Removes the specified file, given by its UUID, from Cisco DNAC and from the Version instance. 209 | :param file_id: str 210 | :return: None 211 | """ 212 | url = '%s%s/%s/%s' % (self.dnac.url, self.resource, CONFIG_FILE_SUB_RESOURCE_PATH[self.dnac.version], file_id) 213 | results, status = self.crud.delete(url, headers=self.dnac.hdrs) 214 | if status != OK: 215 | raise DnacApiError(MODULE, 'delete_config', REQUEST_NOT_OK, url, OK, status, ERROR_MSGS[status], '') 216 | task = Task(self.dnac, results['response']['taskId']) 217 | task.get_task_results() 218 | if task.is_error: 219 | raise DnacApiError(MODULE, 'delete_config', task.progress, '', '', '', task.failure_reason, '') 220 | else: 221 | for config_file_type, config_file in self.__config_files.items(): 222 | if file_id == config_file.id: 223 | del self.__config_files[config_file_type] 224 | break 225 | del self.dnac.api['file_%s' % file_id] 226 | 227 | # end delete_config_file 228 | 229 | # end class Version 230 | 231 | -------------------------------------------------------------------------------- /dnac/xauthtoken.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | import requests 4 | 5 | # error messages 6 | INVALID_RESPONSE = "Invalid response to API call" 7 | 8 | 9 | class XAuthTokenError(Exception): 10 | """ 11 | The XAuthTokenError exception class, derived from Exception, indicates any problems specific to requesting a token 12 | from Cisco DNA Center. 13 | 14 | Attributes: 15 | none 16 | """ 17 | 18 | def __init__(self, msg): 19 | """ 20 | XAuthTokenErrors's __init__ method passes a message to its parent class. 21 | :param msg: An error message indicating the problem. 22 | type: str 23 | required: yes 24 | default: non 25 | """ 26 | super(XAuthTokenError, self).__init__(msg) 27 | 28 | # end class XAuthTokenError 29 | 30 | # end exceptions 31 | 32 | 33 | class XAuthToken(object): 34 | """ 35 | Class XAuthToken stores a token used to authorize Cisco DNAC API calls. 36 | 37 | Dnac objects contain one XAuthToken for all of its DnacApi instances to reference when making a request to Cisco 38 | DNAC. Use the Dnac.token class method to access the current token value instead of referencing the Dnac.xauth 39 | attribute, which is an instantiation of this class. As necessary, use Dnac.get_new_token to refresh the 40 | x-auth-token value. 41 | 42 | Attributes: 43 | url: The base URL for contacting Cisco DNAC. 44 | type: str 45 | default: none 46 | scope: protected 47 | bauth: A basic authentication object for requesting a token 48 | from Cisco DNAC. 49 | type: BasicAuth object 50 | default: none 51 | scope: protected 52 | ctype: A content type object for requesting a token from Cisco DNAC. 53 | type: CType object 54 | default: CType('application/json') 55 | scope: Ppotected 56 | resource: The resource path used to request a token from Cisco DNAC. 57 | type: str 58 | default: '/api/system/v1/auth/token' 59 | scope: protected 60 | verify: Flag indicating whether or not the request should verify 61 | Cisco DNAC's certificate. 62 | type: boolean 63 | default: False 64 | scope: protected 65 | timeout: The number of seconds the request for a token should wait 66 | before assuming Cisco DNAC is unavailable. 67 | type: int 68 | default: 5 69 | scope: protected 70 | token: The x-auth-token used to authorize API calls. 71 | type: str 72 | default: None 73 | scope: protected 74 | hdrs: The headers for requesting an x-auth-token from Cisco DNAC. 75 | type: dict 76 | default: none 77 | scope: protected 78 | """ 79 | 80 | def __init__(self, 81 | url, 82 | basic_auth, 83 | content_type, 84 | resource='/api/system/v1/auth/token', 85 | verify=False, 86 | timeout=5): 87 | """ 88 | The __init__ method initializes an XAuthToken object. It takes a URL pointing to the Cisco DNAC cluster as 89 | well as both a BasicAuth and CType objects for constructing the token request. When creating a Dnac object, 90 | the object's __init__ method passes its base URL (Dnac.url), its BasicAuth object (Dnac.bauth) and its chosen 91 | content type (Dnac.ctype). This ensures that the XAuthToken instance (Dnac.xauth) remains consistent with all 92 | of the DnacApi objects in use, i.e. any changes to the basic authentication or to the content type used remain 93 | the same when calling on the XAuthToken. 94 | :param url: The URL for reaching Cisco DNAC. 95 | type: str 96 | default: none 97 | required: yes 98 | :param basic_auth: A BasicAuth object for logging into Cisco DNAC. 99 | type: BasicAuth object 100 | default: none 101 | required: yes 102 | :param content_type: A CType object indicating the token's format requested from Cisco DNAC 103 | type: CType object 104 | default: none 105 | required: yes 106 | :param resource: The resource path used to request a token. 107 | type: str 108 | default: '/api/system/v1/auth/token' 109 | required: no 110 | :param verify: A flag used to check Cisco DNAC's certificate. 111 | type: boolean 112 | default: False 113 | required: no 114 | :param timeout: The number of seconds to wait for Cisco DNAC's response. 115 | type: int 116 | default: 5 117 | required: no 118 | """ 119 | self.__url = url 120 | self.__bauth = basic_auth 121 | self.__ctype = content_type 122 | self.__resource = resource 123 | self.__token = '' 124 | self.__verify = verify 125 | self.__timeout = timeout 126 | self.__hdrs = {} 127 | 128 | # end __init__() 129 | 130 | def __str__(self): 131 | """ 132 | String handler for an XAuthToken object. Returns the current value of __token. 133 | :return: str 134 | """ 135 | return self.__token 136 | 137 | # end __str__ 138 | 139 | @property 140 | def url(self): 141 | """ 142 | Get method url returns the value of attribute __url. URL is the base path to an HTTP server. 143 | :return: str 144 | """ 145 | return self.__url 146 | 147 | # end url getter 148 | 149 | @property 150 | def bauth(self): 151 | """ 152 | Get method bauth returns the value of attribute __bauth, a BasicAuth object for logging into an HTTP server. 153 | :return: BasicAuth object 154 | """ 155 | return self.__bauth 156 | 157 | # end bauth getter 158 | 159 | @property 160 | def ctype(self): 161 | """ 162 | Get method ctype returns the value of attribute __ctype, a CType object indicating the content type a user 163 | wants the HTTP server to return. 164 | :return: Ctype object 165 | """ 166 | return self.__ctype 167 | 168 | # end ctype getter 169 | 170 | @property 171 | def resource(self): 172 | """ 173 | Returns the value of attribute __resource, the resource path used to request an x-auth-token. 174 | :return: str 175 | """ 176 | return self.__resource 177 | 178 | # end resource getter 179 | 180 | @property 181 | def verify(self): 182 | """ 183 | Get method verify returns the value of attribute __verify. Verify determines whether the HTTP server's 184 | certificate should be validated or not. 185 | :return: bool 186 | """ 187 | return self.__verify 188 | 189 | # end verify getter 190 | 191 | @property 192 | def timeout(self): 193 | """ 194 | Get method timeout returns the value of attribute __timeout. The value returned represents the number of 195 | seconds to wait for the HTTP server to return an x-auth-token. 196 | :return: int 197 | """ 198 | return self.__timeout 199 | 200 | # end timeout getter 201 | 202 | @property 203 | def token(self): 204 | """ 205 | Get method token returns the value of attribute __token, which is the x-auth-token value used by other API 206 | calls to authenticate with the HTTP server. 207 | :return: str 208 | """ 209 | return self.__token 210 | 211 | # end token getter 212 | 213 | def get_token(self): 214 | """ 215 | Class method getToken causes the XAuthToken instance to send a request to the server for a new x-auth-token. 216 | The returned result gets stored in __token and then the function updates the object's __hdrs dictionary with 217 | the new token's value before returning the token to the calling script. 218 | :return: str 219 | """ 220 | url = self.__url + self.__resource 221 | hdrs = {} 222 | hdrs.update(self.bauth.hdrs) 223 | hdrs.update(self.ctype.hdrs) 224 | resp = requests.request('POST', 225 | url, 226 | headers=hdrs, 227 | verify=self.__verify, 228 | timeout=self.__timeout) 229 | if resp.status_code != requests.codes.ok: 230 | raise XAuthTokenError( 231 | 'XAuthToken: getToken: %s: %s: %i: expected %i' % 232 | (INVALID_RESPONSE, url, resp.status_code, requests.codes.ok) 233 | ) 234 | self.__token = str(json.loads(resp.text)['Token']) 235 | self.__hdrs['X-Auth-Token'] = self.__token 236 | return self.__token 237 | 238 | # end get_token() 239 | 240 | @property 241 | def hdrs(self): 242 | """ 243 | Get method hdrs returns the value of attribute __hdrs, a dict whose value is {'X-Auth-Token': ''} and 244 | can be used to construct CRUD headers for authenticating calls to a server. 245 | :return: dict 246 | """ 247 | return self.__hdrs 248 | 249 | # end hdrs getter 250 | 251 | # end class XAuthToken() 252 | 253 | -------------------------------------------------------------------------------- /docs/Cisco DNAC Wrapper UML.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsayle/DNAC-Python-Wrapper/dfccb9453b6fc853177df2cca1669aad57e3d4fa/docs/Cisco DNAC Wrapper UML.pdf -------------------------------------------------------------------------------- /docs/Cisco DNAC Wrapper UML.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsayle/DNAC-Python-Wrapper/dfccb9453b6fc853177df2cca1669aad57e3d4fa/docs/Cisco DNAC Wrapper UML.vsdx -------------------------------------------------------------------------------- /docs/DnacError.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Python: class DnacError 4 | 5 | 6 |

7 | 8 | 9 | 11 | 12 | 13 | 18 | 19 |
 
10 | dnac.DnacError = class DnacError(exceptions.Exception)
   DnacError is an exception class for any errors specific to setting up and maintaining a connection with a Cisco DNA
14 | Center cluster.
15 |  
16 | Attributes:
17 |     None
 
 
Method resolution order:
20 |
DnacError
21 |
exceptions.Exception
22 |
exceptions.BaseException
23 |
__builtin__.object
24 |
25 |
26 | Methods defined here:
27 |
__init__(self, msg)
DnacError's __init__ method passes a message to its parent class.
28 | :param msg: An error message indicating the problem.  Current values
29 | 30 |
31 | Data descriptors defined here:
32 |
__weakref__
33 |
list of weak references to the object (if defined)
34 |
35 |
36 | Data and other attributes inherited from exceptions.Exception:
37 |
__new__ = <built-in method __new__ of type object>
T.__new__(S, ...) -> a new object with type S, a subtype of T
38 | 39 |
40 | Methods inherited from exceptions.BaseException:
41 |
__delattr__(...)
x.__delattr__('name') <==> del x.name
42 | 43 |
__getattribute__(...)
x.__getattribute__('name') <==> x.name
44 | 45 |
__getitem__(...)
x.__getitem__(y) <==> x[y]
46 | 47 |
__getslice__(...)
x.__getslice__(i, j) <==> x[i:j]
48 |  
49 | Use of negative indices is not supported.
50 | 51 |
__reduce__(...)
52 | 53 |
__repr__(...)
x.__repr__() <==> repr(x)
54 | 55 |
__setattr__(...)
x.__setattr__('name', value) <==> x.name = value
56 | 57 |
__setstate__(...)
58 | 59 |
__str__(...)
x.__str__() <==> str(x)
60 | 61 |
__unicode__(...)
62 | 63 |
64 | Data descriptors inherited from exceptions.BaseException:
65 |
__dict__
66 |
67 |
args
68 |
69 |
message
70 |
71 |
72 | -------------------------------------------------------------------------------- /docs/basicauth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Python: module dnac.basicauth 4 | 5 | 6 | 7 | 8 | 9 |
 
10 |  
dnac.basicauth
index
/cygdrive/c/Users/rsayle/PycharmProjects/DNAC-Python-Wrapper/dnac/basicauth.py
13 |

14 |

15 | 16 | 17 | 19 | 20 | 21 |
 
18 | Classes
       
22 |
__builtin__.object 23 |
24 |
25 |
BasicAuth 26 |
27 |
28 |
29 |

30 | 31 | 32 | 34 | 35 | 36 | 38 | 39 |
 
33 | class BasicAuth(__builtin__.object)
   Class BasicAuth stores the username and password for basic authorization of an HTTP request and then encodes it as
37 | "username:password" in base64 format.  The encoded string is used to authenticate connections to an API server.
 
 Methods defined here:
40 |
__init__(self, user, passwd)
The __init__ method initializes a BasicAuth object.  It takes in a username and password, sets the __user and
41 | __passwd, respectively, and then uses those values to create a base64 encoded string for the __creds attribute
42 | and a header dictionary for __hdrs.
43 |  
44 | Usage: b = BasicAuth(user, passwd)
45 |  
46 | :param user: The username for logging into an HTTP server
47 |     type: str
48 |     required: yes
49 |     default: none
50 | :param passwd: The password for logging into an HTTP server
51 |     type: str
52 |     required: yes
53 |     default: none
54 | 55 |
__str__(self)
String handler for a BasicAuth object.
56 | :return: base64 encoded string "<username>:<password>"
57 | 58 |
make_creds(self)
Builds the base64 encoded string <username>:<password> for basic authentication to an API server.
59 | :return: base64 encoded str
60 | 61 |
62 | Data descriptors defined here:
63 |
__dict__
64 |
dictionary for instance variables (if defined)
65 |
66 |
__weakref__
67 |
list of weak references to the object (if defined)
68 |
69 |
creds
70 |
Getter method to retrieve the credentials stored in the object instance.
71 | :return: base64 encoded string
72 |
73 |
hdrs
74 |
Getter method that returns the format of a basic auth request for an API request.
75 | :return: dict of the form {'Authorization': 'Basic <base64 encoded credentials>'
76 |
77 |

78 | -------------------------------------------------------------------------------- /docs/config_archiver.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsayle/DNAC-Python-Wrapper/dfccb9453b6fc853177df2cca1669aad57e3d4fa/docs/config_archiver.pdf -------------------------------------------------------------------------------- /docs/config_archiver.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsayle/DNAC-Python-Wrapper/dfccb9453b6fc853177df2cca1669aad57e3d4fa/docs/config_archiver.vsdx -------------------------------------------------------------------------------- /docs/ctype.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Python: module dnac.ctype 4 | 5 | 6 | 7 | 8 | 9 |
 
10 |  
dnac.ctype
index
/cygdrive/c/Users/rsayle/PycharmProjects/DNAC-Python-Wrapper/dnac/ctype.py
13 |

# globals

14 |

15 | 16 | 17 | 19 | 20 | 21 |
 
18 | Classes
       
22 |
__builtin__.object 23 |
24 |
25 |
CType 26 |
27 |
28 |
29 |

30 | 31 | 32 | 34 | 35 | 36 | 49 | 50 |
 
33 | class CType(__builtin__.object)
   Class CType stores the content type a user wants a CRUD API to return.
37 |  
38 | Attributes:
39 |     ctype: The content type to return.  Valid types include:
40 |             application/json
41 |             application/xml
42 |         type: str
43 |         scope: protected
44 |         default: application/json
45 |     hdrs: a dict for constructing CRUD request headers
46 |         type: dict
47 |         scope: public
48 |         default: {'Content-Type': 'application/json'}
 
 Methods defined here:
51 |
__init__(self, content_type='application/json')
Method __init__ initializes a CType object.
52 | :param content_type: content type to return.  Valid types include:
53 |                  application/json
54 |                  application/xml
55 |      type: str
56 |      default: application/json
57 |      required: no
58 | 59 |
__str__(self)
String handler for a CType object.
60 | :return: str
61 | 62 |
63 | Data descriptors defined here:
64 |
__dict__
65 |
dictionary for instance variables (if defined)
66 |
67 |
__weakref__
68 |
list of weak references to the object (if defined)
69 |
70 |
ctype
71 |
Get method ctype returns the string value of __ctype.
72 | :return: str
73 |
74 |
hdrs
75 |
Get method hdrs returns the string value of __hdrs.
76 | :return: dict
77 |
78 |

79 | 80 | 81 | 83 | 84 | 85 |
 
82 | Data
       JSON = 'application/json'
86 | XML = 'application/xml'
87 | -------------------------------------------------------------------------------- /docs/deployment.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Python: module dnac.deployment 4 | 5 | 6 | 7 | 8 | 9 |
 
10 |  
dnac.deployment
index
/cygdrive/c/Users/rsayle/PycharmProjects/DNAC-Python-Wrapper/dnac/deployment.py
13 |

14 |

15 | 16 | 17 | 19 | 20 | 21 |
 
18 | Classes
       
22 |
dnac.dnacapi.DnacApi(__builtin__.object) 23 |
24 |
25 |
Deployment 26 |
27 |
28 |
29 |

30 | 31 | 32 | 34 | 35 | 36 | 47 | 48 |
 
33 | class Deployment(dnac.dnacapi.DnacApi)
   The Deployment class monitors Cisco DNA Center's progress when pushing
37 | a template to a set of devices.  When using a Template object to
38 | apply changes, the Template automatically creates and stores a
39 | corresponding Deployment instance.  Use the Template to reference the
40 | Deployment for the job's results.
41 |  
42 | Usage:
43 |     d = Dnac()
44 |     template = Template(d, 'Set VLAN')
45 |     d.api['Set VLAN'].deploy()
46 |     print d.api['Set VLAN'].deployment.check_deployment()
 
 
Method resolution order:
49 |
Deployment
50 |
dnac.dnacapi.DnacApi
51 |
__builtin__.object
52 |
53 |
54 | Methods defined here:
55 |
__init__(self, dnac, deployment_id, verify=False, timeout=5)
Creates a deployment object and sets the deployment job's UUID.
56 |  
57 | :param dnac: A reference to the master Dnac instance.
58 |     type: Dnac object
59 |     required: yes
60 |     default: None
61 | :param deployment_id: The deployment job's UUID.
62 |     type: str
63 |     required: yes
64 |     default: None
65 | :param verify: A flag that sets whether or not Cisco DNA Center's certificated should be authenticated.
66 |     type: bool
67 |     required: no
68 |     default: False
69 | :param timeout: The number of seconds to wait for a response from Cisco DNAC.
70 |     type: int
71 |     required: no
72 |     default: 5
73 | 74 |
check_deployment(self)
Makes an API call to Cisco DNA Center for the deployment job's results.
75 | :return: dict
76 | 77 |
78 | Data descriptors defined here:
79 |
deployment_id
80 |
Provides the deployment job's UUID.
81 | :return: str
82 |
83 |
status
84 |
When available, reports the deployment job's status: SUCCESS or FAILURE.  Returns an empty string if the job
85 | has not yet completed.
86 | :return: str
87 |
88 |
89 | Data descriptors inherited from dnac.dnacapi.DnacApi:
90 |
__dict__
91 |
dictionary for instance variables (if defined)
92 |
93 |
__weakref__
94 |
list of weak references to the object (if defined)
95 |
96 |
crud
97 |
Get method crud returns the reference to the Crud object this class uses to make RESTful API calls.  Use this
98 | function in order to access the Crud object's get, put, post and delete methods.
99 | :return: Crud object
100 |
101 |
dnac
102 |
Get method dnac returns the reference to the Dnac instance that stores this object.
103 | :return: Dnac object
104 |
105 |
name
106 |
Get method name returns __name, a user friendly name for accessing this object in Dnac's API store, namely
107 | Dnac.api{}
108 | :return: str
109 |
110 |
resource
111 |
Get method resource returns __resource, which is the API call to Cisco DNAC.  In child classes, use this method
112 | to form requests by appending it to Cisco DNAC's base url, i.e. Dnac.url.
113 | :return: str
114 |
115 |
results
116 |
The results method is decorated as if it was an attribute of this class and can be used accordingly.
117 | It returns the raw results stored in the class' Crud object obtained from an API call.
118 | :return: dict
119 |
120 |
timeout
121 |
Get method timeout returns the value of attribute __timeout.  The value returned represents the number of
122 | seconds to wait for the HTTP server to respond to an API request.
123 | :return: int
124 |
125 |
verify
126 |
Get method verify returns value of __verify, which is used to determine whether or not to check Cisco DNAC's
127 | certificate upon making an API call.
128 | :return: bool
129 |
130 |

131 | 132 | 133 | 135 | 136 | 137 |
 
134 | Data
       ACCEPTED = 202
138 | DEPLOYMENT_RESOURCE_PATH = {'1.2.10': '/api/v1/template-programmer/template/deploy/status', '1.2.8': '/api/v1/template-programmer/template/deploy/status', '1.3.0.2': '/api/v1/template-programmer/template/deploy/status', '1.3.0.3': '/api/v1/template-programmer/template/deploy/status', '1.3.1.3': '/api/v1/template-programmer/template/deploy/status', '1.3.1.4': '/dna/intent/api/v1/template-programmer/template/deploy/status/'}
139 | ERROR_MSGS = {400: '400 - API request syntax is malformed', 401: '401 - API request has missing or invalid credentials', 403: '403 - API request is unauthorized', 404: '404 - API requested resource does not exist', 409: '409 - API requested resource is in a conflicted state', 415: '415 - API request body is in an unsupported format', 417: '417 - The server cannot meet the requirements of the Expect request-header field', 500: '500 - Server could not fulfill the API request', 501: '501 - Server has not implemented the requested function', 503: '503 - Server is unavailable', ...}
140 | MODULE = 'deployment.py'
141 | NO_STATUS = ''
142 | REQUEST_NOT_ACCEPTED = 'API request is not ACCEPTED'
143 | STATUS_KEY = 'status'
144 | SUPPORTED_DNAC_VERSIONS = ['1.2.8', '1.2.10', '1.3.0.2', '1.3.0.3', '1.3.1.3', '1.3.1.4']
145 | UNSUPPORTED_DNAC_VERSION = 'Unsupported Cisco DNA Center version'
146 | -------------------------------------------------------------------------------- /docs/dnac_config.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Python: module dnac.dnac_config 4 | 5 | 6 | 7 | 8 | 9 |
 
10 |  
dnac.dnac_config
index
/cygdrive/c/Users/rsayle/PycharmProjects/DNAC-Python-Wrapper/dnac/dnac_config.py
13 |

Configuration parameters for a Dnac object.

14 |

15 | 16 | 17 | 19 | 20 | 21 |
 
18 | Data
       DNAC_CONTENT_TYPE = 'application/json'
22 | DNAC_IP = ''
23 | DNAC_NAME = 'denlab-en-dnac.cisco.com'
24 | DNAC_PASSWD = 'C!sco123'
25 | DNAC_PORT = '443'
26 | DNAC_USER = 'admin'
27 | DNAC_VERSION = '1.3.1.4'
28 | -------------------------------------------------------------------------------- /docs/intercluster_template_manager.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsayle/DNAC-Python-Wrapper/dfccb9453b6fc853177df2cca1669aad57e3d4fa/docs/intercluster_template_manager.pdf -------------------------------------------------------------------------------- /docs/intercluster_template_manager.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsayle/DNAC-Python-Wrapper/dfccb9453b6fc853177df2cca1669aad57e3d4fa/docs/intercluster_template_manager.vsdx -------------------------------------------------------------------------------- /docs/mkdocs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sh 2 | 3 | # move to the module root dir 4 | cd ../ 5 | 6 | # collect the list of modules 7 | cd dnac/ 8 | modules=`ls *.py` 9 | cd ../ 10 | 11 | # create the module pydocs 12 | for module in $modules 13 | do 14 | if [ $module != "__init__.py" ]; then 15 | target=`echo $module | sed 's/\.py//'` 16 | pydoc -w dnac.$target 17 | mv dnac.$target.html docs/$target.html 18 | fi 19 | done 20 | 21 | # create docs from __init__.py 22 | pydoc -w dnac.Dnac 23 | mv dnac.Dnac.html docs/Dnac.html 24 | pydoc -w dnac.DnacError 25 | mv dnac.DnacError.html docs/DnacError.html 26 | 27 | # return to the docs dir 28 | cd docs/ 29 | 30 | -------------------------------------------------------------------------------- /docs/network_hierarchy_replicator.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsayle/DNAC-Python-Wrapper/dfccb9453b6fc853177df2cca1669aad57e3d4fa/docs/network_hierarchy_replicator.pdf -------------------------------------------------------------------------------- /docs/network_hierarchy_replicator.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsayle/DNAC-Python-Wrapper/dfccb9453b6fc853177df2cca1669aad57e3d4fa/docs/network_hierarchy_replicator.vsdx -------------------------------------------------------------------------------- /docs/template_replicator.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsayle/DNAC-Python-Wrapper/dfccb9453b6fc853177df2cca1669aad57e3d4fa/docs/template_replicator.pdf -------------------------------------------------------------------------------- /docs/template_replicator.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsayle/DNAC-Python-Wrapper/dfccb9453b6fc853177df2cca1669aad57e3d4fa/docs/template_replicator.vsdx -------------------------------------------------------------------------------- /docs/timestamp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Python: module dnac.timestamp 4 | 5 | 6 | 7 | 8 | 9 |
 
10 |  
dnac.timestamp
index
/cygdrive/c/Users/rsayle/PycharmProjects/DNAC-Python-Wrapper/dnac/timestamp.py
13 |

14 |

15 | 16 | 17 | 19 | 20 | 21 |
 
18 | Modules
       
time
22 |

23 | 24 | 25 | 27 | 28 | 29 |
 
26 | Classes
       
30 |
__builtin__.object 31 |
32 |
33 |
TimeStamp 34 |
35 |
36 |
37 |

38 | 39 | 40 | 42 | 43 | 44 | 57 | 58 |
 
41 | class TimeStamp(__builtin__.object)
   The TimeStamp class takes the system's time in UTC and converts it to epoch time given in milliseconds.
45 | Remember that the TimeStamp's value is set when it is created and is not updated whenever it is referenced.
46 | Future enhancements to this class may change this behavior if needed.
47 |  
48 | Attributes:
49 |     timestamp: Current system epoch time in milliseconds
50 |         type: int
51 |         default: current epoch time
52 |         scope: protected
53 |  
54 | Usage:
55 |     time = TimeStamp()
56 |     print(time)
 
 Methods defined here:
59 |
__init__(self, time=0)
The TimeStamp's __init__ method sets its value to the current number of milliseconds in epoch time.
60 | :param time: Epoch time with which to initialize the new object.
61 |     type: int
62 |     required: no
63 |     default: retrieves the current time
64 | 65 |
__str__(self)
TimeStamp class' __str__ function converts its timestamp attribute, an int, into a string.
66 | :return: The epoch time in milliseconds when the TimeStamp object was created.
67 | 68 |
get_current_time(self)
Resets the TimeStamp to the current time.
69 | :return: The current time as an epoch integer.
70 | 71 |
local_timestamp(self)
The local_timestamp method returns the object's current timestamp value as a formatted string in the Cisco
72 | DNA Center instance's local timezone.
73 | :return: The timestamp in local time formatted as %Y-%m-%d %H:%M:%S
74 | 75 |
sleep(self, seconds)
Instructs the TimeStamp object to wait the number of seconds given.
76 | :param seconds: The number of seconds to halt program activity.
77 |     type: int
78 |     required: yes
79 |     default: none
80 | :return: none
81 | 82 |
utc_timestamp(self)
The utc_timestamp method returns the object's current timestamp value as a formatted string in UTC time.
83 | :return: The timestamp in UTC time formatted as %Y-%m-%d %H:%M:%S
84 | 85 |
86 | Data descriptors defined here:
87 |
__dict__
88 |
dictionary for instance variables (if defined)
89 |
90 |
__weakref__
91 |
list of weak references to the object (if defined)
92 |
93 |
timestamp
94 |
The timestamp getter method returns the object's epoch time in milliseconds.
95 | :return: The epoch time in milliseconds when the TimeStamp object was created.
96 |
97 |

98 | 99 | 100 | 102 | 103 | 104 |
 
101 | Data
       GET_CURRENT_TIME = 0
105 | MODULE = 'timestamp.py'
106 | NO_TIME = -1
107 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | # complied python modules 2 | *.pyc 3 | -------------------------------------------------------------------------------- /examples/commandrunner_example.py: -------------------------------------------------------------------------------- 1 | 2 | from dnac import Dnac 3 | from dnac.commandrunner import CommandRunner 4 | import pprint 5 | 6 | 7 | MODULE = 'commandrunner_example.py' 8 | 9 | print('%s: preparing to run a command...' % MODULE) 10 | 11 | dnac = Dnac() 12 | 13 | cmd = CommandRunner(dnac, 'ShowRunInt') 14 | 15 | print('%s: setting the CLI command...' % MODULE) 16 | 17 | command = 'show run interface gig1/0/8' 18 | 19 | print('%s: acquiring the target switch...' % MODULE) 20 | 21 | switch = 'bf5eda5f-05d7-48a8-84e3-481aaa0c4da5' 22 | 23 | print('%s: formatting the command for Cisco DNA Center...' % MODULE) 24 | 25 | dnac.api['ShowRunInt'].format_cmd(command, switch) 26 | 27 | print('%s: running the command...' % MODULE) 28 | 29 | dnac.api['ShowRunInt'].run(wait=10) 30 | 31 | print('%s: command results = ' % MODULE) 32 | pprint.pprint(dnac.api['ShowRunInt'].task.file.results, indent=4) 33 | 34 | print() 35 | -------------------------------------------------------------------------------- /examples/config_archiver/config_archiver.py: -------------------------------------------------------------------------------- 1 | 2 | from dnac import Dnac 3 | from dnac.config_archive import ConfigArchive 4 | from dnac.config_archive_settings import ConfigArchiveSettings 5 | from dnac.networkdevice import NetworkDevice 6 | from dnac.timestamp import TimeStamp 7 | from dnac.dnacapi import DnacApiError 8 | from bottle import Bottle, run, template, request 9 | 10 | MODULE = 'config_archive.py' 11 | SINGLETON = 1 12 | 13 | archiver = Bottle() 14 | 15 | 16 | @archiver.route('/') 17 | @archiver.route('/index') 18 | @archiver.route('/config_archiver') 19 | def config_archiver(): 20 | return template('config_archiver', dnac=dnac, method='GET') 21 | 22 | 23 | @archiver.route('/manage_settings', method='GET') 24 | def manage_settings(): 25 | return template('manage_settings', dnac=dnac, archive_settings=settings) 26 | 27 | 28 | @archiver.route('/change_settings', method='GET') 29 | def change_settings(): 30 | return template('change_settings', dnac=dnac, archive_settings=settings) 31 | 32 | 33 | @archiver.route('/submit_new_settings', method='POST') 34 | def submit_new_settings(): 35 | time = request.forms.get('timeout') 36 | days = request.forms.get('num_days') 37 | vers = request.forms.get('num_vers') 38 | new_settings = {'timeout': time, 'noOfDays': days, 'noOfVersion': vers} 39 | settings.settings = new_settings 40 | return template('manage_settings', dnac=dnac, archive_settings=settings) 41 | 42 | 43 | @archiver.route('/manage_archive', method='GET') 44 | def manage_archive(): 45 | hosts = [] 46 | for device_id in config_archive.archive: 47 | hostname = device_api.get_device_by_id(device_id)['hostname'] 48 | hosts.append(hostname) 49 | hosts.sort() 50 | return template('manage_archive', dnac=dnac, hosts=hosts) 51 | 52 | 53 | @archiver.route('/manage_archive_configs', method='POST') 54 | def manage_archive_configs(): 55 | host = request.forms.get('host') 56 | device_api.get_device_by_name(host) 57 | device_archive = config_archive.archive[device_api.devices['id']] 58 | return template('manage_archive_configs', dnac=dnac, host=host, device_archive=device_archive, timestamp=timestamp) 59 | 60 | 61 | @archiver.route('/view_config', method='POST') 62 | def view_config(): 63 | file_name = 'file_%s' % request.forms.get('file_id') 64 | config = dnac.api[file_name].results 65 | return template('view_config', config=config) 66 | 67 | 68 | @archiver.route('/delete_config', method='POST') 69 | def delete_config(): 70 | file_id = request.forms.get('file_id') 71 | host = request.forms.get('host') 72 | for device_id, device_archive in config_archive.archive.items(): 73 | for version in device_archive.versions: 74 | for file_type, file in version.config_files.items(): 75 | if file_id == file.id: 76 | target_version = version 77 | target_version.delete_config_file(file_id) 78 | return template('delete_config', host=host) 79 | 80 | 81 | @archiver.route('/add_device_archive_version', method='POST') 82 | def add_device_archive_version(): 83 | host = request.forms.get('host') 84 | return template('add_device_archive_version', dnac=dnac, host=host) 85 | 86 | 87 | @archiver.route('/add_new_device_archive_version', method='POST') 88 | def add_new_device_archive_version(): 89 | running = False 90 | startup = False 91 | hosts = set() 92 | requested_config_types = request.forms 93 | for name, value in requested_config_types.items(): 94 | hosts.add(name.split('_')[0]) # format of name = _[running | startup] 95 | if value == 'running': 96 | running = True 97 | elif value == 'startup': 98 | startup = True 99 | if len(hosts) != SINGLETON: 100 | raise Exception('%s: expected one host: received %i: host = %s' % (MODULE, len(hosts), str(hosts))) 101 | # get the host's id 102 | host = hosts.pop() 103 | host_id = device_api.get_device_by_name(host)['id'] 104 | # find the host's device archive 105 | host_archive = config_archive.archive[host_id] 106 | host_archive.add_configs_to_archive(running=running, startup=startup) 107 | return template('add_new_device_archive_version', dnac=dnac, host=host) 108 | 109 | 110 | @archiver.route('/delete_device_archive_versions', method='POST') 111 | def delete_device_archive_versions(): 112 | host = request.forms.get('host') 113 | device_api.get_device_by_name(host) 114 | device_archive = config_archive.archive[device_api.devices['id']] 115 | return template('delete_device_archive_versions', dnac=dnac, host=host, 116 | device_archive=device_archive, timestamp=timestamp) 117 | 118 | 119 | @archiver.route('/delete_archives', method='POST') 120 | def delete_archives(): 121 | timestamps = request.forms 122 | hosts = set() 123 | deleted_versions = [] 124 | # get the hostname 125 | for name in timestamps: 126 | hostname = name.split('_')[0] # format of name = _timestamp_ 127 | hosts.add(hostname) 128 | if len(hosts) != SINGLETON: 129 | raise Exception('%s: expected one host: received %i: host = %s' % (MODULE, len(hosts), str(hosts))) 130 | # get the host's id 131 | host = hosts.pop() 132 | host_id = device_api.get_device_by_name(host)['id'] 133 | # find the host's device archive 134 | host_archive = config_archive.archive[host_id] 135 | # delete the selected versions 136 | for name, time in timestamps.items(): # format of name = _timestamp_ 137 | time = name.split('_')[2] 138 | for version in host_archive.versions: 139 | if version.created_time == int(time): 140 | # found a version to delete 141 | # 142 | # DNAC versions 1.3.0.1 and prior: 143 | # DNAC incorrectly reports a version could not be deleted when in fact it was. Catch the exception 144 | # raised by Version and double-check to see if the Version is gone. 145 | # 146 | try: 147 | host_archive.delete_version(version) 148 | except DnacApiError as error: 149 | error_msg = str(error) 150 | failure_reason = error_msg.split(':')[6] 151 | if failure_reason.find('Failed to delete archives. Internal Error.'): 152 | # double-check that the version was deleted 153 | pass 154 | else: 155 | raise error 156 | timestamp.timestamp = time 157 | deleted_versions.append(timestamp.local_timestamp()) 158 | return template('delete_archives', host=host, deleted_versions=deleted_versions) 159 | 160 | 161 | @archiver.route('/create_new_archive', method='GET') 162 | def create_new_archive(): 163 | # get any devices that don't already have an archive 164 | candidates = {} 165 | all_hosts = device_api.get_all_devices() 166 | for host in all_hosts: 167 | if host['id'] not in config_archive.archive: 168 | candidates[host['hostname']] = host['id'] 169 | return template('create_new_archive', dnac=dnac, hosts=candidates) 170 | 171 | 172 | @archiver.route('/create_new_device_archive', method='POST') 173 | def create_new_device_archive(): 174 | host = request.forms.get('host') 175 | host_id = device_api.get_device_by_name(host)['id'] 176 | config_archive.add_new_archive(host_id) 177 | return template('create_new_device_archive', dnac=dnac, host=host) 178 | 179 | 180 | if __name__ == '__main__': 181 | dnac = Dnac(name='', 182 | version='1.3.0.3', 183 | ip='10.1.41.218', 184 | port='443', 185 | user='admin', 186 | passwd='P@$$w0rd', 187 | content_type='application/json') 188 | timestamp = TimeStamp() 189 | device_api = NetworkDevice(dnac, 'deviceapi') 190 | if bool(dnac.name): 191 | settings = ConfigArchiveSettings(dnac, dnac.name) 192 | config_archive = ConfigArchive(dnac, dnac.name) 193 | config_archive.load_all_archives() 194 | elif bool(dnac.ip): 195 | settings = ConfigArchiveSettings(dnac, dnac.name) 196 | config_archive = ConfigArchive(dnac, dnac.name) 197 | config_archive.load_all_archives() 198 | else: 199 | print('Could not connect to DNA Center. Set the FQDN or IP in dnac_config.') 200 | exit(1) 201 | run(archiver, host='localhost', port=8080, reloader=True, debug=True) -------------------------------------------------------------------------------- /examples/config_archiver/views/add_device_archive_version.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Add Device Archive Version 5 |

6 |

7 | % if bool(dnac.name): 8 | Connected to DNA Center cluster: {{dnac.name}} 9 | % elif bool(dnac.ip): 10 | Connected to DNA Center cluster: {{dnac.ip}} 11 | % else: 12 | Cannot connect to DNA Center cluster. Please set the FQDN or cluster IP in dnac_config. 13 | %end 14 |

15 | Add archive version for {{host}} 16 |

17 | Select the configuration files to add:
18 |

19 | Running Config
20 | Startup Config
21 | 22 |
23 |
24 | 25 |
26 | -------------------------------------------------------------------------------- /examples/config_archiver/views/add_new_device_archive_version.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Add New Device Archive Version Result 5 |

6 |

7 | % if bool(dnac.name): 8 | Connected to DNA Center cluster: {{dnac.name}} 9 | % elif bool(dnac.ip): 10 | Connected to DNA Center cluster: {{dnac.ip}} 11 | % else: 12 | Cannot connect to DNA Center cluster. Please set the FQDN or cluster IP in dnac_config. 13 | %end 14 |

15 | Successfully added a new archive version for {{host}} 16 |

17 |

18 | 19 |
20 | -------------------------------------------------------------------------------- /examples/config_archiver/views/change_settings.tpl: -------------------------------------------------------------------------------- 1 | % current = archive_settings.settings 2 | 3 |

4 | Change Archive Settings 5 |

6 |

7 | % if bool(dnac.name): 8 | Connected to DNA Center cluster: {{dnac.name}} 9 | % elif bool(dnac.ip): 10 | Connected to DNA Center cluster: {{dnac.ip}} 11 | % else: 12 | Cannot connect to DNA Center cluster. Please set the FQDN or cluster IP in dnac_config. 13 | %end 14 |

15 |

16 | Timeout
17 | Number of Days
18 | Number of Versions
19 | 20 | 21 |
22 | -------------------------------------------------------------------------------- /examples/config_archiver/views/config_archiver.tpl: -------------------------------------------------------------------------------- 1 | 2 |

Cisco DNA Center Configuration Archiver

3 |

4 | % if bool(dnac.name): 5 | Connected to DNA Center cluster: {{dnac.name}} 6 | % elif bool(dnac.ip): 7 | Connected to DNA Center cluster: {{dnac.ip}} 8 | % else: 9 | Cannot connect to DNA Center cluster. Please set the FQDN or cluster IP in dnac_config. 10 | %end 11 |

12 | 13 | 14 | 19 | 24 | 29 | 30 |
15 |
16 | 17 |
18 |
20 |
21 | 22 |
23 |
25 |
26 | 27 |
28 |
31 | -------------------------------------------------------------------------------- /examples/config_archiver/views/create_new_archive.tpl: -------------------------------------------------------------------------------- 1 | 2 |

3 | Create New Archive 4 |

5 |

6 | % if bool(dnac.name): 7 | Connected to DNA Center cluster: {{dnac.name}} 8 | % elif bool(dnac.ip): 9 | Connected to DNA Center cluster: {{dnac.ip}} 10 | % else: 11 | Cannot connect to DNA Center cluster. Please set the FQDN or cluster IP in dnac_config. 12 | %end 13 |

14 | % if not bool(hosts): 15 | All devices already have archives. 16 |

17 |

18 | 19 |
20 | % else: 21 |
22 | 27 | 28 |
29 |
30 | 31 |
32 | 33 | % end 34 | -------------------------------------------------------------------------------- /examples/config_archiver/views/create_new_device_archive.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Create New Device Archive 5 |

6 |

7 | % if bool(dnac.name): 8 | Connected to DNA Center cluster: {{dnac.name}} 9 | % elif bool(dnac.ip): 10 | Connected to DNA Center cluster: {{dnac.ip}} 11 | % else: 12 | Cannot connect to DNA Center cluster. Please set the FQDN or cluster IP in dnac_config. 13 | %end 14 |

15 | Create archive for {{host}} 16 |

17 | Select the configuration files to add:
18 |

19 | Running Config
20 | Startup Config
21 | 22 |
23 |
24 | 25 |
26 | 27 | -------------------------------------------------------------------------------- /examples/config_archiver/views/delete_archives.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Successfully deleted selected archive versions: 4 |

5 | % for version in deleted_versions: 6 | {{version}}
7 | % end 8 |

9 |

10 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /examples/config_archiver/views/delete_config.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Successfully deleted selected config file. 4 |

5 |

6 | 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /examples/config_archiver/views/delete_device_archive_versions.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 |

14 | Delete Device Archive Versions 15 |

16 |

17 | % if bool(dnac.name): 18 | Connected to DNA Center cluster: {{dnac.name}} 19 | % elif bool(dnac.ip): 20 | Connected to DNA Center cluster: {{dnac.ip}} 21 | % else: 22 | Cannot connect to DNA Center cluster. Please set the FQDN or cluster IP in dnac_config. 23 | %end 24 |

25 | Configuration archive for {{host}} 26 | 27 | 28 | 29 | 30 | 31 | 32 | % for version in device_archive.versions: 33 | 34 | % timestamp.timestamp = version.created_time 35 | 36 | 40 | % end 41 | 42 | % end 43 | 44 | 45 | 46 | 47 | 48 |
Version
{{timestamp.local_timestamp()}} 37 | 39 |
49 | -------------------------------------------------------------------------------- /examples/config_archiver/views/manage_archive.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Manage Archive

4 |

5 | % if bool(dnac.name): 6 | Connected to DNA Center cluster: {{dnac.name}} 7 | % elif bool(dnac.ip): 8 | Connected to DNA Center cluster: {{dnac.ip}} 9 | % else: 10 | Cannot connect to DNA Center cluster. Please set the FQDN or cluster IP in dnac_config. 11 | %end 12 |

13 |

14 | 19 | 20 | 21 | 22 |
23 |

24 |

25 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /examples/config_archiver/views/manage_archive_configs.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 |

14 | Manage Device Archive Configs 15 |

16 |

17 | % if bool(dnac.name): 18 | Connected to DNA Center cluster: {{dnac.name}} 19 | % elif bool(dnac.ip): 20 | Connected to DNA Center cluster: {{dnac.ip}} 21 | % else: 22 | Cannot connect to DNA Center cluster. Please set the FQDN or cluster IP in dnac_config. 23 | %end 24 |

25 | Configuration archive for {{host}} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | % for version in device_archive.versions: 34 | 35 | % timestamp.timestamp = version.created_time 36 | 37 | % for key in version.config_files.keys(): 38 | 42 | % end 43 | 44 | % end 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
Version
{{timestamp.local_timestamp()}} 39 | % file_id = version.config_files[key].id 40 | {{key}} 41 |
53 | -------------------------------------------------------------------------------- /examples/config_archiver/views/manage_settings.tpl: -------------------------------------------------------------------------------- 1 | % current = archive_settings.settings 2 | 3 |

4 | Configuration Archive Settings 5 |

6 |

7 | % if bool(dnac.name): 8 | Connected to DNA Center cluster: {{dnac.name}} 9 | % elif bool(dnac.ip): 10 | Connected to DNA Center cluster: {{dnac.ip}} 11 | % else: 12 | Cannot connect to DNA Center cluster. Please set the FQDN or cluster IP in dnac_config. 13 | %end 14 |

15 | 16 | 17 | 20 | 23 | 24 | 25 | 28 | 31 | 32 | 33 | 36 | 39 | 40 |
18 | Timeout 19 | 21 | {{current['timeout']}} 22 |
26 | Number of Days 27 | 29 | {{current['noOfDays']}} 30 |
34 | Number of Versions 35 | 37 | {{current['noOfVersion']}} 38 |
41 | 42 | 43 | 50 | 57 | 58 |
44 |
45 | 48 |
49 |
51 |
52 | 55 |
56 |
59 | -------------------------------------------------------------------------------- /examples/config_archiver/views/view_config.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 |             {{config}}:
6 |         
7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/device_finder/device_finder.py: -------------------------------------------------------------------------------- 1 | from dnac import Dnac 2 | from dnac.site import Site, STUB_SITE 3 | from dnac.networkdevice import NetworkDevice, STUB_DEVICE 4 | from dnac.dnacapi import DnacApiError 5 | from bottle import Bottle, run, template, request 6 | import sys 7 | import json 8 | 9 | MODULE = 'device_finder.py' 10 | 11 | NO_CLUSTER_NAME_NOR_IP = "Cluster has no name nor IP address" 12 | HTTPS = 'https://' 13 | INVENTORY_URI = '/dna/provision/devices/inventory?devices-view=inventoryView&selectedSite=' 14 | DEVICE360_URI = '/dna/assurance/home#networkDevice/' 15 | 16 | clusters = [] 17 | finder = Bottle() 18 | 19 | 20 | def get_cluster(cluster_id): 21 | """ 22 | Retrieves the cluster by a cluster identifier. 23 | :param cluster_id: Identifies a cluster by its name or IP address. 24 | type: str 25 | required: yes 26 | default: None 27 | :return: Dnac object 28 | """ 29 | for cluster in clusters: 30 | if cluster_id == cluster.name or cluster_id == cluster.ip: 31 | return cluster 32 | else: 33 | Exception('%s: get_cluster: Could not find cluster %s' % (MODULE, cluster_id)) 34 | 35 | 36 | def get_cluster_id(cluster): 37 | if cluster.name != "": 38 | return cluster.name 39 | elif cluster.ip != "": 40 | return cluster.ip 41 | else: 42 | raise Exception("%s: %s" % (MODULE, NO_CLUSTER_NAME_NOR_IP)) 43 | 44 | 45 | @finder.route('/', method='GET') 46 | @finder.route('/index', method='GET') 47 | @finder.route('/select_device', method='POST') 48 | def select_device(): 49 | return template('select_device', clusters=clusters, method='GET') 50 | 51 | 52 | def get_site(device, cluster): 53 | details = device.get_device_detail_by_name(device.devices['id']) 54 | location = details['location'] 55 | if location in cluster.api.keys(): 56 | location_id = cluster.api[location].id 57 | else: 58 | site = Site(cluster, location) 59 | location_id = site.id 60 | return location_id 61 | 62 | 63 | @finder.route('/find_device_by_name', method='POST') 64 | def find_device_by_name(): 65 | name = request.forms.get('name') 66 | device = None 67 | target_cluster = None 68 | cluster_id = None 69 | inventory_url = '' 70 | device360_url = '' 71 | for cluster in clusters: 72 | # if the device has been cached, return it 73 | if name in cluster.api.keys(): 74 | device = cluster.api[name] 75 | target_cluster = cluster 76 | cluster_id = get_cluster_id(cluster) 77 | break 78 | # otherwise, try finding it in the current cluster 79 | else: 80 | try: 81 | # an exception will be thrown if it's not in the cluster 82 | result = cluster.api[STUB_DEVICE].get_device_by_name(name) 83 | # otherwise, it does exist in the physical cluster; cache it in the cluster and return it 84 | device = NetworkDevice(cluster, result['hostname']) 85 | target_cluster = cluster 86 | cluster_id = get_cluster_id(cluster) 87 | break 88 | except DnacApiError: 89 | # device not found; try the next cluster 90 | continue 91 | # if the device was found, get the associated site info in case the user wants to cross-launch into inventory 92 | if bool(device): 93 | details = device.get_device_detail_by_name(name) 94 | location = details['location'] 95 | if location in target_cluster.api.keys(): 96 | site = target_cluster.api[location] 97 | else: 98 | site = Site(target_cluster, location) 99 | inventory_url = '%s%s%s%s' % (HTTPS, cluster_id, INVENTORY_URI, site.id) 100 | device360_url = '%s%s%s%s' % (HTTPS, cluster_id, DEVICE360_URI, device.get_id_by_device_name(name)) 101 | return template('results', target=name, cluster=cluster_id, device=device, inventory_url=inventory_url, 102 | device360_url=device360_url, method='GET') 103 | 104 | 105 | @finder.route('/find_device_by_ip', method='POST') 106 | def find_device_by_ip(): 107 | ip = request.forms.get('ip') 108 | device = None 109 | target_cluster = None 110 | cluster_id = None 111 | inventory_url = '' 112 | device360_url = '' 113 | for cluster in clusters: 114 | # if the device has been cached, return it 115 | if ip in cluster.api.keys(): 116 | device = cluster.api[ip] 117 | target_cluster = cluster 118 | cluster_id = get_cluster_id(cluster) 119 | break 120 | # otherwise, try finding it in the current cluster 121 | else: 122 | try: 123 | # an exception will be thrown if it's not in the cluster 124 | result = cluster.api[STUB_DEVICE].get_device_by_ip(ip) 125 | # otherwise, it does exist in the physical cluster; cache it in the cluster and return it 126 | device = NetworkDevice(cluster, result['managementIpAddress']) 127 | target_cluster = cluster 128 | cluster_id = get_cluster_id(cluster) 129 | break 130 | except DnacApiError: 131 | # device not found; try the next cluster 132 | continue 133 | # if the device was found, get the associated site info in case the user wants to cross-launch into inventory 134 | if bool(device): 135 | device_by_ip = device.get_device_by_ip(ip) 136 | details = device.get_device_detail_by_name(device_by_ip['hostname']) 137 | location = details['location'] 138 | if location in target_cluster.api.keys(): 139 | site = target_cluster.api[location] 140 | else: 141 | site = Site(target_cluster, location) 142 | inventory_url = '%s%s%s%s' % (HTTPS, cluster_id, INVENTORY_URI, site.id) 143 | device360_url = '%s%s%s%s' % (HTTPS, cluster_id, DEVICE360_URI, device.devices['id']) 144 | return template('results', target=ip, cluster=cluster_id, device=device, inventory_url=inventory_url, 145 | device360_url=device360_url, method='GET') 146 | 147 | 148 | ##### Main Program ##################################################################################################### 149 | 150 | if __name__ == '__main__': 151 | """ 152 | usage: device_finder : finds a network device among multiple DNA Center clusters. 153 | """ 154 | 155 | # read the json encoded config file passed from the CLI 156 | clusters_file = open(sys.argv[1], mode='r') 157 | clusters_from_config_file = json.load(clusters_file) 158 | clusters_file.close() 159 | 160 | # create all the Dnac objects from the clusters that were listed in the config file 161 | clusters = [] 162 | for cluster in clusters_from_config_file: 163 | # create a Dnac object 164 | dnac = Dnac(version=cluster['version'], 165 | name=cluster['name'], 166 | ip=cluster['ip'], 167 | port=cluster['port'], 168 | user=cluster['user'], 169 | passwd=cluster['passwd'], 170 | content_type=cluster['content_type']) 171 | # create a stub site for adding new sites 172 | stub_site = Site(dnac, STUB_SITE) 173 | stub_site.timeout = 60 # my lab's DNAC server is responding slowly; others may not need this 174 | # create a stub network device for finding devices 175 | stub_device = NetworkDevice(dnac, STUB_DEVICE) 176 | # add the new Dnac instance to the global clusters list 177 | clusters.append(dnac) 178 | 179 | run(finder, host='localhost', port=8088, reloader=True, debug=True) -------------------------------------------------------------------------------- /examples/device_finder/dnac_clusters.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "denlab-en-dnac.cisco.com", 4 | "ip": "", 5 | "version": "1.3.1.4", 6 | "port": 443, 7 | "user": "admin", 8 | "passwd": "C!sco123", 9 | "content_type": "application/json" 10 | }, 11 | { 12 | "name": "", 13 | "ip": "173.36.255.155", 14 | "version": "1.3.1.4", 15 | "port": 443, 16 | "user": "admin", 17 | "passwd": "c!sco123", 18 | "content_type": "application/json" 19 | } 20 | ] -------------------------------------------------------------------------------- /examples/device_finder/views/results.tpl: -------------------------------------------------------------------------------- 1 | 2 |

Results

3 |

4 |

5 | % if bool(device): 6 | Found {{target}} in {{cluster}} 7 |

8 | 9 | 10 | 15 | 20 | 21 |
11 |
12 | 13 |
14 |
16 |
17 | 18 |
19 |
22 | % else: 23 | Unable to find {{target}} 24 | % end 25 |

26 |

27 | 28 |
29 |
30 | -------------------------------------------------------------------------------- /examples/device_finder/views/select_device.tpl: -------------------------------------------------------------------------------- 1 | % EMPTY='' 2 | 3 | 4 | 9 | 10 |

Select Device

11 |

12 |

13 | % for cluster in clusters: 14 | % if cluster.name != EMPTY: 15 | Loaded {{cluster.name}}
16 | % elif cluster.ip != EMPTY: 17 | Loaded {{cluster.ip}}
18 | % else: 19 | Cluster has no name nor IP. Check the file used to load your clusters for errors.
20 | % end 21 | % end 22 |
23 |

24 | 25 | 26 | 35 | 44 | 45 |
27 |
28 | 29 |

30 |
31 | 32 |
33 |
34 |
36 |
37 | 38 |

39 |
40 | 41 |
42 |
43 |
46 | -------------------------------------------------------------------------------- /examples/export_project.py: -------------------------------------------------------------------------------- 1 | from dnac import Dnac 2 | from dnac.project import Project 3 | import sys 4 | 5 | ## Main program 6 | 7 | # collect command line arguments 8 | projects = sys.argv[1:] 9 | 10 | # connect to the source Cisco DNA Center cluster 11 | d = Dnac(name='denlab-en-dnac.cisco.com', 12 | version='1.3.1.3', 13 | ip='10.94.164.223', 14 | port='443', 15 | user='admin', 16 | passwd='C!sco123', 17 | content_type='application/json') 18 | 19 | # for each project given on the command line 20 | for project in projects: 21 | 22 | # load the project info 23 | p = Project(d, project) 24 | 25 | # save the project 26 | p.export_project() 27 | 28 | print("Exported project %s" % project) -------------------------------------------------------------------------------- /examples/export_template.py: -------------------------------------------------------------------------------- 1 | from dnac import Dnac 2 | from dnac.template import Template 3 | import sys 4 | 5 | ## Main program 6 | 7 | # collect command line arguments 8 | templates = sys.argv[1:] 9 | 10 | # connect to the source Cisco DNA Center cluster 11 | d = Dnac(name='denlab-en-dnac.cisco.com', 12 | version='1.3.1.3', 13 | ip='10.94.164.223', 14 | port='443', 15 | user='admin', 16 | passwd='C!sco123', 17 | content_type='application/json') 18 | 19 | # for each template named on the command line 20 | for template in templates: 21 | # get the template 22 | t = Template(d, template) 23 | 24 | # save the template 25 | t.export_template() 26 | 27 | # save all the template's versions 28 | for ver in t.versions: 29 | t.export_versioned_template(int(ver['version'])) 30 | 31 | print('Exported template %s' % t.name) 32 | -------------------------------------------------------------------------------- /examples/import_project.py: -------------------------------------------------------------------------------- 1 | from dnac import Dnac 2 | from dnac.project import Project, STUB_PROJECT 3 | import sys 4 | 5 | ## Main program 6 | 7 | # collect command line arguments 8 | projects = sys.argv[1:] 9 | 10 | # connect to the source Cisco DNA Center cluster 11 | d = Dnac(name='', 12 | version='1.3.1.4', 13 | ip='10.91.33.113', 14 | port='443', 15 | user='admin', 16 | passwd='c!sco123', 17 | content_type='application/json') 18 | 19 | # create a dummy Project object to perform the import 20 | p = Project(d, STUB_PROJECT) 21 | 22 | # for each project given on the command line 23 | for project in projects: 24 | 25 | # import the project into DNAC 26 | new_project = p.import_project(project) 27 | 28 | if bool(new_project): 29 | print('Project %s imported.' % new_project.name) 30 | -------------------------------------------------------------------------------- /examples/import_template.py: -------------------------------------------------------------------------------- 1 | from dnac import Dnac 2 | from dnac.template import Template, STUB_TEMPLATE 3 | import sys 4 | 5 | ## Main program 6 | 7 | # collect command line arguments 8 | template = sys.argv[1] 9 | versions = sys.argv[2:] 10 | 11 | # connect to the source Cisco DNA Center cluster 12 | d = Dnac(name='', 13 | version='1.3.1.4', 14 | ip='10.91.33.113', 15 | port='443', 16 | user='admin', 17 | passwd='c!sco123', 18 | content_type='application/json') 19 | 20 | # create a dummy Template object to perform the import 21 | t = Template(d, STUB_TEMPLATE) 22 | 23 | # for each template given on the command line 24 | for version in versions: 25 | 26 | # import the template into DNAC 27 | new_template = t.import_template(template, version) 28 | 29 | # commit the new template so it becomes deployable 30 | new_template.commit_template(comments='Committed by import_template.py') 31 | 32 | if bool(new_template): 33 | print('Imported %s and created version from %s.' % (new_template.name, version)) 34 | -------------------------------------------------------------------------------- /examples/network_hierarchy_replicator/dnac_clusters.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "denlab-en-dnac.cisco.com", 4 | "ip": "", 5 | "version": "1.3.1.4", 6 | "port": 443, 7 | "user": "admin", 8 | "passwd": "C!sco123", 9 | "content_type": "application/json" 10 | }, 11 | { 12 | "name": "", 13 | "ip": "173.36.255.155", 14 | "version": "1.3.1.4", 15 | "port": 443, 16 | "user": "admin", 17 | "passwd": "c!sco123", 18 | "content_type": "application/json" 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /examples/network_hierarchy_replicator/network_hierarchy_replicator.py: -------------------------------------------------------------------------------- 1 | from dnac import Dnac 2 | from dnac.site import Site, STUB_SITE, AREA, BUILDING, FLOOR 3 | from dnac.site_hierarchy import SiteHierarchy, SITE_HIERARCHY_NAME 4 | from bottle import Bottle, run, template, request 5 | import sys 6 | import json 7 | 8 | MODULE = 'network_hierarchy_replicator.py' 9 | 10 | clusters = [] 11 | replicator = Bottle() 12 | 13 | 14 | def get_cluster(cluster_id): 15 | """ 16 | Retrieves the cluster by a cluster identifier. 17 | :param cluster_id: Identifies a cluster by its name or IP address. 18 | type: str 19 | required: yes 20 | default: None 21 | :return: Dnac object 22 | """ 23 | for cluster in clusters: 24 | if cluster_id == cluster.name or cluster_id == cluster.ip: 25 | return cluster 26 | else: 27 | Exception('%s: get_cluster: Could not find cluster %s' % (MODULE, cluster_id)) 28 | 29 | 30 | def get_source_and_target(request): 31 | """ 32 | Retrieves the source and target cluster names passed from a Bottle template in the request forms. Also removes 33 | the source and target from the request's FormsDict so that further processing can be performed on the request's 34 | list of projects or templates. 35 | :param request: The request object returned from a Bottle template's posting. 36 | type: Bottle request object 37 | required: yes 38 | default: none 39 | :return: 40 | source: The source cluster's name 41 | type: str 42 | target: The target cluster's name 43 | type: str 44 | source_cluster: The source cluster's Dnac representation 45 | type: Dnac object 46 | target_cluster: The target cluster's Dnac representation 47 | type: Dnac object 48 | """ 49 | source = request.forms.get('source') 50 | source_cluster = get_cluster(source) 51 | request.forms.pop('source') 52 | target = request.forms.get('target') 53 | target_cluster = get_cluster(target) 54 | request.forms.pop('target') 55 | return source, target, source_cluster, target_cluster 56 | 57 | 58 | @replicator.route('/', method='GET') 59 | @replicator.route('/index', method='GET') 60 | @replicator.route('/select_clusters', method='POST') 61 | def select_clusters(): 62 | """ 63 | Passes the list of all clusters loaded during project initialization to the home page. 64 | :return: Bottle template 65 | """ 66 | return template('select_clusters', clusters=clusters, method='GET') 67 | 68 | 69 | def get_site_hierarchy(cluster): 70 | """ 71 | Retrieves the cluster's SiteHierarchy object. 72 | :param cluster: The Dnac cluster whose SiteHierarchy is required. 73 | type: Dnac object 74 | required: yes 75 | default: none 76 | :return: SiteHierarchy object 77 | """ 78 | if bool(cluster.name): 79 | cluster_id = cluster.name 80 | elif bool(cluster.ip): 81 | cluster_id = cluster.ip 82 | else: 83 | raise Exception('%s: get_site_hierarchy: Dnac cluster has no name nor IP address' % MODULE) 84 | site_hierarchy_name = '%s%s' % (cluster_id, SITE_HIERARCHY_NAME) 85 | if site_hierarchy_name in cluster.api.keys(): 86 | return cluster.api[site_hierarchy_name] 87 | else: 88 | return SiteHierarchy(cluster, timeout=60) 89 | 90 | 91 | @replicator.route('/select_sites', method='POST') 92 | def select_sites(): 93 | """ 94 | Loads the source and target site hierarchies in a web page that allows a user to select the sites from the source 95 | cluster that should be replicated to the target cluster. 96 | :return: Bottle template 97 | """ 98 | source, target, source_cluster, target_cluster = get_source_and_target(request) 99 | if source == target: 100 | raise Exception( 101 | '%s: select_sites: Source and target DNAC clusters cannot be the same' % MODULE 102 | ) 103 | source_hierarchy = get_site_hierarchy(source_cluster) 104 | source_hierarchy.load_sites() 105 | target_hierarchy = get_site_hierarchy(target_cluster) 106 | target_hierarchy.load_sites() 107 | return template('select_sites', source=source, target=target, 108 | source_hierarchy=source_hierarchy, target_hierarchy=target_hierarchy) 109 | 110 | 111 | @replicator.route('/replicate_sites', method='POST') 112 | def replicate_sites(): 113 | """ 114 | Iterates through the sites chosen for replication and copies them from the source to the target cluster. 115 | :return: Bottle template 116 | """ 117 | source, target, source_cluster, target_cluster = get_source_and_target(request) 118 | if source == target: 119 | raise Exception( 120 | '%s: replicate_sites: Source and target DNAC clusters cannot be the same' % MODULE 121 | ) 122 | results = [] 123 | for site in request.forms: 124 | results = copy_site(site, source, target, source_cluster, target_cluster, results) 125 | return template('replicate_sites', source=source, target=target, results=results) 126 | 127 | 128 | def copy_site(site, source, target, source_cluster, target_cluster, results): 129 | """ 130 | Takes the site information from the source cluster and copies it over to the target cluster. 131 | :param site: The site being copied. 132 | type: str 133 | required: yes 134 | default: none 135 | :param source: The source cluster's identifier, i.e. it's name or IP address. 136 | type: str 137 | required: yes 138 | default: none 139 | :param target: The target cluster's identifier, i.e. it's name or IP address. 140 | type: str 141 | required: yest 142 | default: none 143 | :param source_cluster: The source cluster's Dnac object. 144 | type: Dnac object 145 | required: yes 146 | default: none 147 | :param target_cluster: The target cluster's Dnac object. 148 | type: Dnac object 149 | required: yes 150 | default: none 151 | :param results: The list of site replication results. 152 | type: list 153 | required: yes 154 | default: none 155 | :return: The results list with the addition of the result from calling this function appended to it. 156 | """ 157 | if site in target_cluster.api.keys(): 158 | results.append('Site %s already exists in target cluster %s. Skipping...' % (site, target)) 159 | return results 160 | if site not in source_cluster.api.keys(): 161 | results.append('Site %s could not be found in source cluster %s. Skipping...' % (site, source)) 162 | return results 163 | source_site = source_cluster.api[site] 164 | # don't add the site if it's parent doesn't exist in the target 165 | if source_site.parent_name not in target_cluster.api.keys(): 166 | results.append('Parent site %s does not exist for site %s. Skipping...' % (source_site.parent_name, site)) 167 | return results 168 | # check the location type and add the site accordingly using the source_site's location values 169 | try: 170 | if source_site.location['type'] == AREA: 171 | target_cluster.api['STUB_SITE'].add_site( 172 | AREA, source_site.name, source_site.parent_name 173 | ) 174 | elif source_site.location['type'] == BUILDING: 175 | target_cluster.api['STUB_SITE'].add_site( 176 | BUILDING, source_site.name, source_site.parent_name, address=source_site.address, 177 | latitude=source_site.latitude, longitude=source_site.longitude 178 | ) 179 | elif source_site.location['type'] == FLOOR: 180 | target_cluster.api['STUB_SITE'].add_site( 181 | FLOOR, source_site.name, source_site.parent_name, rf_model=source_site.rf_model, 182 | width=source_site.width, length=source_site.length, height=source_site.height 183 | ) 184 | else: 185 | results.append( 186 | '%s: copy_site: Unidentifiable site type for site %s: %s. Skipping...' % 187 | (MODULE, site, source_site.location['type']) 188 | ) 189 | results.append('Successfully added site %s to target cluster %s' % (site, target)) 190 | except Exception as error: 191 | results.append('Failed to add site %s to target cluster %s: %s' % (site, target, error)) 192 | return results 193 | 194 | 195 | # Main Program ######################################################################################################## 196 | 197 | 198 | if __name__ == '__main__': 199 | """ 200 | usage: site_heirarchy_replicator 201 | """ 202 | 203 | # read the json encoded config file passed from the CLI 204 | clusters_file = open(sys.argv[1], mode='r') 205 | clusters_from_config_file = json.load(clusters_file) 206 | clusters_file.close() 207 | 208 | # create all the Dnac objects from the clusters that were listed in the config file 209 | clusters = [] 210 | for cluster in clusters_from_config_file: 211 | # create a Dnac object 212 | dnac = Dnac(version=cluster['version'], 213 | name=cluster['name'], 214 | ip=cluster['ip'], 215 | port=cluster['port'], 216 | user=cluster['user'], 217 | passwd=cluster['passwd'], 218 | content_type=cluster['content_type']) 219 | # create a stub site for adding new sites 220 | stub_site = Site(dnac, STUB_SITE) 221 | stub_site.timeout = 60 # my lab's DNAC server is responding slowly; others may not need this 222 | # add the new Dnac instance to the global clusters list 223 | clusters.append(dnac) 224 | 225 | run(replicator, host='localhost', port=8088, reloader=True, debug=True) -------------------------------------------------------------------------------- /examples/network_hierarchy_replicator/views/replicate_sites.tpl: -------------------------------------------------------------------------------- 1 | 2 |

Replicate Sites Results

3 |

4 | % for result in results: 5 | {{result}}
6 | % end 7 |

8 |

9 |
10 | 11 | 12 | 13 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /examples/network_hierarchy_replicator/views/select_clusters.tpl: -------------------------------------------------------------------------------- 1 | % EMPTY='' 2 | 3 |

Select Clusters

4 |

5 | 6 | 7 | 19 | 31 | 32 |
8 | Source Cluster
9 | 18 |
20 | Target Cluster
21 | 30 |
33 |

34 |

35 |
36 | 37 |
38 |
39 | -------------------------------------------------------------------------------- /examples/network_hierarchy_replicator/views/select_sites.tpl: -------------------------------------------------------------------------------- 1 | 2 |

Select Sites

3 |

4 |

5 | 6 | 7 | 11 | 16 | 17 | 24 | 29 | 30 |
8 |
Sites available for export on {{source}}
9 | 10 |
12 |
Existing sites on {{target}}
13 | 15 |
18 | % for site in source_hierarchy.site_nodes.keys(): 19 | 20 | {{site[0]}} 21 |
22 | % end 23 |
25 | % for site in target_hierarchy.site_nodes.keys(): 26 | {{site[0]}}
27 | % end 28 |
31 |

32 |

33 | 34 |
35 |
36 |
37 |
38 | 39 |
40 |
41 | -------------------------------------------------------------------------------- /examples/networkdevice_example.py: -------------------------------------------------------------------------------- 1 | 2 | from dnac import Dnac 3 | from dnac.networkdevice import NetworkDevice 4 | 5 | MODULE = 'networkdevice_example.py' 6 | 7 | print('%s: setting up Cisco DNA Center and its API...' % MODULE) 8 | dnac = Dnac() 9 | ndapi = NetworkDevice(dnac, 'devices') 10 | 11 | print('%s: getting all devices...' % MODULE) 12 | # The handle 'ndapi' could be used here, but the point of this example is 13 | # demonstrate how to get it directly from the Dnac.api{} using the API's name. 14 | devices = dnac.api['devices'].get_all_devices() 15 | 16 | print('%s: found the following devices:' % MODULE) 17 | for device in devices: 18 | print('hostname: %s\tserial: %s\tIP: %s' % 19 | (device['hostname'], device['serialNumber'], 20 | device['managementIpAddress'])) 21 | print() 22 | -------------------------------------------------------------------------------- /examples/site_hierarchy_replicator/dnac_clusters.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "denlab-en-dnac.cisco.com", 4 | "ip": "", 5 | "version": "1.3.1.4", 6 | "port": 443, 7 | "user": "admin", 8 | "passwd": "C!sco123", 9 | "content_type": "application/json" 10 | }, 11 | { 12 | "name": "", 13 | "ip": "173.36.255.155", 14 | "version": "1.3.1.4", 15 | "port": 443, 16 | "user": "admin", 17 | "passwd": "c!sco123", 18 | "content_type": "application/json" 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /examples/site_hierarchy_replicator/site_hierarchy_replicator.py: -------------------------------------------------------------------------------- 1 | from dnac import Dnac 2 | from dnac.site import Site, STUB_SITE, AREA, BUILDING, FLOOR 3 | from dnac.site_hierarchy import SiteHierarchy, SITE_HIERARCHY_NAME 4 | from bottle import Bottle, run, template, request 5 | import sys 6 | import json 7 | 8 | MODULE = 'site_hierarchy_replicator.py' 9 | 10 | clusters = [] 11 | replicator = Bottle() 12 | 13 | 14 | def get_cluster(cluster_id): 15 | """ 16 | Retrieves the cluster by a cluster identifier. 17 | :param cluster_id: Identifies a cluster by its name or IP address. 18 | type: str 19 | required: yes 20 | default: None 21 | :return: Dnac object 22 | """ 23 | for cluster in clusters: 24 | if cluster_id == cluster.name or cluster_id == cluster.ip: 25 | return cluster 26 | else: 27 | Exception('%s: get_cluster: Could not find cluster %s' % (MODULE, cluster_id)) 28 | 29 | 30 | def get_source_and_target(request): 31 | """ 32 | Retrieves the source and target cluster names passed from a Bottle template in the request forms. Also removes 33 | the source and target from the request's FormsDict so that further processing can be performed on the request's 34 | list of projects or templates. 35 | :param request: The request object returned from a Bottle template's posting. 36 | type: Bottle request object 37 | required: yes 38 | default: none 39 | :return: 40 | source: The source cluster's name 41 | type: str 42 | target: The target cluster's name 43 | type: str 44 | source_cluster: The source cluster's Dnac representation 45 | type: Dnac object 46 | target_cluster: The target cluster's Dnac representation 47 | type: Dnac object 48 | """ 49 | source = request.forms.get('source') 50 | source_cluster = get_cluster(source) 51 | request.forms.pop('source') 52 | target = request.forms.get('target') 53 | target_cluster = get_cluster(target) 54 | request.forms.pop('target') 55 | return source, target, source_cluster, target_cluster 56 | 57 | 58 | @replicator.route('/', method='GET') 59 | @replicator.route('/index', method='GET') 60 | @replicator.route('/select_clusters', method='POST') 61 | def select_clusters(): 62 | """ 63 | Passes the list of all clusters loaded during project initialization to the home page. 64 | :return: Bottle template 65 | """ 66 | return template('select_clusters', clusters=clusters, method='GET') 67 | 68 | 69 | def get_site_hierarchy(cluster): 70 | """ 71 | Retrieves the cluster's SiteHierarchy object. 72 | :param cluster: The Dnac cluster whose SiteHierarchy is required. 73 | type: Dnac object 74 | required: yes 75 | default: none 76 | :return: SiteHierarchy object 77 | """ 78 | if bool(cluster.name): 79 | cluster_id = cluster.name 80 | elif bool(cluster.ip): 81 | cluster_id = cluster.ip 82 | else: 83 | raise Exception('%s: get_site_hierarchy: Dnac cluster has no name nor IP address' % MODULE) 84 | site_hierarchy_name = '%s%s' % (cluster_id, SITE_HIERARCHY_NAME) 85 | if site_hierarchy_name in cluster.api.keys(): 86 | return cluster.api[site_hierarchy_name] 87 | else: 88 | return SiteHierarchy(cluster, timeout=60) 89 | 90 | 91 | @replicator.route('/select_sites', method='POST') 92 | def select_sites(): 93 | """ 94 | Loads the source and target site hierarchies in a web page that allows a user to select the sites from the source 95 | cluster that should be replicated to the target cluster. 96 | :return: Bottle template 97 | """ 98 | source, target, source_cluster, target_cluster = get_source_and_target(request) 99 | if source == target: 100 | raise Exception( 101 | '%s: select_sites: Source and target DNAC clusters cannot be the same' % MODULE 102 | ) 103 | source_hierarchy = get_site_hierarchy(source_cluster) 104 | source_hierarchy.load_sites() 105 | target_hierarchy = get_site_hierarchy(target_cluster) 106 | target_hierarchy.load_sites() 107 | return template('select_sites', source=source, target=target, 108 | source_hierarchy=source_hierarchy, target_hierarchy=target_hierarchy) 109 | 110 | 111 | @replicator.route('/replicate_sites', method='POST') 112 | def replicate_sites(): 113 | """ 114 | Iterates through the sites chosen for replication and copies them from the source to the target cluster. 115 | :return: Bottle template 116 | """ 117 | source, target, source_cluster, target_cluster = get_source_and_target(request) 118 | if source == target: 119 | raise Exception( 120 | '%s: replicate_sites: Source and target DNAC clusters cannot be the same' % MODULE 121 | ) 122 | results = [] 123 | for site in request.forms: 124 | results = copy_site(site, source, target, source_cluster, target_cluster, results) 125 | return template('replicate_sites', source=source, target=target, results=results) 126 | 127 | 128 | def copy_site(site, source, target, source_cluster, target_cluster, results): 129 | """ 130 | Takes the site information from the source cluster and copies it over to the target cluster. 131 | :param site: The site being copied. 132 | type: str 133 | required: yes 134 | default: none 135 | :param source: The source cluster's identifier, i.e. it's name or IP address. 136 | type: str 137 | required: yes 138 | default: none 139 | :param target: The target cluster's identifier, i.e. it's name or IP address. 140 | type: str 141 | required: yest 142 | default: none 143 | :param source_cluster: The source cluster's Dnac object. 144 | type: Dnac object 145 | required: yes 146 | default: none 147 | :param target_cluster: The target cluster's Dnac object. 148 | type: Dnac object 149 | required: yes 150 | default: none 151 | :param results: The list of site replication results. 152 | type: list 153 | required: yes 154 | default: none 155 | :return: The results list with the addition of the result from calling this function appended to it. 156 | """ 157 | if site not in source_cluster.api.keys(): 158 | results.append( 159 | 'Site %s could not be found in source cluster %s. Skipping...' % (site, source) 160 | ) 161 | source_site = source_cluster.api[site] 162 | # don't add the site if it's parent doesn't exist in the target 163 | if source_site.parent_name not in target_cluster.api.keys(): 164 | results.append('Parent site %s does not exist for site %s. Skipping...' % (source_site.parent_name, site)) 165 | return results 166 | # check the location type and add the site accordingly using the source_site's location values 167 | try: 168 | if source_site.location['type'] == AREA: 169 | target_cluster.api['STUB_SITE'].add_site( 170 | AREA, source_site.name, source_site.parent_name 171 | ) 172 | elif source_site.location['type'] == BUILDING: 173 | target_cluster.api['STUB_SITE'].add_site( 174 | BUILDING, source_site.name, source_site.parent_name, address=source_site.address, 175 | latitude=source_site.latitude, longitude=source_site.longitude 176 | ) 177 | elif source_site.location['type'] == FLOOR: 178 | target_cluster.api['STUB_SITE'].add_site( 179 | FLOOR, source_site.name, source_site.parent_name, rf_model=source_site.rf_model, 180 | width=source_site.width, length=source_site.length, height=source_site.height 181 | ) 182 | else: 183 | results.append( 184 | '%s: copy_site: Unidentifiable site type for site %s: %s. Skipping...' % 185 | (MODULE, site, source_site.location['type']) 186 | ) 187 | results.append('Successfully added site %s to target cluster %s' % (site, target)) 188 | # have the target_cluster refresh its SiteHierarchy 189 | except Exception as error: 190 | results.append('Failed to add site %s to target cluster %s: %s' % (site, target, error)) 191 | return results 192 | 193 | 194 | # Main Program ######################################################################################################## 195 | 196 | 197 | if __name__ == '__main__': 198 | """ 199 | usage: site_heirarchy_replicator 200 | """ 201 | 202 | # read the json encoded config file passed from the CLI 203 | clusters_file = open(sys.argv[1], mode='r') 204 | clusters_from_config_file = json.load(clusters_file) 205 | clusters_file.close() 206 | 207 | # create all the Dnac objects from the clusters that were listed in the config file 208 | clusters = [] 209 | for cluster in clusters_from_config_file: 210 | # create a Dnac object 211 | dnac = Dnac(version=cluster['version'], 212 | name=cluster['name'], 213 | ip=cluster['ip'], 214 | port=cluster['port'], 215 | user=cluster['user'], 216 | passwd=cluster['passwd'], 217 | content_type=cluster['content_type']) 218 | # create a stub site for adding new sites 219 | stub_site = Site(dnac, STUB_SITE) 220 | stub_site.timeout = 60 # my lab's DNAC server is responding slowly; others may not need this 221 | # add the new Dnac instance to the global clusters list 222 | clusters.append(dnac) 223 | 224 | run(replicator, host='localhost', port=8088, reloader=True, debug=True) -------------------------------------------------------------------------------- /examples/site_hierarchy_replicator/views/replicate_sites.tpl: -------------------------------------------------------------------------------- 1 | 2 |

Replicate Sites Results

3 |

4 | % for result in results: 5 | {{result}}
6 | % end 7 |

8 |

9 |
10 | 11 | 12 | 13 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /examples/site_hierarchy_replicator/views/select_clusters.tpl: -------------------------------------------------------------------------------- 1 | % EMPTY='' 2 | 3 |

Select Clusters

4 |

5 | 6 | 7 | 19 | 31 | 32 |
8 | Source Cluster
9 | 18 |
20 | Target Cluster
21 | 30 |
33 |

34 |

35 |
36 | 37 |
38 |
39 | -------------------------------------------------------------------------------- /examples/site_hierarchy_replicator/views/select_sites.tpl: -------------------------------------------------------------------------------- 1 | 2 |

Select Sites

3 |

4 |

5 | 6 | 7 | 11 | 16 | 17 | 24 | 29 | 30 |
8 |
Sites available for export on {{source}}
9 | 10 |
12 |
Existing sites on {{target}}
13 | 15 |
18 | % for site in source_hierarchy.site_nodes.keys(): 19 | 20 | {{site[0]}} 21 |
22 | % end 23 |
25 | % for site in target_hierarchy.site_nodes.keys(): 26 | {{site[0]}}
27 | % end 28 |
31 |

32 |

33 | 34 |
35 |
36 |
37 |
38 | 39 |
40 |
41 | -------------------------------------------------------------------------------- /examples/template_example.py: -------------------------------------------------------------------------------- 1 | 2 | from dnac import Dnac 3 | from dnac.template import Template, TARGET_BY_ID 4 | 5 | MODULE = 'template_example.py' 6 | 7 | print('%s: preparing to deploy a template...' % MODULE) 8 | 9 | dnac = Dnac() 10 | 11 | template = Template(dnac, 'Set VLAN') 12 | 13 | print('%s: setting the target device...' % MODULE) 14 | 15 | dnac.api['Set VLAN'].targetId = '84e4b133-2668-4705-8163-5694c84e78fb' 16 | dnac.api['Set VLAN'].targetType = TARGET_BY_ID 17 | 18 | print('%s: setting the template\'s parameters...' % MODULE) 19 | 20 | dnac.api['Set VLAN'].set_param('interface', 'g1/0/8') 21 | dnac.api['Set VLAN'].set_param('description', 'Provisioned by %s' % MODULE) 22 | dnac.api['Set VLAN'].set_param('vlan', 10) 23 | 24 | print('%s: deploying the template...' % MODULE) 25 | 26 | dnac.api['Set VLAN'].deploy_sync() 27 | 28 | print('%s: deploy results: %s' % (MODULE, dnac.api['Set VLAN'].deployment.results)) 29 | 30 | print() 31 | -------------------------------------------------------------------------------- /examples/template_replicator/dnac_clusters.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "denlab-en-dnac.cisco.com", 4 | "ip": "", 5 | "version": "1.3.1.4", 6 | "port": 443, 7 | "user": "admin", 8 | "passwd": "C!sco123", 9 | "content_type": "application/json" 10 | }, 11 | { 12 | "name": "", 13 | "ip": "173.36.255.155", 14 | "version": "1.3.1.4", 15 | "port": 443, 16 | "user": "admin", 17 | "passwd": "c!sco123", 18 | "content_type": "application/json" 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /examples/template_replicator/views/replicate_projects.tpl: -------------------------------------------------------------------------------- 1 | 2 |

Replicate Projects Results

3 |

4 | % for result in results: 5 | {{result}}
6 | % end 7 |

8 |

9 |
10 | 11 | 12 | 13 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /examples/template_replicator/views/replicate_projects_and_templates.tpl: -------------------------------------------------------------------------------- 1 | 2 |

Replicate Projects and Templates Results

3 |

4 | % for result in results: 5 | {{result}}
6 | % end 7 |

8 |

9 |
10 | 11 | 12 | 13 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /examples/template_replicator/views/replicate_templates.tpl: -------------------------------------------------------------------------------- 1 | 2 |

Replicate Templates Results

3 |

4 | % for result in results: 5 | {{result}}
6 | % end 7 |

8 |

9 |
10 | 11 | 12 | 13 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /examples/template_replicator/views/select_clusters.tpl: -------------------------------------------------------------------------------- 1 | % EMPTY='' 2 | 3 |

Select Clusters

4 |

5 | 6 | 7 | 19 | 31 | 32 |
8 | Source Cluster
9 | 18 |
20 | Target Cluster
21 | 30 |
33 |

34 |

35 |
36 | 37 |
38 |
39 | -------------------------------------------------------------------------------- /examples/template_replicator/views/select_projects.tpl: -------------------------------------------------------------------------------- 1 | 2 |

Select Projects

3 |

4 |

5 | 6 | 7 | 11 | 16 | 17 | 24 | 29 | 30 |
8 |
Projects available for export on {{source}}
9 | 10 |
12 |
Existing projects on {{target}}
13 | 15 |
18 | % for project in source_projects.keys(): 19 | 20 | {{project}} 21 |
22 | % end 23 |
25 | % for project in target_projects.keys(): 26 | {{project}}
27 | % end 28 |
31 |

32 |

33 | 34 | 35 | 36 |
37 |
38 |
39 |
40 | 41 |
42 |
43 | -------------------------------------------------------------------------------- /examples/template_replicator/views/select_templates.tpl: -------------------------------------------------------------------------------- 1 | % NO_TEMPLATES = [] 2 | % NO_PROJECTS = {} 3 | 4 |

Select Templates

5 |

6 | % for missing in missing_target_projects: 7 | {{missing}}
8 | % end 9 |

10 | % if projects != NO_PROJECTS: 11 |

12 | 13 | % for project in projects: 14 | 15 | 18 | 19 | 20 | 24 | 29 | 30 | 39 | 46 | 47 | % end 48 |
16 |
{{project}}
17 |
21 |
Templates available for export on {{source}}
22 | 23 |
25 |
Existing templates on {{target}}
26 | 28 |
31 | % for template in source_templates[project]: 32 | % if template != NO_TEMPLATES: 33 | 34 | {{template}} 35 |
36 | % end 37 | % end 38 |
40 | % for template in target_templates[project]: 41 | % if template != NO_TEMPLATES: 42 | {{template}}
43 | % end 44 | % end 45 |
49 |

50 |

51 | 52 |
53 |
54 | % end 55 |
56 |
57 | 58 | 59 | 60 |
61 |
62 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | import setuptools 3 | 4 | with open('README.md', 'r') as file: 5 | long_description = file.read() 6 | 7 | setuptools.setup( 8 | name='dnac', 9 | version='1.3.1.6', 10 | author='Robert Sayle', 11 | author_email='rsayle@cisco.com', 12 | description='A wrapper for using Cisco DNA Center\'s REST API', 13 | long_description=long_description, 14 | long_description_content_type='text/markdown', 15 | url='https://developer.cisco.com/codeexchange/github/repo/rsayle/DNAC-Python-Wrapper', 16 | packages=setuptools.find_packages(exclude=['docs', 'examples']), 17 | python_requires='>=3', 18 | include_package_data=True, 19 | classifiers=[ 20 | 'Development Status :: 3 - Alpha', 21 | 'Programming Language :: Python :: 3', 22 | 'License :: Other/Proprietary License', 23 | 'Operating System :: OS Independent', 24 | 'Topic :: System :: Networking', 25 | 'Topic :: Utilities', 26 | 'Intended Audience :: Information Technology', 27 | 'Intended Audience :: Telecommunications Industry' 28 | ], 29 | install_requires=['requests', 'multi_key_dict'] 30 | ) 31 | --------------------------------------------------------------------------------