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