Configuring multi-area OSPF with Ansible
This time we're going to configure OSPF, but not with the CLI, we're going to use a network automation tool called Ansible. I'm going to walk through the steps how we can configure a relatively simple multi-area topology just with Ansible.

Ansible is an agentless network automation tool, it doesn't require any special software to run on the managed nodes (aka. on the routers). It uses SSH as a transport to connect to the network devices and it pushes the desired configuration to them. We write the tasks in a YAML file called Playbook usually defining the end-state in a declarative approach, which then Ansible analyses and then takes the required actions and makes configuration changes. This time we're going to use GNS3 with the following topology:

We're going to use a Linux machine as a Control Node which runs as a Docker container instance in GNS3 and already has Ansible installed. I've already configured the IP addresses on the devices and created a Mgmt VRF on all of the routers. These interfaces are in the common subnet 192.168.1.0/24, we're going to use these interfaces to SSH into the routers, because there's obviously no routing established yet between the routers. Notice that these interfaces are not in the global routing table, so the OSPF configuration won't affect them, we're only going to configure the interfaces in the global routing table. So first let's assign an IP address to the Control Node as well:
root@NetworkAutomation-1:~/ansible_lab# ifconfig eth0 192.168.1.100 netmask 255.255.255.0 up
Next we need to enable SSH on all routers, and create a user with privilege level 15. To enable SSH we have to generate an RSA cryptographic key pair first. To be able to do that we also have to assign a domain name to the routers first, this is how we do it on R1:
R1(config)#ip domain name test.com
R1(config)#crypto key generate rsa mod 2048
The name for the keys will be: R1.test.com
% The key modulus size is 2048 bits
% Generating 2048 bit RSA keys, keys will be non-exportable...
[OK] (elapsed time was 1 seconds)
%SSH-5-ENABLED: SSH 1.99 has been enabled
Next we enable SSH on the first five vty lines and we use the local database for authentication:
R1(config)#line vty 0 4
R1(config-line)#transport input ssh
R1(config-line)#login local
R1(config)#username ansible priv 15 secret cisco
Now we create the Inventory or the hosts
file in other words on the Control Node, this is where we list all of the devices and the characteristics of the devices:
[routers]
R1 ansible_host=192.168.1.1
R2 ansible_host=192.168.1.2
R3 ansible_host=192.168.1.3
R4 ansible_host=192.168.1.4
[routers:vars]
ansible_connection=network_cli
ansible_network_os=ios
ansible_user=ansible
ansible_password=cisco
In this case I assign an alias to all to the routers: R1 is 192.168.1.1, R2 is 192.168.1.2 etc. In the [routers:vars]
section we have some variables which are common for all of the devices: these are all Cisco IOS routers, and we use SSH to connect to the routers with the same username (ansible) and password (cisco). We can also write this in YAML if we want, here I choose to use the regular INI format.
Feel free to download this, and also every other YAML file related to this lab from my Git repository.
If we don't have all of the public SSH host keys from all of the device in the .ssh/known_hosts
file on our Control Node, it can be advantageous to turn off the host_key_checking
on the Linux host, otherwise Ansible won't run the playbooks. Ansible uses the following global configuration file: /etc/ansible/ansible.cfg
, here we can turn it off:
[defaults]
host_key_checking=False
Just search for the line and uncomment it, or write it under the [defaults]
section. Before writing the playbooks, let's check if we can actually reach our routers defined in the hosts
file:
root@NetworkAutomation-1:~/ansible_lab# ansible routers -m ping -i hosts
R4 | SUCCESS => {
"changed": false,
"ping": "pong"
}
R1 | SUCCESS => {
"changed": false,
"ping": "pong"
}
R2 | SUCCESS => {
"changed": false,
"ping": "pong"
}
R3 | SUCCESS => {
"changed": false,
"ping": "pong"
}
We can also gather some information from our routers with the following command before running the actual playbook:
root@NetworkAutomation-1:~/ansible_lab# ansible routers -m ios_facts -i hosts
R1 | SUCCESS => {
"ansible_facts": {
"ansible_net_api": "cliconf",
"ansible_net_gather_network_resources": [],
"ansible_net_gather_subset": [
"default"
],
"ansible_net_hostname": "R1",
"ansible_net_image": "flash0:/vios-adventerprisek9-m",
"ansible_net_iostype": "IOS",
"ansible_net_model": "IOSv",
"ansible_net_operatingmode": "autonomous",
"ansible_net_python_version": "3.8.5",
"ansible_net_serialnum": "9H24ZY0WDM1GYI1FBLGYZ",
"ansible_net_system": "ios",
"ansible_net_version": "15.6(2)T",
"ansible_network_resources": {}
},
"changed": false
}
R2 | SUCCESS => {
"ansible_facts": {
"ansible_net_api": "cliconf",
"ansible_net_gather_network_resources": [],
"ansible_net_gather_subset": [
"default"
],
"ansible_net_hostname": "R2",
"ansible_net_image": "flash0:/vios-adventerprisek9-m",
"ansible_net_iostype": "IOS",
"ansible_net_model": "IOSv",
"ansible_net_operatingmode": "autonomous",
"ansible_net_python_version": "3.8.5",
"ansible_net_serialnum": "99U0TV7PNS827AOMV8VWM",
"ansible_net_system": "ios",
"ansible_net_version": "15.6(2)T",
"ansible_network_resources": {}
},
"changed": false
}
R3 | SUCCESS => {
"ansible_facts": {
"ansible_net_api": "cliconf",
"ansible_net_gather_network_resources": [],
"ansible_net_gather_subset": [
"default"
],
"ansible_net_hostname": "R3",
"ansible_net_image": "flash0:/vios-adventerprisek9-m",
"ansible_net_iostype": "IOS",
"ansible_net_model": "IOSv",
"ansible_net_operatingmode": "autonomous",
"ansible_net_python_version": "3.8.5",
"ansible_net_serialnum": "9A0INR4IAY5JPCZE1N7UI",
"ansible_net_system": "ios",
"ansible_net_version": "15.6(2)T",
"ansible_network_resources": {}
},
"changed": false
}
R4 | SUCCESS => {
"ansible_facts": {
"ansible_net_api": "cliconf",
"ansible_net_gather_network_resources": [],
"ansible_net_gather_subset": [
"default"
],
"ansible_net_hostname": "R4",
"ansible_net_image": "flash0:/vios-adventerprisek9-m",
"ansible_net_iostype": "IOS",
"ansible_net_model": "IOSv",
"ansible_net_operatingmode": "autonomous",
"ansible_net_python_version": "3.8.5",
"ansible_net_serialnum": "9HDG0VKB6SJWEJNQ63JR7",
"ansible_net_system": "ios",
"ansible_net_version": "15.6(2)T",
"ansible_network_resources": {}
},
"changed": false
}
Next let's write our playbook in YAML. If you're using a Linux machine without GUI, just as I do in this example, don't try to write YAML in vim
or nano
! Instead write it in VS Code or any kind of editor for syntax checking, indentation is very important in YAML, if you miss a space, Ansible simply won't run the playbook.
So we need the following requirements for the topology above: R4 should be configured as an ASBR, advertising the 0.0.0.0/0 default route as an E1
route with a metric of 4. There is no default route present in the RIB of R4, so we also need the always
keyword as well, the command would be default-information originate always
if we were using the CLI. R3 is just a regular backbone router with no additional configuration. R2 is an ABR, and we should configure area 1 as a Totally Not-So-Stubby area, so we need the no-summary
keyword on the ABR. R1 is also an ASBR in the NSSA area which should redistribute his connected routes into the NSSA area. We have one additional connected route on R1 which is not advertised by OSPF: his loopback1 address which is 11.11.11.11/32. So after converging we should have an O N2
route on the ABR which will be converted into O E2
when it is injected into the backbone. Additionally on every router we set the reference bandwidth to 100 Gbps, so every interface should have a cost of 100.
---
- hosts: R4
tasks:
- name: Configuring OSPF on R4
cisco.ios.ios_ospfv2:
config:
processes:
- process_id: 1
areas:
- area_id: "0"
network:
- address: 0.0.0.0
wildcard_bits: 255.255.255.255
area: 0
default_information:
originate: true
always: true
metric_type: 1
metric: 4
auto_cost:
reference_bandwidth: 100000
- hosts: R3
tasks:
- name: Configuring OSPF on R3
cisco.ios.ios_ospfv2:
config:
processes:
- process_id: 1
areas:
- area_id: "0"
network:
- address: 0.0.0.0
wildcard_bits: 255.255.255.255
area: 0
auto_cost:
reference_bandwidth: 100000
- hosts: R2
tasks:
- name: Configuring OSPF on R2
cisco.ios.ios_ospfv2:
config:
processes:
- process_id: 1
areas:
- area_id: "0"
- area_id: "1"
nssa:
no_summary: true
set: true
network:
- address: 10.0.23.0
wildcard_bits: 0.0.0.255
area: 0
- address: 2.2.2.2
wildcard_bits: 0.0.0.0
area: 0
- address: 10.0.12.0
wildcard_bits: 0.0.0.255
area: 1
auto_cost:
reference_bandwidth: 100000
- hosts: R1
tasks:
- name: Configuring OSPF on R1
cisco.ios.ios_ospfv2:
config:
processes:
- process_id: 1
areas:
- area_id: "1"
nssa:
set: true
network:
- address: 10.0.12.0
wildcard_bits: 0.0.0.255
area: 1
- address: 1.1.1.1
wildcard_bits: 0.0.0.0
area: 1
auto_cost:
reference_bandwidth: 100000
state: overridden
- name: Redistributing connected routes on R1
cisco.ios.ios_config:
lines:
- router ospf 1
- redistribute connected subnets
parents: router ospf 1
You shouldn't memorize all of these syntax, we just have to use the documentation of the cisco.ios.ios_ospfv2
module. It shows how we should do the indentation, what keyword we should use and also has some examples at the end. Besides the cisco.ios.ios_ospfv2
I also had to use another module called cisco.ios.ios_config
for the redistribution. Here we have all of the IOS modules. So let's run the playbook:
root@NetworkAutomation-1:~/ansible_lab# ansible-playbook playbook_ospf.yaml -i hosts
PLAY [R4] *******************************************************************************
TASK [Gathering Facts] *******************************************************************************
ok: [R4]
TASK [Configuring OSPF on R4] *******************************************************************************
changed: [R4]
PLAY [R3] *******************************************************************************
TASK [Gathering Facts] *******************************************************************************
ok: [R3]
TASK [Configuring OSPF on R3] *******************************************************************************
changed: [R3]
PLAY [R2] *******************************************************************************
TASK [Gathering Facts] *******************************************************************************
ok: [R2]
TASK [Configuring OSPF on R2] *******************************************************************************
changed: [R2]
PLAY [R1] *******************************************************************************
TASK [Gathering Facts] *******************************************************************************
ok: [R1]
TASK [Configuring OSPF on R1] *******************************************************************************
changed: [R1]
TASK [Redistributing connected routes on R1] *******************************************************************************
changed: [R1]
PLAY RECAP *******************************************************************************
R1 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
R2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
R3 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
R4 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
It looks like Ansible successfully finished the tasks and made the configuration changes.
Verfication
Now let's use the CLI and take a look at the OSPF configuration of each router:
R1#show run | sec ospf
router ospf 1
auto-cost reference-bandwidth 100000
area 1 nssa
redistribute connected subnets
network 1.1.1.1 0.0.0.0 area 1
network 10.0.12.0 0.0.0.255 area 1
R2#show run | sec ospf
router ospf 1
auto-cost reference-bandwidth 100000
area 1 nssa no-summary
network 2.2.2.2 0.0.0.0 area 0
network 10.0.12.0 0.0.0.255 area 1
network 10.0.23.0 0.0.0.255 area 0
R3#show run | sec ospf
router ospf 1
auto-cost reference-bandwidth 100000
network 0.0.0.0 255.255.255.255 area 0
R4#show run | sec ospf
router ospf 1
auto-cost reference-bandwidth 100000
network 0.0.0.0 255.255.255.255 area 0
default-information originate always metric 4 metric-type 1
This is what we wanted to configure with the YAML file. Let's also verify the LSDBs and the routing tables. R1 should generate a single Type-7 NSSA external LSA for the 11.11.11.11/32 prefix:
R1#show ip ospf database nssa-external
OSPF Router with ID (11.11.11.11) (Process ID 1)
Type-7 AS External Link States (Area 1)
LS age: 513
Options: (No TOS-capability, Type 7/5 translation, DC, Upward)
LS Type: AS External Link
Link State ID: 11.11.11.11 (External Network Number )
Advertising Router: 11.11.11.11
LS Seq Number: 80000001
Checksum: 0x3EFB
Length: 36
Network Mask: /32
Metric Type: 2 (Larger than any link state path)
MTID: 0
Metric: 20
Forward Address: 1.1.1.1
External Route Tag: 0
R1 should also have a single Type-3 Summary LSA for the 0.0.0.0/0 default route from the ABR (R2) because we configured the NSSA with the no-summary
keyword:
R1#show ip ospf database summary
OSPF Router with ID (11.11.11.11) (Process ID 1)
Summary Net Link States (Area 1)
LS age: 552
Options: (No TOS-capability, DC, Upward)
LS Type: Summary Links(Network)
Link State ID: 0.0.0.0 (summary Network Number)
Advertising Router: 2.2.2.2
LS Seq Number: 80000001
Checksum: 0xFC31
Length: 28
Network Mask: /0
MTID: 0 Metric: 1
O N2
route from R1 for 11.11.11.11/32 with a default cost of 20, and we should have an O E1
route from R4 for 0.0.0.0/0 with a cost of 204 (100 to get to R3 + 100 to get to R4 + 4 for the default route):R2#show ip route ospf | begin Gateway
Gateway of last resort is 10.0.23.3 to network 0.0.0.0
O*E1 0.0.0.0/0 [110/204] via 10.0.23.3, 00:01:26, GigabitEthernet0/1
1.0.0.0/32 is subnetted, 1 subnets
O 1.1.1.1 [110/101] via 10.0.12.1, 00:01:16, GigabitEthernet0/0
3.0.0.0/32 is subnetted, 1 subnets
O 3.3.3.3 [110/101] via 10.0.23.3, 00:01:26, GigabitEthernet0/1
4.0.0.0/32 is subnetted, 1 subnets
O 4.4.4.4 [110/201] via 10.0.23.3, 00:01:26, GigabitEthernet0/1
10.0.0.0/8 is variably subnetted, 5 subnets, 2 masks
O 10.0.34.0/24 [110/200] via 10.0.23.3, 00:01:26, GigabitEthernet0/1
11.0.0.0/32 is subnetted, 1 subnets
O N2 11.11.11.11 [110/20] via 10.0.12.1, 00:01:16, GigabitEthernet0/0
R4#show ip ospf database external
OSPF Router with ID (4.4.4.4) (Process ID 1)
Type-5 AS External Link States
LS age: 801
Options: (No TOS-capability, DC, Upward)
LS Type: AS External Link
Link State ID: 0.0.0.0 (External Network Number )
Advertising Router: 4.4.4.4
LS Seq Number: 80000001
Checksum: 0x5DC2
Length: 36
Network Mask: /0
Metric Type: 1 (Comparable directly to link state metric)
MTID: 0
Metric: 4
Forward Address: 0.0.0.0
External Route Tag: 1
LS age: 738
Options: (No TOS-capability, DC, Upward)
LS Type: AS External Link
Link State ID: 11.11.11.11 (External Network Number )
Advertising Router: 2.2.2.2
LS Seq Number: 80000001
Checksum: 0xE186
Length: 36
Network Mask: /32
Metric Type: 2 (Larger than any link state path)
MTID: 0
Metric: 20
Forward Address: 1.1.1.1
External Route Tag: 0
So we can confirm that OSPF has converged and we have the LSAs which we wanted.
Setting the DNS servers for all routers
Let's do one more modification, we'll configure two DNS servers for all of the routers with another playbook. This will be common for all of the routers, so we just define the hosts
as the routers
which contains all four routers:
---
- hosts: routers
tasks:
- name: Configuring DNS servers on all routers
cisco.ios.ios_system:
name_servers:
- 1.1.1.1
- 8.8.8.8
And let's run the playbook:
root@NetworkAutomation-1:~/ansible_lab# ansible-playbook playbook_dns.yaml -i hosts
PLAY [routers] *******************************************************************************
TASK [Gathering Facts] *******************************************************************************
ok: [R4]
ok: [R1]
ok: [R3]
ok: [R2]
TASK [Configuring DNS servers on all routers] *******************************************************************************
changed: [R2]
changed: [R1]
changed: [R4]
changed: [R3]
PLAY RECAP *******************************************************************************
R1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
R2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
R3 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
R4 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Ansible made some changes according to the output, let's verify on R1:
R1#show run | sec name-server
ip name-server 1.1.1.1
ip name-server 8.8.8.8
Indeed, now we have two DNS servers configured on all of the routers. These are the situation where Ansible can be really powerful, imagine we have set the same DNS or NTP servers for hundreds or thousands of routers. You don't have to SSH into all of the routers, and type the same configuration like thousand times. You just run a playbook with a few lines in a YAML file with a single task for all of the routers.