The underlay role is responsible for configuring the IP fabric underlay on all NXOS devices. This includes
Layer 3 interfaces, loopback interfaces, OSPF routing, PIM multicast, and iBGP for the EVPN control plane.
Like the common role, the underlay role leverages the pre-staged Jinja2 templates to generate configuration
payloads using ansible.builtin.template lookups with set_fact, which are then
passed to the corresponding NXOS collection modules.
The underlay role makes use of the following NXOS collection modules:
cisco.nxos.nxos_interfacescisco.nxos.nxos_l3_interfacescisco.nxos.nxos_ospfv2cisco.nxos.nxos_ospf_interfacescisco.nxos.nxos_pim_rp_addresscisco.nxos.nxos_pim_interfacecisco.nxos.nxos_bgp_globalcisco.nxos.nxos_bgp_neighbor_address_familycisco.nxos.nxos_configansible.utils.cli_parseGenerates
interface loopback0
ip address 10.0.0.1/32
ip pim sparse-mode
ip router ospf UNDERLAY area 0.0.0.0
interface loopback250
ip address 10.250.250.1/32
ip pim sparse-mode
ip router ospf UNDERLAY area 0.0.0.0
interface Ethernet1/1
no switchport
ip address 10.1.1.0/31
ip ospf network point-to-point
ip router ospf UNDERLAY area 0.0.0.0
ip pim sparse-mode
mtu 9216
router ospf UNDERLAY
router-id 10.0.0.1
ip pim anycast-rp 10.250.250.1 10.0.0.1
ip pim rp-address 10.250.250.1
router bgp 65001
router-id 10.0.0.1
neighbor 10.0.0.101
remote-as 65001
update-source loopback0
address-family l2vpn evpn
send-community both
route-reflector-client
The underlay role is organized into the following logical sections:
set_fact. Also handles cleanup of interfaces
that should no longer be configured by gathering the current state and using state: purged.nxos_config
for commands without a dedicated module, sets the RP address on all devices, enables PIM sparse-mode on
interfaces, and cleans up PIM from interfaces that no longer require it.state: overridden to enforce the desired state.Return to your VSCode Terminal to open and build-out the main.yml file found in roles/underlay/tasks/.
code-server -r /home/pod06/workspace/nxapilab/ansible-nxos/roles/underlay/tasks/main.yml
Copy the below YAML into the roles/underlay/tasks/main.yml file that is opened in VSCode. These tasks use the variables you defined in your group_vars and host_vars along with the pre-staged Jinja2 templates to generate each configuration payload before applying it to the devices.
The first section configures Layer 3 interfaces and their IP addresses. It also gathers the current interface state and removes any interfaces that are no longer defined in the variables, ensuring the device matches the desired configuration exactly.
- name: Generate L3 Interface(s) Payload
ansible.builtin.set_fact:
nxos_interfaces: "{{ lookup('ansible.builtin.template', playbook_dir ~ '/templates/config/nxos_interfaces.j2') }}"
- name: Configure L3 Interface(s)
cisco.nxos.nxos_interfaces:
config: "{{ nxos_interfaces | ansible.builtin.from_yaml }}"
state: overridden
- name: Generate L3 Interface(s) IPv4 Payload
ansible.builtin.set_fact:
nxos_l3_interfaces: "{{ lookup('ansible.builtin.template', playbook_dir ~ '/templates/config/nxos_l3_interfaces.j2') }}"
- name: Configure IP Address on L3 Interfaces
cisco.nxos.nxos_l3_interfaces:
config: "{{ nxos_l3_interfaces | ansible.builtin.from_yaml }}"
state: replaced
- name: Generate Loopback Interface(s) IPv4 Payload
ansible.builtin.set_fact:
nxos_loopback_interfaces_ipv4: "{{ lookup('ansible.builtin.template', playbook_dir ~ '/templates/config/nxos_loopback_interfaces_ipv4.j2') }}"
- name: Configure IP Address on Loopback Interfaces
cisco.nxos.nxos_l3_interfaces:
config: "{{ nxos_loopback_interfaces_ipv4 | ansible.builtin.from_yaml }}"
state: merged
- name: Get Interface(s)
cisco.nxos.nxos_interfaces:
state: gathered
register: current_nxos_interfaces
- name: Generate L3 Interface(s) to Remove Payload
ansible.builtin.set_fact:
nxos_interfaces_to_remove: "{{ lookup('ansible.builtin.template', playbook_dir ~ '/templates/config/nxos_interfaces_to_remove.j2') }}"
- name: Remove L3 Interface(s)
cisco.nxos.nxos_interfaces:
config: "{{ nxos_interfaces_to_remove | ansible.builtin.from_yaml }}"
state: purged
when: nxos_interfaces_to_remove | ansible.builtin.from_yaml | length > 0
The next section configures the OSPF underlay process and associates interfaces with the OSPF area.
Both tasks use state: overridden to ensure the OSPF configuration matches exactly what
is declared.
- name: Generate OSPF Underlay Process Payload
ansible.builtin.set_fact:
nxos_ospf: "{{ lookup('ansible.builtin.template', playbook_dir ~ '/templates/config/nxos_ospf.j2') }}"
- name: Configure OSPF Underlay Process
cisco.nxos.nxos_ospfv2:
config: "{{ nxos_ospf | ansible.builtin.from_yaml }}"
state: overridden
- name: Generate OSPF Interface(s) Payload
ansible.builtin.set_fact:
nxos_ospf_interfaces: "{{ lookup('ansible.builtin.template', playbook_dir ~ '/templates/config/nxos_ospf_interfaces.j2') }}"
- name: Configure OSPF Interface(s)
cisco.nxos.nxos_ospf_interfaces:
config: "{{ nxos_ospf_interfaces | ansible.builtin.from_yaml }}"
state: overridden
The PIM section configures Anycast RP on spine switches using nxos_config with inline Jinja2
to dynamically build the commands. PIM sparse-mode is enabled on the appropriate interfaces, and any
PIM interfaces that are no longer needed are cleaned up by parsing the current PIM state via
cli_parse and comparing against the desired list.
- name: Configure PIM Anycast RP on Spines
cisco.nxos.nxos_config:
lines: |
{% for rtr_rp_addr in hostvars[groups['spines'] | first].pim.anycast_rp_router_addresses %}
ip pim anycast-rp {{ hostvars[groups['spines'] | first].pim.anycast_rp_address }} {{ rtr_rp_addr }}
{% endfor %}
when: inventory_hostname in groups['spines']
- name: Configure PIM RP Address
cisco.nxos.nxos_pim_rp_address:
rp_address: "{{ hostvars[groups['spines'] | first].pim.anycast_rp_address }}"
state: present
- name: Configure PIM Interface(s)
cisco.nxos.nxos_pim_interface:
interface: "{{ item.name }}"
sparse: true
state: present
when: item.pim is defined and item.pim
loop: "{{ all_layer3_interfaces }}"
- name: Get PIM Interfaces
ansible.utils.cli_parse:
command: show ip pim interface brief | json
parser:
name: ansible.utils.json
set_fact: parsed_output
- name: Parse PIM Interfaces
ansible.builtin.set_fact:
parsed_pim_interfaces: "{{ parsed_output.TABLE_vrf.ROW_vrf.TABLE_brief.ROW_brief | map(attribute='if-name') | list | unique | default([]) }}"
- name: Determine PIM Interfaces to Unconfigure
ansible.builtin.set_fact:
pim_interfaces_to_unconfigure: "{{ parsed_pim_interfaces | difference(all_layer3_interfaces | selectattr('pim', 'defined') | selectattr('pim', 'equalto', true) | map(attribute='name') | list) }}"
- name: Remove PIM Interface(s)
cisco.nxos.nxos_pim_interface:
interface: "{{ item }}"
state: absent
when: pim_interfaces_to_unconfigure | length > 0
loop: "{{ pim_interfaces_to_unconfigure }}"
The final section configures the iBGP process, neighbors, and L2VPN EVPN address-families using
Jinja2-generated payloads. Both tasks use state: overridden to enforce the exact desired
BGP configuration.
- name: Generate BGP Process and Neighbor Payload
ansible.builtin.set_fact:
nxos_bgp: "{{ lookup('ansible.builtin.template', playbook_dir ~ '/templates/config/nxos_bgp.j2') }}"
- name: Configure BGP Process and Neighbors
cisco.nxos.nxos_bgp_global:
config: "{{ nxos_bgp | ansible.builtin.from_yaml }}"
state: overridden
- name: Generate BGP Neighbor Address-Family(ies) Payload
ansible.builtin.set_fact:
nxos_bgp_af: "{{ lookup('ansible.builtin.template', playbook_dir ~ '/templates/config/nxos_bgp_af.j2') }}"
- name: Configure BGP Neighbor Address-Families
cisco.nxos.nxos_bgp_neighbor_address_family:
config: "{{ nxos_bgp_af | ansible.builtin.from_yaml }}"
state: overridden
Continue on to the next section to build-out the overlay role, followed by the final pieces needed to execute your Ansible playbook to finish configuring the VXLAN EVPN fabric.