Roles
Ansible

Ansible Underlay Role

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_interfaces
  • cisco.nxos.nxos_l3_interfaces
  • cisco.nxos.nxos_ospfv2
  • cisco.nxos.nxos_ospf_interfaces
  • cisco.nxos.nxos_pim_rp_address
  • cisco.nxos.nxos_pim_interface
  • cisco.nxos.nxos_bgp_global
  • cisco.nxos.nxos_bgp_neighbor_address_family
  • cisco.nxos.nxos_config
  • ansible.utils.cli_parse

Generates

      
  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:

  • Interface configuration — configures Layer 3 physical interfaces and loopback interfaces with IP addresses using Jinja2 templates rendered via set_fact. Also handles cleanup of interfaces that should no longer be configured by gathering the current state and using state: purged.
  • OSPF configuration — configures the OSPF underlay process and associates interfaces with the OSPF process and area using Jinja2-generated payloads.
  • PIM configuration — configures PIM Anycast RP on spine switches using 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.
  • BGP configuration — configures the iBGP process, neighbors, and L2VPN EVPN address-family using Jinja2-generated payloads with state: overridden to enforce the desired state.

Step 1 - Open Underlay Role Tasks File

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


Step 2 - Update Underlay Role Tasks File

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.