Working with ipaddr in Ansible

This blog post from the folks at NetworkToCode about the DRY principle (Don’t Repeat Yourself) reminded me of our early network automation days. There were playbooks involved, external variable definition files as a first step to source of truth and, well, lot’s of redundant data …

As every network engineer has to deal with subnets/prefixes or ip addresses in one form or another, this makes up a great use case to explain DRY in a practical manner. This post is about the Ansible Jinja2 ipaddr integration, which eases ip address operations and help to simplify your data model.

Simplify the data model

First of all we should spend some time on a reasonable data model, preferably way before the first line of Ansible code is written. Let’s take a look at a data structure as a negative example. Someone, not me … ;-), just exported some variables from an existing playbook as a dictionary to an external YAML file:

This kind of approach is quite error prone, bloats your data model with redundant information and needs to separate IPv4 and IPv6 subnets, because of the different prefix length paradigm. So here is what we really want to document in the first place:

And that’s a lesson to learn right there:
Keep your data model as simple but precise as possible.

Think about it, the second version contains exactly the same information, if there is a ‘first usable address = default gateway’ policy in place. Since these data model files are the point of configuration in day-to-day operations, it is wise to put more effort in developing a smart playbook and keep the data model simple and small. We just have to embed some magic into our playbook tasks to convert each subnet to the specific parameter needed.

Prerequisits

You need a working Ansible installation on your host and the additional netaddr Python module:

pip install netaddr

Source of truth validation

It is common best practice to check for valid subnets before executing tasks on the list. This can be part of the playbook itself or external in some form of pipeline like Github/lab CI or Jenkins. Those pipelines are also a great place to check for a valid YAML syntax via YAMLint, showing you exactly what went wrong where in your file, by the way.
Ansible uses asserts to handle validation tasks, like this one:

  - name: All subnets have valid format
    assert:
      that:
        - subnets | length == subnets | ipaddr('net') | length
      fail_msg: "Some subnets are not in a valid range or have the wrong format (network/prefixlength)"
      success_msg: "All subnets valid"

The assert just compares the length of the subnets list to the length of a new generated list of valid subnets and fails if they are different.

IP address operations

The following ‘cheat sheet’ table shows how to generate common IP address information from a list of subnets. Those Jinja2 operations can be used at any point in an Ansible playbook, visit the Ansible homepage for a full documentation of the ipaddr filter.

IPv4IPv6
{{ subnets }}10.0.0.0/242001:db8:10::/64
{{ subnets | ipaddr }}10.0.0.0/242001:db8:10::/64
{{ subnets | ipv4 }}10.0.0.0/24
{{ subnets | ipv6 }}2001:db8:10::/64
{{ subnets | ipaddr(‘network’) }}10.0.0.02001:db8:10::
{{ subnets | ipaddr(‘netmask’) }}255.255.255.0ffff:ffff:ffff:ffff::
{{ subnets | ipaddr(‘hostmask’) }}0.0.0.255::ffff:ffff:ffff:ffff
{{ subnets | ipaddr(‘prefix’) }}2464
{{ subnets | ipaddr(‘net’) | ipaddr(‘1’) }}10.0.0.1/242001:db8:10::1/64
{{ subnets | ipaddr(‘network’) | ipaddr(‘2’) | ipaddr(‘address’) }}10.0.0.22001:db8:10::2
{{ subnets | ipaddr(‘broadcast’) }}10.0.0.255LOL

Real life

So how to leverage this in real life?
Given a task to automate a VTY access list on all your network devices based on a list of subnets like the example above. As at least Cisco IOS uses a slightly different ipv4 vs. ipv6 syntax, we need to separate those address families. One of many possible declarative Ansible IPv4 implementations would look like this:

- name: DELETE IPV4 VTY ACL 199
  ios_config:
    lines: no access-list 199

- name: CONFIGURE IPV4 VTY ACL 199 LINE-BY-LINE
  ios_config:
    lines: access-list 199 permit tcp {{ item | ipaddr('network')}} {{ item | ipaddr('hostmask') }} any eq 22
  loop: "{{ subnets | ipv4 | ipaddr('net') }}"

Pretty much self explanatory considering the table above. The second task loops over a new generated list of only IPv4 subnets in the right format (network/CIDR).

I hope this short piece demonstrates the value of a well thought data model and that the execution on the playbook side is far away from rocket science – it might save you a lot of time though.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.