Skip to content

Virtual Chassis

A virtual chassis represents a set of devices which share a common control plane. A common example of this is a stack of switches which are connected and configured to operate as a single device. A virtual chassis must be assigned a name and may be assigned a domain.

Each device in the virtual chassis is referred to as a VC member, and assigned a position and (optionally) a priority. VC member devices commonly reside within the same rack, though this is not a requirement. One of the devices may be designated as the VC master: This device will typically be assigned a name, services, and other attributes related to managing the VC.

You create your devices, then create the Virtual Chassis and assign the devices to the Virtual Chassis.

Note

It's important to recognize the distinction between a virtual chassis and a chassis-based device. A virtual chassis is not suitable for modeling a chassis-based switch with removable line cards (such as the Juniper EX9208), as its line cards are not physically autonomous devices. Such a chassis should be modeled with module bays and modules instead.

Overview

Choosing between Virtual Chassis and a Device Redundancy Group comes down to the management control plane count — use a Virtual Chassis when the members share one control plane, and a Device Redundancy Group when each keeps its own. See HA Devices for the full comparison.

Use a Virtual Chassis when multiple physical devices operate as a single logical device with one management IP, such as a switch stack. The model is small: a single VirtualChassis object that member devices point to, with each member recording its position in the stack. One member can be explicitly designated as the master, and Nautobot will surface all ports (interfaces, front ports, rear ports, etc.) from every member on that master device, reflecting how the stack actually presents itself on the network. The one exception is an interface with mgmt_only set, which is not surfaced on the master.

Note

Interfaces are not renumbered automatically in Nautobot when their device is assigned to a virtual chassis. As on real hardware, a device's interfaces default to slot 1 (e.g. GigabitEthernet1/0/1); when that device becomes member 3, its interfaces should be GigabitEthernet3/0/1. Use the Bulk Rename feature to renumber them.

LAG interfaces are supported across devices that have the same parent virtual chassis — this is the one case in Nautobot where a LAG's member interfaces may live on different devices. The LAG will show its member interfaces across the multiple devices on the LAG itself. The recommendation is to create the LAG interface itself (e.g. Port-Channel10) on the expected master device. Because the chassis is a single logical device, the LAG fully captures the relationship on its own; no additional grouping model (such as an Interface Redundancy Group) is needed.

Field Type Required Description
name string Yes Unique name identifying the virtual chassis
master ForeignKey to Device No The device that acts as the control plane master for the chassis; all member devices are managed through this device
domain string No The vendor's stack/chassis identifier shared across members (e.g. StackWise stack ID, VSS/StackWise Virtual domain, IRF domain ID). Config templates read this as the domain/stack ID.

The following fields are on the Device model, in support of the Virtual Chassis featureset.

Attribute Type Required Description
virtual_chassis ForeignKey to VirtualChassis No The virtual chassis this device belongs to
vc_position integer (0–255) Yes (if in VC) Slot/position of this device within the virtual chassis; must be unique per chassis
vc_priority integer (0–255) No Election priority for master role; higher values win (vendor behavior varies)

Entity Relationship Diagram

This schema illustrates the connections between the models involved in a virtual chassis.

