diff --git a/.github/workflows/pull-request-management.yml b/.github/workflows/pull-request-management.yml
index b355b7a5429..4632d2a57fc 100644
--- a/.github/workflows/pull-request-management.yml
+++ b/.github/workflows/pull-request-management.yml
@@ -248,6 +248,8 @@ jobs:
avd_scenario:
- "ansible_only"
- "digital_twin"
+ - "digital_twin_containerlab"
+ - "digital_twin_containerlab_negative_tests"
- "eos_designs_deprecated_vars"
- "eos_designs-l2ls"
- "eos_designs-mpls-isis-sr-ldp"
diff --git a/.github/yamllintrc b/.github/yamllintrc
index ce8dd4c9224..cf96e2d16bf 100644
--- a/.github/yamllintrc
+++ b/.github/yamllintrc
@@ -58,6 +58,7 @@ rules:
- /ansible_collections/arista/avd/**/intended/structured_configs/ # Auto generated
- /ansible_collections/arista/avd/tests/inventory/**/expected_output/ # Need to compare with auto generated structure config
- /ansible_collections/arista/avd/extensions/molecule/**/digital_twin/documentation/ # Auto generated topology files
+ - /ansible_collections/arista/avd/extensions/molecule/**/digital_twin_containerlab/digital_twin/ # Auto generated cLab topology file
- /python-avd/tests/**/artifacts/ # Need to compare with auto generated structure config
- /python-avd/pyavd/**/schema/eos_*.schema.yml # Auto generated
octal-values:
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin/digital_twin/documentation/fabric/DIGITAL_TWIN-topology.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin/digital_twin/documentation/fabric/DIGITAL_TWIN-topology.yml
index f762e3dc190..46cb9191a2c 100644
--- a/ansible_collections/arista/avd/extensions/molecule/digital_twin/digital_twin/documentation/fabric/DIGITAL_TWIN-topology.yml
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin/digital_twin/documentation/fabric/DIGITAL_TWIN-topology.yml
@@ -1,3 +1,4 @@
+---
nodes:
- digital-twin-adjust-oob-mgmt-1:
node_type: veos
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin/digital_twin/documentation/fabric/DIGITAL_TWIN_SINGLE_SWITCH_FABRIC-topology.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin/digital_twin/documentation/fabric/DIGITAL_TWIN_SINGLE_SWITCH_FABRIC-topology.yml
index 976ce9d2b54..382902a970b 100644
--- a/ansible_collections/arista/avd/extensions/molecule/digital_twin/digital_twin/documentation/fabric/DIGITAL_TWIN_SINGLE_SWITCH_FABRIC-topology.yml
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin/digital_twin/documentation/fabric/DIGITAL_TWIN_SINGLE_SWITCH_FABRIC-topology.yml
@@ -1,3 +1,4 @@
+---
nodes:
- digital-twin-single-switch-fabric-1:
node_type: veos
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/converge.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/converge.yml
new file mode 100644
index 00000000000..5b31d53cdea
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/converge.yml
@@ -0,0 +1,20 @@
+---
+- name: Converge
+ hosts: DIGITAL_TWIN_CONTAINERLAB
+ gather_facts: false
+ connection: local
+ vars:
+ output_dir_name: "digital_twin/intended"
+ documentation_dir_name: "digital_twin/documentation"
+ avd_digital_twin_mode: true
+ avd_ignore_requirements: true
+ tasks:
+ - name: Generate intended variables in Containerlab Digital Twin mode
+ delegate_to: 127.0.0.1
+ ansible.builtin.import_role:
+ name: arista.avd.eos_designs
+
+ - name: Generate intended EOS configs in Containerlab Digital Twin mode
+ delegate_to: 127.0.0.1
+ ansible.builtin.import_role:
+ name: arista.avd.eos_cli_config_gen
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/create.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/create.yml
new file mode 100644
index 00000000000..08483ed5105
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/create.yml
@@ -0,0 +1,13 @@
+---
+- name: Configure local folders
+ hosts: DIGITAL_TWIN_CONTAINERLAB
+ gather_facts: false
+ connection: local
+ vars:
+ root_dir: '{{ playbook_dir }}'
+ tasks:
+ - name: Create local output folders
+ delegate_to: 127.0.0.1
+ ansible.builtin.import_role:
+ name: arista.avd.build_output_folders
+ run_once: true
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/destroy.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/destroy.yml
new file mode 100644
index 00000000000..de65d33f4d2
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/destroy.yml
@@ -0,0 +1,18 @@
+---
+- name: Remove output folders
+ hosts: DIGITAL_TWIN_CONTAINERLAB
+ gather_facts: false
+ connection: local
+ tasks:
+ - name: Delete local folders
+ delegate_to: 127.0.0.1
+ run_once: true
+ ansible.builtin.file:
+ path: "{{ root_dir }}/{{ item }}"
+ state: absent
+ with_items:
+ - documentation
+ - intended
+ - config_backup
+ - reports
+ - digital_twin
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/DIGITAL_TWIN_CONTAINERLAB-topology.clab.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/DIGITAL_TWIN_CONTAINERLAB-topology.clab.yml
new file mode 100644
index 00000000000..d2014a2e3c2
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/DIGITAL_TWIN_CONTAINERLAB-topology.clab.yml
@@ -0,0 +1,41 @@
+---
+name: DIGITAL_TWIN_CONTAINERLAB
+prefix: ''
+mgmt:
+ network: custom_mgmt
+ ipv4-subnet: 172.16.1.0/24
+topology:
+ defaults:
+ kind: arista_ceos
+ kinds:
+ arista_ceos:
+ enforce-startup-config: true
+ image: arista/ceos:latest
+ binds:
+ - interface_mapping.json:/mnt/flash/EosIntfMapping.json:ro
+ nodes:
+ dc1-leaf1a:
+ mgmt-ipv4: 172.16.1.101
+ startup-config: intended/configs/dc1-leaf1a.cfg
+ dc1-leaf2a:
+ mgmt-ipv4: 172.16.1.102
+ startup-config: intended/configs/dc1-leaf2a.cfg
+ dc1-spine1:
+ mgmt-ipv4: 172.16.1.11
+ startup-config: intended/configs/dc1-spine1.cfg
+ dc1-spine2:
+ mgmt-ipv4: 172.16.1.12
+ startup-config: intended/configs/dc1-spine2.cfg
+ links:
+ - endpoints:
+ - dc1-leaf1a:eth1
+ - dc1-spine1:eth1
+ - endpoints:
+ - dc1-leaf1a:eth1_2
+ - dc1-spine2:eth1_2
+ - endpoints:
+ - dc1-leaf2a:eth1_1
+ - dc1-spine1:eth1_3
+ - endpoints:
+ - dc1-leaf2a:eth2
+ - dc1-spine2:eth2
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/devices/dc1-leaf1a.md b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/devices/dc1-leaf1a.md
new file mode 100644
index 00000000000..7a0ac1b764f
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/devices/dc1-leaf1a.md
@@ -0,0 +1,304 @@
+# dc1-leaf1a
+
+## Table of Contents
+
+- [Management](#management)
+ - [Management Interfaces](#management-interfaces)
+ - [Management API HTTP](#management-api-http)
+- [Authentication](#authentication)
+ - [Enable Password](#enable-password)
+- [Internal VLAN Allocation Policy](#internal-vlan-allocation-policy)
+ - [Internal VLAN Allocation Policy Summary](#internal-vlan-allocation-policy-summary)
+ - [Internal VLAN Allocation Policy Device Configuration](#internal-vlan-allocation-policy-device-configuration)
+- [Interfaces](#interfaces)
+ - [Ethernet Interfaces](#ethernet-interfaces)
+ - [Loopback Interfaces](#loopback-interfaces)
+- [Routing](#routing)
+ - [Service Routing Protocols Model](#service-routing-protocols-model)
+ - [IP Routing](#ip-routing)
+ - [IPv6 Routing](#ipv6-routing)
+ - [Static Routes](#static-routes)
+ - [Router BGP](#router-bgp)
+- [Multicast](#multicast)
+ - [IP IGMP Snooping](#ip-igmp-snooping)
+- [VRF Instances](#vrf-instances)
+ - [VRF Instances Summary](#vrf-instances-summary)
+ - [VRF Instances Device Configuration](#vrf-instances-device-configuration)
+
+## Management
+
+### Management Interfaces
+
+#### Management Interfaces Summary
+
+##### IPv4
+
+| Management Interface | Description | Type | VRF | IP Address | Gateway |
+| -------------------- | ----------- | ---- | --- | ---------- | ------- |
+| Management1 | OOB_MANAGEMENT | oob | MGMT | 172.16.1.101/24 | 172.16.1.1 |
+
+##### IPv6
+
+| Management Interface | Description | Type | VRF | IPv6 Address | IPv6 Gateway | ND RA Disabled | ND RA RX Accept | ND Managed Config Flag | ND Other Config Flag | ND Cache | ND RA DNS Servers |
+| -------------------- | ----------- | ---- | --- | ------------ | ------------ | -------------- | --------------- | ---------------------- | -------------------- | -------- | ----------------- |
+| Management1 | OOB_MANAGEMENT | oob | MGMT | - | - | - | - | - | - | - | - |
+
+#### Management Interfaces Device Configuration
+
+```eos
+!
+interface Management1
+ description OOB_MANAGEMENT
+ no shutdown
+ vrf MGMT
+ ip address 172.16.1.101/24
+```
+
+### Management API HTTP
+
+#### Management API HTTP Summary
+
+| HTTP | HTTPS | UNIX-Socket | Default Services | Session Timeout |
+| ---- | ----- | ----------- | ---------------- | --------------- |
+| False | True | - | - | 1440 minutes |
+
+#### Management API VRF Access
+
+| VRF Name | IPv4 ACL | IPv6 ACL |
+| -------- | -------- | -------- |
+| MGMT | - | - |
+
+#### Management API HTTP Device Configuration
+
+```eos
+!
+management api http-commands
+ protocol https
+ no shutdown
+ !
+ vrf MGMT
+ no shutdown
+```
+
+## Authentication
+
+### Enable Password
+
+Enable password has been disabled
+
+## Internal VLAN Allocation Policy
+
+### Internal VLAN Allocation Policy Summary
+
+| Policy Allocation | Range Beginning | Range Ending |
+| ----------------- | --------------- | ------------ |
+| ascending | 1006 | 1199 |
+
+### Internal VLAN Allocation Policy Device Configuration
+
+```eos
+!
+vlan internal order ascending range 1006 1199
+```
+
+## Interfaces
+
+### Ethernet Interfaces
+
+#### Ethernet Interfaces Summary
+
+##### L2
+
+| Interface | Description | Mode | VLANs | Native VLAN | Trunk Group | Channel-Group |
+| --------- | ----------- | ---- | ----- | ----------- | ----------- | ------------- |
+
+*Inherited from Port-Channel Interface
+
+##### IPv4
+
+| Interface | Description | Channel Group | IP Address | VRF | MTU | Shutdown | ACL In | ACL Out |
+| --------- | ----------- | ------------- | ---------- | --- | --- | -------- | ------ | ------- |
+| Ethernet1 | P2P_dc1-spine1_Ethernet1 | - | 10.255.255.1/31 | default | 9214 | False | - | - |
+| Ethernet1/2 | P2P_dc1-spine2_Ethernet1/2 | - | 10.255.255.3/31 | default | 9214 | False | - | - |
+
+#### Ethernet Interfaces Device Configuration
+
+```eos
+!
+interface Ethernet1
+ description P2P_dc1-spine1_Ethernet1
+ no shutdown
+ mtu 9214
+ no switchport
+ ip address 10.255.255.1/31
+!
+interface Ethernet1/2
+ description P2P_dc1-spine2_Ethernet1/2
+ no shutdown
+ mtu 9214
+ no switchport
+ ip address 10.255.255.3/31
+```
+
+### Loopback Interfaces
+
+#### Loopback Interfaces Summary
+
+##### IPv4
+
+| Interface | Description | VRF | IP Address |
+| --------- | ----------- | --- | ---------- |
+| Loopback0 | ROUTER_ID | default | 10.255.1.1/32 |
+
+##### IPv6
+
+| Interface | Description | VRF | IPv6 Addresses |
+| --------- | ----------- | --- | -------------- |
+| Loopback0 | ROUTER_ID | default | - |
+
+#### Loopback Interfaces Device Configuration
+
+```eos
+!
+interface Loopback0
+ description ROUTER_ID
+ no shutdown
+ ip address 10.255.1.1/32
+```
+
+## Routing
+
+### Service Routing Protocols Model
+
+Multi agent routing protocol model enabled
+
+```eos
+!
+service routing protocols model multi-agent
+```
+
+### IP Routing
+
+#### IP Routing Summary
+
+| VRF | Routing Enabled |
+| --- | --------------- |
+| default | True |
+| MGMT | False |
+
+#### IP Routing Device Configuration
+
+```eos
+!
+ip routing
+no ip routing vrf MGMT
+```
+
+### IPv6 Routing
+
+#### IPv6 Routing Summary
+
+| VRF | Routing Enabled |
+| --- | --------------- |
+| default | False |
+| MGMT | false |
+
+### Static Routes
+
+#### Static Routes Summary
+
+| VRF | Destination Prefix | Next Hop IP | Exit interface | Administrative Distance | Tag | Route Name | Metric |
+| --- | ------------------ | ----------- | -------------- | ----------------------- | --- | ---------- | ------ |
+| MGMT | 0.0.0.0/0 | 172.16.1.1 | - | 1 | - | - | - |
+
+#### Static Routes Device Configuration
+
+```eos
+!
+ip route vrf MGMT 0.0.0.0/0 172.16.1.1
+```
+
+### Router BGP
+
+ASN Notation: asplain
+
+#### Router BGP Summary
+
+| BGP AS | Router ID |
+| ------ | --------- |
+| 65101 | 10.255.1.1 |
+
+| BGP Tuning |
+| ---------- |
+| no bgp default ipv4-unicast |
+| maximum-paths 4 |
+
+#### Router BGP Peer Groups
+
+##### IPv4-UNDERLAY-PEERS
+
+| Settings | Value |
+| -------- | ----- |
+| Address Family | ipv4 |
+| Send community | all |
+| Maximum routes | 256000 |
+
+#### BGP Neighbors
+
+| Neighbor | Remote AS | VRF | Shutdown | Send-community | Maximum-routes | Allowas-in | BFD | RIB Pre-Policy Retain | Route-Reflector Client | Passive | TTL Max Hops |
+| -------- | --------- | --- | -------- | -------------- | -------------- | ---------- | --- | --------------------- | ---------------------- | ------- | ------------ |
+| 10.255.255.0 | 65100 | default | - | Inherited from peer group IPv4-UNDERLAY-PEERS | Inherited from peer group IPv4-UNDERLAY-PEERS | - | - | - | - | - | - |
+| 10.255.255.2 | 65100 | default | - | Inherited from peer group IPv4-UNDERLAY-PEERS | Inherited from peer group IPv4-UNDERLAY-PEERS | - | - | - | - | - | - |
+
+#### Router BGP Device Configuration
+
+```eos
+!
+router bgp 65101
+ router-id 10.255.1.1
+ no bgp default ipv4-unicast
+ maximum-paths 4
+ neighbor IPv4-UNDERLAY-PEERS peer group
+ neighbor IPv4-UNDERLAY-PEERS send-community
+ neighbor IPv4-UNDERLAY-PEERS maximum-routes 256000
+ neighbor 10.255.255.0 peer group IPv4-UNDERLAY-PEERS
+ neighbor 10.255.255.0 remote-as 65100
+ neighbor 10.255.255.0 description dc1-spine1_Ethernet1
+ neighbor 10.255.255.2 peer group IPv4-UNDERLAY-PEERS
+ neighbor 10.255.255.2 remote-as 65100
+ neighbor 10.255.255.2 description dc1-spine2_Ethernet1/2
+ redistribute connected
+ !
+ address-family ipv4
+ neighbor IPv4-UNDERLAY-PEERS activate
+```
+
+## Multicast
+
+### IP IGMP Snooping
+
+#### IP IGMP Snooping Summary
+
+| IGMP Snooping | Fast Leave | Interface Restart Query | Proxy | Restart Query Interval | Robustness Variable |
+| ------------- | ---------- | ----------------------- | ----- | ---------------------- | ------------------- |
+| Enabled | - | - | - | - | - |
+
+#### IP IGMP Snooping Device Configuration
+
+```eos
+```
+
+## VRF Instances
+
+### VRF Instances Summary
+
+| VRF Name | IP Routing |
+| -------- | ---------- |
+| MGMT | disabled |
+
+### VRF Instances Device Configuration
+
+```eos
+!
+vrf instance MGMT
+```
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/devices/dc1-leaf2a.md b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/devices/dc1-leaf2a.md
new file mode 100644
index 00000000000..5be95bfb64c
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/devices/dc1-leaf2a.md
@@ -0,0 +1,304 @@
+# dc1-leaf2a
+
+## Table of Contents
+
+- [Management](#management)
+ - [Management Interfaces](#management-interfaces)
+ - [Management API HTTP](#management-api-http)
+- [Authentication](#authentication)
+ - [Enable Password](#enable-password)
+- [Internal VLAN Allocation Policy](#internal-vlan-allocation-policy)
+ - [Internal VLAN Allocation Policy Summary](#internal-vlan-allocation-policy-summary)
+ - [Internal VLAN Allocation Policy Device Configuration](#internal-vlan-allocation-policy-device-configuration)
+- [Interfaces](#interfaces)
+ - [Ethernet Interfaces](#ethernet-interfaces)
+ - [Loopback Interfaces](#loopback-interfaces)
+- [Routing](#routing)
+ - [Service Routing Protocols Model](#service-routing-protocols-model)
+ - [IP Routing](#ip-routing)
+ - [IPv6 Routing](#ipv6-routing)
+ - [Static Routes](#static-routes)
+ - [Router BGP](#router-bgp)
+- [Multicast](#multicast)
+ - [IP IGMP Snooping](#ip-igmp-snooping)
+- [VRF Instances](#vrf-instances)
+ - [VRF Instances Summary](#vrf-instances-summary)
+ - [VRF Instances Device Configuration](#vrf-instances-device-configuration)
+
+## Management
+
+### Management Interfaces
+
+#### Management Interfaces Summary
+
+##### IPv4
+
+| Management Interface | Description | Type | VRF | IP Address | Gateway |
+| -------------------- | ----------- | ---- | --- | ---------- | ------- |
+| Management1 | OOB_MANAGEMENT | oob | MGMT | 172.16.1.102/24 | 172.16.1.1 |
+
+##### IPv6
+
+| Management Interface | Description | Type | VRF | IPv6 Address | IPv6 Gateway | ND RA Disabled | ND RA RX Accept | ND Managed Config Flag | ND Other Config Flag | ND Cache | ND RA DNS Servers |
+| -------------------- | ----------- | ---- | --- | ------------ | ------------ | -------------- | --------------- | ---------------------- | -------------------- | -------- | ----------------- |
+| Management1 | OOB_MANAGEMENT | oob | MGMT | - | - | - | - | - | - | - | - |
+
+#### Management Interfaces Device Configuration
+
+```eos
+!
+interface Management1
+ description OOB_MANAGEMENT
+ no shutdown
+ vrf MGMT
+ ip address 172.16.1.102/24
+```
+
+### Management API HTTP
+
+#### Management API HTTP Summary
+
+| HTTP | HTTPS | UNIX-Socket | Default Services | Session Timeout |
+| ---- | ----- | ----------- | ---------------- | --------------- |
+| False | True | - | - | 1440 minutes |
+
+#### Management API VRF Access
+
+| VRF Name | IPv4 ACL | IPv6 ACL |
+| -------- | -------- | -------- |
+| MGMT | - | - |
+
+#### Management API HTTP Device Configuration
+
+```eos
+!
+management api http-commands
+ protocol https
+ no shutdown
+ !
+ vrf MGMT
+ no shutdown
+```
+
+## Authentication
+
+### Enable Password
+
+Enable password has been disabled
+
+## Internal VLAN Allocation Policy
+
+### Internal VLAN Allocation Policy Summary
+
+| Policy Allocation | Range Beginning | Range Ending |
+| ----------------- | --------------- | ------------ |
+| ascending | 1006 | 1199 |
+
+### Internal VLAN Allocation Policy Device Configuration
+
+```eos
+!
+vlan internal order ascending range 1006 1199
+```
+
+## Interfaces
+
+### Ethernet Interfaces
+
+#### Ethernet Interfaces Summary
+
+##### L2
+
+| Interface | Description | Mode | VLANs | Native VLAN | Trunk Group | Channel-Group |
+| --------- | ----------- | ---- | ----- | ----------- | ----------- | ------------- |
+
+*Inherited from Port-Channel Interface
+
+##### IPv4
+
+| Interface | Description | Channel Group | IP Address | VRF | MTU | Shutdown | ACL In | ACL Out |
+| --------- | ----------- | ------------- | ---------- | --- | --- | -------- | ------ | ------- |
+| Ethernet1/1 | P2P_dc1-spine1_Ethernet1/3 | - | 10.255.255.5/31 | default | 9214 | False | - | - |
+| Ethernet2 | P2P_dc1-spine2_Ethernet2 | - | 10.255.255.7/31 | default | 9214 | False | - | - |
+
+#### Ethernet Interfaces Device Configuration
+
+```eos
+!
+interface Ethernet1/1
+ description P2P_dc1-spine1_Ethernet1/3
+ no shutdown
+ mtu 9214
+ no switchport
+ ip address 10.255.255.5/31
+!
+interface Ethernet2
+ description P2P_dc1-spine2_Ethernet2
+ no shutdown
+ mtu 9214
+ no switchport
+ ip address 10.255.255.7/31
+```
+
+### Loopback Interfaces
+
+#### Loopback Interfaces Summary
+
+##### IPv4
+
+| Interface | Description | VRF | IP Address |
+| --------- | ----------- | --- | ---------- |
+| Loopback0 | ROUTER_ID | default | 10.255.1.2/32 |
+
+##### IPv6
+
+| Interface | Description | VRF | IPv6 Addresses |
+| --------- | ----------- | --- | -------------- |
+| Loopback0 | ROUTER_ID | default | - |
+
+#### Loopback Interfaces Device Configuration
+
+```eos
+!
+interface Loopback0
+ description ROUTER_ID
+ no shutdown
+ ip address 10.255.1.2/32
+```
+
+## Routing
+
+### Service Routing Protocols Model
+
+Multi agent routing protocol model enabled
+
+```eos
+!
+service routing protocols model multi-agent
+```
+
+### IP Routing
+
+#### IP Routing Summary
+
+| VRF | Routing Enabled |
+| --- | --------------- |
+| default | True |
+| MGMT | False |
+
+#### IP Routing Device Configuration
+
+```eos
+!
+ip routing
+no ip routing vrf MGMT
+```
+
+### IPv6 Routing
+
+#### IPv6 Routing Summary
+
+| VRF | Routing Enabled |
+| --- | --------------- |
+| default | False |
+| MGMT | false |
+
+### Static Routes
+
+#### Static Routes Summary
+
+| VRF | Destination Prefix | Next Hop IP | Exit interface | Administrative Distance | Tag | Route Name | Metric |
+| --- | ------------------ | ----------- | -------------- | ----------------------- | --- | ---------- | ------ |
+| MGMT | 0.0.0.0/0 | 172.16.1.1 | - | 1 | - | - | - |
+
+#### Static Routes Device Configuration
+
+```eos
+!
+ip route vrf MGMT 0.0.0.0/0 172.16.1.1
+```
+
+### Router BGP
+
+ASN Notation: asplain
+
+#### Router BGP Summary
+
+| BGP AS | Router ID |
+| ------ | --------- |
+| 65102 | 10.255.1.2 |
+
+| BGP Tuning |
+| ---------- |
+| no bgp default ipv4-unicast |
+| maximum-paths 4 |
+
+#### Router BGP Peer Groups
+
+##### IPv4-UNDERLAY-PEERS
+
+| Settings | Value |
+| -------- | ----- |
+| Address Family | ipv4 |
+| Send community | all |
+| Maximum routes | 256000 |
+
+#### BGP Neighbors
+
+| Neighbor | Remote AS | VRF | Shutdown | Send-community | Maximum-routes | Allowas-in | BFD | RIB Pre-Policy Retain | Route-Reflector Client | Passive | TTL Max Hops |
+| -------- | --------- | --- | -------- | -------------- | -------------- | ---------- | --- | --------------------- | ---------------------- | ------- | ------------ |
+| 10.255.255.4 | 65100 | default | - | Inherited from peer group IPv4-UNDERLAY-PEERS | Inherited from peer group IPv4-UNDERLAY-PEERS | - | - | - | - | - | - |
+| 10.255.255.6 | 65100 | default | - | Inherited from peer group IPv4-UNDERLAY-PEERS | Inherited from peer group IPv4-UNDERLAY-PEERS | - | - | - | - | - | - |
+
+#### Router BGP Device Configuration
+
+```eos
+!
+router bgp 65102
+ router-id 10.255.1.2
+ no bgp default ipv4-unicast
+ maximum-paths 4
+ neighbor IPv4-UNDERLAY-PEERS peer group
+ neighbor IPv4-UNDERLAY-PEERS send-community
+ neighbor IPv4-UNDERLAY-PEERS maximum-routes 256000
+ neighbor 10.255.255.4 peer group IPv4-UNDERLAY-PEERS
+ neighbor 10.255.255.4 remote-as 65100
+ neighbor 10.255.255.4 description dc1-spine1_Ethernet1/3
+ neighbor 10.255.255.6 peer group IPv4-UNDERLAY-PEERS
+ neighbor 10.255.255.6 remote-as 65100
+ neighbor 10.255.255.6 description dc1-spine2_Ethernet2
+ redistribute connected
+ !
+ address-family ipv4
+ neighbor IPv4-UNDERLAY-PEERS activate
+```
+
+## Multicast
+
+### IP IGMP Snooping
+
+#### IP IGMP Snooping Summary
+
+| IGMP Snooping | Fast Leave | Interface Restart Query | Proxy | Restart Query Interval | Robustness Variable |
+| ------------- | ---------- | ----------------------- | ----- | ---------------------- | ------------------- |
+| Enabled | - | - | - | - | - |
+
+#### IP IGMP Snooping Device Configuration
+
+```eos
+```
+
+## VRF Instances
+
+### VRF Instances Summary
+
+| VRF Name | IP Routing |
+| -------- | ---------- |
+| MGMT | disabled |
+
+### VRF Instances Device Configuration
+
+```eos
+!
+vrf instance MGMT
+```
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/devices/dc1-spine1.md b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/devices/dc1-spine1.md
new file mode 100644
index 00000000000..d4f70cb43e4
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/devices/dc1-spine1.md
@@ -0,0 +1,303 @@
+# dc1-spine1
+
+## Table of Contents
+
+- [Management](#management)
+ - [Management Interfaces](#management-interfaces)
+ - [Management API HTTP](#management-api-http)
+- [Authentication](#authentication)
+ - [Enable Password](#enable-password)
+- [Spanning Tree](#spanning-tree)
+ - [Spanning Tree Summary](#spanning-tree-summary)
+ - [Spanning Tree Device Configuration](#spanning-tree-device-configuration)
+- [Internal VLAN Allocation Policy](#internal-vlan-allocation-policy)
+ - [Internal VLAN Allocation Policy Summary](#internal-vlan-allocation-policy-summary)
+ - [Internal VLAN Allocation Policy Device Configuration](#internal-vlan-allocation-policy-device-configuration)
+- [Interfaces](#interfaces)
+ - [Ethernet Interfaces](#ethernet-interfaces)
+ - [Loopback Interfaces](#loopback-interfaces)
+- [Routing](#routing)
+ - [Service Routing Protocols Model](#service-routing-protocols-model)
+ - [IP Routing](#ip-routing)
+ - [IPv6 Routing](#ipv6-routing)
+ - [Static Routes](#static-routes)
+ - [Router BGP](#router-bgp)
+- [VRF Instances](#vrf-instances)
+ - [VRF Instances Summary](#vrf-instances-summary)
+ - [VRF Instances Device Configuration](#vrf-instances-device-configuration)
+
+## Management
+
+### Management Interfaces
+
+#### Management Interfaces Summary
+
+##### IPv4
+
+| Management Interface | Description | Type | VRF | IP Address | Gateway |
+| -------------------- | ----------- | ---- | --- | ---------- | ------- |
+| Management1 | OOB_MANAGEMENT | oob | MGMT | 172.16.1.11/24 | 172.16.1.1 |
+
+##### IPv6
+
+| Management Interface | Description | Type | VRF | IPv6 Address | IPv6 Gateway | ND RA Disabled | ND RA RX Accept | ND Managed Config Flag | ND Other Config Flag | ND Cache | ND RA DNS Servers |
+| -------------------- | ----------- | ---- | --- | ------------ | ------------ | -------------- | --------------- | ---------------------- | -------------------- | -------- | ----------------- |
+| Management1 | OOB_MANAGEMENT | oob | MGMT | - | - | - | - | - | - | - | - |
+
+#### Management Interfaces Device Configuration
+
+```eos
+!
+interface Management1
+ description OOB_MANAGEMENT
+ no shutdown
+ vrf MGMT
+ ip address 172.16.1.11/24
+```
+
+### Management API HTTP
+
+#### Management API HTTP Summary
+
+| HTTP | HTTPS | UNIX-Socket | Default Services | Session Timeout |
+| ---- | ----- | ----------- | ---------------- | --------------- |
+| False | True | - | - | 1440 minutes |
+
+#### Management API VRF Access
+
+| VRF Name | IPv4 ACL | IPv6 ACL |
+| -------- | -------- | -------- |
+| MGMT | - | - |
+
+#### Management API HTTP Device Configuration
+
+```eos
+!
+management api http-commands
+ protocol https
+ no shutdown
+ !
+ vrf MGMT
+ no shutdown
+```
+
+## Authentication
+
+### Enable Password
+
+Enable password has been disabled
+
+## Spanning Tree
+
+### Spanning Tree Summary
+
+STP mode: **none**
+
+### Spanning Tree Device Configuration
+
+```eos
+!
+spanning-tree mode none
+```
+
+## Internal VLAN Allocation Policy
+
+### Internal VLAN Allocation Policy Summary
+
+| Policy Allocation | Range Beginning | Range Ending |
+| ----------------- | --------------- | ------------ |
+| ascending | 1006 | 1199 |
+
+### Internal VLAN Allocation Policy Device Configuration
+
+```eos
+!
+vlan internal order ascending range 1006 1199
+```
+
+## Interfaces
+
+### Ethernet Interfaces
+
+#### Ethernet Interfaces Summary
+
+##### L2
+
+| Interface | Description | Mode | VLANs | Native VLAN | Trunk Group | Channel-Group |
+| --------- | ----------- | ---- | ----- | ----------- | ----------- | ------------- |
+
+*Inherited from Port-Channel Interface
+
+##### IPv4
+
+| Interface | Description | Channel Group | IP Address | VRF | MTU | Shutdown | ACL In | ACL Out |
+| --------- | ----------- | ------------- | ---------- | --- | --- | -------- | ------ | ------- |
+| Ethernet1 | P2P_dc1-leaf1a_Ethernet1 | - | 10.255.255.0/31 | default | 9214 | False | - | - |
+| Ethernet1/3 | P2P_dc1-leaf2a_Ethernet1/1 | - | 10.255.255.4/31 | default | 9214 | False | - | - |
+
+#### Ethernet Interfaces Device Configuration
+
+```eos
+!
+interface Ethernet1
+ description P2P_dc1-leaf1a_Ethernet1
+ no shutdown
+ mtu 9214
+ no switchport
+ ip address 10.255.255.0/31
+!
+interface Ethernet1/3
+ description P2P_dc1-leaf2a_Ethernet1/1
+ no shutdown
+ mtu 9214
+ no switchport
+ ip address 10.255.255.4/31
+```
+
+### Loopback Interfaces
+
+#### Loopback Interfaces Summary
+
+##### IPv4
+
+| Interface | Description | VRF | IP Address |
+| --------- | ----------- | --- | ---------- |
+| Loopback0 | ROUTER_ID | default | 10.255.0.1/32 |
+
+##### IPv6
+
+| Interface | Description | VRF | IPv6 Addresses |
+| --------- | ----------- | --- | -------------- |
+| Loopback0 | ROUTER_ID | default | - |
+
+#### Loopback Interfaces Device Configuration
+
+```eos
+!
+interface Loopback0
+ description ROUTER_ID
+ no shutdown
+ ip address 10.255.0.1/32
+```
+
+## Routing
+
+### Service Routing Protocols Model
+
+Multi agent routing protocol model enabled
+
+```eos
+!
+service routing protocols model multi-agent
+```
+
+### IP Routing
+
+#### IP Routing Summary
+
+| VRF | Routing Enabled |
+| --- | --------------- |
+| default | True |
+| MGMT | False |
+
+#### IP Routing Device Configuration
+
+```eos
+!
+ip routing
+no ip routing vrf MGMT
+```
+
+### IPv6 Routing
+
+#### IPv6 Routing Summary
+
+| VRF | Routing Enabled |
+| --- | --------------- |
+| default | False |
+| MGMT | false |
+
+### Static Routes
+
+#### Static Routes Summary
+
+| VRF | Destination Prefix | Next Hop IP | Exit interface | Administrative Distance | Tag | Route Name | Metric |
+| --- | ------------------ | ----------- | -------------- | ----------------------- | --- | ---------- | ------ |
+| MGMT | 0.0.0.0/0 | 172.16.1.1 | - | 1 | - | - | - |
+
+#### Static Routes Device Configuration
+
+```eos
+!
+ip route vrf MGMT 0.0.0.0/0 172.16.1.1
+```
+
+### Router BGP
+
+ASN Notation: asplain
+
+#### Router BGP Summary
+
+| BGP AS | Router ID |
+| ------ | --------- |
+| 65100 | 10.255.0.1 |
+
+| BGP Tuning |
+| ---------- |
+| no bgp default ipv4-unicast |
+| maximum-paths 4 |
+
+#### Router BGP Peer Groups
+
+##### IPv4-UNDERLAY-PEERS
+
+| Settings | Value |
+| -------- | ----- |
+| Address Family | ipv4 |
+| Send community | all |
+| Maximum routes | 256000 |
+
+#### BGP Neighbors
+
+| Neighbor | Remote AS | VRF | Shutdown | Send-community | Maximum-routes | Allowas-in | BFD | RIB Pre-Policy Retain | Route-Reflector Client | Passive | TTL Max Hops |
+| -------- | --------- | --- | -------- | -------------- | -------------- | ---------- | --- | --------------------- | ---------------------- | ------- | ------------ |
+| 10.255.255.1 | 65101 | default | - | Inherited from peer group IPv4-UNDERLAY-PEERS | Inherited from peer group IPv4-UNDERLAY-PEERS | - | - | - | - | - | - |
+| 10.255.255.5 | 65102 | default | - | Inherited from peer group IPv4-UNDERLAY-PEERS | Inherited from peer group IPv4-UNDERLAY-PEERS | - | - | - | - | - | - |
+
+#### Router BGP Device Configuration
+
+```eos
+!
+router bgp 65100
+ router-id 10.255.0.1
+ no bgp default ipv4-unicast
+ maximum-paths 4
+ neighbor IPv4-UNDERLAY-PEERS peer group
+ neighbor IPv4-UNDERLAY-PEERS send-community
+ neighbor IPv4-UNDERLAY-PEERS maximum-routes 256000
+ neighbor 10.255.255.1 peer group IPv4-UNDERLAY-PEERS
+ neighbor 10.255.255.1 remote-as 65101
+ neighbor 10.255.255.1 description dc1-leaf1a_Ethernet1
+ neighbor 10.255.255.5 peer group IPv4-UNDERLAY-PEERS
+ neighbor 10.255.255.5 remote-as 65102
+ neighbor 10.255.255.5 description dc1-leaf2a_Ethernet1/1
+ redistribute connected
+ !
+ address-family ipv4
+ neighbor IPv4-UNDERLAY-PEERS activate
+```
+
+## VRF Instances
+
+### VRF Instances Summary
+
+| VRF Name | IP Routing |
+| -------- | ---------- |
+| MGMT | disabled |
+
+### VRF Instances Device Configuration
+
+```eos
+!
+vrf instance MGMT
+```
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/devices/dc1-spine2.md b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/devices/dc1-spine2.md
new file mode 100644
index 00000000000..31b2c5f32ce
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/devices/dc1-spine2.md
@@ -0,0 +1,303 @@
+# dc1-spine2
+
+## Table of Contents
+
+- [Management](#management)
+ - [Management Interfaces](#management-interfaces)
+ - [Management API HTTP](#management-api-http)
+- [Authentication](#authentication)
+ - [Enable Password](#enable-password)
+- [Spanning Tree](#spanning-tree)
+ - [Spanning Tree Summary](#spanning-tree-summary)
+ - [Spanning Tree Device Configuration](#spanning-tree-device-configuration)
+- [Internal VLAN Allocation Policy](#internal-vlan-allocation-policy)
+ - [Internal VLAN Allocation Policy Summary](#internal-vlan-allocation-policy-summary)
+ - [Internal VLAN Allocation Policy Device Configuration](#internal-vlan-allocation-policy-device-configuration)
+- [Interfaces](#interfaces)
+ - [Ethernet Interfaces](#ethernet-interfaces)
+ - [Loopback Interfaces](#loopback-interfaces)
+- [Routing](#routing)
+ - [Service Routing Protocols Model](#service-routing-protocols-model)
+ - [IP Routing](#ip-routing)
+ - [IPv6 Routing](#ipv6-routing)
+ - [Static Routes](#static-routes)
+ - [Router BGP](#router-bgp)
+- [VRF Instances](#vrf-instances)
+ - [VRF Instances Summary](#vrf-instances-summary)
+ - [VRF Instances Device Configuration](#vrf-instances-device-configuration)
+
+## Management
+
+### Management Interfaces
+
+#### Management Interfaces Summary
+
+##### IPv4
+
+| Management Interface | Description | Type | VRF | IP Address | Gateway |
+| -------------------- | ----------- | ---- | --- | ---------- | ------- |
+| Management1 | OOB_MANAGEMENT | oob | MGMT | 172.16.1.12/24 | 172.16.1.1 |
+
+##### IPv6
+
+| Management Interface | Description | Type | VRF | IPv6 Address | IPv6 Gateway | ND RA Disabled | ND RA RX Accept | ND Managed Config Flag | ND Other Config Flag | ND Cache | ND RA DNS Servers |
+| -------------------- | ----------- | ---- | --- | ------------ | ------------ | -------------- | --------------- | ---------------------- | -------------------- | -------- | ----------------- |
+| Management1 | OOB_MANAGEMENT | oob | MGMT | - | - | - | - | - | - | - | - |
+
+#### Management Interfaces Device Configuration
+
+```eos
+!
+interface Management1
+ description OOB_MANAGEMENT
+ no shutdown
+ vrf MGMT
+ ip address 172.16.1.12/24
+```
+
+### Management API HTTP
+
+#### Management API HTTP Summary
+
+| HTTP | HTTPS | UNIX-Socket | Default Services | Session Timeout |
+| ---- | ----- | ----------- | ---------------- | --------------- |
+| False | True | - | - | 1440 minutes |
+
+#### Management API VRF Access
+
+| VRF Name | IPv4 ACL | IPv6 ACL |
+| -------- | -------- | -------- |
+| MGMT | - | - |
+
+#### Management API HTTP Device Configuration
+
+```eos
+!
+management api http-commands
+ protocol https
+ no shutdown
+ !
+ vrf MGMT
+ no shutdown
+```
+
+## Authentication
+
+### Enable Password
+
+Enable password has been disabled
+
+## Spanning Tree
+
+### Spanning Tree Summary
+
+STP mode: **none**
+
+### Spanning Tree Device Configuration
+
+```eos
+!
+spanning-tree mode none
+```
+
+## Internal VLAN Allocation Policy
+
+### Internal VLAN Allocation Policy Summary
+
+| Policy Allocation | Range Beginning | Range Ending |
+| ----------------- | --------------- | ------------ |
+| ascending | 1006 | 1199 |
+
+### Internal VLAN Allocation Policy Device Configuration
+
+```eos
+!
+vlan internal order ascending range 1006 1199
+```
+
+## Interfaces
+
+### Ethernet Interfaces
+
+#### Ethernet Interfaces Summary
+
+##### L2
+
+| Interface | Description | Mode | VLANs | Native VLAN | Trunk Group | Channel-Group |
+| --------- | ----------- | ---- | ----- | ----------- | ----------- | ------------- |
+
+*Inherited from Port-Channel Interface
+
+##### IPv4
+
+| Interface | Description | Channel Group | IP Address | VRF | MTU | Shutdown | ACL In | ACL Out |
+| --------- | ----------- | ------------- | ---------- | --- | --- | -------- | ------ | ------- |
+| Ethernet1/2 | P2P_dc1-leaf1a_Ethernet1/2 | - | 10.255.255.2/31 | default | 9214 | False | - | - |
+| Ethernet2 | P2P_dc1-leaf2a_Ethernet2 | - | 10.255.255.6/31 | default | 9214 | False | - | - |
+
+#### Ethernet Interfaces Device Configuration
+
+```eos
+!
+interface Ethernet1/2
+ description P2P_dc1-leaf1a_Ethernet1/2
+ no shutdown
+ mtu 9214
+ no switchport
+ ip address 10.255.255.2/31
+!
+interface Ethernet2
+ description P2P_dc1-leaf2a_Ethernet2
+ no shutdown
+ mtu 9214
+ no switchport
+ ip address 10.255.255.6/31
+```
+
+### Loopback Interfaces
+
+#### Loopback Interfaces Summary
+
+##### IPv4
+
+| Interface | Description | VRF | IP Address |
+| --------- | ----------- | --- | ---------- |
+| Loopback0 | ROUTER_ID | default | 10.255.0.2/32 |
+
+##### IPv6
+
+| Interface | Description | VRF | IPv6 Addresses |
+| --------- | ----------- | --- | -------------- |
+| Loopback0 | ROUTER_ID | default | - |
+
+#### Loopback Interfaces Device Configuration
+
+```eos
+!
+interface Loopback0
+ description ROUTER_ID
+ no shutdown
+ ip address 10.255.0.2/32
+```
+
+## Routing
+
+### Service Routing Protocols Model
+
+Multi agent routing protocol model enabled
+
+```eos
+!
+service routing protocols model multi-agent
+```
+
+### IP Routing
+
+#### IP Routing Summary
+
+| VRF | Routing Enabled |
+| --- | --------------- |
+| default | True |
+| MGMT | False |
+
+#### IP Routing Device Configuration
+
+```eos
+!
+ip routing
+no ip routing vrf MGMT
+```
+
+### IPv6 Routing
+
+#### IPv6 Routing Summary
+
+| VRF | Routing Enabled |
+| --- | --------------- |
+| default | False |
+| MGMT | false |
+
+### Static Routes
+
+#### Static Routes Summary
+
+| VRF | Destination Prefix | Next Hop IP | Exit interface | Administrative Distance | Tag | Route Name | Metric |
+| --- | ------------------ | ----------- | -------------- | ----------------------- | --- | ---------- | ------ |
+| MGMT | 0.0.0.0/0 | 172.16.1.1 | - | 1 | - | - | - |
+
+#### Static Routes Device Configuration
+
+```eos
+!
+ip route vrf MGMT 0.0.0.0/0 172.16.1.1
+```
+
+### Router BGP
+
+ASN Notation: asplain
+
+#### Router BGP Summary
+
+| BGP AS | Router ID |
+| ------ | --------- |
+| 65100 | 10.255.0.2 |
+
+| BGP Tuning |
+| ---------- |
+| no bgp default ipv4-unicast |
+| maximum-paths 4 |
+
+#### Router BGP Peer Groups
+
+##### IPv4-UNDERLAY-PEERS
+
+| Settings | Value |
+| -------- | ----- |
+| Address Family | ipv4 |
+| Send community | all |
+| Maximum routes | 256000 |
+
+#### BGP Neighbors
+
+| Neighbor | Remote AS | VRF | Shutdown | Send-community | Maximum-routes | Allowas-in | BFD | RIB Pre-Policy Retain | Route-Reflector Client | Passive | TTL Max Hops |
+| -------- | --------- | --- | -------- | -------------- | -------------- | ---------- | --- | --------------------- | ---------------------- | ------- | ------------ |
+| 10.255.255.3 | 65101 | default | - | Inherited from peer group IPv4-UNDERLAY-PEERS | Inherited from peer group IPv4-UNDERLAY-PEERS | - | - | - | - | - | - |
+| 10.255.255.7 | 65102 | default | - | Inherited from peer group IPv4-UNDERLAY-PEERS | Inherited from peer group IPv4-UNDERLAY-PEERS | - | - | - | - | - | - |
+
+#### Router BGP Device Configuration
+
+```eos
+!
+router bgp 65100
+ router-id 10.255.0.2
+ no bgp default ipv4-unicast
+ maximum-paths 4
+ neighbor IPv4-UNDERLAY-PEERS peer group
+ neighbor IPv4-UNDERLAY-PEERS send-community
+ neighbor IPv4-UNDERLAY-PEERS maximum-routes 256000
+ neighbor 10.255.255.3 peer group IPv4-UNDERLAY-PEERS
+ neighbor 10.255.255.3 remote-as 65101
+ neighbor 10.255.255.3 description dc1-leaf1a_Ethernet1/2
+ neighbor 10.255.255.7 peer group IPv4-UNDERLAY-PEERS
+ neighbor 10.255.255.7 remote-as 65102
+ neighbor 10.255.255.7 description dc1-leaf2a_Ethernet2
+ redistribute connected
+ !
+ address-family ipv4
+ neighbor IPv4-UNDERLAY-PEERS activate
+```
+
+## VRF Instances
+
+### VRF Instances Summary
+
+| VRF Name | IP Routing |
+| -------- | ---------- |
+| MGMT | disabled |
+
+### VRF Instances Device Configuration
+
+```eos
+!
+vrf instance MGMT
+```
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/fabric/DIGITAL_TWIN_CONTAINERLAB-documentation.md b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/fabric/DIGITAL_TWIN_CONTAINERLAB-documentation.md
new file mode 100644
index 00000000000..af670650fb9
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/fabric/DIGITAL_TWIN_CONTAINERLAB-documentation.md
@@ -0,0 +1,83 @@
+# DIGITAL_TWIN_CONTAINERLAB
+
+## Table of Contents
+
+- [Fabric Switches and Management IP](#fabric-switches-and-management-ip)
+ - [Fabric Switches with inband Management IP](#fabric-switches-with-inband-management-ip)
+- [Fabric Topology](#fabric-topology)
+- [Fabric IP Allocation](#fabric-ip-allocation)
+ - [Fabric Point-To-Point Links](#fabric-point-to-point-links)
+ - [Point-To-Point Links Node Allocation](#point-to-point-links-node-allocation)
+ - [Loopback Interfaces (BGP EVPN Peering)](#loopback-interfaces-bgp-evpn-peering)
+ - [Loopback0 Interfaces Node Allocation](#loopback0-interfaces-node-allocation)
+ - [VTEP Loopback VXLAN Tunnel Source Interfaces (VTEPs Only)](#vtep-loopback-vxlan-tunnel-source-interfaces-vteps-only)
+ - [VTEP Loopback Node allocation](#vtep-loopback-node-allocation)
+
+## Fabric Switches and Management IP
+
+| POD | Type | Node | Management IP | Platform | Provisioned in CloudVision | Serial Number |
+| --- | ---- | ---- | ------------- | -------- | -------------------------- | ------------- |
+| DIGITAL_TWIN_CONTAINERLAB | l3leaf | dc1-leaf1a | 172.16.1.101/24 | cEOSLab | Provisioned | - |
+| DIGITAL_TWIN_CONTAINERLAB | l3leaf | dc1-leaf2a | 172.16.1.102/24 | cEOSLab | Provisioned | - |
+| DIGITAL_TWIN_CONTAINERLAB | spine | dc1-spine1 | 172.16.1.11/24 | cEOSLab | Provisioned | - |
+| DIGITAL_TWIN_CONTAINERLAB | spine | dc1-spine2 | 172.16.1.12/24 | cEOSLab | Provisioned | - |
+
+> Provision status is based on Ansible inventory declaration and do not represent real status from CloudVision.
+
+### Fabric Switches with inband Management IP
+
+| POD | Type | Node | Management IP | Inband Interface |
+| --- | ---- | ---- | ------------- | ---------------- |
+
+## Fabric Topology
+
+| Type | Node | Node Interface | Peer Type | Peer Node | Peer Interface |
+| ---- | ---- | -------------- | --------- | --------- | -------------- |
+| l3leaf | dc1-leaf1a | Ethernet1 | spine | dc1-spine1 | Ethernet1 |
+| l3leaf | dc1-leaf1a | Ethernet1/2 | spine | dc1-spine2 | Ethernet1/2 |
+| l3leaf | dc1-leaf2a | Ethernet1/1 | spine | dc1-spine1 | Ethernet1/3 |
+| l3leaf | dc1-leaf2a | Ethernet2 | spine | dc1-spine2 | Ethernet2 |
+
+## Fabric IP Allocation
+
+### Fabric Point-To-Point Links
+
+| Uplink IPv4 Pool | Available Addresses | Assigned addresses | Assigned Address % |
+| ---------------- | ------------------- | ------------------ | ------------------ |
+| 10.255.255.0/26 | 64 | 8 | 12.5 % |
+
+### Point-To-Point Links Node Allocation
+
+| Node | Node Interface | Node IP Address | Peer Node | Peer Interface | Peer IP Address |
+| ---- | -------------- | --------------- | --------- | -------------- | --------------- |
+| dc1-leaf1a | Ethernet1 | 10.255.255.1/31 | dc1-spine1 | Ethernet1 | 10.255.255.0/31 |
+| dc1-leaf1a | Ethernet1/2 | 10.255.255.3/31 | dc1-spine2 | Ethernet1/2 | 10.255.255.2/31 |
+| dc1-leaf2a | Ethernet1/1 | 10.255.255.5/31 | dc1-spine1 | Ethernet1/3 | 10.255.255.4/31 |
+| dc1-leaf2a | Ethernet2 | 10.255.255.7/31 | dc1-spine2 | Ethernet2 | 10.255.255.6/31 |
+
+### Loopback Interfaces (BGP EVPN Peering)
+
+| Loopback Pool | Available Addresses | Assigned addresses | Assigned Address % |
+| ------------- | ------------------- | ------------------ | ------------------ |
+| 10.255.0.0/27 | 32 | 2 | 6.25 % |
+| 10.255.1.0/27 | 32 | 2 | 6.25 % |
+
+### Loopback0 Interfaces Node Allocation
+
+| POD | Node | Loopback0 |
+| --- | ---- | --------- |
+| DIGITAL_TWIN_CONTAINERLAB | dc1-leaf1a | 10.255.1.1/32 |
+| DIGITAL_TWIN_CONTAINERLAB | dc1-leaf2a | 10.255.1.2/32 |
+| DIGITAL_TWIN_CONTAINERLAB | dc1-spine1 | 10.255.0.1/32 |
+| DIGITAL_TWIN_CONTAINERLAB | dc1-spine2 | 10.255.0.2/32 |
+
+### VTEP Loopback VXLAN Tunnel Source Interfaces (VTEPs Only)
+
+| VTEP Loopback Pool | Available Addresses | Assigned addresses | Assigned Address % |
+| ------------------ | ------------------- | ------------------ | ------------------ |
+| 10.255.2.0/27 | 32 | 0 | 0.0 % |
+
+### VTEP Loopback Node allocation
+
+| POD | Node | Loopback1 |
+| --- | ---- | --------- |
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/fabric/DIGITAL_TWIN_CONTAINERLAB-p2p-links.csv b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/fabric/DIGITAL_TWIN_CONTAINERLAB-p2p-links.csv
new file mode 100644
index 00000000000..f856c1423b8
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/fabric/DIGITAL_TWIN_CONTAINERLAB-p2p-links.csv
@@ -0,0 +1,5 @@
+Type,Node,Serial Number,Node Interface,Leaf IP Address,Peer Type,Peer Node,Peer Serial Number,Peer Interface,Peer IP Address
+l3leaf,dc1-leaf1a,,Ethernet1,10.255.255.1/31,spine,dc1-spine1,,Ethernet1,10.255.255.0/31
+l3leaf,dc1-leaf1a,,Ethernet1/2,10.255.255.3/31,spine,dc1-spine2,,Ethernet1/2,10.255.255.2/31
+l3leaf,dc1-leaf2a,,Ethernet1/1,10.255.255.5/31,spine,dc1-spine1,,Ethernet1/3,10.255.255.4/31
+l3leaf,dc1-leaf2a,,Ethernet2,10.255.255.7/31,spine,dc1-spine2,,Ethernet2,10.255.255.6/31
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/fabric/DIGITAL_TWIN_CONTAINERLAB-topology.csv b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/fabric/DIGITAL_TWIN_CONTAINERLAB-topology.csv
new file mode 100644
index 00000000000..31951f5255c
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/documentation/fabric/DIGITAL_TWIN_CONTAINERLAB-topology.csv
@@ -0,0 +1,9 @@
+Node Type,Node,Serial Number,Node Interface,Peer Type,Peer Node,Peer Serial Number,Peer Interface,Node Interface Enabled
+l3leaf,dc1-leaf1a,,Ethernet1,spine,dc1-spine1,,Ethernet1,True
+l3leaf,dc1-leaf1a,,Ethernet1/2,spine,dc1-spine2,,Ethernet1/2,True
+l3leaf,dc1-leaf2a,,Ethernet1/1,spine,dc1-spine1,,Ethernet1/3,True
+l3leaf,dc1-leaf2a,,Ethernet2,spine,dc1-spine2,,Ethernet2,True
+spine,dc1-spine1,,Ethernet1,l3leaf,dc1-leaf1a,,Ethernet1,True
+spine,dc1-spine1,,Ethernet1/3,l3leaf,dc1-leaf2a,,Ethernet1/1,True
+spine,dc1-spine2,,Ethernet1/2,l3leaf,dc1-leaf1a,,Ethernet1/2,True
+spine,dc1-spine2,,Ethernet2,l3leaf,dc1-leaf2a,,Ethernet2,True
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/configs/dc1-leaf1a.cfg b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/configs/dc1-leaf1a.cfg
new file mode 100644
index 00000000000..b46cbe4f397
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/configs/dc1-leaf1a.cfg
@@ -0,0 +1,70 @@
+!
+no enable password
+no aaa root
+!
+vlan internal order ascending range 1006 1199
+!
+transceiver qsfp default-mode 4x10G
+!
+service routing protocols model multi-agent
+!
+hostname dc1-leaf1a
+!
+vrf instance MGMT
+!
+management api http-commands
+ protocol https
+ no shutdown
+ !
+ vrf MGMT
+ no shutdown
+!
+interface Ethernet1
+ description P2P_dc1-spine1_Ethernet1
+ no shutdown
+ mtu 9214
+ no switchport
+ ip address 10.255.255.1/31
+!
+interface Ethernet1/2
+ description P2P_dc1-spine2_Ethernet1/2
+ no shutdown
+ mtu 9214
+ no switchport
+ ip address 10.255.255.3/31
+!
+interface Loopback0
+ description ROUTER_ID
+ no shutdown
+ ip address 10.255.1.1/32
+!
+interface Management1
+ description OOB_MANAGEMENT
+ no shutdown
+ vrf MGMT
+ ip address 172.16.1.101/24
+!
+ip routing
+no ip routing vrf MGMT
+!
+ip route vrf MGMT 0.0.0.0/0 172.16.1.1
+!
+router bgp 65101
+ router-id 10.255.1.1
+ no bgp default ipv4-unicast
+ maximum-paths 4
+ neighbor IPv4-UNDERLAY-PEERS peer group
+ neighbor IPv4-UNDERLAY-PEERS send-community
+ neighbor IPv4-UNDERLAY-PEERS maximum-routes 256000
+ neighbor 10.255.255.0 peer group IPv4-UNDERLAY-PEERS
+ neighbor 10.255.255.0 remote-as 65100
+ neighbor 10.255.255.0 description dc1-spine1_Ethernet1
+ neighbor 10.255.255.2 peer group IPv4-UNDERLAY-PEERS
+ neighbor 10.255.255.2 remote-as 65100
+ neighbor 10.255.255.2 description dc1-spine2_Ethernet1/2
+ redistribute connected
+ !
+ address-family ipv4
+ neighbor IPv4-UNDERLAY-PEERS activate
+!
+end
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/configs/dc1-leaf2a.cfg b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/configs/dc1-leaf2a.cfg
new file mode 100644
index 00000000000..a642f6dc0c6
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/configs/dc1-leaf2a.cfg
@@ -0,0 +1,70 @@
+!
+no enable password
+no aaa root
+!
+vlan internal order ascending range 1006 1199
+!
+transceiver qsfp default-mode 4x10G
+!
+service routing protocols model multi-agent
+!
+hostname dc1-leaf2a
+!
+vrf instance MGMT
+!
+management api http-commands
+ protocol https
+ no shutdown
+ !
+ vrf MGMT
+ no shutdown
+!
+interface Ethernet1/1
+ description P2P_dc1-spine1_Ethernet1/3
+ no shutdown
+ mtu 9214
+ no switchport
+ ip address 10.255.255.5/31
+!
+interface Ethernet2
+ description P2P_dc1-spine2_Ethernet2
+ no shutdown
+ mtu 9214
+ no switchport
+ ip address 10.255.255.7/31
+!
+interface Loopback0
+ description ROUTER_ID
+ no shutdown
+ ip address 10.255.1.2/32
+!
+interface Management1
+ description OOB_MANAGEMENT
+ no shutdown
+ vrf MGMT
+ ip address 172.16.1.102/24
+!
+ip routing
+no ip routing vrf MGMT
+!
+ip route vrf MGMT 0.0.0.0/0 172.16.1.1
+!
+router bgp 65102
+ router-id 10.255.1.2
+ no bgp default ipv4-unicast
+ maximum-paths 4
+ neighbor IPv4-UNDERLAY-PEERS peer group
+ neighbor IPv4-UNDERLAY-PEERS send-community
+ neighbor IPv4-UNDERLAY-PEERS maximum-routes 256000
+ neighbor 10.255.255.4 peer group IPv4-UNDERLAY-PEERS
+ neighbor 10.255.255.4 remote-as 65100
+ neighbor 10.255.255.4 description dc1-spine1_Ethernet1/3
+ neighbor 10.255.255.6 peer group IPv4-UNDERLAY-PEERS
+ neighbor 10.255.255.6 remote-as 65100
+ neighbor 10.255.255.6 description dc1-spine2_Ethernet2
+ redistribute connected
+ !
+ address-family ipv4
+ neighbor IPv4-UNDERLAY-PEERS activate
+!
+end
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/configs/dc1-spine1.cfg b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/configs/dc1-spine1.cfg
new file mode 100644
index 00000000000..81f60203847
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/configs/dc1-spine1.cfg
@@ -0,0 +1,72 @@
+!
+no enable password
+no aaa root
+!
+vlan internal order ascending range 1006 1199
+!
+transceiver qsfp default-mode 4x10G
+!
+service routing protocols model multi-agent
+!
+hostname dc1-spine1
+!
+spanning-tree mode none
+!
+vrf instance MGMT
+!
+management api http-commands
+ protocol https
+ no shutdown
+ !
+ vrf MGMT
+ no shutdown
+!
+interface Ethernet1
+ description P2P_dc1-leaf1a_Ethernet1
+ no shutdown
+ mtu 9214
+ no switchport
+ ip address 10.255.255.0/31
+!
+interface Ethernet1/3
+ description P2P_dc1-leaf2a_Ethernet1/1
+ no shutdown
+ mtu 9214
+ no switchport
+ ip address 10.255.255.4/31
+!
+interface Loopback0
+ description ROUTER_ID
+ no shutdown
+ ip address 10.255.0.1/32
+!
+interface Management1
+ description OOB_MANAGEMENT
+ no shutdown
+ vrf MGMT
+ ip address 172.16.1.11/24
+!
+ip routing
+no ip routing vrf MGMT
+!
+ip route vrf MGMT 0.0.0.0/0 172.16.1.1
+!
+router bgp 65100
+ router-id 10.255.0.1
+ no bgp default ipv4-unicast
+ maximum-paths 4
+ neighbor IPv4-UNDERLAY-PEERS peer group
+ neighbor IPv4-UNDERLAY-PEERS send-community
+ neighbor IPv4-UNDERLAY-PEERS maximum-routes 256000
+ neighbor 10.255.255.1 peer group IPv4-UNDERLAY-PEERS
+ neighbor 10.255.255.1 remote-as 65101
+ neighbor 10.255.255.1 description dc1-leaf1a_Ethernet1
+ neighbor 10.255.255.5 peer group IPv4-UNDERLAY-PEERS
+ neighbor 10.255.255.5 remote-as 65102
+ neighbor 10.255.255.5 description dc1-leaf2a_Ethernet1/1
+ redistribute connected
+ !
+ address-family ipv4
+ neighbor IPv4-UNDERLAY-PEERS activate
+!
+end
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/configs/dc1-spine2.cfg b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/configs/dc1-spine2.cfg
new file mode 100644
index 00000000000..223e0a38bca
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/configs/dc1-spine2.cfg
@@ -0,0 +1,72 @@
+!
+no enable password
+no aaa root
+!
+vlan internal order ascending range 1006 1199
+!
+transceiver qsfp default-mode 4x10G
+!
+service routing protocols model multi-agent
+!
+hostname dc1-spine2
+!
+spanning-tree mode none
+!
+vrf instance MGMT
+!
+management api http-commands
+ protocol https
+ no shutdown
+ !
+ vrf MGMT
+ no shutdown
+!
+interface Ethernet1/2
+ description P2P_dc1-leaf1a_Ethernet1/2
+ no shutdown
+ mtu 9214
+ no switchport
+ ip address 10.255.255.2/31
+!
+interface Ethernet2
+ description P2P_dc1-leaf2a_Ethernet2
+ no shutdown
+ mtu 9214
+ no switchport
+ ip address 10.255.255.6/31
+!
+interface Loopback0
+ description ROUTER_ID
+ no shutdown
+ ip address 10.255.0.2/32
+!
+interface Management1
+ description OOB_MANAGEMENT
+ no shutdown
+ vrf MGMT
+ ip address 172.16.1.12/24
+!
+ip routing
+no ip routing vrf MGMT
+!
+ip route vrf MGMT 0.0.0.0/0 172.16.1.1
+!
+router bgp 65100
+ router-id 10.255.0.2
+ no bgp default ipv4-unicast
+ maximum-paths 4
+ neighbor IPv4-UNDERLAY-PEERS peer group
+ neighbor IPv4-UNDERLAY-PEERS send-community
+ neighbor IPv4-UNDERLAY-PEERS maximum-routes 256000
+ neighbor 10.255.255.3 peer group IPv4-UNDERLAY-PEERS
+ neighbor 10.255.255.3 remote-as 65101
+ neighbor 10.255.255.3 description dc1-leaf1a_Ethernet1/2
+ neighbor 10.255.255.7 peer group IPv4-UNDERLAY-PEERS
+ neighbor 10.255.255.7 remote-as 65102
+ neighbor 10.255.255.7 description dc1-leaf2a_Ethernet2
+ redistribute connected
+ !
+ address-family ipv4
+ neighbor IPv4-UNDERLAY-PEERS activate
+!
+end
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/containerlab-dhcp-mgmt.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/containerlab-dhcp-mgmt.yml
new file mode 100644
index 00000000000..86c2e6daf35
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/containerlab-dhcp-mgmt.yml
@@ -0,0 +1,72 @@
+aaa_root:
+ disabled: true
+config_end: true
+enable_password:
+ disabled: true
+hostname: containerlab-dhcp-mgmt
+ip_routing: true
+loopback_interfaces:
+- name: Loopback0
+ description: ROUTER_ID
+ shutdown: false
+ ip_address: 10.255.0.1/32
+management_api_http:
+ enable_https: true
+ enable_vrfs:
+ - name: MGMT
+management_interfaces:
+- name: Management1
+ description: OOB_MANAGEMENT
+ shutdown: false
+ vrf: MGMT
+ ip_address: dhcp
+ type: oob
+metadata:
+ is_deployed: true
+ platform: cEOSLab
+ fabric_name: DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_DHCP
+ validate_hardware:
+ enabled: false
+ digital_twin:
+ environment: containerlab
+prefix_lists:
+- name: PL-LOOPBACKS-EVPN-OVERLAY
+ sequence_numbers:
+ - sequence: 10
+ action: permit 10.255.0.0/27 eq 32
+route_maps:
+- name: RM-CONN-2-BGP
+ sequence_numbers:
+ - sequence: 10
+ type: permit
+ match:
+ - ip address prefix-list PL-LOOPBACKS-EVPN-OVERLAY
+router_bfd:
+ multihop:
+ interval: 300
+ min_rx: 300
+ multiplier: 3
+router_bgp:
+ as: '65100'
+ router_id: 10.255.0.1
+ maximum_paths:
+ paths: 4
+ bgp:
+ default:
+ ipv4_unicast: false
+ redistribute:
+ connected:
+ enabled: true
+ route_map: RM-CONN-2-BGP
+service_routing_protocols_model: multi-agent
+spanning_tree:
+ mode: none
+transceiver_qsfp_default_mode_4x10: true
+vlan_internal_order:
+ allocation: ascending
+ range:
+ beginning: 1006
+ ending: 1199
+vrfs:
+- name: MGMT
+ ip_routing: false
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/containerlab-empty-mgmt.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/containerlab-empty-mgmt.yml
new file mode 100644
index 00000000000..7c01fab61c3
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/containerlab-empty-mgmt.yml
@@ -0,0 +1,65 @@
+aaa_root:
+ disabled: true
+config_end: true
+enable_password:
+ disabled: true
+hostname: containerlab-empty-mgmt
+ip_routing: true
+loopback_interfaces:
+- name: Loopback0
+ description: ROUTER_ID
+ shutdown: false
+ ip_address: 10.255.0.1/32
+management_api_http:
+ enable_https: true
+ enable_vrfs:
+ - name: MGMT
+metadata:
+ is_deployed: true
+ platform: cEOSLab
+ fabric_name: DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_EMPTY
+ validate_hardware:
+ enabled: false
+ digital_twin:
+ environment: containerlab
+prefix_lists:
+- name: PL-LOOPBACKS-EVPN-OVERLAY
+ sequence_numbers:
+ - sequence: 10
+ action: permit 10.255.0.0/27 eq 32
+route_maps:
+- name: RM-CONN-2-BGP
+ sequence_numbers:
+ - sequence: 10
+ type: permit
+ match:
+ - ip address prefix-list PL-LOOPBACKS-EVPN-OVERLAY
+router_bfd:
+ multihop:
+ interval: 300
+ min_rx: 300
+ multiplier: 3
+router_bgp:
+ as: '65100'
+ router_id: 10.255.0.1
+ maximum_paths:
+ paths: 4
+ bgp:
+ default:
+ ipv4_unicast: false
+ redistribute:
+ connected:
+ enabled: true
+ route_map: RM-CONN-2-BGP
+service_routing_protocols_model: multi-agent
+spanning_tree:
+ mode: none
+transceiver_qsfp_default_mode_4x10: true
+vlan_internal_order:
+ allocation: ascending
+ range:
+ beginning: 1006
+ ending: 1199
+vrfs:
+- name: MGMT
+ ip_routing: false
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/containerlab-null-mgmt.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/containerlab-null-mgmt.yml
new file mode 100644
index 00000000000..4ec6ca7a8d6
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/containerlab-null-mgmt.yml
@@ -0,0 +1,61 @@
+aaa_root:
+ disabled: true
+config_end: true
+enable_password:
+ disabled: true
+hostname: containerlab-null-mgmt
+ip_routing: true
+loopback_interfaces:
+- name: Loopback0
+ description: ROUTER_ID
+ shutdown: false
+ ip_address: 10.255.0.1/32
+metadata:
+ is_deployed: true
+ platform: cEOSLab
+ fabric_name: DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_NULL
+ validate_hardware:
+ enabled: false
+ digital_twin:
+ environment: containerlab
+prefix_lists:
+- name: PL-LOOPBACKS-EVPN-OVERLAY
+ sequence_numbers:
+ - sequence: 10
+ action: permit 10.255.0.0/27 eq 32
+route_maps:
+- name: RM-CONN-2-BGP
+ sequence_numbers:
+ - sequence: 10
+ type: permit
+ match:
+ - ip address prefix-list PL-LOOPBACKS-EVPN-OVERLAY
+router_bfd:
+ multihop:
+ interval: 300
+ min_rx: 300
+ multiplier: 3
+router_bgp:
+ as: '65100'
+ router_id: 10.255.0.1
+ maximum_paths:
+ paths: 4
+ bgp:
+ default:
+ ipv4_unicast: false
+ redistribute:
+ connected:
+ enabled: true
+ route_map: RM-CONN-2-BGP
+service_routing_protocols_model: multi-agent
+spanning_tree:
+ mode: none
+transceiver_qsfp_default_mode_4x10: true
+vlan_internal_order:
+ allocation: ascending
+ range:
+ beginning: 1006
+ ending: 1199
+vrfs:
+- name: MGMT
+ ip_routing: false
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/containerlab-subnet-a.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/containerlab-subnet-a.yml
new file mode 100644
index 00000000000..c01795563ea
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/containerlab-subnet-a.yml
@@ -0,0 +1,72 @@
+aaa_root:
+ disabled: true
+config_end: true
+enable_password:
+ disabled: true
+hostname: containerlab-subnet-a
+ip_routing: true
+loopback_interfaces:
+- name: Loopback0
+ description: ROUTER_ID
+ shutdown: false
+ ip_address: 10.255.0.1/32
+management_api_http:
+ enable_https: true
+ enable_vrfs:
+ - name: MGMT
+management_interfaces:
+- name: Management1
+ description: OOB_MANAGEMENT
+ shutdown: false
+ vrf: MGMT
+ ip_address: 172.16.1.201/24
+ type: oob
+metadata:
+ is_deployed: true
+ platform: cEOSLab
+ fabric_name: DIGITAL_TWIN_CONTAINERLAB_MULTIPLE_MGMT_SUBNETS
+ validate_hardware:
+ enabled: false
+ digital_twin:
+ environment: containerlab
+prefix_lists:
+- name: PL-LOOPBACKS-EVPN-OVERLAY
+ sequence_numbers:
+ - sequence: 10
+ action: permit 10.255.0.0/27 eq 32
+route_maps:
+- name: RM-CONN-2-BGP
+ sequence_numbers:
+ - sequence: 10
+ type: permit
+ match:
+ - ip address prefix-list PL-LOOPBACKS-EVPN-OVERLAY
+router_bfd:
+ multihop:
+ interval: 300
+ min_rx: 300
+ multiplier: 3
+router_bgp:
+ as: '65100'
+ router_id: 10.255.0.1
+ maximum_paths:
+ paths: 4
+ bgp:
+ default:
+ ipv4_unicast: false
+ redistribute:
+ connected:
+ enabled: true
+ route_map: RM-CONN-2-BGP
+service_routing_protocols_model: multi-agent
+spanning_tree:
+ mode: none
+transceiver_qsfp_default_mode_4x10: true
+vlan_internal_order:
+ allocation: ascending
+ range:
+ beginning: 1006
+ ending: 1199
+vrfs:
+- name: MGMT
+ ip_routing: false
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/containerlab-subnet-b.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/containerlab-subnet-b.yml
new file mode 100644
index 00000000000..e67b83616a5
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/containerlab-subnet-b.yml
@@ -0,0 +1,72 @@
+aaa_root:
+ disabled: true
+config_end: true
+enable_password:
+ disabled: true
+hostname: containerlab-subnet-b
+ip_routing: true
+loopback_interfaces:
+- name: Loopback0
+ description: ROUTER_ID
+ shutdown: false
+ ip_address: 10.255.0.2/32
+management_api_http:
+ enable_https: true
+ enable_vrfs:
+ - name: MGMT
+management_interfaces:
+- name: Management1
+ description: OOB_MANAGEMENT
+ shutdown: false
+ vrf: MGMT
+ ip_address: 172.16.2.202/24
+ type: oob
+metadata:
+ is_deployed: true
+ platform: cEOSLab
+ fabric_name: DIGITAL_TWIN_CONTAINERLAB_MULTIPLE_MGMT_SUBNETS
+ validate_hardware:
+ enabled: false
+ digital_twin:
+ environment: containerlab
+prefix_lists:
+- name: PL-LOOPBACKS-EVPN-OVERLAY
+ sequence_numbers:
+ - sequence: 10
+ action: permit 10.255.0.0/27 eq 32
+route_maps:
+- name: RM-CONN-2-BGP
+ sequence_numbers:
+ - sequence: 10
+ type: permit
+ match:
+ - ip address prefix-list PL-LOOPBACKS-EVPN-OVERLAY
+router_bfd:
+ multihop:
+ interval: 300
+ min_rx: 300
+ multiplier: 3
+router_bgp:
+ as: '65100'
+ router_id: 10.255.0.2
+ maximum_paths:
+ paths: 4
+ bgp:
+ default:
+ ipv4_unicast: false
+ redistribute:
+ connected:
+ enabled: true
+ route_map: RM-CONN-2-BGP
+service_routing_protocols_model: multi-agent
+spanning_tree:
+ mode: none
+transceiver_qsfp_default_mode_4x10: true
+vlan_internal_order:
+ allocation: ascending
+ range:
+ beginning: 1006
+ ending: 1199
+vrfs:
+- name: MGMT
+ ip_routing: false
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/dc1-leaf1a.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/dc1-leaf1a.yml
new file mode 100644
index 00000000000..8a0dd5583df
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/dc1-leaf1a.yml
@@ -0,0 +1,105 @@
+aaa_root:
+ disabled: true
+config_end: true
+enable_password:
+ disabled: true
+ethernet_interfaces:
+- name: Ethernet1
+ description: P2P_dc1-spine1_Ethernet1
+ shutdown: false
+ mtu: 9214
+ ip_address: 10.255.255.1/31
+ metadata:
+ peer: dc1-spine1
+ peer_interface: Ethernet1
+ peer_type: spine
+ switchport:
+ enabled: false
+- name: Ethernet1/2
+ description: P2P_dc1-spine2_Ethernet1/2
+ shutdown: false
+ mtu: 9214
+ ip_address: 10.255.255.3/31
+ metadata:
+ peer: dc1-spine2
+ peer_interface: Ethernet1/2
+ peer_type: spine
+ switchport:
+ enabled: false
+hostname: dc1-leaf1a
+ip_igmp_snooping:
+ globally_enabled: true
+ip_routing: true
+loopback_interfaces:
+- name: Loopback0
+ description: ROUTER_ID
+ shutdown: false
+ ip_address: 10.255.1.1/32
+management_api_http:
+ enable_https: true
+ enable_vrfs:
+ - name: MGMT
+management_interfaces:
+- name: Management1
+ description: OOB_MANAGEMENT
+ shutdown: false
+ vrf: MGMT
+ ip_address: 172.16.1.101/24
+ type: oob
+ gateway: 172.16.1.1
+metadata:
+ is_deployed: true
+ platform: cEOSLab
+ fabric_name: DIGITAL_TWIN_CONTAINERLAB
+ validate_hardware:
+ enabled: false
+ digital_twin:
+ environment: containerlab
+router_bgp:
+ as: '65101'
+ router_id: 10.255.1.1
+ maximum_paths:
+ paths: 4
+ bgp:
+ default:
+ ipv4_unicast: false
+ peer_groups:
+ - name: IPv4-UNDERLAY-PEERS
+ metadata:
+ type: ipv4
+ send_community: all
+ maximum_routes: 256000
+ neighbors:
+ - ip_address: 10.255.255.0
+ peer_group: IPv4-UNDERLAY-PEERS
+ remote_as: '65100'
+ metadata:
+ peer: dc1-spine1
+ description: dc1-spine1_Ethernet1
+ - ip_address: 10.255.255.2
+ peer_group: IPv4-UNDERLAY-PEERS
+ remote_as: '65100'
+ metadata:
+ peer: dc1-spine2
+ description: dc1-spine2_Ethernet1/2
+ redistribute:
+ connected:
+ enabled: true
+ address_family_ipv4:
+ peer_groups:
+ - name: IPv4-UNDERLAY-PEERS
+ activate: true
+service_routing_protocols_model: multi-agent
+static_routes:
+- vrf: MGMT
+ prefix: 0.0.0.0/0
+ next_hop: 172.16.1.1
+transceiver_qsfp_default_mode_4x10: true
+vlan_internal_order:
+ allocation: ascending
+ range:
+ beginning: 1006
+ ending: 1199
+vrfs:
+- name: MGMT
+ ip_routing: false
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/dc1-leaf2a.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/dc1-leaf2a.yml
new file mode 100644
index 00000000000..63f1dbbc7ed
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/dc1-leaf2a.yml
@@ -0,0 +1,105 @@
+aaa_root:
+ disabled: true
+config_end: true
+enable_password:
+ disabled: true
+ethernet_interfaces:
+- name: Ethernet1/1
+ description: P2P_dc1-spine1_Ethernet1/3
+ shutdown: false
+ mtu: 9214
+ ip_address: 10.255.255.5/31
+ metadata:
+ peer: dc1-spine1
+ peer_interface: Ethernet1/3
+ peer_type: spine
+ switchport:
+ enabled: false
+- name: Ethernet2
+ description: P2P_dc1-spine2_Ethernet2
+ shutdown: false
+ mtu: 9214
+ ip_address: 10.255.255.7/31
+ metadata:
+ peer: dc1-spine2
+ peer_interface: Ethernet2
+ peer_type: spine
+ switchport:
+ enabled: false
+hostname: dc1-leaf2a
+ip_igmp_snooping:
+ globally_enabled: true
+ip_routing: true
+loopback_interfaces:
+- name: Loopback0
+ description: ROUTER_ID
+ shutdown: false
+ ip_address: 10.255.1.2/32
+management_api_http:
+ enable_https: true
+ enable_vrfs:
+ - name: MGMT
+management_interfaces:
+- name: Management1
+ description: OOB_MANAGEMENT
+ shutdown: false
+ vrf: MGMT
+ ip_address: 172.16.1.102/24
+ type: oob
+ gateway: 172.16.1.1
+metadata:
+ is_deployed: true
+ platform: cEOSLab
+ fabric_name: DIGITAL_TWIN_CONTAINERLAB
+ validate_hardware:
+ enabled: false
+ digital_twin:
+ environment: containerlab
+router_bgp:
+ as: '65102'
+ router_id: 10.255.1.2
+ maximum_paths:
+ paths: 4
+ bgp:
+ default:
+ ipv4_unicast: false
+ peer_groups:
+ - name: IPv4-UNDERLAY-PEERS
+ metadata:
+ type: ipv4
+ send_community: all
+ maximum_routes: 256000
+ neighbors:
+ - ip_address: 10.255.255.4
+ peer_group: IPv4-UNDERLAY-PEERS
+ remote_as: '65100'
+ metadata:
+ peer: dc1-spine1
+ description: dc1-spine1_Ethernet1/3
+ - ip_address: 10.255.255.6
+ peer_group: IPv4-UNDERLAY-PEERS
+ remote_as: '65100'
+ metadata:
+ peer: dc1-spine2
+ description: dc1-spine2_Ethernet2
+ redistribute:
+ connected:
+ enabled: true
+ address_family_ipv4:
+ peer_groups:
+ - name: IPv4-UNDERLAY-PEERS
+ activate: true
+service_routing_protocols_model: multi-agent
+static_routes:
+- vrf: MGMT
+ prefix: 0.0.0.0/0
+ next_hop: 172.16.1.1
+transceiver_qsfp_default_mode_4x10: true
+vlan_internal_order:
+ allocation: ascending
+ range:
+ beginning: 1006
+ ending: 1199
+vrfs:
+- name: MGMT
+ ip_routing: false
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/dc1-spine1.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/dc1-spine1.yml
new file mode 100644
index 00000000000..4635492dd9d
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/dc1-spine1.yml
@@ -0,0 +1,105 @@
+aaa_root:
+ disabled: true
+config_end: true
+enable_password:
+ disabled: true
+ethernet_interfaces:
+- name: Ethernet1
+ description: P2P_dc1-leaf1a_Ethernet1
+ shutdown: false
+ mtu: 9214
+ ip_address: 10.255.255.0/31
+ metadata:
+ peer: dc1-leaf1a
+ peer_interface: Ethernet1
+ peer_type: l3leaf
+ switchport:
+ enabled: false
+- name: Ethernet1/3
+ description: P2P_dc1-leaf2a_Ethernet1/1
+ shutdown: false
+ mtu: 9214
+ ip_address: 10.255.255.4/31
+ metadata:
+ peer: dc1-leaf2a
+ peer_interface: Ethernet1/1
+ peer_type: l3leaf
+ switchport:
+ enabled: false
+hostname: dc1-spine1
+ip_routing: true
+loopback_interfaces:
+- name: Loopback0
+ description: ROUTER_ID
+ shutdown: false
+ ip_address: 10.255.0.1/32
+management_api_http:
+ enable_https: true
+ enable_vrfs:
+ - name: MGMT
+management_interfaces:
+- name: Management1
+ description: OOB_MANAGEMENT
+ shutdown: false
+ vrf: MGMT
+ ip_address: 172.16.1.11/24
+ type: oob
+ gateway: 172.16.1.1
+metadata:
+ is_deployed: true
+ platform: cEOSLab
+ fabric_name: DIGITAL_TWIN_CONTAINERLAB
+ validate_hardware:
+ enabled: false
+ digital_twin:
+ environment: containerlab
+router_bgp:
+ as: '65100'
+ router_id: 10.255.0.1
+ maximum_paths:
+ paths: 4
+ bgp:
+ default:
+ ipv4_unicast: false
+ peer_groups:
+ - name: IPv4-UNDERLAY-PEERS
+ metadata:
+ type: ipv4
+ send_community: all
+ maximum_routes: 256000
+ neighbors:
+ - ip_address: 10.255.255.1
+ peer_group: IPv4-UNDERLAY-PEERS
+ remote_as: '65101'
+ metadata:
+ peer: dc1-leaf1a
+ description: dc1-leaf1a_Ethernet1
+ - ip_address: 10.255.255.5
+ peer_group: IPv4-UNDERLAY-PEERS
+ remote_as: '65102'
+ metadata:
+ peer: dc1-leaf2a
+ description: dc1-leaf2a_Ethernet1/1
+ redistribute:
+ connected:
+ enabled: true
+ address_family_ipv4:
+ peer_groups:
+ - name: IPv4-UNDERLAY-PEERS
+ activate: true
+service_routing_protocols_model: multi-agent
+spanning_tree:
+ mode: none
+static_routes:
+- vrf: MGMT
+ prefix: 0.0.0.0/0
+ next_hop: 172.16.1.1
+transceiver_qsfp_default_mode_4x10: true
+vlan_internal_order:
+ allocation: ascending
+ range:
+ beginning: 1006
+ ending: 1199
+vrfs:
+- name: MGMT
+ ip_routing: false
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/dc1-spine2.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/dc1-spine2.yml
new file mode 100644
index 00000000000..af673284b75
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/intended/structured_configs/dc1-spine2.yml
@@ -0,0 +1,105 @@
+aaa_root:
+ disabled: true
+config_end: true
+enable_password:
+ disabled: true
+ethernet_interfaces:
+- name: Ethernet1/2
+ description: P2P_dc1-leaf1a_Ethernet1/2
+ shutdown: false
+ mtu: 9214
+ ip_address: 10.255.255.2/31
+ metadata:
+ peer: dc1-leaf1a
+ peer_interface: Ethernet1/2
+ peer_type: l3leaf
+ switchport:
+ enabled: false
+- name: Ethernet2
+ description: P2P_dc1-leaf2a_Ethernet2
+ shutdown: false
+ mtu: 9214
+ ip_address: 10.255.255.6/31
+ metadata:
+ peer: dc1-leaf2a
+ peer_interface: Ethernet2
+ peer_type: l3leaf
+ switchport:
+ enabled: false
+hostname: dc1-spine2
+ip_routing: true
+loopback_interfaces:
+- name: Loopback0
+ description: ROUTER_ID
+ shutdown: false
+ ip_address: 10.255.0.2/32
+management_api_http:
+ enable_https: true
+ enable_vrfs:
+ - name: MGMT
+management_interfaces:
+- name: Management1
+ description: OOB_MANAGEMENT
+ shutdown: false
+ vrf: MGMT
+ ip_address: 172.16.1.12/24
+ type: oob
+ gateway: 172.16.1.1
+metadata:
+ is_deployed: true
+ platform: cEOSLab
+ fabric_name: DIGITAL_TWIN_CONTAINERLAB
+ validate_hardware:
+ enabled: false
+ digital_twin:
+ environment: containerlab
+router_bgp:
+ as: '65100'
+ router_id: 10.255.0.2
+ maximum_paths:
+ paths: 4
+ bgp:
+ default:
+ ipv4_unicast: false
+ peer_groups:
+ - name: IPv4-UNDERLAY-PEERS
+ metadata:
+ type: ipv4
+ send_community: all
+ maximum_routes: 256000
+ neighbors:
+ - ip_address: 10.255.255.3
+ peer_group: IPv4-UNDERLAY-PEERS
+ remote_as: '65101'
+ metadata:
+ peer: dc1-leaf1a
+ description: dc1-leaf1a_Ethernet1/2
+ - ip_address: 10.255.255.7
+ peer_group: IPv4-UNDERLAY-PEERS
+ remote_as: '65102'
+ metadata:
+ peer: dc1-leaf2a
+ description: dc1-leaf2a_Ethernet2
+ redistribute:
+ connected:
+ enabled: true
+ address_family_ipv4:
+ peer_groups:
+ - name: IPv4-UNDERLAY-PEERS
+ activate: true
+service_routing_protocols_model: multi-agent
+spanning_tree:
+ mode: none
+static_routes:
+- vrf: MGMT
+ prefix: 0.0.0.0/0
+ next_hop: 172.16.1.1
+transceiver_qsfp_default_mode_4x10: true
+vlan_internal_order:
+ allocation: ascending
+ range:
+ beginning: 1006
+ ending: 1199
+vrfs:
+- name: MGMT
+ ip_routing: false
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/interface_mapping.json b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/interface_mapping.json
new file mode 100644
index 00000000000..0afd6febdaa
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/digital_twin/interface_mapping.json
@@ -0,0 +1,12 @@
+{
+ "ManagementIntf": {
+ "eth0": "Management1"
+ },
+ "EthernetIntf": {
+ "eth1": "Ethernet1",
+ "eth1_1": "Ethernet1/1",
+ "eth1_2": "Ethernet1/2",
+ "eth1_3": "Ethernet1/3",
+ "eth2": "Ethernet2"
+ }
+}
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/group_vars/DC1.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/group_vars/DC1.yml
new file mode 100644
index 00000000000..3acc7b8dff7
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/group_vars/DC1.yml
@@ -0,0 +1,2 @@
+---
+mgmt_gateway: 172.16.1.1
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/group_vars/DC1_L3_LEAVES.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/group_vars/DC1_L3_LEAVES.yml
new file mode 100644
index 00000000000..16c08b27222
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/group_vars/DC1_L3_LEAVES.yml
@@ -0,0 +1,23 @@
+---
+type: l3leaf
+
+l3leaf:
+ defaults:
+ platform: cEOSLab
+ loopback_ipv4_pool: 10.255.1.0/27
+ vtep_loopback_ipv4_pool: 10.255.2.0/27
+ uplink_switches: [dc1-spine1, dc1-spine2]
+ uplink_ipv4_pool: 10.255.255.0/26
+ nodes:
+ - name: dc1-leaf1a
+ id: 1
+ bgp_as: 65101
+ mgmt_ip: 172.16.1.101/24
+ uplink_interfaces: [Ethernet1, Ethernet1/2]
+ uplink_switch_interfaces: [Ethernet1, Ethernet1/2]
+ - name: dc1-leaf2a
+ id: 2
+ bgp_as: 65102
+ mgmt_ip: 172.16.1.102/24
+ uplink_interfaces: [Ethernet1/1, Ethernet2]
+ uplink_switch_interfaces: [Ethernet1/3, Ethernet2]
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/group_vars/DC1_SPINES.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/group_vars/DC1_SPINES.yml
new file mode 100644
index 00000000000..cb0f31da821
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/group_vars/DC1_SPINES.yml
@@ -0,0 +1,15 @@
+---
+type: spine
+
+spine:
+ defaults:
+ platform: cEOSLab
+ loopback_ipv4_pool: 10.255.0.0/27
+ bgp_as: 65100
+ nodes:
+ - name: dc1-spine1
+ id: 1
+ mgmt_ip: 172.16.1.11/24
+ - name: dc1-spine2
+ id: 2
+ mgmt_ip: 172.16.1.12/24
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/group_vars/DIGITAL_TWIN_CONTAINERLAB.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/group_vars/DIGITAL_TWIN_CONTAINERLAB.yml
new file mode 100644
index 00000000000..b76e5ae5b9b
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/group_vars/DIGITAL_TWIN_CONTAINERLAB.yml
@@ -0,0 +1,14 @@
+---
+fabric_name: DIGITAL_TWIN_CONTAINERLAB
+
+management_eapi:
+ enabled: true
+
+eos_designs_documentation:
+ topology_csv: true
+ p2p_links_csv: true
+
+digital_twin:
+ environment: containerlab
+ # TODO: remove `fabric` once #5649 is ready
+ fabric: {}
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/group_vars/DIGITAL_TWIN_CONTAINERLAB_FABRIC.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/group_vars/DIGITAL_TWIN_CONTAINERLAB_FABRIC.yml
new file mode 100644
index 00000000000..f77228f6565
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/group_vars/DIGITAL_TWIN_CONTAINERLAB_FABRIC.yml
@@ -0,0 +1,11 @@
+---
+underlay_routing_protocol: ebgp
+overlay_routing_protocol: none
+
+default_interfaces:
+ - types: [spine]
+ platforms: [default]
+ downlink_interfaces: [Ethernet1-2]
+ - types: [l3leaf]
+ platforms: [default]
+ uplink_interfaces: [Ethernet1-2]
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/group_vars/all.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/group_vars/all.yml
new file mode 100644
index 00000000000..e7e19d60245
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/group_vars/all.yml
@@ -0,0 +1,2 @@
+---
+root_dir: '{{ playbook_dir }}'
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/host_vars/all.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/host_vars/all.yml
new file mode 100644
index 00000000000..e7e19d60245
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/host_vars/all.yml
@@ -0,0 +1,2 @@
+---
+root_dir: '{{ playbook_dir }}'
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/hosts.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/hosts.yml
new file mode 100644
index 00000000000..bd822221aff
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/inventory/hosts.yml
@@ -0,0 +1,21 @@
+---
+all:
+ children:
+ DIGITAL_TWIN_CONTAINERLAB:
+ children:
+ DIGITAL_TWIN_CONTAINERLAB_FABRIC:
+ children:
+ DC1:
+ children:
+ DC1_SPINES:
+ hosts:
+ dc1-spine1:
+ ansible_host: 172.16.1.11
+ dc1-spine2:
+ ansible_host: 172.16.1.12
+ DC1_L3_LEAVES:
+ hosts:
+ dc1-leaf1a:
+ ansible_host: 172.16.1.101
+ dc1-leaf2a:
+ ansible_host: 172.16.1.102
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/molecule.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/molecule.yml
new file mode 100644
index 00000000000..0abb4f0654e
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/molecule.yml
@@ -0,0 +1,30 @@
+---
+ansible:
+ executor:
+ backend: ansible-playbook
+ args:
+ ansible_playbook:
+ - --inventory=inventory/
+ env:
+ ANSIBLE_FORCE_COLOR: true
+ cfg:
+ defaults:
+ gathering: explicit
+scenario:
+ converge_sequence:
+ - syntax
+ - converge
+ - verify
+ test_sequence:
+ - syntax
+ - create
+ - converge
+ - idempotence
+ - verify
+ cleanup_sequence:
+ - destroy
+dependency:
+ name: galaxy
+ enabled: false
+verifier:
+ name: ansible
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/verify.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/verify.yml
new file mode 100644
index 00000000000..f5a2cbc895c
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab/verify.yml
@@ -0,0 +1,26 @@
+---
+- name: Verify
+ hosts: DIGITAL_TWIN_CONTAINERLAB
+ gather_facts: false
+ connection: local
+ tasks:
+ - name: Assert Containerlab topology file exists
+ delegate_to: 127.0.0.1
+ ansible.builtin.stat:
+ path: "{{ root_dir }}/digital_twin/{{ fabric_name }}-topology.clab.yml"
+ register: containerlab_topology_file
+ run_once: true
+
+ - name: Assert Containerlab interface mapping file exists
+ delegate_to: 127.0.0.1
+ ansible.builtin.stat:
+ path: "{{ root_dir }}/digital_twin/interface_mapping.json"
+ register: containerlab_interface_mapping_file
+ run_once: true
+
+ - name: Validate generated Containerlab topology artifact
+ ansible.builtin.assert:
+ that:
+ - "containerlab_topology_file.stat.exists"
+ - "containerlab_interface_mapping_file.stat.exists"
+ run_once: true
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/converge.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/converge.yml
new file mode 100644
index 00000000000..1a0d731a392
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/converge.yml
@@ -0,0 +1,75 @@
+---
+- name: Converge expected failure for Containerlab Digital Twin without static management IP
+ hosts: DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_DHCP
+ gather_facts: false
+ connection: local
+ vars: &unsupported_mgmt_ip_vars
+ output_dir_name: "digital_twin/intended"
+ documentation_dir_name: "digital_twin/documentation"
+ avd_digital_twin_mode: true
+ avd_ignore_requirements: true
+ eos_designs_documentation:
+ enable: false
+ topology_csv: false
+ p2p_links_csv: false
+ tasks: &unsupported_mgmt_ip_tasks
+ - name: Run expected failure scenario
+ block:
+ - name: Generate intended variables in Containerlab Digital Twin mode
+ delegate_to: 127.0.0.1
+ ansible.builtin.import_role:
+ name: arista.avd.eos_designs
+
+ - name: Fail if unsupported management IP did not fail
+ ansible.builtin.fail:
+ msg: "Expected Containerlab Digital Twin generation to fail for unsupported management IP."
+
+ rescue:
+ - name: Assert Containerlab Digital Twin failed with the expected error message
+ run_once: true
+ ansible.builtin.assert:
+ that:
+ - ansible_failed_result is defined
+ - expected_error_message in ansible_failed_result.msg
+
+- name: Converge expected failure for Containerlab Digital Twin with empty management IP
+ hosts: DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_EMPTY
+ gather_facts: false
+ connection: local
+ vars: *unsupported_mgmt_ip_vars
+ tasks: *unsupported_mgmt_ip_tasks
+
+- name: Converge expected failure for Containerlab Digital Twin with null management IP
+ hosts: DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_NULL
+ gather_facts: false
+ connection: local
+ vars: *unsupported_mgmt_ip_vars
+ tasks: *unsupported_mgmt_ip_tasks
+
+- name: Converge expected failure for Containerlab Digital Twin with multiple management subnets
+ hosts: DIGITAL_TWIN_CONTAINERLAB_MULTIPLE_MGMT_SUBNETS
+ gather_facts: false
+ connection: local
+ vars: *unsupported_mgmt_ip_vars
+ tasks:
+ - name: Run expected failure scenario
+ block:
+ - name: Generate intended variables in Containerlab Digital Twin mode
+ delegate_to: 127.0.0.1
+ ansible.builtin.import_role:
+ name: arista.avd.eos_designs
+
+ - name: Fail if multiple management subnets did not fail
+ ansible.builtin.fail:
+ msg: "Expected Containerlab Digital Twin generation to fail for multiple management subnets."
+
+ rescue:
+ - name: Assert Containerlab Digital Twin failed with the expected multiple subnet error message
+ run_once: true
+ ansible.builtin.assert:
+ that:
+ - ansible_failed_result is defined
+ - expected_error_message in ansible_failed_result.msg
+ - "'Found multiple subnets:' in ansible_failed_result.msg"
+ - expected_mgmt_subnets[0] in ansible_failed_result.msg
+ - expected_mgmt_subnets[1] in ansible_failed_result.msg
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/digital_twin/intended/structured_configs/containerlab-dhcp-mgmt.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/digital_twin/intended/structured_configs/containerlab-dhcp-mgmt.yml
new file mode 100644
index 00000000000..86c2e6daf35
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/digital_twin/intended/structured_configs/containerlab-dhcp-mgmt.yml
@@ -0,0 +1,72 @@
+aaa_root:
+ disabled: true
+config_end: true
+enable_password:
+ disabled: true
+hostname: containerlab-dhcp-mgmt
+ip_routing: true
+loopback_interfaces:
+- name: Loopback0
+ description: ROUTER_ID
+ shutdown: false
+ ip_address: 10.255.0.1/32
+management_api_http:
+ enable_https: true
+ enable_vrfs:
+ - name: MGMT
+management_interfaces:
+- name: Management1
+ description: OOB_MANAGEMENT
+ shutdown: false
+ vrf: MGMT
+ ip_address: dhcp
+ type: oob
+metadata:
+ is_deployed: true
+ platform: cEOSLab
+ fabric_name: DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_DHCP
+ validate_hardware:
+ enabled: false
+ digital_twin:
+ environment: containerlab
+prefix_lists:
+- name: PL-LOOPBACKS-EVPN-OVERLAY
+ sequence_numbers:
+ - sequence: 10
+ action: permit 10.255.0.0/27 eq 32
+route_maps:
+- name: RM-CONN-2-BGP
+ sequence_numbers:
+ - sequence: 10
+ type: permit
+ match:
+ - ip address prefix-list PL-LOOPBACKS-EVPN-OVERLAY
+router_bfd:
+ multihop:
+ interval: 300
+ min_rx: 300
+ multiplier: 3
+router_bgp:
+ as: '65100'
+ router_id: 10.255.0.1
+ maximum_paths:
+ paths: 4
+ bgp:
+ default:
+ ipv4_unicast: false
+ redistribute:
+ connected:
+ enabled: true
+ route_map: RM-CONN-2-BGP
+service_routing_protocols_model: multi-agent
+spanning_tree:
+ mode: none
+transceiver_qsfp_default_mode_4x10: true
+vlan_internal_order:
+ allocation: ascending
+ range:
+ beginning: 1006
+ ending: 1199
+vrfs:
+- name: MGMT
+ ip_routing: false
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/digital_twin/intended/structured_configs/containerlab-empty-mgmt.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/digital_twin/intended/structured_configs/containerlab-empty-mgmt.yml
new file mode 100644
index 00000000000..7c01fab61c3
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/digital_twin/intended/structured_configs/containerlab-empty-mgmt.yml
@@ -0,0 +1,65 @@
+aaa_root:
+ disabled: true
+config_end: true
+enable_password:
+ disabled: true
+hostname: containerlab-empty-mgmt
+ip_routing: true
+loopback_interfaces:
+- name: Loopback0
+ description: ROUTER_ID
+ shutdown: false
+ ip_address: 10.255.0.1/32
+management_api_http:
+ enable_https: true
+ enable_vrfs:
+ - name: MGMT
+metadata:
+ is_deployed: true
+ platform: cEOSLab
+ fabric_name: DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_EMPTY
+ validate_hardware:
+ enabled: false
+ digital_twin:
+ environment: containerlab
+prefix_lists:
+- name: PL-LOOPBACKS-EVPN-OVERLAY
+ sequence_numbers:
+ - sequence: 10
+ action: permit 10.255.0.0/27 eq 32
+route_maps:
+- name: RM-CONN-2-BGP
+ sequence_numbers:
+ - sequence: 10
+ type: permit
+ match:
+ - ip address prefix-list PL-LOOPBACKS-EVPN-OVERLAY
+router_bfd:
+ multihop:
+ interval: 300
+ min_rx: 300
+ multiplier: 3
+router_bgp:
+ as: '65100'
+ router_id: 10.255.0.1
+ maximum_paths:
+ paths: 4
+ bgp:
+ default:
+ ipv4_unicast: false
+ redistribute:
+ connected:
+ enabled: true
+ route_map: RM-CONN-2-BGP
+service_routing_protocols_model: multi-agent
+spanning_tree:
+ mode: none
+transceiver_qsfp_default_mode_4x10: true
+vlan_internal_order:
+ allocation: ascending
+ range:
+ beginning: 1006
+ ending: 1199
+vrfs:
+- name: MGMT
+ ip_routing: false
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/digital_twin/intended/structured_configs/containerlab-null-mgmt.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/digital_twin/intended/structured_configs/containerlab-null-mgmt.yml
new file mode 100644
index 00000000000..4ec6ca7a8d6
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/digital_twin/intended/structured_configs/containerlab-null-mgmt.yml
@@ -0,0 +1,61 @@
+aaa_root:
+ disabled: true
+config_end: true
+enable_password:
+ disabled: true
+hostname: containerlab-null-mgmt
+ip_routing: true
+loopback_interfaces:
+- name: Loopback0
+ description: ROUTER_ID
+ shutdown: false
+ ip_address: 10.255.0.1/32
+metadata:
+ is_deployed: true
+ platform: cEOSLab
+ fabric_name: DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_NULL
+ validate_hardware:
+ enabled: false
+ digital_twin:
+ environment: containerlab
+prefix_lists:
+- name: PL-LOOPBACKS-EVPN-OVERLAY
+ sequence_numbers:
+ - sequence: 10
+ action: permit 10.255.0.0/27 eq 32
+route_maps:
+- name: RM-CONN-2-BGP
+ sequence_numbers:
+ - sequence: 10
+ type: permit
+ match:
+ - ip address prefix-list PL-LOOPBACKS-EVPN-OVERLAY
+router_bfd:
+ multihop:
+ interval: 300
+ min_rx: 300
+ multiplier: 3
+router_bgp:
+ as: '65100'
+ router_id: 10.255.0.1
+ maximum_paths:
+ paths: 4
+ bgp:
+ default:
+ ipv4_unicast: false
+ redistribute:
+ connected:
+ enabled: true
+ route_map: RM-CONN-2-BGP
+service_routing_protocols_model: multi-agent
+spanning_tree:
+ mode: none
+transceiver_qsfp_default_mode_4x10: true
+vlan_internal_order:
+ allocation: ascending
+ range:
+ beginning: 1006
+ ending: 1199
+vrfs:
+- name: MGMT
+ ip_routing: false
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/digital_twin/intended/structured_configs/containerlab-subnet-a.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/digital_twin/intended/structured_configs/containerlab-subnet-a.yml
new file mode 100644
index 00000000000..c01795563ea
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/digital_twin/intended/structured_configs/containerlab-subnet-a.yml
@@ -0,0 +1,72 @@
+aaa_root:
+ disabled: true
+config_end: true
+enable_password:
+ disabled: true
+hostname: containerlab-subnet-a
+ip_routing: true
+loopback_interfaces:
+- name: Loopback0
+ description: ROUTER_ID
+ shutdown: false
+ ip_address: 10.255.0.1/32
+management_api_http:
+ enable_https: true
+ enable_vrfs:
+ - name: MGMT
+management_interfaces:
+- name: Management1
+ description: OOB_MANAGEMENT
+ shutdown: false
+ vrf: MGMT
+ ip_address: 172.16.1.201/24
+ type: oob
+metadata:
+ is_deployed: true
+ platform: cEOSLab
+ fabric_name: DIGITAL_TWIN_CONTAINERLAB_MULTIPLE_MGMT_SUBNETS
+ validate_hardware:
+ enabled: false
+ digital_twin:
+ environment: containerlab
+prefix_lists:
+- name: PL-LOOPBACKS-EVPN-OVERLAY
+ sequence_numbers:
+ - sequence: 10
+ action: permit 10.255.0.0/27 eq 32
+route_maps:
+- name: RM-CONN-2-BGP
+ sequence_numbers:
+ - sequence: 10
+ type: permit
+ match:
+ - ip address prefix-list PL-LOOPBACKS-EVPN-OVERLAY
+router_bfd:
+ multihop:
+ interval: 300
+ min_rx: 300
+ multiplier: 3
+router_bgp:
+ as: '65100'
+ router_id: 10.255.0.1
+ maximum_paths:
+ paths: 4
+ bgp:
+ default:
+ ipv4_unicast: false
+ redistribute:
+ connected:
+ enabled: true
+ route_map: RM-CONN-2-BGP
+service_routing_protocols_model: multi-agent
+spanning_tree:
+ mode: none
+transceiver_qsfp_default_mode_4x10: true
+vlan_internal_order:
+ allocation: ascending
+ range:
+ beginning: 1006
+ ending: 1199
+vrfs:
+- name: MGMT
+ ip_routing: false
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/digital_twin/intended/structured_configs/containerlab-subnet-b.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/digital_twin/intended/structured_configs/containerlab-subnet-b.yml
new file mode 100644
index 00000000000..e67b83616a5
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/digital_twin/intended/structured_configs/containerlab-subnet-b.yml
@@ -0,0 +1,72 @@
+aaa_root:
+ disabled: true
+config_end: true
+enable_password:
+ disabled: true
+hostname: containerlab-subnet-b
+ip_routing: true
+loopback_interfaces:
+- name: Loopback0
+ description: ROUTER_ID
+ shutdown: false
+ ip_address: 10.255.0.2/32
+management_api_http:
+ enable_https: true
+ enable_vrfs:
+ - name: MGMT
+management_interfaces:
+- name: Management1
+ description: OOB_MANAGEMENT
+ shutdown: false
+ vrf: MGMT
+ ip_address: 172.16.2.202/24
+ type: oob
+metadata:
+ is_deployed: true
+ platform: cEOSLab
+ fabric_name: DIGITAL_TWIN_CONTAINERLAB_MULTIPLE_MGMT_SUBNETS
+ validate_hardware:
+ enabled: false
+ digital_twin:
+ environment: containerlab
+prefix_lists:
+- name: PL-LOOPBACKS-EVPN-OVERLAY
+ sequence_numbers:
+ - sequence: 10
+ action: permit 10.255.0.0/27 eq 32
+route_maps:
+- name: RM-CONN-2-BGP
+ sequence_numbers:
+ - sequence: 10
+ type: permit
+ match:
+ - ip address prefix-list PL-LOOPBACKS-EVPN-OVERLAY
+router_bfd:
+ multihop:
+ interval: 300
+ min_rx: 300
+ multiplier: 3
+router_bgp:
+ as: '65100'
+ router_id: 10.255.0.2
+ maximum_paths:
+ paths: 4
+ bgp:
+ default:
+ ipv4_unicast: false
+ redistribute:
+ connected:
+ enabled: true
+ route_map: RM-CONN-2-BGP
+service_routing_protocols_model: multi-agent
+spanning_tree:
+ mode: none
+transceiver_qsfp_default_mode_4x10: true
+vlan_internal_order:
+ allocation: ascending
+ range:
+ beginning: 1006
+ ending: 1199
+vrfs:
+- name: MGMT
+ ip_routing: false
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/inventory/group_vars/DIGITAL_TWIN_CONTAINERLAB_MULTIPLE_MGMT_SUBNETS.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/inventory/group_vars/DIGITAL_TWIN_CONTAINERLAB_MULTIPLE_MGMT_SUBNETS.yml
new file mode 100644
index 00000000000..c0296792aef
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/inventory/group_vars/DIGITAL_TWIN_CONTAINERLAB_MULTIPLE_MGMT_SUBNETS.yml
@@ -0,0 +1,36 @@
+---
+fabric_name: DIGITAL_TWIN_CONTAINERLAB_MULTIPLE_MGMT_SUBNETS
+
+management_eapi:
+ enabled: true
+
+eos_designs_documentation:
+ enable: false
+ topology_csv: false
+ p2p_links_csv: false
+
+digital_twin:
+ environment: containerlab
+ # TODO: remove `fabric` once #5649 is ready
+ fabric: {}
+
+type: spine
+
+spine:
+ defaults:
+ platform: cEOSLab
+ loopback_ipv4_pool: 10.255.0.0/27
+ bgp_as: 65100
+ nodes:
+ - name: containerlab-subnet-a
+ id: 1
+ mgmt_ip: 172.16.1.201/24
+ - name: containerlab-subnet-b
+ id: 2
+ mgmt_ip: 172.16.2.202/24
+
+expected_error_message: >-
+ Containerlab Digital Twin requires all node management IPv4 addresses to belong to the same subnet.
+expected_mgmt_subnets:
+ - 172.16.1.0/24
+ - 172.16.2.0/24
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/inventory/group_vars/DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_DHCP.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/inventory/group_vars/DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_DHCP.yml
new file mode 100644
index 00000000000..5ab2e86e514
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/inventory/group_vars/DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_DHCP.yml
@@ -0,0 +1,31 @@
+---
+fabric_name: DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_DHCP
+
+management_eapi:
+ enabled: true
+
+eos_designs_documentation:
+ enable: false
+ topology_csv: false
+ p2p_links_csv: false
+
+digital_twin:
+ environment: containerlab
+ # TODO: remove `fabric` once #5649 is ready
+ fabric: {}
+
+type: spine
+
+spine:
+ defaults:
+ platform: cEOSLab
+ loopback_ipv4_pool: 10.255.0.0/27
+ bgp_as: 65100
+ nodes:
+ - name: containerlab-dhcp-mgmt
+ id: 1
+ mgmt_ip: dhcp
+
+expected_error_message: >-
+ Containerlab Digital Twin is unsupported for nodes without a static management IPv4 address.
+ Found unsupported management IPv4 address settings: containerlab-dhcp-mgmt.
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/inventory/group_vars/DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_EMPTY.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/inventory/group_vars/DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_EMPTY.yml
new file mode 100644
index 00000000000..a1255cab0ac
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/inventory/group_vars/DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_EMPTY.yml
@@ -0,0 +1,31 @@
+---
+fabric_name: DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_EMPTY
+
+management_eapi:
+ enabled: true
+
+eos_designs_documentation:
+ enable: false
+ topology_csv: false
+ p2p_links_csv: false
+
+digital_twin:
+ environment: containerlab
+ # TODO: remove `fabric` once #5649 is ready
+ fabric: {}
+
+type: spine
+
+spine:
+ defaults:
+ platform: cEOSLab
+ loopback_ipv4_pool: 10.255.0.0/27
+ bgp_as: 65100
+ nodes:
+ - name: containerlab-empty-mgmt
+ id: 1
+ mgmt_ip: ""
+
+expected_error_message: >-
+ Containerlab Digital Twin is unsupported for nodes without a static management IPv4 address.
+ Found unsupported management IPv4 address settings: containerlab-empty-mgmt.
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/inventory/group_vars/DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_NULL.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/inventory/group_vars/DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_NULL.yml
new file mode 100644
index 00000000000..e413de80ea0
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/inventory/group_vars/DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_NULL.yml
@@ -0,0 +1,31 @@
+---
+fabric_name: DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_NULL
+
+management_eapi:
+ enabled: false
+
+eos_designs_documentation:
+ enable: false
+ topology_csv: false
+ p2p_links_csv: false
+
+digital_twin:
+ environment: containerlab
+ # TODO: remove `fabric` once #5649 is ready
+ fabric: {}
+
+type: spine
+
+spine:
+ defaults:
+ platform: cEOSLab
+ loopback_ipv4_pool: 10.255.0.0/27
+ bgp_as: 65100
+ nodes:
+ - name: containerlab-null-mgmt
+ id: 1
+ mgmt_ip:
+
+expected_error_message: >-
+ Containerlab Digital Twin is unsupported for nodes without a static management IPv4 address.
+ Found unsupported management IPv4 address settings: containerlab-null-mgmt.
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/inventory/group_vars/all.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/inventory/group_vars/all.yml
new file mode 100644
index 00000000000..e7e19d60245
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/inventory/group_vars/all.yml
@@ -0,0 +1,2 @@
+---
+root_dir: '{{ playbook_dir }}'
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/inventory/hosts.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/inventory/hosts.yml
new file mode 100644
index 00000000000..aade27cea52
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/inventory/hosts.yml
@@ -0,0 +1,23 @@
+---
+all:
+ children:
+ DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP:
+ children:
+ DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_DHCP:
+ hosts:
+ containerlab-dhcp-mgmt:
+ ansible_host: containerlab-dhcp-mgmt
+ DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_EMPTY:
+ hosts:
+ containerlab-empty-mgmt:
+ ansible_host: containerlab-empty-mgmt
+ DIGITAL_TWIN_CONTAINERLAB_UNSUPPORTED_MGMT_IP_NULL:
+ hosts:
+ containerlab-null-mgmt:
+ ansible_host: containerlab-null-mgmt
+ DIGITAL_TWIN_CONTAINERLAB_MULTIPLE_MGMT_SUBNETS:
+ hosts:
+ containerlab-subnet-a:
+ ansible_host: containerlab-subnet-a
+ containerlab-subnet-b:
+ ansible_host: containerlab-subnet-b
diff --git a/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/molecule.yml b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/molecule.yml
new file mode 100644
index 00000000000..9f7510b6f56
--- /dev/null
+++ b/ansible_collections/arista/avd/extensions/molecule/digital_twin_containerlab_negative_tests/molecule.yml
@@ -0,0 +1,24 @@
+---
+ansible:
+ executor:
+ backend: ansible-playbook
+ args:
+ ansible_playbook:
+ - --inventory=inventory/hosts.yml
+ env:
+ ANSIBLE_FORCE_COLOR: true
+ cfg:
+ defaults:
+ gathering: explicit
+scenario:
+ converge_sequence:
+ - syntax
+ - converge
+ test_sequence:
+ - syntax
+ - converge
+dependency:
+ name: galaxy
+ enabled: false
+verifier:
+ name: ansible
diff --git a/ansible_collections/arista/avd/extensions/molecule/eos_designs-twodc-5stage-clos/digital_twin/documentation/fabric/TWODC_5STAGE_CLOS-topology.yml b/ansible_collections/arista/avd/extensions/molecule/eos_designs-twodc-5stage-clos/digital_twin/documentation/fabric/TWODC_5STAGE_CLOS-topology.yml
index fe109c5c3c6..c108090baf9 100644
--- a/ansible_collections/arista/avd/extensions/molecule/eos_designs-twodc-5stage-clos/digital_twin/documentation/fabric/TWODC_5STAGE_CLOS-topology.yml
+++ b/ansible_collections/arista/avd/extensions/molecule/eos_designs-twodc-5stage-clos/digital_twin/documentation/fabric/TWODC_5STAGE_CLOS-topology.yml
@@ -1,3 +1,4 @@
+---
nodes:
- DC1-POD1-L2LEAF1A:
node_type: cloudeos
diff --git a/ansible_collections/arista/avd/plugins/action/eos_designs_documentation.py b/ansible_collections/arista/avd/plugins/action/eos_designs_documentation.py
index b82032a1472..a2ff00ecc61 100644
--- a/ansible_collections/arista/avd/plugins/action/eos_designs_documentation.py
+++ b/ansible_collections/arista/avd/plugins/action/eos_designs_documentation.py
@@ -4,7 +4,7 @@
from __future__ import annotations
import json
-from dataclasses import asdict
+from dataclasses import fields, is_dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Any
@@ -51,6 +51,20 @@
}
+def _normalize_yaml_data(data: Any) -> Any:
+ """Recursively normalize data for YAML output while honoring per-field YAML key aliases on dataclasses."""
+ if is_dataclass(data):
+ return {
+ str(dataclass_field.metadata.get("yaml_key", dataclass_field.name)): _normalize_yaml_data(getattr(data, dataclass_field.name))
+ for dataclass_field in fields(data)
+ }
+ if isinstance(data, dict):
+ return {str(key): _normalize_yaml_data(value) for key, value in data.items()}
+ if isinstance(data, tuple | list):
+ return [_normalize_yaml_data(value) for value in data]
+ return data
+
+
class ActionModule(AVDActionPlugin):
"""Action Module for eos_designs_documentation."""
@@ -114,11 +128,29 @@ def main(self, task_vars: dict[str, Any]) -> None:
self.result["changed"] = self.result["changed"] or changed
if output.digital_twin:
- content = strip_empties_from_dict(
- {str(key).replace("_", "-"): list(value) if isinstance(value, tuple) else value for key, value in asdict(output.digital_twin).items()}
- )
+ content = strip_empties_from_dict(_normalize_yaml_data(output.digital_twin))
+ # for cLab we want empty `prefix` at all times in the topology to avoid modifying hostnames
+ if get(task_vars, "digital_twin.environment") == "containerlab" and hasattr(output.digital_twin, "prefix"):
+ interface_mapping = content.pop("interface_mapping", None)
+ if interface_mapping:
+ changed = write_file(
+ content=json.dumps(interface_mapping, indent=4) + "\n",
+ filename=str(Path(validated_args["digital_twin_file"]).parent / "interface_mapping.json"),
+ file_mode=validated_args["mode"],
+ )
+ self.result["changed"] = self.result["changed"] or changed
+
+ content["topology"]["nodes"] = {
+ node_name: {
+ "mgmt-ipv4": node_settings["mgmt-ipv4"],
+ "startup-config": f"intended/configs/{node_name}.cfg",
+ }
+ for node_name, node_settings in content["topology"]["nodes"].items()
+ }
+ # add keys in a very specific order - name, prefix, everything else
+ content = {"name": content["name"], "prefix": output.digital_twin.prefix, **{key: value for key, value in content.items() if key != "name"}}
changed = write_file(
- content=yaml.dump(content, Dumper=AnsibleDumper, sort_keys=False, indent=2, width=130),
+ content=yaml.dump(content, Dumper=AnsibleDumper, sort_keys=False, indent=2, width=130, explicit_start=True),
filename=validated_args["digital_twin_file"],
file_mode=validated_args["mode"],
)
diff --git a/ansible_collections/arista/avd/roles/eos_cli_config_gen/docs/tables/metadata.md b/ansible_collections/arista/avd/roles/eos_cli_config_gen/docs/tables/metadata.md
index 4056e7b9a6a..ef5ec060d8a 100644
--- a/ansible_collections/arista/avd/roles/eos_cli_config_gen/docs/tables/metadata.md
+++ b/ansible_collections/arista/avd/roles/eos_cli_config_gen/docs/tables/metadata.md
@@ -136,7 +136,7 @@
| [ - name](## "metadata.cv_pathfinder.applications.categories.user_defined_applications.[].name") | String | | | | |
| [ category](## "metadata.cv_pathfinder.applications.categories.user_defined_applications.[].category") | String | | | | |
| [ digital_twin](## "metadata.digital_twin") | Dictionary | | | | Metadata used to generate Digital Twin topology. |
- | [ environment](## "metadata.digital_twin.environment") | String | | | Valid Values:
- act | Targeted Digital Twin environment. |
+ | [ environment](## "metadata.digital_twin.environment") | String | | | Valid Values:
- act
- containerlab | Targeted Digital Twin environment. |
| [ node_type](## "metadata.digital_twin.node_type") | String | | | | Digital Twin's internal device type used for deploying a replica of the fabric device.
Possible values depend on the target Digital Twin environment. |
| [ ip_addr](## "metadata.digital_twin.ip_addr") | String | | | | Management IPv4_address/Mask assigned to a replica of the fabric device within the Digital Twin environment. |
| [ version](## "metadata.digital_twin.version") | String | | | | OS version used for deploying a replica of the fabric device within the Digital Twin environment. |
@@ -326,7 +326,7 @@
digital_twin:
# Targeted Digital Twin environment.
- environment:
+ environment:
# Digital Twin's internal device type used for deploying a replica of the fabric device.
# Possible values depend on the target Digital Twin environment.
diff --git a/ansible_collections/arista/avd/roles/eos_designs/docs/data-models.md b/ansible_collections/arista/avd/roles/eos_designs/docs/data-models.md
index a5d028cf8a3..75e997b77f4 100644
--- a/ansible_collections/arista/avd/roles/eos_designs/docs/data-models.md
+++ b/ansible_collections/arista/avd/roles/eos_designs/docs/data-models.md
@@ -1838,8 +1838,9 @@ The generated artifacts are automatically optimized for the specific Digital Twi
AVD currently supports the following Digital Twin environments:
- ACT (Arista Cloud Test)
+- Containerlab
-To generate the ACT Digital Twin artifacts, run the `eos_designs` and `eos_cli_config_gen` roles with the `avd_digital_twin_mode` flag set to `true` in your Ansible playbook:
+To generate Digital Twin artifacts, run the `eos_designs` and `eos_cli_config_gen` roles with the `avd_digital_twin_mode` flag set to `true` in your Ansible playbook:
```yaml
---
@@ -1880,7 +1881,7 @@ To generate the ACT Digital Twin artifacts, run the `eos_designs` and `eos_cli_c
```
-Produced artifacts:
+Produced artifacts (ACT Digital Twin):
```text
.
@@ -1921,6 +1922,177 @@ ansible_collections/arista/avd/roles/eos_designs/docs/tables/digital-twin-config
ansible_collections/arista/avd/roles/eos_designs/docs/tables/node-type-digital-twin-configuration.md
--8<--
+### Containerlab Digital Twin
+
+Produced artifacts (Containerlab Digital Twin):
+
+```text
+.
+├── digital_twin
+│ ├── -topology.clab.yml
+│ ├── interface_mapping.json
+│ ├── documentation
+│ │ ├── devices
+│ │ │ ├── .md
+│ │ │ └── ...
+│ │ └── fabric
+│ │ ├── -documentation.md
+│ │ ├── -p2p-links.csv
+│ │ └── -topology.csv
+│ └── intended
+│ ├── configs
+│ │ ├── .cfg
+│ │ └── ...
+│ └── structured_configs
+│ ├── .yml
+│ └── ...
+```
+
+Containerlab Digital Twin is available as a `PREVIEW` feature. It is not ready for production use and is intended for user testing only.
+
+To enable it, set `digital_twin.environment: containerlab` and run the same Digital Twin playbook shown above with the environment set to `containerlab`:
+
+```yaml
+---
+- name: Build Containerlab Digital Twin artifacts
+ hosts: FABRIC
+ gather_facts: false
+ vars:
+ output_dir_name: "digital_twin/intended"
+ documentation_dir_name: "digital_twin/documentation"
+ avd_digital_twin_mode: true
+ digital_twin:
+ environment: containerlab
+ fabric: {}
+ tasks:
+ - name: Generate AVD Structured Configurations and Fabric Documentation
+ ansible.builtin.import_role:
+ name: arista.avd.eos_designs
+
+ - name: Generate Device Configurations and Documentation
+ ansible.builtin.import_role:
+ name: arista.avd.eos_cli_config_gen
+```
+
+For Containerlab, `digital_twin.fabric` is still required for schema validation, even if it is left empty as `fabric: {}`. This is a temporary limitation and it will be removed once the shared Digital Twin schema is updated.
+
+Important caveats for the current Containerlab implementation:
+
+- It is a `PREVIEW` feature and behavior may still change.
+- Only out-of-band management is supported.
+- Only a single IPv4 management subnet is supported across the lab.
+- Management IP addresses in the lab and production must currently match.
+- Each node must have a static `mgmt_ip`. Unset management IPs and `mgmt_ip: dhcp` are not supported.
+- `digital_twin.mgmt_ip` is not currently used by the Containerlab topology generator. Instead, topology generation uses each node `mgmt_ip` value from the AVD fabric facts.
+- The generated topology includes fabric nodes and point-to-point fabric links. Connected endpoints and non-point-to-point links are not modeled.
+- The generated topology sets `prefix: ""` intentionally to preserve the original AVD hostnames.
+- The generated topology currently defaults to `arista_ceos` with `image: arista/ceos:latest`. Please set up your lab environment accordingly, or update the generated topology file if needed.
+
+!!! warning
+ Make sure your lab environment is isolated from production. The management addresses are reused, so take the necessary precautions to avoid pushing configurations to the wrong environment.
+
+The generated topology file is written to the Digital Twin root directory as `{{ output_dir | dirname }}/{{ fabric_name }}-topology.clab.yml`. The generated intended configs remain under `{{ output_dir }}/configs/.cfg`, and the topology references them as `intended/configs/.cfg`.
+
+Example generated topology:
+
+```yaml
+---
+name: FABRIC, Containerlab Digital Twin
+prefix: ''
+mgmt:
+ network: custom_mgmt
+ ipv4-subnet: 172.16.1.0/24
+topology:
+ defaults:
+ kind: arista_ceos
+ kinds:
+ arista_ceos:
+ enforce-startup-config: true
+ image: arista/ceos:latest
+ binds:
+ - interface_mapping.json:/mnt/flash/EosIntfMapping.json:ro
+ nodes:
+ dc1-leaf1a:
+ mgmt-ipv4: 172.16.1.101
+ startup-config: intended/configs/dc1-leaf1a.cfg
+ dc1-leaf1b:
+ mgmt-ipv4: 172.16.1.102
+ startup-config: intended/configs/dc1-leaf1b.cfg
+ dc1-leaf1c:
+ mgmt-ipv4: 172.16.1.151
+ startup-config: intended/configs/dc1-leaf1c.cfg
+ dc1-leaf2a:
+ mgmt-ipv4: 172.16.1.103
+ startup-config: intended/configs/dc1-leaf2a.cfg
+ dc1-leaf2b:
+ mgmt-ipv4: 172.16.1.104
+ startup-config: intended/configs/dc1-leaf2b.cfg
+ dc1-leaf2c:
+ mgmt-ipv4: 172.16.1.152
+ startup-config: intended/configs/dc1-leaf2c.cfg
+ dc1-spine1:
+ mgmt-ipv4: 172.16.1.11
+ startup-config: intended/configs/dc1-spine1.cfg
+ dc1-spine2:
+ mgmt-ipv4: 172.16.1.12
+ startup-config: intended/configs/dc1-spine2.cfg
+ links:
+ - endpoints:
+ - dc1-leaf1a:eth1
+ - dc1-spine1:eth1
+ - endpoints:
+ - dc1-leaf1a:eth2
+ - dc1-spine2:eth1
+ - endpoints:
+ - dc1-leaf1a:eth3
+ - dc1-leaf1b:eth3
+ - endpoints:
+ - dc1-leaf1a:eth4
+ - dc1-leaf1b:eth4
+ - endpoints:
+ - dc1-leaf1a:eth8
+ - dc1-leaf1c:eth1
+ - endpoints:
+ - dc1-leaf1b:eth1
+ - dc1-spine1:eth2
+ - endpoints:
+ - dc1-leaf1b:eth2
+ - dc1-spine2:eth2
+ - endpoints:
+ - dc1-leaf1b:eth8
+ - dc1-leaf1c:eth2
+ - endpoints:
+ - dc1-leaf2a:eth1
+ - dc1-spine1:eth3
+ - endpoints:
+ - dc1-leaf2a:eth2
+ - dc1-spine2:eth3
+ - endpoints:
+ - dc1-leaf2a:eth3
+ - dc1-leaf2b:eth3
+ - endpoints:
+ - dc1-leaf2a:eth4
+ - dc1-leaf2b:eth4
+ - endpoints:
+ - dc1-leaf2a:eth8
+ - dc1-leaf2c:eth1
+ - endpoints:
+ - dc1-leaf2b:eth1
+ - dc1-spine1:eth4
+ - endpoints:
+ - dc1-leaf2b:eth2
+ - dc1-spine2:eth4
+ - endpoints:
+ - dc1-leaf2b:eth8
+ - dc1-leaf2c:eth2
+```
+
+To launch the generated topology use following command:
+
+```bash
+containerlab deploy -t digital_twin/documentation/fabric/-topology.clab.yml
+```
+
## PREVIEW - New devices models
See the [Node type settings](#node-type-settings) section for available keys.
diff --git a/ansible_collections/arista/avd/roles/eos_designs/docs/tables/digital-twin-configuration.md b/ansible_collections/arista/avd/roles/eos_designs/docs/tables/digital-twin-configuration.md
index b6eec35d882..5a99d7c0fef 100644
--- a/ansible_collections/arista/avd/roles/eos_designs/docs/tables/digital-twin-configuration.md
+++ b/ansible_collections/arista/avd/roles/eos_designs/docs/tables/digital-twin-configuration.md
@@ -8,7 +8,7 @@
| Variable | Type | Required | Default | Value Restrictions | Description |
| -------- | ---- | -------- | ------- | ------------------ | ----------- |
| [digital_twin](## "digital_twin") | Dictionary | | | | PREVIEW: This option is marked as "preview", meaning the data models or generated configuration can change at any time.
Global settings to configure the Digital Twin of the Fabric. |
- | [ environment](## "digital_twin.environment") | String | | `act` | Valid Values:
- act | Targeted Digital Twin environment. |
+ | [ environment](## "digital_twin.environment") | String | | `act` | Valid Values:
- act
- containerlab | Targeted Digital Twin environment. |
| [ fabric](## "digital_twin.fabric") | Dictionary | Required | | | Settings for Digital Twin fabric devices. |
| [ act_os_version](## "digital_twin.fabric.act_os_version") | String | | | | OS version for ACT Digital Twin fabric devices. |
| [ act_username](## "digital_twin.fabric.act_username") | String | | `cvpadmin` | | Username for ACT Digital Twin fabric devices. |
@@ -25,7 +25,7 @@
digital_twin:
# Targeted Digital Twin environment.
- environment:
+ environment:
# Settings for Digital Twin fabric devices.
fabric: # required
diff --git a/ansible_collections/arista/avd/roles/eos_designs/tasks/main.yml b/ansible_collections/arista/avd/roles/eos_designs/tasks/main.yml
index 82edf905be4..24ae27adc9d 100644
--- a/ansible_collections/arista/avd/roles/eos_designs/tasks/main.yml
+++ b/ansible_collections/arista/avd/roles/eos_designs/tasks/main.yml
@@ -83,7 +83,13 @@
p2p_links_csv_file: "{{ fabric_dir }}/{{ fabric_name }}-p2p-links.csv"
p2p_links_csv: "{{ eos_designs_documentation.p2p_links_csv | arista.avd.default(false) }}"
toc: "{{ eos_designs_documentation.toc | arista.avd.default(true) }}"
- digital_twin_file: "{{ fabric_dir }}/{{ fabric_name }}-topology.yml"
+ digital_twin_file: >-
+ {{
+ (output_dir | dirname) ~ '/' ~ fabric_name ~ '-topology.clab.yml'
+ if avd_digital_twin_mode | arista.avd.default(false)
+ and digital_twin.environment | arista.avd.default('act') == 'containerlab'
+ else fabric_dir ~ '/' ~ fabric_name ~ '-topology.yml'
+ }}
digital_twin: "{{ avd_digital_twin_mode | arista.avd.default(false) }}"
mode: "0o664"
delegate_to: localhost
diff --git a/ansible_collections/arista/avd/tests/unit/action/test_eos_designs_documentation.py b/ansible_collections/arista/avd/tests/unit/action/test_eos_designs_documentation.py
index 7fb546ffcf4..d7ecf077bf7 100644
--- a/ansible_collections/arista/avd/tests/unit/action/test_eos_designs_documentation.py
+++ b/ansible_collections/arista/avd/tests/unit/action/test_eos_designs_documentation.py
@@ -5,13 +5,24 @@
import json
import logging
+from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
import pytest
from ansible.errors import AnsibleActionFail
-from ansible_collections.arista.avd.plugins.action.eos_designs_documentation import ActionModule
+from ansible_collections.arista.avd.plugins.action.eos_designs_documentation import ActionModule, _normalize_yaml_data
+from pyavd.api.fabric_documentation import (
+ ACTDigitalTwin,
+ ActNodeSettings,
+ ContainerlabDefaults,
+ ContainerlabDigitalTwin,
+ ContainerlabKind,
+ ContainerlabMgmt,
+ ContainerlabNode,
+ ContainerlabTopology,
+)
if TYPE_CHECKING:
from collections.abc import Callable
@@ -41,6 +52,53 @@
}
+def test_normalize_yaml_data_recursively_honors_dataclass_yaml_keys() -> None:
+ """Normalize nested cLab-shaped dataclasses into YAML-ready data while preserving explicit YAML key aliases."""
+
+ @dataclass(frozen=True)
+ class ContainerlabNode:
+ mgmt_ipv4: str = field(metadata={"yaml_key": "mgmt-ipv4"})
+
+ @dataclass(frozen=True)
+ class ContainerlabKind:
+ enforce_startup_config: bool = field(metadata={"yaml_key": "enforce-startup-config"})
+ image: str
+
+ @dataclass(frozen=True)
+ class ContainerlabMgmt:
+ network: str
+ ipv4_subnet: str = field(metadata={"yaml_key": "ipv4-subnet"})
+
+ @dataclass(frozen=True)
+ class ContainerlabTopology:
+ nodes: dict[object, ContainerlabNode]
+ kinds: tuple[ContainerlabKind, ...]
+ endpoint_lists: list[tuple[str, str]]
+
+ @dataclass(frozen=True)
+ class ContainerlabDigitalTwin:
+ mgmt: ContainerlabMgmt
+ topology: ContainerlabTopology
+
+ data = ContainerlabDigitalTwin(
+ mgmt=ContainerlabMgmt(network="clab-mgmt", ipv4_subnet="172.16.1.0/24"),
+ topology=ContainerlabTopology(
+ nodes={1: ContainerlabNode(mgmt_ipv4="172.16.1.101")},
+ kinds=(ContainerlabKind(enforce_startup_config=True, image="ceos:latest"),),
+ endpoint_lists=[("leaf1:eth1", "spine1:eth1")],
+ ),
+ )
+
+ assert _normalize_yaml_data(data) == {
+ "mgmt": {"network": "clab-mgmt", "ipv4-subnet": "172.16.1.0/24"},
+ "topology": {
+ "nodes": {"1": {"mgmt-ipv4": "172.16.1.101"}},
+ "kinds": [{"enforce-startup-config": True, "image": "ceos:latest"}],
+ "endpoint_lists": [["leaf1:eth1", "spine1:eth1"]],
+ },
+ }
+
+
def _empty_output() -> MagicMock:
"""Return a mock FabricDocumentation with all outputs empty so no files are written."""
output = MagicMock()
@@ -99,6 +157,190 @@ def test_run_routes_missing_device_warning_to_display(action_module: Callable[..
assert result.get("failed") is not True
+def test_main_marks_changed_when_p2p_links_csv_changes(action_module: Callable[..., ActionModule], tmp_path: Path) -> None:
+ """Cover changed aggregation for p2p link CSV output."""
+ module = action_module(ActionModule)
+ module.result["changed"] = False
+ p2p_links_csv_file = tmp_path / "p2p-links.csv"
+ output = _empty_output()
+ output.p2p_links_csv = "node,peer\nleaf1,spine1\n"
+ written_files: dict[str, str] = {}
+
+ def mock_write_file(content: str, filename: str, file_mode: str) -> bool:
+ assert file_mode == "0o664"
+ written_files[filename] = content
+ return True
+
+ with (
+ patch.object(
+ module,
+ "_validate_args",
+ return_value={
+ **BASE_VALIDATED_ARGS,
+ "p2p_links_csv": True,
+ "p2p_links_csv_file": str(p2p_links_csv_file),
+ "mode": "0o664",
+ },
+ ),
+ patch.object(module, "load_facts", return_value={}),
+ patch.object(module, "read_structured_configs", return_value={}),
+ patch(f"{MODULE_PATH}.get_fabric_documentation", return_value=output),
+ patch(f"{MODULE_PATH}.write_file", side_effect=mock_write_file),
+ ):
+ module.main(task_vars={"fabric_name": FABRIC_NAME})
+
+ assert written_files == {str(p2p_links_csv_file): "node,peer\nleaf1,spine1\n"}
+ assert module.result["changed"] is True
+
+
+def test_main_writes_containerlab_topology_with_ordered_name_and_prefix(action_module: Callable[..., ActionModule], tmp_path: Path) -> None:
+ """Cover the cLab-only topology key ordering: name and prefix must be emitted first."""
+ module = action_module(ActionModule)
+ module.result["changed"] = False
+ topology_file = tmp_path / "fabric.clab.yml"
+ output = _empty_output()
+ output.digital_twin = ContainerlabDigitalTwin(
+ name="DC1",
+ prefix="",
+ mgmt=ContainerlabMgmt(network="clab-mgmt", ipv4_subnet="172.16.1.0/24"),
+ topology=ContainerlabTopology(
+ defaults=ContainerlabDefaults(kind="arista_ceos"),
+ kinds={"arista_ceos": ContainerlabKind(enforce_startup_config=True, image="ceos:latest")},
+ nodes={"leaf1": ContainerlabNode(mgmt_ipv4="172.16.1.101")},
+ links=(),
+ ),
+ interface_mapping={
+ "ManagementIntf": {"eth0": "Management1"},
+ "EthernetIntf": {"eth1": "Ethernet1"},
+ },
+ )
+ written_files: dict[str, str] = {}
+
+ def mock_write_file(content: str, filename: str, file_mode: str) -> bool:
+ assert file_mode == "0o664"
+ written_files[filename] = content
+ return True
+
+ with (
+ patch.object(
+ module,
+ "_validate_args",
+ return_value={
+ **BASE_VALIDATED_ARGS,
+ "digital_twin": True,
+ "digital_twin_file": str(topology_file),
+ "mode": "0o664",
+ },
+ ),
+ patch.object(module, "load_facts", return_value={}),
+ patch.object(module, "read_structured_configs", return_value={}),
+ patch(f"{MODULE_PATH}.get_fabric_documentation", return_value=output),
+ patch(f"{MODULE_PATH}.write_file", side_effect=mock_write_file),
+ ):
+ module.main(task_vars={"fabric_name": FABRIC_NAME, "digital_twin": {"environment": "containerlab"}})
+
+ assert written_files[str(topology_file)].splitlines()[:3] == ["---", "name: DC1", "prefix: ''"]
+ assert json.loads(written_files[str(tmp_path / "interface_mapping.json")]) == {
+ "ManagementIntf": {"eth0": "Management1"},
+ "EthernetIntf": {"eth1": "Ethernet1"},
+ }
+
+
+def test_main_writes_containerlab_topology_without_interface_mapping(action_module: Callable[..., ActionModule], tmp_path: Path) -> None:
+ """Cover cLab topology rendering when no interface mapping sidecar is present."""
+ module = action_module(ActionModule)
+ module.result["changed"] = False
+ topology_file = tmp_path / "fabric.clab.yml"
+ output = _empty_output()
+ output.digital_twin = ContainerlabDigitalTwin(
+ name="DC1",
+ prefix="",
+ mgmt=ContainerlabMgmt(network="clab-mgmt", ipv4_subnet="172.16.1.0/24"),
+ topology=ContainerlabTopology(
+ defaults=ContainerlabDefaults(kind="arista_ceos"),
+ kinds={"arista_ceos": ContainerlabKind(enforce_startup_config=True, image="ceos:latest")},
+ nodes={"leaf1": ContainerlabNode(mgmt_ipv4="172.16.1.101")},
+ links=(),
+ ),
+ interface_mapping=None,
+ )
+ written_files: dict[str, str] = {}
+
+ def mock_write_file(content: str, filename: str, file_mode: str) -> bool:
+ assert file_mode == "0o664"
+ written_files[filename] = content
+ return True
+
+ with (
+ patch.object(
+ module,
+ "_validate_args",
+ return_value={
+ **BASE_VALIDATED_ARGS,
+ "digital_twin": True,
+ "digital_twin_file": str(topology_file),
+ "mode": "0o664",
+ },
+ ),
+ patch.object(module, "load_facts", return_value={}),
+ patch.object(module, "read_structured_configs", return_value={}),
+ patch(f"{MODULE_PATH}.get_fabric_documentation", return_value=output),
+ patch(f"{MODULE_PATH}.write_file", side_effect=mock_write_file),
+ ):
+ module.main(task_vars={"fabric_name": FABRIC_NAME, "digital_twin": {"environment": "containerlab"}})
+
+ assert list(written_files) == [str(topology_file)]
+ assert written_files[str(topology_file)].splitlines()[:3] == ["---", "name: DC1", "prefix: ''"]
+
+
+def test_main_writes_act_topology_without_containerlab_post_processing(action_module: Callable[..., ActionModule], tmp_path: Path) -> None:
+ """Cover the generic Digital Twin serialization path for non-cLab topologies."""
+ module = action_module(ActionModule)
+ module.result["changed"] = False
+ topology_file = tmp_path / "fabric.yml"
+ output = _empty_output()
+ output.digital_twin = ACTDigitalTwin(
+ nodes=(
+ {
+ "leaf1": ActNodeSettings(
+ node_type="veos",
+ ip_addr="192.0.2.1",
+ version="4.33.1.1F",
+ internet_access=None,
+ ports=None,
+ )
+ },
+ ),
+ )
+ written_files: dict[str, str] = {}
+
+ def mock_write_file(content: str, filename: str, file_mode: str) -> bool:
+ assert file_mode == "0o664"
+ written_files[filename] = content
+ return True
+
+ with (
+ patch.object(
+ module,
+ "_validate_args",
+ return_value={
+ **BASE_VALIDATED_ARGS,
+ "digital_twin": True,
+ "digital_twin_file": str(topology_file),
+ "mode": "0o664",
+ },
+ ),
+ patch.object(module, "load_facts", return_value={}),
+ patch.object(module, "read_structured_configs", return_value={}),
+ patch(f"{MODULE_PATH}.get_fabric_documentation", return_value=output),
+ patch(f"{MODULE_PATH}.write_file", side_effect=mock_write_file),
+ ):
+ module.main(task_vars={"fabric_name": FABRIC_NAME, "digital_twin": {"environment": "act"}})
+
+ assert list(written_files) == [str(topology_file)]
+ assert written_files[str(topology_file)].splitlines()[:2] == ["---", "nodes:"]
+
+
# ---------------------------------------------------------------------------
# Exceptions: user-visible failure messages.
# ---------------------------------------------------------------------------
diff --git a/python-avd/pyavd/_eos_cli_config_gen/schema/__init__.py b/python-avd/pyavd/_eos_cli_config_gen/schema/__init__.py
index 27cb963fe58..7f43890e51b 100644
--- a/python-avd/pyavd/_eos_cli_config_gen/schema/__init__.py
+++ b/python-avd/pyavd/_eos_cli_config_gen/schema/__init__.py
@@ -29279,7 +29279,7 @@ def __init__(
class DigitalTwin(AvdModel):
"""Subclass of AvdModel."""
- Environment: TypeAlias = Literal["act"]
+ Environment: TypeAlias = Literal["act", "containerlab"]
_fields: ClassVar[dict] = {
"environment": {"type": str},
"node_type": {"type": str},
diff --git a/python-avd/pyavd/_eos_cli_config_gen/schema/eos_cli_config_gen.schema.yml b/python-avd/pyavd/_eos_cli_config_gen/schema/eos_cli_config_gen.schema.yml
index 5cb852a96cd..e7ce2644879 100644
--- a/python-avd/pyavd/_eos_cli_config_gen/schema/eos_cli_config_gen.schema.yml
+++ b/python-avd/pyavd/_eos_cli_config_gen/schema/eos_cli_config_gen.schema.yml
@@ -11240,6 +11240,7 @@ keys:
description: Targeted Digital Twin environment.
valid_values:
- act
+ - containerlab
node_type:
type: str
description: 'Digital Twin''s internal device type used for deploying
diff --git a/python-avd/pyavd/_eos_cli_config_gen/schema/schema_fragments/metadata.schema.yml b/python-avd/pyavd/_eos_cli_config_gen/schema/schema_fragments/metadata.schema.yml
index 23b1864c63d..2db7663d48f 100644
--- a/python-avd/pyavd/_eos_cli_config_gen/schema/schema_fragments/metadata.schema.yml
+++ b/python-avd/pyavd/_eos_cli_config_gen/schema/schema_fragments/metadata.schema.yml
@@ -434,6 +434,7 @@ keys:
description: Targeted Digital Twin environment.
valid_values:
- act
+ - containerlab
node_type:
type: str
description: |-
diff --git a/python-avd/pyavd/_eos_designs/schema/__init__.py b/python-avd/pyavd/_eos_designs/schema/__init__.py
index d802f792a31..f0e232e0851 100644
--- a/python-avd/pyavd/_eos_designs/schema/__init__.py
+++ b/python-avd/pyavd/_eos_designs/schema/__init__.py
@@ -17968,7 +17968,7 @@ class Devices(AvdIndexedList[str, DevicesItem]):
class DigitalTwin(AvdModel):
"""Subclass of AvdModel."""
- Environment: TypeAlias = Literal["act"]
+ Environment: TypeAlias = Literal["act", "containerlab"]
class Fabric(AvdModel):
"""Subclass of AvdModel."""
diff --git a/python-avd/pyavd/_eos_designs/schema/eos_designs.schema.yml b/python-avd/pyavd/_eos_designs/schema/eos_designs.schema.yml
index 89dc6d14777..b036d3d0e9f 100644
--- a/python-avd/pyavd/_eos_designs/schema/eos_designs.schema.yml
+++ b/python-avd/pyavd/_eos_designs/schema/eos_designs.schema.yml
@@ -2297,6 +2297,7 @@ keys:
description: Targeted Digital Twin environment.
valid_values:
- act
+ - containerlab
default: act
fabric:
type: dict
diff --git a/python-avd/pyavd/_eos_designs/schema/schema_fragments/digital_twin.schema.yml b/python-avd/pyavd/_eos_designs/schema/schema_fragments/digital_twin.schema.yml
index 4d5426a152c..b6a3794a6a4 100644
--- a/python-avd/pyavd/_eos_designs/schema/schema_fragments/digital_twin.schema.yml
+++ b/python-avd/pyavd/_eos_designs/schema/schema_fragments/digital_twin.schema.yml
@@ -20,6 +20,7 @@ keys:
Targeted Digital Twin environment.
valid_values:
- act
+ - containerlab
default: act
fabric:
type: dict
diff --git a/python-avd/pyavd/_eos_designs/structured_config/metadata/digital_twin.py b/python-avd/pyavd/_eos_designs/structured_config/metadata/digital_twin.py
index 57f8b193c88..7bd91445098 100644
--- a/python-avd/pyavd/_eos_designs/structured_config/metadata/digital_twin.py
+++ b/python-avd/pyavd/_eos_designs/structured_config/metadata/digital_twin.py
@@ -85,3 +85,5 @@ def _set_digital_twin(self: AvdStructuredConfigMetadataProtocol) -> None:
internet_access=act_internet_access,
)
return
+ case "containerlab":
+ self.structured_config.metadata.digital_twin._update(environment=environment)
diff --git a/python-avd/pyavd/api/fabric_documentation/__init__.py b/python-avd/pyavd/api/fabric_documentation/__init__.py
index 77003f60562..8de2f033ef4 100644
--- a/python-avd/pyavd/api/fabric_documentation/__init__.py
+++ b/python-avd/pyavd/api/fabric_documentation/__init__.py
@@ -2,7 +2,7 @@
# Use of this source code is governed by the Apache License 2.0
# that can be found in the LICENSE file.
-from dataclasses import dataclass
+from dataclasses import dataclass, field
@dataclass(frozen=True)
@@ -10,6 +10,42 @@ class ActLinkSettings:
connection: tuple[str, str]
+@dataclass(frozen=True)
+class ContainerlabLinkSettings:
+ endpoints: tuple[str, str]
+
+
+@dataclass(frozen=True)
+class ContainerlabNode:
+ mgmt_ipv4: str = field(metadata={"yaml_key": "mgmt-ipv4"})
+
+
+@dataclass(frozen=True)
+class ContainerlabDefaults:
+ kind: str
+
+
+@dataclass(frozen=True)
+class ContainerlabKind:
+ enforce_startup_config: bool = field(metadata={"yaml_key": "enforce-startup-config"})
+ image: str
+ binds: tuple[str, ...] | None = None
+
+
+@dataclass(frozen=True)
+class ContainerlabMgmt:
+ network: str
+ ipv4_subnet: str = field(metadata={"yaml_key": "ipv4-subnet"})
+
+
+@dataclass(frozen=True)
+class ContainerlabTopology:
+ defaults: ContainerlabDefaults
+ kinds: dict[str, ContainerlabKind]
+ nodes: dict[str, ContainerlabNode]
+ links: tuple[ContainerlabLinkSettings, ...]
+
+
@dataclass(frozen=True)
class ActNodeTypeSettings:
username: str
@@ -34,12 +70,23 @@ class ACTDigitalTwin:
cloudeos: ActNodeTypeSettings | None = None
cvp: ActNodeTypeSettings | None = None
generic: ActNodeTypeSettings | None = None
- third_party: ActNodeTypeSettings | None = None
- tools_server: ActNodeTypeSettings | None = None
+ third_party: ActNodeTypeSettings | None = field(default=None, metadata={"yaml_key": "third-party"})
+ tools_server: ActNodeTypeSettings | None = field(default=None, metadata={"yaml_key": "tools-server"})
veos: ActNodeTypeSettings | None = None
links: tuple[ActLinkSettings, ...] | None = None
+@dataclass(frozen=True)
+class ContainerlabDigitalTwin:
+ """Containerlab Digital Twin fabric documentation dataclass."""
+
+ name: str
+ prefix: str
+ mgmt: ContainerlabMgmt
+ topology: ContainerlabTopology
+ interface_mapping: dict[str, dict[str, str]] | None = None
+
+
class FabricDocumentation:
"""
Object containing the requested documentation.
@@ -54,4 +101,4 @@ class FabricDocumentation:
fabric_documentation: str = ""
topology_csv: str = ""
p2p_links_csv: str = ""
- digital_twin: ACTDigitalTwin | None = None
+ digital_twin: ACTDigitalTwin | ContainerlabDigitalTwin | None = None
diff --git a/python-avd/pyavd/get_fabric_documentation.py b/python-avd/pyavd/get_fabric_documentation.py
index 01232301213..48fb264caaf 100644
--- a/python-avd/pyavd/get_fabric_documentation.py
+++ b/python-avd/pyavd/get_fabric_documentation.py
@@ -3,15 +3,23 @@
# that can be found in the LICENSE file.
from __future__ import annotations
+from ipaddress import ip_network
from re import findall as re_findall
from typing import TYPE_CHECKING, cast
-from pyavd._utils import get
+from pyavd._utils import get, get_ip_from_ip_prefix
from pyavd.api.fabric_documentation import (
ACTDigitalTwin,
ActLinkSettings,
ActNodeSettings,
ActNodeTypeSettings,
+ ContainerlabDefaults,
+ ContainerlabDigitalTwin,
+ ContainerlabKind,
+ ContainerlabLinkSettings,
+ ContainerlabMgmt,
+ ContainerlabNode,
+ ContainerlabTopology,
FabricDocumentation,
)
@@ -147,7 +155,7 @@ def _get_p2p_links_csv(fabric_documentation_facts: FabricDocumentationFacts) ->
return csv_content.read()
-def _get_digital_twin(fabric_documentation_facts: FabricDocumentationFacts) -> ACTDigitalTwin | None:
+def _get_digital_twin(fabric_documentation_facts: FabricDocumentationFacts) -> ACTDigitalTwin | ContainerlabDigitalTwin | None:
digital_twin_env = next(
(
environment
@@ -159,10 +167,100 @@ def _get_digital_twin(fabric_documentation_facts: FabricDocumentationFacts) -> A
match digital_twin_env:
case "act":
return _get_digital_twin_act(fabric_documentation_facts)
+ case "containerlab":
+ return _get_digital_twin_containerlab(fabric_documentation_facts)
case _:
return None
+def _is_p2p_link(topology_link: dict) -> bool:
+ # Skip connections where at least one of the contributing sources is not a non-empty string
+ return bool(
+ isinstance(topology_link["node"], str)
+ and topology_link["node"]
+ and isinstance(topology_link["node_interface"], str)
+ and topology_link["node_interface"]
+ and "." not in topology_link["node_interface"]
+ and isinstance(topology_link["peer"], str)
+ and topology_link["peer"]
+ and isinstance(topology_link["peer_interface"], str)
+ and topology_link["peer_interface"]
+ and "." not in topology_link["peer_interface"]
+ )
+
+
+def _get_digital_twin_containerlab(fabric_documentation_facts: FabricDocumentationFacts) -> ContainerlabDigitalTwin:
+ """
+ Build and return the Containerlab topology data.
+
+ The returned object contains the minimal information required to render
+ Containerlab nodes with management addresses under `topology.nodes` and
+ inter-switch links under `topology.links`.
+ """
+ from pyavd._errors import AristaAvdError # noqa: PLC0415
+
+ sorted_avd_facts = sorted(fabric_documentation_facts.avd_facts.items())
+
+ unsupported_devices = [device for device, facts in sorted_avd_facts if not facts.mgmt_ip or facts.mgmt_ip == "dhcp"]
+ if unsupported_devices:
+ msg = (
+ "Containerlab Digital Twin is unsupported for nodes without a static management IPv4 address. "
+ f"Found unsupported management IPv4 address settings: {', '.join(unsupported_devices)}."
+ )
+ raise AristaAvdError(msg)
+
+ mgmt_ips = [(device, cast("str", facts.mgmt_ip)) for device, facts in sorted_avd_facts]
+ nodes = {device: ContainerlabNode(mgmt_ipv4=get_ip_from_ip_prefix(mgmt_ip)) for device, mgmt_ip in mgmt_ips}
+
+ links = [
+ ContainerlabLinkSettings(
+ endpoints=(
+ f"{topology_link['node']}:{topology_link['node_interface'].replace('Ethernet', 'eth').replace('/', '_')}",
+ f"{topology_link['peer']}:{topology_link['peer_interface'].replace('Ethernet', 'eth').replace('/', '_')}",
+ )
+ )
+ for topology_link in fabric_documentation_facts.topology_links
+ if _is_p2p_link(topology_link)
+ ]
+ ethernet_interface_mapping = {
+ endpoint_interface: endpoint_interface.replace("eth", "Ethernet", 1).replace("_", "/")
+ for link in links
+ for endpoint in link.endpoints
+ for endpoint_interface in [endpoint.rsplit(":", maxsplit=1)[1]]
+ }
+ default_kind = "arista_ceos"
+
+ # find Containerlab mgmt network and raise and error if nodes are not in the same subnet
+ unique_mgmt_networks = {ip_network(mgmt_ip, strict=False) for _, mgmt_ip in mgmt_ips}
+
+ if len(unique_mgmt_networks) > 1:
+ mgmt_networks = ", ".join(f"{network}" for network in unique_mgmt_networks)
+ msg = f"Containerlab Digital Twin requires all node management IPv4 addresses to belong to the same subnet. Found multiple subnets: {mgmt_networks}."
+ raise AristaAvdError(msg)
+
+ return ContainerlabDigitalTwin(
+ name=f"{fabric_documentation_facts.fabric_name}",
+ prefix="",
+ mgmt=ContainerlabMgmt(network="custom_mgmt", ipv4_subnet=str(next(iter(unique_mgmt_networks)))),
+ topology=ContainerlabTopology(
+ defaults=ContainerlabDefaults(kind=default_kind),
+ kinds={
+ default_kind: ContainerlabKind(
+ enforce_startup_config=True,
+ image="arista/ceos:latest",
+ binds=("interface_mapping.json:/mnt/flash/EosIntfMapping.json:ro",),
+ )
+ },
+ nodes=nodes,
+ links=tuple(links),
+ ),
+ interface_mapping={
+ "ManagementIntf": {"eth0": "Management1"},
+ "EthernetIntf": dict(sorted(ethernet_interface_mapping.items())),
+ },
+ )
+
+
def _get_digital_twin_act(fabric_documentation_facts: FabricDocumentationFacts) -> ACTDigitalTwin:
"""
Build and return the ACT topology data.
@@ -206,23 +304,6 @@ def _get_digital_twin_act(fabric_documentation_facts: FabricDocumentationFacts)
}
digital_twin_devices: list[dict[str, ActNodeSettings]] = []
device_list: list[str] = list(fabric_documentation_facts.avd_facts)
- verified_topology_links: list[dict] = [
- topology_link
- for topology_link in fabric_documentation_facts.topology_links
- # Skip connections where at least one of the contributing sources is not a non-empty string
- if (
- isinstance(topology_link["node"], str)
- and topology_link["node"]
- and isinstance(topology_link["node_interface"], str)
- and "." not in topology_link["node_interface"]
- and topology_link["node_interface"]
- and isinstance(topology_link["peer"], str)
- and topology_link["peer"]
- and isinstance(topology_link["peer_interface"], str)
- and "." not in topology_link["peer_interface"]
- and topology_link["peer_interface"]
- )
- ]
for device in sorted(device_list):
if (
digital_twin_node_type := get(fabric_documentation_facts.structured_configs, f"{device}..metadata..digital_twin..node_type", separator="..")
@@ -277,7 +358,8 @@ def _get_digital_twin_act(fabric_documentation_facts: FabricDocumentationFacts)
ActLinkSettings(
connection=(f"{topology_link['node']}:{topology_link['node_interface']}", f"{topology_link['peer']}:{topology_link['peer_interface']}")
)
- for topology_link in verified_topology_links
+ for topology_link in fabric_documentation_facts.topology_links
+ if _is_p2p_link(topology_link)
),
cloudeos=digital_twin_node_types["cloudeos"],
cvp=digital_twin_node_types["cvp"],
diff --git a/python-avd/tests/pyavd/molecule_scenarios/test_get_device_structured_config.py b/python-avd/tests/pyavd/molecule_scenarios/test_get_device_structured_config.py
index 06b8e65a1bf..660347b2f21 100644
--- a/python-avd/tests/pyavd/molecule_scenarios/test_get_device_structured_config.py
+++ b/python-avd/tests/pyavd/molecule_scenarios/test_get_device_structured_config.py
@@ -32,7 +32,7 @@
)
# TODO: Remove inline jinja
# @pytest.mark.digital_twin_molecule_scenarios("eos_designs-twodc-5stage-clos")
-@pytest.mark.digital_twin_molecule_scenarios("digital_twin")
+@pytest.mark.digital_twin_molecule_scenarios("digital_twin", "digital_twin_containerlab")
def test_get_device_structured_config(molecule_host: MoleculeHost) -> None:
"""Test get_device_structured_config."""
hostvars = molecule_host.hostvars
diff --git a/python-avd/tests/pyavd/molecule_scenarios/test_get_fabric_documentation.py b/python-avd/tests/pyavd/molecule_scenarios/test_get_fabric_documentation.py
index 805977c826a..dcb03e8b097 100644
--- a/python-avd/tests/pyavd/molecule_scenarios/test_get_fabric_documentation.py
+++ b/python-avd/tests/pyavd/molecule_scenarios/test_get_fabric_documentation.py
@@ -3,13 +3,15 @@
# that can be found in the LICENSE file.
import sys
from copy import deepcopy
+from types import SimpleNamespace
from unittest.mock import patch
import pytest
from pyavd import get_fabric_documentation
from pyavd._utils import get
-from pyavd.api.fabric_documentation import ACTDigitalTwin, FabricDocumentation
+from pyavd.api.fabric_documentation import ACTDigitalTwin, ContainerlabDigitalTwin, FabricDocumentation
+from pyavd.get_fabric_documentation import _get_digital_twin
from tests.models import MoleculeScenario
@@ -48,6 +50,13 @@ def render(self) -> dict[str, object]:
assert "## Connected Endpoints\n\nNo connected endpoint configured!" in fabric_documentation_obj.fabric_documentation
+def test_get_digital_twin_returns_none_for_unsupported_environment() -> None:
+ """Test fallback path for unsupported Digital Twin environment."""
+ fabric_documentation_facts = SimpleNamespace(structured_configs={"leaf1": {"metadata": {"digital_twin": {"environment": "unsupported"}}}})
+
+ assert _get_digital_twin(fabric_documentation_facts) is None
+
+
@pytest.mark.molecule_scenarios(
"digital_twin",
"eos_designs_unit_tests",
@@ -67,7 +76,7 @@ def render(self) -> dict[str, object]:
"example-single-dc-l3ls",
"example-single-dc-l3ls-ipv6",
)
-@pytest.mark.digital_twin_molecule_scenarios("eos_designs-twodc-5stage-clos", "digital_twin")
+@pytest.mark.digital_twin_molecule_scenarios("eos_designs-twodc-5stage-clos", "digital_twin", "digital_twin_containerlab")
def test_get_fabric_documentation(molecule_scenario: MoleculeScenario) -> None:
"""Test get_fabric_documentation."""
with patch("sys.path", [*sys.path, *molecule_scenario.extra_python_paths]):
@@ -126,7 +135,11 @@ def test_get_fabric_documentation(molecule_scenario: MoleculeScenario) -> None:
if molecule_scenario.digital_twin:
# We expect digital twin topology
- assert isinstance(fabric_documentation_obj.digital_twin, ACTDigitalTwin)
+ digital_twin_environment = get(first_hostvars, "digital_twin.environment", default="act")
+ if digital_twin_environment == "containerlab":
+ assert isinstance(fabric_documentation_obj.digital_twin, ContainerlabDigitalTwin)
+ else:
+ assert isinstance(fabric_documentation_obj.digital_twin, ACTDigitalTwin)
# TODO: add shortcut to the digital twin topology file contents in the MoleculeScenario object and assert that it matches.
else:
# No digital twin topology
diff --git a/python-avd/tests/pyavd/molecule_scenarios/test_negative_eos_designs.py b/python-avd/tests/pyavd/molecule_scenarios/test_negative_eos_designs.py
index bdfb8068473..6ea8f976cb7 100644
--- a/python-avd/tests/pyavd/molecule_scenarios/test_negative_eos_designs.py
+++ b/python-avd/tests/pyavd/molecule_scenarios/test_negative_eos_designs.py
@@ -2,11 +2,15 @@
# Use of this source code is governed by the Apache License 2.0
# that can be found in the LICENSE file.
import re
+import sys
from copy import deepcopy
+from types import SimpleNamespace
+from unittest.mock import MagicMock, patch
import pytest
-from pyavd import get_avd_facts, get_device_structured_config, validate_inputs
+from pyavd import get_avd_facts, get_device_structured_config, get_fabric_documentation, validate_inputs
+from pyavd._eos_designs.structured_config.metadata.digital_twin import DigitalTwinMixin
from pyavd._errors import AristaAvdError
from tests.models import MoleculeHost, MoleculeScenario
@@ -47,3 +51,62 @@ def test_negative_eos_designs(molecule_scenario: MoleculeScenario, molecule_host
# Run get_device_structured_config excepting an error to be raised.
with pytest.raises(Exception, match=re.escape(host_inputs["expected_error_message"])):
_ = get_device_structured_config(molecule_host.name, host_inputs, avd_facts, digital_twin=_is_digital_twin_host)
+
+
+@pytest.mark.molecule_scenarios("digital_twin_containerlab_negative_tests")
+def test_negative_containerlab_fabric_documentation(molecule_scenario: MoleculeScenario) -> None:
+ """Test get_fabric_documentation errors for invalid Containerlab Digital Twin inputs."""
+ tested_fabrics = set()
+
+ with patch("sys.path", [*sys.path, *molecule_scenario.extra_python_paths]):
+ for molecule_host in molecule_scenario.hosts:
+ fabric_name = molecule_host.hostvars["fabric_name"]
+ if fabric_name in tested_fabrics:
+ continue
+ tested_fabrics.add(fabric_name)
+
+ fabric_hosts = molecule_host.hostvars["groups"][fabric_name]
+ fabric_inputs = {host.name: host for host in molecule_scenario.hosts if host.name in fabric_hosts}
+ molecule_structured_configs = {host.name: deepcopy(host.structured_config) for host in fabric_inputs.values()}
+ for structured_config in molecule_structured_configs.values():
+ structured_config.setdefault("metadata", {}).setdefault("digital_twin", {})["environment"] = "containerlab"
+ molecule_avd_facts = {host.name: molecule_scenario.avd_facts[host.name] for host in fabric_inputs.values()}
+
+ with pytest.raises(AristaAvdError, match=re.escape(molecule_host.hostvars["expected_error_message"])) as exc_info:
+ get_fabric_documentation(
+ avd_facts=molecule_avd_facts,
+ structured_configs=molecule_structured_configs,
+ fabric_name=fabric_name,
+ fabric_documentation=False,
+ topology_csv=False,
+ p2p_links_csv=False,
+ digital_twin=True,
+ )
+
+ for expected_mgmt_subnet in molecule_host.hostvars.get("expected_mgmt_subnets", []):
+ assert expected_mgmt_subnet in str(exc_info.value)
+
+
+def test_set_digital_twin_containerlab_and_unsupported_environment() -> None:
+ """Test metadata Digital Twin match handling for cLab and unsupported environments."""
+ digital_twin_metadata = MagicMock()
+ structured_config = SimpleNamespace(metadata=SimpleNamespace(digital_twin=digital_twin_metadata))
+
+ DigitalTwinMixin._set_digital_twin(
+ SimpleNamespace(
+ inputs=SimpleNamespace(digital_twin=SimpleNamespace(environment="containerlab")),
+ structured_config=structured_config,
+ )
+ )
+
+ digital_twin_metadata._update.assert_called_once_with(environment="containerlab")
+ digital_twin_metadata.reset_mock()
+
+ DigitalTwinMixin._set_digital_twin(
+ SimpleNamespace(
+ inputs=SimpleNamespace(digital_twin=SimpleNamespace(environment="unsupported")),
+ structured_config=structured_config,
+ )
+ )
+
+ digital_twin_metadata._update.assert_not_called()