├── README.md
├── config.yaml
├── get-acl.py
├── get-bgp.py
├── get-ntp.py
├── get-ospf.py
├── groups.yaml
├── host_vars
└── R1.yaml
├── hosts.yaml
├── requirements.txt
├── templates
├── acl.j2
├── bgp.j2
├── ntp.j2
└── ospf.j2
└── xr-demo.py
/README.md:
--------------------------------------------------------------------------------
1 | # IOS-XR NETCONF LAB
2 | This lab uses NETCONF to configure the IOS-XR Always On Sandbox.
3 | -------------------------------------------------------------------
4 | ## Dependencies
5 |
6 | ```
7 | python3 -m pip install -r requirements.txt
8 | ```
9 | ----------------------------------------------------------------
10 | ### WARNING
11 | This lab uses a shared resource. If others are using it at the same you may experience errors (lock denied...) and failures due to timeouts.
12 | When this happens you will need to wait until the resource is freed up before trying again.
13 |
14 | ### LAB INSTRUCTIONS
15 | Pip install the dependencies and git clone the repository. Change into the ```IOS-XR-NETCONF-LAB``` directory.
16 |
17 | To change the configurations simply enter the ```host_vars``` directory and modify the values in the ```R1.yaml``` file.
18 |
19 | #### To execute the script:
20 | ```python3 xr-demo.py```
21 |
22 | -------------------------------------------------
23 |
24 | #### To manually log into the XR Always-On Sandbox and run show commands for verification:
25 | ```ssh admin@sbx-iosxr-mgmt.cisco.com -p 8181```
26 |
27 | Then enter the password:
28 | ```C1sco12345```
29 |
30 | -------------------------------------------------
31 |
32 | #### You can also retrieve the configuration information directly over NETCONF using the scripts in this repo.
33 |
34 | ##### To retrieve Access Control List configuration information:
35 | ```python3 get-acl.py```
36 |
37 | ##### To retrieve BGP configurarion information:
38 | ```python3 get-bgp.py```
39 |
40 | ##### To retrieve OSPF configuration information:
41 | ```python3 get-ospf.py```
42 |
43 | ##### To retrieve NTP configuration information:
44 | ```python3 get-ntp.py```
45 |
46 | -------------------------------------------------
47 | ## Video Walkthrough
48 | You can find a video walkthrough of this lab on my [youtube](https://www.youtube.com/watch?v=tFN7-jXX2dQ) channel!
49 |
50 | ---------------------------------------------
51 | ### About Me
52 | My name's John McGovern, I maintain a Youtube channel called IPvZero and I am trainer for CBT Nuggets.
53 |
54 | I create instructional videos on Python Network Automation.
55 |
56 | ### Contact
57 |
58 | [Twitter](https://twitter.com/IPvZero)
59 |
60 | [Youtube](https://youtube.com/c/IPvZero)
61 |
62 | [LinkedIn](https://www.linkedin.com/in/ipvzero)
63 |
64 | ### CBT Nuggets
65 |
66 | [Advanced Network Automation with Cisco and Python](http://learn.gg/adv-net)
67 |
68 |
--------------------------------------------------------------------------------
/config.yaml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | inventory:
4 | plugin: SimpleInventory
5 | options:
6 | host_file: "hosts.yaml"
7 | group_file: "groups.yaml"
8 | defaults_file: "defaults.yaml"
9 |
10 | runners:
11 | plugin: threaded
12 | options:
13 | num_workers: 10
14 |
--------------------------------------------------------------------------------
/get-acl.py:
--------------------------------------------------------------------------------
1 | """
2 | AUTHOR: IPvZero
3 | Date: 15 January 2021
4 |
5 | This script retrieves ACL configuration information
6 | """
7 |
8 | from nornir_scrapli.tasks import netconf_get_config
9 | from nornir_utils.plugins.functions import print_result
10 | from nornir import InitNornir
11 |
12 | nr = InitNornir(config_file="config.yaml")
13 |
14 | acl_filter = """
15 |
16 |
17 | """
18 |
19 |
20 | def get_yang(task):
21 | """ Pull ACL Configuration """
22 | task.run(
23 | task=netconf_get_config,
24 | source="running",
25 | filter_type="subtree",
26 | filters=acl_filter,
27 | )
28 |
29 |
30 | result = nr.run(task=get_yang)
31 | print_result(result)
32 |
--------------------------------------------------------------------------------
/get-bgp.py:
--------------------------------------------------------------------------------
1 | """
2 | AUTHOR: IPvZero
3 | Date: 15 January 2021
4 |
5 | This script retrieves BGP configuration information
6 | """
7 |
8 | from nornir_scrapli.tasks import netconf_get_config
9 | from nornir_utils.plugins.functions import print_result
10 | from nornir import InitNornir
11 |
12 | nr = InitNornir(config_file="config.yaml")
13 |
14 | bgp_filter = """
15 |
16 | """
17 |
18 |
19 | def get_yang(task):
20 | """ Pull BGP Configuration """
21 | task.run(
22 | task=netconf_get_config,
23 | source="running",
24 | filter_type="subtree",
25 | filters=bgp_filter,
26 | )
27 |
28 |
29 | result = nr.run(task=get_yang)
30 | print_result(result)
31 |
--------------------------------------------------------------------------------
/get-ntp.py:
--------------------------------------------------------------------------------
1 | """
2 | AUTHOR: IPvZero
3 | Date: 15 January 2021
4 |
5 | This script retrieves NTP configuration information
6 | """
7 |
8 | from nornir_scrapli.tasks import netconf_get_config
9 | from nornir_utils.plugins.functions import print_result
10 | from nornir import InitNornir
11 |
12 | nr = InitNornir(config_file="config.yaml")
13 |
14 | ntp_filter = """
15 |
16 | """
17 |
18 |
19 | def get_yang(task):
20 | """ Pull NTP Configuration """
21 | task.run(
22 | task=netconf_get_config,
23 | source="running",
24 | filter_type="subtree",
25 | filters=ntp_filter,
26 | )
27 |
28 |
29 | result = nr.run(task=get_yang)
30 | print_result(result)
31 |
--------------------------------------------------------------------------------
/get-ospf.py:
--------------------------------------------------------------------------------
1 | """
2 | AUTHOR: IPvZero
3 | Date: 15 January 2021
4 |
5 | This script retrieves OSPF configuration information
6 | """
7 |
8 | from nornir_scrapli.tasks import netconf_get_config
9 | from nornir_utils.plugins.functions import print_result
10 | from nornir import InitNornir
11 |
12 | nr = InitNornir(config_file="config.yaml")
13 |
14 | ospf_filter = """
15 |
16 | """
17 |
18 |
19 | def get_yang(task):
20 | """ Pull OSPF Configuration """
21 | task.run(
22 | task=netconf_get_config,
23 | source="running",
24 | filter_type="subtree",
25 | filters=ospf_filter,
26 | )
27 |
28 |
29 | result = nr.run(task=get_yang)
30 | print_result(result)
31 |
--------------------------------------------------------------------------------
/groups.yaml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | cisco_group:
4 | platform: 'ios'
5 | connection_options:
6 | scrapli_netconf:
7 | extras:
8 | port: 10000
9 | ssh_config_file: True
10 | #transport: ssh2
11 | auth_strict_key: False
12 | timeout_transport: 20
13 |
--------------------------------------------------------------------------------
/host_vars/R1.yaml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | ntp:
4 | - server_ip: 4.4.4.4
5 | preferred: {}
6 |
7 | - server_ip: 5.5.5.5
8 | preferred: "True"
9 |
10 | - server_ip: 6.6.6.6
11 | preferred: {}
12 |
13 |
14 | ospf:
15 | process_id: "IPvZero"
16 | router_id: "44.44.44.44"
17 | areas:
18 | - area: 0
19 | interfaces:
20 | - "GigabitEthernet0/0/0/3"
21 | - "GigabitEthernet0/0/0/4"
22 | - "GigabitEthernet0/0/0/5"
23 |
24 | - area: 51
25 | interfaces:
26 | - "GigabitEthernet0/0/0/8"
27 |
28 | - area: 431
29 | interfaces:
30 | - "GigabitEthernet0/0/0/15"
31 |
32 | - area: 99
33 | interfaces:
34 | - "GigabitEthernet0/0/0/10"
35 | - "GigabitEthernet0/0/0/11"
36 |
37 |
38 | bgp:
39 | autonomous_system: 65001
40 | bgp_rid: 1.1.1.1
41 | networks:
42 | - network: "23.23.23.0"
43 | prefix: "24"
44 | - network: "24.24.24.0"
45 | prefix: "24"
46 |
47 | neighbors:
48 | - neighbor: 2.2.2.2
49 | remote_as: "65002"
50 |
51 | - neighbor: 3.3.3.3
52 | remote_as: "65003"
53 | keepalive: "15"
54 | hold-time: "45"
55 |
56 | - neighbor: 4.4.4.4
57 | remote_as: "65004"
58 | keepalive: "20"
59 | hold-time: "60"
60 |
61 |
62 | ACLs:
63 | - acl_name: "LIST1"
64 | remarks:
65 | - sequence: 10
66 | statement: "~~ PERMITS WEB ACCESS FOR 192.168.1.1 -- 34.34.34.0/24 ~~"
67 |
68 | - sequence: 30
69 | statement: "~~ PERMITS IP TRAFFIC FROM 172.16.10.0/24 -- 32.32.32.0/24 ~~"
70 |
71 | rules:
72 | - sequence: 20
73 | grant: "permit"
74 | protocol: "tcp"
75 | source_address: "192.168.1.1"
76 | source_wildcard: "0.0.0.0"
77 | destination_address: "34.34.34.0"
78 | destination_wildcard: "0.0.0.255"
79 | destination_port: "www"
80 | destination_operator: "equal"
81 |
82 | - sequence: 40
83 | grant: "permit"
84 | protocol: "ip"
85 | source_address: "172.16.10.0"
86 | source_wildcard: "0.0.0.255"
87 | destination_address: "32.32.32.0"
88 | destination_wildcard: "0.0.0.255"
89 |
90 |
91 | - acl_name: "LIST2"
92 | remarks:
93 | - sequence: 10
94 | statement: "~~ PERMITS SSH ACCESS FOR 192.168.55.5 -- 34.34.34.0/24 ~~"
95 |
96 | - sequence: 30
97 | statement: "~~ PERMITS IP TRAFFIC FROM 172.25.10.0/24 -- ANYWHERE ~~"
98 |
99 | rules:
100 | - sequence: 20
101 | grant: "permit"
102 | protocol: "tcp"
103 | source_address: "192.168.55.5"
104 | source_wildcard: "0.0.0.0"
105 | destination_address: "34.34.34.0"
106 | destination_wildcard: "0.0.0.255"
107 | destination_port: 22
108 | destination_operator: "equal"
109 |
110 | - sequence: 40
111 | grant: "permit"
112 | protocol: "ip"
113 | source_address: "172.25.10.0"
114 | source_wildcard: "0.0.0.255"
115 |
116 |
117 | - acl_name: "LIST3"
118 | remarks:
119 | - sequence: 10
120 | statement: "~~ DENY ICMP TO 8.8.8.8 ~~"
121 |
122 | - sequence: 30
123 | statement: "PERMITS ALL OTHER TRAFFIC"
124 |
125 | rules:
126 | - sequence: 20
127 | grant: "deny"
128 | protocol: "icmp"
129 | destination_address: "8.8.8.8"
130 | destination_wildcard: "0.0.0.0"
131 |
132 | - sequence: 40
133 | grant: "permit"
134 | protocol: "ip"
135 |
--------------------------------------------------------------------------------
/hosts.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | R1:
3 | hostname: "sbx-iosxr-mgmt.cisco.com"
4 | username: "admin"
5 | password: "C1sco12345"
6 | port: 10000
7 | groups:
8 | - cisco_group
9 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | asyncssh==2.5.0
2 | backcall==0.2.0
3 | cffi==1.14.4
4 | colorama==0.4.4
5 | commonmark==0.9.1
6 | cryptography==3.3.1
7 | decorator==4.4.2
8 | future==0.18.2
9 | ipdb==0.13.4
10 | ipython==7.19.0
11 | ipython-genutils==0.2.0
12 | jedi==0.18.0
13 | Jinja2==2.11.2
14 | lxml==4.6.2
15 | MarkupSafe==1.1.1
16 | mypy-extensions==0.4.3
17 | nornir==3.0.0
18 | nornir-jinja2==0.1.1
19 | nornir-scrapli==2020.11.1
20 | nornir-utils==0.1.1
21 | ntc-templates==1.6.0
22 | parso==0.8.1
23 | pexpect==4.8.0
24 | pickleshare==0.7.5
25 | pkg-resources==0.0.0
26 | prompt-toolkit==3.0.14
27 | ptyprocess==0.7.0
28 | pycparser==2.20
29 | Pygments==2.7.4
30 | rich==9.8.2
31 | ruamel.yaml==0.16.12
32 | ruamel.yaml.clib==0.2.2
33 | scrapli==2020.12.31
34 | scrapli-asyncssh==2020.12.23
35 | scrapli-community==2020.11.15
36 | scrapli-netconf==2021.1.17
37 | six==1.15.0
38 | textfsm==1.1.0
39 | traitlets==5.0.5
40 | typing-extensions==3.7.4.3
41 | wcwidth==0.2.5
42 | xmltodict==0.12.0
43 |
--------------------------------------------------------------------------------
/templates/acl.j2:
--------------------------------------------------------------------------------
1 |
3 |
5 |
6 | {% for acl in host.facts.ACLs %}
7 |
8 | {{ acl.acl_name }}
9 |
10 | {% if "remarks" in acl %}
11 | {% set remarks = acl.remarks %}
12 | {% for remark in remarks %}
13 |
14 |
15 | {{ remark.sequence }}
16 | {{ remark.statement }}
17 | {{ remark.sequence }}
18 |
19 | {% endfor %}
20 | {% endif %}
21 | {% if "rules" in acl %}
22 | {% set rules = acl.rules %}
23 | {% for rule in rules %}
24 |
25 | {{ rule.sequence }}
26 | {{ rule.grant }}
27 | {{ rule.protocol }}
28 | {% if rule.source_address is defined %}
29 |
30 |
31 | {{ rule.source_address }}
32 | {{ rule.source_wildcard }}
33 |
34 | {% endif %}
35 | {% if rule.destination_address is defined %}
36 |
37 |
38 | {{ rule.destination_address }}
39 | {{ rule.destination_wildcard }}
40 |
41 | {% endif %}
42 | {% if rule.source_port is defined %}
43 |
44 |
45 | {{ rule.source_operator }}
46 | {{ rule.source_port }}
47 |
48 | {% endif %}
49 | {% if rule.destination_port is defined %}
50 |
51 |
52 | {{ rule.destination_operator }}
53 | {{ rule.destination_port }}
54 |
55 | {% endif %}
56 |
57 | {% endfor %}
58 | {% endif %}
59 |
60 |
61 |
62 | {% endfor %}
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/templates/bgp.j2:
--------------------------------------------------------------------------------
1 |
3 |
5 |
6 | default
7 |
8 | 0
9 |
10 | {{ host.facts.bgp.autonomous_system }}
11 |
12 |
13 |
14 | {{ host.facts.bgp.bgp_rid }}
15 |
16 |
17 | ipv4-unicast
18 |
19 | {% if "networks" in host.facts.bgp %}
20 |
21 | {% for n in host.facts.bgp.networks %}
22 |
23 |
24 | {{ n.network }}
25 | {{ n.prefix }}
26 |
27 | {% endfor %}
28 |
29 |
30 | {% endif %}
31 |
32 |
33 |
34 |
35 |
36 | {% set neighbors = host.facts.bgp.neighbors %}
37 | {% for neighbor in neighbors %}
38 | {% if "keepalive" in neighbor %}
39 |
40 |
41 | {{ neighbor["neighbor"] }}
42 |
43 | 0
44 | {{ neighbor["remote_as"] }}
45 |
46 |
47 | {{ neighbor["keepalive"] }}
48 | {{ neighbor["hold-time"] }}
49 |
50 |
51 | {% else %}
52 |
53 |
54 | {{ neighbor["neighbor"] }}
55 |
56 | 0
57 | {{ neighbor["remote_as"] }}
58 |
59 |
60 | {% endif %}
61 | {% endfor %}
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/templates/ntp.j2:
--------------------------------------------------------------------------------
1 |
3 |
5 |
6 |
7 | default
8 |
9 | {% for n in host.facts.ntp %}
10 |
11 |
12 | {{ n.server_ip }}
13 |
14 | server
15 | {% if n.preferred == "True" %}
16 |
17 |
18 | {% endif %}
19 |
20 |
21 |
22 | {% endfor %}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/templates/ospf.j2:
--------------------------------------------------------------------------------
1 |
3 |
5 |
6 |
7 | {{ host.facts.ospf.process_id }}
8 |
9 | {{ host.facts.ospf.router_id }}
10 |
11 | {% set areas = host.facts.ospf.areas %}
12 | {% for area in areas %}
13 |
14 |
15 | {{ area["area"] }}
16 |
17 |
18 | {% set interfaces = area["interfaces"] %}
19 | {% for intf in interfaces %}
20 |
21 |
22 | {{ intf }}
23 |
24 |
25 | {% endfor %}
26 |
27 |
28 |
29 | {% endfor %}
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/xr-demo.py:
--------------------------------------------------------------------------------
1 | """
2 | AUTHOR: IPvZero
3 | DATE: 15th January 2021
4 |
5 | WARNING: This script is designed to work on the Cisco IOS-XR Always-On Sandbox.
6 | Be aware this Sandbox is a shared resource and if others are using it at the same time
7 | you may experience failures/timeouts.
8 |
9 | INSTRUCTIONS: This script utilised the Nornir Automation Framework and the Scrapli Netconf library.
10 | You can install with either pip or pip3.
11 | PIP: python3 -m pip install -r requirements.txt
12 |
13 |
14 | PIP3: pip3 install -r requirements.txt
15 |
16 | """
17 |
18 | from nornir import InitNornir
19 | from nornir_utils.plugins.functions import print_result
20 | from nornir_utils.plugins.tasks.data import load_yaml
21 | from nornir_jinja2.plugins.tasks import template_file
22 | from nornir_scrapli.tasks import (
23 | netconf_edit_config,
24 | netconf_commit,
25 | netconf_lock,
26 | netconf_unlock,
27 | )
28 |
29 | nr = InitNornir(config_file="config.yaml")
30 |
31 |
32 | def load_vars(task):
33 | """
34 | Load host variables and bind them to a per-host dict key called "facts"
35 | """
36 |
37 | data = task.run(
38 | task=load_yaml,
39 | name="Loading Vars Into Memory...",
40 | file=f"./host_vars/{task.host}.yaml",
41 | )
42 | task.host["facts"] = data.result
43 |
44 |
45 | def lock_config(task):
46 | task.run(task=netconf_lock, target="candidate", name="Locking...")
47 |
48 |
49 | def config_bgp(task):
50 | """
51 | Build BGP config based on IOS-XR YANG Model
52 | Push configuration over NETCONF
53 | """
54 |
55 | bgp_template = task.run(
56 | task=template_file,
57 | name="Buildling BGP Configuration",
58 | template="bgp.j2",
59 | path="./templates",
60 | )
61 | bgp_output = bgp_template.result
62 | task.run(
63 | task=netconf_edit_config,
64 | name="Automating BGP",
65 | target="candidate",
66 | config=bgp_output,
67 | )
68 |
69 |
70 | def config_ospf(task):
71 | """
72 | Build OSPF config based on IOS-XR YANG Model
73 | Push configuration over NETCONF
74 | """
75 |
76 | ospf_template = task.run(
77 | task=template_file,
78 | name="Buildling OSPF Configuration",
79 | template="ospf.j2",
80 | path="./templates",
81 | )
82 | ospf_output = ospf_template.result
83 | task.run(
84 | task=netconf_edit_config,
85 | name="Automating OSPF",
86 | target="candidate",
87 | config=ospf_output,
88 | )
89 |
90 |
91 | def config_ntp(task):
92 | """
93 | Build NTP config based on IOS-XR YANG Model
94 | Push configuration over NETCONF
95 | """
96 |
97 | ntp_template = task.run(
98 | task=template_file,
99 | name="Buildling NTP Configuration",
100 | template="ntp.j2",
101 | path="./templates",
102 | )
103 | ntp_output = ntp_template.result
104 | task.run(
105 | task=netconf_edit_config,
106 | name="Automating NTP",
107 | target="candidate",
108 | config=ntp_output,
109 | )
110 |
111 |
112 | def config_acl(task):
113 | """
114 | Build ACL config based on IOS-XR YANG Model
115 | Push configuration over NETCONF
116 | """
117 |
118 | acl_template = task.run(
119 | task=template_file,
120 | name="Buildling Configuration",
121 | template="acl.j2",
122 | path="./templates",
123 | )
124 | acl_output = acl_template.result
125 | task.run(
126 | task=netconf_edit_config,
127 | name="Automating ACL",
128 | target="candidate",
129 | config=acl_output,
130 | )
131 |
132 |
133 | def commit_configs(task):
134 | """
135 | Commit the configuration changes.
136 | This moves then into the running config.
137 | """
138 | task.run(
139 | task=netconf_commit, name="Committing Changes into the Running Configuration"
140 | )
141 |
142 |
143 | def unlock_config(task):
144 | task.run(task=netconf_unlock, target="candidate")
145 |
146 |
147 | # DON'T COMMENT THIS ONE OUT - THIS LOADS THE VARIABLE DATA
148 | var = nr.run(task=load_vars)
149 | print_result(var)
150 |
151 | # DON'T COMMENT THIS ONE OUT - THIS LOCKS THE CONFIGURATION DATASTORE
152 | locker = nr.run(task=lock_config, name="NETCONF_LOCK")
153 | print_result(locker)
154 |
155 | # COMMENT THIS OUT IF YOU WANT TO SKIP BGP CONFIG
156 | bgp_results = nr.run(task=config_bgp, name="BGP CONFIGURATION")
157 | print_result(bgp_results)
158 |
159 | # COMMENT THIS OUT IF YOU WANT TO SKIP OSPF CONFIG
160 | ospf_results = nr.run(task=config_ospf, name="OSPF CONFIGURATION")
161 | print_result(ospf_results)
162 |
163 | # COMMENT THIS OUT IF YOU WANT TO SKIP NTP CONFIG
164 | ntp_results = nr.run(task=config_ntp, name="NTP CONFIGURATION")
165 | print_result(ntp_results)
166 |
167 | # COMMENT THIS OUT IF YOU WANT TO SKIP ACL CONFIG
168 | acl_results = nr.run(task=config_acl, name="ACL CONFIGUTATION")
169 | print_result(acl_results)
170 |
171 | # DON'T COMMENT THIS ONE OUT - THIS PUSHES THE CHANGES TO THE RUNNING CONFIG
172 | commit_results = nr.run(task=commit_configs, name="NETCONF_COMMIT")
173 | print_result(commit_results)
174 |
175 | # DON'T COMMENT THIS OUT - THIS UNLOCKS THE CONFIGURATION DATASTORE
176 | unlocker = nr.run(task=unlock_config, name="NETCONF_UNLOCK")
177 | print_result(unlocker)
178 |
--------------------------------------------------------------------------------