---
title: Virtual Chassis Entity Relationship Diagram
---
erDiagram
    VirtualChassis {
        string name UK
        string domain
        Device master FK "one-to-one, optional"
    }

    Device {
        string name
        VirtualChassis virtual_chassis FK "optional"
        int vc_position "0-255, required if in a virtual chassis, unique per chassis"
        int vc_priority "0-255, optional master-election priority"
        DeviceType device_type FK
    }

    Interface {
        string name
        string type
        Device device FK
        Module module FK "optional, set when provided by an installed module"
        Interface lag FK "optional parent LAG"
    }

    ModuleBay {
        string name
        string position
        Device parent_device FK "exactly one of parent_device"
        Module parent_module FK "or parent_module is set"
    }

    Module {
        string serial
        ModuleType module_type FK
        ModuleBay parent_module_bay FK "one-to-one"
    }

    VirtualChassis |o--o{ Device : "members (virtual_chassis + vc_position)"
    VirtualChassis |o--o| Device : "master"
    Device ||--o{ Interface : "has"
    Device ||--o{ ModuleBay : "has"
    ModuleBay |o--o| Module : "installed_module"
    Module ||--o{ ModuleBay : "nested module bays"
    Module ||--o{ Interface : "module-provided interfaces"
    Interface }o--o| Interface : "LAG membership (lag)"

Note

Prior to Nautobot 3.2, not every interface had a direct foreign key to its device. An interface installed in a module instead referenced the module, which in turn referenced the device, so the link to the device was indirect.

Sample API

The below Python snippet creates a HA pair two-member virtual chassis as jcy-vc01. It demonstrates the patterns required to handle the circular dependency between a Device and its VirtualChassis: switch 1 is created first and tagged with "!ref": "sw1", the VirtualChassis is then created inline with master: "!ref:sw1". Switch 2 joins the existing chassis via "!ref:virtual_chassis". The below diagram describes what the code will create.

You can drop this snippet into an iPython shell or file. It leverages the public demo sandbox. In addition, you can update the first set of variables to more easily integrate with other systems.

---
title: Virtual Chassis Created by the Sample Script
---
graph TB
    vc["VirtualChassis: jcy-vc01<br><i>domain: jcy-vc01</i>"]
    mgmt_prefix["Prefix: 192.168.1.0/24<br>(management)"]
    vlans["VLANs: 10, 20, 30, 40"]

    subgraph loc["Location: JCY"]
        subgraph sw1["Device: jcy-vc01 (position 1, priority 15)"]
            s1_mgmt["GigabitEthernet0/0 (mgmt_only)<br>192.168.1.10/24 (primary IP)"]
            s1_po1["Port-Channel1 (LAG)<br><i>mode: tagged, VLANs 10,20,30,40</i>"]
            s1_uplink["TenGigabitEthernet1/1/1"]
            s1_sp1["StackPort1/1"]
            s1_sp2["StackPort1/2"]
        end
        subgraph sw2["Device: jcy-vc01:2 (position 2, priority 14)"]
            s2_mgmt["GigabitEthernet0/0 (mgmt_only)<br>no IP - reached via the master"]
            s2_uplink["TenGigabitEthernet2/1/1"]
            s2_sp1["StackPort2/1"]
            s2_sp2["StackPort2/2"]
        end
    end

    sw1 -- "member" --> vc
    sw2 -- "member" --> vc
    vc -- "master" --> sw1
    s1_mgmt -.- mgmt_prefix
    s1_po1 -.- vlans

    s1_uplink -- "lag" --> s1_po1
    s2_uplink -- "lag (cross-stack)" --> s1_po1

    s2_sp1 -. "cable (stack ring)" .- s1_sp2
    s2_sp2 -. "cable (stack ring)" .- s1_sp1
Show pynautobot script
import sys
import pynautobot

NAUTOBOT_URL = "http://demo.nautobot.com"
NAUTOBOT_TOKEN = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

ROLE_NAME = "router"
ROOT_NAME = "jcy"
MGMT_PREFIX = "192.168.1.0/24"
DEVICE_TYPE_MODEL = "C9300"
# VLANs carried by the cross-stack uplink LAG (so a config template can render the trunk).
VLAN_IDS = [10, 20, 30, 40]

LOCATION_NAME = f"{ROOT_NAME.upper()}"
VC_NAME = f"{ROOT_NAME}-vc01"
VC_DOMAIN = f"{ROOT_NAME}-vc01"
DEVICE_1_NAME = f"{ROOT_NAME}-vc01"
DEVICE_2_NAME = f"{ROOT_NAME}-vc01:2"

# Only the master carries a management IP; the chassis is reached through it.
DEVICES = [
    {"name": DEVICE_1_NAME, "position": 1, "priority": 15, "mgmt_ip": "192.168.1.10/24"},
    {"name": DEVICE_2_NAME, "position": 2, "priority": 14, "mgmt_ip": None},
]

# Stack cabling: member 2's stack ports loop back to member 1's (ring topology).
STACK_CABLES = [
    ((DEVICE_2_NAME, "StackPort2/1"), (DEVICE_1_NAME, "StackPort1/2")),
    ((DEVICE_2_NAME, "StackPort2/2"), (DEVICE_1_NAME, "StackPort1/1")),
]

nb = pynautobot.api(url=NAUTOBOT_URL, token=NAUTOBOT_TOKEN)


def get_or_create(endpoint, lookup, defaults=None):
    """Return (record, created) for the given endpoint, matching pynautobot filter kwargs."""
    record = endpoint.get(**lookup)
    if record:
        return record, False
    return endpoint.create(**{**lookup, **(defaults or {})}), True


def log(created, kind, name):
    print(f"  {'created' if created else 'exists '}  {kind}: {name}")


active = nb.extras.statuses.get(name="Active")
connected = nb.extras.statuses.get(name="Connected")
location = nb.dcim.locations.get(name=LOCATION_NAME)
device_type = nb.dcim.device_types.get(model=DEVICE_TYPE_MODEL)
namespace = nb.ipam.namespaces.get(name="Global")
for obj, label in [
    (active, "Status Active"),
    (connected, "Status Connected"),
    (location, f"Location {LOCATION_NAME}"),
    (device_type, f"DeviceType {DEVICE_TYPE_MODEL}"),
    (namespace, "Namespace Global"),
]:
    if obj is None:
        sys.exit(f"Prerequisite not found in {NAUTOBOT_URL}: {label}")

print("Seeding prerequisites...")
role = nb.extras.roles.get(name=ROLE_NAME)

_, created = get_or_create(nb.ipam.prefixes, {"prefix": MGMT_PREFIX, "namespace": namespace.id}, {"status": active.id})
log(created, "Prefix", MGMT_PREFIX)

# Global VLANs (no location) so they can be assigned to interfaces on any member device.
vlan_ids = []
for vid in VLAN_IDS:
    vlan, created = get_or_create(nb.ipam.vlans, {"vid": vid}, {"name": f"vlan{vid}", "status": active.id})
    log(created, "VLAN", str(vid))
    vlan_ids.append(vlan.id)

# The VirtualChassis is created first, without a master; the master is set after
# both member devices exist and have joined (master must be a member).
print("Seeding virtual chassis...")
vc, created = get_or_create(nb.dcim.virtual_chassis, {"name": VC_NAME}, {"domain": VC_DOMAIN})
log(created, "VirtualChassis", vc.name)

interfaces = {}
lag = None
for spec in DEVICES:
    print(f"Seeding {spec['name']}...")
    device, created = get_or_create(
        nb.dcim.devices,
        {"name": spec["name"]},
        {
            "device_type": device_type.id,
            "role": role.id,
            "location": location.id,
            "status": active.id,
            "virtual_chassis": vc.id,
            "vc_position": spec["position"],
            "vc_priority": spec["priority"],
        },
    )
    log(created, "Device", device.name)
    if spec["position"] == 1:
        master = device

    mgmt, created = get_or_create(
        nb.dcim.interfaces,
        {"device": device.id, "name": "GigabitEthernet0/0"},
        {"type": "1000base-t", "status": active.id, "mgmt_only": True, "description": "Management Interface"},
    )
    log(created, "Interface", f"{device.name} GigabitEthernet0/0")

    if spec["mgmt_ip"]:
        mgmt_ip, created = get_or_create(
            nb.ipam.ip_addresses, {"address": spec["mgmt_ip"], "namespace": namespace.id}, {"status": active.id}
        )
        log(created, "IPAddress", str(mgmt_ip.address))
        _, created = get_or_create(nb.ipam.ip_address_to_interface, {"interface": mgmt.id, "ip_address": mgmt_ip.id})
        log(created, "IP assignment", f"{mgmt_ip.address} -> {device.name} GigabitEthernet0/0")
        device.update({"primary_ip4": mgmt_ip.id})

    for port_number in (1, 2):
        stack_port_name = f"StackPort{spec['position']}/{port_number}"
        stack_port, created = get_or_create(
            nb.dcim.interfaces,
            {"device": device.id, "name": stack_port_name},
            {"type": "cisco-stackwise-480", "status": active.id, "description": "Stack ring"},
        )
        log(created, "Interface", f"{device.name} {stack_port_name}")
        interfaces[(device.name, stack_port_name)] = stack_port

    if spec["position"] == 1:
        lag, created = get_or_create(
            nb.dcim.interfaces,
            {"device": device.id, "name": "Port-Channel1"},
            {
                "type": "lag",
                "status": active.id,
                "description": "Cross-stack uplink LAG to upstream distribution",
                # mode + tagged_vlans let a template render `switchport mode trunk` and the allowed VLAN list.
                "mode": "tagged",
                "tagged_vlans": vlan_ids,
            },
        )
        log(created, "Interface", f"{device.name} Port-Channel1")

    uplink_name = f"TenGigabitEthernet{spec['position']}/1/1"
    uplink, created = get_or_create(
        nb.dcim.interfaces,
        {"device": device.id, "name": uplink_name},
        {"type": "10gbase-x-sfpp", "status": active.id, "lag": lag.id, "description": "Uplink (Po1 member)"},
    )
    log(created, "Interface", f"{device.name} {uplink_name}")
    if uplink.lag is None:
        uplink.update({"lag": lag.id})

# Master can only be set once it is a member of the chassis.
if vc.master is None:
    vc.update({"master": master.id})
    log(True, "VC master", f"{vc.name} -> {master.name}")
else:
    log(False, "VC master", f"{vc.name} -> {master.name}")

print("Seeding stack port cables...")
for (a_device, a_name), (b_device, b_name) in STACK_CABLES:
    side_a = nb.dcim.interfaces.get(interfaces[(a_device, a_name)].id)
    if side_a.cable:
        log(False, "Cable", f"{a_device} {a_name} <-> {b_device} {b_name}")
        continue
    nb.dcim.cables.create(
        termination_a_type="dcim.interface",
        termination_a_id=side_a.id,
        termination_b_type="dcim.interface",
        termination_b_id=interfaces[(b_device, b_name)].id,
        status=connected.id,
    )
    log(True, "Cable", f"{a_device} {a_name} <-> {b_device} {b_name}")

Sample Design Builder

The following Design Builder example models the same HA pair as the Sample API above.

Show Design Builder YAML
# Prefixes are created first so the management IPs below can parent to them.
prefixes:
  - "!create_or_update:prefix": "192.168.1.0/24"
    status__name: "Active"
    "!ref": "mgmt_prefix"

devices:
    # Switch 1 of the stack
  - "!create_or_update:name": "jcy-vc01"
    location__name: "JCY"
    status__name: "Active"
    device_type__model: "C9300"
    role__name: "router"
    "!ref": "sw1"
    # Virtual chassis attributes
    vc_position: 1
    vc_priority: 15
    # Virtual chassis creation with deferred assignment (Device created first then VC created with switch 1 as master)
    virtual_chassis:
      "!create_or_update:name": "jcy-vc01"
      domain: "jcy-vc01"
      master: "!ref:sw1"
      deferred: true
      "!ref": "virtual_chassis"
    # Interfaces (subset for brevity)
    interfaces:
      - "!create_or_update:name": "GigabitEthernet0/0"
        type: "1000base-t"
        status__name: "Active"
        mgmt_only: true
        description: "Management Interface"
        ip_address_assignments:
          - ip_address:
              "!create_or_update:address": "192.168.1.10/24"
              "!create_or_update:parent": "!ref:mgmt_prefix"
              status__name: "Active"
      - "!create_or_update:name": "StackPort1/1"
        type: "cisco-stackwise-480"
        status__name: "Active"
        description: "Stack ring"
        "!ref": "sw1_stackport_1"
      - "!create_or_update:name": "StackPort1/2"
        type: "cisco-stackwise-480"
        status__name: "Active"
        description: "Stack ring"
        "!ref": "sw1_stackport_2"
      - "!create_or_update:name": "Port-Channel1"
        type: "lag"
        status__name: "Active"
        description: "Cross-stack uplink LAG to upstream distribution"
        # mode + tagged_vlans let a config template render the trunk and its allowed VLAN list
        mode: "tagged"
        tagged_vlans:
          - "!create_or_update:vid": 10
            name: "vlan10"
            status__name: "Active"
          - "!create_or_update:vid": 20
            name: "vlan20"
            status__name: "Active"
          - "!create_or_update:vid": 30
            name: "vlan30"
            status__name: "Active"
          - "!create_or_update:vid": 40
            name: "vlan40"
            status__name: "Active"
        "!ref": "po1"
      - "!create_or_update:name": "TenGigabitEthernet1/1/1"
        type: "10gbase-x-sfpp"
        status__name: "Active"
        description: "Uplink (Po1 member)"
        lag: "!ref:po1"
    # Deferred IP assignment to avoid dependency issues with interface creation/assignment.
    # `!get` looks the address up after the interface and its IP have been created;
    # `deferred` waits until the device is saved before assigning it.
    primary_ip4:
      "!get:address": "192.168.1.10/24"
      deferred: true

    # Switch 2 of the stack
  - "!create_or_update:name": "jcy-vc01:2"
    location__name: "JCY"
    status__name: "Active"
    device_type__model: "C9300"
    role__name: "router"
    # VC assignment to existing VC with switch 1 as master
    virtual_chassis: "!ref:virtual_chassis"
    # VC attributes
    vc_position: 2
    vc_priority: 14
    # interfaces (subset for brevity)
    interfaces:
      - "!create_or_update:name": "GigabitEthernet0/0"
        type: "1000base-t"
        status__name: "Active"
        mgmt_only: true
        description: "Management Interface"
      - "!create_or_update:name": "StackPort2/1"
        type: "cisco-stackwise-480"
        status__name: "Active"
        description: "Stack ring"
        "!ref": "sw2_stackport_1"
      - "!create_or_update:name": "StackPort2/2"
        type: "cisco-stackwise-480"
        status__name: "Active"
        description: "Stack ring"
        "!ref": "sw2_stackport_2"
      - "!create_or_update:name": "TenGigabitEthernet2/1/1"
        type: "10gbase-x-sfpp"
        status__name: "Active"
        description: "Uplink (Po1 member)"
        lag: "!ref:po1"
    # No primary IP assignment on switch 2 to avoid conflicts with switch 1 management IP

# Stack ring cabling (member 2's stack ports loop back to member 1's)
cables:
  - "!create_or_update:label": "jcy-vc01 stack ring 1"
    status__name: "Connected"
    terminations:
      - "!create_or_update:cable_end": "A"
        interface: "!ref:sw2_stackport_1"
      - "!create_or_update:cable_end": "B"
        interface: "!ref:sw1_stackport_2"
  - "!create_or_update:label": "jcy-vc01 stack ring 2"
    status__name: "Connected"
    terminations:
      - "!create_or_update:cable_end": "A"
        interface: "!ref:sw2_stackport_2"
      - "!create_or_update:cable_end": "B"
        interface: "!ref:sw1_stackport_1"

GraphQL

The following query retrieves a virtual chassis by name and walks each member device and its interfaces. Querying members { interfaces { ... } } (rather than the master's vc_interfaces) returns every device fully — including each member's own management interface, which vc_interfaces filters out — so a template can generate the complete configuration for both devices. It returns the chassis domain, the master (to identify the primary), and per member its vc_position/vc_priority/primary_ip4 plus each interface's type, lag membership, mode, and tagged/untagged VLANs.

query ($vc_name: [String]) {
  virtual_chassis(name: $vc_name) {
    name
    domain
    master {
      name
    }
    members {
      name
      vc_position
      vc_priority
      primary_ip4 {
        address
      }
      interfaces {
        name
        type
        enabled
        mode
        mgmt_only
        description
        lag {
          name
        }
        untagged_vlan {
          vid
        }
        tagged_vlans {
          vid
        }
        ip_addresses {
          address
        }
      }
    }
  }
}

Query variables:

{
  "vc_name": "jcy-vc01"
}
{
  "data": {
    "virtual_chassis": [
      {
        "name": "jcy-vc01",
        "domain": "jcy-vc01",
        "master": {
          "name": "jcy-vc01"
        },
        "members": [
          {
            "name": "jcy-vc01",
            "vc_position": 1,
            "vc_priority": 15,
            "primary_ip4": {
              "address": "192.168.1.10/24"
            },
            "interfaces": [
              {
                "name": "GigabitEthernet0/0",
                "type": "A_1000BASE_T",
                "enabled": true,
                "mode": null,
                "mgmt_only": true,
                "description": "Management Interface",
                "lag": null,
                "untagged_vlan": null,
                "tagged_vlans": [],
                "ip_addresses": [
                  {
                    "address": "192.168.1.10/24"
                  }
                ]
              },
              {
                "name": "TenGigabitEthernet1/1/1",
                "type": "A_10GBASE_X_SFPP",
                "enabled": true,
                "mode": null,
                "mgmt_only": false,
                "description": "Uplink (Po1 member)",
                "lag": {
                  "name": "Port-Channel1"
                },
                "untagged_vlan": null,
                "tagged_vlans": [],
                "ip_addresses": []
              },
              {
                "name": "StackPort1/1",
                "type": "CISCO_STACKWISE_480",
                "enabled": true,
                "mode": null,
                "mgmt_only": false,
                "description": "Stack ring",
                "lag": null,
                "untagged_vlan": null,
                "tagged_vlans": [],
                "ip_addresses": []
              },
              {
                "name": "StackPort1/2",
                "type": "CISCO_STACKWISE_480",
                "enabled": true,
                "mode": null,
                "mgmt_only": false,
                "description": "Stack ring",
                "lag": null,
                "untagged_vlan": null,
                "tagged_vlans": [],
                "ip_addresses": []
              },
              {
                "name": "Port-Channel1",
                "type": "LAG",
                "enabled": true,
                "mode": "TAGGED",
                "mgmt_only": false,
                "description": "Cross-stack uplink LAG to upstream distribution",
                "lag": null,
                "untagged_vlan": null,
                "tagged_vlans": [
                  {
                    "vid": 10
                  },
                  {
                    "vid": 20
                  },
                  {
                    "vid": 30
                  },
                  {
                    "vid": 40
                  }
                ],
                "ip_addresses": []
              }
            ]
          },
          {
            "name": "jcy-vc01:2",
            "vc_position": 2,
            "vc_priority": 14,
            "primary_ip4": null,
            "interfaces": [
              {
                "name": "GigabitEthernet0/0",
                "type": "A_1000BASE_T",
                "enabled": true,
                "mode": null,
                "mgmt_only": true,
                "description": "Management Interface",
                "lag": null,
                "untagged_vlan": null,
                "tagged_vlans": [],
                "ip_addresses": []
              },
              {
                "name": "TenGigabitEthernet2/1/1",
                "type": "A_10GBASE_X_SFPP",
                "enabled": true,
                "mode": null,
                "mgmt_only": false,
                "description": "Uplink (Po1 member)",
                "lag": {
                  "name": "Port-Channel1"
                },
                "untagged_vlan": null,
                "tagged_vlans": [],
                "ip_addresses": []
              },
              {
                "name": "StackPort2/1",
                "type": "CISCO_STACKWISE_480",
                "enabled": true,
                "mode": null,
                "mgmt_only": false,
                "description": "Stack ring",
                "lag": null,
                "untagged_vlan": null,
                "tagged_vlans": [],
                "ip_addresses": []
              },
              {
                "name": "StackPort2/2",
                "type": "CISCO_STACKWISE_480",
                "enabled": true,
                "mode": null,
                "mgmt_only": false,
                "description": "Stack ring",
                "lag": null,
                "untagged_vlan": null,
                "tagged_vlans": [],
                "ip_addresses": []
              }
            ]
          }
        ]
      }
    ]
  }
}

Note

members { interfaces } returns each device independently, so it works for any redundant model — including a Device Redundancy Group, which has no master and no vc_interfaces. If you only need the flat list of interfaces surfaced on the master, you can instead query master { vc_interfaces { ... } }.

Key Characteristics

  • Can you port channel across multiple devices? Yes — a virtual chassis is the one case in Nautobot where a LAG's member interfaces may live on different devices (any members sharing the same parent virtual chassis)
  • Can you see all interfaces on the Primary (master)? Yes — you can see every member's ports on the master device (except the members' mgmt_only interfaces)
  • Can you see all interfaces on the Backup (member)? No — a non-master member shows only its own interfaces
  • On Primary, can you tell which interfaces are assigned to which device? Yes — each interface keeps its relationship to the member that physically owns it, and slot-based names (e.g. GigabitEthernet2/0/1) convey the member position
  • When do you see all the interfaces on the primary device? Once the device is designated as the virtual chassis master
  • Can you connect interfaces from primary to non-primary? Yes — the stack/VSL ports are cabled between the members either directly or in a ring
  • What should the naming standard be for the chassis device? The master carries the logical chassis name (e.g., jcy-vc01); the other members are suffixed with their position (e.g., jcy-vc01:2)
  • Should I name the interfaces via interface device component templates? Yes, but you will have to change the interfaces on the second device after the fact with the bulk rename.

Questions to ask of the data model

Given the data model, what questions would a user ask?

  • Given a device, I would like to know whether it is a member of a virtual chassis.
  • Given a device in a virtual chassis, I would like to know whether it is the master.
  • Given a device in a virtual chassis, I would like to know its position (slot) within the stack.
  • Given a device in a virtual chassis, I would like to know which member would be elected master next.
  • Given a device in a virtual chassis, I would like to know its sibling members.
  • Given a virtual chassis, I would like to know all of its member devices and how many there are.
  • Given a virtual chassis, I would like to know how to connect to its management plane (the master's primary IP — the members do not have one of their own).
  • Given a virtual chassis, I would like to know its domain or stack identifier.
  • Given a virtual chassis, I would like to know every interface across all of its members.
  • Given an interface shown on the master, I would like to know which physical member it actually lives on.
  • Given a member device, I would like to know which stack/HA ports connect it to which sibling, and on which port (via cables).
  • Given a LAG, I would like to know its member interfaces and which stack member each one lives on.

Tip

You can answer all of these questions with the prior defined GraphQL query.

Configuration Generation

Operating systems and technologies include Dual-chassis Single Control Plane VSS / StackWise Virtual (Cisco)

A config template driven entirely by the GraphQL response above. Each member becomes a virtual-switch identity, and every LAG is rendered as a port channel with its member interfaces.

{% set vc = data.virtual_chassis[0] %}
{% for member in vc.members %}
# ~~~~~ {{ member.name }} ~~~~~

## Standard Global Config

!
switch virtual domain {{ vc.domain }}
  switch {{ member.vc_position }}
!

## note: Switch number is local, domain must match

## Management Plane Configuration

int port-channel {{ 200 + member.vc_position }}
 switchport
 switch virtual link {{ member.vc_position }}
!
{% for vsl in member.interfaces if vsl.type == "CISCO_STACKWISE_480" %}
interface {{ vsl.name }}
 description VSL Link
 no switchport
 no ip address
 no cdp enable
 channel-group {{ 200 + member.vc_position }} mode on
!
{% endfor %}

## note: Port Channel is different on the different switches

{% endfor %}
# ~~~~~ Data Plane (all members) ~~~~~

## Data Plane Configuration

{% set ns = namespace(vlans="") %}
{% for member in vc.members %}
{% for lag in member.interfaces if lag.type == "LAG" %}
{% set ns.vlans = lag.tagged_vlans | map(attribute="vid") | join(",") %}
interface {{ lag.name }}
  description {{ lag.description }}
  switchport mode trunk
  switchport trunk allowed vlan {{ ns.vlans }}
!
{% endfor %}
{% endfor %}
{% for member in vc.members %}
{% for port in member.interfaces if port.lag %}
interface {{ port.name }}
  switchport mode trunk
  switchport trunk allowed vlan {{ ns.vlans }}
  channel-group {{ port.lag.name | replace("Port-Channel", "") }} mode active
!
{% endfor %}
{% endfor %}

Note

This config is on a single management IP

Operating systems and technologies include Multi-chassis Stack Stackwise / Virtual Chassis / Arista Stack / HPE IRF / Extreme SummitStack

A config template driven entirely by the GraphQL response above. Member priority drives master election and vc_position renumbers each member; management is configured once on the master, and the uplink LAG becomes a trunked port channel.

{% set vc = data.virtual_chassis[0] %}
{% for member in vc.members %}
# ~~~~~ {{ "Master" if member.name == vc.master.name else "Member" }} Device ({{ member.name }}) ~~~~~

switch {{ member.vc_position }} priority {{ member.vc_priority }}
switch {{ member.vc_position }} renumber {{ member.vc_position }}

{% endfor %}
# ~~~~~ Management Device ~~~~~

## Management Plane Configuration

# note: Configuration is applied to all devices in the stack.

{% for member in vc.members %}
{% for iface in member.interfaces if iface.mgmt_only and iface.ip_addresses %}
interface {{ iface.name }}
 ip address {{ iface.ip_addresses[0].address | ios_ip }}
 no shut
{% endfor %}
{% endfor %}

## Data Plane Configuration

{% for member in vc.members %}
{% for lag in member.interfaces if lag.type == "LAG" %}
interface {{ lag.name }}
 description {{ lag.description }}
 switchport mode trunk
{% endfor %}
{% endfor %}
{% for member in vc.members %}
{% for port in member.interfaces if port.lag %}

interface {{ port.name }} # <== member {{ member.vc_position }} of stack.
 channel-group {{ port.lag.name | replace("Port-Channel", "") }} mode active
{% endfor %}
{% endfor %}

Note

On member, keep a lower priority. You should renumber them so their interfaces are easily identifiable (e.g., Member 2 uses 2/0/x).

Operating systems and technologies include Cisco Secure Firewall (FTD) and Juniper SRX. Cisco FTD inter-chassis clustering is shown as the representative example; the configuration is applied per chassis through the FXOS CLI (Firepower 4100/9300), which bootstraps the clustered FTD logical device. The same data model drives the equivalent Juniper SRX chassis cluster stanzas.

A config template driven entirely by the GraphQL response above. Each member renders as its own chassis: its cluster control link (CCL) port channel is built from the member's own ports that reference the LAG, and the cluster bootstrap assigns chassis-id from vc_position with the chassis domain as the cluster group name.

{% set vc = data.virtual_chassis[0] %}
{% for member in vc.members %}
# ~~~~~ {{ member.name }} (chassis {{ member.vc_position }}) ~~~~~

## Cluster Control Link (CCL) Port Channel Configuration

{% for lag_name in member.interfaces | selectattr("lag") | map(attribute="lag.name") | unique %}
scope eth-uplink
  scope fabric a
    create port-channel {{ lag_name | replace("Port-Channel", "") }}
      set port-type cluster
      set port-channel-mode active
{% for port in member.interfaces if port.lag and port.lag.name == lag_name %}
      create member-port {{ port.name }}
        exit
{% endfor %}
      exit
    exit
  exit
{% endfor %}

## Logical Device (Cluster Bootstrap) Configuration

scope ssa
  enter logical-device {{ vc.name }} ftd 1 clustered
    enter cluster-bootstrap
      set chassis-id {{ member.vc_position }}
      set cluster-group name {{ vc.domain }}
      set mode spanned-etherchannel
      exit
    exit
  exit
{% endfor %}

Note

set chassis-id is unique per chassis (from vc_position); the cluster group name and the CCL port-channel ID must match on all chassis. Use dedicated high-bandwidth interfaces for the CCL.

Note

The control/data role is not configured — the cluster elects the control unit when it forms. The Nautobot master field records which member is expected to hold the control role.

The script below renders the templates against GraphQL query. Paste the GraphQL query from the GraphQL section into a variable called GRAPHQL_QUERY, and one of the three templates above into CLI_CONFIG_TEMPLATE. This script is a continuation of the prior script above and assumes the variables nb, NAUTOBOT_URL, and NAUTOBOT_TOKEN are already set.

Config Generation Script
GRAPHQL_QUERY = """."""              # Replace with the GraphQL query from above
CLI_CONFIG_TEMPLATE = """."""         # Replace with one of the three config templates above

import ipaddress

from jinja2 import Environment

VC_NAME = "jcy-vc01"


def ios_ip(cidr):
    """Render "10.1.1.10/24" as IOS-style "10.1.1.10 255.255.255.0"."""
    iface = ipaddress.ip_interface(cidr)
    return f"{iface.ip} {iface.netmask}"


gql = nb.graphql.query(query=GRAPHQL_QUERY, variables={"vc_name": VC_NAME})

env = Environment(trim_blocks=True, lstrip_blocks=True)
env.filters["ios_ip"] = ios_ip

print(env.from_string(CLI_CONFIG_TEMPLATE).render(**gql.json))