├── .gitignore ├── LICENSE ├── Postman ├── Cisco-Reservable-SD-WAN-Env.postman_environment.json └── Cisco-Reservable-SD-WAN.postman_collection.json ├── README.md ├── Site-3-vEdge-Variables.yaml ├── get_request.py ├── login.py ├── post_request.py ├── requirements.txt ├── rest_api_lib.py └── sdwan.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Postman/Cisco-Reservable-SD-WAN-Env.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1bd29ba3-2f5b-45fc-8ae7-b5d352e44ccb", 3 | "name": "Cisco-Reservable-SD-WAN-Environment", 4 | "values": [ 5 | { 6 | "key": "vmanage", 7 | "value": "10.10.20.90", 8 | "enabled": true 9 | }, 10 | { 11 | "key": "j_username", 12 | "value": "admin", 13 | "enabled": true 14 | }, 15 | { 16 | "key": "j_password", 17 | "value": "C1sco12345", 18 | "enabled": true 19 | }, 20 | { 21 | "key": "port", 22 | "value": "8443", 23 | "enabled": true 24 | }, 25 | { 26 | "key": "X-XSRF-TOKEN", 27 | "value": "", 28 | "enabled": true 29 | } 30 | ], 31 | "_postman_variable_scope": "environment", 32 | "_postman_exported_at": "2020-05-07T01:44:29.596Z", 33 | "_postman_exported_using": "Postman/7.23.0" 34 | } -------------------------------------------------------------------------------- /Postman/Cisco-Reservable-SD-WAN.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "6b9300d8-c5fb-48d7-85fd-57d4353cd903", 4 | "name": "Cisco-Reservable-SD-WAN", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "1.Authentication", 10 | "item": [ 11 | { 12 | "name": "Authentication", 13 | "request": { 14 | "method": "POST", 15 | "header": [ 16 | { 17 | "key": "Content-Type", 18 | "value": "application/x-www-form-urlencoded" 19 | } 20 | ], 21 | "body": { 22 | "mode": "urlencoded", 23 | "urlencoded": [ 24 | { 25 | "key": "j_username", 26 | "value": "{{j_username}}", 27 | "type": "text" 28 | }, 29 | { 30 | "key": "j_password", 31 | "value": "{{j_password}}", 32 | "type": "text" 33 | } 34 | ] 35 | }, 36 | "url": { 37 | "raw": "https://{{vmanage}}:{{port}}/j_security_check", 38 | "protocol": "https", 39 | "host": [ 40 | "{{vmanage}}" 41 | ], 42 | "port": "{{port}}", 43 | "path": [ 44 | "j_security_check" 45 | ] 46 | } 47 | }, 48 | "response": [] 49 | }, 50 | { 51 | "name": "Token", 52 | "event": [ 53 | { 54 | "listen": "test", 55 | "script": { 56 | "id": "92af74e2-ccda-4a82-912a-7554abf16ea9", 57 | "exec": [ 58 | "var XSRFToken = pm.response.text()", 59 | "pm.environment.set(\"X-XSRF-TOKEN\", XSRFToken);", 60 | "" 61 | ], 62 | "type": "text/javascript" 63 | } 64 | } 65 | ], 66 | "request": { 67 | "method": "GET", 68 | "header": [], 69 | "url": { 70 | "raw": "https://{{vmanage}}:{{port}}/dataservice/client/token", 71 | "protocol": "https", 72 | "host": [ 73 | "{{vmanage}}" 74 | ], 75 | "port": "{{port}}", 76 | "path": [ 77 | "dataservice", 78 | "client", 79 | "token" 80 | ] 81 | } 82 | }, 83 | "response": [] 84 | } 85 | ], 86 | "protocolProfileBehavior": {} 87 | }, 88 | { 89 | "name": "2. SD-WAN Fabric Devices", 90 | "item": [ 91 | { 92 | "name": "Fabric Devices", 93 | "request": { 94 | "method": "GET", 95 | "header": [ 96 | { 97 | "key": "X-XSRF-TOKEN", 98 | "value": "{{X-XSRF-TOKEN}}", 99 | "type": "text" 100 | } 101 | ], 102 | "url": { 103 | "raw": "https://{{vmanage}}:{{port}}/dataservice/device", 104 | "protocol": "https", 105 | "host": [ 106 | "{{vmanage}}" 107 | ], 108 | "port": "{{port}}", 109 | "path": [ 110 | "dataservice", 111 | "device" 112 | ] 113 | } 114 | }, 115 | "response": [] 116 | }, 117 | { 118 | "name": "Devices Status", 119 | "request": { 120 | "method": "GET", 121 | "header": [], 122 | "url": { 123 | "raw": "https://{{vmanage}}:{{port}}/dataservice/device/monitor", 124 | "protocol": "https", 125 | "host": [ 126 | "{{vmanage}}" 127 | ], 128 | "port": "{{port}}", 129 | "path": [ 130 | "dataservice", 131 | "device", 132 | "monitor" 133 | ] 134 | } 135 | }, 136 | "response": [] 137 | }, 138 | { 139 | "name": "Device Counters", 140 | "request": { 141 | "method": "GET", 142 | "header": [ 143 | { 144 | "key": "X-XSRF-TOKEN", 145 | "value": "{{X-XSRF-TOKEN}}", 146 | "type": "text" 147 | } 148 | ], 149 | "url": { 150 | "raw": "https://{{vmanage}}:{{port}}/dataservice/device/counters", 151 | "protocol": "https", 152 | "host": [ 153 | "{{vmanage}}" 154 | ], 155 | "port": "{{port}}", 156 | "path": [ 157 | "dataservice", 158 | "device", 159 | "counters" 160 | ] 161 | } 162 | }, 163 | "response": [] 164 | }, 165 | { 166 | "name": "Interface statistics", 167 | "request": { 168 | "method": "GET", 169 | "header": [ 170 | { 171 | "key": "X-XSRF-TOKEN", 172 | "value": "{{X-XSRF-TOKEN}}", 173 | "type": "text" 174 | } 175 | ], 176 | "url": { 177 | "raw": "https://{{vmanage}}:{{port}}/dataservice/statistics/interface", 178 | "protocol": "https", 179 | "host": [ 180 | "{{vmanage}}" 181 | ], 182 | "port": "{{port}}", 183 | "path": [ 184 | "dataservice", 185 | "statistics", 186 | "interface" 187 | ] 188 | } 189 | }, 190 | "response": [] 191 | } 192 | ], 193 | "protocolProfileBehavior": {} 194 | }, 195 | { 196 | "name": "3. SD-WAN Device Template", 197 | "item": [ 198 | { 199 | "name": "Template Feature", 200 | "request": { 201 | "method": "GET", 202 | "header": [ 203 | { 204 | "key": "X-XSRF-TOKEN", 205 | "value": "{{X-XSRF-TOKEN}}", 206 | "type": "text" 207 | } 208 | ], 209 | "url": { 210 | "raw": "https://{{vmanage}}:{{port}}/dataservice/template/feature", 211 | "protocol": "https", 212 | "host": [ 213 | "{{vmanage}}" 214 | ], 215 | "port": "{{port}}", 216 | "path": [ 217 | "dataservice", 218 | "template", 219 | "feature" 220 | ] 221 | } 222 | }, 223 | "response": [] 224 | }, 225 | { 226 | "name": "Template Feature Type", 227 | "request": { 228 | "method": "GET", 229 | "header": [ 230 | { 231 | "key": "X-XSRF-TOKEN", 232 | "value": "{{X-XSRF-TOKEN}}", 233 | "type": "text" 234 | } 235 | ], 236 | "url": { 237 | "raw": "https://{{vmanage}}:{{port}}/dataservice/template/feature/types", 238 | "protocol": "https", 239 | "host": [ 240 | "{{vmanage}}" 241 | ], 242 | "port": "{{port}}", 243 | "path": [ 244 | "dataservice", 245 | "template", 246 | "feature", 247 | "types" 248 | ] 249 | } 250 | }, 251 | "response": [] 252 | } 253 | ], 254 | "protocolProfileBehavior": {} 255 | }, 256 | { 257 | "name": "4. SD-WAN Device Policy", 258 | "item": [ 259 | { 260 | "name": "vEdge Template Policy", 261 | "request": { 262 | "method": "GET", 263 | "header": [], 264 | "url": { 265 | "raw": "https://{{vmanage}}:{{port}}/dataservice/template/policy/vedge/devices", 266 | "protocol": "https", 267 | "host": [ 268 | "{{vmanage}}" 269 | ], 270 | "port": "{{port}}", 271 | "path": [ 272 | "dataservice", 273 | "template", 274 | "policy", 275 | "vedge", 276 | "devices" 277 | ] 278 | } 279 | }, 280 | "response": [] 281 | }, 282 | { 283 | "name": "Policy List", 284 | "request": { 285 | "method": "GET", 286 | "header": [ 287 | { 288 | "key": "X-XSRF-TOKEN", 289 | "value": "{{X-XSRF-TOKEN}}", 290 | "type": "text" 291 | } 292 | ], 293 | "url": { 294 | "raw": "https://{{vmanage}}:{{port}}/dataservice/template/policy/list", 295 | "protocol": "https", 296 | "host": [ 297 | "{{vmanage}}" 298 | ], 299 | "port": "{{port}}", 300 | "path": [ 301 | "dataservice", 302 | "template", 303 | "policy", 304 | "list" 305 | ] 306 | } 307 | }, 308 | "response": [] 309 | } 310 | ], 311 | "protocolProfileBehavior": {} 312 | } 313 | ], 314 | "protocolProfileBehavior": {} 315 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting started with Cisco SD-WAN REST APIs 2 | 3 | The Cisco SD-WAN Solution (a.k.a Viptela) is a cloud-delivered overlay WAN architecture that facilitates digital and cloud transformation for enterprises. It significantly reduces WAN costs and time to deploy new services. 4 | 5 | Cisco SD-WAN builds a robust security architecture that's crucial for hybrid networks. It provides a strong policy framework. 6 | 7 | The solution has been deployed in every major industry. It solves many critical enterprise problems, including: 8 | 9 | * Establishing transport-independent WAN for lower cost and higher diversity 10 | * Meeting SLA for business-critical and real-time applications 11 | * Providing end-to-end segmentation for protecting critical enterprise compute resources 12 | * Extending seamlessly into the public cloud 13 | * Providing optimal user experience for SaaS applications 14 | 15 | ### Project SD-WAN 16 | 17 | The goals of this application are two fold. First is to show how simple it is to 18 | develop applications that extend the Cisco SD-WAN fabric by using the REST API 19 | it provides. Second is to help IT operations teams that are managing Cisco SD-WAN fabrics to make the transition to an automated and programmable infrastructure. 20 | 21 | The main application is a CLI tool through which users can see a list of the devices that are part of the fabric, the configuration templates, which devices are associated to which templates, and options to attach and detach configuration templates to specific devices. 22 | 23 | Example usage: 24 | 25 | `./sdwan.py attach --template db4c997a-7212-4ec1-906e-ed2b86c3f42f --variables Site-3-vEdge-Variables.yaml` 26 | 27 | ### Requirements 28 | 29 | To use this application you will need: 30 | 31 | * Python 3.6+ 32 | * Cisco SD-WAN 18+ 33 | * A Cisco SD-WAN account with permissions to attach and detach templates 34 | 35 | ### Install and Setup 36 | 37 | Clone the code to your local machine. 38 | 39 | ``` 40 | git clone https://github.com/ai-devnet/Getting-started-with-Cisco-SD-WAN-REST-APIs.git 41 | cd Getting-started-with-Cisco-SD-WAN-REST-APIs 42 | ``` 43 | 44 | Setup Python Virtual Environment (requires Python 3.6+) 45 | 46 | ``` 47 | python3.6 -m venv venv 48 | source venv/bin/activate 49 | pip install -r requirements.txt 50 | ``` 51 | 52 | Setup local environment variables for your Cisco SD-WAN fabric. Provide the info for your Cisco SD-WAN environment. 53 | 54 | Examples: 55 | 56 | ``` 57 | export vManage_IP=10.10.20.90 58 | export vManage_PORT=8443 59 | export vManage_USERNAME=admin 60 | export vManage_PASSWORD=C1sco12345 61 | ``` 62 | 63 | ### Using the application 64 | 65 | Once installed and setup, you can now get started. 66 | 67 | Investigate the built-in help with the tool. 68 | 69 | `./sdwan.py --help` 70 | 71 | OUTPUT 72 | 73 | ``` 74 | Usage: sdwan.py [OPTIONS] COMMAND [ARGS]... 75 | 76 | Command line tool for deploying templates to CISCO SDWAN. 77 | 78 | Options: 79 | --help Show this message and exit. 80 | 81 | Commands: 82 | attach Attach a template with Cisco SDWAN. 83 | attached_devices Retrieve and return devices associated to a... 84 | detach Detach a template with Cisco SDWAN. 85 | device_list Retrieve and return network devices list. 86 | template_list Retrieve and return templates list. 87 | ``` 88 | 89 | Look at the available templates. Each template will provide the number of devices already attached and the template ID. 90 | 91 | `./sdwan.py template_list` 92 | 93 | OUTPUT 94 | 95 | ``` 96 | Retrieving the templates available. 97 | ╒═════════════════════════════╤═════════════════╤══════════════════════════════════════╤════════════════════╤════════════════════╕ 98 | │ Template Name │ Device Type │ Template ID │ Attached devices │ Template version │ 99 | ╞═════════════════════════════╪═════════════════╪══════════════════════════════════════╪════════════════════╪════════════════════╡ 100 | │ vSmart_Template │ vsmart │ 90f26d2d-8136-4414-84de-4e8df52374e6 │ 1 │ 9 │ 101 | ├─────────────────────────────┼─────────────────┼──────────────────────────────────────┼────────────────────┼────────────────────┤ 102 | │ Site_1_and_2_cEdge_Template │ vedge-CSR-1000v │ c566d38e-2219-4764-a714-4abeeab607dc │ 2 │ 13 │ 103 | ├─────────────────────────────┼─────────────────┼──────────────────────────────────────┼────────────────────┼────────────────────┤ 104 | │ Site_3_vEdge_Template │ vedge-cloud │ db4c997a-7212-4ec1-906e-ed2b86c3f42f │ 1 │ 13 │ 105 | ├─────────────────────────────┼─────────────────┼──────────────────────────────────────┼────────────────────┼────────────────────┤ 106 | │ DC_cEdge_Template │ vedge-CSR-1000v │ 24d4be69-8038-48a3-b546-c6df199b6e29 │ 1 │ 14 │ 107 | ╘═════════════════════════════╧═════════════════╧══════════════════════════════════════╧════════════════════╧════════════════════╛ 108 | ``` 109 | 110 | Retrieve the list of devices that make up the SD-WAN fabric with ./sdwan.py device_list. 111 | 112 | `$ ./sdwan.py device_list` 113 | 114 | OUTPUT 115 | 116 | ``` 117 | Retrieving the devices. 118 | ╒═══════════════╤═══════════════╤══════════════════════════════════════════╤═════════════╤═══════════╤════════════════╤═════════════════╕ 119 | │ Host-Name │ Device Type │ Device ID │ System IP │ Site ID │ Version │ Device Model │ 120 | ╞═══════════════╪═══════════════╪══════════════════════════════════════════╪═════════════╪═══════════╪════════════════╪═════════════════╡ 121 | │ vmanage │ vmanage │ 81ac6722-a226-4411-9d5d-45c0ca7d567b │ 10.10.1.1 │ 101 │ 19.2.2 │ vmanage │ 122 | ├───────────────┼───────────────┼──────────────────────────────────────────┼─────────────┼───────────┼────────────────┼─────────────────┤ 123 | │ vsmart │ vsmart │ f7b49da3-383e-4cd5-abc1-c8e97d345a9f │ 10.10.1.5 │ 101 │ 19.2.2 │ vsmart │ 124 | ├───────────────┼───────────────┼──────────────────────────────────────────┼─────────────┼───────────┼────────────────┼─────────────────┤ 125 | │ vbond │ vbond │ ed0863cb-83e7-496c-b118-068e2371b13c │ 10.10.1.3 │ 101 │ 19.2.2 │ vedge-cloud │ 126 | ├───────────────┼───────────────┼──────────────────────────────────────────┼─────────────┼───────────┼────────────────┼─────────────────┤ 127 | │ dc-cedge01 │ vedge │ CSR-61CD2335-4775-650F-8538-4EC7BDFFD04C │ 10.10.1.11 │ 100 │ 16.12.3.0.3752 │ vedge-CSR-1000v │ 128 | ├───────────────┼───────────────┼──────────────────────────────────────────┼─────────────┼───────────┼────────────────┼─────────────────┤ 129 | │ site1-cedge01 │ vedge │ CSR-807E37A3-537A-07BA-BD71-8FB76DE9DC38 │ 10.10.1.13 │ 1001 │ 16.12.3.0.3752 │ vedge-CSR-1000v │ 130 | ├───────────────┼───────────────┼──────────────────────────────────────────┼─────────────┼───────────┼────────────────┼─────────────────┤ 131 | │ site2-cedge01 │ vedge │ CSR-DE6DAB19-BA1A-E543-959C-FD117F4A6205 │ 10.10.1.15 │ 1002 │ 16.12.3.0.3752 │ vedge-CSR-1000v │ 132 | ├───────────────┼───────────────┼──────────────────────────────────────────┼─────────────┼───────────┼────────────────┼─────────────────┤ 133 | │ site3-vedge01 │ vedge │ 0140a336-5fd5-9829-10d2-f6ba0b177efd │ 10.10.1.17 │ 1003 │ 19.2.2 │ vedge-cloud │ 134 | ╘═══════════════╧═══════════════╧══════════════════════════════════════════╧═════════════╧═══════════╧════════════════╧═════════════════╛ 135 | ``` 136 | 137 | Attaching a template is as easy as calling the *attach* option of the application and passing in the requested parameters. 138 | 139 | `./sdwan.py attach --template db4c997a-7212-4ec1-906e-ed2b86c3f42f --variables Site-3-vEdge-Variables.yaml` 140 | 141 | OUTPUT 142 | 143 | ``` 144 | Attempting to attach template. 145 | Attached Site 3 vEdge Template 146 | ``` 147 | 148 | To detach a template from a specific device you need to call the detach option of the application and pass in the parameters for the target device ID and the system-ip of that device: 149 | 150 | `./sdwan.py detach --target 0140a336-5fd5-9829-10d2-f6ba0b177efd --sysip 10.10.1.17` 151 | 152 | OUTPUT 153 | 154 | ``` 155 | Attempting to detach template. 156 | Changed configuration mode to CLI 157 | ``` 158 | 159 | ### POSTMAN 160 | 161 | I've also included a POSTMAN environment and collection in the `postman` folder. 162 | -------------------------------------------------------------------------------- /Site-3-vEdge-Variables.yaml: -------------------------------------------------------------------------------- 1 | system_ip: "10.10.1.17" 2 | host_name: "site3-vedge01" 3 | device_id: "0140a336-5fd5-9829-10d2-f6ba0b177efd" 4 | site_id: "1003" 5 | vpn_1_if_name: "ge0/1" 6 | vpn_1_if_ipv4_address: "10.10.24.1/24" 7 | vpn_512_next_hop_ip_address: "10.10.20.254" 8 | vpn_512_if_name: "eth0" 9 | vpn_512_if_ipv4_address: "10.10.20.178/24" 10 | mpls_next_hop: "10.10.23.17" 11 | public_internet_next_hop: "10.10.23.49" 12 | vpn_public_internet_interface: "ge0/2" 13 | vpn_public_interface_if_ipv4_address: "10.10.23.50/30" 14 | vpn_mpls_interface: "ge0/0" 15 | vpn_mpls_if_ipv4_address: "10.10.23.18/30" 16 | latitude: "53.408" 17 | longitude: "-2.228" -------------------------------------------------------------------------------- /get_request.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | def get_request(vmanage_ip, mount_point): 4 | """GET request""" 5 | url = "https://%s:8443/dataservice/%s"%(vmanage_ip, mount_point) 6 | 7 | response = requests.request("GET", url, verify=False) 8 | data = response.content 9 | return data -------------------------------------------------------------------------------- /login.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import sys 3 | 4 | def login(vmanage_ip, username, password): 5 | """Login to vmanage""" 6 | base_url_str = 'https://%s:8443/'%vmanage_ip 7 | 8 | login_action = '/j_security_check' 9 | 10 | #Format data for loginForm 11 | login_data = {'j_username' : username, 'j_password' : password} 12 | 13 | #Url for posting login data 14 | login_url = base_url_str + login_action 15 | 16 | sess = requests.session() 17 | #If the vmanage has a certificate signed by a trusted authority change verify to True 18 | login_response = sess.post(url=login_url, data=login_data, verify=False) 19 | 20 | if b'' in login_response.content: 21 | print ("Login Failed") 22 | sys.exit(0) -------------------------------------------------------------------------------- /post_request.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | def post_request(vmanage_ip, mount_point, payload, headers={'Content-Type': 'application/json'}): 4 | """POST request""" 5 | url = "https://%s:8443/dataservice/%s"%(vmanage_ip, mount_point) 6 | payload = json.dumps(payload) 7 | 8 | response = requests.request("POST", url, data=payload, headers=headers, verify=False) 9 | data = response.json() 10 | return data -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asn1crypto==0.24.0 2 | astroid==1.6.2 3 | certifi==2018.4.16 4 | cffi==1.15.0 5 | chardet==3.0.4 6 | ciscosparkapi==0.9.2 7 | click==6.7 8 | future==0.16.0 9 | idna==2.6 10 | isort==4.3.4 11 | #jwt==0.5.3 12 | lazy-object-proxy==1.3.1 13 | mccabe==0.6.1 14 | pexpect==4.5.0 15 | ptyprocess==0.5.2 16 | pycparser==2.18 17 | pylint==1.8.3 18 | pypandoc==1.4 19 | pyyaml==5.3.1 20 | requests==2.20.0 21 | requests-toolbelt==0.8.0 22 | six==1.11.0 23 | tabulate==0.8.2 24 | urllib3==1.24.2 25 | wrapt==1.10.11 26 | -------------------------------------------------------------------------------- /rest_api_lib.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import sys 4 | 5 | class rest_api_lib: 6 | def __init__(self, vmanage_ip, username, password): 7 | self.vmanage_ip = vmanage_ip 8 | self.session = {} 9 | self.login(self.vmanage_ip, username, password) 10 | 11 | def login(self, vmanage_ip, username, password): 12 | """Login to vmanage""" 13 | base_url_str = 'https://%s:8443/'%vmanage_ip 14 | 15 | login_action = '/j_security_check' 16 | 17 | #Format data for loginForm 18 | login_data = {'j_username' : username, 'j_password' : password} 19 | 20 | #Url for posting login data 21 | login_url = base_url_str + login_action 22 | url = base_url_str + login_url 23 | 24 | sess = requests.session() 25 | #If the vmanage has a certificate signed by a trusted authority change verify to True 26 | login_response = sess.post(url=login_url, data=login_data, verify=False) 27 | 28 | 29 | if b'' in login_response.content: 30 | print ("Login Failed") 31 | sys.exit(0) 32 | 33 | self.session[vmanage_ip] = sess 34 | 35 | def get_request(self, mount_point): 36 | """GET request""" 37 | url = "https://%s:8443/dataservice/%s"%(self.vmanage_ip, mount_point) 38 | #print url 39 | response = self.session[self.vmanage_ip].get(url, verify=False) 40 | data = response.content 41 | return data 42 | 43 | def post_request(self, mount_point, payload, headers={'Content-Type': 'application/json'}): 44 | """POST request""" 45 | url = "https://%s:8443/dataservice/%s"%(self.vmanage_ip, mount_point) 46 | payload = json.dumps(payload) 47 | print (payload) 48 | 49 | response = self.session[self.vmanage_ip].post(url=url, data=payload, headers=headers, verify=False) 50 | data = response.json() 51 | return data 52 | -------------------------------------------------------------------------------- /sdwan.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | """ 3 | Class with REST Api GET and POST libraries 4 | 5 | Example: python rest_api_lib.py vmanage_hostname username password 6 | 7 | PARAMETERS: 8 | vmanage_hostname : Ip address of the vmanage or the dns name of the vmanage 9 | username : Username to login the vmanage 10 | password : Password to login the vmanage 11 | 12 | Note: All the three arguments are manadatory 13 | """ 14 | import requests 15 | import sys 16 | import json 17 | import click 18 | import os 19 | import tabulate 20 | import yaml 21 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 22 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 23 | 24 | vmanage_host = os.environ.get("vManage_IP") 25 | vmanage_port = os.environ.get("vManage_PORT") 26 | vmanage_username = os.environ.get("vManage_USERNAME") 27 | vmanage_password = os.environ.get("vManage_PASSWORD") 28 | 29 | if vmanage_host is None or vmanage_port is None or vmanage_username is None or vmanage_password is None : 30 | print("CISCO SDWAN details must be set via environment variables before running.") 31 | print("export vManage_IP=10.10.20.90") 32 | print("export vManage_PORT=8443") 33 | print("export vManage_USERNAME=admin") 34 | print("export vManage_PASSWORD=C1sco12345") 35 | print("") 36 | exit() 37 | 38 | class Authentication: 39 | 40 | @staticmethod 41 | def get_jsessionid(vmanage_host, vmanage_port, username, password): 42 | api = "/j_security_check" 43 | base_url = "https://%s:%s"%(vmanage_host, vmanage_port) 44 | url = base_url + api 45 | payload = {'j_username' : username, 'j_password' : password} 46 | 47 | response = requests.post(url=url, data=payload, verify=False) 48 | try: 49 | cookies = response.headers["Set-Cookie"] 50 | jsessionid = cookies.split(";") 51 | return(jsessionid[0]) 52 | except: 53 | if logger is not None: 54 | logger.error("No valid JSESSION ID returned\n") 55 | exit() 56 | 57 | @staticmethod 58 | def get_token(vmanage_host, vmanage_port, jsessionid): 59 | headers = {'Cookie': jsessionid} 60 | base_url = "https://%s:%s"%(vmanage_host, vmanage_port) 61 | api = "/dataservice/client/token" 62 | url = base_url + api 63 | response = requests.get(url=url, headers=headers, verify=False) 64 | if response.status_code == 200: 65 | return(response.text) 66 | else: 67 | return None 68 | 69 | Auth = Authentication() 70 | jsessionid = Auth.get_jsessionid(vmanage_host,vmanage_port,vmanage_username,vmanage_password) 71 | token = Auth.get_token(vmanage_host,vmanage_port,jsessionid) 72 | 73 | if token is not None: 74 | header = {'Content-Type': "application/json",'Cookie': jsessionid, 'X-XSRF-TOKEN': token} 75 | else: 76 | header = {'Content-Type': "application/json",'Cookie': jsessionid} 77 | 78 | base_url = "https://%s:%s/dataservice"%(vmanage_host, vmanage_port) 79 | 80 | @click.group() 81 | def cli(): 82 | """Command line tool for deploying templates to CISCO SDWAN. 83 | """ 84 | pass 85 | 86 | @click.command() 87 | def device_list(): 88 | """Retrieve and return network devices list. 89 | 90 | Returns information about each device that is part of the fabric. 91 | 92 | Example command: 93 | 94 | ./sdwan.py device_list 95 | 96 | """ 97 | click.secho("Retrieving the devices.") 98 | 99 | url = base_url + "/device" 100 | 101 | response = requests.get(url=url, headers=header,verify=False) 102 | if response.status_code == 200: 103 | items = response.json()['data'] 104 | else: 105 | print("Failed to get list of devices " + str(response.text)) 106 | exit() 107 | 108 | headers = ["Host-Name", "Device Type", "Device ID", "System IP", "Site ID", "Version", "Device Model"] 109 | table = list() 110 | 111 | for item in items: 112 | tr = [item['host-name'], item['device-type'], item['uuid'], item['system-ip'], item['site-id'], item['version'], item['device-model']] 113 | table.append(tr) 114 | try: 115 | click.echo(tabulate.tabulate(table, headers, tablefmt="fancy_grid")) 116 | except UnicodeEncodeError: 117 | click.echo(tabulate.tabulate(table, headers, tablefmt="grid")) 118 | 119 | @click.command() 120 | def template_list(): 121 | """Retrieve and return templates list. 122 | 123 | Returns the templates available on the vManage instance. 124 | 125 | Example command: 126 | 127 | ./sdwan.py template_list 128 | 129 | """ 130 | click.secho("Retrieving the templates available.") 131 | 132 | url = base_url + "/template/device" 133 | 134 | response = requests.get(url=url, headers=header,verify=False) 135 | if response.status_code == 200: 136 | items = response.json()['data'] 137 | else: 138 | print("Failed to get list of templates") 139 | exit() 140 | 141 | headers = ["Template Name", "Device Type", "Template ID", "Attached devices", "Template version"] 142 | table = list() 143 | 144 | for item in items: 145 | tr = [item['templateName'], item['deviceType'], item['templateId'], item['devicesAttached'], item['templateAttached']] 146 | table.append(tr) 147 | try: 148 | click.echo(tabulate.tabulate(table, headers, tablefmt="fancy_grid")) 149 | except UnicodeEncodeError: 150 | click.echo(tabulate.tabulate(table, headers, tablefmt="grid")) 151 | 152 | @click.command() 153 | @click.option("--template", help="Name of the template you wish to retrieve information for") 154 | def attached_devices(template): 155 | """Retrieve and return devices associated to a template. 156 | 157 | Example command: 158 | 159 | ./sdwan.py attached_devices --template db4c997a-7212-4ec1-906e-ed2b86c3f42f 160 | 161 | """ 162 | 163 | url = base_url + "/template/device/config/attached/{0}".format(template) 164 | 165 | response = requests.get(url=url, headers=header,verify=False) 166 | if response.status_code == 200: 167 | items = response.json()['data'] 168 | else: 169 | print("Failed to get template details") 170 | exit() 171 | 172 | headers = ["Host Name", "Device IP", "Site ID", "Host ID", "Host Type"] 173 | table = list() 174 | 175 | for item in items: 176 | tr = [item['host-name'], item['deviceIP'], item['site-id'], item['uuid'], item['personality']] 177 | table.append(tr) 178 | try: 179 | click.echo(tabulate.tabulate(table, headers, tablefmt="fancy_grid")) 180 | except UnicodeEncodeError: 181 | click.echo(tabulate.tabulate(table, headers, tablefmt="grid")) 182 | 183 | @click.command() 184 | @click.option("--template", help="Name of the template to deploy") 185 | @click.option("--variables", help="Device Template variable values yaml file") 186 | #@click.argument("parameters", nargs=-1) 187 | def attach(template, variables): 188 | """Attach a template with Cisco SDWAN. 189 | 190 | Provide all template parameters and their values as arguments. 191 | 192 | Example command: 193 | 194 | ./sdwan.py attach --template template-id --variables Site-3-vEdge-Variables.yaml 195 | """ 196 | click.secho("Attempting to attach template.") 197 | 198 | with open(variables) as f: 199 | config = yaml.safe_load(f.read()) 200 | 201 | system_ip = config.get("system_ip") 202 | host_name = config.get("host_name") 203 | template_id = template 204 | 205 | template_variables = { 206 | "csv-status":"complete", 207 | "csv-deviceId": config.get("device_id"), 208 | "csv-deviceIP": system_ip, 209 | "csv-host-name": host_name, 210 | "//system/host-name": host_name, 211 | "//system/system-ip": system_ip, 212 | "//system/site-id": config.get("site_id"), 213 | "/1/vpn_1_if_name/interface/if-name": config.get("vpn_1_if_name"), 214 | "/1/vpn_1_if_name/interface/ip/address": config.get("vpn_1_if_ipv4_address"), 215 | "/512/vpn-instance/ip/route/0.0.0.0/0/next-hop/vpn_512_next_hop_ip_address/address": config.get("vpn_512_next_hop_ip_address"), 216 | "/512/vpn_512_if_name/interface/if-name": config.get("vpn_512_if_name"), 217 | "/512/vpn_512_if_name/interface/ip/address": config.get("vpn_512_if_ipv4_address"), 218 | "/0/vpn-instance/ip/route/0.0.0.0/0/next-hop/mpls_next_hop/address": config.get("mpls_next_hop"), 219 | "/0/vpn-instance/ip/route/0.0.0.0/0/next-hop/public_internet_next_hop/address": config.get("public_internet_next_hop"), 220 | "/0/vpn_public_internet_interface/interface/if-name": config.get("vpn_public_internet_interface"), 221 | "/0/vpn_public_internet_interface/interface/ip/address": config.get("vpn_public_interface_if_ipv4_address"), 222 | "/0/vpn_mpls_interface/interface/if-name": config.get("vpn_mpls_interface"), 223 | "/0/vpn_mpls_interface/interface/ip/address": config.get("vpn_mpls_if_ipv4_address"), 224 | "//system/gps-location/latitude": config.get("latitude"), 225 | "//system/gps-location/longitude": config.get("longitude") 226 | } 227 | 228 | 229 | payload = { 230 | "deviceTemplateList":[ 231 | { 232 | "templateId":template_id, 233 | "device":[ template_variables ], 234 | "isEdited":"false", 235 | "isMasterEdited":"false" 236 | } 237 | ] 238 | } 239 | 240 | url = base_url + "/template/device/config/attachfeature" 241 | 242 | response = requests.post(url=url, data=json.dumps(payload), headers=header, verify=False) 243 | if response.status_code == 200: 244 | attach_template_pushid = response.json()['id'] 245 | url = base_url + "/device/action/status/%s"%attach_template_pushid 246 | while(1): 247 | template_status_res = requests.get(url,headers=header,verify=False) 248 | if template_status_res.status_code == 200: 249 | template_push_status = template_status_res.json() 250 | if template_push_status['summary']['status'] == "done": 251 | if 'Success' in template_push_status['summary']['count']: 252 | print("Attached Site 3 vEdge Template") 253 | elif 'Failure' in template_push_status['summary']['count']: 254 | print("Failed to attach Site 3 vEdge Template") 255 | exit() 256 | break 257 | else: 258 | print("\nFetching template push status failed") 259 | exit() 260 | 261 | else: 262 | print("Failed to attach Site 3 vEdge Template") 263 | exit() 264 | 265 | @click.command() 266 | @click.option("--target", help="ID of the device to detach") 267 | @click.option("--sysip", help="System IP of the system to detach") 268 | def detach(target, sysip): 269 | """Detach a template with Cisco SDWAN. 270 | 271 | Provide all template parameters and their values as arguments. 272 | 273 | Example command: 274 | 275 | ./sdwan.py detach --target TargetID --sysip 1.1.1.1 276 | """ 277 | click.secho("Attempting to detach template.") 278 | 279 | payload = { 280 | "deviceType":"vedge", 281 | "devices":[ 282 | { 283 | "deviceId":str(target), 284 | "deviceIP":str(sysip) 285 | } 286 | ] 287 | } 288 | 289 | url = base_url + "/template/config/device/mode/cli" 290 | 291 | response = requests.post(url=url, data=json.dumps(payload), headers=header, verify=False) 292 | if response.status_code == 200: 293 | id = response.json()["id"] 294 | url = base_url + "/device/action/status/" + str(id) 295 | while(1): 296 | status_res = requests.get(url,headers=header,verify=False) 297 | if status_res.status_code == 200: 298 | push_status = status_res.json() 299 | if push_status['summary']['status'] == "done": 300 | if 'Success' in push_status['summary']['count']: 301 | print("Changed configuration mode to CLI") 302 | elif 'Failure' in push_status['summary']['count']: 303 | print("Failed to change configuration mode to CLI") 304 | exit() 305 | break 306 | else: 307 | print("Failed to detach template with error " + response.text) 308 | exit() 309 | 310 | cli.add_command(attach) 311 | cli.add_command(detach) 312 | cli.add_command(device_list) 313 | cli.add_command(attached_devices) 314 | cli.add_command(template_list) 315 | 316 | if __name__ == "__main__": 317 | cli() 318 | --------------------------------------------------------------------------------