├── .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 |
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 |
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 |
--------------------------------------------------------------------------------