Network configuration with Ansible

It’s the next morning, and in our story, the crisis has been averted…​ but the way you fixed that router wasn’t exactly sustainable. Manually copying and pasting configs in the middle of the night? We can do better than that!

Infrastructure as Code

We’ll be making use of Network Resource Modules, which differ from other module designs you may have worked with in Ansible.

Ansible network resource modules simplify and standardize how you manage different network devices. Network devices separate configuration into sections (such as interfaces and VLANs) that apply to a network service. Ansible network resource modules take advantage of this to allow you to configure subsections or resources within the network device configuration. Network resource modules provide a consistent experience across different network devices.

Source and more info: Ansible Documentation

In the file explorer on the left side of your VS Code environment, expand the playbooks folder, then click on gather_ios_routing.yml to open it.

gather

This is a playbook that gathers a minimal configuration from rtr1 in our lab using resource modules cisco.ios.ios_interfaces and cisco.ios.ios_bgp_global. You can see that the state parameter is set to "gathered".

Since we are only using the "gathered" resource state, this playbook will be read-only. So, we can try this out as much as we want without affecting anything.

Let’s run it (using -vvv for verbosity level 3):

ansible-navigator run playbooks/gather_ios_routing.yml -vvv

In the first task’s output, we see:

ok: [rtr1] => {
    "changed": false,
    "gathered": [
        {
            "enabled": true,
            "name": "GigabitEthernet1"
        },
        {
            "enabled": true,
            "name": "Loopback0"
        },
        {
            "enabled": true,
            "name": "Tunnel0"
        },
        {
            "enabled": true,
            "name": "Tunnel1"
        },
[...]
With Ansible Network Resources, the interfaces resource only contains basic interface configuration, such as enabled true/false. There are other interface resources (l2_interfaces and l3_interfaces) for Layer 2 and Layer 3 configuration aspects.

In the second task’s output, we see:

ok: [rtr1] => {
    "changed": false,
    "gathered": {
        "as_number": "65000",
        "bgp": {
            "default": {
                "ipv4_unicast": true,
                "route_target": {
                    "filter": true
                }
            },
            "log_neighbor_changes": true,
            "router_id": {
                "address": "192.168.1.1"
            }
        },
        "neighbors": [
            {
                "ebgp_multihop": {
                    "enable": true,
                    "hop_count": 255
                },
                "neighbor_address": "10.200.200.2",
                "remote_as": "65001"
            }
        ]
    },
[...]

This is the same configuration that we just saw in Exercise 1.2, but this time we have the module presenting it in a structured data format that’s standard across network platforms.

Right now we see the data in JSON format, but JSON and YAML are compatible formats, and Ansible has filter plugins like to_nice_yaml available to convert between them. This capturing and converting has been done ahead of time, as we’ll see next.

In the file explorer, expand the host_vars directory under playbooks. There is a directory here for each router in the environment. Expand rtr1 and take a look at the files interfaces.yml and bgp_global.yml.

host-vars

These files are YAML representations of the data we just saw (with one addition for BGP which we will look at later). The locations of these files will get them loaded into Ansible as part of the inventory.

You can look at the other files for more examples of other network resources available, and you can also look at the same files under the rtr2 directory to note the differences.

Collapse host_vars before moving on.

Next, click on configure_ios_routing.yml to open it.

This is a playbook that applies the configuration we just looked at. Let’s dig into a few things about this playbook:

  • The hosts line

    • Later in the lab, Event-Driven Ansible (EDA) is going to identify which devices in our lab have a bad configuration. We want to be able to only run remediation on devices that we know need it, which is what the ansible_eda variable is for.

    • We don’t have EDA involved yet, so ansible_eda is not defined, and we’ll therefore default to rtr1 and rtr2 for now.

  • The resource state parameter

    • For the sake of this lab, we’re using state merged, which is the least destructive state we can use, and is recommended for testing.

    • You can check out the documentation on Network Resource Modules for a description of the available states and why you’d want to use each one. The replaced state, for example, would wipe out any configuration in the relevant subsection not found in the resource data, which is helpful for compliance and drift control.

  • The resource config parameter

    • The module inputs are very minimal, as they reference variable data. This data comes from the inventory, which we just looked at.

This playbook and its resource data represent an Infrastructure as Code approach to managing the configuration on our routers. The host_vars files we looked at will be our source of truth for this lab’s network configuration.

We want to treat this code as the definitive source for what the router configurations should be, regardless of what’s currently applied. With a source of truth in place, if you want to make a change, you modify the source of truth, not the device directly. Automation is responsible for reconciling the two.

Let’s run this playbook a couple of times.

For our first run, use the following command (using -vv for verbosity level 2):

ansible-navigator run playbooks/configure_ios_routing.yml -vv

We should see the first task report ok - so nothing changed. But the in the second task’s output, you should see something like below:

changed: [rtr1] \=> {"after": {"as_number": "65000", "bgp": {"default": {"ipv4_unicast": true, "route_target": {"filter": true}}, "log_neighbor_changes": true, "router_id": {"address": "192.168.1.1"}}, "neighbors": [{"neighbor_address": "10.200.200.2", "remote_as": "65001"}]}, "before": {"as_number": "65000", "bgp": {"default": {"ipv4_unicast": true, "route_target": {"filter": true}}, "log_neighbor_changes": true, "router_id": {"address": "192.168.1.1"}}, "neighbors": [{"neighbor_address": "10.200.200.2", "remote_as": "65001"}]}, "changed": true, "commands": ["router bgp 65000", "timers bgp 5 15"]}

Refer to the commands output at the end (scroll all the way right in the box above) to see that the resource module was able to figure out which commands needed to be run to accomplish the configuration, even though our configuration data is stored as YAML. We were only missing the timers part. This is for the lab so that BGP can more quickly tell when something is wrong, and we’ll be making use of that later.

Run the playbook again and drop the verbosity:

ansible-navigator run playbooks/configure_ios_routing.yml

This time both of the tasks report ok. This means that our live configuration still matches our source of truth. It also means that the playbook is idempotent, meaning that we can run this playbook as many times as we want without applying any change, as long as the configuration stays correct.

Recovering from a misconfiguration

Time to break things. Let’s start by logging into rtr1 over SSH and looking at the output of a couple commands.

Run the following:

ssh rtr1
show ip interface brief

Example output:

rtr1#show ip interface brief
Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet1       172.16.147.219  YES DHCP   up                    up
Loopback0              192.168.1.1     YES manual up                    up
Tunnel0                10.100.100.1    YES manual up                    up
Tunnel1                10.200.200.1    YES manual up                    up
VirtualPortGroup0      192.168.35.101  YES TFTP   up                    up

We can see that all of the interfaces on the router are up, including Tunnel0, which we identified earlier as being involved in OSPF. Let’s look at that next.

Run the following:

show ip ospf neighbor

Expected output:

rtr1#show ip ospf neighbor

Neighbor ID     Pri   State           Dead Time   Address         Interface
192.168.3.3       0   FULL/  -        00:00:30    10.100.100.2    Tunnel0

Again we see the importance of Tunnel0. What happens if Tunnel0 gets shut down? Let’s find out.

Run the following commands:

configure terminal
interface Tunnel0
shutdown
end
When copy/pasting multi-line blocks like this in the terminal, all lines except the last one will automatically be issued. Be sure to hit [Enter] after pasting so that all lines are issued.

Check to see that the interface came down:

show ip interface brief

Example output:

rtr1#show ip interface brief
Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet1       172.16.147.219  YES DHCP   up                    up
Loopback0              192.168.1.1     YES manual up                    up
Tunnel0                10.100.100.1    YES manual administratively down down
Tunnel1                10.200.200.1    YES manual up                    up
VirtualPortGroup0      192.168.35.101  YES TFTP   up                    up

And see its effect on OSPF:

show ip ospf neighbor

You get no output from running this command, which is not good. We should turn Tunnel0 back on. But, rather than doing that manually, we can have Ansible get us back to a known-good state.

Exit out of the SSH session and run the configuration playbook that we were running before.

exit
ansible-navigator run playbooks/configure_ios_routing.yml

We should get changed reported only on rtr1 and only on the interface configuration task.

[student@ansible-1 telemetry]$ ansible-navigator run playbooks/configure_ios_routing.yml

PLAY [Configure IOS Routing] **********************************************************************************

TASK [Apply interfaces config] ********************************************************************************
ok: [rtr2]
changed: [rtr1]

TASK [Apply BGP Global config] ********************************************************************************
ok: [rtr1]
ok: [rtr2]

PLAY RECAP ****************************************************************************************************
rtr1                       : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
rtr2                       : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

SSH back into rtr1 and check the interface state again.

ssh rtr1
show ip interface brief
show ip ospf neighbor

We should see that our state is back to normal. This shows us that we can use this playbook to recover from misconfiguration issues.

Exit your SSH session before moving on.

exit