Project structure#
The Ansible Inventory#
In this project, we use the native file-based Ansible inventory. It lists the hosts that are part of the fabric and groups them in a way that reflects the fabric topology. The inventory file - ansible-inventory.yml
- is located in the inv
directory; host_vars
and group_vars
directories next to it contain host- and group-specific variables.
inv
├── ansible-inventory.yml # the inventory file
├── group_vars
│  └── srl.yml # group-specific variables for the srl group
└── host_vars
├── clab-4l2s-l1.yml # host-specific variables for the clab-4l2s-l1 host
├── clab-4l2s-l2.yml
├── clab-4l2s-l3.yml
├── clab-4l2s-l4.yml
├── clab-4l2s-s1.yaml
└── clab-4l2s-s2.yml
Ansible is instructed to use this inventory file by setting inventory = inv
in the ansible.cfg
configuration file.
The ansible-inventory.yml
defines four groups:
srl
- for all SR Linux nodesspine
- for the spine nodesleaf
- for the leaf nodes.hosts
- for emulated hosts.
The host_vars
directory contains a file for each host that defines host-specific variables. The group_vars
directory contains a single file for the srl
group to define Ansible-specific variables that are required for the JSON-RPC connection-plugin as well as some system-level configuration data.
The Ansible Playbook#
The Ansible playbook cf_fabric.yml
is the main entry point for the project. It contains a single play that applies a sequence of roles to all nodes in the leaf
and spine
groups:
cf_fabric.yml
- name: Configure fabric
gather_facts: no
hosts:
- leaf
- spine
vars:
purge: yes # purge resources from device not in intent
purgeable:
- interface
- subinterface
- network-instance
- tunnel-interface
roles:
## INIT ##
- { role: common/init, tags: [always] }
## INFRA ##
- { role: infra/system, tags: [infra, system] }
- { role: infra/interface, tags: [infra, interface] }
- { role: infra/policy, tags: [infra, policy] }
- { role: infra/networkinstance, tags: [infra] }
## SERVICES ##
- { role: services/l2vpn, tags: [services, l2vpn] }
- { role: services/l3vpn, tags: [services, l3vpn] }
## CONFIG PUSH ##
- { role: common/configure, tags: [always] }
The playbook is structured in 3 sections:
- the
hosts
variable at play-level defines the hosts that are part of the fabric. In this case, all hosts in theleaf
andspine
groups. Group definition and membership is defined in the inventory file. - the
vars
variable defines variables that are used by the roles. In this case, thepurge
variable is set toyes
to remove resources from the nodes that are not defined in the intent. Thepurgeable
variable defines the resource types that are purged from the nodes when missing from the intent. In this case, these resources are: interfaces, sub-interfaces and network instances. - the
roles
variable defines the roles that are applied to the hosts in theleaf
andspine
groups. The roles are applied in the order they are defined in the playbook. The roles are grouped in 4 sections:INIT
,INFRA
,SERVICES
andCONFIG
.- INIT: This section initializes some extra global variables or Ansible facts that are used by other roles. These facts include:
- the current 'running config' of the device
- the SR Linux software version
- the LLDP neighborship states
- INFRA: This section configures the infrastructural network resources needed for services to operate. It configures the inter-switch interfaces, base routing, policies and the default instance
- SERVICES: This section configures the services on the nodes. It configures the L2VPN and L3VPN services based on a high-level abstraction defined in each role's variables
- CONFIG: This section applies configuration to the nodes. It is always executed, even if no changes are made to the configuration. This is to ensure that the configuration on the node is always in sync with the intent.
- INIT: This section initializes some extra global variables or Ansible facts that are used by other roles. These facts include:
The common/init
role checks if the ENV
environment variable is set. If it's missing, the playbook will fail. The value of the ENV
variable is used to select the correct role variables that represent the intent. This is to support multiple environments, like 'test' and 'prod' environments, for which intents may be different. In this project, only the test
environment is defined.
Roles also have tags associated with them to run a subset of the roles in the playbook. For example, to only run the infra
roles, you can use the following command:
Note
To leverage the pruning capability of the playbook, all roles must be executed to achieve a full intent. If tags are specified for a partial run, no purging will be performed by the playbook.
Role structure#
This project provides a set of Ansible roles to manage the resources on SR Linux nodes. The roles are organized in a directory structure that reflects the configuration section of the nodes it manages.
The roles are grouped in the following directories:
roles
├── common
│  ├── configure
│  └── init
├── infra
│  ├── interface
│  ├── networkinstance
│  ├── policy
│  └── system
├── services
│  ├── l2vpn
│  └── l3vpn
└── utils
├── interface
├── load_intent
├── network-instance
└── policy
The infra
and services
roles operate on the configuration of the underlay of the fabric and the services that run on it respectively. Each of the roles in these directories contributes to an global intent for the SR Linux node.
INFRA roles#
Following INFRA roles are defined:
interface
: manages intent for interfaces in the underlay configurationnetworkinstance
: manages intent for the 'default' network-instancepolicy
: manages intent for routing policies in the underlay configurationsystem
: manages system-wide configuration of the node
The generic structure of the infra
roles is as follows:
├── tasks
│  └── main.yml
├── templates
└── vars
├── prod
└── test
└── xxx.yml # the intent
The tasks/main.yml
file defines the tasks that are executed by the role. The templates
folder contains jinja templates per supported platform; these templates are used by the role when executing tasks. Let's look at the infra/interface
role as an example:
roles/infra/interface/tasks/main.yml
- set_fact:
my_intent: {}
- name: "Load vars for ENV:{{ env }}"
include_vars:
dir: "{{ lookup('env', 'ENV') }}" # Load vars from files in 'dir'
- name: "{{ ansible_role_name}}: Load Intent for /interfaces"
ansible.builtin.include_role:
name: utils/load_intent
- name: Generate itf descriptions
set_fact:
itf_desc: "{{ lookup('template', template_file, template_vars=my_intent) | from_yaml }}"
when: lldp_nbrs is defined
vars:
template_file: "{{ platform + '/' + sw_version + '/set_itf_desc.j2'}}"
- set_fact:
my_intent: "{{ my_intent | combine(itf_desc, recursive=true) }}"
- set_fact:
intent: "{{ intent | default({}) | combine(my_intent, recursive=true) }}"
The infra/interface
role loads the host-specific intent by calling another role - utils/load_intent
. This role takes the group- and host-level intents from the vars/${ENV}
folder - in our case ENV=test
- and merges them into a single role-specific intent (my_intent
). The my_intent
variable is then merged with the global per-device intent
variable that may have been already partially populated by other roles.
Other infra roles follow the same approach.
SERVICES roles#
Two service roles are defined:
- l2vpn: manages intent for fabric-wide L2VPN services. These are a set of mac-vrf instances on a subset of the nodes in the fabric with associated interfaces and policies
- l3vpn: manages intent for fabric-wide L3VPN services. These are a set of ip-vrf instances on a subset of the nodes in the fabric and are associated with mac-vrf instances
For these roles, we decided to take the abstraction to a new level. Below is an example how a L2VPN is defined:
roles/services/l2vpn/vars/test/l2vpn.yml
l2vpn: # root of l2vpn intent, mapping of mac-vrf instances, with key=mac-vrf name
macvrf-200: # name of the mac-vrf instance
id: 200 # id of the mac-vrf instance: used for vlan-id and route-targets
type: mac-vrf
description: MACVRF1
interface_list: # a mapping with key=node-name and value=list of interfaces
clab-4l2s-l1: # node on which the mac-vrf instance is configured
- ethernet-1/1.200 # interface that will be associated with the mac-vrf instance
clab-4l2s-l2:
- ethernet-1/1.200
export_rt: 100:200 # export route-target for EVPN address-family
import_rt: 100:200 # import route-target for EVPN address-family
vlan: 200 # vlan-id for the mac-vrf instance.
# all sub-interfaces on all participating nodes will be configured with this vlan-id
The l2vpn role will transform this fabric-wide intent into a node-specific intent per resource (network-instance, subinterface, tunnel-interface) and will merge this with the global node intent.
The l3vpn role follows a similar approach but depends on the l2vpn role to define the intent for the mac-vrf instances. If not, the playbook will fail. The l3vpn role knows if an ip-vrf instance applies to the node based of the mac-vrf definitions associated with the ip-vrf. The mac-vrf definition in the L2VPN intent includes the node association.
An example of a L3VPN intent is shown below:
roles/services/l3vpn/vars/test/l3vpn.yml
l3vpn: # root of l3vpn intent, mapping of ip-vrf instances, with key=ip-vrf name
ipvrf-2001: # name of the ip-vrf instance
id: 2001 # id of the ip-vrf instance: used for route-targets
type: ip-vrf
description: IPVRF1
snet_list: # a list of (macvrf, gw) pairs. The macvrf must be present in the l2vpn intent
- macvrf: macvrf-300 # the macvrf instance to associate with the ip-vrf instance
gw: 10.1.1.254/24 # the gateway address for the subnet
- macvrf: macvrf-301
gw: 10.1.2.254/24
export_rt: 100:2001 # export route-target for EVPN address-family (route-type: 5)
import_rt: 100:2001 # import route-target for EVPN address-family (route-type: 5)
COMMON and UTILS roles#
Once the nodal intent has been constructed by the INFRA and SERVICES roles, the playbook calls the common/configure
role as the last task. This role will take the nodal intent and construct the final configuration for the node. It calls roles in the utils
folder to construct the configuration for the various resources (interfaces, network-instances, policies, etc) and thus generates the variables update
and replace
that are passed as arguments to the nokia.srlinux.config
module.
It also generates a delete
variable containing a list of configuration paths to delete when the play variable purge=true
and when no tags are specified with the ansible-playbook
command that would result in a partial nodal intent. It uses the node for configuration state (running configuration) that was retrieved by the common/init
role and compares this against the nodal intent to generate the delete
variable.
Following diagram gives an overview how the low-level device intent is constructed from the various roles:
The abstraction level defined in the roles eventually transforms to the low-level device configs that is then applied to the node. Essentially, the role designers have to decide how much abstraction they want to provide to the user of the role. The more abstraction, the easier it is to use the role, but the less flexibility the user has to configure the node. Network automation engineers then can adapt the provided roles to their needs by changing the abstraction level of the roles.