↓ Skip to main content

Simple MCLAG, Part 3: Automating It with Ansible

Author
Tyler Kiziah
Open Networking Enthusiast
Simple MCLAG - This article is part of a series.
Part 3: This Article

In Part 2 we configured MCLAG with about twenty CLI commands per leaf, and the two leaves differed in only two IP addresses. That’s a template waiting to happen.

SONiC makes this straightforward because all configuration lives in CONFIG_DB. Instead of driving the CLI, we render the CONFIG_DB tables we need as JSON and hand them to config load, which merges them into the running config.

Why config load?
#

ApproachBehavior
config load file.jsonMerges the file into CONFIG_DB. Keys you don’t mention are left alone.
config reload file.jsonReplaces CONFIG_DB with the file and restarts services. Disruptive.
config apply-patch patch.jsonApplies an RFC 6902 JSON Patch with validation (Generic Config Updater).

A merge is the right fit here: we own the MCLAG-related keys and leave everything else, such as management, users, and syslog, untouched. The catch is that a merge never deletes. Removing a member from the template won’t remove it from the switch. We’ll come back to that.

Project layout
#

mclag-ansible/
├── ansible.cfg
├── inventory.ini
├── group_vars/
│   └── leaves.yml
├── host_vars/
│   ├── leaf1.yml
│   └── leaf2.yml
├── templates/
│   └── mclag.json.j2
└── mclag.yml

Inventory and variables
#

SONiC is Debian underneath, so Ansible manages it like any Linux host over SSH. No network-specific connection plugin is needed.

inventory.ini
[leaves]
leaf1 ansible_host=clab-simple-mclag-leaf1
leaf2 ansible_host=clab-simple-mclag-leaf2

[leaves:vars]
ansible_user=admin
# Lab only. Use SSH keys or ansible-vault for anything real.
ansible_password=admin
ansible.cfg
[defaults]
inventory = inventory.ini
host_key_checking = False

Everything symmetrical goes in group_vars:

group_vars/leaves.yml
mclag_domain_id: 1
mclag_keepalive_vlan: 4094
mclag_keepalive_prefixlen: 30
mclag_data_vlans: [10]

mclag_peer_link:
  name: PortChannel0001
  ports: [Ethernet0, Ethernet4]

mclag_members:
  - { name: PortChannel0002, ports: [Ethernet8],  vlan: 10 }   # host1
  - { name: PortChannel0003, ports: [Ethernet12], vlan: 10 }   # host2

Only the ICCP addresses differ per leaf:

host_vars/leaf1.yml
mclag_source_ip: 192.168.255.1
mclag_peer_ip: 192.168.255.2
host_vars/leaf2.yml
mclag_source_ip: 192.168.255.2
mclag_peer_ip: 192.168.255.1

The template
#

Building the JSON by string concatenation is fragile, because one stray comma breaks it. Instead, the template builds a dictionary and lets to_nice_json handle the syntax:

