├── .gitignore ├── .python-version ├── LICENSE ├── Makefile ├── README.md ├── inventories ├── inventory-10-csr-local │ ├── defaults.yaml │ ├── groups.yaml │ └── hosts.yaml ├── inventory-3-tier-local │ ├── defaults.yaml │ ├── groups.yaml │ ├── hosts.yaml │ └── nr-config.yaml └── simple-topology │ ├── defaults.yaml │ ├── groups.yaml │ ├── hosts.yaml │ └── nr-config.yaml ├── netconf-data └── config.yaml ├── netconf-xml-samples ├── get-config.xml ├── get-config2.xml └── get-config3.xml ├── notebooks ├── basics.ipynb ├── build_diagram.ipynb └── workshop.ipynb ├── nr-config-local.yaml ├── nr_app ├── __init__.py ├── constants.py ├── interface.py ├── link.py ├── topology.py └── utils.py ├── output └── topology.png ├── poetry.lock ├── pyproject.toml ├── requirements.txt ├── scripts ├── build_network_diagram_lldp.py ├── cli_configure.py ├── find_mac.py ├── gather_commands.py ├── netconf_configure.py ├── netconf_save_config.py └── restconf_get_lldp_neighbors.py └── templates └── config.j2 /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .coverage.* 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | *.cover 44 | .hypothesis/ 45 | .pytest_cache/ 46 | .mypy_cache/ 47 | 48 | 49 | # Environments 50 | .env 51 | .venv 52 | env/ 53 | venv/ 54 | ENV/ 55 | env.bak/ 56 | venv.bak/ 57 | 58 | .DS_Store 59 | .idea/ 60 | tmp/ 61 | *.log 62 | *.xlsx 63 | settings.toml 64 | 65 | .ipynb_checkpoints/ 66 | session-details.txt 67 | .vscode 68 | sandbox*.py 69 | live-coding-selection.txt 70 | timings.txt 71 | output/cli 72 | output/netconf 73 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.9.4 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dmitry Figol 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: restconf-check 2 | restconf-check: 3 | for number in 101 102 103 104 105 106 107 108 109 110 ; do \ 4 | curl -k -u cisco:cisco -H "Accept: application/yang-data+json" https://198.18.1.$$number/restconf/data/native/hostname ; \ 5 | done 6 | 7 | .PHONY: pull 8 | pull: 9 | docker pull dmfigol/jupyter-netdevops:latest 10 | 11 | .PHONY: up 12 | up: 13 | docker kill $(shell docker ps -q); \ 14 | docker pull dmfigol/jupyter-netdevops:latest; \ 15 | docker run -it --rm -d -p 58888:58888 -v ${PWD}/jupyter:/jupyter/ --name=nornir-workshop dmfigol/jupyter-netdevops:latest 16 | 17 | .PHONY: start 18 | start: 19 | docker run -it --rm -p 58888:58888 -v ${PWD}/jupyter:/jupyter/ --name=nornir-workshop dmfigol/jupyter-netdevops:latest 20 | 21 | .PHONY: stop 22 | stop: 23 | docker kill $(shell docker ps -q) 24 | 25 | .PHONY: remove-ssh-keys 26 | remove-ssh-keys: 27 | sed -i "" '/198.18.1/d' ${HOME}/.ssh/known_hosts; 28 | 29 | .PHONY: browser 30 | browser: 31 | open "http://localhost:58888/"; \ 32 | open "http://localhost:58888/notebooks/workshop.ipynb" 33 | 34 | .PHONY: credentials 35 | credentials: 36 | mv session-details.example session-details.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Repository with examples of Nornir applications 2 | #### Supporting my DEVNET-2192 Automate your Network with Nornir presentation. 3 | 4 | To run these examples, you need an environment with IOS-XE routers. You can have any number, but at least two is recommended. You would also want to modify the inventory folder with the details for your environment (IP addresses, credentials). 5 | 6 | On the control machine, you need to have Python 3.6+ installed (tested on Python 3.9.4). Dependencies are managed with [poetry](https://python-poetry.org/). After cloning the repository, use `poetry install` to install dependencies. When you are running the scripts, you would want to prepend them with `poetry run`, for example: `poetry run python scripts/gather_commands.py`. Run scripts in the root of the repository. 7 | 8 | #### License 9 | It is MIT licensed. Feel free to do whatever you want with the code (including commercial use). No attribution is required. 10 | 11 | #### Scripts details 12 | * `gather_commands.py` - executes some show commands and saves outputs to a file, one per each device 13 | * `cli_configure.py` - configures network devices based on Jinja2 template `templates/config.j2` 14 | * `find_mac.py` - find a mac address location (attached to a switch or on any network device). It was built for a specific topology and needs to be slightly reworked to support an arbitrary topology. 15 | * `netconf_save_config.py` - retrieves XML configuration via NETCONF and saves it to a file, one per each device 16 | * `restconf_get_lldp_neighbors.py` - retrieves LLDP details using OpenConfig LLDP YANG model + RESTCONF as a tranport protocol. Prints all connections on the console 17 | * `build_network_diagram_lldp.py` - retrieves LLDP details using OpenConfig LLDP YANG model + RESTCONF as a tranport protocol, then parses them, builds a graph and creates a network diagram based on that graph 18 | * `netconf_configure.py` - configures network devices using NETCONF where the data resides in YAML files. Jinja XML templates are NOT used. -------------------------------------------------------------------------------- /inventories/inventory-10-csr-local/defaults.yaml: -------------------------------------------------------------------------------- 1 | username: cisco 2 | password: cisco 3 | platform: cisco_ios 4 | connection_options: 5 | netconf: 6 | extras: 7 | # allow_agent: False 8 | hostkey_verify: False 9 | scrapli: 10 | platform: cisco_iosxe 11 | extras: 12 | transport: ssh2 13 | ssh_config_file: True 14 | auth_strict_key: False 15 | scrapli_netconf: 16 | platform: cisco_iosxe 17 | extras: 18 | transport: ssh2 19 | ssh_config_file: True 20 | auth_strict_key: False 21 | napalm: 22 | platform: ios 23 | -------------------------------------------------------------------------------- /inventories/inventory-10-csr-local/groups.yaml: -------------------------------------------------------------------------------- 1 | Miami: 2 | data: {} 3 | London: 4 | data: {} -------------------------------------------------------------------------------- /inventories/inventory-10-csr-local/hosts.yaml: -------------------------------------------------------------------------------- 1 | R1: 2 | hostname: 192.168.152.101 3 | groups: 4 | - Miami 5 | data: 6 | tags: 7 | - isr4400 8 | R2: 9 | hostname: 192.168.152.102 10 | groups: 11 | - Miami 12 | data: 13 | tags: 14 | - isr4300 15 | R3: 16 | hostname: 192.168.152.103 17 | groups: 18 | - Miami 19 | data: 20 | tags: 21 | - isr4400 22 | - edge 23 | R4: 24 | hostname: 192.168.152.104 25 | groups: 26 | - Miami 27 | data: 28 | tags: 29 | - isr4300 30 | R5: 31 | hostname: 192.168.152.105 32 | groups: 33 | - Miami 34 | data: 35 | tags: 36 | - isr4400 37 | - edge 38 | R6: 39 | hostname: 192.168.152.106 40 | groups: 41 | - London 42 | data: 43 | tags: 44 | - isr4300 45 | - edge 46 | R7: 47 | hostname: 192.168.152.107 48 | groups: 49 | - London 50 | data: 51 | tags: 52 | - isr4400 53 | - edge 54 | R8: 55 | hostname: 192.168.152.108 56 | groups: 57 | - London 58 | data: 59 | tags: 60 | - isr4300 61 | R9: 62 | hostname: 192.168.152.109 63 | groups: 64 | - London 65 | data: 66 | tags: 67 | - isr4400 68 | R10: 69 | hostname: 192.168.152.110 70 | groups: 71 | - London 72 | data: 73 | tags: 74 | - isr4300 75 | -------------------------------------------------------------------------------- /inventories/inventory-3-tier-local/defaults.yaml: -------------------------------------------------------------------------------- 1 | platform: ios 2 | username: cisco 3 | password: cisco 4 | data: 5 | snmp_community: secret 6 | connection_options: 7 | netconf: 8 | extras: 9 | # allow_agent: False 10 | hostkey_verify: False 11 | scrapli: 12 | platform: cisco_iosxe 13 | port: 22 14 | extras: 15 | transport: ssh2 16 | ssh_config_file: True 17 | auth_strict_key: False 18 | -------------------------------------------------------------------------------- /inventories/inventory-3-tier-local/groups.yaml: -------------------------------------------------------------------------------- 1 | New York: 2 | data: 3 | ntp: 4 | servers: 5 | - 1.2.3.4 6 | Lisbon: 7 | data: 8 | ntp: 9 | servers: 10 | - 4.5.6.7 -------------------------------------------------------------------------------- /inventories/inventory-3-tier-local/hosts.yaml: -------------------------------------------------------------------------------- 1 | inside-host1: 2 | hostname: 192.168.153.179 3 | groups: 4 | - hosts 5 | inside-host2: 6 | hostname: 192.168.153.180 7 | groups: 8 | - hosts 9 | dist-sw1: 10 | hostname: 192.168.153.103 11 | groups: 12 | - New York 13 | data: 14 | tags: 15 | - isr4400 16 | - edge 17 | - bgp 18 | - ospf 19 | R4: 20 | hostname: 192.168.153.104 21 | groups: 22 | - New York 23 | data: 24 | tags: 25 | - isr4300 26 | - bgp 27 | - ospf 28 | R5: 29 | hostname: 192.168.153.105 30 | groups: 31 | - New York 32 | data: 33 | tags: 34 | - isr4400 35 | - edge 36 | - bgp 37 | - ospf 38 | R6: 39 | hostname: 192.168.153.106 40 | groups: 41 | - Lisbon 42 | data: 43 | tags: 44 | - isr4300 45 | - edge 46 | - bgp 47 | - eigrp 48 | R7: 49 | hostname: 192.168.153.107 50 | groups: 51 | - Lisbon 52 | data: 53 | tags: 54 | - isr4400 55 | - edge 56 | - eigrp 57 | R8: 58 | hostname: 192.168.153.108 59 | groups: 60 | - Lisbon 61 | data: 62 | tags: 63 | - isr4300 64 | - eigrp 65 | R9: 66 | hostname: 192.168.153.109 67 | groups: 68 | - Lisbon 69 | data: 70 | tags: 71 | - isr4400 72 | - eigrp 73 | R10: 74 | hostname: 192.168.153.110 75 | groups: 76 | - Lisbon 77 | data: 78 | tags: 79 | - isr4300 80 | - eigrp -------------------------------------------------------------------------------- /inventories/inventory-3-tier-local/nr-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | core: 3 | num_workers: 15 4 | raise_on_error: False 5 | 6 | logging: 7 | enabled: False 8 | 9 | inventory: 10 | plugin: nornir.plugins.inventory.simple.SimpleInventory 11 | options: 12 | host_file: "inventory-3-tier-local/hosts.yaml" 13 | group_file: "inventory-3-tier-local/groups.yaml" 14 | defaults_file: "inventory-3-tier-local/defaults.yaml" -------------------------------------------------------------------------------- /inventories/simple-topology/defaults.yaml: -------------------------------------------------------------------------------- 1 | # platform: ios 2 | username: cisco 3 | password: cisco 4 | connection_options: 5 | napalm: 6 | extras: 7 | # allow_agent: False 8 | hostkey_verify: False 9 | scrapli: 10 | # platform: cisco_iosxe 11 | port: 22 12 | extras: 13 | transport: ssh2 14 | ssh_config_file: False 15 | auth_strict_key: False 16 | -------------------------------------------------------------------------------- /inventories/simple-topology/groups.yaml: -------------------------------------------------------------------------------- 1 | hosts: 2 | platform: linux 3 | cisco-nexus: 4 | platform: nx_os 5 | iosv: 6 | platform: cisco_ios 7 | connection_options: 8 | scrapli: 9 | platform: cisco_iosxe 10 | csr: 11 | connection_options: 12 | scrapli: 13 | platform: cisco_iosxe -------------------------------------------------------------------------------- /inventories/simple-topology/hosts.yaml: -------------------------------------------------------------------------------- 1 | PC1: 2 | hostname: 192.168.153.201 3 | groups: 4 | - hosts 5 | PC2: 6 | hostname: 192.168.153.202 7 | groups: 8 | - hosts 9 | PC3: 10 | hostname: 192.168.153.203 11 | groups: 12 | - hosts 13 | PC4: 14 | hostname: 192.168.153.204 15 | groups: 16 | - hosts 17 | ASW1: 18 | hostname: 192.168.153.101 19 | groups: 20 | - iosv 21 | data: 22 | tags: 23 | - access 24 | ASW2: 25 | hostname: 192.168.153.102 26 | groups: 27 | - iosv 28 | data: 29 | tags: 30 | - access 31 | ASW3: 32 | hostname: 192.168.153.103 33 | groups: 34 | - cisco-nexus 35 | data: 36 | tags: 37 | - access 38 | DSW1: 39 | hostname: 192.168.153.104 40 | groups: 41 | - iosv 42 | data: 43 | tags: 44 | - distribution 45 | CORE1: 46 | hostname: 192.168.153.105 47 | groups: 48 | - csr 49 | data: 50 | tags: 51 | - core 52 | EDGE1: 53 | hostname: 192.168.153.106 54 | groups: 55 | - csr 56 | data: 57 | tags: 58 | - edge 59 | -------------------------------------------------------------------------------- /inventories/simple-topology/nr-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | core: 3 | num_workers: 15 4 | raise_on_error: False 5 | 6 | logging: 7 | enabled: True 8 | 9 | inventory: 10 | plugin: nornir.plugins.inventory.simple.SimpleInventory 11 | options: 12 | host_file: "inventories/simple-topology/hosts.yaml" 13 | group_file: "inventories/simple-topology/groups.yaml" 14 | defaults_file: "inventories/simple-topology/defaults.yaml" -------------------------------------------------------------------------------- /netconf-data/config.yaml: -------------------------------------------------------------------------------- 1 | native: 2 | _xmlns: http://cisco.com/ns/yang/Cisco-IOS-XE-native 3 | vrf: 4 | _operation: replace 5 | definition: 6 | - name: Mgmt-vrf 7 | address-family: 8 | ipv4: null 9 | ipv6: null 10 | - name: NORNIR 11 | address-family: 12 | ipv4: null 13 | ipv6: null 14 | ip: 15 | access-list: 16 | _operation: replace 17 | extended: 18 | - name: TEST 19 | _xmlns: http://cisco.com/ns/yang/Cisco-IOS-XE-acl 20 | access-list-seq-rule: 21 | - sequence: 10 22 | ace-rule: 23 | action: permit 24 | protocol: ip 25 | any: null 26 | # ipv4-address: 192.168.1.0 27 | # mask: 0.0.0.255 28 | dst-any: null 29 | - name: NOT-SO-RANDOM 30 | _xmlns: http://cisco.com/ns/yang/Cisco-IOS-XE-acl 31 | access-list-seq-rule: 32 | - sequence: 10 33 | ace-rule: 34 | action: deny 35 | protocol: ip 36 | any: null 37 | dst-host: 1.2.3.4 38 | -------------------------------------------------------------------------------- /netconf-xml-samples/get-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17.1 6 | 7 | 8 | 9 | 10 | 11 | 72107 12 | 13 | 14 | 15 | 16 | sch-smart-licensing@cisco.com 17 | 18 | CiscoTAC-1 19 | true 20 | 21 | http 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | auto 43 | 44 | 45 | 46 | 47 | 80 48 | 49 | 50 | 51 | 52 | R1 53 | 54 | 55 | cisco 56 | 57 | 58 | 9 59 | $14$OAMZ$LWwcNBkf2sPSOk$7nj.eF52utMU3Vr1dxB82.Uw1EGkkTkMMxMgqZS4PkU 60 | 61 | 62 | 63 | cisco 64 | 15 65 | 66 | 0 67 | cisco 68 | 69 | 70 | 71 | vagrant 72 | 15 73 | 74 | 0 75 | vagrant 76 | 77 | 78 | 79 | 80 | Mgmt-vrf 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | ciscolive.com 90 | 91 | 92 | nd 93 | 94 | 95 | 2147483647 96 | 97 | 98 | 99 | Mgmt-vrf 100 | 192.168.153.1 101 | 198.18.153.1 102 | 192.168.153.1 103 | 104 | 105 | 106 | 107 | false 108 | 109 | 110 | 111 | 112 | Mgmt-vrf 113 | 114 | 0.0.0.0 115 | 0.0.0.0 116 | 117 | 192.168.153.1 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | vagrant 131 | 132 | ssh-rsa 133 | DD3BB82E850406E9ABFFA80AC0046ED6 134 | 135 | 136 | 137 | 2 138 | 139 | 140 | 141 | 1 142 | 143 | 144 | 145 | 146 | meraki-fqdn-dns 147 | 148 | 149 | 150 | 151 | 152 | 153 | false 154 | true 155 | 156 | GigabitEthernet1 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | false 165 | 166 | 167 | 168 | 169 | 170 | true 171 | 172 | 173 | 174 | 175 | 1 176 | 177 | Mgmt-vrf 178 | 179 | 180 |
181 | 182 |
192.168.153.101
183 | 255.255.255.0 184 |
185 |
186 | 187 | false 188 | false 189 | false 190 | false 191 | 192 |
193 | 194 | false 195 | false 196 | 197 | 198 | true 199 | 200 | 201 | false 202 | false 203 | 204 |
205 | 206 | 2 207 | 208 |
209 | 210 |
100.65.1.1
211 | 255.255.255.0 212 |
213 |
214 | 215 | false 216 | false 217 | false 218 | false 219 | 220 |
221 | 222 | false 223 | false 224 | 225 | 226 | true 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | true 235 | 236 |
237 | 238 | 3 239 | 240 |
241 | 242 |
100.64.12.1
243 | 255.255.255.0 244 |
245 |
246 | 247 | false 248 | false 249 | false 250 | false 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 |
260 | 261 | false 262 | false 263 | 264 | 265 | true 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | true 274 | 275 |
276 | 277 | 4 278 | 279 |
280 | 281 |
100.64.14.1
282 | 255.255.255.0 283 |
284 |
285 | 286 | false 287 | false 288 | false 289 | false 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 |
299 | 300 | false 301 | false 302 | 303 | 304 | true 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | true 313 | 314 |
315 | 316 | 0 317 | 318 |
319 | 320 |
100.64.1.1
321 | 255.255.255.255 322 |
323 |
324 | 325 | false 326 | false 327 | false 328 | false 329 | 330 |
331 |
332 | 333 | 1 334 | 335 |
336 | 337 |
100.70.1.1
338 | 255.255.255.255 339 |
340 |
341 | 342 | false 343 | false 344 | false 345 | false 346 | 347 |
348 |
349 |
350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | authenticated 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 2500 372 | 10000 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | SLA-TrustPoint 381 | 382 | 01 383 | ca 384 | 385 | 386 | 387 | TP-self-signed-2101032369 388 | 389 | 01 390 | self-signed 391 | 392 | 393 | 394 | 395 | SLA-TrustPoint 396 | 397 | 398 | 399 | crl 400 | 401 | 402 | TP-self-signed-2101032369 403 | 404 | 405 | 406 | none 407 | cn=IOS-Self-Signed-Certificate-2101032369 408 | 409 | 410 | 411 | 412 | 413 | 100 414 | 415 | true 416 | 417 | 1.1.1.1 418 | 419 | 420 | 421 | 100.64.2.2 422 | 100 423 | 424 | 425 | 0 426 | 427 | 428 | 429 | 430 | 100.64.3.3 431 | 100 432 | 433 | 434 | 0 435 | 436 | 437 | 438 | 439 | 100.64.4.4 440 | 100 441 | 442 | 443 | 0 444 | 445 | 446 | 447 | 448 | 100.64.5.5 449 | 100 450 | 451 | 452 | 0 453 | 454 | 455 | 456 | 457 | 100.65.1.11 458 | 2222 459 | 460 | 461 | 462 | 463 | 464 | 1 465 | 466 | 100.64.0.0 467 | 0.1.255.255 468 | 0 469 | 470 | 1.1.1.1 471 | 472 | 473 | 474 | 475 | 1 476 | 477 | 100 478 | 479 | 480 | 481 | 482 | 50 483 | 200 484 | 5000 485 | 486 | 487 | 488 | 1.1.1.1 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | CSR1000V 497 | 9A1DLTZIU44 498 | 499 | 500 | 501 | 502 | 0 503 | 504 | 505 | 506 | 507 | 508 | 15 509 | 510 | 511 | 1 512 | 513 | 514 | none 515 | 516 | 517 | 518 | 519 | 0 520 | 4 521 | 522 | 523 | 524 | 525 | 526 | 15 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | none 535 | 536 | 537 | 538 | 539 | 5 540 | 14 541 | 542 | 543 | 544 | 545 | 546 | 15 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | none 555 | 556 | 557 | 558 | 559 | 560 | 561 | minimal 562 | 563 | 564 | 565 | 566 | 567 |
568 | 569 | 570 | false 571 | 572 | false 573 | false 574 | 575 | 576 | false 577 | 578 | 579 | 580 | 581 | 582 | 583 | true 584 | 585 | private 586 | 587 | 588 | 589 | 590 | 591 | meraki-fqdn-dns 592 | ACL_IPV4 593 | 594 | meraki-fqdn-dns 595 | ACL_IPV4 596 | 597 | 598 | 599 | 600 | 601 | 602 | GigabitEthernet1 603 | 604 | GigabitEthernet1 605 | ianaift:ethernetCsmacd 606 | true 607 | 608 | 609 | 610 | 0 611 | 612 | 0 613 | true 614 | 615 | 616 | 617 |
618 | 192.168.153.101 619 | 620 | 192.168.153.101 621 | 24 622 | 623 |
624 |
625 |
626 | 627 | 628 | false 629 | 630 | 631 |
632 |
633 | 634 | 635 | 0c:83:a0:1d:c4:00 636 | true 637 | true 638 | 639 | 640 |
641 | 642 | GigabitEthernet2 643 | 644 | GigabitEthernet2 645 | ianaift:ethernetCsmacd 646 | true 647 | 648 | 649 | 650 | 0 651 | 652 | 0 653 | true 654 | 655 | 656 | 657 |
658 | 100.65.1.1 659 | 660 | 100.65.1.1 661 | 24 662 | 663 |
664 |
665 |
666 | 667 | 668 | false 669 | 670 | 671 |
672 |
673 | 674 | 675 | 0c:83:a0:1d:c4:01 676 | true 677 | true 678 | 679 | 680 |
681 | 682 | GigabitEthernet3 683 | 684 | GigabitEthernet3 685 | ianaift:ethernetCsmacd 686 | true 687 | 688 | 689 | 690 | 0 691 | 692 | 0 693 | true 694 | 695 | 696 | 697 |
698 | 100.64.12.1 699 | 700 | 100.64.12.1 701 | 24 702 | 703 |
704 |
705 |
706 | 707 | 708 | false 709 | 710 | 711 |
712 |
713 | 714 | 715 | 0c:83:a0:1d:c4:02 716 | true 717 | true 718 | 719 | 720 |
721 | 722 | GigabitEthernet4 723 | 724 | GigabitEthernet4 725 | ianaift:ethernetCsmacd 726 | true 727 | 728 | 729 | 730 | 0 731 | 732 | 0 733 | true 734 | 735 | 736 | 737 |
738 | 100.64.14.1 739 | 740 | 100.64.14.1 741 | 24 742 | 743 |
744 |
745 |
746 | 747 | 748 | false 749 | 750 | 751 |
752 |
753 | 754 | 755 | 0c:83:a0:1d:c4:03 756 | true 757 | true 758 | 759 | 760 |
761 | 762 | Loopback0 763 | 764 | Loopback0 765 | ianaift:softwareLoopback 766 | true 767 | 768 | 769 | 770 | 0 771 | 772 | 0 773 | true 774 | 775 | 776 | 777 |
778 | 100.64.1.1 779 | 780 | 100.64.1.1 781 | 32 782 | 783 |
784 |
785 |
786 | 787 | 788 | false 789 | 790 | 791 |
792 |
793 |
794 | 795 | Loopback1 796 | 797 | Loopback1 798 | ianaift:softwareLoopback 799 | true 800 | 801 | 802 | 803 | 0 804 | 805 | 0 806 | true 807 | 808 | 809 | 810 |
811 | 100.70.1.1 812 | 813 | 100.70.1.1 814 | 32 815 | 816 |
817 |
818 |
819 | 820 | 821 | false 822 | 823 | 824 |
825 |
826 |
827 |
828 | 829 | 830 | true 831 | 832 | 833 | 834 | GigabitEthernet1 835 | 836 | GigabitEthernet1 837 | false 838 | 839 | 840 | 841 | GigabitEthernet2 842 | 843 | GigabitEthernet2 844 | true 845 | 846 | 847 | 848 | GigabitEthernet3 849 | 850 | GigabitEthernet3 851 | true 852 | 853 | 854 | 855 | GigabitEthernet4 856 | 857 | GigabitEthernet4 858 | true 859 | 860 | 861 | 862 | 863 | 864 | 865 | Mgmt-vrf 866 | 867 | Mgmt-vrf 868 | oc-ni-types:L3VRF 869 | oc-types:IPV6 870 | oc-types:IPV4 871 | 872 | 873 | 874 | GigabitEthernet1 875 | 876 | GigabitEthernet1 877 | GigabitEthernet1 878 | 879 | 880 | 881 | 882 | 883 | oc-pol-types:DIRECTLY_CONNECTED 884 | oc-types:IPV4 885 | 886 | oc-pol-types:DIRECTLY_CONNECTED 887 | oc-types:IPV4 888 | 889 |
890 | 891 | oc-pol-types:DIRECTLY_CONNECTED 892 | oc-types:IPV6 893 | 894 | oc-pol-types:DIRECTLY_CONNECTED 895 | oc-types:IPV6 896 | 897 |
898 | 899 | oc-pol-types:STATIC 900 | oc-types:IPV4 901 | 902 | oc-pol-types:STATIC 903 | oc-types:IPV4 904 | 905 |
906 | 907 | oc-pol-types:STATIC 908 | oc-types:IPV6 909 | 910 | oc-pol-types:STATIC 911 | oc-types:IPV6 912 | 913 |
914 |
915 | 916 | 917 | oc-pol-types:STATIC 918 | DEFAULT 919 | 920 | oc-pol-types:STATIC 921 | DEFAULT 922 | 923 | 924 | 925 | 0.0.0.0/0 926 | 927 | 0.0.0.0/0 928 | 929 | 930 | 931 | 192.168.153.1 932 | 933 | 192.168.153.1 934 | 192.168.153.1 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | oc-pol-types:DIRECTLY_CONNECTED 943 | DEFAULT 944 | 945 | oc-pol-types:DIRECTLY_CONNECTED 946 | DEFAULT 947 | 948 | 949 | 950 |
951 | 952 | default 953 | 954 | default 955 | oc-ni-types:DEFAULT_INSTANCE 956 | default-vrf [read-only] 957 | 958 | 959 | 960 | oc-pol-types:DIRECTLY_CONNECTED 961 | oc-types:IPV4 962 | 963 | oc-pol-types:DIRECTLY_CONNECTED 964 | oc-types:IPV4 965 | 966 |
967 | 968 | oc-pol-types:DIRECTLY_CONNECTED 969 | oc-types:IPV6 970 | 971 | oc-pol-types:DIRECTLY_CONNECTED 972 | oc-types:IPV6 973 | 974 |
975 | 976 | oc-pol-types:OSPF 977 | oc-types:IPV4 978 | 979 | oc-pol-types:OSPF 980 | oc-types:IPV4 981 | 982 |
983 | 984 | oc-pol-types:STATIC 985 | oc-types:IPV4 986 | 987 | oc-pol-types:STATIC 988 | oc-types:IPV4 989 | 990 |
991 | 992 | oc-pol-types:STATIC 993 | oc-types:IPV6 994 | 995 | oc-pol-types:STATIC 996 | oc-types:IPV6 997 | 998 |
999 |
1000 | 1001 | 1002 | oc-pol-types:BGP 1003 | 100 1004 | 1005 | oc-pol-types:BGP 1006 | 100 1007 | 1008 | 1009 | 1010 | 1011 | 100 1012 | 1.1.1.1 1013 | 1014 | 1015 | 1016 | false 1017 | 1018 | 1019 | 1020 | 1021 | false 1022 | true 1023 | 1024 | 1025 | 1026 | 1027 | 1028 | 100.64.2.2 1029 | 1030 | 100.64.2.2 1031 | 100 1032 | 1033 | 1034 | 1035 | 180.0 1036 | 60.0 1037 | 1038 | 1039 | 1040 | 1041 | false 1042 | 1043 | 1044 | 1045 | 1046 | 100.64.3.3 1047 | 1048 | 100.64.3.3 1049 | 100 1050 | 1051 | 1052 | 1053 | 180.0 1054 | 60.0 1055 | 1056 | 1057 | 1058 | 1059 | false 1060 | 1061 | 1062 | 1063 | 1064 | 100.64.4.4 1065 | 1066 | 100.64.4.4 1067 | 100 1068 | 1069 | 1070 | 1071 | 180.0 1072 | 60.0 1073 | 1074 | 1075 | 1076 | 1077 | false 1078 | 1079 | 1080 | 1081 | 1082 | 100.64.5.5 1083 | 1084 | 100.64.5.5 1085 | 100 1086 | 1087 | 1088 | 1089 | 180.0 1090 | 60.0 1091 | 1092 | 1093 | 1094 | 1095 | false 1096 | 1097 | 1098 | 1099 | 1100 | 100.65.1.11 1101 | 1102 | 100.65.1.11 1103 | 2222 1104 | 1105 | 1106 | 1107 | 180.0 1108 | 60.0 1109 | 1110 | 1111 | 1112 | 1113 | false 1114 | 1115 | 1116 | 1117 | 1118 | 1119 | 1120 | 1121 | oc-pol-types:OSPF 1122 | 1 1123 | 1124 | oc-pol-types:OSPF 1125 | 1 1126 | 1127 | 1128 | 1129 | oc-pol-types:STATIC 1130 | DEFAULT 1131 | 1132 | oc-pol-types:STATIC 1133 | DEFAULT 1134 | 1135 | 1136 | 1137 | oc-pol-types:DIRECTLY_CONNECTED 1138 | DEFAULT 1139 | 1140 | oc-pol-types:DIRECTLY_CONNECTED 1141 | DEFAULT 1142 | 1143 | 1144 | 1145 |
1146 |
1147 | 1148 | 1149 | GigabitEthernet1 1150 | ianaift:ethernetCsmacd 1151 | true 1152 | 1153 |
1154 | 192.168.153.101 1155 | 255.255.255.0 1156 |
1157 |
1158 | 1159 |
1160 | 1161 | GigabitEthernet2 1162 | ianaift:ethernetCsmacd 1163 | true 1164 | 1165 |
1166 | 100.65.1.1 1167 | 255.255.255.0 1168 |
1169 |
1170 | 1171 |
1172 | 1173 | GigabitEthernet3 1174 | ianaift:ethernetCsmacd 1175 | true 1176 | 1177 |
1178 | 100.64.12.1 1179 | 255.255.255.0 1180 |
1181 |
1182 | 1183 |
1184 | 1185 | GigabitEthernet4 1186 | ianaift:ethernetCsmacd 1187 | true 1188 | 1189 |
1190 | 100.64.14.1 1191 | 255.255.255.0 1192 |
1193 |
1194 | 1195 |
1196 | 1197 | Loopback0 1198 | ianaift:softwareLoopback 1199 | true 1200 | 1201 |
1202 | 100.64.1.1 1203 | 255.255.255.255 1204 |
1205 |
1206 | 1207 |
1208 | 1209 | Loopback1 1210 | ianaift:softwareLoopback 1211 | true 1212 | 1213 |
1214 | 100.70.1.1 1215 | 255.255.255.255 1216 |
1217 |
1218 | 1219 |
1220 |
1221 | 1222 | true 1223 | deny 1224 | deny 1225 | deny 1226 | true 1227 | 1228 | admin 1229 | PRIV15 1230 | 1231 | permit-all 1232 | * 1233 | * 1234 | permit 1235 | 1236 | 1237 | 1238 | 1239 | 1240 | Mgmt-vrf 1241 | 1242 | GigabitEthernet1 1243 | 1244 | 1245 | 1246 | static 1247 | 1 1248 | 1249 | 1250 | 1251 | 0.0.0.0/0 1252 | 1253 | 192.168.153.1 1254 | 1255 | 1256 | 1257 | 1258 | 1259 | 1260 | 1261 | 1262 | default 1263 | default-vrf [read-only] 1264 | 1265 | 1266 | ospf:ospfv2 1267 | 1 1268 | 1269 | 1270 | rt:ipv4 1271 | 1.1.1.1 1272 | 1273 | false 1274 | 1275 | 1276 | true 1277 | 100 1278 | 1279 | 1280 | 1281 | 50 1282 | 200 1283 | 5000 1284 | 1285 | 1286 | 1287 | 1288 | 1289 | 1290 | 1291 | 1292 | 1293 | static 1294 | 1 1295 | 1296 | 1297 | 1298 | 1299 |
1300 |
-------------------------------------------------------------------------------- /netconf-xml-samples/get-config2.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 17.1 6 | 7 | 8 | 9 | 10 | 11 | 72107 12 | 13 | 14 | 15 | 16 | sch-smart-licensing@cisco.com 17 | 18 | CiscoTAC-1 19 | true 20 | 21 | http 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | auto 43 | 44 | 45 | 46 | 47 | 80 48 | 49 | 50 | 51 | 52 | R2 53 | 54 | 55 | cisco 56 | 57 | 58 | 9 59 | $14$OAMZ$1.aMXANqwuD6n.$JvNykr1paPK5argrRMvnOKFznKNgfQzfayM3GOHBSlc 60 | 61 | 62 | 63 | cisco 64 | 15 65 | 66 | 0 67 | cisco 68 | 69 | 70 | 71 | vagrant 72 | 15 73 | 74 | 0 75 | vagrant 76 | 77 | 78 | 79 | 80 | Mgmt-vrf 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | ciscolive.com 90 | 91 | 92 | nd 93 | 94 | 95 | 2147483647 96 | 97 | 98 | 99 | Mgmt-vrf 100 | 192.168.153.1 101 | 192.168.153.1 102 | 103 | 104 | 105 | 106 | false 107 | 108 | 109 | 110 | 111 | Mgmt-vrf 112 | 113 | 0.0.0.0 114 | 0.0.0.0 115 | 116 | 192.168.153.1 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | vagrant 130 | 131 | ssh-rsa 132 | DD3BB82E850406E9ABFFA80AC0046ED6 133 | 134 | 135 | 136 | 2 137 | 138 | 139 | 140 | 1 141 | 142 | 143 | 144 | 145 | TEST 146 | 147 | 10 148 | 149 | deny 150 | icmp 151 | 152 | 153 | 154 | 155 | 156 | 20 157 | 158 | permit 159 | ip 160 | 161 | 162 | 163 | 164 | 165 | 166 | meraki-fqdn-dns 167 | 168 | 169 | 170 | 171 | 172 | 173 | false 174 | true 175 | 176 | GigabitEthernet1 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | false 185 | 186 | 187 | 188 | 189 | 190 | true 191 | 192 | 193 | 194 | 195 | 1 196 | 197 | Mgmt-vrf 198 | 199 | 200 |
201 | 202 |
192.168.153.102
203 | 255.255.255.0 204 |
205 |
206 | 207 | false 208 | false 209 | false 210 | false 211 | 212 |
213 | 214 | false 215 | false 216 | 217 | 218 | true 219 | 220 | 221 | false 222 | false 223 | 224 |
225 | 226 | 2 227 | 228 |
229 | 230 |
100.64.12.2
231 | 255.255.255.0 232 |
233 |
234 | 235 | false 236 | false 237 | false 238 | false 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 |
248 | 249 | false 250 | false 251 | 252 | 253 | true 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | true 262 | 263 |
264 | 265 | 3 266 | 267 |
268 | 269 |
100.64.23.2
270 | 255.255.255.0 271 |
272 |
273 | 274 | false 275 | false 276 | false 277 | false 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 |
287 | 288 | false 289 | false 290 | 291 | 292 | true 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | true 301 | 302 |
303 | 304 | 4 305 | 306 | 307 | 308 | false 309 | false 310 | false 311 | false 312 | 313 | 314 | 315 | false 316 | false 317 | 318 | 319 | true 320 | 321 | 322 | 323 | 0 324 | 325 |
326 | 327 |
100.64.2.2
328 | 255.255.255.255 329 |
330 |
331 | 332 | false 333 | false 334 | false 335 | false 336 | 337 |
338 |
339 | 340 | 1 341 | 342 |
343 | 344 |
100.70.2.2
345 | 255.255.255.255 346 |
347 |
348 | 349 | false 350 | false 351 | false 352 | false 353 | 354 |
355 |
356 |
357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | authenticated 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 2500 379 | 10000 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | SLA-TrustPoint 388 | 389 | 01 390 | ca 391 | 392 | 393 | 394 | TP-self-signed-2939361440 395 | 396 | 01 397 | self-signed 398 | 399 | 400 | 401 | 402 | SLA-TrustPoint 403 | 404 | 405 | 406 | crl 407 | 408 | 409 | TP-self-signed-2939361440 410 | 411 | 412 | 413 | none 414 | cn=IOS-Self-Signed-Certificate-2939361440 415 | 416 | 417 | 418 | 419 | 420 | 100 421 | 422 | true 423 | 424 | 2.2.2.2 425 | 426 | 427 | 428 | 100.64.1.1 429 | 100 430 | 431 | 432 | 0 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 1 441 | 442 | 100.64.0.0 443 | 0.1.255.255 444 | 0 445 | 446 | 2.2.2.2 447 | 448 | 449 | 450 | 451 | 1 452 | 453 | 100 454 | 455 | 456 | 457 | 458 | 50 459 | 200 460 | 5000 461 | 462 | 463 | 464 | 2.2.2.2 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | CSR1000V 473 | 9Y4HO06LI74 474 | 475 | 476 | 477 | 478 | 0 479 | 480 | 481 | 482 | 483 | 484 | 15 485 | 486 | 487 | 1 488 | 489 | 490 | none 491 | 492 | 493 | 494 | 495 | 0 496 | 4 497 | 498 | 499 | 500 | 501 | 502 | 15 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | none 511 | 512 | 513 | 514 | 515 | 5 516 | 14 517 | 518 | 519 | 520 | 521 | 522 | 15 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | none 531 | 532 | 533 | 534 | 535 | 536 | 537 | minimal 538 | 539 | 540 | 541 | 542 | 543 |
544 | 545 | 546 | false 547 | 548 | false 549 | false 550 | 551 | 552 | false 553 | 554 | 555 | 556 | 557 | 558 | 559 | true 560 | 561 | private 562 | 563 | 564 | 565 | 566 | 567 | TEST 568 | ACL_IPV4 569 | 570 | TEST 571 | ACL_IPV4 572 | 573 | 574 | 575 | 10 576 | 577 | 10 578 | 579 | 580 | 581 | oc-pkt-match-types:IP_ICMP 582 | 583 | 584 | 585 | 586 | ANY 587 | ANY 588 | 589 | 590 | 591 | 592 | DROP 593 | LOG_NONE 594 | 595 | 596 | 597 | 598 | 20 599 | 600 | 20 601 | 602 | 603 | 604 | oc-acl-cisco:IP 605 | 606 | 607 | 608 | 609 | ANY 610 | ANY 611 | 612 | 613 | 614 | 615 | ACCEPT 616 | LOG_NONE 617 | 618 | 619 | 620 | 621 | 622 | 623 | meraki-fqdn-dns 624 | ACL_IPV4 625 | 626 | meraki-fqdn-dns 627 | ACL_IPV4 628 | 629 | 630 | 631 | 632 | 633 | 634 | GigabitEthernet1 635 | 636 | GigabitEthernet1 637 | ianaift:ethernetCsmacd 638 | true 639 | 640 | 641 | 642 | 0 643 | 644 | 0 645 | true 646 | 647 | 648 | 649 |
650 | 192.168.153.102 651 | 652 | 192.168.153.102 653 | 24 654 | 655 |
656 |
657 |
658 | 659 | 660 | false 661 | 662 | 663 |
664 |
665 | 666 | 667 | 0c:83:a0:5a:a7:00 668 | true 669 | true 670 | 671 | 672 |
673 | 674 | GigabitEthernet2 675 | 676 | GigabitEthernet2 677 | ianaift:ethernetCsmacd 678 | true 679 | 680 | 681 | 682 | 0 683 | 684 | 0 685 | true 686 | 687 | 688 | 689 |
690 | 100.64.12.2 691 | 692 | 100.64.12.2 693 | 24 694 | 695 |
696 |
697 |
698 | 699 | 700 | false 701 | 702 | 703 |
704 |
705 | 706 | 707 | 0c:83:a0:5a:a7:01 708 | true 709 | true 710 | 711 | 712 |
713 | 714 | GigabitEthernet3 715 | 716 | GigabitEthernet3 717 | ianaift:ethernetCsmacd 718 | true 719 | 720 | 721 | 722 | 0 723 | 724 | 0 725 | true 726 | 727 | 728 | 729 |
730 | 100.64.23.2 731 | 732 | 100.64.23.2 733 | 24 734 | 735 |
736 |
737 |
738 | 739 | 740 | false 741 | 742 | 743 |
744 |
745 | 746 | 747 | 0c:83:a0:5a:a7:02 748 | true 749 | true 750 | 751 | 752 |
753 | 754 | GigabitEthernet4 755 | 756 | GigabitEthernet4 757 | ianaift:ethernetCsmacd 758 | false 759 | 760 | 761 | 762 | 0 763 | 764 | 0 765 | false 766 | 767 | 768 | 769 | false 770 | 771 | 772 | 773 | 774 | 775 | 776 | 0c:83:a0:5a:a7:03 777 | true 778 | true 779 | 780 | 781 | 782 | 783 | Loopback0 784 | 785 | Loopback0 786 | ianaift:softwareLoopback 787 | true 788 | 789 | 790 | 791 | 0 792 | 793 | 0 794 | true 795 | 796 | 797 | 798 |
799 | 100.64.2.2 800 | 801 | 100.64.2.2 802 | 32 803 | 804 |
805 |
806 |
807 | 808 | 809 | false 810 | 811 | 812 |
813 |
814 |
815 | 816 | Loopback1 817 | 818 | Loopback1 819 | ianaift:softwareLoopback 820 | true 821 | 822 | 823 | 824 | 0 825 | 826 | 0 827 | true 828 | 829 | 830 | 831 |
832 | 100.70.2.2 833 | 834 | 100.70.2.2 835 | 32 836 | 837 |
838 |
839 |
840 | 841 | 842 | false 843 | 844 | 845 |
846 |
847 |
848 |
849 | 850 | 851 | true 852 | 853 | 854 | 855 | GigabitEthernet1 856 | 857 | GigabitEthernet1 858 | false 859 | 860 | 861 | 862 | GigabitEthernet2 863 | 864 | GigabitEthernet2 865 | true 866 | 867 | 868 | 869 | GigabitEthernet3 870 | 871 | GigabitEthernet3 872 | true 873 | 874 | 875 | 876 | GigabitEthernet4 877 | 878 | GigabitEthernet4 879 | true 880 | 881 | 882 | 883 | 884 | 885 | 886 | Mgmt-vrf 887 | 888 | Mgmt-vrf 889 | oc-ni-types:L3VRF 890 | oc-types:IPV6 891 | oc-types:IPV4 892 | 893 | 894 | 895 | GigabitEthernet1 896 | 897 | GigabitEthernet1 898 | GigabitEthernet1 899 | 900 | 901 | 902 | 903 | 904 | oc-pol-types:DIRECTLY_CONNECTED 905 | oc-types:IPV4 906 | 907 | oc-pol-types:DIRECTLY_CONNECTED 908 | oc-types:IPV4 909 | 910 |
911 | 912 | oc-pol-types:DIRECTLY_CONNECTED 913 | oc-types:IPV6 914 | 915 | oc-pol-types:DIRECTLY_CONNECTED 916 | oc-types:IPV6 917 | 918 |
919 | 920 | oc-pol-types:STATIC 921 | oc-types:IPV4 922 | 923 | oc-pol-types:STATIC 924 | oc-types:IPV4 925 | 926 |
927 | 928 | oc-pol-types:STATIC 929 | oc-types:IPV6 930 | 931 | oc-pol-types:STATIC 932 | oc-types:IPV6 933 | 934 |
935 |
936 | 937 | 938 | oc-pol-types:STATIC 939 | DEFAULT 940 | 941 | oc-pol-types:STATIC 942 | DEFAULT 943 | 944 | 945 | 946 | 0.0.0.0/0 947 | 948 | 0.0.0.0/0 949 | 950 | 951 | 952 | 192.168.153.1 953 | 954 | 192.168.153.1 955 | 192.168.153.1 956 | 957 | 958 | 959 | 960 | 961 | 962 | 963 | oc-pol-types:DIRECTLY_CONNECTED 964 | DEFAULT 965 | 966 | oc-pol-types:DIRECTLY_CONNECTED 967 | DEFAULT 968 | 969 | 970 | 971 |
972 | 973 | default 974 | 975 | default 976 | oc-ni-types:DEFAULT_INSTANCE 977 | default-vrf [read-only] 978 | 979 | 980 | 981 | oc-pol-types:DIRECTLY_CONNECTED 982 | oc-types:IPV4 983 | 984 | oc-pol-types:DIRECTLY_CONNECTED 985 | oc-types:IPV4 986 | 987 |
988 | 989 | oc-pol-types:DIRECTLY_CONNECTED 990 | oc-types:IPV6 991 | 992 | oc-pol-types:DIRECTLY_CONNECTED 993 | oc-types:IPV6 994 | 995 |
996 | 997 | oc-pol-types:OSPF 998 | oc-types:IPV4 999 | 1000 | oc-pol-types:OSPF 1001 | oc-types:IPV4 1002 | 1003 |
1004 | 1005 | oc-pol-types:STATIC 1006 | oc-types:IPV4 1007 | 1008 | oc-pol-types:STATIC 1009 | oc-types:IPV4 1010 | 1011 |
1012 | 1013 | oc-pol-types:STATIC 1014 | oc-types:IPV6 1015 | 1016 | oc-pol-types:STATIC 1017 | oc-types:IPV6 1018 | 1019 |
1020 |
1021 | 1022 | 1023 | oc-pol-types:BGP 1024 | 100 1025 | 1026 | oc-pol-types:BGP 1027 | 100 1028 | 1029 | 1030 | 1031 | 1032 | 100 1033 | 2.2.2.2 1034 | 1035 | 1036 | 1037 | false 1038 | 1039 | 1040 | 1041 | 1042 | false 1043 | true 1044 | 1045 | 1046 | 1047 | 1048 | 1049 | 100.64.1.1 1050 | 1051 | 100.64.1.1 1052 | 100 1053 | 1054 | 1055 | 1056 | 180.0 1057 | 60.0 1058 | 1059 | 1060 | 1061 | 1062 | false 1063 | 1064 | 1065 | 1066 | 1067 | 1068 | 1069 | 1070 | oc-pol-types:OSPF 1071 | 1 1072 | 1073 | oc-pol-types:OSPF 1074 | 1 1075 | 1076 | 1077 | 1078 | oc-pol-types:STATIC 1079 | DEFAULT 1080 | 1081 | oc-pol-types:STATIC 1082 | DEFAULT 1083 | 1084 | 1085 | 1086 | oc-pol-types:DIRECTLY_CONNECTED 1087 | DEFAULT 1088 | 1089 | oc-pol-types:DIRECTLY_CONNECTED 1090 | DEFAULT 1091 | 1092 | 1093 | 1094 |
1095 |
1096 | 1097 | 1098 | GigabitEthernet1 1099 | ianaift:ethernetCsmacd 1100 | true 1101 | 1102 |
1103 | 192.168.153.102 1104 | 255.255.255.0 1105 |
1106 |
1107 | 1108 |
1109 | 1110 | GigabitEthernet2 1111 | ianaift:ethernetCsmacd 1112 | true 1113 | 1114 |
1115 | 100.64.12.2 1116 | 255.255.255.0 1117 |
1118 |
1119 | 1120 |
1121 | 1122 | GigabitEthernet3 1123 | ianaift:ethernetCsmacd 1124 | true 1125 | 1126 |
1127 | 100.64.23.2 1128 | 255.255.255.0 1129 |
1130 |
1131 | 1132 |
1133 | 1134 | GigabitEthernet4 1135 | ianaift:ethernetCsmacd 1136 | false 1137 | 1138 | 1139 | 1140 | 1141 | Loopback0 1142 | ianaift:softwareLoopback 1143 | true 1144 | 1145 |
1146 | 100.64.2.2 1147 | 255.255.255.255 1148 |
1149 |
1150 | 1151 |
1152 | 1153 | Loopback1 1154 | ianaift:softwareLoopback 1155 | true 1156 | 1157 |
1158 | 100.70.2.2 1159 | 255.255.255.255 1160 |
1161 |
1162 | 1163 |
1164 |
1165 | 1166 | true 1167 | deny 1168 | deny 1169 | deny 1170 | true 1171 | 1172 | admin 1173 | PRIV15 1174 | 1175 | permit-all 1176 | * 1177 | * 1178 | permit 1179 | 1180 | 1181 | 1182 | 1183 | 1184 | Mgmt-vrf 1185 | 1186 | GigabitEthernet1 1187 | 1188 | 1189 | 1190 | static 1191 | 1 1192 | 1193 | 1194 | 1195 | 0.0.0.0/0 1196 | 1197 | 192.168.153.1 1198 | 1199 | 1200 | 1201 | 1202 | 1203 | 1204 | 1205 | 1206 | default 1207 | default-vrf [read-only] 1208 | 1209 | 1210 | ospf:ospfv2 1211 | 1 1212 | 1213 | 1214 | rt:ipv4 1215 | 2.2.2.2 1216 | 1217 | false 1218 | 1219 | 1220 | true 1221 | 100 1222 | 1223 | 1224 | 1225 | 50 1226 | 200 1227 | 5000 1228 | 1229 | 1230 | 1231 | 1232 | 1233 | 1234 | 1235 | 1236 | 1237 | static 1238 | 1 1239 | 1240 | 1241 | 1242 | 1243 |
1244 | -------------------------------------------------------------------------------- /notebooks/basics.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Automate your network with Nornir – Python automation framework!" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "### Exploring inventory" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "from pprint import pprint\n", 24 | "from colorama import Fore\n", 25 | "import time\n", 26 | "\n", 27 | "from nornir import InitNornir\n", 28 | "nr = InitNornir(config_file=\"config.yaml\")\n", 29 | "pprint(nr.inventory.hosts)" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "pprint(nr.inventory.groups)" 39 | ] 40 | }, 41 | { 42 | "cell_type": "markdown", 43 | "metadata": {}, 44 | "source": [ 45 | "### Simple output collection with netmiko" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": null, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "from nornir.plugins.functions.text import print_result\n", 55 | "from nornir.plugins.tasks.networking import netmiko_send_command\n", 56 | "\n", 57 | "results = nr.run(task=netmiko_send_command, command_string=\"show ip int brief | ex una\")\n", 58 | "print_result(results)" 59 | ] 60 | }, 61 | { 62 | "cell_type": "markdown", 63 | "metadata": {}, 64 | "source": [ 65 | "### ...or the commands parsed with TextFSM" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": null, 71 | "metadata": {}, 72 | "outputs": [], 73 | "source": [ 74 | "results = nr.run(task=netmiko_send_command, command_string=\"show version\", use_textfsm=True)\n", 75 | "print_result(results)" 76 | ] 77 | }, 78 | { 79 | "cell_type": "markdown", 80 | "metadata": {}, 81 | "source": [ 82 | "### Simple data retrieval using napalm" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": null, 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "from nornir.plugins.tasks.networking import napalm_get\n", 92 | "results = nr.run(\n", 93 | " task=napalm_get, getters=[\"facts\", \"interfaces\"]\n", 94 | ")\n", 95 | "print_result(results)" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "metadata": {}, 101 | "source": [ 102 | "### Exploring connections" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": null, 108 | "metadata": {}, 109 | "outputs": [], 110 | "source": [ 111 | "for host in nr.inventory.hosts.values():\n", 112 | " print(f\"{host.name} connections: {host.connections}\")\n", 113 | " \n", 114 | "nr.close_connections()\n", 115 | "print(f\"{Fore.RED}All connections have been closed{Fore.RESET}\", end=\"\\n\\n\")\n", 116 | "\n", 117 | "for host in nr.inventory.hosts.values():\n", 118 | " print(f\"{host.name} connections: {host.connections}\")" 119 | ] 120 | }, 121 | { 122 | "cell_type": "markdown", 123 | "metadata": {}, 124 | "source": [ 125 | "### Data retrieval" 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": null, 131 | "metadata": {}, 132 | "outputs": [], 133 | "source": [ 134 | "r1 = nr.inventory.hosts['R1']\n", 135 | "print(r1.data)" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": null, 141 | "metadata": {}, 142 | "outputs": [], 143 | "source": [ 144 | "print(r1['tags']) # directly from data\n", 145 | "print(r1['ntp']) # from group\n", 146 | "print(r1['snmp_community']) # from defaults\n", 147 | "print(r1.get('non-existent-key', 'Placeholder')) # this key does not exist in any group" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "metadata": {}, 153 | "source": [ 154 | "### Change data dynamically" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": null, 160 | "metadata": {}, 161 | "outputs": [], 162 | "source": [ 163 | "# Settings site and locator for every host\n", 164 | "for host in nr.inventory.hosts.values():\n", 165 | " site = host.groups[0]\n", 166 | " host.data['site'] = site\n", 167 | " locator = f'{host.name}.{site}'\n", 168 | " host.data['locator'] = locator\n", 169 | "\n", 170 | "r1 = nr.inventory.hosts['R1']\n", 171 | "print(f\"{r1.name} has the following data: {r1.data}\")\n", 172 | "print(f\"{r1.name} site: {r1['site']}, locator: {r1['locator']}\")" 173 | ] 174 | }, 175 | { 176 | "cell_type": "markdown", 177 | "metadata": {}, 178 | "source": [ 179 | "### Filtering" 180 | ] 181 | }, 182 | { 183 | "cell_type": "code", 184 | "execution_count": null, 185 | "metadata": {}, 186 | "outputs": [], 187 | "source": [ 188 | "print(list(nr.filter(locator=\"R1.New York\").inventory.hosts.keys()))\n", 189 | "print(list(nr.filter(site=\"New York\").inventory.hosts.keys()))" 190 | ] 191 | }, 192 | { 193 | "cell_type": "markdown", 194 | "metadata": {}, 195 | "source": [ 196 | "#### Advanced filtering" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": null, 202 | "metadata": {}, 203 | "outputs": [], 204 | "source": [ 205 | "from nornir.core.filter import F\n", 206 | "\n", 207 | "print(list(nr.filter(F(locator=\"R1.New York\")).inventory.hosts.keys()))\n", 208 | "print(list(nr.filter(F(groups__contains=\"London\")).inventory.hosts.keys()))\n", 209 | "print(list(nr.filter(F(groups__contains=\"London\") & F(tags__contains=\"isr4400\")).inventory.hosts.keys()))\n", 210 | "print(list(nr.filter(F(groups__contains=\"London\") & F(tags__all=[\"isr4400\", \"edge\"])).inventory.hosts.keys()))\n", 211 | "print(list(nr.filter(F(ntp__servers__contains=\"1.2.3.4\")).inventory.hosts.keys()))" 212 | ] 213 | }, 214 | { 215 | "cell_type": "markdown", 216 | "metadata": {}, 217 | "source": [ 218 | "### Combining filtering and task execution\n" 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": null, 224 | "metadata": {}, 225 | "outputs": [], 226 | "source": [ 227 | "from nornir.plugins.functions.text import print_result\n", 228 | "from nornir.plugins.tasks.networking import netmiko_send_command\n", 229 | "\n", 230 | "london_devices = nr.filter(F(groups__contains=\"London\"))\n", 231 | "result = london_devices.run(task=netmiko_send_command, command_string=\"show ip route\")\n", 232 | "print_result(result)" 233 | ] 234 | }, 235 | { 236 | "cell_type": "markdown", 237 | "metadata": {}, 238 | "source": [ 239 | "### Custom tasks" 240 | ] 241 | }, 242 | { 243 | "cell_type": "code", 244 | "execution_count": null, 245 | "metadata": {}, 246 | "outputs": [], 247 | "source": [ 248 | "from nornir.plugins.functions.text import print_result\n", 249 | "from nornir.plugins.tasks.networking import netmiko_send_command\n", 250 | "\n", 251 | "def get_commands(task, commands):\n", 252 | " for command in commands:\n", 253 | " task.run(task=netmiko_send_command, command_string=command)\n", 254 | " \n", 255 | "london_devices = nr.filter(F(groups__contains=\"London\"))\n", 256 | "result = london_devices.run(task=get_commands, commands=[\"show ip int br\", \"show arp\"])\n", 257 | "print_result(result)\n", 258 | "nr.close_connections()" 259 | ] 260 | }, 261 | { 262 | "cell_type": "markdown", 263 | "metadata": {}, 264 | "source": [ 265 | "## Building network diagram with Nornir" 266 | ] 267 | }, 268 | { 269 | "cell_type": "code", 270 | "execution_count": null, 271 | "metadata": {}, 272 | "outputs": [], 273 | "source": [ 274 | "%matplotlib inline\n", 275 | "\n", 276 | "from typing import List, Dict, Tuple\n", 277 | "import time\n", 278 | "\n", 279 | "from colorama import Fore\n", 280 | "from nornir import InitNornir\n", 281 | "import networkx as nx\n", 282 | "import matplotlib.pyplot as plt\n", 283 | "\n", 284 | "from topology import parse_cdp_neighbors, build_graph\n", 285 | "\n", 286 | "TOPOLOGY_FILENAME = \"topology.png\"\n", 287 | "\n", 288 | "def draw_and_save_topology(graph: nx.Graph, edge_labels: List[Dict[Tuple[str, str], str]]) -> None:\n", 289 | " plt.figure(1, figsize=(12, 12))\n", 290 | " pos = nx.spring_layout(graph, seed=5)\n", 291 | " nx.draw_networkx(graph, pos, node_size=1300, node_color='orange')\n", 292 | " nx.draw_networkx_edge_labels(graph, pos, edge_labels=edge_labels[0], label_pos=0.8)\n", 293 | " nx.draw_networkx_edge_labels(graph, pos, edge_labels=edge_labels[1], label_pos=0.2)\n", 294 | " plt.savefig(TOPOLOGY_FILENAME)\n", 295 | " print(f\"The network topology diagram has been saved to {TOPOLOGY_FILENAME}\")\n", 296 | "\n", 297 | "start_time = time.time()\n", 298 | "nr = InitNornir(\"config.yaml\")\n", 299 | "nr.run(task=parse_cdp_neighbors)\n", 300 | "print(\"CDP details were successfully fetched using RESTCONF\")\n", 301 | "milestone = time.time()\n", 302 | "time_to_run = milestone - start_time\n", 303 | "print(f\"{Fore.RED}It took {time_to_run:.2f} seconds to get and parse CDP details{Fore.RESET}\")\n", 304 | "graph, edge_labels = build_graph(nr.inventory.hosts.values())\n", 305 | "draw_and_save_topology(graph, edge_labels)\n", 306 | "time_to_run = time.time() - milestone\n", 307 | "print(f\"{Fore.RED}It took additional {time_to_run:.2f} seconds to draw and save the network topology{Fore.RESET}\")" 308 | ] 309 | }, 310 | { 311 | "cell_type": "markdown", 312 | "metadata": {}, 313 | "source": [ 314 | "## END" 315 | ] 316 | } 317 | ], 318 | "metadata": { 319 | "kernelspec": { 320 | "display_name": "Python 3", 321 | "language": "python", 322 | "name": "python3" 323 | }, 324 | "language_info": { 325 | "codemirror_mode": { 326 | "name": "ipython", 327 | "version": 3 328 | }, 329 | "file_extension": ".py", 330 | "mimetype": "text/x-python", 331 | "name": "python", 332 | "nbconvert_exporter": "python", 333 | "pygments_lexer": "ipython3", 334 | "version": "3.6.8" 335 | } 336 | }, 337 | "nbformat": 4, 338 | "nbformat_minor": 2 339 | } 340 | -------------------------------------------------------------------------------- /notebooks/build_diagram.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Automate your network with Nornir – Python automation framework!" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "### Building network diagram" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 2, 20 | "metadata": {}, 21 | "outputs": [ 22 | { 23 | "name": "stdout", 24 | "output_type": "stream", 25 | "text": [ 26 | "{'R1': Host: R1,\n", 27 | " 'R10': Host: R10,\n", 28 | " 'R2': Host: R2,\n", 29 | " 'R3': Host: R3,\n", 30 | " 'R4': Host: R4,\n", 31 | " 'R5': Host: R5,\n", 32 | " 'R6': Host: R6,\n", 33 | " 'R7': Host: R7,\n", 34 | " 'R8': Host: R8,\n", 35 | " 'R9': Host: R9}\n" 36 | ] 37 | } 38 | ], 39 | "source": [ 40 | "import os\n", 41 | "from pprint import pprint\n", 42 | "from colorama import Fore\n", 43 | "import time\n", 44 | "\n", 45 | "from nornir import InitNornir\n", 46 | "os.chdir('..')\n", 47 | "\n", 48 | "with InitNornir(config_file=\"scripts/config.yaml\") as nr:\n", 49 | " \n", 50 | "\n" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": null, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "from nornir.plugins.functions.text import print_result\n", 60 | "from nornir.plugins.tasks.networking import netmiko_send_command\n", 61 | "\n", 62 | "london_devices = nr.filter(F(groups__contains=\"London\"))\n", 63 | "result = london_devices.run(task=netmiko_send_command, command_string=\"show ip route\")\n", 64 | "print_result(result)" 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "metadata": {}, 70 | "source": [ 71 | "### Custom tasks" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": null, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "from nornir.plugins.functions.text import print_result\n", 81 | "from nornir.plugins.tasks.networking import netmiko_send_command\n", 82 | "\n", 83 | "def get_commands(task, commands):\n", 84 | " for command in commands:\n", 85 | " task.run(task=netmiko_send_command, command_string=command)\n", 86 | " \n", 87 | "london_devices = nr.filter(F(groups__contains=\"London\"))\n", 88 | "result = london_devices.run(task=get_commands, commands=[\"show ip int br\", \"show arp\"])\n", 89 | "print_result(result)\n", 90 | "nr.close_connections()" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "## Building network diagram with Nornir" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "%matplotlib inline\n", 107 | "\n", 108 | "from typing import List, Dict, Tuple\n", 109 | "import time\n", 110 | "\n", 111 | "from colorama import Fore\n", 112 | "from nornir import InitNornir\n", 113 | "import networkx as nx\n", 114 | "import matplotlib.pyplot as plt\n", 115 | "\n", 116 | "from topology import parse_cdp_neighbors, build_graph\n", 117 | "\n", 118 | "TOPOLOGY_FILENAME = \"topology.png\"\n", 119 | "\n", 120 | "def draw_and_save_topology(graph: nx.Graph, edge_labels: List[Dict[Tuple[str, str], str]]) -> None:\n", 121 | " plt.figure(1, figsize=(12, 12))\n", 122 | " pos = nx.spring_layout(graph, seed=5)\n", 123 | " nx.draw_networkx(graph, pos, node_size=1300, node_color='orange')\n", 124 | " nx.draw_networkx_edge_labels(graph, pos, edge_labels=edge_labels[0], label_pos=0.8)\n", 125 | " nx.draw_networkx_edge_labels(graph, pos, edge_labels=edge_labels[1], label_pos=0.2)\n", 126 | " plt.savefig(TOPOLOGY_FILENAME)\n", 127 | " print(f\"The network topology diagram has been saved to {TOPOLOGY_FILENAME}\")\n", 128 | "\n", 129 | "start_time = time.time()\n", 130 | "nr = InitNornir(\"config.yaml\")\n", 131 | "nr.run(task=parse_cdp_neighbors)\n", 132 | "print(\"CDP details were successfully fetched using RESTCONF\")\n", 133 | "milestone = time.time()\n", 134 | "time_to_run = milestone - start_time\n", 135 | "print(f\"{Fore.RED}It took {time_to_run:.2f} seconds to get and parse CDP details{Fore.RESET}\")\n", 136 | "graph, edge_labels = build_graph(nr.inventory.hosts.values())\n", 137 | "draw_and_save_topology(graph, edge_labels)\n", 138 | "time_to_run = time.time() - milestone\n", 139 | "print(f\"{Fore.RED}It took additional {time_to_run:.2f} seconds to draw and save the network topology{Fore.RESET}\")" 140 | ] 141 | }, 142 | { 143 | "cell_type": "markdown", 144 | "metadata": {}, 145 | "source": [ 146 | "## END" 147 | ] 148 | } 149 | ], 150 | "metadata": { 151 | "kernelspec": { 152 | "display_name": "Python 3", 153 | "language": "python", 154 | "name": "python3" 155 | }, 156 | "language_info": { 157 | "codemirror_mode": { 158 | "name": "ipython", 159 | "version": 3 160 | }, 161 | "file_extension": ".py", 162 | "mimetype": "text/x-python", 163 | "name": "python", 164 | "nbconvert_exporter": "python", 165 | "pygments_lexer": "ipython3", 166 | "version": "3.7.5" 167 | } 168 | }, 169 | "nbformat": 4, 170 | "nbformat_minor": 2 171 | } 172 | -------------------------------------------------------------------------------- /notebooks/workshop.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Automate your network with Nornir – Python automation framework!" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "### Exploring inventory" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "import os\n", 24 | "os." 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "from pprint import pprint\n", 34 | "from colorama import Fore\n", 35 | "import time\n", 36 | "\n", 37 | "from nornir import InitNornir\n", 38 | "nr = InitNornir(config_file=\"config.yaml\")\n", 39 | "pprint(nr.inventory.hosts)" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": null, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "pprint(nr.inventory.groups)" 49 | ] 50 | }, 51 | { 52 | "cell_type": "markdown", 53 | "metadata": {}, 54 | "source": [ 55 | "### Simple output collection with netmiko" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": null, 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "from nornir.plugins.functions.text import print_result\n", 65 | "from nornir.plugins.tasks.networking import netmiko_send_command\n", 66 | "\n", 67 | "results = nr.run(task=netmiko_send_command, command_string=\"show ip int brief | ex una\")\n", 68 | "print_result(results)" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "metadata": {}, 74 | "source": [ 75 | "### ...or the commands parsed with TextFSM" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": null, 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "results = nr.run(task=netmiko_send_command, command_string=\"show version\", use_textfsm=True)\n", 85 | "print_result(results)" 86 | ] 87 | }, 88 | { 89 | "cell_type": "markdown", 90 | "metadata": {}, 91 | "source": [ 92 | "### Simple data retrieval using napalm" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": null, 98 | "metadata": {}, 99 | "outputs": [], 100 | "source": [ 101 | "from nornir.plugins.tasks.networking import napalm_get\n", 102 | "results = nr.run(\n", 103 | " task=napalm_get, getters=[\"facts\", \"interfaces\"]\n", 104 | ")\n", 105 | "print_result(results)" 106 | ] 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "metadata": {}, 111 | "source": [ 112 | "### Exploring connections" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": null, 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [ 121 | "for host in nr.inventory.hosts.values():\n", 122 | " print(f\"{host.name} connections: {host.connections}\")\n", 123 | " \n", 124 | "nr.close_connections()\n", 125 | "print(f\"{Fore.RED}All connections have been closed{Fore.RESET}\", end=\"\\n\\n\")\n", 126 | "\n", 127 | "for host in nr.inventory.hosts.values():\n", 128 | " print(f\"{host.name} connections: {host.connections}\")" 129 | ] 130 | }, 131 | { 132 | "cell_type": "markdown", 133 | "metadata": {}, 134 | "source": [ 135 | "### Data retrieval" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": null, 141 | "metadata": {}, 142 | "outputs": [], 143 | "source": [ 144 | "r1 = nr.inventory.hosts['R1']\n", 145 | "print(r1.data)" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": null, 151 | "metadata": {}, 152 | "outputs": [], 153 | "source": [ 154 | "print(r1['tags']) # directly from data\n", 155 | "print(r1['ntp']) # from group\n", 156 | "print(r1['snmp_community']) # from defaults\n", 157 | "print(r1.get('non-existent-key', 'Placeholder')) # this key does not exist in any group" 158 | ] 159 | }, 160 | { 161 | "cell_type": "markdown", 162 | "metadata": {}, 163 | "source": [ 164 | "### Change data dynamically" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": null, 170 | "metadata": {}, 171 | "outputs": [], 172 | "source": [ 173 | "# Settings site and locator for every host\n", 174 | "for host in nr.inventory.hosts.values():\n", 175 | " site = host.groups[0]\n", 176 | " host.data['site'] = site\n", 177 | " locator = f'{host.name}.{site}'\n", 178 | " host.data['locator'] = locator\n", 179 | "\n", 180 | "r1 = nr.inventory.hosts['R1']\n", 181 | "print(f\"{r1.name} has the following data: {r1.data}\")\n", 182 | "print(f\"{r1.name} site: {r1['site']}, locator: {r1['locator']}\")" 183 | ] 184 | }, 185 | { 186 | "cell_type": "markdown", 187 | "metadata": {}, 188 | "source": [ 189 | "### Filtering" 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": null, 195 | "metadata": {}, 196 | "outputs": [], 197 | "source": [ 198 | "print(list(nr.filter(locator=\"R1.New York\").inventory.hosts.keys()))\n", 199 | "print(list(nr.filter(site=\"New York\").inventory.hosts.keys()))" 200 | ] 201 | }, 202 | { 203 | "cell_type": "markdown", 204 | "metadata": {}, 205 | "source": [ 206 | "#### Advanced filtering" 207 | ] 208 | }, 209 | { 210 | "cell_type": "code", 211 | "execution_count": null, 212 | "metadata": {}, 213 | "outputs": [], 214 | "source": [ 215 | "from nornir.core.filter import F\n", 216 | "\n", 217 | "print(list(nr.filter(F(locator=\"R1.New York\")).inventory.hosts.keys()))\n", 218 | "print(list(nr.filter(F(groups__contains=\"London\")).inventory.hosts.keys()))\n", 219 | "print(list(nr.filter(F(groups__contains=\"London\") & F(tags__contains=\"isr4400\")).inventory.hosts.keys()))\n", 220 | "print(list(nr.filter(F(groups__contains=\"London\") & F(tags__all=[\"isr4400\", \"edge\"])).inventory.hosts.keys()))\n", 221 | "print(list(nr.filter(F(ntp__servers__contains=\"1.2.3.4\")).inventory.hosts.keys()))" 222 | ] 223 | }, 224 | { 225 | "cell_type": "markdown", 226 | "metadata": {}, 227 | "source": [ 228 | "### Combining filtering and task execution\n" 229 | ] 230 | }, 231 | { 232 | "cell_type": "code", 233 | "execution_count": null, 234 | "metadata": {}, 235 | "outputs": [], 236 | "source": [ 237 | "from nornir.plugins.functions.text import print_result\n", 238 | "from nornir.plugins.tasks.networking import netmiko_send_command\n", 239 | "\n", 240 | "london_devices = nr.filter(F(groups__contains=\"London\"))\n", 241 | "result = london_devices.run(task=netmiko_send_command, command_string=\"show ip route\")\n", 242 | "print_result(result)" 243 | ] 244 | }, 245 | { 246 | "cell_type": "markdown", 247 | "metadata": {}, 248 | "source": [ 249 | "### Custom tasks" 250 | ] 251 | }, 252 | { 253 | "cell_type": "code", 254 | "execution_count": null, 255 | "metadata": {}, 256 | "outputs": [], 257 | "source": [ 258 | "from nornir.plugins.functions.text import print_result\n", 259 | "from nornir.plugins.tasks.networking import netmiko_send_command\n", 260 | "\n", 261 | "def get_commands(task, commands):\n", 262 | " for command in commands:\n", 263 | " task.run(task=netmiko_send_command, command_string=command)\n", 264 | " \n", 265 | "london_devices = nr.filter(F(groups__contains=\"London\"))\n", 266 | "result = london_devices.run(task=get_commands, commands=[\"show ip int br\", \"show arp\"])\n", 267 | "print_result(result)\n", 268 | "nr.close_connections()" 269 | ] 270 | }, 271 | { 272 | "cell_type": "markdown", 273 | "metadata": {}, 274 | "source": [ 275 | "## Building network diagram with Nornir" 276 | ] 277 | }, 278 | { 279 | "cell_type": "code", 280 | "execution_count": null, 281 | "metadata": {}, 282 | "outputs": [], 283 | "source": [ 284 | "%matplotlib inline\n", 285 | "\n", 286 | "from typing import List, Dict, Tuple\n", 287 | "import time\n", 288 | "\n", 289 | "from colorama import Fore\n", 290 | "from nornir import InitNornir\n", 291 | "import networkx as nx\n", 292 | "import matplotlib.pyplot as plt\n", 293 | "\n", 294 | "from topology import parse_cdp_neighbors, build_graph\n", 295 | "\n", 296 | "TOPOLOGY_FILENAME = \"topology.png\"\n", 297 | "\n", 298 | "def draw_and_save_topology(graph: nx.Graph, edge_labels: List[Dict[Tuple[str, str], str]]) -> None:\n", 299 | " plt.figure(1, figsize=(12, 12))\n", 300 | " pos = nx.spring_layout(graph, seed=5)\n", 301 | " nx.draw_networkx(graph, pos, node_size=1300, node_color='orange')\n", 302 | " nx.draw_networkx_edge_labels(graph, pos, edge_labels=edge_labels[0], label_pos=0.8)\n", 303 | " nx.draw_networkx_edge_labels(graph, pos, edge_labels=edge_labels[1], label_pos=0.2)\n", 304 | " plt.savefig(TOPOLOGY_FILENAME)\n", 305 | " print(f\"The network topology diagram has been saved to {TOPOLOGY_FILENAME}\")\n", 306 | "\n", 307 | "start_time = time.time()\n", 308 | "nr = InitNornir(\"config.yaml\")\n", 309 | "nr.run(task=parse_cdp_neighbors)\n", 310 | "print(\"CDP details were successfully fetched using RESTCONF\")\n", 311 | "milestone = time.time()\n", 312 | "time_to_run = milestone - start_time\n", 313 | "print(f\"{Fore.RED}It took {time_to_run:.2f} seconds to get and parse CDP details{Fore.RESET}\")\n", 314 | "graph, edge_labels = build_graph(nr.inventory.hosts.values())\n", 315 | "draw_and_save_topology(graph, edge_labels)\n", 316 | "time_to_run = time.time() - milestone\n", 317 | "print(f\"{Fore.RED}It took additional {time_to_run:.2f} seconds to draw and save the network topology{Fore.RESET}\")" 318 | ] 319 | }, 320 | { 321 | "cell_type": "markdown", 322 | "metadata": {}, 323 | "source": [ 324 | "## END" 325 | ] 326 | } 327 | ], 328 | "metadata": { 329 | "kernelspec": { 330 | "display_name": "Python 3", 331 | "language": "python", 332 | "name": "python3" 333 | }, 334 | "language_info": { 335 | "codemirror_mode": { 336 | "name": "ipython", 337 | "version": 3 338 | }, 339 | "file_extension": ".py", 340 | "mimetype": "text/x-python", 341 | "name": "python", 342 | "nbconvert_exporter": "python", 343 | "pygments_lexer": "ipython3", 344 | "version": "3.6.8" 345 | } 346 | }, 347 | "nbformat": 4, 348 | "nbformat_minor": 2 349 | } -------------------------------------------------------------------------------- /nr-config-local.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | runners: 3 | plugin: threaded 4 | options: 5 | num_workers: 10 6 | 7 | logging: 8 | enabled: False 9 | 10 | inventory: 11 | plugin: SimpleInventory 12 | options: 13 | host_file: "inventories/inventory-10-csr-local/hosts.yaml" 14 | group_file: "inventories/inventory-10-csr-local/groups.yaml" 15 | defaults_file: "inventories/inventory-10-csr-local/defaults.yaml" 16 | -------------------------------------------------------------------------------- /nr_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmfigol/nornir-apps/a6c0dae756cbe971e743359a1ca0a0a1d6c18380/nr_app/__init__.py -------------------------------------------------------------------------------- /nr_app/constants.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | RESTCONF_ROOT = "https://{host}/restconf/data" 4 | CDP_NEIGHBORS_ENDPOINT = "/cdp-neighbor-details/cdp-neighbor-detail" 5 | OPENCONFIG_LLDP_NEIGHBORS_ENDPOINT = "/lldp/interfaces/interface" 6 | RESTCONF_HEADERS = { 7 | "Accept": "application/yang-data+json", 8 | "Content-Type": "application/yang-data+json", 9 | } 10 | 11 | NORMALIZED_INTERFACES = ( 12 | "FastEthernet", 13 | "GigabitEthernet", 14 | "TenGigabitEthernet", 15 | "FortyGigabitEthernet", 16 | "Ethernet", 17 | "Loopback", 18 | "Serial", 19 | "Vlan", 20 | "Tunnel", 21 | "Portchannel", 22 | "Management", 23 | ) 24 | 25 | INTERFACE_NAME_RE = re.compile( 26 | r"(?P[a-zA-Z\-_ ]*)(?P[\d.\/]*)" 27 | ) 28 | 29 | 30 | LOGGING_DICT = { 31 | "version": 1, 32 | "disable_existing_loggers": False, 33 | "formatters": { 34 | "std-module": { 35 | "format": "[%(asctime)s] %(levelname)-8s {%(name)s:%(lineno)d} %(message)s" 36 | }, 37 | "std": { 38 | "format": "[%(asctime)s] %(levelname)-8s {%(filename)s:%(lineno)d} %(message)s" 39 | }, 40 | }, 41 | "handlers": { 42 | "file-module": { 43 | "level": "DEBUG", 44 | "class": "logging.handlers.RotatingFileHandler", 45 | "filename": "app.log", 46 | "maxBytes": 1024 * 1024 * 5, 47 | "backupCount": 5, 48 | "formatter": "std-module", 49 | }, 50 | "file": { 51 | "level": "DEBUG", 52 | "class": "logging.handlers.RotatingFileHandler", 53 | "filename": "app.log", 54 | "maxBytes": 1024 * 1024 * 5, 55 | "backupCount": 5, 56 | "formatter": "std", 57 | }, 58 | "console-module": { 59 | "level": "DEBUG", 60 | "class": "logging.StreamHandler", 61 | "stream": "ext://sys.stdout", 62 | "formatter": "std-module", 63 | }, 64 | "console": { 65 | "level": "DEBUG", 66 | "class": "logging.StreamHandler", 67 | "stream": "ext://sys.stdout", 68 | "formatter": "std", 69 | }, 70 | }, 71 | "loggers": { 72 | "nornir": { 73 | "handlers": ["console-module", "file-module"], 74 | "level": "WARNING", 75 | "propagate": False, 76 | }, 77 | "netmiko": { 78 | "handlers": ["console-module", "file-module"], 79 | "level": "WARNING", 80 | "propagate": False, 81 | }, 82 | "paramiko": { 83 | "handlers": ["console-module", "file-module"], 84 | "level": "WARNING", 85 | "propagate": False, 86 | }, 87 | # "": { 88 | # "handlers": ["console", "default"], 89 | # "level": "DEBUG", 90 | # "propagate": False 91 | # } 92 | }, 93 | "root": {"handlers": ["console", "file"], "level": "INFO"}, 94 | } 95 | -------------------------------------------------------------------------------- /nr_app/interface.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Optional, Tuple, List 3 | 4 | from nr_app.link import Link 5 | 6 | 7 | INTERFACE_NAME_RE = re.compile( 8 | r"(?P[a-zA-Z\-_ ]*)(?P[\d.\/]*)" 9 | ) 10 | 11 | NORMALIZED_INTERFACES = ( 12 | "FastEthernet", 13 | "GigabitEthernet", 14 | "TenGigabitEthernet", 15 | "FortyGigabitEthernet", 16 | "Ethernet", 17 | "Loopback", 18 | "Serial", 19 | "Vlan", 20 | "Tunnel", 21 | "Portchannel", 22 | "Management", 23 | ) 24 | 25 | 26 | class Interface: 27 | def __init__(self, name: str, device_name: Optional[str] = None) -> None: 28 | self.type, self.num = self.normalize_interface_name(name) 29 | self.device_name = device_name 30 | # self.connected_to: List["Interface"] = [] 31 | 32 | def __repr__(self) -> str: 33 | return ( 34 | f"{self.__class__.__qualname__}(" 35 | f"name={self.name!r}, " 36 | f"device_name={self.device_name!r})" 37 | ) 38 | 39 | def __str__(self) -> str: 40 | return f"{self.device_name}:{self.name}" 41 | 42 | def __lt__(self, other) -> bool: 43 | return (self.device_name, self.name) < (other.device_name, other.name) 44 | 45 | def __eq__(self, other) -> bool: 46 | return (self.name, self.device_name) == (other.name, other.device_name) 47 | 48 | def __hash__(self) -> int: 49 | return hash((self.name, self.device_name)) 50 | 51 | @property 52 | def name(self) -> str: 53 | return self.type + self.num 54 | 55 | @property 56 | def short_name(self) -> str: 57 | return self.type[:2] + self.num 58 | 59 | def link_from_neighbors(self) -> Link: 60 | interfaces = [self, *self.neighbors] 61 | return Link(interfaces) 62 | 63 | @staticmethod 64 | def normalize_interface_name(interface_name: str) -> Tuple[str, str]: 65 | """Normalizes interface name 66 | 67 | For example: 68 | Gi0/1 is converted to GigabitEthernet1 69 | Te1/1 is converted to TenGigabitEthernet1/1 70 | """ 71 | match = INTERFACE_NAME_RE.search(interface_name) 72 | if match: 73 | int_type = match.group("interface_type") 74 | normalized_int_type = Interface.normalize_interface_type(int_type) 75 | int_num = match.group("interface_num") 76 | return normalized_int_type, int_num 77 | raise ValueError(f"Does not recognize {interface_name} as an interface name") 78 | 79 | @staticmethod 80 | def normalize_interface_type(interface_type: str) -> str: 81 | """Normalizes interface type 82 | 83 | For example: 84 | G is converted to GigabitEthernet 85 | Te is converted to TenGigabitEthernet 86 | """ 87 | int_type = interface_type.strip().lower() 88 | for norm_int_type in NORMALIZED_INTERFACES: 89 | if norm_int_type.lower().startswith(int_type): 90 | return norm_int_type 91 | 92 | return int_type 93 | 94 | # def connect_to(self, interface: "Interface") -> None: 95 | # if interface in self.connected_to: 96 | # raise ValueError(f"Interface {interface} is already connected to {self}") 97 | # self.connected_to.append(interface) 98 | -------------------------------------------------------------------------------- /nr_app/link.py: -------------------------------------------------------------------------------- 1 | from typing import List, TYPE_CHECKING 2 | 3 | if TYPE_CHECKING: 4 | from interface import Interface 5 | 6 | 7 | class Link: 8 | def __init__(self, interfaces: List["Interface"]) -> None: 9 | self.interfaces = sorted(interfaces) 10 | 11 | def __eq__(self, other) -> bool: 12 | return all( 13 | int1 == int2 for int1, int2 in zip(self.interfaces, other.interfaces) 14 | ) 15 | 16 | def __hash__(self) -> int: 17 | return hash(tuple(self.interfaces)) 18 | 19 | def __str__(self) -> str: 20 | return " <-> ".join(str(interface) for interface in self.interfaces) 21 | 22 | def __repr__(self) -> str: 23 | return f"{self.__class__.__qualname__}(" f"interfaces={self.interfaces})" 24 | 25 | @property 26 | def is_point_to_point(self) -> bool: 27 | return len(self.interfaces) == 2 28 | 29 | @property 30 | def first_interface(self) -> "Interface": 31 | if not self.is_point_to_point: 32 | raise ValueError( 33 | "Can't return the first interface because " 34 | "there are more than two interfaces forming a link" 35 | ) 36 | return self.interfaces[0] 37 | 38 | @property 39 | def second_interface(self) -> "Interface": 40 | if not self.is_point_to_point: 41 | raise ValueError( 42 | "Can't return the second interface because " 43 | "there are more than two interfaces forming a link" 44 | ) 45 | return self.interfaces[1] 46 | -------------------------------------------------------------------------------- /nr_app/topology.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import logging 3 | import logging.config 4 | import time 5 | from typing import List, Dict, Tuple 6 | 7 | from colorama import Fore 8 | from nornir import InitNornir 9 | from nornir.core.inventory import Host 10 | import colorama 11 | import matplotlib.pyplot as plt 12 | import networkx as nx 13 | import requests 14 | import urllib3 15 | 16 | import constants 17 | from interface import Interface 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 22 | 23 | 24 | def extract_hostname_from_fqdn(fqdn: str) -> str: 25 | """Extracts hostname from fqdn-like string 26 | 27 | For example, R1.cisco.com -> R1, sw1 -> sw1" 28 | """ 29 | return fqdn.split(".")[0] 30 | 31 | 32 | def parse_cdp_neighbors(task): 33 | url = constants.RESTCONF_ROOT + constants.CDP_NEIGHBORS_ENDPOINT 34 | url = url.format(host=task.host.hostname) 35 | response = requests.get( 36 | url, 37 | headers=constants.HEADERS, 38 | auth=(task.host.username, task.host.password), 39 | verify=False, 40 | ) 41 | response.raise_for_status() 42 | cdp_entries = response.json().get("Cisco-IOS-XE-cdp-oper:cdp-neighbor-detail", []) 43 | device_name = task.host.name 44 | host_interfaces = {} 45 | task.host.data["interfaces"] = host_interfaces 46 | for cdp_entry in cdp_entries: 47 | interface_name = cdp_entry["local-intf-name"] 48 | if interface_name in host_interfaces: 49 | interface = host_interfaces[interface_name] 50 | else: 51 | interface = Interface(interface_name, device_name) 52 | host_interfaces[interface_name] = interface 53 | 54 | remote_interface_name = cdp_entry["port-id"] 55 | remote_device_fqdn = cdp_entry["device-name"] 56 | remote_device_name = extract_hostname_from_fqdn(remote_device_fqdn) 57 | remote_interface = Interface(remote_interface_name, remote_device_name) 58 | interface.neighbors.append(remote_interface) 59 | 60 | 61 | def build_graph(hosts: List[Host]) -> Tuple[nx.Graph, List[Dict[Tuple[str, str], str]]]: 62 | edge_labels: List[Dict[Tuple[str, str], str]] = [{}, {}] 63 | links = set( 64 | [ 65 | interface.link_from_neighbors() 66 | for host in hosts 67 | for interface in host.data["interfaces"].values() 68 | ] 69 | ) 70 | graph = nx.Graph() 71 | graph.add_nodes_from([host.name for host in hosts]) 72 | 73 | for link in links: 74 | if not link.is_point_to_point: 75 | continue 76 | 77 | edge: Tuple[str, str] = tuple( 78 | interface.device_name for interface in link.interfaces 79 | ) 80 | for i, interface in enumerate(link.interfaces): 81 | edge_labels[i][edge] = interface.short_name 82 | graph.add_edge(*edge) 83 | logger.info("The network graph was built") 84 | return graph, edge_labels 85 | -------------------------------------------------------------------------------- /nr_app/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, Union 2 | from xml.dom.minidom import parseString 3 | 4 | from lxml import etree 5 | from ruamel.yaml import YAML 6 | 7 | from nr_app.constants import NORMALIZED_INTERFACES, INTERFACE_NAME_RE 8 | 9 | 10 | def extract_hostname_from_fqdn(fqdn: str) -> str: 11 | """Extracts hostname from fqdn-like string 12 | 13 | For example, R1.cisco.com -> R1, sw1 -> sw1" 14 | """ 15 | return fqdn.split(".")[0] 16 | 17 | 18 | def normalize_interface_type(interface_type: str) -> str: 19 | """Normalizes interface type 20 | For example, G is converted to GigabitEthernet, Te is converted to TenGigabitEthernet 21 | """ 22 | int_type = interface_type.strip().lower() 23 | for norm_int_type in NORMALIZED_INTERFACES: 24 | if norm_int_type.lower().startswith(int_type): 25 | return norm_int_type 26 | return int_type 27 | 28 | 29 | def normalize_interface_name(interface_name: str) -> str: 30 | """Normalizes interface name 31 | 32 | For example, Gi0/1 is converted to GigabitEthernet1, 33 | Te1/1 is converted to TenGigabitEthernet1/1 34 | """ 35 | match = INTERFACE_NAME_RE.search(interface_name) 36 | if match: 37 | int_type = match.group("interface_type") 38 | normalized_int_type = normalize_interface_type(int_type) 39 | int_num = match.group("interface_num") 40 | return normalized_int_type + int_num 41 | raise ValueError(f"Does not recognize {interface_name} as an interface name") 42 | 43 | 44 | def dict_to_xml( 45 | data: Any, root: Union[None, str, etree._Element] = None, attr_marker: str = "_" 46 | ) -> etree.Element: 47 | """Converts Python dictionary with YANG data to lxml etree.Element object. 48 | 49 | XML attributes must be represented in nested dictionary, which is accessed by the 50 | element name. Attribute keys must be prepended with underscore. Common use-cases: 51 | * operation attribute. For example: 52 | {"vrf": {"_operation": "replace"}} -> 53 | * changing default namespace. For example: 54 | {"native": {"hostname": "R1", "_xmlns": "http://cisco.com/ns/yang/Cisco-IOS-XE-native"}} -> 55 | R1 56 | 57 | Empty XML tags (including self-closing tags) are represented with value `None`: 58 | {"address-family": {"ipv4": None}} -> 59 | 60 | Namespaces with prefix: 61 | 1. They need to be defined under the top-level key "_namespaces" in the dictionary 62 | in the form prefix:namespace. E.g.: 63 | {"_namespaces": {"ianaift": "urn:ietf:params:xml:ns:yang:iana-if-type"}} 64 | 2. Use the form `element-name+prefix` to use it for a specific element. E.g.: 65 | {"type+ianaift": "ianaift:ethernetCsmacd"} -> 66 | ianaift:ethernetCsmacd 67 | """ 68 | namespaces = data.pop("_namespaces", {}) 69 | 70 | def _dict_to_xml(data_: Any, parent: Optional[etree._Element] = None) -> None: 71 | nonlocal root 72 | if not isinstance(data_, dict): 73 | raise ValueError("provided data must be a dictionary") 74 | 75 | for key, value in data_.items(): 76 | if key.startswith(attr_marker): 77 | # handle keys starting with attr_marker as tag attributes 78 | attr_name = key.lstrip(attr_marker) 79 | parent.attrib[attr_name] = value 80 | else: 81 | if "+" in key: 82 | key, *_namespaces = key.split("+") 83 | nsmap = {ns: namespaces[ns] for ns in _namespaces} 84 | else: 85 | nsmap = None 86 | element = etree.Element(key, nsmap=nsmap) 87 | if root is None: 88 | root = element 89 | 90 | if parent is not None and not isinstance(value, list): 91 | parent.append(element) 92 | 93 | if isinstance(value, dict): 94 | _dict_to_xml(value, element) 95 | elif isinstance(value, list): 96 | for item in value: 97 | list_key = etree.Element(key) 98 | parent.append(list_key) 99 | _dict_to_xml(item, list_key) 100 | else: 101 | if value is True or value is False: 102 | value = str(value).lower() 103 | elif value is not None and not isinstance(value, str): 104 | value = str(value) 105 | 106 | element.text = value 107 | 108 | if isinstance(root, str): 109 | root = etree.Element(root) 110 | _dict_to_xml(data, root) 111 | return root 112 | 113 | 114 | def yaml_to_xml_str( 115 | yaml_content: str, root: Union[None, str, etree._Element] = None 116 | ) -> str: 117 | yml = YAML(typ="safe") 118 | data = yml.load(yaml_content) 119 | _xml = dict_to_xml(data=data, root=root) 120 | result = etree.tostring(_xml).decode("utf-8") 121 | return result 122 | 123 | 124 | def prettify_xml(xml: Union[str, etree._Element]) -> str: 125 | if isinstance(xml, etree._Element): 126 | result = etree.tostring(xml, pretty_print=True).decode("utf-8") 127 | else: 128 | result = parseString(xml).toprettyxml() 129 | return result 130 | -------------------------------------------------------------------------------- /output/topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmfigol/nornir-apps/a6c0dae756cbe971e743359a1ca0a0a1d6c18380/output/topology.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nr-app" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Dmitry Figol "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.9" 9 | nornir = "^3" 10 | colorama = "*" 11 | textfsm = "^1" 12 | matplotlib = "^3" 13 | requests = "^2" 14 | networkx = "^2" 15 | lxml = "^4" 16 | jupyter = "^1" 17 | httpx = "*" 18 | nornir-utils = "*" 19 | nornir-jinja2 = "*" 20 | scrapli = {extras = ["ssh2", "textfsm", "paramiko", "genie", "asyncssh"], version = "*", allow-prereleases = true } 21 | scrapli-netconf = { version = "*", allow-prereleases = true } 22 | scrapli-cfg = { git = "https://github.com/scrapli/scrapli_cfg.git", branch = "main", allow-prereleases = true } 23 | nornir-scrapli = { git = "https://github.com/scrapli/nornir_scrapli", branch = "develop"} 24 | "ruamel.yaml" = "*" 25 | genie = "^21.3" 26 | pyats = "^21.3" 27 | 28 | [tool.poetry.dev-dependencies] 29 | bpython = "*" 30 | pdbpp = "*" 31 | black = {version = "*", allow-prereleases = true} 32 | flake8 = "*" 33 | mypy = "*" 34 | 35 | [build-system] 36 | requires = ["poetry_core>=1.0.0"] 37 | build-backend = "poetry.core.masonry.api" 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==0.6.0; python_version >= "3.5" 2 | aiohttp-swagger==1.0.15; python_version >= "3.5" 3 | aiohttp==3.7.2; python_version >= "3.6" 4 | appnope==0.1.2; platform_system == "Darwin" and python_version >= "3.7" and sys_platform == "darwin" 5 | argon2-cffi==20.1.0; python_version >= "3.6" 6 | async-generator==1.10; python_full_version >= "3.6.1" and python_version >= "3.6" 7 | async-lru==1.0.2; python_version >= "3.5" 8 | async-timeout==3.0.1; python_full_version >= "3.5.3" and python_version >= "3.6" 9 | asyncssh==2.5.0; python_version >= "3.6" 10 | attrs==20.3.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" 11 | backcall==0.2.0; python_version >= "3.7" 12 | bcrypt==3.2.0; python_version >= "3.6" 13 | bleach==3.3.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" 14 | certifi==2020.12.5; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" 15 | cffi==1.14.5; implementation_name == "pypy" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.6.0") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0") 16 | chardet==3.0.4; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" 17 | colorama==0.4.4; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") 18 | cryptography==3.3.1; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.5" 19 | cycler==0.10.0; python_version >= "3.7" 20 | decorator==4.4.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.2.0" and python_version >= "3.7" 21 | defusedxml==0.7.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" 22 | dill==0.3.3; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.1.0" and python_version >= "3.5" 23 | distro==1.5.0; python_version >= "3.5" 24 | entrypoints==0.3; python_version >= "3.6" 25 | future==0.18.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.3.0" 26 | genie.libs.clean==21.3; python_version >= "3.5" 27 | genie.libs.conf==21.3; python_version >= "3.5" 28 | genie.libs.filetransferutils==21.3; python_version >= "3.5" 29 | genie.libs.health==21.3; python_version >= "3.5" 30 | genie.libs.ops==21.3; python_version >= "3.5" 31 | genie.libs.parser==21.3; python_version >= "3.5" 32 | genie.libs.sdk==21.3; python_version >= "3.5" 33 | genie==21.3.1; python_version >= "3.5" 34 | h11==0.12.0; python_version >= "3.6" 35 | httpcore==0.12.3; python_version >= "3.6" 36 | httpx==0.17.1; python_version >= "3.6" 37 | idna==2.10; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.5.0" 38 | ipykernel==5.5.3; python_version >= "3.6" 39 | ipython-genutils==0.2.0; python_version >= "3.7" 40 | ipython==7.22.0; python_version >= "3.7" 41 | ipywidgets==7.6.3 42 | jedi==0.18.0; python_version >= "3.7" 43 | jinja2==2.11.3; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.5.0" 44 | jsonpickle==2.0.0; python_version >= "3.5" 45 | jsonschema==3.2.0; python_version >= "3.5" 46 | junit-xml==1.9; python_version >= "3.5" 47 | jupyter-client==6.2.0; python_full_version >= "3.6.1" and python_version >= "3.6" 48 | jupyter-console==6.4.0; python_version >= "3.6" 49 | jupyter-core==4.7.1; python_full_version >= "3.6.1" and python_version >= "3.6" 50 | jupyter==1.0.0 51 | jupyterlab-pygments==0.1.2; python_version >= "3.6" 52 | jupyterlab-widgets==1.0.0; python_version >= "3.6" 53 | kiwisolver==1.3.1; python_version >= "3.7" 54 | lxml==4.6.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") 55 | markupsafe==1.1.1; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.5.0" 56 | matplotlib==3.4.1; python_version >= "3.7" 57 | mistune==0.8.4; python_version >= "3.6" 58 | multidict==5.1.0; python_version >= "3.6" 59 | mypy-extensions==0.4.3; python_version >= "3.6" and python_version < "4.0" 60 | nbclient==0.5.3; python_full_version >= "3.6.1" and python_version >= "3.6" 61 | nbconvert==6.0.7; python_version >= "3.6" 62 | nbformat==5.1.3; python_full_version >= "3.6.1" and python_version >= "3.6" 63 | nest-asyncio==1.5.1; python_full_version >= "3.6.1" and python_version >= "3.6" 64 | netaddr==0.8.0; python_version >= "3.5" 65 | networkx==2.5.1; python_version >= "3.6" 66 | nornir-jinja2==0.1.2; python_version >= "3.6" and python_version < "4.0" 67 | nornir-scrapli @ git+https://github.com/scrapli/nornir_scrapli@develop ; python_version >= "3.6" 68 | nornir-utils==0.1.2; python_version >= "3.6" and python_version < "4.0" 69 | nornir==3.1.0; python_version >= "3.6" and python_version < "4.0" 70 | notebook==6.3.0; python_version >= "3.6" 71 | ntc-templates==2.0.0; python_version >= "3.6" and python_version < "4.0" 72 | numpy==1.20.2; python_version >= "3.7" 73 | packaging==20.9; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" 74 | pandocfilters==1.4.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" 75 | paramiko==2.7.2; python_version >= "3.6" 76 | parso==0.8.2; python_version >= "3.7" 77 | pathspec==0.8.1; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5" 78 | pexpect==4.8.0; sys_platform != "win32" and python_version >= "3.7" 79 | pickleshare==0.7.5; python_version >= "3.7" 80 | pillow==8.2.0; python_version >= "3.7" 81 | prettytable==2.1.0; python_version >= "3.6" 82 | prometheus-client==0.10.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" 83 | prompt-toolkit==3.0.18; python_full_version >= "3.6.1" and python_version >= "3.7" 84 | psutil==5.8.0; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.5" 85 | ptyprocess==0.7.0; os_name != "nt" and python_version >= "3.7" and sys_platform != "win32" 86 | py==1.10.0; python_version >= "3.6" and python_full_version < "3.0.0" and implementation_name == "pypy" or implementation_name == "pypy" and python_version >= "3.6" and python_full_version >= "3.4.0" 87 | pyats.aereport==21.3; python_version >= "3.5" 88 | pyats.aetest==21.3; python_version >= "3.5" 89 | pyats.async==21.3; python_version >= "3.5" 90 | pyats.connections==21.3; python_version >= "3.5" 91 | pyats.datastructures==21.3; python_version >= "3.5" 92 | pyats.easypy==21.3; python_version >= "3.5" 93 | pyats.kleenex==21.3; python_version >= "3.5" 94 | pyats.log==21.3; python_version >= "3.5" 95 | pyats.reporter==21.3; python_version >= "3.5" 96 | pyats.results==21.3; python_version >= "3.5" 97 | pyats.tcl==21.3; python_version >= "3.5" 98 | pyats.topology==21.3; python_version >= "3.5" 99 | pyats.utils==21.3; python_version >= "3.5" 100 | pyats==21.3; python_version >= "3.5" 101 | pycparser==2.20; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" 102 | pyftpdlib==1.5.6; python_version >= "3.5" 103 | pygments==2.8.1; python_version >= "3.7" 104 | pynacl==1.4.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0" 105 | pyparsing==2.4.7; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" 106 | pyrsistent==0.17.3; python_version >= "3.5" 107 | python-dateutil==2.8.1; python_full_version >= "3.6.1" and python_version >= "3.7" 108 | python-engineio==3.13.2; python_version >= "3.5" 109 | python-socketio==4.6.0; python_version >= "3.5" 110 | pywin32==300; sys_platform == "win32" and python_version >= "3.6" 111 | pywinpty==0.5.7; os_name == "nt" and python_version >= "3.6" 112 | pyyaml==5.4.1; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.5" 113 | pyzmq==22.0.3; python_full_version >= "3.6.1" and python_version >= "3.6" 114 | qtconsole==5.0.3; python_version >= "3.6" 115 | qtpy==1.9.0; python_version >= "3.6" 116 | requests==2.25.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") 117 | rfc3986==1.4.0; python_version >= "3.6" 118 | ruamel.yaml.clib==0.2.2; platform_python_implementation == "CPython" and python_version < "3.10" and python_version >= "3.6" 119 | ruamel.yaml==0.16.13 120 | scrapli-cfg @ git+https://github.com/scrapli/scrapli_cfg.git@main ; python_version >= "3.6" 121 | scrapli-community==2021.1.30; python_version >= "3.6" 122 | scrapli-netconf==2021.1.30; python_version >= "3.6" 123 | scrapli==2021.7.30a1; python_version >= "3.6" 124 | send2trash==1.5.0; python_version >= "3.6" 125 | six==1.15.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_version >= "3.6" and python_full_version >= "3.4.0" 126 | sniffio==1.2.0; python_version >= "3.6" 127 | ssh2-python==0.26.0; python_version >= "3.6" 128 | terminado==0.9.4; python_version >= "3.6" 129 | testpath==0.4.4; python_version >= "3.6" 130 | textfsm==1.1.0 131 | tftpy==0.8.0; python_version >= "3.5" 132 | tornado==6.1; python_full_version >= "3.6.1" and python_version >= "3.6" 133 | tqdm==4.60.0; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.5" 134 | traitlets==5.0.5; python_full_version >= "3.6.1" and python_version >= "3.7" 135 | typing-extensions==3.7.4.3; python_version >= "3.6" and python_version < "4.0" 136 | unicon.plugins==21.3; python_version >= "3.5" 137 | unicon==21.3; python_version >= "3.5" 138 | urllib3==1.26.4; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" and python_version >= "3.5" 139 | wcwidth==0.2.5; python_full_version >= "3.6.1" and python_version >= "3.6" 140 | webencodings==0.5.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" 141 | widgetsnbextension==3.5.1 142 | xmltodict==0.12.0; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.5" 143 | yamllint==1.26.1; python_version >= "3.5" 144 | yarl==1.6.3; python_version >= "3.6" 145 | -------------------------------------------------------------------------------- /scripts/build_network_diagram_lldp.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import logging.config 3 | import time 4 | from queue import Queue 5 | from typing import TYPE_CHECKING, Dict, Any, Tuple, List, Set 6 | 7 | import colorama 8 | from colorama import Fore 9 | import httpx 10 | import matplotlib.pyplot as plt 11 | import networkx as nx 12 | from nornir import InitNornir 13 | 14 | from nr_app import constants 15 | from nr_app import utils 16 | from nr_app.link import Link 17 | from nr_app.interface import Interface 18 | 19 | if TYPE_CHECKING: 20 | from nornir.core.inventory import Host 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | def fetch_and_parse_lldp_neighbors(task, links_q: Queue): 26 | lldp_data = fetch_lldp_neighbors(task.host) 27 | parse_lldp_neighbors(host=task.host, data=lldp_data, links_q=links_q) 28 | 29 | 30 | def fetch_lldp_neighbors(host: "Host"): 31 | url = constants.RESTCONF_ROOT + constants.OPENCONFIG_LLDP_NEIGHBORS_ENDPOINT 32 | url = url.format(host=host.hostname) 33 | response = httpx.get( 34 | url, 35 | headers=constants.RESTCONF_HEADERS, 36 | auth=(host.username, host.password), 37 | verify=False, 38 | ) 39 | response.raise_for_status() 40 | response_data = response.json()["openconfig-lldp:interface"] 41 | return response_data 42 | 43 | 44 | def parse_lldp_neighbors(host, data: Dict[str, Any], links_q: Queue) -> None: 45 | device_name = host.name 46 | host_interfaces = {} 47 | host.data["interfaces"] = host_interfaces 48 | for interface_info in data: 49 | interface_name = interface_info["name"] 50 | interface = Interface(interface_name, device_name) 51 | neighbors = interface_info.get("neighbors") 52 | if not neighbors: 53 | continue 54 | 55 | link_interfaces: List["Interface"] = [interface] 56 | for neighbor_info in neighbors["neighbor"]: 57 | neighbor_state = neighbor_info["state"] 58 | remote_interface_name = neighbor_state["port-description"] 59 | remote_device_fqdn = neighbor_state["system-name"] 60 | remote_device_name = utils.extract_hostname_from_fqdn(remote_device_fqdn) 61 | remote_interface = Interface(remote_interface_name, remote_device_name) 62 | link_interfaces.append(remote_interface) 63 | 64 | link = Link(interfaces=link_interfaces) 65 | links_q.put(link) 66 | 67 | host_interfaces[interface.name] = interface 68 | 69 | 70 | def build_graph( 71 | nodes: List[str], links: Set[Link] 72 | ) -> Tuple[nx.Graph, List[Dict[Tuple[str, str], str]]]: 73 | edge_labels: List[Dict[Tuple[str, str], str]] = [{}, {}] 74 | graph = nx.Graph() 75 | graph.add_nodes_from(nodes) 76 | 77 | for link in links: 78 | if not link.is_point_to_point: 79 | continue 80 | 81 | edge: Tuple[str, str] = tuple( 82 | interface.device_name for interface in link.interfaces 83 | ) # type: ignore 84 | for i, interface in enumerate(link.interfaces): 85 | edge_labels[i][edge] = interface.short_name 86 | graph.add_edge(*edge) 87 | logger.info("The network graph was built") 88 | return graph, edge_labels 89 | 90 | 91 | def draw_and_save_topology( 92 | graph: nx.Graph, edge_labels: List[Dict[Tuple[str, str], str]] 93 | ) -> None: 94 | # plt.figure(1, figsize=(12, 12)) 95 | fig, ax = plt.subplots(figsize=(14, 9)) 96 | fig.tight_layout() 97 | pos = nx.spring_layout(graph) 98 | nx.draw_networkx(graph, pos, node_size=1500, node_color="orange") 99 | nx.draw_networkx_edge_labels(graph, pos, edge_labels=edge_labels[0], label_pos=0.8) 100 | nx.draw_networkx_edge_labels(graph, pos, edge_labels=edge_labels[1], label_pos=0.2) 101 | filename = "output/topology.png" 102 | plt.savefig(filename) 103 | logger.info("The network topology diagram has been saved to %r", filename) 104 | # plt.draw() 105 | 106 | 107 | def main() -> None: 108 | start_time = time.time() 109 | links_q: Queue = Queue() 110 | with InitNornir(config_file="nr-config-local.yaml") as nr: 111 | nr.run(fetch_and_parse_lldp_neighbors, links_q=links_q) 112 | 113 | milestone = time.time() 114 | time_to_run = milestone - start_time 115 | print( 116 | f"{Fore.RED}It took {time_to_run:.2f} seconds to get and parse LLDP details" 117 | f"{Fore.RESET}" 118 | ) 119 | 120 | links = set(links_q.queue) 121 | nodes = [host.name for host in nr.inventory.hosts.values()] 122 | graph, edge_labels = build_graph(nodes=nodes, links=links) 123 | draw_and_save_topology(graph, edge_labels) 124 | 125 | new_milestone = time.time() 126 | time_to_run = new_milestone - milestone 127 | print( 128 | f"{Fore.RED}It took additional {time_to_run:.2f} seconds to draw the diagram" 129 | f"{Fore.RESET}" 130 | ) 131 | plt.show() 132 | 133 | 134 | if __name__ == "__main__": 135 | # matplotlib.use("TkAgg") 136 | logging.config.dictConfig(constants.LOGGING_DICT) 137 | # urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 138 | colorama.init() 139 | main() 140 | -------------------------------------------------------------------------------- /scripts/cli_configure.py: -------------------------------------------------------------------------------- 1 | from nornir import InitNornir 2 | from nornir_jinja2.plugins.tasks import template_file 3 | 4 | # from nornir_napalm.plugins.tasks import napalm_configure 5 | # from nornir_netmiko.tasks import netmiko_send_config 6 | from nornir_scrapli.tasks import send_configs as scrapli_send_configs 7 | from nornir_utils.plugins.functions import print_result 8 | 9 | 10 | def configure_from_template(task): 11 | cfg = task.run(template_file, path="templates", template="config.j2").result 12 | # task.run(napalm_configure, configuration=cfg, replace=False) 13 | # task.run(netmiko_send_config, config_commands=cfg) 14 | task.run(scrapli_send_configs, configs=cfg.splitlines()) 15 | 16 | 17 | def main(): 18 | with InitNornir(config_file="nr-config-local.yaml") as nr: 19 | result = nr.run(configure_from_template) 20 | print_result(result) 21 | 22 | 23 | if __name__ == "__main__": 24 | main() 25 | -------------------------------------------------------------------------------- /scripts/find_mac.py: -------------------------------------------------------------------------------- 1 | from nornir import InitNornir 2 | from nornir_scrapli.tasks import send_command as scrapli_send_command 3 | from nornir_utils.plugins.functions import print_result 4 | from nornir.core.filter import F 5 | from nornir.core.task import Result 6 | 7 | from collections import ChainMap 8 | from typing import List, Dict, Any, Tuple, NamedTuple, Set 9 | 10 | from nr_app.utils import normalize_interface_name 11 | 12 | 13 | class ResultInterface(NamedTuple): 14 | device_name: str 15 | int_name: str 16 | belongs: bool 17 | 18 | 19 | def build_mac_to_int_mapping( 20 | parsed_data: List[Dict[str, str]], device_name: str 21 | ) -> Dict[str, ResultInterface]: 22 | """ 23 | Returns: 24 | dictionary with mac address as key in the format 5254.001b.091c 25 | """ 26 | result: Dict[str, ResultInterface] = {} 27 | for int_data in parsed_data: 28 | mac = int_data.get("address") 29 | int_name = int_data.get("interface") 30 | if mac and int_name: 31 | interface = ResultInterface( 32 | device_name=device_name, int_name=int_name, belongs=True 33 | ) 34 | result[mac] = interface 35 | return result 36 | 37 | 38 | def build_mac_to_int_mapping_task(task): 39 | nr_result = task.run(scrapli_send_command, command="show interface") 40 | parsed_data = nr_result.scrapli_response.textfsm_parse_output() 41 | task.host["mac_to_int"] = build_mac_to_int_mapping(parsed_data, task.host.name) 42 | # return Result(host=task.host, result=parsed_data) 43 | 44 | 45 | def get_access_ports_from_int_sw_data(parsed_data: List[Dict[str, str]]) -> Set[str]: 46 | result = set() 47 | for int_data in parsed_data: 48 | if int_data["mode"] == "static access": 49 | int_name = normalize_interface_name(int_data["interface"]) 50 | result.add(int_name) 51 | return result 52 | 53 | 54 | def mac_to_int_from_mac_addr_table( 55 | parsed_data: List[Dict[str, str]], access_ports: Set[str], device_name: str 56 | ) -> Dict[str, ResultInterface]: 57 | result: Dict[str, ResultInterface] = {} 58 | for mac_data in parsed_data: 59 | interface_name = normalize_interface_name(mac_data["destination_port"]) 60 | if interface_name in access_ports: 61 | mac_addr = mac_data["destination_address"] 62 | result[mac_addr] = ResultInterface( 63 | device_name=device_name, int_name=interface_name, belongs=False 64 | ) 65 | return result 66 | 67 | 68 | def gather_connected_macs(task): 69 | int_sw_nr_result = task.run( 70 | scrapli_send_command, command="show interface switchport" 71 | ) 72 | parsed_int_sw_data = int_sw_nr_result.scrapli_response.textfsm_parse_output() 73 | 74 | access_ports = get_access_ports_from_int_sw_data(parsed_int_sw_data) 75 | # breakpoint() 76 | mac_addr_table_result = task.run( 77 | scrapli_send_command, command="show mac address-table" 78 | ) 79 | parsed_mac_addr_table = ( 80 | mac_addr_table_result.scrapli_response.textfsm_parse_output() 81 | ) 82 | mac_to_int = mac_to_int_from_mac_addr_table( 83 | parsed_mac_addr_table, access_ports, device_name=task.host.name 84 | ) 85 | task.host["mac_to_int"].update(mac_to_int) 86 | 87 | 88 | def gather_all_macs() -> Dict[str, ResultInterface]: 89 | nr = InitNornir(config_file="inventories/simple-topology/nr-config.yaml") 90 | iosv = nr.filter(F(groups__contains="iosv")) 91 | all_ios_devices = nr.filter(F(groups__any=["iosv", "csr"])) 92 | all_ios_devices.run(build_mac_to_int_mapping_task) 93 | 94 | results = iosv.run(gather_connected_macs) 95 | print_result(results) 96 | 97 | mac_to_interface = {} 98 | for host in all_ios_devices.inventory.hosts.values(): 99 | mac_to_interface.update(host["mac_to_int"]) 100 | return mac_to_interface 101 | 102 | 103 | def main(): 104 | """This scripts is looking for the location of MACs on the network. 105 | It might need some additional work for an arbitrary network""" 106 | mac = "5254.001b.d37b" 107 | mac = "5254.0003.9b4c" 108 | mac_to_interface = gather_all_macs() 109 | 110 | interface = mac_to_interface.get(mac) 111 | if interface is None: 112 | print(f"Mac {mac} was not found") 113 | elif interface.belongs: 114 | print( 115 | f"Interface {interface.int_name} on device {interface.device_name} has mac {mac}" 116 | ) 117 | elif not interface.belongs: 118 | print( 119 | f"A client with mac {mac} is connected to {interface.device_name}.{interface.int_name}" 120 | ) 121 | 122 | 123 | if __name__ == "__main__": 124 | main() 125 | 126 | 127 | # mac -> (device, interface) 128 | -------------------------------------------------------------------------------- /scripts/gather_commands.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from pathlib import Path 3 | import shutil 4 | 5 | from nornir import InitNornir 6 | 7 | # from nornir_netmiko.tasks import netmiko_send_command 8 | # from nornir.core.filter import F 9 | from nornir_scrapli.tasks import send_command as scrapli_send_command 10 | 11 | 12 | COMMANDS = [ 13 | "show version", 14 | "show ip int br", 15 | "show ip arp", 16 | "show platform resources", 17 | ] 18 | 19 | OUTPUT_DIR = Path("output/cli") 20 | 21 | 22 | def gather_commands(task, commands): 23 | dt = datetime.now() 24 | dt_str = dt.strftime("%Y-%m-%dT%H:%M:%S") 25 | 26 | file_path = OUTPUT_DIR / f"{task.host.name}_{dt_str}.txt" 27 | with open(file_path, "w") as f: 28 | for command in commands: 29 | # gather commands using netmiko 30 | # output = task.run(netmiko_send_command, command_string=command) 31 | # gather commands using scrapli w/ libssh2 32 | output = task.run(scrapli_send_command, command=command) 33 | f.write(f"===== {command} ======\n{output.result}\n\n") 34 | 35 | 36 | def main(): 37 | with InitNornir(config_file="nr-config-local.yaml") as nr: 38 | # lisbon = nr.filter(F(groups__contains="Lisbon")) 39 | nr.run(gather_commands, commands=COMMANDS) 40 | 41 | 42 | if __name__ == "__main__": 43 | if OUTPUT_DIR.is_dir(): 44 | shutil.rmtree(OUTPUT_DIR) 45 | OUTPUT_DIR.mkdir(parents=True, exist_ok=True) 46 | main() 47 | -------------------------------------------------------------------------------- /scripts/netconf_configure.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from pathlib import Path 3 | import shutil 4 | from typing import Any, Union, Optional 5 | 6 | from nornir import InitNornir 7 | 8 | # from nornir_netconf.plugins.tasks import netconf_get_config, netconf_edit_config 9 | from nornir_scrapli.tasks import netconf_edit_config, netconf_get_config 10 | from nornir_utils.plugins.functions import print_result 11 | from nornir.core.filter import F 12 | from nornir.core.task import Result 13 | from lxml import etree 14 | from ruamel.yaml import YAML 15 | 16 | 17 | from nr_app import utils 18 | 19 | OUTPUT_DIR = Path("output/netconf") 20 | CONFIG_YAML = Path("netconf-data/config.yaml") 21 | 22 | 23 | def save_nc_get_config(task): 24 | with open(f"output/netconf/{task.host.name}.xml", "w") as f: 25 | cfg_xml = task.run(task=netconf_get_config, source="running").result 26 | f.write(cfg_xml) 27 | 28 | 29 | def edit_nc_config_from_yaml(task): 30 | with open(CONFIG_YAML) as f: 31 | cfg = utils.yaml_to_xml_str(f.read(), root="config") 32 | result = task.run(task=netconf_edit_config, config=cfg).result 33 | return Result(host=task.host, result=result) 34 | 35 | 36 | def main(): 37 | if OUTPUT_DIR.is_dir(): 38 | shutil.rmtree(OUTPUT_DIR) 39 | OUTPUT_DIR.mkdir(parents=True, exist_ok=True) 40 | 41 | with InitNornir(config_file="nr-config-local.yaml") as nr: 42 | # results = nr.run(task=save_nc_get_config) 43 | # print_result(results) 44 | 45 | results = nr.run(task=edit_nc_config_from_yaml) 46 | print_result(results) 47 | 48 | 49 | if __name__ == "__main__": 50 | main() 51 | -------------------------------------------------------------------------------- /scripts/netconf_save_config.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import shutil 3 | 4 | from nornir import InitNornir 5 | from nornir_scrapli.tasks import netconf_get_config 6 | from nornir_utils.plugins.functions import print_result 7 | 8 | OUTPUT_DIR = Path("output/netconf") 9 | 10 | 11 | def save_nc_get_config(task): 12 | with open(f"output/netconf/{task.host.name}.xml", "w") as f: 13 | cfg_xml = task.run(task=netconf_get_config, source="running").result 14 | f.write(cfg_xml) 15 | 16 | 17 | def main(): 18 | if OUTPUT_DIR.is_dir(): 19 | shutil.rmtree(OUTPUT_DIR) 20 | OUTPUT_DIR.mkdir(parents=True, exist_ok=True) 21 | with InitNornir(config_file="nr-config-local.yaml") as nr: 22 | results = nr.run(task=save_nc_get_config) 23 | # print_result(results) 24 | 25 | 26 | if __name__ == "__main__": 27 | main() 28 | -------------------------------------------------------------------------------- /scripts/restconf_get_lldp_neighbors.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import logging.config 3 | from typing import TYPE_CHECKING, Dict, Any, Tuple, List, Set 4 | 5 | import httpx 6 | from nornir import InitNornir 7 | from nornir.core.task import Result 8 | from nornir_utils.plugins.functions import print_result 9 | 10 | from nr_app import constants 11 | 12 | 13 | if TYPE_CHECKING: 14 | from nornir.core.inventory import Host 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | def parse_lldp_neighbors_data( 20 | device_name: str, data: List[Dict[str, Any]] 21 | ) -> List[str]: 22 | neighbors: List[str] = [] 23 | for lldp_entry in data: 24 | local_int = lldp_entry["local-interface"] 25 | remote_int = lldp_entry["connecting-interface"] 26 | remote_device_fqdn = lldp_entry["device-id"] 27 | remote_device, _, _ = remote_device_fqdn.partition(".") 28 | neighbor_str = f"{device_name}:{local_int} <-> {remote_device}:{remote_int}" 29 | neighbors.append(neighbor_str) 30 | return neighbors 31 | 32 | 33 | def fetch_and_parse_lldp_neighbors(task): 34 | url = f"https://{task.host.hostname}/restconf/data/Cisco-IOS-XE-lldp-oper:lldp-entries" 35 | headers = {"Accept": "application/yang-data+json"} 36 | response = httpx.get( 37 | url, 38 | headers=headers, 39 | auth=(task.host.username, task.host.password), 40 | verify=False, 41 | ) 42 | lldp_data = response.json()["Cisco-IOS-XE-lldp-oper:lldp-entries"]["lldp-entry"] 43 | neighbors = parse_lldp_neighbors_data(task.host.name, lldp_data) 44 | result = "\n".join(neighbors) 45 | return Result(host=task.host, result=result) 46 | 47 | 48 | def main() -> None: 49 | with InitNornir(config_file="nr-config-local.yaml") as nr: 50 | results = nr.run(fetch_and_parse_lldp_neighbors) 51 | print_result(results) 52 | 53 | 54 | if __name__ == "__main__": 55 | logging.config.dictConfig(constants.LOGGING_DICT) 56 | main() 57 | -------------------------------------------------------------------------------- /templates/config.j2: -------------------------------------------------------------------------------- 1 | banner motd ^Just testing MOTD on >{{ host.name }}< using nornir^ 2 | --------------------------------------------------------------------------------