templates/mclag.json.j2
{#- CONFIG_DB fragment for a SONiC MCLAG peer. Rendered per host. -#}
{%- set cfg = {
  "FEATURE": {"iccpd": {"state": "enabled"}},
  "PORT": {}, "PORTCHANNEL": {}, "PORTCHANNEL_MEMBER": {},
  "VLAN": {}, "VLAN_MEMBER": {}, "VLAN_INTERFACE": {},
  "MCLAG_DOMAIN": {}, "MCLAG_INTERFACE": {}, "MCLAG_UNIQUE_IP": {}
} -%}
{%- set kvlan = "Vlan" ~ mclag_keepalive_vlan -%}

{#- Port channels and their member ports -#}
{%- for pc in [mclag_peer_link] + mclag_members -%}
  {%- set _ = cfg.PORTCHANNEL.update({pc.name: {"admin_status": "up", "mtu": "9100", "min_links": "1"}}) -%}
  {%- for port in pc.ports -%}
    {%- set _ = cfg.PORT.update({port: {"admin_status": "up"}}) -%}
    {%- set _ = cfg.PORTCHANNEL_MEMBER.update({pc.name ~ "|" ~ port: {}}) -%}
  {%- endfor -%}
{%- endfor -%}

{#- VLANs: everything tagged on the peer-link, members untagged -#}
{%- for vid in mclag_data_vlans + [mclag_keepalive_vlan] -%}
  {%- set _ = cfg.VLAN.update({"Vlan" ~ vid: {"vlanid": vid | string}}) -%}
  {%- set _ = cfg.VLAN_MEMBER.update({"Vlan" ~ vid ~ "|" ~ mclag_peer_link.name: {"tagging_mode": "tagged"}}) -%}
{%- endfor -%}
{%- for m in mclag_members -%}
  {%- set _ = cfg.VLAN_MEMBER.update({"Vlan" ~ m.vlan ~ "|" ~ m.name: {"tagging_mode": "untagged"}}) -%}
  {%- set _ = cfg.MCLAG_INTERFACE.update({mclag_domain_id ~ "|" ~ m.name: {"if_type": "PortChannel"}}) -%}
{%- endfor -%}

{#- ICCP interface and domain -#}
{%- set _ = cfg.VLAN_INTERFACE.update({
  kvlan: {},
  kvlan ~ "|" ~ mclag_source_ip ~ "/" ~ mclag_keepalive_prefixlen: {}
}) -%}
{%- set _ = cfg.MCLAG_UNIQUE_IP.update({kvlan: {"unique_ip": "enable"}}) -%}
{%- set _ = cfg.MCLAG_DOMAIN.update({mclag_domain_id | string: {
  "source_ip": mclag_source_ip,
  "peer_ip": mclag_peer_ip,
  "peer_link": mclag_peer_link.name
}}) -%}

{{ cfg | to_nice_json }}

The playbook
#

mclag.yml
- name: Configure MCLAG on SONiC leaves
  hosts: leaves
  gather_facts: false
  become: true

  tasks:
    - name: Render the CONFIG_DB fragment
      ansible.builtin.template:
        src: templates/mclag.json.j2
        dest: /etc/sonic/mclag.json
        mode: "0644"
      register: fragment

    - name: Merge the fragment into CONFIG_DB
      ansible.builtin.command: config load /etc/sonic/mclag.json -y
      when: fragment.changed or (force_load | default(false) | bool)

    - name: Persist the running config
      ansible.builtin.command: config save -y
      when: fragment.changed or (force_load | default(false) | bool)

    - name: Wait for the ICCP session to come up
      ansible.builtin.command: docker exec iccpd mclagdctl -i {{ mclag_domain_id }} dump state
      register: mclag_state
      changed_when: false
      retries: 12
      delay: 10
      until: "'keepalive is: OK' in mclag_state.stdout"

Run it with --diff to see exactly what changes on each leaf:

ansible-playbook mclag.yml --diff

A second run reports no changes. The rendered file is identical, so the config load and config save tasks are skipped.

sshpass must be installed on the control node for password authentication. With SSH keys, drop ansible_password from the inventory.

Limitations to know about
#

  • Merges don’t delete. If you remove PortChannel0003 from mclag_members, the playbook won’t remove it from the switch. For a lab, that’s fine. For production, either generate a JSON Patch with explicit remove operations and use config apply-patch, or adopt a “full config” model that renders the entire config_db.json.
  • Drift isn’t detected. The playbook compares the template to the file it last wrote, not to live CONFIG_DB. If someone changes the switch by hand, re-run with -e force_load=true to push the intended state again.
  • Order matters on first boot. Make sure no factory IPs or VLAN memberships remain on the member ports (Part 2 covers this). Otherwise the port channel membership won’t take effect.

Next: Part 4: Verifying and troubleshooting MCLAG.

Simple MCLAG - This article is part of a series.
Part 3: This Article

Related