diff --git a/docs/cisco.dcnm.dcnm_vrf_module.rst b/docs/cisco.dcnm.dcnm_vrf_module.rst index 8e91db499..150d7c776 100644 --- a/docs/cisco.dcnm.dcnm_vrf_module.rst +++ b/docs/cisco.dcnm.dcnm_vrf_module.rst @@ -5,7 +5,7 @@ cisco.dcnm.dcnm_vrf ******************* -**Add and remove VRFs from a DCNM managed VXLAN fabric.** +**Add and remove VRFs from a ND managed VXLAN fabric.** Version added: 0.9.0 @@ -17,7 +17,7 @@ Version added: 0.9.0 Synopsis -------- -- Add and remove VRFs and VRF Lite Extension from a DCNM managed VXLAN fabric. +- Add and remove VRFs and VRF Lite Extension from a ND managed VXLAN fabric. - In Multisite fabrics, VRFs can be created only on Multisite fabric - In Multisite fabrics, VRFs cannot be created on member fabric @@ -35,6 +35,29 @@ Parameters Choices/Defaults Comments + + +
+ _fabric_type + +
+ string +
+ + + + + +
INTERNAL PARAMETER - DO NOT USE
+
Fabric type is determined by the module's action plugin
+
This parameter is used internally by the module for multisite fabric processing
+
Valid values are 'multisite_child', 'multisite_parent' and 'standalone'
+ +
@@ -69,6 +92,7 @@ Parameters
Flag to Control Advertisement of Default Route Internally
+
Not applicable at Multisite Parent fabric level
@@ -89,6 +113,7 @@ Parameters
Flag to Control Advertisement of /32 and /128 Routes to Edge Routers
+
Not applicable at Multisite Parent fabric level
@@ -127,7 +152,7 @@ Parameters
Per switch knob to control whether to deploy the attachment
-
This knob has been deprecated from Ansible NDFC Collection Version 2.1.0 onwards. There will not be any functional impact if specified in playbook.
+
This knob has been deprecated from Ansible ND Collection Version 2.1.0 onwards. There will not be any functional impact if specified in playbook.
@@ -145,7 +170,7 @@ Parameters
export evpn route-target
-
supported on NDFC only
+
supported on ND only
Use ',' to separate multiple route-targets
@@ -164,7 +189,7 @@ Parameters
import evpn route-target
-
supported on NDFC only
+
supported on ND only
Use ',' to separate multiple route-targets
@@ -361,6 +386,7 @@ Parameters
VRF Lite BGP Key Encryption Type
Allowed values are 3 (3DES) and 7 (Cisco)
+
Not applicable at Multisite parent fabric level
@@ -378,8 +404,413 @@ Parameters
VRF Lite BGP neighbor password
Password should be in Hex string format
+
Not applicable at Multisite parent fabric level
+ + + +
+ child_fabric_config + +
+ list + / elements=dictionary +
+ + + + +
Configuration for Child fabrics in multisite (MSD) deployments
+
Only applicable for Parent multisite fabrics
+
Defines VRF behavior on each Child fabric
+
Not supported with state 'deleted'
+ + + + + + +
+ adv_default_routes + +
+ boolean +
+ + + + + +
Advertise default routes on Child fabric
+ + + + + + +
+ adv_host_routes + +
+ boolean +
+ + + + + +
Advertise host routes on Child fabric
+ + + + + + +
+ bgp_passwd_encrypt + +
+ integer +
+ + + + + +
BGP password encryption type on Child fabric
+
3 for 3DES encryption, 7 for Cisco encryption
+ + + + + + +
+ bgp_password + +
+ string +
+ + + + +
BGP password for Child fabric VRF Lite
+
Password should be in Hex string format
+ + + + + + +
+ export_mvpn_rt + +
+ string +
+ + + + +
MVPN routes to export on Child fabric
+
Can be configured only when TRM is enabled
+
Use ',' to separate multiple route-targets
+ + + + + + +
+ fabric + +
+ string + / required +
+ + + + +
Name of the Child fabric
+
Must be a valid Child fabric associated with the Parent
+ + + + + + +
+ import_mvpn_rt + +
+ string +
+ + + + +
MVPN routes to import on Child fabric
+
Can be configured only when TRM is enabled
+
Use ',' to separate multiple route-targets
+ + + + + + +
+ l3vni_wo_vlan + +
+ boolean +
+ + + + + +
Enable L3 VNI without VLAN on Child fabric
+ + + + + + +
+ netflow_enable + +
+ boolean +
+ + + + + +
Enable netflow on Child fabric
+
Netflow is supported only if it is enabled on fabric
+ + + + + + +
+ nf_monitor + +
+ string +
+ + + + +
Netflow monitor on Child fabric
+ + + + + + +
+ no_rp + +
+ boolean +
+ + + + + +
No RP, only SSM is used on Child fabric
+
Cannot be used with TRM enabled
+ + + + + + +
+ overlay_mcast_group + +
+ string +
+ + + + +
Overlay IPv4 Multicast group on Child fabric
+
Format (224.0.0.0/4 to 239.255.255.255/4)
+
Can be configured only when TRM is enabled
+ + + + + + +
+ rp_address + +
+ string +
+ + + + +
IPv4 Address of RP (Rendezvous Point) on Child fabric
+
Can be configured only when TRM is enabled
+ + + + + + +
+ rp_external + +
+ boolean +
+ + + + + +
Specifies if RP is external to the Child fabric
+
Can be configured only when TRM is enabled
+ + + + + + +
+ rp_loopback_id + +
+ integer +
+ + + + +
Loopback ID of RP on Child fabric
+
Can be configured only when TRM is enabled
+
Range 0-1023
+ + + + + + +
+ static_default_route + +
+ boolean +
+ + + + + +
Configure static default route on Child fabric
+ + + + + + +
+ trm_bgw_msite + +
+ boolean +
+ + + + + +
Enable TRM on Border Gateway Multisite for Child fabric
+
Can be configured only when TRM is enabled
+
Required for multicast across sites
+ + + + + + +
+ trm_enable + +
+ boolean +
+ + + + + +
Enable TRM (Tenant Routed Multicast) on Child fabric
+
Required for multicast traffic within VRF on Child fabric
+ + + + + + +
+ underlay_mcast_ip + +
+ string +
+ + + + +
Underlay IPv4 Multicast Address on Child fabric
+
Can be configured only when TRM is enabled
+ + + @@ -398,10 +829,11 @@ Parameters
Global knob to control whether to deploy the attachment
-
Ansible NDFC Collection Behavior for Version 2.0.1 and earlier
-
This knob will create and deploy the attachment in DCNM only when set to "True" in playbook
-
Ansible NDFC Collection Behavior for Version 2.1.0 and later
-
Attachments specified in the playbook will always be created in DCNM. This knob, when set to "True", will deploy the attachment in DCNM, by pushing the configs to switch. If set to "False", the attachments will be created in DCNM, but will not be deployed
+
Ansible ND Collection Behavior for Version 2.0.1 and earlier
+
This knob will create and deploy the attachment in ND only when set to "True" in playbook
+
Ansible ND Collection Behavior for Version 2.1.0 and later
+
Attachments specified in the playbook will always be created in ND This knob, when set to "True", will deploy the attachment in ND, by pushing the configs to switch. If set to "False", the attachments will be created in ND, but will not be deployed
+
In case of Multisite fabrics, deploy flag on parent will be inherited by the specified child fabrics.
@@ -422,7 +854,7 @@ Parameters
Disable RT Auto-Generate
-
supported on NDFC only
+
supported on ND only
@@ -439,7 +871,7 @@ Parameters
EVPN routes to export
-
supported on NDFC only
+
supported on ND only
Use ',' to separate multiple route-targets
@@ -457,9 +889,10 @@ Parameters
MVPN routes to export
-
supported on NDFC only
+
supported on ND only
Can be configured only when TRM is enabled
Use ',' to separate multiple route-targets
+
Not applicable at Multisite parent fabric level
@@ -476,7 +909,7 @@ Parameters
VPN routes to export
-
supported on NDFC only
+
supported on ND only
Use ',' to separate multiple route-targets
@@ -494,7 +927,7 @@ Parameters
EVPN routes to import
-
supported on NDFC only
+
supported on ND only
Use ',' to separate multiple route-targets
@@ -512,9 +945,10 @@ Parameters
MVPN routes to import
-
supported on NDFC only
+
supported on ND only
Can be configured only when TRM is enabled
Use ',' to separate multiple route-targets
+
Not applicable at Multisite parent fabric level
@@ -531,7 +965,7 @@ Parameters
VPN routes to import
-
supported on NDFC only
+
supported on ND only
Use ',' to separate multiple route-targets
@@ -575,6 +1009,7 @@ Parameters
Enable L3 VNI without VLAN
+
Not applicable at Multisite parent fabric level
@@ -647,7 +1082,8 @@ Parameters
Enable netflow on VRF-LITE Sub-interface
Netflow is supported only if it is enabled on fabric
-
Netflow configs are supported on NDFC only
+
Netflow configs are supported on ND only
+
Not applicable at Multisite parent fabric level
@@ -664,7 +1100,8 @@ Parameters
Netflow Monitor
-
Netflow configs are supported on NDFC only
+
Netflow configs are supported on ND only
+
Not applicable at Multisite parent fabric level
@@ -685,7 +1122,8 @@ Parameters
No RP, only SSM is used
-
supported on NDFC only
+
supported on ND only
+
Not applicable at Multisite parent fabric level
@@ -703,6 +1141,7 @@ Parameters
Underlay IPv4 Multicast group (224.0.0.0/4 to 239.255.255.255/4)
Can be configured only when TRM is enabled
+
Not applicable at Multisite parent fabric level
@@ -737,6 +1176,7 @@ Parameters
IPv4 Address of RP
Can be configured only when TRM is enabled
+
Not applicable at Multisite parent fabric level
@@ -758,6 +1198,7 @@ Parameters
Specifies if RP is external to the fabric
Can be configured only when TRM is enabled
+
Not applicable at Multisite parent fabric level
@@ -775,6 +1216,7 @@ Parameters
loopback ID of RP
Can be configured only when TRM is enabled
+
Not applicable at Multisite parent fabric level
@@ -812,6 +1254,7 @@ Parameters
Flag to Control Static Default Route Configuration
+
Not applicable at Multisite parent fabric level
@@ -833,6 +1276,7 @@ Parameters
Enable TRM on Border Gateway Multisite
Can be configured only when TRM is enabled
+
Not applicable at Multisite parent fabric level
@@ -853,6 +1297,7 @@ Parameters
Enable Tenant Routed Multicast
+
Not applicable at Multisite parent fabric level
@@ -870,6 +1315,7 @@ Parameters
Underlay IPv4 Multicast Address
Can be configured only when TRM is enabled
+
Not applicable at Multisite parent fabric level
@@ -903,7 +1349,7 @@ Parameters
vlan ID for the vrf attachment
-
If not specified in the playbook, DCNM will auto-select an available vlan_id
+
If not specified in the playbook, ND will auto-select an available vlan_id
@@ -1078,7 +1524,7 @@ Parameters -
The state of DCNM after module completion.
+
The state of ND after module completion.
@@ -1121,163 +1567,388 @@ Examples # # Deleted: # VRFs defined in the playbook will be deleted. - # If no VRFs are provided in the playbook, all VRFs present on that DCNM fabric will be deleted. + # If no VRFs are provided in the playbook, all VRFs present on that ND fabric will be deleted. # # Query: - # Returns the current DCNM state for the VRFs listed in the playbook. + # Returns the current ND state for the VRFs listed in the playbook. # # rollback functionality: # This module supports task level rollback functionality. If any task runs into failures, as part of failure - # handling, the module tries to bring the state of the DCNM back to the state captured in have structure at the + # handling, the module tries to bring the state of the ND back to the state captured in have structure at the # beginning of the task execution. Following few lines provide a logical description of how this works, # if (failure) # want data = have data - # have data = get state of DCNM + # have data = get state of ND # Run the module in override state with above set of data to produce the required set of diffs - # and push the diff payloads to DCNM. + # and push the diff payloads to ND. # If rollback fails, the module does not attempt to rollback again, it just quits with appropriate error messages. - # The two VRFs below will be merged into the target fabric. - - name: Merge vrfs + # =========================================================================== + # Non-MSD/Standalone Fabric Examples + # =========================================================================== + + - name: MERGE | Create two VRFs on a standalone fabric cisco.dcnm.dcnm_vrf: fabric: vxlan-fabric state: merged config: - - vrf_name: ansible-vrf-r1 - vrf_id: 9008011 - vrf_template: Default_VRF_Universal - vrf_extension_template: Default_VRF_Extension_Universal - vlan_id: 2000 - service_vrf_template: null - attach: - - ip_address: 192.168.1.224 - - ip_address: 192.168.1.225 - - vrf_name: ansible-vrf-r2 - vrf_id: 9008012 - vrf_template: Default_VRF_Universal - vrf_extension_template: Default_VRF_Extension_Universal - service_vrf_template: null - attach: - - ip_address: 192.168.1.224 - - ip_address: 192.168.1.225 + - vrf_name: ansible-vrf-r1 + vrf_id: 9008011 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + attach: + - ip_address: 192.168.1.224 + - ip_address: 192.168.1.225 + - vrf_name: ansible-vrf-r2 + vrf_id: 9008012 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + attach: + - ip_address: 192.168.1.224 + - ip_address: 192.168.1.225 - # VRF LITE Extension attached - - name: Merge vrfs + - name: MERGE | Create a VRF with VRF-Lite extensions cisco.dcnm.dcnm_vrf: fabric: vxlan-fabric state: merged config: - - vrf_name: ansible-vrf-r1 - vrf_id: 9008011 - vrf_template: Default_VRF_Universal - vrf_extension_template: Default_VRF_Extension_Universal - vlan_id: 2000 - service_vrf_template: null - attach: - - ip_address: 192.168.1.224 - - ip_address: 192.168.1.225 - vrf_lite: - - peer_vrf: test_vrf_1 # optional - interface: Ethernet1/16 # mandatory - ipv4_addr: 10.33.0.2/30 # optional - neighbor_ipv4: 10.33.0.1 # optional - ipv6_addr: 2010::10:34:0:7/64 # optional - neighbor_ipv6: 2010::10:34:0:3 # optional - dot1q: 2 # dot1q can be got from dcnm/optional - - peer_vrf: test_vrf_2 # optional - interface: Ethernet1/17 # mandatory - ipv4_addr: 20.33.0.2/30 # optional - neighbor_ipv4: 20.33.0.1 # optional - ipv6_addr: 3010::10:34:0:7/64 # optional - neighbor_ipv6: 3010::10:34:0:3 # optional - dot1q: 3 # dot1q can be got from dcnm/optional + - vrf_name: ansible-vrf-r1 + vrf_id: 9008011 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + attach: + - ip_address: 192.168.1.224 + - ip_address: 192.168.1.225 + vrf_lite: + - peer_vrf: test_vrf_1 # optional + interface: Ethernet1/16 # mandatory + ipv4_addr: 192.168.0.2/30 # optional + neighbor_ipv4: 192.168.0.1 # optional + ipv6_addr: 2012::30:34:0:7/64 # optional + neighbor_ipv6: 2012::30:34:0:3 # optional + dot1q: 2 # dot1q can be got from ND/optional + - peer_vrf: test_vrf_2 # optional + interface: Ethernet1/17 # mandatory + ipv4_addr: 192.169.0.2/30 # optional + neighbor_ipv4: 192.169.0.1 # optional + ipv6_addr: 3000::30:34:0:7/64 # optional + neighbor_ipv6: 3000::30:34:0:3 # optional + dot1q: 3 # dot1q can be got from ND/optional - # The two VRFs below will be replaced in the target fabric. - - name: Replace vrfs + - name: REPLACE | Update attachments for a VRF cisco.dcnm.dcnm_vrf: fabric: vxlan-fabric state: replaced config: - - vrf_name: ansible-vrf-r1 - vrf_id: 9008011 - vrf_template: Default_VRF_Universal - vrf_extension_template: Default_VRF_Extension_Universal - vlan_id: 2000 - service_vrf_template: null - attach: - - ip_address: 192.168.1.224 - # Delete this attachment - # - ip_address: 192.168.1.225 - # Create the following attachment - - ip_address: 192.168.1.226 - # Dont touch this if its present on DCNM - # - vrf_name: ansible-vrf-r2 - # vrf_id: 9008012 - # vrf_template: Default_VRF_Universal - # vrf_extension_template: Default_VRF_Extension_Universal - # attach: - # - ip_address: 192.168.1.224 - # - ip_address: 192.168.1.225 + - vrf_name: ansible-vrf-r1 + vrf_id: 9008011 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + attach: + - ip_address: 192.168.1.224 + # Delete this attachment + # - ip_address: 192.168.1.225 + # Create the following attachment + - ip_address: 192.168.1.226 + # Dont touch this if its present on ND + # - vrf_name: ansible-vrf-r2 + # vrf_id: 9008012 + # vrf_template: Default_VRF_Universal + # vrf_extension_template: Default_VRF_Extension_Universal + # attach: + # - ip_address: 192.168.1.224 + # - ip_address: 192.168.1.225 - # The two VRFs below will be overridden in the target fabric. - - name: Override vrfs + - name: OVERRIDE | Override all VRFs on a fabric cisco.dcnm.dcnm_vrf: fabric: vxlan-fabric state: overridden config: - - vrf_name: ansible-vrf-r1 - vrf_id: 9008011 - vrf_template: Default_VRF_Universal - vrf_extension_template: Default_VRF_Extension_Universal - vlan_id: 2000 - service_vrf_template: null - attach: - - ip_address: 192.168.1.224 - # Delete this attachment - # - ip_address: 192.168.1.225 - # Create the following attachment - - ip_address: 192.168.1.226 - # Delete this vrf - # - vrf_name: ansible-vrf-r2 - # vrf_id: 9008012 - # vrf_template: Default_VRF_Universal - # vrf_extension_template: Default_VRF_Extension_Universal - # vlan_id: 2000 - # service_vrf_template: null - # attach: - # - ip_address: 192.168.1.224 - # - ip_address: 192.168.1.225 + - vrf_name: ansible-vrf-r1 + vrf_id: 9008011 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + attach: + - ip_address: 192.168.1.224 + # Delete this attachment + # - ip_address: 192.168.1.225 + # Create the following attachment + - ip_address: 192.168.1.226 + # Delete this vrf + # - vrf_name: ansible-vrf-r2 + # vrf_id: 9008012 + # vrf_template: Default_VRF_Universal + # vrf_extension_template: Default_VRF_Extension_Universal + # vlan_id: 2000 + # attach: + # - ip_address: 192.168.1.224 + # - ip_address: 192.168.1.225 - - name: Delete selected vrfs + - name: DELETE | Delete selected VRFs cisco.dcnm.dcnm_vrf: fabric: vxlan-fabric state: deleted config: - - vrf_name: ansible-vrf-r1 - vrf_id: 9008011 - vrf_template: Default_VRF_Universal - vrf_extension_template: Default_VRF_Extension_Universal - vlan_id: 2000 - service_vrf_template: null - - vrf_name: ansible-vrf-r2 - vrf_id: 9008012 - vrf_template: Default_VRF_Universal - vrf_extension_template: Default_VRF_Extension_Universal - vlan_id: 2000 - service_vrf_template: null + - vrf_name: ansible-vrf-r1 + vrf_id: 9008011 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + - vrf_name: ansible-vrf-r2 + vrf_id: 9008012 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 - - name: Delete all the vrfs + - name: DELETE | Delete all VRFs on a fabric cisco.dcnm.dcnm_vrf: fabric: vxlan-fabric state: deleted - - name: Query vrfs + - name: QUERY | Query specific VRFs cisco.dcnm.dcnm_vrf: fabric: vxlan-fabric state: query config: - - vrf_name: ansible-vrf-r1 - - vrf_name: ansible-vrf-r2 + - vrf_name: ansible-vrf-r1 + - vrf_name: ansible-vrf-r2 + + # =========================================================================== + # MSD (Multi-Site Domain) Fabric Examples + # =========================================================================== + + # Note: For fabrics which are "member" (part of an MSD fabric), + # operations are permitted only through the parent MSD fabric tasks. + + # --------------------------------------------------------------------------- + # STATE: MERGED - Create/Update VRFs on Parent and Child Fabrics + # --------------------------------------------------------------------------- + + - name: MSD MERGE | Create a VRF on Parent and extend to Child fabrics + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric # Must be the Parent MSD fabric + state: merged + config: + - vrf_name: ansible-vrf-msd-1 + vrf_id: 9008011 + vlan_id: 2000 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + # Attachments are for switches at the Parent fabric + attach: + - ip_address: 192.168.1.224 + - ip_address: 192.168.1.225 + # Define how this VRF behaves on each Child fabric + child_fabric_config: + - fabric: vxlan-child-fabric1 + adv_default_routes: true + adv_host_routes: false + - fabric: vxlan-child-fabric2 + adv_default_routes: false + adv_host_routes: true + - vrf_name: ansible-vrf-msd-2 # A second VRF in the same task + vrf_id: 9008012 + vlan_id: 2001 + child_fabric_config: + - fabric: vxlan-child-fabric1 + adv_default_routes: false + adv_host_routes: false + # Attachments are for switches at the Parent fabric + attach: + - ip_address: 192.168.1.224 + - ip_address: 192.168.1.225 + + - name: MSD MERGE | Create VRF with L3VNI and advanced routing settings + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: merged + config: + - vrf_name: ansible-vrf-advanced + vrf_id: 9008020 + vlan_id: 2020 + vrf_int_mtu: 9000 + max_bgp_paths: 4 + max_ibgp_paths: 4 + ipv6_linklocal_enable: true + # Parent-specific settings + redist_direct_rmap: CUSTOM-RMAP-REDIST + v6_redist_direct_rmap: CUSTOM-RMAP-REDIST-V6 + # Child fabric configuration with multicast settings + child_fabric_config: + - fabric: vxlan-child-fabric1 + l3vni_wo_vlan: true + trm_enable: true + trm_bgw_msite: true + rp_address: 10.1.1.1 + underlay_mcast_ip: 239.1.1.1 + overlay_mcast_group: 239.2.1.1 + - fabric: vxlan-child-fabric2 + bgp_password: 1234ABCD + bgp_passwd_encrypt: 7 + netflow_enable: true + nf_monitor: NETFLOW_MONITOR_1 + + # --------------------------------------------------------------------------- + # STATE: REPLACED - Replace VRF configuration on Parent and Child Fabrics + # --------------------------------------------------------------------------- + + - name: MSD REPLACE | Update VRF properties on Parent and Child fabrics + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: replaced + config: + - vrf_name: ansible-vrf-msd-1 + vrf_id: 9008011 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + vrf_int_mtu: 9000 # Update MTU on Parent + # Child fabric configs are replaced: child1 is updated + child_fabric_config: + - fabric: vxlan-child-fabric1 + adv_default_routes: false # Value is updated + adv_host_routes: true # Value is updated + attach: + - ip_address: 192.168.1.224 + # Delete this attachment + # - ip_address: 192.168.1.225 + # Create the following attachment + - ip_address: 192.168.1.226 + # Dont touch this if its present on ND + # - vrf_name: ansible-vrf-r2 + # vrf_id: 9008012 + # vrf_template: Default_VRF_Universal + # vrf_extension_template: Default_VRF_Extension_Universal + # attach: + # - ip_address: 192.168.1.224 + # - ip_address: 192.168.1.225 + + - name: MSD REPLACE | Update VRF with route-target configuration + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: replaced + config: + - vrf_name: ansible-vrf-advanced + vrf_id: 9008020 + vlan_id: 2020 + # Parent route-target settings + disable_rt_auto: false + import_vpn_rt: "65000:10001,65000:10002" + export_vpn_rt: "65000:10001,65000:10002" + import_evpn_rt: "65000:20001,65000:20002" + export_evpn_rt: "65000:20001,65000:20002" + # Child fabric configuration updates + child_fabric_config: + - fabric: vxlan-child-fabric1 + trm_enable: true + import_mvpn_rt: "65000:30001" + export_mvpn_rt: "65000:30001" + + # --------------------------------------------------------------------------- + # STATE: OVERRIDDEN - Override all VRFs on Parent and Child Fabrics + # --------------------------------------------------------------------------- + + - name: MSD OVERRIDE | Override all VRFs ensuring only specified ones exist + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: overridden + config: + - vrf_name: ansible-vrf-production + vrf_id: 9008050 + vlan_id: 2050 + vrf_description: "Production VRF for critical workloads" + child_fabric_config: + - fabric: vxlan-child-fabric1 + adv_default_routes: true + static_default_route: true + - fabric: vxlan-child-fabric2 + adv_default_routes: true + static_default_route: true + attach: + - ip_address: 192.168.1.224 + - ip_address: 192.168.1.225 + # All other VRFs will be deleted from both parent and child fabrics + + # --------------------------------------------------------------------------- + # STATE: DELETED - Delete VRFs from Parent and all Child Fabrics + # --------------------------------------------------------------------------- + + - name: MSD DELETE | Delete a VRF from the Parent and all associated Child fabrics + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: deleted + config: + - vrf_name: ansible-vrf-msd-1 + # The 'child_fabric_config' parameter is not used or allowed for 'deleted' state. + + - name: MSD DELETE | Delete multiple VRFs from Parent and Child fabrics + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: deleted + config: + - vrf_name: ansible-vrf-msd-1 + - vrf_name: ansible-vrf-msd-2 + - vrf_name: ansible-vrf-advanced + + - name: MSD DELETE | Delete all VRFs from the Parent and all associated Child fabrics + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: deleted + + # --------------------------------------------------------------------------- + # STATE: QUERY - Query VRFs + # --------------------------------------------------------------------------- + + - name: MSD QUERY | Query specific VRFs on the Parent MSD fabric + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: query + config: + - vrf_name: ansible-vrf-msd-1 + - vrf_name: ansible-vrf-msd-2 + # The query will return the VRF's configuration on the parent + # and its attachments on all associated child fabrics. + + - name: MSD QUERY | Query all VRFs on the Parent MSD fabric + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: query + # No config specified - returns all VRFs + + - name: MSD QUERY | Query specific VRFs on the Child MSD fabric + cisco.dcnm.dcnm_vrf: + fabric: vxlan-child-fabric1 + state: query + config: + - vrf_name: ansible-vrf-msd-1 + - vrf_name: ansible-vrf-msd-2 + # The query will return the VRF's configuration on the child + # and its attachments. + + - name: MSD QUERY | Query all VRFs on the Child MSD fabric + cisco.dcnm.dcnm_vrf: + fabric: vxlan-child-fabric1 + state: query + # No config specified - returns all VRFs on the child. + + - name: MSD QUERY | Query specific VRFs on Parent & Child fabric + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: query + config: + - vrf_name: ansible-vrf-msd-1 + child_fabric_config: + - fabric: vxlan-child-fabric1 + - vrf_name: ansible-vrf-msd-2 + child_fabric_config: + - fabric: vxlan-child-fabric2 + # The query will return the VRF's configuration on the parent and the + # configuration on the specified childs and its attachments at + # the parent and child level respectively. diff --git a/playbooks/roles/dcnm_network/dcnm_tests.yaml b/playbooks/roles/dcnm_network/dcnm_tests.yaml index bae7aceaa..a806950fe 100644 --- a/playbooks/roles/dcnm_network/dcnm_tests.yaml +++ b/playbooks/roles/dcnm_network/dcnm_tests.yaml @@ -19,59 +19,100 @@ # Uncomment testcase to run a specific test # testcase: replaced_net_all test_data_common: - #--- - fabric: fabric-stage + #---------------------------------- + # Fabric Configuration + #---------------------------------- + fabric: fabric deploy: false - #--- - # Resources - #--- + + #---------------------------------- + # MSD Fabric Configuration + #---------------------------------- + child_fabric: fabric-1 # Child fabric 1 + child_fabric2: fabric-2 # Child fabric 2 + + #---------------------------------- + # Resources (switches) + #---------------------------------- sw1: 192.168.1.1 sw2: 192.168.1.2 - #--- - # Common VRF setup - #--- - vrf_1: ansible-vrf-int1 - vrf_1_id: 9008011 - vrf_1_vlan_id: 500 - #--- - vrf_2: Tenant-1 - vrf_2_id: 9008012 - vrf_2_vlan_id: 501 - #--- - vrf_3: Tenant-2 - vrf_3_id: 9008013 - vrf_3_vlan_id: 502 - #--- + + #---------------------------------- # Interfaces - #--- + #---------------------------------- sw1_int1: Ethernet1/15 sw1_int2: Ethernet1/16 sw1_int3: Ethernet1/17 sw1_int4: Ethernet1/18 - #--- + sw1_int5: Ethernet1/19 + sw1_int6: Ethernet1/20 + sw2_int1: Ethernet1/15 sw2_int2: Ethernet1/16 sw2_int3: Ethernet1/17 sw2_int4: Ethernet1/18 sw2_int5: Ethernet1/19 sw2_int6: Ethernet1/20 - #--- + + #---------------------------------- + # VRF Configuration + #---------------------------------- + vrf_1: ansible-vrf-int1 + vrf_1_id: 9008011 + vrf_1_vlan_id: 500 + + vrf_2: Tenant-1 + vrf_2_id: 9008012 + vrf_2_vlan_id: 501 + + vrf_3: Tenant-2 + vrf_3_id: 9008013 + vrf_3_vlan_id: 502 + + #---------------------------------- + # Standard Network Configuration + #---------------------------------- net1: ansible-net13 net1_net_id: 7005 net1_default_net_template: Default_Network_Universal net1_net_extension_template: Default_Network_Extension_Universal - net1_vlan_id: 1500 + net1_vlan_id: 1502 net1_gw_ip_subnet: '192.168.30.1/24' - #--- + net1_vrf: Tenant-1 + net2: ansible-net12 net2_net_id: 7002 net2_default_net_template: Default_Network_Universal net2_net_extension_template: Default_Network_Extension_Universal net2_vlan_id: 151 net2_gw_ip_subnet: '192.168.40.1/24' - #--- - net1_vrf: Tenant-1 net2_vrf: Tenant-2 - + + #---------------------------------- + # MSD Network Configuration + #---------------------------------- + # MSD Basic Network + msd_net1: ansible-msd-net1 + msd_net1_net_id: 8001 + msd_net1_vlan_id: 2101 + msd_net1_gw_ip_subnet: '192.168.101.1/24' + + # MSD Multi-Child Network + msd_net2: ansible-msd-net2 + msd_net2_net_id: 8002 + msd_net2_vlan_id: 2102 + msd_net2_gw_ip_subnet: '192.168.102.1/24' + + # MSD L2-only Network + msd_l2_net: ansible-msd-l2net + msd_l2_net_id: 8003 + msd_l2_vlan_id: 2103 + + # MSD DHCP Network + msd_dhcp_net: ansible-msd-dhcp-net + msd_dhcp_net_id: 8004 + msd_dhcp_vlan_id: 2104 + msd_dhcp_gw_ip_subnet: '192.168.104.1/24' + roles: - dcnm_network diff --git a/playbooks/roles/dcnm_vrf/dcnm_tests.yaml b/playbooks/roles/dcnm_vrf/dcnm_tests.yaml index e4e9c90aa..161d649ae 100644 --- a/playbooks/roles/dcnm_vrf/dcnm_tests.yaml +++ b/playbooks/roles/dcnm_vrf/dcnm_tests.yaml @@ -35,17 +35,45 @@ # how each var below is used in each test. Some tests, # for example, do not use interface_1. # vars: + # standalone (non-msd) test vars # fabric_1: f1 - # switch_1: 10.1.1.2 - # switch_2: 10.1.1.3 - # switch_3: 10.1.1.4 + # switch_1: 192.168.1.2 + # switch_2: 192.168.1.3 + # switch_3: 192.168.1.4 + + # multisite test vars + # parent_fabric: pf1 + # child_fabric: cf1 + # child_switch_1: 192.168.1.2 + # child_switch_2: 192.168.1.3 + + # common vars # interface_1: Ethernet1/1 - # interface_2: Ethernet1/2 + # interface_2a: Ethernet1/2 # interface_3: Ethernet1/3 + ## Uncomment ONE of the following testcases - # testcase: deleted - # testcase: merged - # testcase: query + + # To run all tests under standalone, + # testcase: standalone/* + # To run individual standalone tests, + # testcase: standalone/ + # Example: + # testcase: standalone/deleted + + # To run all tests under standalone/self-contained-tests, + # testcase: standalone/self-contained-tests/* + # To run individual self-contained tests, + # testcase: standalone/self-contained-tests/ + # Example: + # testcase: standalone/self-contained-tests/scale + + # To run all tests under multisite, + # testcase: msd/* + # To run individual multisite tests, + # testcase: msd/ + # Example: + # testcase: msd/sanity roles: - dcnm_vrf diff --git a/plugins/action/dcnm_network.py b/plugins/action/dcnm_network.py new file mode 100644 index 000000000..d7ed0691d --- /dev/null +++ b/plugins/action/dcnm_network.py @@ -0,0 +1,586 @@ +# Copyright (c) 2020-2022 Cisco and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Action plugin for dcnm_network module with Multi-Site Domain (MSD) support. + +This action plugin provides intelligent processing of network configurations across +MSD fabric hierarchies. It automatically detects fabric types and handles the complex +workflow of configuring networks on both parent and child fabrics in MSD environments. + +Key Features: +- Automatic fabric type detection using NDFC fabric associations API +- MSD parent/child configuration validation and processing +- Intelligent config splitting for parent and child fabric operations +- Structured result aggregation for MSD workflows +- Fail-fast error handling with detailed error messages + +Workflow Overview: +1. Detect fabric type (standalone, msd_parent, msd_child) via API +2. Validate MSD hierarchy rules and configuration constraints +3. Split unified config into separate parent/child configurations +4. Execute dcnm_network module for each fabric with appropriate parameters +5. Aggregate results into structured format for MSD operations + +Supported Fabric Types: +- standalone: Regular fabric not part of MSD hierarchy +- multisite_parent: Parent fabric in MSD domain (fabricState='msd', fabricParent='None') +- multisite_child: Child/member fabric in MSD domain (fabricState='member') + +Author: Neil John +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from ansible.plugins.action import ActionBase +from ansible.utils.display import Display +import json + +display = Display() + + +class ActionModule(ActionBase): + """ + Action plugin for dcnm_network module with comprehensive MSD fabric support. + + This plugin extends the standard dcnm_network module to intelligently handle + Multi-Site Domain (MSD) fabric configurations. It provides automatic fabric + type detection, configuration validation, and orchestrated execution across + parent and child fabrics. + + Core Responsibilities: + 1. Fabric Type Detection: Uses NDFC fabric associations API to automatically + classify fabrics as standalone, multisite_parent, or multisite_child + + 2. Configuration Validation: Enforces MSD hierarchy rules and validates + child_fabric_config parameters for correctness + + 3. Configuration Processing: Transforms unified network configurations into + separate parent and child fabric configurations with proper inheritance + + 4. Execution Orchestration: Coordinates dcnm_network module execution across + multiple fabrics with appropriate state transformations + + 5. Result Aggregation: Structures execution results for both standalone and + MSD workflows with comprehensive error handling + + MSD Configuration Features: + - Automatic deploy flag inheritance from parent to child (no override allowed) + - VRF and L2-only settings propagation to child fabrics + - State transformation (parent 'overridden' becomes child 'replaced') + - Attachment restriction enforcement (parent-only configuration) + - Deploy flag restriction enforcement (parent-only configuration) + - Comprehensive validation of fabric hierarchy relationships + + Error Handling: + - Fail-fast execution with detailed error messages + - Comprehensive validation before any execution begins + - Structured error reporting with fabric context information + """ + + def run(self, tmp=None, task_vars=None): + """ + Main entry point for the action plugin. + + This method orchestrates the MSD fabric detection and processing workflow: + 1. Validates required parameters + 2. Detects fabric type using NDFC fabric associations API + 3. Validates and splits MSD parent/child configurations + 4. Executes network operations on parent and child fabrics + + Args: + tmp (str, optional): Temporary directory path for file operations + task_vars (dict, optional): Ansible task variables and context + + Returns: + dict: Ansible result dictionary containing: + - changed (bool): Whether any changes were made + - failed (bool): Whether the operation failed + - msg (str): Error message if failed + - workflow (str): Type of workflow executed + - parent_fabric (dict): Parent fabric results for MSD + - child_fabrics (list): Child fabric results for MSD + """ + if task_vars is None: + task_vars = dict() + + result = dict( + changed=False + ) + + # Get module arguments + module_args = self._task.args.copy() + fabric_name = module_args.get('fabric') + + if not fabric_name: + result['failed'] = True + result['msg'] = "fabric parameter is required" + return result + + # Detect fabric type using fabric associations API + fabrics = self._get_fabric_associations(task_vars) + + if fabrics is None: + result['failed'] = True + result['msg'] = "Failed to get fabric associations" + return result + + # Validate fabric hierarchy before processing + + state = module_args.get('state') # Get the state parameter + if not state: + result['failed'] = True + result['msg'] = "The 'state' parameter is required" + return result + + config = module_args.get('config') + + if not isinstance(config, list) or not config: + # For 'query' and 'deleted', allow empty config (interpreted as all networks) + if state in ['query', 'deleted']: + config = [] + else: + result['failed'] = True + result['msg'] = f"The 'config' parameter must be a non-empty list for state '{state}'." + return result + + configs, error_msg = self._split_config(fabrics, fabric_name, config, state, result) + + if configs is None: + result['failed'] = True + result['msg'] = error_msg + return result + + # Execute fabric configurations + execution_result = self._execute_fabric_configs(configs, module_args, result, task_vars, tmp) + if execution_result: + return execution_result + + return result + + def _get_fabric_associations(self, task_vars): + """ + Retrieve fabric association information from NDFC using the MSD fabric associations API. + + This method calls the NDFC REST API to get information about all fabrics and their + MSD (Multi-Site Domain) relationships. The API returns fabric state and parent + information needed to determine fabric type classification. + + Args: + task_vars (dict): Ansible task variables for API authentication and context + + Returns: + dict or None: Dictionary mapping fabric names to their association info: + { + 'fabric_name': { + 'fabricState': 'msd'|'member'|'standalone', + 'fabricParent': 'parent_name' or 'None' + } + } + Returns None if API call fails. + + API Endpoint: + GET /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/msd/fabric-associations + """ + + # Use the fabric associations API to get MSD fabric information + msd_fabric_associations = self._execute_module( + module_name="cisco.dcnm.dcnm_rest", + module_args={ + "method": "GET", + "path": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/msd/fabric-associations", + }, + task_vars=task_vars + ) + # Check if the API call was successful + if msd_fabric_associations.get('failed'): + display.error("Fabric associations API call failed") + return + + msd_fabric_associations = msd_fabric_associations.get('response', {}).get('DATA', {}) + fabrics = {} + for fabric in msd_fabric_associations: + fabrics[fabric['fabricName']] = { + 'fabricState': fabric['fabricState'], + 'fabricParent': fabric['fabricParent'] + } + + return fabrics + + def _get_fabric_type(self, fabric_name, fabrics): + """ + Classify fabric type based on MSD fabric association information. + + This method analyzes the fabric state and parent relationship to determine + the correct fabric classification for MSD processing logic. + + Classification Rules: + - multisite_child: fabricState='member' (child fabric in MSD) + - multisite_parent: fabricState='msd' AND fabricParent='None' (parent fabric in MSD) + - standalone: fabricState='standalone' (not part of MSD) + - unknown: fabric not found or unrecognized state + + Args: + fabric_name (str): Name of the fabric to classify + fabrics (dict): Fabric association information from _get_fabric_associations + + Returns: + str: Fabric type classification: + - 'multisite_parent': MSD parent fabric + - 'multisite_child': MSD child/member fabric + - 'standalone': Standalone fabric (not MSD) + - 'unknown': Fabric not found or unrecognized state + """ + fabric_info = fabrics.get(fabric_name) + if not fabric_info: + return 'unknown' + + fabric_state = fabric_info.get('fabricState') + fabric_parent = fabric_info.get('fabricParent') + + if fabric_state == 'member': + return 'multisite_child' + elif fabric_state == 'msd' and fabric_parent == 'None': + return 'multisite_parent' + elif fabric_state == 'standalone': + return 'standalone' + else: + return 'unknown' + + def _split_config(self, fabrics, fabric_name, config, state, result): + """ + Validate MSD fabric hierarchy and split network configurations for parent/child processing. + + This method performs comprehensive validation of MSD fabric relationships and transforms + the unified network configuration into separate configurations for parent and child fabrics. + It enforces MSD hierarchy rules and handles configuration inheritance. + + Validation Rules: + 1. Top-level fabric must not be a child (fabricParent is 'None') + 2. Child fabric configs must be members with correct parent relationship + 3. Child fabrics cannot contain 'attach' configurations (parent-only) + 4. Child fabrics cannot contain 'deploy' flag (must inherit from parent) + 5. Child fabric configurations must reference valid MSD member fabrics + + Configuration Processing: + 1. Split config into parent fabric config (without child_fabric_config) and + separate child fabric configs (with only net_name and child-specific settings) + 2. State handling: If parent state is 'overridden', child state is set to 'replaced'. + Child state is never 'overridden'. + 3. Deploy handling: Child fabrics always inherit the deploy flag from parent level. + Users cannot specify deploy at child level (validation error thrown). + Default to deploy=True if not specified at parent level. + 4. Automatic copying of is_l2only and vrf_name from parent to child + + Args: + fabrics (dict): Fabric association information from _get_fabric_associations + fabric_name (str): Top-level fabric name from playbook + config (list): Network configuration list from playbook + state (str): Ansible state parameter (merged, replaced, deleted, etc.) + result (dict): Result dictionary to update with workflow information + + Returns: + tuple: (list_of_fabric_configs, error_message) + - list_of_fabric_configs (list): List of fabric configurations for execution + - error_message (str): Error description if validation fails, None if successful + + Each fabric config has structure: + { + 'fabric': fabric_name, + '_fabric_type': fabric_type, # 'multisite_parent', 'multisite_child', 'standalone', or 'unknown' + 'state': state, # For parent: original state; For child: 'replaced' if parent state is 'overridden', otherwise original state + 'config': [network_configs...] # deploy defaults to True, child always inherits from parent + } + """ + # Check if top-level fabric exists in fabrics dict + if fabric_name not in fabrics: + return None, f"Top-level fabric '{fabric_name}' not found in fabric associations" + + top_level_fabric = fabrics[fabric_name] + + # Rule 1: Top-level fabric must not be a child (fabricParent is 'None') + # Skip this check for query state since queries are read-only and should work on any fabric + if state != 'query' and top_level_fabric['fabricParent'] != 'None': + return None, f"Top-level fabric '{fabric_name}' cannot be a child fabric. " \ + f"It has parent '{top_level_fabric['fabricParent']}' but must have parent 'None'" + + # Rule 2: Validate child fabric configs and build child fabric configs + child_fabric_configs_by_fabric = {} + + for net_config in config: + # If parent state is 'deleted', discard child_fabric_config and continue + if state == 'deleted': + if 'child_fabric_config' in net_config: + del net_config['child_fabric_config'] + continue + + child_fabric_configs = net_config.get('child_fabric_config', []) + + # Check if child_fabric_config key is present but empty or has no value + if 'child_fabric_config' in net_config and not child_fabric_configs: + return None, ( + f"Network '{net_config.get('net_name', 'unknown')}' has 'child_fabric_config' key " + "but no child fabric configurations provided. Either remove the key or provide " + "child fabric configurations." + ) + + if not child_fabric_configs: + continue + + for child_config in child_fabric_configs: + child_fabric_name = child_config.get('fabric') + + if not child_fabric_name: + return None, f"Child fabric config missing 'fabric' name in network '{net_config.get('net_name', 'unknown')}'" + + # Check if attach configuration is present in child fabric config + if 'attach' in child_config: + return None, f"Child fabric config for '{child_fabric_name}' in network '{net_config.get('net_name', 'unknown')}' " \ + "cannot contain 'attach' configuration. Attachments should only be configured on the parent fabric." + + # Check if deploy flag is present in child fabric config + if 'deploy' in child_config: + return None, f"Child fabric config for '{child_fabric_name}' in network '{net_config.get('net_name', 'unknown')}' " \ + "cannot contain 'deploy' flag. The deploy flag is automatically inherited from the parent fabric configuration." + + # Check if child fabric exists in fabrics dict + if child_fabric_name not in fabrics: + return None, f"Child fabric '{child_fabric_name}' not found in fabric associations" + + child_fabric = fabrics[child_fabric_name] + + # Child fabric must be in 'member' state + if child_fabric['fabricState'] != 'member': + return None, f"Child fabric '{child_fabric_name}' must have fabricState 'member' " \ + f"but has '{child_fabric['fabricState']}'" + + # Child fabric's parent must be the top-level fabric + if child_fabric['fabricParent'] != fabric_name: + return None, f"Child fabric '{child_fabric_name}' must have parent '{fabric_name}' " \ + f"but has parent '{child_fabric['fabricParent']}'" + + # Build child fabric configs while validating + if child_fabric_name not in child_fabric_configs_by_fabric: + child_fabric_configs_by_fabric[child_fabric_name] = [] + + # Create child network config with only net_name and child-specific settings + child_net_config = { + 'net_name': net_config['net_name'] + } + + # Always copy is_l2only and vrf_name from parent to child + if 'is_l2only' in net_config: + child_net_config['is_l2only'] = net_config['is_l2only'] + if 'vrf_name' in net_config: + child_net_config['vrf_name'] = net_config['vrf_name'] + + # Always inherit deploy flag from parent level (default to True if not specified) + if 'deploy' in net_config: + child_net_config['deploy'] = net_config['deploy'] + else: + child_net_config['deploy'] = True + + # Add all child-specific settings except 'fabric' and 'deploy' + for key, value in child_config.items(): + if key not in ['fabric', 'deploy']: + child_net_config[key] = value + + child_fabric_configs_by_fabric[child_fabric_name].append(child_net_config) + + # Set default workflow to standalone + result['workflow'] = 'Standalone' + + # Split configuration into separate fabric configs + fabric_configs = [] + + # 1. Create parent fabric config (remove child_fabric_config from each network) + parent_config = [] + for net_config in config: + parent_net = net_config.copy() + + # Set default deploy to True if not specified + if 'deploy' not in parent_net: + parent_net['deploy'] = True + + if 'child_fabric_config' in parent_net: + del parent_net['child_fabric_config'] + parent_config.append(parent_net) + + parent_fabric_type = self._get_fabric_type(fabric_name, fabrics) + parent_fabric_config = { + 'fabric': fabric_name, + '_fabric_type': parent_fabric_type, + 'state': state, + 'config': parent_config + } + fabric_configs.append(parent_fabric_config) + + # Update workflow if parent is MSD + if parent_fabric_type == 'multisite_parent': + result['workflow'] = 'Parent MSD Processing without child fabric' + + # 2. Create fabric config for each child fabric + for child_fabric_name, child_net_configs in child_fabric_configs_by_fabric.items(): + child_fabric_type = self._get_fabric_type(child_fabric_name, fabrics) + + # Determine child fabric state: if parent state is 'overridden', child state should be 'replaced' + # Child state should never be 'overridden' + if state == 'overridden': + child_state = 'replaced' + else: + child_state = state + + child_fabric_config = { + 'fabric': child_fabric_name, + '_fabric_type': child_fabric_type, + 'state': child_state, + 'config': child_net_configs + } + fabric_configs.append(child_fabric_config) + + # Update workflow when child is detected + if parent_fabric_type == 'multisite_parent': + result['workflow'] = 'Parent MSD with Child Fabric Processing' + else: + result['workflow'] = 'Child Fabric Processing' + + return fabric_configs, None + + def _execute_fabric_configs(self, configs, module_args, result, task_vars, tmp): + """ + Execute dcnm_network module for each fabric configuration and aggregate results. + + This method orchestrates the execution of network operations across parent and child + fabrics in the correct order. It handles different fabric types appropriately and + aggregates results into a structured format for MSD operations. + + Execution Behavior: + - Standalone fabrics: Returns module result exactly as-is (pass-through) + - MSD fabrics: Executes parent first, then children, with aggregated results + - Fail-fast: Stops on first error and returns failure immediately + - Verbose logging: Displays detailed execution information in vvv mode + + Result Aggregation: + - Standalone: Direct pass-through of module result + - MSD: Structured output with separate parent_fabric and child_fabrics sections + - Changed flag: Set if any fabric execution results in changes + + Args: + configs (list): List of fabric configurations from _split_config + Each config dict contains: fabric, _fabric_type, state, config + module_args (dict): Original Ansible module arguments from playbook + result (dict): Base result dictionary to update with execution results + task_vars (dict): Ansible task variables for module execution context + tmp (str): Temporary directory path for module execution + + Returns: + dict or None: + - dict: Error result if execution fails (with failed=True, msg=error) + - None: Successful execution (result dict is updated in-place) + + Side Effects: + - Updates result['changed'] if any fabric execution changes + - Updates result['parent_fabric'] for MSD parent results + - Updates result['child_fabrics'] for MSD child results + - Logs verbose execution details in vvv mode + """ + + # Track fabric results for new output structure + parent_fabric_result = None + child_fabric_results = [] + + # Process each fabric config by calling the dcnm_network module + for fabric_config in configs: + # Prepare module arguments for this fabric + fabric_module_args = module_args.copy() + fabric_module_args['fabric'] = fabric_config['fabric'] + fabric_module_args['state'] = fabric_config['state'] + fabric_module_args['config'] = fabric_config['config'] + fabric_module_args['_fabric_type'] = fabric_config['_fabric_type'] + + # Call the dcnm_network module for this fabric + display.vvv(f"Processing fabric '{fabric_config['fabric']}' with {len(fabric_config['config'])} network(s)") + + # In vvv mode, display fabric and key attributes being pushed to module + if display.verbosity >= 3: + display.vvv(f"Fabric: {fabric_module_args['fabric']}") + display.vvv(f"Fabric Type: {fabric_module_args['_fabric_type']}") + display.vvv(f"State: {fabric_module_args['state']}") + display.vvv("Networks being processed:") + for i, net_config in enumerate(fabric_module_args['config'], 1): + net_name = net_config.get('net_name', 'unknown') + vrf_name = net_config.get('vrf_name', 'N/A') + net_id = net_config.get('net_id', 'auto') + vlan_id = net_config.get('vlan_id', 'auto') + deploy = net_config.get('deploy', True) + display.vvv(f" {i}. {net_name} (VRF: {vrf_name}, Net ID: {net_id}, VLAN: {vlan_id}, Deploy: {deploy})") + + fabric_result = self._execute_module( + module_name="cisco.dcnm.dcnm_network", + module_args=fabric_module_args, + task_vars=task_vars, + tmp=tmp + ) + + # Show raw output in vvv mode + if display.verbosity >= 3: + display.vvv(f"Raw execution result for fabric '{fabric_config['fabric']}':") + display.vvv(json.dumps(fabric_result, indent=2)) + + # For standalone fabrics, return the module result exactly as-is + if fabric_config['_fabric_type'] == 'standalone': + return fabric_result + + # FAIL FAST on first error + if fabric_result.get('failed'): + result['failed'] = True + result['msg'] = ( + f"Failed processing fabric '{fabric_config['fabric']}' " + f"({fabric_config['_fabric_type']}): {fabric_result.get('msg', 'Unknown error')}" + ) + return result + + # Set overall changed flag if any fabric changed + if fabric_result.get('changed'): + result['changed'] = True + display.vvv(f"Fabric '{fabric_config['fabric']}' execution resulted in changes") + + # Store results based on fabric type for new output structure + if fabric_config['_fabric_type'] == 'multisite_parent': + parent_fabric_result = { + 'fabric_name': fabric_config['fabric'], + 'changed': fabric_result.get('changed', False), + 'failed': fabric_result.get('failed', False), + 'response': fabric_result.get('response', []), + 'diff': fabric_result.get('diff', []) + } + elif fabric_config['_fabric_type'] == 'multisite_child': + child_fabric_results.append({ + 'fabric_name': fabric_config['fabric'], + 'changed': fabric_result.get('changed', False), + 'failed': fabric_result.get('failed', False), + 'response': fabric_result.get('response', []), + 'diff': fabric_result.get('diff', []) + }) + + # Structure the final result based on what we processed + if parent_fabric_result: + result['parent_fabric'] = parent_fabric_result + + if child_fabric_results: + result['child_fabrics'] = child_fabric_results + + return None diff --git a/plugins/action/dcnm_vrf.py b/plugins/action/dcnm_vrf.py index a8a473049..0756a8b74 100644 --- a/plugins/action/dcnm_vrf.py +++ b/plugins/action/dcnm_vrf.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2022 Cisco and/or its affiliates. +# Copyright (c) 2020-2025 Cisco and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,35 +16,1523 @@ __metaclass__ = type +# Standard Library Imports +import copy +import json +import time +import traceback +from datetime import datetime + +# Ansible Imports from ansible_collections.ansible.netcommon.plugins.action.network import ( ActionModule as ActionNetworkModule, ) from ansible.utils.display import Display +from ansible.errors import AnsibleError + +# Module Constants +WAIT_TIME_FOR_DELETE_LOOP = 5 +VALID_VRF_STATES = ["DEPLOYED", "PENDING", "NA"] +MAX_RETRY_COUNT = 50 display = Display() +class Logger: + """ + Centralized logging system for NDFC VRF action plugin operations. + + This class provides structured logging with context awareness for fabric operations. + It formats log messages with timestamps, fabric context, and operation context to + facilitate debugging and monitoring of VRF operations across different fabric types. + + Features: + - Contextual logging with fabric and operation information + - Multiple log levels mapped to Ansible display verbosity + - Consistent message formatting across all operations + - Timestamp tracking for performance analysis + + Args: + name (str): Logger instance name for identification + + Attributes: + name (str): Logger identifier used in message formatting + start_time (datetime): Initialization timestamp for duration calculations + """ + + def __init__(self, name="NDFC_VRF_ActionPlugin"): + # Set logger identification name + self.name = name + # Record initialization time for performance tracking + self.start_time = datetime.now() + + def log(self, level, message, fabric=None, operation=None): + """ + Core logging method that formats and outputs messages with context. + + This method creates structured log messages with timestamps and contextual + information, then routes them to appropriate Ansible display methods based + on log level severity. + + Message Format: + - YYYY-MM-DD HH:MM:SS [Logger][Fabric][Operation] LEVEL: message + + Args: + level (str): Log level (debug, info, warning, error) + message (str): Log message content + fabric (str, optional): Fabric context for the log message + operation (str, optional): Operation context for the log message + + Display Routing: + - debug: display.vvv() - Most verbose, debug information + - info: display.vv() - Informational messages + - warning: display.warning() - Warning messages + - error: display.error() - Error messages + - default: display.v() - Standard verbosity + """ + # Generate timestamp for log entry + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + # Build context string starting with logger name + context = f"[{self.name}]" + + # Add fabric context if provided + if fabric: + context += f"[{fabric}]" + # Add operation context if provided + if operation: + context += f"[{operation}]" + + # Format complete log message + log_msg = f"{timestamp} {context} {level.upper()}: {message}" + + # Route to appropriate Ansible display method based on level + if level == "debug": + display.vvv(log_msg) # Highest verbosity for debugging + elif level == "info": + display.vv(log_msg) # Medium verbosity for information + elif level == "warning": + display.warning(log_msg) # Warning level + elif level == "error": + display.error(log_msg) # Error level + else: + display.v(log_msg) # Default verbosity + + def debug(self, message, fabric=None, operation=None): + """Log debug level message with optional context.""" + self.log("debug", message, fabric, operation) + + def info(self, message, fabric=None, operation=None): + """Log info level message with optional context.""" + self.log("info", message, fabric, operation) + + def warning(self, message, fabric=None, operation=None): + """Log warning level message with optional context.""" + self.log("warning", message, fabric, operation) + + def error(self, message, fabric=None, operation=None): + """Log error level message with optional context.""" + self.log("error", message, fabric, operation) + + +class ErrorHandler: + """ + Centralized error handling and API response validation for NDFC operations. + + This class provides standardized error handling, exception management, and API + response validation for all NDFC VRF operations. It ensures consistent error + reporting and helps maintain robust operation flows across fabric types. + + Features: + - Structured exception handling with context preservation + - API response validation with detailed error reporting + - Traceback management for debugging complex failures + - Integration with logging system for error tracking + - Consistent error response formatting for Ansible + + Args: + logger (Logger): Logger instance for error reporting + + Attributes: + logger (Logger): Associated logger for error message output + """ + + def __init__(self, logger): + # Store logger reference for error reporting + self.logger = logger + + def handle_failure( + self, msg, changed=False + ): + """ + Handle failure scenarios with error logging and structured error responses. + + This method processes failure conditions by logging the error with message context, + creating structured error responses suitable for Ansible module returns + + Failure Processing: + - Logs error + - Creates structured error response dictionary + + Args: + msg (str): Failure message to report + changed (bool): Whether any changes were made before failure + + Returns: + dict: Structured error response with failed=True and error details + + """ + + # Log the failure with full context + self.logger.error(msg) + + # Create structured error response + error_response = { + "failed": True, + "changed": changed, + "msg": msg, + } + + return error_response + + def handle_exception( + self, e, operation="unknown", fabric=None, include_traceback=False + ): + """ + Handle exceptions with comprehensive logging and structured error responses. + + This method processes exceptions by extracting relevant information, logging + the error with context, and creating structured error responses suitable for + Ansible module returns. It supports optional traceback inclusion for debugging. + + Exception Processing: + - Extracts exception type and message + - Logs error with operation and fabric context + - Creates structured error response dictionary + - Optionally includes Python traceback for debugging + - Raises AnsibleError with structured information + + Args: + e (Exception): Exception object to handle + operation (str): Operation context where exception occurred + fabric (str, optional): Fabric context for the exception + include_traceback (bool): Whether to include Python traceback + + Returns: + dict: Structured error response with failed=True and error details + + Raises: + AnsibleError: Always raises with structured error information + """ + # Extract exception type and message for structured reporting + error_type = type(e).__name__ + error_msg = str(e) + + # Build context string for error reporting + context = f"Operation: {operation}" + if fabric: + context += f", Fabric: {fabric}" + + # Log the error with full context + self.logger.error(f"{error_type} in {context}: {error_msg}") + + # Create structured error response for Ansible + error_response = { + "failed": True, + "msg": error_msg, + "error_type": error_type, + "operation": operation + } + + # Add fabric context if provided + if fabric: + error_response["fabric"] = fabric + + # Include traceback for debugging if requested + if include_traceback: + error_response["traceback"] = traceback.format_exc() + self.logger.debug(f"Traceback: {traceback.format_exc()}") + + # Raise structured Ansible error + raise AnsibleError(error_response) + + def validate_api_response( + self, response, operation="API call", fabric=None + ): + """ + Validate NDFC API responses and handle various error conditions. + + This method performs comprehensive validation of NDFC API responses, + checking for proper structure, success indicators, and data presence. + It handles the common NDFC API response format and provides detailed + error reporting for debugging failed API calls. + + Validation Checks: + - Response existence and basic structure + - Response format validation (dict expected) + - Failed flag checking for operation failures + - Response data structure validation + - HTTP return code validation (expects 200) + - Data payload presence validation + + NDFC API Response Format: + { + "failed": false, + "response": { + "RETURN_CODE": 200, + "MESSAGE": "OK", + "DATA": [...] + } + } + + Args: + response (dict): NDFC API response to validate + operation (str): Operation description for error reporting + fabric (str, optional): Fabric context for error reporting + + Returns: + dict: Validated response['response'] section with DATA + """ + # Check for response existence + if not response: + raise AnsibleError(f"No response received for {operation}") + + # Validate response format + if not isinstance(response, dict): + raise AnsibleError(f"Invalid response format for {operation}: {response}") + + # Check for operation failure flag + if response.get("failed"): + error_msg = response.get("msg", "Unknown error") + self.logger.error(f"API failure for {operation}: {json.dumps(response, indent=2)}") + raise AnsibleError(f"{operation} failed: {error_msg}") + + # Validate response structure + if not response.get("response"): + self.logger.error(f"Empty response msg for {operation}: {json.dumps(response, indent=2)}") + raise AnsibleError(f"Empty response msg received for {operation}") + + # Extract response data section + resp = response.get("response") + if not isinstance(resp, dict): + raise AnsibleError(f"Invalid response format for {operation}: {resp}") + + # Validate HTTP return code + return_code = resp.get("RETURN_CODE") + if not return_code or return_code != 200: + error_msg = response.get("MESSAGE", f"HTTP {return_code} error") + self.logger.error(f"API error for {operation}: {json.dumps(resp, indent=2)}") + raise AnsibleError(f"{operation} failed: {error_msg}") + + # Validate data payload presence + if not resp.get("DATA"): + self.logger.error(f"Empty response DATA for {operation}: {json.dumps(resp, indent=2)}") + raise AnsibleError(f"Empty response DATA received for {operation}") + + # Return validated response data + return resp + + class ActionModule(ActionNetworkModule): + """ + NDFC VRF Action Plugin supporting Multisite (Multi-Site Domain) workflows + + This action plugin extends the base dcnm_vrf module with Multisite fabric support, + handling Multisite Parent, Child Multisite, and Standalone fabric types. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.logger = Logger("NDFC_VRF_ActionPlugin") + self.error_handler = ErrorHandler(self.logger) + self.logger.info("NDFC VRF Action Plugin initialized") + + # ========================================================================= + # MAIN ENTRY POINT + # ========================================================================= + def run(self, tmp=None, task_vars=None): + """ + Main entry point for NDFC VRF action plugin execution. + + This method orchestrates the complete VRF operation workflow, handling fabric + type detection, validation, and appropriate workflow routing. It serves as the + central coordinator for all VRF operations across different fabric types. + + Execution Flow: + - Performs initial validation of module parameters + - Discovers fabric associations from NDFC controller + - Detects fabric type (Multisite Parent, Multisite Child, Standalone) + - Routes to appropriate workflow handler based on fabric type + - Returns structured results with operation outcomes + + Fabric Type Workflows: + - Multisite Parent: Handles parent VRF config and child fabric coordination + - Multisite Child: Restricts direct access, requires parent fabric routing + - Standalone: Standard VRF operations without Multisite considerations + + Error Handling: + - Comprehensive exception handling with structured error responses + - Detailed logging for debugging and monitoring + - Fail-fast approach with immediate error returns + + Args: + tmp (str, optional): Temporary directory path for module operations + task_vars (dict, optional): Ansible task variables and context + + Returns: + dict: Structured result dictionary containing: + - failed (bool): True if operation failed + - changed (bool): True if fabric state was modified + - msg (str): Success/failure message + - fabric_type (str): Detected fabric type + - workflow (str): Executed workflow description + - Additional workflow-specific result data + """ + # Log workflow initiation + self.logger.info("Starting NDFC VRF action plugin execution") + + # Perform initial parameter validation + result = self.run_pre_validation() + if result is False: + return self.error_handler.handle_failure("Pre-validation failed") + + # Discover fabric associations from NDFC + fabric_data = self.obtain_fabric_associations(task_vars, tmp) + # Extract module arguments and fabric name + module_args = self._task.args.copy() + fabric_name = module_args.get("fabric") + + # Validate required fabric parameter + if not fabric_name: + return self.error_handler.handle_failure("Parameter 'fabric' is required") + + # Log fabric processing initiation + self.logger.info(f"Processing fabric: {fabric_name}", fabric=fabric_name) + # Detect fabric type for workflow routing + fabric_type = self.detect_fabric_type(fabric_name, fabric_data) + if not fabric_type: + return self.error_handler.handle_failure(f"Fabric '{fabric_name}' not found in NDFC.") + + self.logger.info(f"Detected fabric type: {fabric_type}", fabric=fabric_name) + + # Route to appropriate workflow based on fabric type + if fabric_type == "multisite_parent": + result = self.handle_parent_msd_workflow(module_args, fabric_data, task_vars, tmp) + elif fabric_type == "multisite_child": + result = self.handle_child_msd_workflow(module_args, task_vars, tmp) + else: + result = self.handle_standalone_workflow(module_args, task_vars, tmp) + + return result + + # ========================================================================= + # VALIDATION METHODS + # ========================================================================= + + def run_pre_validation(self): + """ + Perform comprehensive input validation for VRF module parameters. + + This method validates the input configuration to ensure proper parameter + usage and catch common configuration errors early in the execution flow. + It checks for parameter placement, structure validation, and state-specific + restrictions to prevent invalid operations. + + Validation Checks: + - vlan_id placement validation (must be in config, not attach block) + - vrf_lite structure validation (must contain interface parameter) + - child_fabric_config restrictions for delete operations + - Parameter structure and format validation + + State-Specific Validations: + - merged/overridden/replaced: Full parameter validation + - deleted: Restricted child_fabric_config usage + + Args: + None (uses self._task.args for validation input) + + Returns: + bool: True if validation passes, False if any validation fails + + Raises: + AnsibleError: On validation failures with detailed error info + """ + # Log validation initiation + self.logger.debug("Starting input validation", operation="validation") + + try: + # Extract state and configuration from task arguments + state = self._task.args.get("state") + config = self._task.args.get("config") + + # Validate configurations for create/update states + if state in ["merged", "overridden", "replaced", "query"] or not state: + if config: + # Iterate through each VRF configuration + for con_idx, con in enumerate(config): + # Validate attach block parameters if present + if "attach" in con: + for at_idx, at in enumerate(con["attach"]): + # Check for vlan_id misplacement + if "vlan_id" in at: + msg = ( + f"Config[{con_idx}].attach[{at_idx}]: vlan_id should not be " + "specified under attach block. Please specify under config block instead" + ) + self.logger.error(msg, operation="validation") + return False + + # Validate vrf_lite structure + if "vrf_lite" in at: + try: + # Attempt to iterate vrf_lite to check structure + for vl in at["vrf_lite"]: + continue + except TypeError: + # vrf_lite is not iterable - missing interface parameter + msg = ( + f"Config[{con_idx}].attach[{at_idx}]: Please specify interface " + "parameter under vrf_lite section in the playbook" + ) + self.logger.error(msg, operation="validation") + return False + + # Log successful validation completion + self.logger.debug( + "Input pre-validation completed successfully", operation="validation" + ) + return True + + except Exception as e: + # Handle validation exceptions + return self.error_handler.handle_exception(e, "validation") + + # ========================================================================= + # FABRIC DISCOVERY & TYPE DETECTION + # ========================================================================= + + def obtain_fabric_associations(self, task_vars, tmp): + """ + Retrieve fabric associations and relationships from NDFC controller. + + This method queries the NDFC controller to obtain fabric association data, + which includes fabric types, relationships (parent-child), and states. + This information is essential for fabric type detection and Multisite workflow + routing decisions. + + API Endpoint: + - GET /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/msd/fabric-associations + + Response Processing: + - Validates API response structure and success + - Extracts fabric data and builds lookup dictionary + - Indexes fabrics by name for efficient access + - Handles empty responses and API errors + + Fabric Data Structure: + Each fabric entry contains: + - fabricName: Fabric identifier + - fabricType: Type (MSD, VXLAN, etc.) + - fabricState: State (member, parent, standalone) + - fabricParent: Parent fabric name (for child fabrics) + + Args: + task_vars (dict): Ansible task variables for module execution + tmp (str): Temporary directory path for module operations + + Returns: + dict: Fabric associations indexed by fabric name: + { + "fabric_name": { + "fabricName": "fabric_name", + "fabricType": "MSD", + "fabricState": "parent", + "fabricParent": null + } + } + + Raises: + AnsibleError: On API failure or invalid response structure + """ + # Log fabric discovery initiation + self.logger.debug( + "Fetching fabric associations from NDFC", operation="fabric_discovery" + ) + + try: + # Execute NDFC REST API call to get fabric associations + msd_fabric_associations = self._execute_module( + module_name="cisco.dcnm.dcnm_rest", + module_args={ + "method": "GET", + "path": ( + "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/" + "fabrics/msd/fabric-associations" + ), + }, + task_vars=task_vars, + tmp=tmp + ) + + # Validate API response structure and extract data + response_data = self.error_handler.validate_api_response( + msd_fabric_associations, + "fabric associations retrieval" + ) + + # Build fabric data lookup dictionary + fabric_data = {} + for fabric in response_data.get("DATA", []): + fabric_name = fabric.get("fabricName") + if fabric_name: + # Index fabric data by fabric name for efficient lookups + fabric_data[fabric_name] = fabric + + # Log successful fabric data retrieval + self.logger.info(f"Retrieved {len(fabric_data)} fabric associations", operation="fabric_discovery") + return fabric_data + + except Exception as e: + # Handle fabric discovery failures + return self.error_handler.handle_exception(e, "fabric_discovery") + + def detect_fabric_type(self, fabric_name, fabric_data): + """ + Analyze fabric data to determine fabric type for workflow routing. + + This method examines fabric properties from NDFC to classify the fabric + into one of three types that determine the appropriate VRF workflow. + The classification drives the execution path and operation restrictions. + + Fabric Type Classification: + - Multisite Parent: fabricType="MSD" - Can orchestrate child fabrics + - Multisite Child: fabricState="member" - Restricted to parent-driven operations + - Standalone: All others - Standard VRF operations without Multisite features + + Detection Logic: + 1. Check if fabric exists in NDFC fabric associations + 2. Examine fabricType field for Multisite Parent identification + 3. Examine fabricState field for child fabric identification + 4. Default to standalone for all other configurations + + Args: + fabric_name (str): Name of fabric to classify + fabric_data (dict): Fabric associations data from NDFC + + Returns: + None|str: Detected fabric type: + - "multisite_parent" + - "multisite_child" + - "standalone" + - None if fabric not found + """ + # Log fabric type detection initiation + self.logger.debug( + f"Detecting fabric type for: {fabric_name}", + fabric=fabric_name, + operation="type_detection" + ) + + # Validate fabric exists in NDFC associations + if fabric_name not in fabric_data: + return None + + # Extract fabric properties for classification + fabric_info = fabric_data.get(fabric_name) + fabric_type = fabric_info.get("fabricType") + fabric_state = fabric_info.get("fabricState") + + # Classify fabric based on properties + if fabric_type == "MSD": + # Multisite type indicates parent fabric + detected_type = "multisite_parent" + elif fabric_state == "member": + # Member state indicates child fabric + detected_type = "multisite_child" + else: + # All others are standalone fabrics + detected_type = "standalone" + + # Log classification result with details + self.logger.debug( + f"Fabric type detected: {detected_type} " + f"(fabricType={fabric_type}, fabricState={fabric_state})", + fabric=fabric_name, + operation="type_detection" + ) + return detected_type + + def validate_child_parent_fabric(self, child_fabric, parent_fabric, fabric_data): + """ + Validate the relationship between child and Multisite Parent fabrics. + + This method ensures that child fabrics are properly associated with their + Multisite Parent fabric and validates the hierarchical relationship integrity. + It prevents misconfigurations that could lead to operational issues in + multi-site domain environments. + + Validation Checks: + - Child fabric exists in NDFC fabric associations + - Parent fabric exists in NDFC fabric associations + - Child fabric has fabricState="member" (indicating child status) + - Child fabric's fabricParent matches specified parent fabric + - Proper Multisite hierarchy enforcement + + Multisite Hierarchy Rules: + - Child fabrics must be in "member" state + - Child fabrics must reference correct parent fabric + - Parent-child relationships must be properly established in NDFC + + Args: + child_fabric (str): Name of child fabric to validate + parent_fabric (str): Name of expected parent fabric + fabric_data (dict): Fabric associations data from NDFC + + Returns: + bool: True if child-parent relationship is valid, False otherwise + """ + # Log validation initiation with context + self.logger.debug( + f"Validating child-parent fabric relationship: {child_fabric} <-> {parent_fabric}", + fabric=child_fabric, + operation="child_parent_validation" + ) + + # Validate both fabrics exist in NDFC + if child_fabric not in fabric_data: + available_fabrics = list(fabric_data.keys()) + error_msg = ( + f"Fabric '{child_fabric}' and not found in NDFC. " + f"Available fabrics: {available_fabrics}" + ) + self.logger.error( + error_msg, fabric=child_fabric, operation="child_parent_validation" + ) + return False + + # Extract child fabric properties + fabric_info = fabric_data.get(child_fabric) + fabric_state = fabric_info.get("fabricState") + fabric_parent = fabric_info.get("fabricParent") + + # Validate child fabric is in member state + if fabric_state != "member": + error_msg = f"Fabric '{child_fabric}' is not a Child fabric (fabricState={fabric_state})" + self.logger.error( + error_msg, fabric=child_fabric, operation="child_parent_validation" + ) + return False + + # Validate parent-child relationship + if fabric_parent != parent_fabric: + error_msg = ( + f"Fabric '{child_fabric}' is not associated with Multisite Parent fabric '{parent_fabric}' " + f"(detected parent: '{fabric_parent}')" + ) + self.logger.error( + error_msg, fabric=child_fabric, operation="child_parent_validation" + ) + return False + + # Validation passed + return True + + # ========================================================================= + # WORKFLOW HANDLERS + # ========================================================================= + + def handle_parent_msd_workflow(self, module_args, fabric_data, task_vars, tmp): + """ + Execute comprehensive Multisite Parent fabric workflow with child fabric orchestration. + This method implements the complete Multisite Parent workflow that coordinates VRF + operations across parent and child fabrics in the correct sequence. It handles + configuration splitting, validation, execution coordination, and result aggregation + for complex multi-site domain scenarios. + + Workflow Sequence: + 1. Configuration Validation and Splitting + - Validates child fabric configurations and relationships + - Splits parent and child configurations into separate tasks + - Ensures proper Multisite hierarchy and parameter usage + + 2. Parent Fabric Operations + - Executes VRF creation, configuration, and attachment on parent + - Handles parent-specific parameters and templates + - Validates parent operation completion before child processing + 3. Child Fabric Coordination + - Waits for VRF readiness on child fabrics + - Executes child fabric tasks sequentially + - Applies child-specific VRF parameters and configurations + + 4. Result Aggregation + - Combines parent and child operation results + - Creates structured response with fabric-specific outcomes + - Handles error propagation and rollback scenarios + + Configuration Processing: + - Extracts child_fabric_config from parent VRF definitions + - Creates clean parent configurations without child parameters + - Generates child tasks grouped by fabric with VRF lists + - Validates child fabric relationships and capabilities + + Error Handling: + - Fail-fast on validation errors with detailed messages + - Child task failures abort workflow with context preservation + - Comprehensive logging for debugging complex multi-fabric scenarios + Args: + module_args (dict): Original module arguments from playbook + fabric_data (dict): Fabric associations data from NDFC + task_vars (dict): Ansible task variables for execution context + tmp (str): Temporary directory path for operations + + Returns: + dict: Comprehensive workflow result containing: + - failed (bool): True if any operation failed + - changed (bool): True if any fabric was modified + - fabric_type (str): "multisite_parent" + - workflow (str): Workflow description + - parent_fabric (dict): Parent fabric operation results + - child_fabrics (list): Child fabric operation results + - Error details and context if failures occurred + """ + # Extract parent fabric name for context + parent_fabric = module_args.get("fabric") + # Log workflow initiation + self.logger.info( + "Starting Multisite Parent workflow", + fabric=parent_fabric, + operation="parent_multisite_workflow" + ) + + try: + # Step 1: Validate and split parent/child configurations + config = module_args.get("config") + state = module_args.get("state") + parent_config = [] + child_tasks_dict = {} + + if config: + # Process each VRF configuration for parent/child splitting + for vrf_idx, vrf in enumerate(config): + child_fabric_configs = vrf.get("child_fabric_config") + if "child_fabric_config" in vrf: + if state != "deleted": + + child_fabric_configs = vrf.get("child_fabric_config") + if not child_fabric_configs: + error_msg = ( + f"Config[{vrf_idx+1}]: child_fabric_config is required for " + "Multisite Parent fabrics. It can be optionally removed when state is query/deleted." + ) + return self.error_handler.handle_failure(error_msg) + + # Validate each child fabric configuration + for child_idx, child_config in enumerate(child_fabric_configs): + fabric_name = child_config.get("fabric") + if not fabric_name: + error_msg = ( + f"Config[{vrf_idx+1}].child_fabric_config[{child_idx+1}]: " + "fabric is required" + ) + return self.error_handler.handle_failure(error_msg) + # Validate child fabric type and child-parent relationship + if not self.validate_child_parent_fabric( + fabric_name, parent_fabric, fabric_data + ): + error_msg = ( + f"Multisite Child-Parent fabric validation failed: {fabric_name} -> {parent_fabric}" + ) + return self.error_handler.handle_failure(error_msg) + + # Create child tasks and group by child fabric name + child_tasks_dict = self.create_child_task( + vrf, child_config, module_args, child_tasks_dict + ) + + # Create parent VRF without child_fabric_config + parent_vrf = copy.deepcopy(vrf) + del parent_vrf["child_fabric_config"] + parent_config.append(parent_vrf) + else: + # Handle VRFs without child fabric configurations + parent_vrf = copy.deepcopy(vrf) + parent_config.append(parent_vrf) + + # Step 2: Execute parent VRF operations + self.logger.info( + f"Executing parent operations for {len(parent_config)} VRF configurations", + fabric=parent_fabric, + operation="parent_execution" + ) + parent_module_args = copy.deepcopy(module_args) + parent_module_args["config"] = parent_config + parent_module_args["_fabric_type"] = "multisite_parent" + + # Execute parent fabric VRF operations + parent_result = self.execute_module_with_args(parent_module_args, task_vars, tmp) + + # Step 3: Execute child fabric tasks if parent succeeded + child_results = [] + if not parent_result.get("failed", False) and child_tasks_dict: + self.logger.info(f"Processing {len(child_tasks_dict)} child fabrics", + fabric=parent_fabric, operation="child_execution") + + for child_task in child_tasks_dict.values(): + if state != "query": + # Wait for VRF readiness on child fabric before processing + all_vrf_ready, vrf_not_ready = self.wait_for_vrf_ready( + child_task["vrf_list"], + child_task["fabric"], + task_vars, + tmp + ) + if not all_vrf_ready: + error_msg = ( + f"VRF(s) {', '.join(vrf_not_ready)} not in a deployable state on fabric " + f"{child_task['fabric']}. Please ensure VRF(s) are in DEPLOYED/PENDING/NA " + "state before proceeding." + ) + return self.error_handler.handle_failure(error_msg, changed=True) + + # Execute child fabric task + self.logger.info("Executing child task", fabric=child_task["fabric"], operation="child_execution") + child_result = self.execute_child_task(child_task, task_vars, tmp) + child_results.append(child_result) + + # Handle child task failures with immediate abort + if child_result.get("failed", False): + error_msg = f"Child fabric task failed for {child_task['fabric']}: {child_result.get('msg', 'Unknown error')}" + self.logger.error(error_msg, fabric=child_task["fabric"], operation="child_execution") + break + + # Step 4: Create structured results + result = self.create_structured_results(parent_result, child_results, parent_fabric) + self.logger.info("Multisite Parent workflow completed successfully", fabric=parent_fabric, operation="parent_multisite_workflow") + return result + + except Exception as e: + # Handle workflow-level exceptions + return self.error_handler.handle_exception(e, "parent_multisite_workflow", parent_fabric) + + def handle_child_msd_workflow(self, module_args, task_vars, tmp): + """ + Handle restricted access attempts to Child Multisite fabrics. + This method enforces the Multisite operational model by preventing direct + access to child fabrics. In Multisite architectures, all VRF operations + must be coordinated through the parent fabric to maintain consistency + and proper orchestration across the multi-site domain. + + Operational Restrictions: + - Direct VRF operations on child fabrics are not permitted + - All child fabric changes must be initiated from parent fabric + - Prevents configuration drift and maintains Multisite integrity + - Enforces proper Multisite workflow patterns + + Security Model: + - Child fabrics should only be modified through parent orchestration + - Direct access could bypass Multisite coordination mechanisms + - Prevents unauthorized or uncoordinated fabric modifications + + Args: + module_args (dict): Original module arguments from playbook + task_vars (dict): Ansible task variables for module execution + + Returns: + dict: Result indicating operation: + - failed (bool): True or False based on operation + - changed (bool): False (no changes allowed) + - msg (str): Operation specific data message + - fabric_type (str): "multisite_child" + - workflow (str): "Child Multisite Workflow" + """ + # Extract fabric name for logging + fabric_name = module_args.get("fabric") + state = module_args.get("state") + self.logger.info("Starting Multisite Child workflow", operation="multisite_child_workflow") + if state == "query": + child_module_args = { + "fabric": module_args["fabric"], + "state": "query", + "config": module_args.get("config"), + "_fabric_type": "standalone" + } + + # Execute base dcnm_vrf module functionality + result = self.execute_module_with_args(child_module_args, task_vars, tmp) + + # Add workflow identification to result if not present + if "fabric_type" not in result: + result["fabric_type"] = "multisite_child" + result["workflow"] = "Multisite Child VRF Processing" + + # Log successful completion + self.logger.info("Multisite Child workflow completed successfully", operation="multisite_child_workflow") + else: + # Log attempted direct child fabric access for other states + error_msg = f"Attempted task on Child Multisite fabric '{fabric_name}'. State 'query' is only allowed." + return self.error_handler.handle_failure(error_msg) + + return result + + def handle_standalone_workflow(self, module_args, task_vars, tmp): + """ + Execute standard VRF operations for non-Multisite (standalone) fabrics. + + This method handles VRF operations for fabrics that are not part of + Multi-Site Domain (Multisite) configurations. It provides a direct pass-through + to the base dcnm_vrf module functionality without Multisite-specific processing. + + Workflow Characteristics: + - Direct pass-through to base module functionality + - No child fabric considerations or orchestration + - Standard VRF operations (create, update, delete, attach) + - No additional Multisite-specific validation or processing + + Operation Types Supported: + - VRF creation and configuration + - VRF attachments to switches + - VRF updates and modifications + - VRF deletion and cleanup + + Args: + module_args (dict): Original module arguments from playbook + task_vars (dict): Ansible task variables for module execution + Returns: + dict: Module execution result containing: + - changed (bool): True if fabric state was modified + - failed (bool): True if operation failed + - fabric_type (str): Set to "standalone" + - workflow (str): Workflow description + - Additional standard dcnm_vrf module results + """ + # Log standalone workflow initiation + self.logger.info("Starting standalone Non-Multisite workflow", operation="standalone_workflow") + + parent_module_args = { + "fabric": module_args["fabric"], + "config": module_args.get("config"), + "_fabric_type": "standalone" + } + + if module_args.get("state"): + parent_module_args["state"] = module_args["state"] + + # Execute base dcnm_vrf module functionality + result = self.execute_module_with_args(parent_module_args, task_vars, tmp) + + # Add workflow identification to result if not present + if "fabric_type" not in result: + result["fabric_type"] = "standalone" + result["workflow"] = "Standalone Fabric VRF Processing" + + # Log successful completion + self.logger.info("Standalone workflow completed successfully", operation="standalone_workflow") + return result + + # ========================================================================= + # CHILD FABRIC TASK MANAGEMENT + # ========================================================================= + + def create_child_task(self, parent_vrf, child_config, parent_module_args, child_tasks_dict): + """ + Create and organize child fabric tasks from parent VRF and child configurations. + + This method processes child fabric configurations and creates structured tasks + that can be executed independently on child fabrics. It handles task grouping + by fabric name to optimize execution and maintains VRF context from parent + configurations while applying child-specific parameters. + + Task Creation Process: + - Extracts fabric name from child configuration + - Removes fabric name from config (used as task key) + - Inherits VRF name and deploy settings from parent VRF + - Groups multiple VRFs by child fabric for batch processing + - Maintains VRF list for readiness checking + + Task Grouping Logic: + - Child tasks are grouped by fabric name for efficiency + - Multiple VRFs for same child fabric are batched together + - Each child fabric gets one task with multiple VRF configurations + - VRF names tracked separately for status monitoring + + Configuration Inheritance: + - VRF name: Inherited from parent VRF configuration + - Deploy flag: Inherited from parent if specified + - State: Inherited from parent module arguments + - Child-specific parameters: From child_config block + + Args: + parent_vrf (dict): Parent VRF configuration containing VRF name and settings + child_config (dict): Child fabric configuration with fabric name and parameters + parent_module_args (dict): Original module arguments for state inheritance + child_tasks_dict (dict): Existing child tasks dictionary for accumulation + + Returns: + dict: Updated child tasks dictionary with structure: + { + "child_fabric_name": { + "fabric": "child_fabric_name", + "state": "merged|replaced|overridden", + "config": [list of VRF configs for this fabric], + "vrf_list": [list of VRF names for readiness checking] + } + } + + Raises: + Exception: On task creation failures with context preservation + """ + try: + child_config = copy.deepcopy(child_config) + # Extract and remove fabric name from child configuration + child_fabric_name = child_config["fabric"] + del child_config["fabric"] + + # Inherit VRF context from parent configuration + child_config["vrf_name"] = parent_vrf["vrf_name"] + + # Inherit deploy setting from parent only + if "deploy" in child_config: + del child_config["deploy"] + if "deploy" in parent_vrf: + child_config["deploy"] = parent_vrf["deploy"] + + # Check if child fabric already has tasks (for grouping multiple VRFs) + if child_tasks_dict.get(child_fabric_name): + # Append to existing child fabric task + child_tasks_dict[child_fabric_name]["config"].append(child_config) + child_tasks_dict[child_fabric_name]["vrf_list"].append(child_config["vrf_name"]) + else: + # Create new child fabric task + child_task = { + "fabric": child_fabric_name, + "state": parent_module_args.get("state"), + "config": [child_config], + "vrf_list": [child_config["vrf_name"]] + } + child_tasks_dict[child_fabric_name] = child_task + + # Log task creation progress + self.logger.debug(f"Created child task for VRF: {child_config['vrf_name']}", fabric=child_fabric_name, + operation="create_child_task") + return child_tasks_dict + except Exception as e: + raise e + + def execute_child_task(self, child_task, task_vars, tmp): + """ + Execute child fabric VRF operations using specialized Child Multisite workflow. + + This method handles the execution of VRF operations on child fabrics within + an Multisite environment. It adapts parent module arguments for child fabric + execution, applies state transformations, and maintains proper context + for child fabric operations while ensuring Multisite operational consistency. + + Child Multisite Execution Model: + - Child fabrics operate with restricted parameter sets + - State transformations applied (overridden -> replaced) + - Child-specific VRF parameters applied from parent orchestration + - Independent execution context with parent-derived configurations + + State Handling: + - overridden: Transformed to 'replaced' for child fabric compatibility + - merged/replaced: Passed through unchanged + - deleted: Should be prevented at validation stage + + Module Argument Adaptation: + - fabric: Set to child fabric name + - config: Child-specific VRF configurations + - state: Transformed as needed for child fabric + - _fabric_type: Set to "multisite_child" for module behavior + + Result Enhancement: + - Adds child_fabric identifier to result + - Includes invocation details for debugging + - Preserves all standard dcnm_vrf result data + - Maintains error context for troubleshooting + + Args: + child_task (dict): Child fabric task containing: + - fabric (str): Child fabric name + - config (list): VRF configurations for child fabric + - state (str): Operation state + - vrf_list (list): VRF names for tracking + task_vars (dict): Ansible task variables for execution context + + Returns: + dict: Child fabric execution result containing: + - Standard dcnm_vrf module results (changed, failed, diff, response) + - child_fabric (str): Child fabric identifier + - invocation (dict): Module arguments used for execution + - Error details if execution failed + """ + # Extract fabric name for logging and result context + fabric_name = child_task["fabric"] + + # Log child task execution initiation + self.logger.info(f"Executing child task for fabric: {fabric_name}", fabric=fabric_name, operation="execute_child_task") + + # Build child fabric module arguments + child_module_args = { + "fabric": fabric_name, + "config": child_task["config"], + "_fabric_type": "multisite_child" + } + + # Handle state transformations for child fabric compatibility + state = child_task.get("state") + if state: + if state == "overridden": + # Transform overridden to replaced for child fabrics + child_module_args["state"] = "replaced" + else: + # Pass through other states unchanged + child_module_args["state"] = state + + # Execute child fabric operations using base module + child_result = self.execute_module_with_args(child_module_args, task_vars, tmp) + + # Enhance result with child fabric context + child_result["child_fabric"] = fabric_name + child_result["invocation"] = { + "module_args": copy.deepcopy(child_module_args) + } + + # Log execution outcome + success = not child_result.get("failed", False) + self.logger.info(f"Child task execution completed: {'Success' if success else 'Failed'}", + fabric=fabric_name, operation="execute_child_task") + return child_result + + # ========================================================================= + # UTILITY & HELPER METHODS + # ========================================================================= + + def execute_module_with_args(self, module_args, task_vars, tmp): + """ + Execute the dcnm_vrf module with specified arguments and context. + + This method provides a wrapper for executing the base dcnm_vrf module + with custom arguments while preserving the original task context. + It temporarily replaces task arguments, executes the module, and + restores the original arguments to maintain task state integrity. + + Execution Flow: + - Preserves original task arguments + - Temporarily replaces with custom module arguments + - Executes base dcnm_vrf module functionality + - Adds invocation details to result for debugging + - Restores original task arguments + + Use Cases: + - Parent fabric VRF operations with modified configurations + - Child fabric VRF operations with derived configurations + - Module execution with dynamically generated parameters + Args: + module_args (dict): Custom module arguments to execute with + task_vars (dict): Ansible task variables for execution context + + Returns: + dict: Module execution result containing: + - Standard dcnm_vrf module results + - invocation: Module arguments used for execution + - Additional execution context and debugging information + """ + # Extract fabric name for logging context + fabric_name = module_args.get("fabric", "Unknown") + # Preserve original task arguments + original_args = self._task.args + + try: + # Log module execution initiation + self.logger.debug("Executing NDFC VRF module", fabric=fabric_name, operation="execute_module") + # Temporarily replace task arguments with custom ones + self._task.args = module_args + # Execute base dcnm_vrf module with custom arguments + result = self._execute_module( + module_name="cisco.dcnm.dcnm_vrf", + module_args=module_args, + task_vars=task_vars, + tmp=tmp + ) + + # Add invocation details for debugging and audit trail + result["invocation"] = { + "module_args": copy.deepcopy(module_args) + } + + # Log execution outcome + success = not result.get("failed", False) + self.logger.debug(f"Module execution completed: {'Success' if success else 'Failed'}", + fabric=fabric_name, operation="execute_module") + return result + + except Exception as e: + # Handle module execution failures + raise e + finally: + # Always restore original task arguments + self._task.args = original_args + + def wait_for_vrf_ready(self, vrf_list, fabric_name, task_vars, tmp): + """ + Wait for VRFs to reach a deployable state on the specified fabric. + + This method monitors VRF status on child fabrics to ensure they are in + appropriate states before child fabric operations proceed. It implements + a polling mechanism with configurable retry logic and handles various + VRF states that may occur during parent-to-child fabric propagation. + VRF State Monitoring: + - DEPLOYED: VRF is fully deployed and ready for operations + - PENDING: VRF deployment is in progress, acceptable for operations + - NA: VRF state not applicable, typically ready for operations + - OUT-OF-SYNC: VRF configuration drift, handled with retry logic + + Polling Algorithm: + - Queries NDFC REST API for current VRF status on fabric + - Removes VRFs from wait list as they reach ready states + - Implements exponential backoff with configurable wait times + - Handles persistent OUT-OF-SYNC states with tolerance logic + + Retry Logic: + - Maximum retry count based on MAX_RETRY_COUNT and WAIT_TIME_FOR_DELETE_LOOP + - OUT-OF-SYNC VRFs get additional chances before removal + - Persistent wait list items eventually timeout and fail + - Progressive logging of wait status and remaining retries + + State Transition Handling: + - Newly created VRFs may initially show as non-existent + - Parent fabric changes propagate to child fabrics over time + - Configuration drift scenarios handled with retry tolerance + - Network connectivity issues accommodated with retry logic + + Args: + vrf_list (list): List of VRF names to monitor for readiness + fabric_name (str): Child fabric name where VRFs should be ready + task_vars (dict): Ansible task variables for API calls + tmp (str): Temporary directory path for operations + + Returns: + tuple: (all_ready, remaining_vrfs) + - all_ready (bool): True if all VRFs reached ready state + - remaining_vrfs (list): VRF names still not ready (if any) + + Raises: + Exception: On API failures or critical polling errors + """ + # Handle empty VRF list case + if not vrf_list: + return True, None + + # Initialize retry tracking and OUT-OF-SYNC handling + vrf_oos_list = [] + retry_count = max(MAX_RETRY_COUNT // WAIT_TIME_FOR_DELETE_LOOP, 1) + + # Log readiness monitoring initiation + self.logger.info(f"Waiting for VRF(s) to be ready: {', '.join(vrf_list)}", + fabric=fabric_name, operation="wait_for_vrf_ready") + + # Continue polling while VRFs remain and retries available + while retry_count > 0 and vrf_list: + try: + # Query NDFC for current VRF status on fabric + resp = self._execute_module( + module_name="cisco.dcnm.dcnm_rest", + module_args={ + "method": "GET", + "path": f"/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{fabric_name}/vrfs", + }, + task_vars=task_vars, + tmp=tmp + ) + + # Validate API response and extract VRF data + response_data = self.error_handler.validate_api_response(resp, "VRF status check", fabric_name) + vrf_data = response_data.get("DATA", []) + + # Process each VRF in the fabric response + for vrf in vrf_data: + vrf_name = vrf.get("vrfName") + if vrf_name in vrf_list: + vrf_status = vrf.get("vrfStatus") + + # Check if VRF is in ready state + if vrf_status in VALID_VRF_STATES: + # VRF is ready, remove from wait list + vrf_list.remove(vrf_name) + self.logger.debug(f"VRF {vrf_name} is ready (status: {vrf_status})", + fabric=fabric_name, operation="wait_for_vrf_ready") + elif vrf_status == "OUT-OF-SYNC": + # Handle OUT-OF-SYNC state with tolerance + if vrf not in vrf_oos_list: + # First time seeing this VRF as OUT-OF-SYNC + vrf_oos_list.append(vrf) + self.logger.debug(f"VRF {vrf_name} is OUT-OF-SYNC", + fabric=fabric_name, operation="wait_for_vrf_ready") + else: + # VRF has been OUT-OF-SYNC before, remove from wait list + vrf_list.remove(vrf_name) + self.logger.debug(f"VRF {vrf_name} removed after persistent OUT-OF-SYNC", + fabric=fabric_name, operation="wait_for_vrf_ready") + + # If VRFs still waiting, sleep and retry + if vrf_list: + time.sleep(WAIT_TIME_FOR_DELETE_LOOP) + retry_count -= 1 + self.logger.debug(f"VRF(s) still not ready: {', '.join(vrf_list)}, retries left: {retry_count}", + fabric=fabric_name, operation="wait_for_vrf_ready") + + except Exception as e: + # Log API or processing errors + self.logger.error(f"VRF readiness check failed: {str(e)}", + fabric=fabric_name, operation="wait_for_vrf_ready") + raise e + + # Determine final outcome + if vrf_list: + # Some VRFs never reached ready state + self.logger.warning(f"VRF(s) not ready after maximum retries: {', '.join(vrf_list)}", + fabric=fabric_name, operation="wait_for_vrf_ready") + return False, vrf_list + else: + # All VRFs reached ready state + self.logger.info("All VRFs are ready", fabric=fabric_name, operation="wait_for_vrf_ready") + return True, None + + def create_structured_results(self, parent_result, child_results, parent_fabric): + """ + Create structured results combining parent and child fabric operations. + + This method aggregates execution results from Multi-Site Domain (Multisite) + operations to create a unified response structure that clearly separates + parent fabric outcomes from child fabric orchestration results. It + provides consistent output format for both simple parent-only operations + and complex parent-with-children workflows. + + Result Structure Design: + - Multisite Parent operations: Primary fabric configuration changes + - Child fabric operations: Secondary orchestrated operations + - Combined status: Overall operation success/failure indicators + - Workflow metadata: Operation type and fabric relationship context + + Parent-Only Workflow: + - Simple augmentation of parent result with workflow metadata + - Fabric type marked as "multisite_parent" for identification + - Workflow description indicates no child processing occurred + - All original parent result data preserved unchanged + + Parent-with-Children Workflow: + - Comprehensive structure separating parent and child results + - Parent fabric section with original operation outcomes + - Child fabrics array with individual fabric results + - Aggregated changed/failed status across all fabrics + - Detailed failure messaging for child fabric issues + + Status Aggregation Logic: + - changed: True if parent OR any child fabric changed + - failed: True if parent OR any child fabric failed + - Child failures include detailed error messaging + - Parent failures preserved from original result + + Args: + parent_result (dict): Result from parent fabric operations + Expected keys: changed, failed, diff, response, msg + child_results (list): List of child fabric operation results + Each item expected keys: child_fabric, changed, failed, diff, response, msg + parent_fabric (str): Name of the Multisite Parent fabric for context + + Returns: + dict: Structured result with parent/child separation + For parent-only operations: + { + 'changed': bool, + 'failed': bool, + 'fabric_type': 'multisite_parent', + 'workflow': 'Multisite Parent without Child Fabric Processing', + ... (original parent_result fields) + } + + For parent-with-children operations: + { + 'changed': bool, # Aggregated across all fabrics + 'failed': bool, # Aggregated across all fabrics + 'fabric_type': 'multisite_parent', + 'workflow': 'Multisite Parent with Child Fabric Processing', + 'parent_fabric': { + 'fabric': str, + 'changed': bool, + 'diff': list, + 'response': list + }, + 'child_fabrics': [ + { + 'fabric': str, + 'changed': bool, + 'failed': bool, + 'diff': list, + 'response': list + }, ... + ], + 'msg': str # Present if any child fabric failed + } + + Raises: + Exception: On critical result processing errors (handled internally) + """ + # Log structured result creation initiation + self.logger.debug("Creating structured results", fabric=parent_fabric, operation="create_structured_results") + + try: + # Determine workflow type based on child results presence + if child_results: + # Parent-with-children workflow: Create comprehensive structure + structured_result = { + "changed": parent_result.get("changed", False), + "failed": parent_result.get("failed", False), + "fabric_type": "multisite_parent", + "workflow": "Multisite Parent with Child Fabric Processing", + "parent_fabric": { + "fabric": parent_fabric, + "invocation": parent_result.get("invocation"), + "changed": parent_result.get("changed", False), + "diff": parent_result.get("diff", []), + "response": parent_result.get("response", []) + }, + "child_fabrics": [] + } + + # Process each child fabric result + for child_result in child_results: + # Create child fabric entry with all relevant data + child_entry = { + "fabric": child_result.get("child_fabric"), + "invocation": child_result.get("invocation"), + "changed": child_result.get("changed", False), + "failed": child_result.get("failed", False), + "diff": child_result.get("diff", []), + "response": child_result.get("response", []) + } + structured_result["child_fabrics"].append(child_entry) + + # Aggregate child changed status into overall result + if child_result.get("changed", False): + structured_result["changed"] = True + + # Aggregate child failed status and capture error details + if child_result.get("failed", False): + structured_result["failed"] = True + structured_result["msg"] = ( + f"Child fabric task failed for {child_result.get('child_fabric')}: " + f"{child_result.get('msg', 'Unknown error')}" + ) + + return structured_result + else: + # Parent-only workflow: Augment original result with metadata + parent_result["workflow"] = "Multisite Parent without Child Fabric Processing" + return parent_result - if ( - self._task.args.get("state") == "merged" - or self._task.args.get("state") == "overridden" - or self._task.args.get("state") == "replaced" - ): - for con in self._task.args["config"]: - if "attach" in con: - for at in con["attach"]: - if "vlan_id" in at: - msg = "Playbook parameter vlan_id should not be specified under the attach: block. Please specify this under the config: block instead" # noqa - return {"failed": True, "msg": msg} - if "vrf_lite" in at: - try: - for vl in at["vrf_lite"]: - continue - except TypeError: - msg = "Please specify interface parameter under vrf_lite section in the playbook" - return {"failed": True, "msg": msg} - - self.result = super(ActionModule, self).run(task_vars=task_vars) - return self.result + except Exception as e: + # Handle result structuring errors + self.logger.error(f"Failed to create structured results: {str(e)}", + fabric=parent_fabric, operation="create_structured_results") + raise e diff --git a/plugins/modules/dcnm_network.py b/plugins/modules/dcnm_network.py index 5307dcfbe..437508b1c 100644 --- a/plugins/modules/dcnm_network.py +++ b/plugins/modules/dcnm_network.py @@ -21,11 +21,12 @@ DOCUMENTATION = """ --- module: dcnm_network -short_description: Add and remove Networks from a DCNM managed VXLAN fabric. +short_description: Add and remove Networks from a ND managed VXLAN fabric. version_added: "0.9.0" description: - - "Add and remove Networks from a DCNM managed VXLAN fabric." - - "In Multisite fabrics, Networks can be created only on Multisite fabric" + - "Add and remove Networks from a ND managed VXLAN fabric." + - "For multisite (MSD) fabrics, child fabric configurations can be specified using the child_fabric_config parameter" + - "The attribute _fabric_type (standalone, multisite_parent, multisite_child) is automatically detected and should not be manually specified by the user" author: Chris Van Heuveln(@chrisvanheuveln), Shrishail Kariyappanavar(@nkshrishail) Praveen Ramoorthy(@praveenramoorthy) options: fabric: @@ -33,9 +34,19 @@ - Name of the target fabric for network operations type: str required: yes + _fabric_type: + description: + - INTERNAL PARAMETER - DO NOT USE + - Fabric type is automatically detected by the module using fabric associations API + - Valid values are 'standalone', 'multisite_parent', 'multisite_child' but should never be manually specified + - This parameter is used internally by the action plugin for MSD fabric processing + type: str + required: false + default: standalone + choices: ['multisite_child', 'standalone', 'multisite_parent'] state: description: - - The state of DCNM after module completion. + - The state of ND after module completion. type: str choices: - merged @@ -64,7 +75,7 @@ net_id: description: - ID of the network being managed - - If not specified in the playbook, DCNM will auto-select an available net_id + - If not specified in the playbook, ND will auto-select an available net_id type: int required: false net_template: @@ -80,7 +91,7 @@ vlan_id: description: - VLAN ID for the network. - - If not specified in the playbook, DCNM will auto-select an available vlan_id + - If not specified in the playbook, ND will auto-select an available vlan_id type: int required: false routing_tag: @@ -129,31 +140,37 @@ dhcp_srvr1_ip: description: - DHCP relay IP address of the first DHCP server + - Not applicable at Multisite parent fabric level type: str required: false dhcp_srvr1_vrf: description: - VRF ID of first DHCP server + - Not applicable at Multisite parent fabric level type: str required: false dhcp_srvr2_ip: description: - DHCP relay IP address of the second DHCP server + - Not applicable at Multisite parent fabric level type: str required: false dhcp_srvr2_vrf: description: - VRF ID of second DHCP server + - Not applicable at Multisite parent fabric level type: str required: false dhcp_srvr3_ip: description: - DHCP relay IP address of the third DHCP server + - Not applicable at Multisite parent fabric level type: str required: false dhcp_srvr3_vrf: description: - VRF ID of third DHCP server + - Not applicable at Multisite parent fabric level type: str required: false dhcp_servers: @@ -163,7 +180,8 @@ dhcp_srvr3_ip, dhcp_srvr3_vrf - If both dhcp_servers and any of dhcp_srvr1_ip, dhcp_srvr1_vrf, dhcp_srvr2_ip, dhcp_srvr2_vrf, dhcp_srvr3_ip, dhcp_srvr3_vrf are specified an error message is generated - indicating these are mutually exclusive options + indicating these are mutually exclusive options. Max of 16 servers can be specified. + - Not applicable at Multisite parent fabric level type: list elements: dict required: false @@ -171,11 +189,13 @@ description: - Loopback ID for DHCP Relay interface - Configured ID value should be in range 0-1023 + - Not applicable at Multisite parent fabric level type: int required: false multicast_group_address: description: - The multicast IP address for the network + - Not applicable at Multisite parent fabric level type: str required: false gw_ipv6_subnet: @@ -206,6 +226,7 @@ trm_enable: description: - Enable Tenant Routed Multicast + - Not applicable at Multisite parent fabric level type: bool required: false default: false @@ -218,6 +239,7 @@ l3gw_on_border: description: - Enable L3 Gateway on Border + - Not applicable at Multisite parent fabric level type: bool required: false default: false @@ -226,6 +248,7 @@ - Enable Netflow - Netflow is supported only if it is enabled on fabric - Netflow configs are supported on NDFC only + - Not applicable at Multisite parent fabric level type: bool required: false default: false @@ -241,6 +264,7 @@ - Vlan Netflow Monitor - Provide monitor name defined in fabric setting for Layer 3 Record - Netflow configs are supported on NDFC only + - Not applicable at Multisite parent fabric level type: str required: false attach: @@ -290,13 +314,77 @@ description: - Global knob to control whether to deploy the attachment - Ansible NDFC Collection Behavior for Version 2.0.1 and earlier - - This knob will create and deploy the attachment in DCNM only when set to "True" in playbook + - This knob will create and deploy the attachment in ND only when set to "True" in playbook - Ansible NDFC Collection Behavior for Version 2.1.0 and later - Attachments specified in the playbook will always be created in DCNM. This knob, when set to "True", will deploy the attachment in DCNM, by pushing the configs to switch. If set to "False", the attachments will be created in DCNM, but will not be deployed + - Defaults to true. For MSD parent fabrics, this value is copied to child fabrics unless overridden at child level type: bool default: true + child_fabric_config: + description: + - List of child fabric configurations for MSD (Multi-Site Domain) parent fabrics + - Only valid when the fabric is an MSD parent fabric + - Child fabric configurations cannot contain 'attach' parameter - attachments are managed at parent level only + - Child-specific parameters like dhcp_loopback_id, l3gw_on_border, netflow_enable, etc. can be specified per child + - Deploy setting defaults to parent's deploy value but can be overridden per child fabric + type: list + elements: dict + required: false + suboptions: + fabric: + description: + - Name of the child fabric + - Child fabric must be a member of the specified MSD parent fabric + type: str + required: true + deploy: + description: + - Override deploy setting for this child fabric + - If not specified, inherits the deploy value from parent fabric configuration + type: bool + required: false + dhcp_loopback_id: + description: + - Child-specific Loopback ID for DHCP Relay interface + - Configured ID value should be in range 0-1023 + type: int + required: false + l3gw_on_border: + description: + - Child-specific Enable L3 Gateway on Border setting + type: bool + required: false + netflow_enable: + description: + - Child-specific Enable Netflow setting + - Netflow is supported only if it is enabled on fabric + - Netflow configs are supported on NDFC only + type: bool + required: false + multicast_group_address: + description: + - Child-specific multicast IP address for the network + type: str + required: false + vlan_nf_monitor: + description: + - Child-specific Vlan Netflow Monitor + - Provide monitor name defined in fabric setting for Layer 3 Record + - Netflow configs are supported on NDFC only + type: str + required: false + dhcp_srvr1_ip: + description: + - Child-specific DHCP relay IP address of the first DHCP server + type: str + required: false + dhcp_srvr1_vrf: + description: + - Child-specific VRF ID of first DHCP server + type: str + required: false """ EXAMPLES = """ @@ -329,10 +417,25 @@ # # Deleted: # Networks defined in the playbook will be deleted. -# If no Networks are provided in the playbook, all Networks present on that DCNM fabric will be deleted. +# If no Networks are provided in the playbook, all Networks present on that ND fabric will be deleted. # # Query: -# Returns the current DCNM state for the Networks listed in the playbook. +# Returns the current ND state for the Networks listed in the playbook. +# +# MSD (Multi-Site Domain) Fabric Support: +# - The module automatically detects fabric type (standalone, multisite_parent, multisite_child) using fabric associations API +# - For MSD parent fabrics, use child_fabric_config to specify child-specific network parameters +# - Child fabric configurations inherit deploy setting from parent unless explicitly overridden +# - Attachments (attach parameter) can only be specified at parent fabric level, not in child_fabric_config +# - When parent state is 'overridden', child fabrics use 'replaced' state (never 'overridden') +# - Deploy defaults to true for both parent and child configurations + +# =========================================================================== +# Standalone Fabric Examples +# =========================================================================== +# --------------------------------------------------------------------------- +# STATE: MERGED - Merge Network Configuration +# --------------------------------------------------------------------------- - name: Merge networks cisco.dcnm.dcnm_network: @@ -369,6 +472,10 @@ ports: [Ethernet1/11, Ethernet1/12] deploy: false +# --------------------------------------------------------------------------- +# STATE: REPLACED - Replace Network Configuration +# --------------------------------------------------------------------------- + - name: Replace networks cisco.dcnm.dcnm_network: fabric: vxlan-fabric @@ -426,6 +533,10 @@ # ports: [Ethernet1/11, Ethernet1/12] # deploy: false +# --------------------------------------------------------------------------- +# STATE: OVERRIDDEN - Override all Networks +# --------------------------------------------------------------------------- + - name: Override networks cisco.dcnm.dcnm_network: fabric: vxlan-fabric @@ -462,6 +573,10 @@ # ports: [Ethernet1/11, Ethernet1/12] # deploy: false +# --------------------------------------------------------------------------- +# STATE: DELETED - Delete Networks +# --------------------------------------------------------------------------- + - name: Delete selected networks cisco.dcnm.dcnm_network: fabric: vxlan-fabric @@ -488,6 +603,10 @@ fabric: vxlan-fabric state: deleted +# --------------------------------------------------------------------------- +# STATE: QUERY - Query Networks +# --------------------------------------------------------------------------- + - name: Query Networks cisco.dcnm.dcnm_network: fabric: vxlan-fabric @@ -495,6 +614,266 @@ config: - net_name: ansible-net13 - net_name: ansible-net12 + +# =========================================================================== +# MSD (Multi-Site Domain) Fabric Examples +# =========================================================================== + +# Note: The module automatically detects fabric type using fabric associations API. + +# --------------------------------------------------------------------------- +# STATE: MERGED - Create/Update Networks on Parent and Child Fabrics +# --------------------------------------------------------------------------- + +- name: MSD MERGE | Create a Network on Parent and extend to Child fabrics + cisco.dcnm.dcnm_network: + fabric: vxlan-parent-fabric # Must be the Parent MSD fabric + state: merged + config: + - net_name: ansible-net-msd-1 + vrf_name: Tenant-1 + net_id: 130001 + vlan_id: 2301 + net_template: Default_Network_Universal + net_extension_template: Default_Network_Extension_Universal + gw_ip_subnet: '192.168.12.1/24' + routing_tag: 1234 + # Attachments are for switches at the Parent fabric + attach: + - ip_address: 192.168.10.203 + ports: [Ethernet1/13, Ethernet1/14] + - ip_address: 192.168.10.204 + ports: [Ethernet1/13, Ethernet1/14] + # Define how this Network behaves on each Child fabric + child_fabric_config: + - fabric: vxlan-child-fabric1 + l3gw_on_border: true + dhcp_loopback_id: 204 + multicast_group_address: '239.1.1.1' + - fabric: vxlan-child-fabric2 + l3gw_on_border: false + dhcp_loopback_id: 205 + deploy: true + - net_name: ansible-net-msd-2 # A second Network in the same task + vrf_name: Tenant-2 + net_id: 130002 + vlan_id: 2302 + gw_ip_subnet: '192.168.13.1/24' + child_fabric_config: + - fabric: vxlan-child-fabric1 + netflow_enable: false + # Attachments are for switches at the Parent fabric + attach: + - ip_address: 192.168.10.203 + ports: [Ethernet1/15, Ethernet1/16] + - ip_address: 192.168.10.204 + ports: [Ethernet1/15, Ethernet1/16] + +- name: MSD MERGE | Create Network with advanced DHCP and multicast settings + cisco.dcnm.dcnm_network: + fabric: vxlan-parent-fabric + state: merged + config: + - net_name: ansible-net-advanced + vrf_name: Tenant-1 + net_id: 130010 + vlan_id: 2310 + vlan_name: advanced_network_vlan2310 + gw_ip_subnet: '192.168.20.1/24' + int_desc: "Advanced Network Configuration" + mtu_l3intf: 9216 + arp_suppress: true + route_target_both: true + # Parent-specific DHCP settings + dhcp_servers: + - srvr_ip: 192.168.1.1 + srvr_vrf: management + - srvr_ip: 192.168.1.2 + srvr_vrf: management + # Child fabric configuration with different settings per child + child_fabric_config: + - fabric: vxlan-child-fabric1 + multicast_group_address: '239.2.1.1' + dhcp_loopback_id: 210 + dhcp_srvr1_ip: '10.1.1.10' + dhcp_srvr1_vrf: 'management' + - fabric: vxlan-child-fabric2 + multicast_group_address: '239.2.2.1' + l3gw_on_border: true + deploy: false # Override parent deploy setting + attach: + - ip_address: 192.168.10.203 + ports: [Ethernet1/17, Ethernet1/18] + - ip_address: 192.168.10.204 + ports: [Ethernet1/17, Ethernet1/18] + deploy: true # Parent deploy setting, inherited by children unless overridden + +# --------------------------------------------------------------------------- +# STATE: REPLACED - Replace Network configuration on Parent and Child Fabrics +# --------------------------------------------------------------------------- + +- name: MSD REPLACE | Update Network properties on Parent and Child fabrics + cisco.dcnm.dcnm_network: + fabric: vxlan-parent-fabric + state: replaced + config: + - net_name: ansible-net-msd-1 + vrf_name: Tenant-1 + net_id: 130001 + net_template: Default_Network_Universal + net_extension_template: Default_Network_Extension_Universal + vlan_id: 2301 + gw_ip_subnet: '192.168.12.1/24' + mtu_l3intf: 9000 # Update MTU on Parent + # Child fabric configs are replaced: child1 is updated + child_fabric_config: + - fabric: vxlan-child-fabric1 + l3gw_on_border: false # Value is updated + dhcp_loopback_id: 205 # Value is updated + attach: + - ip_address: 192.168.10.203 + # Delete this attachment + # - ip_address: 192.168.10.204 + # Create the following attachment + - ip_address: 192.168.10.205 + ports: [Ethernet1/13, Ethernet1/14] + # Dont touch this if its present on ND + # - net_name: ansible-net-msd-2 + # vrf_name: Tenant-2 + # net_id: 130002 + # net_template: Default_Network_Universal + # net_extension_template: Default_Network_Extension_Universal + # attach: + # - ip_address: 192.168.10.203 + # ports: [Ethernet1/15, Ethernet1/16] + # - ip_address: 192.168.10.204 + # ports: [Ethernet1/15, Ethernet1/16] + +- name: MSD REPLACE | Update Network with netflow configuration + cisco.dcnm.dcnm_network: + fabric: vxlan-parent-fabric + state: replaced + config: + - net_name: ansible-net-advanced + vrf_name: Tenant-1 + net_id: 130010 + vlan_id: 2310 + gw_ip_subnet: '192.168.20.1/24' + # Parent settings + arp_suppress: false # Updated value + # Child fabric configuration updates + child_fabric_config: + - fabric: vxlan-child-fabric1 + netflow_enable: true + vlan_nf_monitor: NETFLOW_MONITOR_2 # Updated monitor + multicast_group_address: '239.2.1.2' # Updated address + +# --------------------------------------------------------------------------- +# STATE: OVERRIDDEN - Override all Networks on Parent and Child Fabrics +# --------------------------------------------------------------------------- + +- name: MSD OVERRIDE | Override all Networks ensuring only specified ones exist + cisco.dcnm.dcnm_network: + fabric: vxlan-parent-fabric + state: overridden + config: + - net_name: ansible-net-production + vrf_name: Tenant-Production + net_id: 140001 + vlan_id: 3001 + gw_ip_subnet: '172.16.1.1/24' + int_desc: "Production Network for critical workloads" + child_fabric_config: + - fabric: vxlan-child-fabric1 + l3gw_on_border: true + netflow_enable: true + - fabric: vxlan-child-fabric2 + l3gw_on_border: true + netflow_enable: true + attach: + - ip_address: 192.168.10.203 + ports: [Ethernet1/19, Ethernet1/20] + - ip_address: 192.168.10.204 + ports: [Ethernet1/19, Ethernet1/20] + deploy: true + # All other Networks will be deleted from both parent and child fabrics + +# --------------------------------------------------------------------------- +# STATE: DELETED - Delete Networks from Parent and all Child Fabrics +# --------------------------------------------------------------------------- + +- name: MSD DELETE | Delete a Network from the Parent and all associated Child fabrics + cisco.dcnm.dcnm_network: + fabric: vxlan-parent-fabric + state: deleted + config: + - net_name: ansible-net-msd-1 + # The 'child_fabric_config' parameter is ignored for 'deleted' state. + +- name: MSD DELETE | Delete multiple Networks from Parent and Child fabrics + cisco.dcnm.dcnm_network: + fabric: vxlan-parent-fabric + state: deleted + config: + - net_name: ansible-net-msd-1 + - net_name: ansible-net-msd-2 + - net_name: ansible-net-advanced + +- name: MSD DELETE | Delete all Networks from the Parent and all associated Child fabrics + cisco.dcnm.dcnm_network: + fabric: vxlan-parent-fabric + state: deleted + +# --------------------------------------------------------------------------- +# STATE: QUERY - Query Networks +# --------------------------------------------------------------------------- + +- name: MSD QUERY | Query specific Networks on the Parent MSD fabric + cisco.dcnm.dcnm_network: + fabric: vxlan-parent-fabric + state: query + config: + - net_name: ansible-net-msd-1 + - net_name: ansible-net-msd-2 + # The query will return the Network's configuration on the parent + # and its attachments on all associated child fabrics. + +- name: MSD QUERY | Query all Networks on the Parent MSD fabric + cisco.dcnm.dcnm_network: + fabric: vxlan-parent-fabric + state: query + # No config specified - returns all Networks + +- name: MSD QUERY | Query specific Networks on the Child MSD fabric + cisco.dcnm.dcnm_network: + fabric: vxlan-child-fabric1 + state: query + config: + - net_name: ansible-net-msd-1 + - net_name: ansible-net-msd-2 + # The query will return the Network's configuration on the child + # and its attachments. + +- name: MSD QUERY | Query all Networks on the Child MSD fabric + cisco.dcnm.dcnm_network: + fabric: vxlan-child-fabric1 + state: query + # No config specified - returns all Networks on the child. + +- name: MSD QUERY | Query specific Networks on Parent & Child fabric + cisco.dcnm.dcnm_network: + fabric: vxlan-parent-fabric + state: query + config: + - net_name: ansible-net-msd-1 + child_fabric_config: + - fabric: vxlan-child-fabric1 + - net_name: ansible-net-msd-2 + child_fabric_config: + - fabric: vxlan-child-fabric2 + # The query will return the Network's configuration on the parent and the + # configuration on the specified childs and its attachments at + # the parent and child level respectively. """ import copy @@ -529,7 +908,7 @@ class DcnmNetwork: "GET_NET_NAME": "/rest/top-down/fabrics/{}/networks/{}", "GET_VLAN": "/rest/resource-manager/vlan/{}?vlanUsageType=TOP_DOWN_NETWORK_VLAN", "GET_NET_STATUS": "/rest/top-down/fabrics/{}/networks/{}/status", - "GET_NET_SWITCH_DEPLOY": "/rest/top-down/fabrics/networks/deploy" + "GET_NET_SWITCH_DEPLOY": "/rest/top-down/fabrics/networks/deploy", }, 12: { "GET_VRF": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{}/vrfs", @@ -540,7 +919,7 @@ class DcnmNetwork: "GET_NET_NAME": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{}/networks/{}", "GET_VLAN": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/resource-manager/vlan/{}?vlanUsageType=TOP_DOWN_NETWORK_VLAN", "GET_NET_STATUS": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{}/networks/{}/status", - "GET_NET_SWITCH_DEPLOY": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/networks/deploy" + "GET_NET_SWITCH_DEPLOY": "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/networks/deploy", }, } @@ -555,7 +934,7 @@ def __init__(self, module): self.diff_create = [] self.diff_create_update = [] # This variable is created specifically to hold all the create payloads which are missing a - # networkId. These payloads are sent to DCNM out of band (basically in the get_diff_merge()) + # networkId. These payloads are sent to ND out of band (basically in the get_diff_merge()) # We lose diffs for these without this variable. The content stored here will be helpful for # cases like "check_mode" and to print diffs[] in the output of each task. self.diff_create_quick = [] @@ -576,6 +955,7 @@ def __init__(self, module): self.diff_delete = {} self.diff_input_format = [] self.query = [] + self.deployment_states = {} self.dcnm_version = dcnm_version_supported(self.module) self.inventory_data = get_fabric_inventory_details(self.module, self.fabric) self.ip_sn, self.hn_sn = get_ip_sn_dict(self.inventory_data) @@ -587,6 +967,12 @@ def __init__(self, module): else: self.paths = self.dcnm_network_paths[self.dcnm_version] + # Get fabric type from parameter (set by action plugin) + self.fabric_type = module.params.get("_fabric_type") + + if self.fabric_type is None: + self.module.fail_json(msg="Could not determine fabric type. Please set fabric_type in the playbook") + self.check_extra_params = True self.result = dict(changed=False, diff=[], response=[], warnings=[]) @@ -716,7 +1102,7 @@ def diff_for_attach_deploy(self, want_a, have_a, replace=False): # This is needed to handle cases where vlan is updated after deploying the network # and attachments. This ensures that the attachments before vlan update will use previous - # vlan id. All the active attachments on DCNM will have a vlan-id. + # vlan id. All the active attachments on ND will have a vlan-id. if have.get("vlan"): want["vlan"] = have.get("vlan") @@ -743,7 +1129,6 @@ def diff_for_attach_deploy(self, want_a, have_a, replace=False): attach_list.append(want) if bool(want["is_deploy"]): dep_net = True - continue if not atch_sw_ports: @@ -912,6 +1297,16 @@ def diff_for_create(self, want, have): if not have: return {} + # Get skipped attributes for parent fabrics + skipped_attributes = self.get_skipped_attributes() + template_mapping = self.get_template_config_mapping() + + # Convert skipped spec attributes to template config keys + skipped_template_keys = set() + for attr in skipped_attributes: + if attr in template_mapping: + skipped_template_keys.add(template_mapping[attr]) + gw_changed = False tg_changed = False create = {} @@ -958,7 +1353,7 @@ def diff_for_create(self, want, have): vlanId_want = json_to_dict_want.get("vlanId", "") vlanId_have = json_to_dict_have.get("vlanId") l2only_want = str(json_to_dict_want.get("isLayer2Only", "")).lower() - l2only_have = json_to_dict_have.get("isLayer2Only", "") + l2only_have = str(json_to_dict_have.get("isLayer2Only", "")).lower() vlanName_want = json_to_dict_want.get("vlanName", "") vlanName_have = json_to_dict_have.get("vlanName", "") intDesc_want = json_to_dict_want.get("intfDescription", "") @@ -966,7 +1361,8 @@ def diff_for_create(self, want, have): mtu_want = json_to_dict_want.get("mtu", "") mtu_have = json_to_dict_have.get("mtu", "") arpsup_want = str(json_to_dict_want.get("suppressArp", "")).lower() - arpsup_have = json_to_dict_have.get("suppressArp", "") + arpsup_have = str(json_to_dict_have.get("suppressArp", "")).lower() + dhcp1_ip_want = json_to_dict_want.get("dhcpServerAddr1", "") dhcp1_ip_want = json_to_dict_want.get("dhcpServerAddr1", "") dhcp1_ip_have = json_to_dict_have.get("dhcpServerAddr1", "") dhcp2_ip_want = json_to_dict_want.get("dhcpServerAddr2", "") @@ -981,8 +1377,8 @@ def diff_for_create(self, want, have): dhcp3_vrf_have = json_to_dict_have.get("vrfDhcp3", "") dhcp_servers_want = json_to_dict_want.get("dhcpServers", "") dhcp_servers_have = json_to_dict_have.get("dhcpServers", "") - dhcp_loopback_want = json_to_dict_want.get("loopbackId", "") - dhcp_loopback_have = json_to_dict_have.get("loopbackId", "") + dhcp_loopback_want = str(json_to_dict_want.get("loopbackId", "")) + dhcp_loopback_have = str(json_to_dict_have.get("loopbackId", "")) multicast_group_address_want = json_to_dict_want.get("mcastGroup", "") multicast_group_address_have = json_to_dict_have.get("mcastGroup", "") gw_ipv6_want = json_to_dict_want.get("gatewayIpV6Address", "") @@ -996,7 +1392,7 @@ def diff_for_create(self, want, have): secip_gw4_want = json_to_dict_want.get("secondaryGW4", "") secip_gw4_have = json_to_dict_have.get("secondaryGW4", "") trmen_want = str(json_to_dict_want.get("trmEnabled", "")).lower() - trmen_have = json_to_dict_have.get("trmEnabled", "") + trmen_have = str(json_to_dict_have.get("trmEnabled", "")).lower() rt_both_want = str(json_to_dict_want.get("rtBothAuto", "")).lower() rt_both_have = json_to_dict_have.get("rtBothAuto", "") l3gw_onbd_want = str(json_to_dict_want.get("enableL3OnBorder", "")).lower() @@ -1010,6 +1406,8 @@ def diff_for_create(self, want, have): if vlanId_have != "": vlanId_have = int(vlanId_have) + if vlanId_want != "": + vlanId_want = int(vlanId_want) tag_want = json_to_dict_want.get("tag", "") tag_have = json_to_dict_have.get("tag") if tag_have != "": @@ -1019,38 +1417,130 @@ def diff_for_create(self, want, have): if vlanId_want: - if ( - have["networkTemplate"] != want["networkTemplate"] - or have["networkExtensionTemplate"] != want["networkExtensionTemplate"] - or gw_ip_have != gw_ip_want - or vlanId_have != vlanId_want - or tag_have != tag_want - or l2only_have != l2only_want - or vlanName_have != vlanName_want - or intDesc_have != intDesc_want - or mtu_have != mtu_want - or arpsup_have != arpsup_want - or dhcp1_ip_have != dhcp1_ip_want - or dhcp2_ip_have != dhcp2_ip_want - or dhcp3_ip_have != dhcp3_ip_want - or dhcp1_vrf_have != dhcp1_vrf_want - or dhcp2_vrf_have != dhcp2_vrf_want - or dhcp3_vrf_have != dhcp3_vrf_want - or dhcp_servers_have != dhcp_servers_want - or dhcp_loopback_have != dhcp_loopback_want - or multicast_group_address_have != multicast_group_address_want - or gw_ipv6_have != gw_ipv6_want - or secip_gw1_have != secip_gw1_want - or secip_gw2_have != secip_gw2_want - or secip_gw3_have != secip_gw3_want - or secip_gw4_have != secip_gw4_want - or trmen_have != trmen_want - or rt_both_have != rt_both_want - or l3gw_onbd_have != l3gw_onbd_want - or nf_en_have != nf_en_want - or intvlan_nfen_have != intvlan_nfen_want - or vlan_nfen_have != vlan_nfen_want - ): + # Build comparison conditions, skipping those in skipped_template_keys + comparisons = [] + + # Always compare network templates + template_diff = have["networkTemplate"] != want["networkTemplate"] + comparisons.append(template_diff) + + ext_template_diff = have["networkExtensionTemplate"] != want["networkExtensionTemplate"] + comparisons.append(ext_template_diff) + + # Compare other attributes only if not skipped + if "gatewayIpAddress" not in skipped_template_keys: + gw_diff = gw_ip_have != gw_ip_want + comparisons.append(gw_diff) + + if "vlanId" not in skipped_template_keys: + vlan_diff = vlanId_have != vlanId_want + comparisons.append(vlan_diff) + + if "tag" not in skipped_template_keys: + tag_diff = tag_have != tag_want + comparisons.append(tag_diff) + + if "isLayer2Only" not in skipped_template_keys: + l2_diff = l2only_have != l2only_want + comparisons.append(l2_diff) + + if "vlanName" not in skipped_template_keys: + vname_diff = vlanName_have != vlanName_want + comparisons.append(vname_diff) + + if "intfDescription" not in skipped_template_keys: + intdesc_diff = intDesc_have != intDesc_want + comparisons.append(intdesc_diff) + + if "mtu" not in skipped_template_keys: + mtu_diff = mtu_have != mtu_want + comparisons.append(mtu_diff) + + if "suppressArp" not in skipped_template_keys: + arp_diff = arpsup_have != arpsup_want + comparisons.append(arp_diff) + + if "dhcpServerAddr1" not in skipped_template_keys: + dhcp1_diff = dhcp1_ip_have != dhcp1_ip_want + comparisons.append(dhcp1_diff) + + if "dhcpServerAddr2" not in skipped_template_keys: + dhcp2_diff = dhcp2_ip_have != dhcp2_ip_want + comparisons.append(dhcp2_diff) + + if "dhcpServerAddr3" not in skipped_template_keys: + dhcp3_diff = dhcp3_ip_have != dhcp3_ip_want + comparisons.append(dhcp3_diff) + + if "vrfDhcp" not in skipped_template_keys: + dhcp1vrf_diff = dhcp1_vrf_have != dhcp1_vrf_want + comparisons.append(dhcp1vrf_diff) + + if "vrfDhcp2" not in skipped_template_keys: + dhcp2vrf_diff = dhcp2_vrf_have != dhcp2_vrf_want + comparisons.append(dhcp2vrf_diff) + + if "vrfDhcp3" not in skipped_template_keys: + dhcp3vrf_diff = dhcp3_vrf_have != dhcp3_vrf_want + comparisons.append(dhcp3vrf_diff) + + if "dhcpServers" not in skipped_template_keys: + dhcp_servers_diff = dhcp_servers_have != dhcp_servers_want + comparisons.append(dhcp_servers_diff) + + if "loopbackId" not in skipped_template_keys: + loopback_diff = dhcp_loopback_have != dhcp_loopback_want + comparisons.append(loopback_diff) + + if "mcastGroup" not in skipped_template_keys: + mcast_diff = multicast_group_address_have != multicast_group_address_want + comparisons.append(mcast_diff) + + if "gatewayIpV6Address" not in skipped_template_keys: + gwv6_diff = gw_ipv6_have != gw_ipv6_want + comparisons.append(gwv6_diff) + + if "secondaryGW1" not in skipped_template_keys: + secgw1_diff = secip_gw1_have != secip_gw1_want + comparisons.append(secgw1_diff) + + if "secondaryGW2" not in skipped_template_keys: + secgw2_diff = secip_gw2_have != secip_gw2_want + comparisons.append(secgw2_diff) + + if "secondaryGW3" not in skipped_template_keys: + secgw3_diff = secip_gw3_have != secip_gw3_want + comparisons.append(secgw3_diff) + + if "secondaryGW4" not in skipped_template_keys: + secgw4_diff = secip_gw4_have != secip_gw4_want + comparisons.append(secgw4_diff) + + if "trmEnabled" not in skipped_template_keys: + trm_diff = trmen_have != trmen_want + comparisons.append(trm_diff) + + if "rtBothAuto" not in skipped_template_keys: + rt_diff = rt_both_have != rt_both_want + comparisons.append(rt_diff) + + if "enableL3OnBorder" not in skipped_template_keys: + l3border_diff = l3gw_onbd_have != l3gw_onbd_want + comparisons.append(l3border_diff) + + if "ENABLE_NETFLOW" not in skipped_template_keys: + nf_diff = nf_en_have != nf_en_want + comparisons.append(nf_diff) + + if "SVI_NETFLOW_MONITOR" not in skipped_template_keys: + svi_nf_diff = intvlan_nfen_have != intvlan_nfen_want + comparisons.append(svi_nf_diff) + + if "VLAN_NETFLOW_MONITOR" not in skipped_template_keys: + vlan_nf_diff = vlan_nfen_have != vlan_nfen_want + comparisons.append(vlan_nf_diff) + + if any(comparisons): # The network updates with missing networkId will have to use existing # networkId from the instance of the same network on DCNM. @@ -1118,37 +1608,130 @@ def diff_for_create(self, want, have): else: - if ( - have["networkTemplate"] != want["networkTemplate"] - or have["networkExtensionTemplate"] != want["networkExtensionTemplate"] - or gw_ip_have != gw_ip_want - or tag_have != tag_want - or l2only_have != l2only_want - or vlanName_have != vlanName_want - or intDesc_have != intDesc_want - or mtu_have != mtu_want - or arpsup_have != arpsup_want - or dhcp1_ip_have != dhcp1_ip_want - or dhcp2_ip_have != dhcp2_ip_want - or dhcp3_ip_have != dhcp3_ip_want - or dhcp1_vrf_have != dhcp1_vrf_want - or dhcp2_vrf_have != dhcp2_vrf_want - or dhcp3_vrf_have != dhcp3_vrf_want - or dhcp_servers_have != dhcp_servers_want - or dhcp_loopback_have != dhcp_loopback_want - or multicast_group_address_have != multicast_group_address_want - or gw_ipv6_have != gw_ipv6_want - or secip_gw1_have != secip_gw1_want - or secip_gw2_have != secip_gw2_want - or secip_gw3_have != secip_gw3_want - or secip_gw4_have != secip_gw4_want - or trmen_have != trmen_want - or rt_both_have != rt_both_want - or l3gw_onbd_have != l3gw_onbd_want - or nf_en_have != nf_en_want - or intvlan_nfen_have != intvlan_nfen_want - or vlan_nfen_have != vlan_nfen_want - ): + # Build comparison conditions, skipping those in skipped_template_keys + comparisons = [] + + # Always compare network templates + template_diff = have["networkTemplate"] != want["networkTemplate"] + comparisons.append(template_diff) + + ext_template_diff = have["networkExtensionTemplate"] != want["networkExtensionTemplate"] + comparisons.append(ext_template_diff) + + # Compare other attributes only if not skipped + if "gatewayIpAddress" not in skipped_template_keys: + gw_diff = gw_ip_have != gw_ip_want + comparisons.append(gw_diff) + + if "vlanId" not in skipped_template_keys: + vlan_diff = vlanId_have != vlanId_want + comparisons.append(vlan_diff) + + if "tag" not in skipped_template_keys: + tag_diff = tag_have != tag_want + comparisons.append(tag_diff) + + if "isLayer2Only" not in skipped_template_keys: + l2_diff = l2only_have != l2only_want + comparisons.append(l2_diff) + + if "vlanName" not in skipped_template_keys: + vname_diff = vlanName_have != vlanName_want + comparisons.append(vname_diff) + + if "intfDescription" not in skipped_template_keys: + intdesc_diff = intDesc_have != intDesc_want + comparisons.append(intdesc_diff) + + if "mtu" not in skipped_template_keys: + mtu_diff = mtu_have != mtu_want + comparisons.append(mtu_diff) + + if "suppressArp" not in skipped_template_keys: + arp_diff = arpsup_have != arpsup_want + comparisons.append(arp_diff) + + if "dhcpServerAddr1" not in skipped_template_keys: + dhcp1_diff = dhcp1_ip_have != dhcp1_ip_want + comparisons.append(dhcp1_diff) + + if "dhcpServerAddr2" not in skipped_template_keys: + dhcp2_diff = dhcp2_ip_have != dhcp2_ip_want + comparisons.append(dhcp2_diff) + + if "dhcpServerAddr3" not in skipped_template_keys: + dhcp3_diff = dhcp3_ip_have != dhcp3_ip_want + comparisons.append(dhcp3_diff) + + if "vrfDhcp" not in skipped_template_keys: + dhcp1vrf_diff = dhcp1_vrf_have != dhcp1_vrf_want + comparisons.append(dhcp1vrf_diff) + + if "vrfDhcp2" not in skipped_template_keys: + dhcp2vrf_diff = dhcp2_vrf_have != dhcp2_vrf_want + comparisons.append(dhcp2vrf_diff) + + if "vrfDhcp3" not in skipped_template_keys: + dhcp3vrf_diff = dhcp3_vrf_have != dhcp3_vrf_want + comparisons.append(dhcp3vrf_diff) + + if "dhcpServers" not in skipped_template_keys: + dhcp_servers_diff = dhcp_servers_have != dhcp_servers_want + comparisons.append(dhcp_servers_diff) + + if "loopbackId" not in skipped_template_keys: + loopback_diff = dhcp_loopback_have != dhcp_loopback_want + comparisons.append(loopback_diff) + + if "mcastGroup" not in skipped_template_keys: + mcast_diff = multicast_group_address_have != multicast_group_address_want + comparisons.append(mcast_diff) + + if "gatewayIpV6Address" not in skipped_template_keys: + gwv6_diff = gw_ipv6_have != gw_ipv6_want + comparisons.append(gwv6_diff) + + if "secondaryGW1" not in skipped_template_keys: + secgw1_diff = secip_gw1_have != secip_gw1_want + comparisons.append(secgw1_diff) + + if "secondaryGW2" not in skipped_template_keys: + secgw2_diff = secip_gw2_have != secip_gw2_want + comparisons.append(secgw2_diff) + + if "secondaryGW3" not in skipped_template_keys: + secgw3_diff = secip_gw3_have != secip_gw3_want + comparisons.append(secgw3_diff) + + if "secondaryGW4" not in skipped_template_keys: + secgw4_diff = secip_gw4_have != secip_gw4_want + comparisons.append(secgw4_diff) + + if "trmEnabled" not in skipped_template_keys: + trm_diff = trmen_have != trmen_want + comparisons.append(trm_diff) + + if "rtBothAuto" not in skipped_template_keys: + rt_diff = rt_both_have != rt_both_want + comparisons.append(rt_diff) + + if "enableL3OnBorder" not in skipped_template_keys: + l3border_diff = l3gw_onbd_have != l3gw_onbd_want + comparisons.append(l3border_diff) + + if "ENABLE_NETFLOW" not in skipped_template_keys: + nf_diff = nf_en_have != nf_en_want + comparisons.append(nf_diff) + + if "SVI_NETFLOW_MONITOR" not in skipped_template_keys: + svi_nf_diff = intvlan_nfen_have != intvlan_nfen_want + comparisons.append(svi_nf_diff) + + if "VLAN_NETFLOW_MONITOR" not in skipped_template_keys: + vlan_nf_diff = vlan_nfen_have != vlan_nfen_want + comparisons.append(vlan_nf_diff) + + if any(comparisons): # The network updates with missing networkId will have to use existing # networkId from the instance of the same network on DCNM. @@ -1264,7 +1847,7 @@ def update_create_params(self, net): else: net_upd = { "fabric": self.fabric, - "vrf": net["vrf_name"], + "vrf": net.get("vrf_name"), "networkName": net["net_name"], "networkId": net.get("net_id", None), # Network id will be auto generated in get_diff_merge() "networkTemplate": n_template, @@ -1289,7 +1872,12 @@ def update_create_params(self, net): "dhcpServers": [ {"srvrAddr": srvr["srvr_ip"], "srvrVrf": srvr["srvr_vrf"]} for srvr in net.get("dhcp_servers", []) ], - "loopbackId": net.get("dhcp_loopback_id", ""), + } + + dhcp_loopback_val = net.get("dhcp_loopback_id", "") + + template_conf.update({ + "loopbackId": dhcp_loopback_val, "mcastGroup": net.get("multicast_group_address", ""), "gatewayIpV6Address": net.get("gw_ipv6_subnet", ""), "secondaryGW1": net.get("secondary_ip_gw1", ""), @@ -1299,7 +1887,7 @@ def update_create_params(self, net): "trmEnabled": net.get("trm_enable", False), "rtBothAuto": net.get("route_target_both", False), "enableL3OnBorder": net.get("l3gw_on_border", False), - } + }) if self.dcnm_version > 11: template_conf.update(ENABLE_NETFLOW=net.get("netflow_enable", False)) @@ -1363,7 +1951,6 @@ def update_create_params(self, net): template_conf["secondaryGW3"] = "" if template_conf["secondaryGW4"] is None: template_conf["secondaryGW4"] = "" - if self.dcnm_version > 11: if template_conf["SVI_NETFLOW_MONITOR"] is None: template_conf["SVI_NETFLOW_MONITOR"] = "" @@ -1552,7 +2139,7 @@ def get_have(self): deployed = False if attach_state and (attach["lanAttachState"] == "OUT-OF-SYNC" or attach["lanAttachState"] == "PENDING"): deployed = False - else: + elif attach_state and (attach["lanAttachState"] == "IN-SYNC" or attach["lanAttachState"] == "DEPLOYED"): deployed = True if bool(deployed): @@ -1695,6 +2282,64 @@ def get_want(self): self.want_attach = want_attach self.want_deploy = want_deploy + def check_want_networks_deployment_state(self): + """ + Check deployment state of wanted networks and wait for networks that are not + in pending, out-of-sync, or deployed state to become ready before proceeding. + + This method should be called right after get_have() to ensure networks from + the playbook (want) are in a stable state before making any changes. + """ + + time.sleep(3) + networks_to_check = set() + + # Get networks from want_create that exist in ND and need to be checked + for want_net in self.want_create: + want_net_name = want_net['networkName'] + + # Find corresponding network in have_attach to see if it exists + have_net = next( + (net for net in self.have_attach if net["networkName"] == want_net_name), + None + ) + + if have_net: + # Network exists in DCNM, check if any attachments need deployment check + needs_check = False + if have_net.get("lanAttachList"): + for attach in have_net["lanAttachList"]: + # Check if attachment is not fully deployed (is_deploy=False means not IN-SYNC) + if not attach.get("is_deploy", False): + needs_check = True + break + + if needs_check: + networks_to_check.add(want_net_name) + + # Wait for networks to be in ready state before proceeding + if networks_to_check: + networks_list = list(networks_to_check) + deployment_states = self.wait_for_deploy_ready(networks_list) + + # Store deployment_states as instance variable for use in diff methods + self.deployment_states = deployment_states + + # Check if any networks failed to reach ready state + # Success states: DEPLOYED, PENDING, OUT-OF-SYNC, NA + # Failure states: FAILED, TIMEOUT and any other unexpected states + failed_networks = [net for net, state in deployment_states.items() + if state not in ['DEPLOYED', 'PENDING', 'OUT-OF-SYNC', 'NA']] + + if failed_networks: + error_msg = f"Pre-operation deployment check failed. Want networks not ready: {failed_networks}. Network states: {deployment_states}" + + # Call failure immediately for failed networks + self.failure(error_msg) + else: + # Initialize empty deployment_states when no networks need checking + self.deployment_states = {} + def get_diff_delete(self): diff_detach = [] @@ -1771,7 +2416,7 @@ def get_diff_override(self): diff_undeploy = self.diff_undeploy for have_a in self.have_attach: - # This block will take care of deleting all the networks that are only present on DCNM but not on playbook + # This block will take care of deleting all the networks that are only present on ND but not on playbook # The "if not found" block will go through all attachments under those networks and update them so that # they will be detached and also the network name will be added to delete payload. @@ -1824,7 +2469,7 @@ def get_diff_replace(self): for want_a in self.want_attach: # This block will take care of deleting any attachments that are present only on DCNM # but, not on the playbook. In this case, the playbook will have a network and few attaches under it, - # but, the attaches may be different to what the DCNM has for the same network. + # but, the attaches may be different to what the ND has for the same network. if have_a["networkName"] == want_a["networkName"]: h_in_w = True atch_h = have_a["lanAttachList"] @@ -1848,7 +2493,7 @@ def get_diff_replace(self): break if not h_in_w: - # This block will take care of deleting all the attachments which are in DCNM but + # This block will take care of deleting all the attachments which are in ND but # are not mentioned in the playbook. The playbook just has the network, but, does not have any attach # under it. found = next( @@ -1916,7 +2561,7 @@ def get_diff_merge(self, replace=False): # 2. Update vlan-id on an existing network: # This change will only affect new attachments of the same network. # 3. Auto generate networkId if its not mentioned by user: - # In this case, we need to query the DCNM to get a usable ID and use it in the payload. + # In this case, we need to query the ND to get a usable ID and use it in the payload. # And also, any such network create requests need to be pushed individually(not bulk op). diff_create = [] @@ -1992,6 +2637,7 @@ def get_diff_merge(self, replace=False): intvlan_nfmon_chg, vlan_nfmon_chg, ) = self.diff_for_create(want_c, have_c) + gw_changed.update({want_c["networkName"]: gw_chg}) tg_changed.update({want_c["networkName"]: tg_chg}) l2only_changed.update({want_c["networkName"]: l2only_chg}) @@ -2028,7 +2674,7 @@ def get_diff_merge(self, replace=False): if not net_id: # networkId(VNI-id) is not provided by user. - # Need to query DCNM to fetch next available networkId and use it here. + # Need to query ND to fetch next available networkId and use it here. method = "POST" @@ -2057,7 +2703,7 @@ def get_diff_merge(self, replace=False): elif self.dcnm_version >= 12: net_id = net_id_obj["DATA"].get("l2vni") else: - msg = "Unsupported DCNM version: version {0}".format(self.dcnm_version) + msg = "Unsupported ND version: version {0}".format(self.dcnm_version) self.module.fail_json(msg) if net_id != prev_net_id_fetched: @@ -2085,6 +2731,7 @@ def get_diff_merge(self, replace=False): else: diff_create.append(want_c) + # Check for deployment needed due to configuration changes (without attachment changes) all_nets = [] for want_a in self.want_attach: dep_net = "" @@ -2103,6 +2750,9 @@ def get_diff_merge(self, replace=False): if net: dep_net = want_a["networkName"] else: + # Check if any configuration changes require deployment + network_name = want_a["networkName"] + if ( net or gw_changed.get(want_a["networkName"], False) @@ -2155,7 +2805,6 @@ def get_diff_merge(self, replace=False): diff_attach.append(base) if bool(attach["is_deploy"]): dep_net = want_a["networkName"] - for atch in atch_list: atch["deployment"] = True @@ -2170,6 +2819,47 @@ def get_diff_merge(self, replace=False): if (want_net_data is not None) and (want_net_data.get("deploy") is False): modified_all_nets.remove(net) + # Check for networks that have deploy=True in config but are not in deployed state + # Use deployment_states from check_want_networks_deployment_state() instead of new API calls + additional_deploy_nets = [] + + for cfg in self.config: + net_name = cfg.get("net_name") + deploy_setting = cfg.get("deploy", True) # Default to True if not specified + + # Only check networks that have deploy=True and are not already in modified_all_nets + if deploy_setting and net_name and net_name not in modified_all_nets: + # Check if this network exists in ND (have_attach or have_create) + network_exists = False + for have_net in self.have_create: + if have_net.get("networkName") == net_name: + network_exists = True + break + + if not network_exists: + for have_net in self.have_attach: + if have_net.get("networkName") == net_name: + network_exists = True + break + + if network_exists: + # Check deployment status from cached deployment_states + if net_name in self.deployment_states: + network_status = self.deployment_states[net_name] + + # Add to deployment if not in DEPLOYED or NA state + if network_status not in ["DEPLOYED", "NA"]: + additional_deploy_nets.append(net_name) + + # Add additional networks to the deployment list (avoid duplicates) + if additional_deploy_nets: + original_count = len(modified_all_nets) + modified_all_nets.extend(additional_deploy_nets) + # Remove duplicates by converting to set and back to list + modified_all_nets = list(set(modified_all_nets)) + final_count = len(modified_all_nets) + added_count = final_count - original_count + if modified_all_nets: diff_deploy.update({"networkNames": ",".join(modified_all_nets)}) @@ -2478,6 +3168,74 @@ def wait_for_del_ready(self): return True + def wait_for_deploy_ready(self, networks_to_check): + """ + Wait for networks to reach a ready state for operations. + + Based on the actual API response structure: + - networkStatus can be: "DEPLOYED", "PENDING", "OUT-OF-SYNC", "FAILED", etc. + - DEPLOYED, PENDING, NA are immediately ready + - OUT-OF-SYNC and FAILED require two consecutive checks to be considered ready + + Parameters: + networks_to_check (list): List of network names to check deployment status + + Returns: + dict: Dictionary mapping network names to their final states + """ + + method = "GET" + network_states = {} + + if not networks_to_check: + return network_states + + for net in networks_to_check: + state_achieved = False + path = self.paths["GET_NET_STATUS"].format(self.fabric, net) + retry = max(100 // self.WAIT_TIME_FOR_DELETE_LOOP, 1) + # Track previous state for this network to detect consecutive OUT-OF-SYNC states + prev_state = None + + while not state_achieved and retry >= 0: + retry -= 1 + resp = dcnm_send(self.module, method, path) + + if resp["DATA"] and "networkStatus" in resp["DATA"]: + network_status = resp["DATA"]["networkStatus"].upper() + + # Check if network is in ready state + if network_status in ["DEPLOYED", "PENDING", "NA"]: + # These states are immediately ready + network_states[net] = network_status + state_achieved = True + break + elif network_status == "OUT-OF-SYNC": + # OUT-OF-SYNC requires two consecutive checks to be considered ready + if prev_state == "OUT-OF-SYNC": + network_states[net] = network_status + state_achieved = True + break + elif network_status == "FAILED": + # FAILED requires two consecutive checks to be considered ready + if prev_state == "FAILED": + network_states[net] = network_status + state_achieved = True + break + else: + # Network not in ready state yet, keep waiting + time.sleep(self.WAIT_TIME_FOR_DELETE_LOOP) + prev_state = network_status + else: + # No data received, treat as not ready + time.sleep(self.WAIT_TIME_FOR_DELETE_LOOP) + + # Handle timeout case + if retry < 0: + network_states[net] = "TIMEOUT" + + return network_states + def update_ms_fabric(self, diff): if not self.is_ms_fabric: return @@ -2492,7 +3250,25 @@ def push_to_remote(self, is_rollback=False): method = "PUT" if self.diff_create_update: + # Get skipped attributes for parent fabrics + skipped_attributes = self.get_skipped_attributes() + template_mapping = self.get_template_config_mapping() + + # Convert skipped spec attributes to template config keys + skipped_template_keys = set() + for attr in skipped_attributes: + if attr in template_mapping: + skipped_template_keys.add(template_mapping[attr]) + for net in self.diff_create_update: + # Remove skipped attributes from template config for parent fabrics + if net.get("networkTemplateConfig") and skipped_template_keys: + json_to_dict = json.loads(net["networkTemplateConfig"]) + for key in list(json_to_dict.keys()): + if key in skipped_template_keys: + del json_to_dict[key] + net["networkTemplateConfig"] = json.dumps(json_to_dict) + update_path = path + "/{0}".format(net["networkName"]) resp = dcnm_send(self.module, method, update_path, json.dumps(net)) self.result["response"].append(resp) @@ -2581,6 +3357,15 @@ def push_to_remote(self, is_rollback=False): self.failure(fail_msg) if self.diff_create: + # Get skipped attributes for parent fabrics + skipped_attributes = self.get_skipped_attributes() + template_mapping = self.get_template_config_mapping() + + # Convert skipped spec attributes to template config keys + skipped_template_keys = set() + for attr in skipped_attributes: + if attr in template_mapping: + skipped_template_keys.add(template_mapping[attr]) for net in self.diff_create: json_to_dict = json.loads(net["networkTemplateConfig"]) vlanId = json_to_dict.get("vlanId", "") @@ -2608,7 +3393,6 @@ def push_to_remote(self, is_rollback=False): "vrfDhcp": json_to_dict.get("vrfDhcp", ""), "vrfDhcp2": json_to_dict.get("vrfDhcp2", ""), "vrfDhcp3": json_to_dict.get("vrfDhcp3", ""), - "dhcpServers": json_to_dict.get("dhcpServers", ""), "loopbackId": json_to_dict.get("loopbackId", ""), "mcastGroup": json_to_dict.get("mcastGroup", ""), "gatewayIpV6Address": json_to_dict.get("gatewayIpV6Address", ""), @@ -2626,6 +3410,11 @@ def push_to_remote(self, is_rollback=False): t_conf.update(SVI_NETFLOW_MONITOR=json_to_dict.get("SVI_NETFLOW_MONITOR", "")) t_conf.update(VLAN_NETFLOW_MONITOR=json_to_dict.get("VLAN_NETFLOW_MONITOR", "")) + # Remove skipped attributes from template config for parent fabrics + for key in list(t_conf.keys()): + if key in skipped_template_keys: + del t_conf[key] + net.update({"networkTemplateConfig": json.dumps(t_conf)}) method = "POST" @@ -2691,7 +3480,7 @@ def get_fabric_multicast_group_address(self) -> str: - If fabric REPLICATION_MODE is "Ingress", default multicast group address should be set to "" - If fabric REPLICATION_MODE is "Multicast", default multicast group - address is set to 239.1.1.0 for DCNM version 11, and 239.1.1.1 for + address is set to 239.1.1.0 for ND version 11, and 239.1.1.1 for NDFC version 12 ## Raises @@ -2718,42 +3507,142 @@ def get_fabric_multicast_group_address(self) -> str: self.module.fail_json(msg=msg) return fabric_multicast_group_address - def validate_input(self): - """Parse the playbook values, validate to param specs.""" + def get_skipped_attributes(self): + """ + Get list of attributes that should be skipped for parent fabrics. - # Make sure mutually exclusive dhcp properties are not set - if self.config: - for net in self.config: - if net.get("dhcp_servers"): - conflicting_keys = [] - dhcp_individual_keys = [ - "dhcp_srvr1_ip", "dhcp_srvr1_vrf", - "dhcp_srvr2_ip", "dhcp_srvr2_vrf", - "dhcp_srvr3_ip", "dhcp_srvr3_vrf" - ] + For parent fabrics, returns all child spec attributes except net_name and deploy. + For child and standalone fabrics, returns empty list. - for key in dhcp_individual_keys: - if net.get(key) is not None: - conflicting_keys.append(key) + Returns: + list: List of attribute names to skip + """ + if self.fabric_type != "multisite_parent": + return [] - if conflicting_keys: - msg = "Network '{0}': dhcp_servers cannot be used together with individual DHCP server properties: {1}".format( - net.get("net_name", "unknown"), ", ".join(conflicting_keys) - ) - self.module.fail_json(msg=msg) + # Get child spec dynamically using parameter + child_net_spec = self.get_network_spec(fabric_type="multisite_child") - state = self.params["state"] + # Extract all attribute names except net_name and deploy + skipped_attrs = [attr for attr in child_net_spec.keys() + if attr not in ["net_name", "deploy", "vlan_id", "vrf_name", "is_l2only"]] - if state == "query": + return skipped_attrs - mcast_group_addr = self.get_fabric_multicast_group_address() + def get_template_config_mapping(self): + """ + Get mapping from network spec attributes to template config keys. + Returns: + dict: Mapping from spec attribute to template config key + """ + mapping = { + "vlan_id": "vlanId", + "gw_ip_subnet": "gatewayIpAddress", + "is_l2only": "isLayer2Only", + "routing_tag": "tag", + "vlan_name": "vlanName", + "int_desc": "intfDescription", + "mtu_l3intf": "mtu", + "arp_suppress": "suppressArp", + "dhcp_srvr1_ip": "dhcpServerAddr1", + "dhcp_srvr2_ip": "dhcpServerAddr2", + "dhcp_srvr3_ip": "dhcpServerAddr3", + "dhcp_srvr1_vrf": "vrfDhcp", + "dhcp_srvr2_vrf": "vrfDhcp2", + "dhcp_srvr3_vrf": "vrfDhcp3", + "dhcp_servers": "dhcpServers", + "dhcp_loopback_id": "loopbackId", + "multicast_group_address": "mcastGroup", + "gw_ipv6_subnet": "gatewayIpV6Address", + "secondary_ip_gw1": "secondaryGW1", + "secondary_ip_gw2": "secondaryGW2", + "secondary_ip_gw3": "secondaryGW3", + "secondary_ip_gw4": "secondaryGW4", + "trm_enable": "trmEnabled", + "route_target_both": "rtBothAuto", + "l3gw_on_border": "enableL3OnBorder", + "netflow_enable": "ENABLE_NETFLOW", + "intfvlan_nf_monitor": "SVI_NETFLOW_MONITOR", + "vlan_nf_monitor": "VLAN_NETFLOW_MONITOR" + } + return mapping + + def get_network_spec(self, fabric_type=None): + """ + Get network specification based on fabric type and state. + + Args: + fabric_type (str, optional): Override fabric type. If None, uses self.fabric_type. + + Returns: + dict: Network specification dictionary + """ + mcast_group_addr = self.get_fabric_multicast_group_address() + is_query_state = self.params["state"] == "query" + + # Use parameter if provided, otherwise use instance fabric_type + net_fabric_type = fabric_type if fabric_type is not None else self.fabric_type + + # Define the restricted spec for MSD child configurations + if net_fabric_type == "multisite_child": + net_spec = dict( + net_name=dict(required=True, type="str", length_max=64), + vrf_name=dict(type="str", length_max=32), + dhcp_loopback_id=dict(type="int", range_min=0, range_max=1023), + netflow_enable=dict(type="bool", default=False), + vlan_nf_monitor=dict(type="str"), + trm_enable=dict(type="bool", default=False), + multicast_group_address=dict(type="ipv4", default=mcast_group_addr), + l3gw_on_border=dict(type="bool", default=False), + dhcp_srvr1_ip=dict(type="ipv4", default=""), + dhcp_srvr2_ip=dict(type="ipv4", default=""), + dhcp_srvr3_ip=dict(type="ipv4", default=""), + dhcp_srvr1_vrf=dict(type="str", length_max=32), + dhcp_srvr2_vrf=dict(type="str", length_max=32), + dhcp_srvr3_vrf=dict(type="str", length_max=32), + dhcp_servers=dict(type="list", elements="dict", default=[]), + deploy=dict(type="bool", default=True if not is_query_state else None), + is_l2only=dict(type="bool", default=False), + ) + elif net_fabric_type == "multisite_parent": + # Parent-specific attributes: attributes present in full spec but not in child spec + net_spec = dict( + net_name=dict(required=True, type="str", length_max=64), + net_id=dict(type="int", range_max=16777214), + vrf_name=dict(type="str", length_max=32), + attach=dict(type="list"), + deploy=dict(type="bool", default=True if not is_query_state else None), + gw_ip_subnet=dict(type="ipv4_subnet", default=""), + vlan_id=dict(type="int", range_max=4094), + routing_tag=dict(type="int", default=12345, range_max=4294967295), + net_template=dict(type="str", default="Default_Network_Universal"), + net_extension_template=dict(type="str", default="Default_Network_Extension_Universal"), + is_l2only=dict(type="bool", default=False), + vlan_name=dict(type="str", length_max=128), + int_desc=dict(type="str", length_max=258), + mtu_l3intf=dict(type="int", range_min=68, range_max=9216), + arp_suppress=dict(type="bool", default=False), + gw_ipv6_subnet=dict(type="ipv6_subnet", default=""), + secondary_ip_gw1=dict(type="ipv4", default=""), + secondary_ip_gw2=dict(type="ipv4", default=""), + secondary_ip_gw3=dict(type="ipv4", default=""), + secondary_ip_gw4=dict(type="ipv4", default=""), + route_target_both=dict(type="bool", default=False), + intfvlan_nf_monitor=dict(type="str"), + ) + + # Adjust deploy field for query state + if is_query_state: + net_spec["deploy"] = dict(type="bool") + else: + # Full specification for non-child, non-parent fabrics net_spec = dict( net_name=dict(required=True, type="str", length_max=64), net_id=dict(type="int", range_max=16777214), vrf_name=dict(type="str", length_max=32), attach=dict(type="list"), - deploy=dict(type="bool"), + deploy=dict(type="bool", default=True if not is_query_state else None), gw_ip_subnet=dict(type="ipv4_subnet", default=""), vlan_id=dict(type="int", range_max=4094), routing_tag=dict(type="int", default=12345, range_max=4294967295), @@ -2785,6 +3674,56 @@ def validate_input(self): intfvlan_nf_monitor=dict(type="str"), vlan_nf_monitor=dict(type="str"), ) + + # Adjust deploy field for query state + if is_query_state: + net_spec["deploy"] = dict(type="bool") + + return net_spec + + def validate_input(self): + + """Parse the playbook values, validate to param specs.""" + + # Make sure mutually exclusive dhcp properties are not set + if self.config: + for net in self.config: + if net.get("dhcp_servers"): + conflicting_keys = [] + dhcp_individual_keys = [ + "dhcp_srvr1_ip", "dhcp_srvr1_vrf", + "dhcp_srvr2_ip", "dhcp_srvr2_vrf", + "dhcp_srvr3_ip", "dhcp_srvr3_vrf" + ] + + for key in dhcp_individual_keys: + if net.get(key) is not None: + conflicting_keys.append(key) + + if conflicting_keys: + msg = "Network '{0}': dhcp_servers cannot be used together with individual DHCP server properties: {1}".format( + net.get("net_name", "unknown"), ", ".join(conflicting_keys) + ) + self.module.fail_json(msg=msg) + + state = self.params["state"] + + if state == "query": + + mcast_group_addr = self.get_fabric_multicast_group_address() + state = self.params["state"] + + # Check for invalid state combinations with MSD child + if self.fabric_type == "multisite_child": + if state in ["overridden", "deleted"]: + self.module.fail_json( + msg=f"State '{state}' is not allowed for MSD child networks. Networks cannot be " + "deleted or overridden in MSD child fabrics." + ) + + if state == "query": + + net_spec = self.get_network_spec() att_spec = dict( ip_address=dict(required=True, type="str"), ports=dict(type="list", default=[]), @@ -2796,6 +3735,14 @@ def validate_input(self): # Validate net params valid_net, invalid_params = validate_list_of_dicts(self.config, net_spec, check_extra_params=self.check_extra_params) for net in valid_net: + # Check for attachment attributes in MSD child fabrics + if self.fabric_type == "multisite_child" and net.get("attach"): + self.module.fail_json( + msg=f"Network '{net.get('net_name', 'unknown')}': Attachment attributes are " + "not allowed for MSD child networks. MSD child fabrics do not support " + "network attachments." + ) + if net.get("attach"): valid_att, invalid_att = validate_list_of_dicts(net["attach"], att_spec, check_extra_params=self.check_extra_params) net["attach"] = valid_att @@ -2813,45 +3760,7 @@ def validate_input(self): else: - mcast_group_addr = self.get_fabric_multicast_group_address() - - net_spec = dict( - net_name=dict(required=True, type="str", length_max=64), - net_id=dict(type="int", range_max=16777214), - vrf_name=dict(type="str", length_max=32), - attach=dict(type="list"), - deploy=dict(type="bool", default=True), - gw_ip_subnet=dict(type="ipv4_subnet", default=""), - vlan_id=dict(type="int", range_max=4094), - routing_tag=dict(type="int", default=12345, range_max=4294967295), - net_template=dict(type="str", default="Default_Network_Universal"), - net_extension_template=dict(type="str", default="Default_Network_Extension_Universal"), - is_l2only=dict(type="bool", default=False), - vlan_name=dict(type="str", length_max=128), - int_desc=dict(type="str", length_max=258), - mtu_l3intf=dict(type="int", range_min=68, range_max=9216), - arp_suppress=dict(type="bool", default=False), - dhcp_srvr1_ip=dict(type="ipv4", default=""), - dhcp_srvr2_ip=dict(type="ipv4", default=""), - dhcp_srvr3_ip=dict(type="ipv4", default=""), - dhcp_srvr1_vrf=dict(type="str", length_max=32), - dhcp_srvr2_vrf=dict(type="str", length_max=32), - dhcp_srvr3_vrf=dict(type="str", length_max=32), - dhcp_servers=dict(type="list", elements="dict", default=[]), - dhcp_loopback_id=dict(type="int", range_min=0, range_max=1023), - multicast_group_address=dict(type="ipv4", default=mcast_group_addr), - gw_ipv6_subnet=dict(type="ipv6_subnet", default=""), - secondary_ip_gw1=dict(type="ipv4", default=""), - secondary_ip_gw2=dict(type="ipv4", default=""), - secondary_ip_gw3=dict(type="ipv4", default=""), - secondary_ip_gw4=dict(type="ipv4", default=""), - trm_enable=dict(type="bool", default=False), - route_target_both=dict(type="bool", default=False), - l3gw_on_border=dict(type="bool", default=False), - netflow_enable=dict(type="bool", default=False), - intfvlan_nf_monitor=dict(type="str"), - vlan_nf_monitor=dict(type="str"), - ) + net_spec = self.get_network_spec() att_spec = dict( ip_address=dict(required=True, type="str"), ports=dict(type="list", default=[]), @@ -2868,6 +3777,14 @@ def validate_input(self): # Validate net params valid_net, invalid_params = validate_list_of_dicts(self.config, net_spec, check_extra_params=self.check_extra_params) for net in valid_net: + # Check for attachment attributes in MSD child fabrics + if self.fabric_type == "multisite_child" and net.get("attach"): + self.module.fail_json( + msg=f"Network '{net.get('net_name', 'unknown')}': Attachment attributes are " + "not allowed for MSD child networks. MSD child fabrics do not support " + "network attachments." + ) + if net.get("attach"): valid_att, invalid_att = validate_list_of_dicts(net["attach"], att_spec, check_extra_params=self.check_extra_params) net["attach"] = valid_att @@ -2917,6 +3834,23 @@ def validate_input(self): if net.get("netflow_enable") or net.get("intfvlan_nf_monitor") or net.get("vlan_nf_monitor"): invalid_params.append("Netflow configurations are supported only on NDFC") + # Check if netflow monitors are specified without enabling netflow + netflow_enable = net.get("netflow_enable", False) + intfvlan_nf_monitor = net.get("intfvlan_nf_monitor") + vlan_nf_monitor = net.get("vlan_nf_monitor") + + if not netflow_enable: + if intfvlan_nf_monitor: + invalid_params.append( + f"Network '{net.get('net_name', 'unknown')}': intfvlan_nf_monitor " + "(Interface VLAN Netflow Monitor) cannot be specified when netflow_enable is False or not set" + ) + if vlan_nf_monitor: + invalid_params.append( + f"Network '{net.get('net_name', 'unknown')}': vlan_nf_monitor " + "(VLAN Netflow Monitor) cannot be specified when netflow_enable is False or not set" + ) + self.validated.append(net) if invalid_params: @@ -2964,20 +3898,35 @@ def handle_response(self, resp, op): if op == "attach" and "Invalid interfaces" in str(res.values()): fail = True changed = True - if op == "deploy" and "No switches PENDING for deployment" in str(res.values()): + if op == "deploy" and "No switches PENDING for deployment" in str(res.values()) and "multisite_" not in self.fabric_type: + # For parent fabrics, don't set changed=False as they will never have switches changed = False + # Check for VLAN ID already in use errors in DATA section + # This handles cases where RETURN_CODE is 200 but DATA contains error messages + if op == "attach" and res.get("DATA") and isinstance(res["DATA"], dict): + for key, value in res["DATA"].items(): + if isinstance(value, str) and "is already in use" in value.lower(): + fail = True + changed = False + break + # Check for multisite overlay link error + if isinstance(value, str) and "multisite overlay link should be available to extend multisite" in value.lower(): + fail = True + changed = False + break + return fail, changed def failure(self, resp): # Donot Rollback for Multi-site fabrics - if self.is_ms_fabric: + if self.is_ms_fabric or self.fabric_type != "standalone": self.failed_to_rollback = True self.module.fail_json(msg=resp) return - # Implementing a per task rollback logic here so that we rollback DCNM to the have state + # Implementing a per task rollback logic here so that we rollback ND to the have state # whenever there is a failure in any of the APIs. # The idea would be to run overridden state with want=have and have=dcnm_state self.want_create = self.have_create @@ -3015,7 +3964,17 @@ def failure(self, resp): self.module.fail_json(msg=res) def dcnm_update_network_information(self, want, have, cfg): + # Check if this is a replaced state with MSD child fabric + is_replaced_multisite_child = ( + self.module.params["state"] == "replaced" + and self.fabric_type == "multisite_child" + ) + + # MSD child configurable attributes (can be updated in replaced state): + # dhcp_loopback_id, netflow_enable, vlan_nf_monitor, trm_enable, + # multicast_group_address, l3gw_on_border + # Update basic network information if cfg.get("vrf_name", None) is None: want["vrf"] = have["vrf"] @@ -3031,6 +3990,7 @@ def dcnm_update_network_information(self, want, have, cfg): json_to_dict_want = json.loads(want["networkTemplateConfig"]) json_to_dict_have = json.loads(have["networkTemplateConfig"]) + # Update template configuration - common attributes if cfg.get("vlan_id", None) is None: json_to_dict_want["vlanId"] = json_to_dict_have["vlanId"] if json_to_dict_want["vlanId"] != "": @@ -3069,51 +4029,7 @@ def dcnm_update_network_information(self, want, have, cfg): elif str(json_to_dict_want["suppressArp"]).lower() == "false": json_to_dict_want["suppressArp"] = False - if cfg.get("dhcp_srvr1_ip", None) is None: - json_to_dict_want["dhcpServerAddr1"] = json_to_dict_have["dhcpServerAddr1"] - - if cfg.get("dhcp_srvr2_ip", None) is None: - json_to_dict_want["dhcpServerAddr2"] = json_to_dict_have["dhcpServerAddr2"] - - if cfg.get("dhcp_srvr3_ip", None) is None: - json_to_dict_want["dhcpServerAddr3"] = json_to_dict_have["dhcpServerAddr3"] - - if cfg.get("dhcp_srvr1_vrf", None) is None: - json_to_dict_want["vrfDhcp"] = json_to_dict_have["vrfDhcp"] - - if cfg.get("dhcp_srvr2_vrf", None) is None: - json_to_dict_want["vrfDhcp2"] = json_to_dict_have["vrfDhcp2"] - - if cfg.get("dhcp_srvr3_vrf", None) is None: - json_to_dict_want["vrfDhcp3"] = json_to_dict_have["vrfDhcp3"] - - if cfg.get("dhcp_servers", None) is None: - want_have_dhcp_servers = [None] * 3 - if cfg.get("dhcp_srvr1_ip", None) is not None: - want_have_dhcp_servers[0] = dict(srvrAddr=cfg.get("dhcp_srvr1_ip"), srvrVrf=cfg.get("dhcp_srvr1_vrf")) - elif json_to_dict_have["dhcpServerAddr1"] != "": - want_have_dhcp_servers[0] = dict(srvrAddr=json_to_dict_have["dhcpServerAddr1"], srvrVrf=json_to_dict_have["vrfDhcp"]) - if cfg.get("dhcp_srvr2_ip", None) is not None: - want_have_dhcp_servers[1] = dict(srvrAddr=cfg.get("dhcp_srvr2_ip"), srvrVrf=cfg.get("dhcp_srvr2_vrf")) - elif json_to_dict_have["dhcpServerAddr2"] != "": - want_have_dhcp_servers[1] = dict(srvrAddr=json_to_dict_have["dhcpServerAddr2"], srvrVrf=json_to_dict_have["vrfDhcp2"]) - if cfg.get("dhcp_srvr3_ip", None) is not None: - want_have_dhcp_servers[2] = dict(srvrAddr=cfg.get("dhcp_srvr3_ip"), srvrVrf=cfg.get("dhcp_srvr3_vrf")) - elif json_to_dict_have["dhcpServerAddr3"] != "": - want_have_dhcp_servers[2] = dict(srvrAddr=json_to_dict_have["dhcpServerAddr3"], srvrVrf=json_to_dict_have["vrfDhcp3"]) - want_have_dhcp_servers = [srvr for srvr in want_have_dhcp_servers[:] if srvr is not None] - if want_have_dhcp_servers != []: - json_to_dict_want["dhcpServers"] = json.dumps(dict(dhcpServers=want_have_dhcp_servers, separators=(",", ":"))) - else: - json_to_dict_want["dhcpServers"] = json_to_dict_have["dhcpServers"] - - if cfg.get("dhcp_loopback_id", None) is None: - json_to_dict_want["loopbackId"] = json_to_dict_have["loopbackId"] - - if self.is_ms_fabric is False: - if cfg.get("multicast_group_address", None) is None: - json_to_dict_want["mcastGroup"] = json_to_dict_have["mcastGroup"] - + # IPv6 and secondary gateway configuration if cfg.get("gw_ipv6_subnet", None) is None: json_to_dict_want["gatewayIpV6Address"] = json_to_dict_have["gatewayIpV6Address"] @@ -3129,13 +4045,7 @@ def dcnm_update_network_information(self, want, have, cfg): if cfg.get("secondary_ip_gw4", None) is None: json_to_dict_want["secondaryGW4"] = json_to_dict_have["secondaryGW4"] - if cfg.get("trm_enable", None) is None: - json_to_dict_want["trmEnabled"] = json_to_dict_have["trmEnabled"] - if str(json_to_dict_want["trmEnabled"]).lower() == "true": - json_to_dict_want["trmEnabled"] = True - else: - json_to_dict_want["trmEnabled"] = False - + # Route target configuration (common for all fabric types) if cfg.get("route_target_both", None) is None: json_to_dict_want["rtBothAuto"] = json_to_dict_have["rtBothAuto"] if str(json_to_dict_want["rtBothAuto"]).lower() == "true": @@ -3143,26 +4053,71 @@ def dcnm_update_network_information(self, want, have, cfg): else: json_to_dict_want["rtBothAuto"] = False - if cfg.get("l3gw_on_border", None) is None: - json_to_dict_want["enableL3OnBorder"] = json_to_dict_have["enableL3OnBorder"] - if str(json_to_dict_want["enableL3OnBorder"]).lower() == "true": - json_to_dict_want["enableL3OnBorder"] = True - else: - json_to_dict_want["enableL3OnBorder"] = False + # MSD child configurable attributes - grouped together + # These can be modified in MSD child replaced state + if not is_replaced_multisite_child: + # DHCP server configuration + if cfg.get("dhcp_srvr1_ip", None) is None: + json_to_dict_want["dhcpServerAddr1"] = json_to_dict_have["dhcpServerAddr1"] - if self.dcnm_version > 11: - if cfg.get("netflow_enable", None) is None: - json_to_dict_want["ENABLE_NETFLOW"] = json_to_dict_have["ENABLE_NETFLOW"] - if str(json_to_dict_want["ENABLE_NETFLOW"]).lower() == "true": - json_to_dict_want["ENABLE_NETFLOW"] = True + if cfg.get("dhcp_srvr2_ip", None) is None: + json_to_dict_want["dhcpServerAddr2"] = json_to_dict_have["dhcpServerAddr2"] + + if cfg.get("dhcp_srvr3_ip", None) is None: + json_to_dict_want["dhcpServerAddr3"] = json_to_dict_have["dhcpServerAddr3"] + + if cfg.get("dhcp_srvr1_vrf", None) is None: + json_to_dict_want["vrfDhcp"] = json_to_dict_have["vrfDhcp"] + + if cfg.get("dhcp_srvr2_vrf", None) is None: + json_to_dict_want["vrfDhcp2"] = json_to_dict_have["vrfDhcp2"] + + if cfg.get("dhcp_srvr3_vrf", None) is None: + json_to_dict_want["vrfDhcp3"] = json_to_dict_have["vrfDhcp3"] + + # DHCP servers list configuration + if cfg.get("dhcp_servers", None) is None: + json_to_dict_want["dhcpServers"] = json_to_dict_have.get("dhcpServers", "") + + # DHCP loopback configuration + if cfg.get("dhcp_loopback_id", None) is None: + json_to_dict_want["loopbackId"] = json_to_dict_have["loopbackId"] + + # TRM enable configuration + if cfg.get("trm_enable", None) is None: + json_to_dict_want["trmEnabled"] = json_to_dict_have["trmEnabled"] + if str(json_to_dict_want["trmEnabled"]).lower() == "true": + json_to_dict_want["trmEnabled"] = True else: - json_to_dict_want["ENABLE_NETFLOW"] = False + json_to_dict_want["trmEnabled"] = False - if cfg.get("intfvlan_nf_monitor", None) is None: - json_to_dict_want["SVI_NETFLOW_MONITOR"] = json_to_dict_have["SVI_NETFLOW_MONITOR"] + # L3 gateway on border configuration + if cfg.get("l3gw_on_border", None) is None: + json_to_dict_want["enableL3OnBorder"] = json_to_dict_have["enableL3OnBorder"] + if str(json_to_dict_want["enableL3OnBorder"]).lower() == "true": + json_to_dict_want["enableL3OnBorder"] = True + else: + json_to_dict_want["enableL3OnBorder"] = False - if cfg.get("vlan_nf_monitor", None) is None: - json_to_dict_want["VLAN_NETFLOW_MONITOR"] = json_to_dict_have["VLAN_NETFLOW_MONITOR"] + # Multicast configuration (skip for MS fabric) + if self.is_ms_fabric is False and cfg.get("multicast_group_address", None) is None: + json_to_dict_want["mcastGroup"] = json_to_dict_have["mcastGroup"] + + # NetFlow configuration (version 12+ only) + if self.dcnm_version > 11: + if cfg.get("netflow_enable", None) is None: + json_to_dict_want["ENABLE_NETFLOW"] = json_to_dict_have["ENABLE_NETFLOW"] + if str(json_to_dict_want["ENABLE_NETFLOW"]).lower() == "true": + json_to_dict_want["ENABLE_NETFLOW"] = True + else: + json_to_dict_want["ENABLE_NETFLOW"] = False + + if cfg.get("vlan_nf_monitor", None) is None: + json_to_dict_want["VLAN_NETFLOW_MONITOR"] = json_to_dict_have["VLAN_NETFLOW_MONITOR"] + + # NetFlow SVI monitor configuration (common for all fabric types, version 12+ only) + if self.dcnm_version > 11 and cfg.get("intfvlan_nf_monitor", None) is None: + json_to_dict_want["SVI_NETFLOW_MONITOR"] = json_to_dict_have["SVI_NETFLOW_MONITOR"] want.update({"networkTemplateConfig": json.dumps(json_to_dict_want)}) @@ -3180,10 +4135,24 @@ def update_want(self): None """ + # For child fabrics, copy have attachments to want attachments since child fabrics don't support attachments in config + if self.fabric_type == "multisite_child": + # Copy have attachments to want attachments for child fabrics + import copy + self.want_attach = copy.deepcopy(self.have_attach) + # only for 'merged' state we need to update the objects that are not included in playbook with # values from self.have. - - if self.module.params["state"] != "merged": + # Also for 'replaced' state when MSD exists and is child, we need to ignore certain attributes + + state = self.module.params["state"] + if state == "merged": + # Normal merged state processing + pass + elif state == "replaced" and self.fabric_type == "multisite_child": + # For MSD child in replaced state, we need special handling + pass + else: return if self.want_create == []: @@ -3209,6 +4178,12 @@ def main(): element_spec = dict( fabric=dict(required=True, type="str"), + _fabric_type=dict( + required=False, + type="str", + default="standalone", + choices=["multisite_child", "standalone", "multisite_parent"] + ), config=dict(required=False, type="list", elements="dict"), state=dict( default="merged", @@ -3221,12 +4196,14 @@ def main(): dcnm_net = DcnmNetwork(module) if not dcnm_net.ip_sn: - module.fail_json(msg="Fabric {0} missing on DCNM or does not have any switches".format(dcnm_net.fabric)) + module.fail_json(msg="Fabric {0} missing on ND or does not have any switches".format(dcnm_net.fabric)) dcnm_net.validate_input() dcnm_net.get_want() dcnm_net.get_have() + if module.params["_fabric_type"] == "multisite_child": + dcnm_net.check_want_networks_deployment_state() warn_msg = None diff --git a/plugins/modules/dcnm_vrf.py b/plugins/modules/dcnm_vrf.py index a4af8efdb..dc37531b5 100644 --- a/plugins/modules/dcnm_vrf.py +++ b/plugins/modules/dcnm_vrf.py @@ -21,10 +21,10 @@ DOCUMENTATION = """ --- module: dcnm_vrf -short_description: Add and remove VRFs from a DCNM managed VXLAN fabric. +short_description: Add and remove VRFs from a ND managed VXLAN fabric. version_added: "0.9.0" description: - - "Add and remove VRFs and VRF Lite Extension from a DCNM managed VXLAN fabric." + - "Add and remove VRFs and VRF Lite Extension from a ND managed VXLAN fabric." - "In Multisite fabrics, VRFs can be created only on Multisite fabric" - "In Multisite fabrics, VRFs cannot be created on member fabric" author: Shrishail Kariyappanavar(@nkshrishail), Karthik Babu Harichandra Babu (@kharicha), Praveen Ramoorthy(@praveenramoorthy) @@ -36,7 +36,7 @@ required: yes state: description: - - The state of DCNM after module completion. + - The state of ND after module completion. type: str choices: - merged @@ -64,7 +64,7 @@ vlan_id: description: - vlan ID for the vrf attachment - - If not specified in the playbook, DCNM will auto-select an available vlan_id + - If not specified in the playbook, ND will auto-select an available vlan_id type: int required: false vrf_template: @@ -147,19 +147,22 @@ l3vni_wo_vlan: description: - Enable L3 VNI without VLAN + - Not applicable at Multisite parent fabric level type: bool required: false default: Inherited from fabric level settings trm_enable: description: - Enable Tenant Routed Multicast + - Not applicable at Multisite parent fabric level type: bool required: false default: false no_rp: description: - No RP, only SSM is used - - supported on NDFC only + - supported on ND only + - Not applicable at Multisite parent fabric level type: bool required: false default: false @@ -167,6 +170,7 @@ description: - Specifies if RP is external to the fabric - Can be configured only when TRM is enabled + - Not applicable at Multisite parent fabric level type: bool required: false default: false @@ -174,48 +178,56 @@ description: - IPv4 Address of RP - Can be configured only when TRM is enabled + - Not applicable at Multisite parent fabric level type: str required: false rp_loopback_id: description: - loopback ID of RP - Can be configured only when TRM is enabled + - Not applicable at Multisite parent fabric level type: int required: false underlay_mcast_ip: description: - Underlay IPv4 Multicast Address - Can be configured only when TRM is enabled + - Not applicable at Multisite parent fabric level type: str required: false overlay_mcast_group: description: - Underlay IPv4 Multicast group (224.0.0.0/4 to 239.255.255.255/4) - Can be configured only when TRM is enabled + - Not applicable at Multisite parent fabric level type: str required: false trm_bgw_msite: description: - Enable TRM on Border Gateway Multisite - Can be configured only when TRM is enabled + - Not applicable at Multisite parent fabric level type: bool required: false default: false adv_host_routes: description: - Flag to Control Advertisement of /32 and /128 Routes to Edge Routers + - Not applicable at Multisite Parent fabric level type: bool required: false default: false adv_default_routes: description: - Flag to Control Advertisement of Default Route Internally + - Not applicable at Multisite Parent fabric level type: bool required: false default: true static_default_route: description: - Flag to Control Static Default Route Configuration + - Not applicable at Multisite parent fabric level type: bool required: false default: true @@ -223,12 +235,14 @@ description: - VRF Lite BGP neighbor password - Password should be in Hex string format + - Not applicable at Multisite parent fabric level type: str required: false bgp_passwd_encrypt: description: - VRF Lite BGP Key Encryption Type - Allowed values are 3 (3DES) and 7 (Cisco) + - Not applicable at Multisite parent fabric level type: int choices: - 3 @@ -239,67 +253,208 @@ description: - Enable netflow on VRF-LITE Sub-interface - Netflow is supported only if it is enabled on fabric - - Netflow configs are supported on NDFC only + - Netflow configs are supported on ND only + - Not applicable at Multisite parent fabric level type: bool required: false default: false nf_monitor: description: - Netflow Monitor - - Netflow configs are supported on NDFC only + - Netflow configs are supported on ND only + - Not applicable at Multisite parent fabric level type: str required: false disable_rt_auto: description: - Disable RT Auto-Generate - - supported on NDFC only + - supported on ND only type: bool required: false default: false import_vpn_rt: description: - VPN routes to import - - supported on NDFC only + - supported on ND only - Use ',' to separate multiple route-targets type: str required: false export_vpn_rt: description: - VPN routes to export - - supported on NDFC only + - supported on ND only - Use ',' to separate multiple route-targets type: str required: false import_evpn_rt: description: - EVPN routes to import - - supported on NDFC only + - supported on ND only - Use ',' to separate multiple route-targets type: str required: false export_evpn_rt: description: - EVPN routes to export - - supported on NDFC only + - supported on ND only - Use ',' to separate multiple route-targets type: str required: false import_mvpn_rt: description: - MVPN routes to import - - supported on NDFC only + - supported on ND only - Can be configured only when TRM is enabled - Use ',' to separate multiple route-targets + - Not applicable at Multisite parent fabric level type: str required: false export_mvpn_rt: description: - MVPN routes to export - - supported on NDFC only + - supported on ND only - Can be configured only when TRM is enabled - Use ',' to separate multiple route-targets + - Not applicable at Multisite parent fabric level type: str required: false + child_fabric_config: + description: + - Configuration for Child fabrics in multisite (MSD) deployments + - Only applicable for Parent multisite fabrics + - Defines VRF behavior on each Child fabric + - Not supported with state 'deleted' + type: list + elements: dict + required: false + suboptions: + fabric: + description: + - Name of the Child fabric + - Must be a valid Child fabric associated with the Parent + type: str + required: true + l3vni_wo_vlan: + description: + - Enable L3 VNI without VLAN on Child fabric + type: bool + required: false + default: false + adv_default_routes: + description: + - Advertise default routes on Child fabric + type: bool + required: false + default: true + adv_host_routes: + description: + - Advertise host routes on Child fabric + type: bool + required: false + default: false + static_default_route: + description: + - Configure static default route on Child fabric + type: bool + required: false + default: true + bgp_password: + description: + - BGP password for Child fabric VRF Lite + - Password should be in Hex string format + type: str + required: false + bgp_passwd_encrypt: + description: + - BGP password encryption type on Child fabric + - 3 for 3DES encryption, 7 for Cisco encryption + type: int + choices: + - 3 + - 7 + required: false + default: 3 + netflow_enable: + description: + - Enable netflow on Child fabric + - Netflow is supported only if it is enabled on fabric + type: bool + required: false + default: false + nf_monitor: + description: + - Netflow monitor on Child fabric + type: str + required: false + trm_enable: + description: + - Enable TRM (Tenant Routed Multicast) on Child fabric + - Required for multicast traffic within VRF on Child fabric + type: bool + required: false + default: false + no_rp: + description: + - No RP, only SSM is used on Child fabric + - Cannot be used with TRM enabled + type: bool + required: false + default: false + rp_address: + description: + - IPv4 Address of RP (Rendezvous Point) on Child fabric + - Can be configured only when TRM is enabled + type: str + required: false + rp_external: + description: + - Specifies if RP is external to the Child fabric + - Can be configured only when TRM is enabled + type: bool + required: false + default: false + rp_loopback_id: + description: + - Loopback ID of RP on Child fabric + - Can be configured only when TRM is enabled + - Range 0-1023 + type: int + required: false + underlay_mcast_ip: + description: + - Underlay IPv4 Multicast Address on Child fabric + - Can be configured only when TRM is enabled + type: str + required: false + overlay_mcast_group: + description: + - Overlay IPv4 Multicast group on Child fabric + - Format (224.0.0.0/4 to 239.255.255.255/4) + - Can be configured only when TRM is enabled + type: str + required: false + trm_bgw_msite: + description: + - Enable TRM on Border Gateway Multisite for Child fabric + - Can be configured only when TRM is enabled + - Required for multicast across sites + type: bool + required: false + default: false + import_mvpn_rt: + description: + - MVPN routes to import on Child fabric + - Can be configured only when TRM is enabled + - Use ',' to separate multiple route-targets + type: str + required: false + export_mvpn_rt: + description: + - MVPN routes to export on Child fabric + - Can be configured only when TRM is enabled + - Use ',' to separate multiple route-targets + type: str + required: false attach: description: - List of vrf attachment details @@ -357,35 +512,46 @@ import_evpn_rt: description: - import evpn route-target - - supported on NDFC only + - supported on ND only - Use ',' to separate multiple route-targets type: str required: false export_evpn_rt: description: - export evpn route-target - - supported on NDFC only + - supported on ND only - Use ',' to separate multiple route-targets type: str required: false deploy: description: - Per switch knob to control whether to deploy the attachment - - This knob has been deprecated from Ansible NDFC Collection Version 2.1.0 onwards. + - This knob has been deprecated from Ansible ND Collection Version 2.1.0 onwards. There will not be any functional impact if specified in playbook. type: bool default: true deploy: description: - Global knob to control whether to deploy the attachment - - Ansible NDFC Collection Behavior for Version 2.0.1 and earlier - - This knob will create and deploy the attachment in DCNM only when set to "True" in playbook - - Ansible NDFC Collection Behavior for Version 2.1.0 and later - - Attachments specified in the playbook will always be created in DCNM. - This knob, when set to "True", will deploy the attachment in DCNM, by pushing the configs to switch. - If set to "False", the attachments will be created in DCNM, but will not be deployed + - Ansible ND Collection Behavior for Version 2.0.1 and earlier + - This knob will create and deploy the attachment in ND only when set to "True" in playbook + - Ansible ND Collection Behavior for Version 2.1.0 and later + - Attachments specified in the playbook will always be created in ND + This knob, when set to "True", will deploy the attachment in ND, by pushing the configs to switch. + If set to "False", the attachments will be created in ND, but will not be deployed + - In case of Multisite fabrics, deploy flag on parent will be inherited by the specified child fabrics. type: bool default: true + _fabric_type: + description: + - INTERNAL PARAMETER - DO NOT USE + - Fabric type is determined by the module's action plugin + - This parameter is used internally by the module for multisite fabric processing + - Valid values are 'multisite_child', 'multisite_parent' and 'standalone' + type: str + required: false + choices: ['multisite_child', 'multisite_parent', 'standalone'] + default: 'standalone' """ EXAMPLES = """ @@ -418,163 +584,389 @@ # # Deleted: # VRFs defined in the playbook will be deleted. -# If no VRFs are provided in the playbook, all VRFs present on that DCNM fabric will be deleted. +# If no VRFs are provided in the playbook, all VRFs present on that ND fabric will be deleted. # # Query: -# Returns the current DCNM state for the VRFs listed in the playbook. +# Returns the current ND state for the VRFs listed in the playbook. # # rollback functionality: # This module supports task level rollback functionality. If any task runs into failures, as part of failure -# handling, the module tries to bring the state of the DCNM back to the state captured in have structure at the +# handling, the module tries to bring the state of the ND back to the state captured in have structure at the # beginning of the task execution. Following few lines provide a logical description of how this works, # if (failure) # want data = have data -# have data = get state of DCNM +# have data = get state of ND # Run the module in override state with above set of data to produce the required set of diffs -# and push the diff payloads to DCNM. +# and push the diff payloads to ND. # If rollback fails, the module does not attempt to rollback again, it just quits with appropriate error messages. -# The two VRFs below will be merged into the target fabric. -- name: Merge vrfs +# =========================================================================== +# Non-MSD/Standalone Fabric Examples +# =========================================================================== + +- name: MERGE | Create two VRFs on a standalone fabric cisco.dcnm.dcnm_vrf: fabric: vxlan-fabric state: merged config: - - vrf_name: ansible-vrf-r1 - vrf_id: 9008011 - vrf_template: Default_VRF_Universal - vrf_extension_template: Default_VRF_Extension_Universal - vlan_id: 2000 - service_vrf_template: null - attach: - - ip_address: 192.168.1.224 - - ip_address: 192.168.1.225 - - vrf_name: ansible-vrf-r2 - vrf_id: 9008012 - vrf_template: Default_VRF_Universal - vrf_extension_template: Default_VRF_Extension_Universal - service_vrf_template: null - attach: - - ip_address: 192.168.1.224 - - ip_address: 192.168.1.225 - -# VRF LITE Extension attached -- name: Merge vrfs + - vrf_name: ansible-vrf-r1 + vrf_id: 9008011 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + attach: + - ip_address: 192.168.1.224 + - ip_address: 192.168.1.225 + - vrf_name: ansible-vrf-r2 + vrf_id: 9008012 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + attach: + - ip_address: 192.168.1.224 + - ip_address: 192.168.1.225 + +- name: MERGE | Create a VRF with VRF-Lite extensions cisco.dcnm.dcnm_vrf: fabric: vxlan-fabric state: merged config: - - vrf_name: ansible-vrf-r1 - vrf_id: 9008011 - vrf_template: Default_VRF_Universal - vrf_extension_template: Default_VRF_Extension_Universal - vlan_id: 2000 - service_vrf_template: null - attach: - - ip_address: 192.168.1.224 - - ip_address: 192.168.1.225 - vrf_lite: - - peer_vrf: test_vrf_1 # optional - interface: Ethernet1/16 # mandatory - ipv4_addr: 10.33.0.2/30 # optional - neighbor_ipv4: 10.33.0.1 # optional - ipv6_addr: 2010::10:34:0:7/64 # optional - neighbor_ipv6: 2010::10:34:0:3 # optional - dot1q: 2 # dot1q can be got from dcnm/optional - - peer_vrf: test_vrf_2 # optional - interface: Ethernet1/17 # mandatory - ipv4_addr: 20.33.0.2/30 # optional - neighbor_ipv4: 20.33.0.1 # optional - ipv6_addr: 3010::10:34:0:7/64 # optional - neighbor_ipv6: 3010::10:34:0:3 # optional - dot1q: 3 # dot1q can be got from dcnm/optional - -# The two VRFs below will be replaced in the target fabric. -- name: Replace vrfs + - vrf_name: ansible-vrf-r1 + vrf_id: 9008011 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + attach: + - ip_address: 192.168.1.224 + - ip_address: 192.168.1.225 + vrf_lite: + - peer_vrf: test_vrf_1 # optional + interface: Ethernet1/16 # mandatory + ipv4_addr: 192.168.0.2/30 # optional + neighbor_ipv4: 192.168.0.1 # optional + ipv6_addr: 2012::30:34:0:7/64 # optional + neighbor_ipv6: 2012::30:34:0:3 # optional + dot1q: 2 # dot1q can be got from ND/optional + - peer_vrf: test_vrf_2 # optional + interface: Ethernet1/17 # mandatory + ipv4_addr: 192.169.0.2/30 # optional + neighbor_ipv4: 192.169.0.1 # optional + ipv6_addr: 3000::30:34:0:7/64 # optional + neighbor_ipv6: 3000::30:34:0:3 # optional + dot1q: 3 # dot1q can be got from ND/optional + +- name: REPLACE | Update attachments for a VRF cisco.dcnm.dcnm_vrf: fabric: vxlan-fabric state: replaced config: - - vrf_name: ansible-vrf-r1 - vrf_id: 9008011 - vrf_template: Default_VRF_Universal - vrf_extension_template: Default_VRF_Extension_Universal - vlan_id: 2000 - service_vrf_template: null - attach: - - ip_address: 192.168.1.224 - # Delete this attachment - # - ip_address: 192.168.1.225 - # Create the following attachment - - ip_address: 192.168.1.226 - # Dont touch this if its present on DCNM - # - vrf_name: ansible-vrf-r2 - # vrf_id: 9008012 - # vrf_template: Default_VRF_Universal - # vrf_extension_template: Default_VRF_Extension_Universal - # attach: - # - ip_address: 192.168.1.224 - # - ip_address: 192.168.1.225 - -# The two VRFs below will be overridden in the target fabric. -- name: Override vrfs + - vrf_name: ansible-vrf-r1 + vrf_id: 9008011 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + attach: + - ip_address: 192.168.1.224 + # Delete this attachment + # - ip_address: 192.168.1.225 + # Create the following attachment + - ip_address: 192.168.1.226 + # Dont touch this if its present on ND + # - vrf_name: ansible-vrf-r2 + # vrf_id: 9008012 + # vrf_template: Default_VRF_Universal + # vrf_extension_template: Default_VRF_Extension_Universal + # attach: + # - ip_address: 192.168.1.224 + # - ip_address: 192.168.1.225 + +- name: OVERRIDE | Override all VRFs on a fabric cisco.dcnm.dcnm_vrf: fabric: vxlan-fabric state: overridden config: - - vrf_name: ansible-vrf-r1 - vrf_id: 9008011 - vrf_template: Default_VRF_Universal - vrf_extension_template: Default_VRF_Extension_Universal - vlan_id: 2000 - service_vrf_template: null - attach: - - ip_address: 192.168.1.224 - # Delete this attachment - # - ip_address: 192.168.1.225 - # Create the following attachment - - ip_address: 192.168.1.226 - # Delete this vrf - # - vrf_name: ansible-vrf-r2 - # vrf_id: 9008012 - # vrf_template: Default_VRF_Universal - # vrf_extension_template: Default_VRF_Extension_Universal - # vlan_id: 2000 - # service_vrf_template: null - # attach: - # - ip_address: 192.168.1.224 - # - ip_address: 192.168.1.225 - -- name: Delete selected vrfs + - vrf_name: ansible-vrf-r1 + vrf_id: 9008011 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + attach: + - ip_address: 192.168.1.224 + # Delete this attachment + # - ip_address: 192.168.1.225 + # Create the following attachment + - ip_address: 192.168.1.226 + # Delete this vrf + # - vrf_name: ansible-vrf-r2 + # vrf_id: 9008012 + # vrf_template: Default_VRF_Universal + # vrf_extension_template: Default_VRF_Extension_Universal + # vlan_id: 2000 + # attach: + # - ip_address: 192.168.1.224 + # - ip_address: 192.168.1.225 + +- name: DELETE | Delete selected VRFs cisco.dcnm.dcnm_vrf: fabric: vxlan-fabric state: deleted config: - - vrf_name: ansible-vrf-r1 - vrf_id: 9008011 - vrf_template: Default_VRF_Universal - vrf_extension_template: Default_VRF_Extension_Universal - vlan_id: 2000 - service_vrf_template: null - - vrf_name: ansible-vrf-r2 - vrf_id: 9008012 - vrf_template: Default_VRF_Universal - vrf_extension_template: Default_VRF_Extension_Universal - vlan_id: 2000 - service_vrf_template: null - -- name: Delete all the vrfs + - vrf_name: ansible-vrf-r1 + vrf_id: 9008011 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + - vrf_name: ansible-vrf-r2 + vrf_id: 9008012 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + +- name: DELETE | Delete all VRFs on a fabric cisco.dcnm.dcnm_vrf: fabric: vxlan-fabric state: deleted -- name: Query vrfs +- name: QUERY | Query specific VRFs cisco.dcnm.dcnm_vrf: fabric: vxlan-fabric state: query config: - - vrf_name: ansible-vrf-r1 - - vrf_name: ansible-vrf-r2 + - vrf_name: ansible-vrf-r1 + - vrf_name: ansible-vrf-r2 + +# =========================================================================== +# MSD (Multi-Site Domain) Fabric Examples +# =========================================================================== + +# Note: For fabrics which are "member" (part of an MSD fabric), +# operations are permitted only through the parent MSD fabric tasks. + +# --------------------------------------------------------------------------- +# STATE: MERGED - Create/Update VRFs on Parent and Child Fabrics +# --------------------------------------------------------------------------- + +- name: MSD MERGE | Create a VRF on Parent and extend to Child fabrics + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric # Must be the Parent MSD fabric + state: merged + config: + - vrf_name: ansible-vrf-msd-1 + vrf_id: 9008011 + vlan_id: 2000 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + # Attachments are for switches at the Parent fabric + attach: + - ip_address: 192.168.1.224 + - ip_address: 192.168.1.225 + # Define how this VRF behaves on each Child fabric + child_fabric_config: + - fabric: vxlan-child-fabric1 + adv_default_routes: true + adv_host_routes: false + - fabric: vxlan-child-fabric2 + adv_default_routes: false + adv_host_routes: true + - vrf_name: ansible-vrf-msd-2 # A second VRF in the same task + vrf_id: 9008012 + vlan_id: 2001 + child_fabric_config: + - fabric: vxlan-child-fabric1 + adv_default_routes: false + adv_host_routes: false + # Attachments are for switches at the Parent fabric + attach: + - ip_address: 192.168.1.224 + - ip_address: 192.168.1.225 + +- name: MSD MERGE | Create VRF with L3VNI and advanced routing settings + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: merged + config: + - vrf_name: ansible-vrf-advanced + vrf_id: 9008020 + vlan_id: 2020 + vrf_int_mtu: 9000 + max_bgp_paths: 4 + max_ibgp_paths: 4 + ipv6_linklocal_enable: true + # Parent-specific settings + redist_direct_rmap: CUSTOM-RMAP-REDIST + v6_redist_direct_rmap: CUSTOM-RMAP-REDIST-V6 + # Child fabric configuration with multicast settings + child_fabric_config: + - fabric: vxlan-child-fabric1 + l3vni_wo_vlan: true + trm_enable: true + trm_bgw_msite: true + rp_address: 10.1.1.1 + underlay_mcast_ip: 239.1.1.1 + overlay_mcast_group: 239.2.1.1 + - fabric: vxlan-child-fabric2 + bgp_password: 1234ABCD + bgp_passwd_encrypt: 7 + netflow_enable: true + nf_monitor: NETFLOW_MONITOR_1 + +# --------------------------------------------------------------------------- +# STATE: REPLACED - Replace VRF configuration on Parent and Child Fabrics +# --------------------------------------------------------------------------- + +- name: MSD REPLACE | Update VRF properties on Parent and Child fabrics + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: replaced + config: + - vrf_name: ansible-vrf-msd-1 + vrf_id: 9008011 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2000 + vrf_int_mtu: 9000 # Update MTU on Parent + # Child fabric configs are replaced: child1 is updated + child_fabric_config: + - fabric: vxlan-child-fabric1 + adv_default_routes: false # Value is updated + adv_host_routes: true # Value is updated + attach: + - ip_address: 192.168.1.224 + # Delete this attachment + # - ip_address: 192.168.1.225 + # Create the following attachment + - ip_address: 192.168.1.226 + # Dont touch this if its present on ND + # - vrf_name: ansible-vrf-r2 + # vrf_id: 9008012 + # vrf_template: Default_VRF_Universal + # vrf_extension_template: Default_VRF_Extension_Universal + # attach: + # - ip_address: 192.168.1.224 + # - ip_address: 192.168.1.225 + +- name: MSD REPLACE | Update VRF with route-target configuration + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: replaced + config: + - vrf_name: ansible-vrf-advanced + vrf_id: 9008020 + vlan_id: 2020 + # Parent route-target settings + disable_rt_auto: false + import_vpn_rt: "65000:10001,65000:10002" + export_vpn_rt: "65000:10001,65000:10002" + import_evpn_rt: "65000:20001,65000:20002" + export_evpn_rt: "65000:20001,65000:20002" + # Child fabric configuration updates + child_fabric_config: + - fabric: vxlan-child-fabric1 + trm_enable: true + import_mvpn_rt: "65000:30001" + export_mvpn_rt: "65000:30001" + +# --------------------------------------------------------------------------- +# STATE: OVERRIDDEN - Override all VRFs on Parent and Child Fabrics +# --------------------------------------------------------------------------- + +- name: MSD OVERRIDE | Override all VRFs ensuring only specified ones exist + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: overridden + config: + - vrf_name: ansible-vrf-production + vrf_id: 9008050 + vlan_id: 2050 + vrf_description: "Production VRF for critical workloads" + child_fabric_config: + - fabric: vxlan-child-fabric1 + adv_default_routes: true + static_default_route: true + - fabric: vxlan-child-fabric2 + adv_default_routes: true + static_default_route: true + attach: + - ip_address: 192.168.1.224 + - ip_address: 192.168.1.225 + # All other VRFs will be deleted from both parent and child fabrics + +# --------------------------------------------------------------------------- +# STATE: DELETED - Delete VRFs from Parent and all Child Fabrics +# --------------------------------------------------------------------------- + +- name: MSD DELETE | Delete a VRF from the Parent and all associated Child fabrics + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: deleted + config: + - vrf_name: ansible-vrf-msd-1 + # The 'child_fabric_config' parameter is not used or allowed for 'deleted' state. + +- name: MSD DELETE | Delete multiple VRFs from Parent and Child fabrics + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: deleted + config: + - vrf_name: ansible-vrf-msd-1 + - vrf_name: ansible-vrf-msd-2 + - vrf_name: ansible-vrf-advanced + +- name: MSD DELETE | Delete all VRFs from the Parent and all associated Child fabrics + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: deleted + +# --------------------------------------------------------------------------- +# STATE: QUERY - Query VRFs +# --------------------------------------------------------------------------- + +- name: MSD QUERY | Query specific VRFs on the Parent MSD fabric + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: query + config: + - vrf_name: ansible-vrf-msd-1 + - vrf_name: ansible-vrf-msd-2 + # The query will return the VRF's configuration on the parent + # and its attachments on all associated child fabrics. + +- name: MSD QUERY | Query all VRFs on the Parent MSD fabric + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: query + # No config specified - returns all VRFs + +- name: MSD QUERY | Query specific VRFs on the Child MSD fabric + cisco.dcnm.dcnm_vrf: + fabric: vxlan-child-fabric1 + state: query + config: + - vrf_name: ansible-vrf-msd-1 + - vrf_name: ansible-vrf-msd-2 + # The query will return the VRF's configuration on the child + # and its attachments. + +- name: MSD QUERY | Query all VRFs on the Child MSD fabric + cisco.dcnm.dcnm_vrf: + fabric: vxlan-child-fabric1 + state: query + # No config specified - returns all VRFs on the child. + +- name: MSD QUERY | Query specific VRFs on Parent & Child fabric + cisco.dcnm.dcnm_vrf: + fabric: vxlan-parent-fabric + state: query + config: + - vrf_name: ansible-vrf-msd-1 + child_fabric_config: + - fabric: vxlan-child-fabric1 + - vrf_name: ansible-vrf-msd-2 + child_fabric_config: + - fabric: vxlan-child-fabric2 + # The query will return the VRF's configuration on the parent and the + # configuration on the specified childs and its attachments at + # the parent and child level respectively. + """ import ast import copy @@ -672,6 +1064,7 @@ def __init__(self, module): # another vrf. Without this additional logic, the create+attach+deploy # go out first and complain the VLAN is already in use. self.diff_detach = [] + self.chg_deploy = {} self.have_deploy = {} self.want_deploy = {} self.diff_deploy = {} @@ -698,6 +1091,7 @@ def __init__(self, module): msg += f"{json.dumps(self.fabric_data, indent=4, sort_keys=True)}" self.log.debug(msg) + self.action_fabric_type = self.params.get("_fabric_type") self.fabric_type = self.fabric_data.get("fabricType") self.fabric_nvpairs = self.fabric_data.get("nvPairs") self.fabric_l3vni_wo_vlan = False @@ -1150,9 +1544,9 @@ def update_attach_params_extension_values(self, attach) -> dict: { "dot1q": 2, "interface": "Ethernet1/2", - "ipv4_addr": "10.33.0.2/30", + "ipv4_addr": "192.168.0.2/30", "ipv6_addr": "2010::10:34:0:7/64", - "neighbor_ipv4": "10.33.0.1", + "neighbor_ipv4": "192.168.0.1", "neighbor_ipv6": "2010::10:34:0:3", "peer_vrf": "ansible-vrf-int1" } @@ -1419,6 +1813,11 @@ def diff_for_create(self, want, have): skip_keys = ["vrfVlanId"] if vrfSegmentId_want is None: skip_keys.append("vrfSegmentId") + + template_skip_keys = self.get_template_skip_keys() + if template_skip_keys: + skip_keys.extend(template_skip_keys) + templates_differ = self.dict_values_differ( json_to_dict_want, json_to_dict_have, skip_keys=skip_keys ) @@ -1439,6 +1838,11 @@ def diff_for_create(self, want, have): # The vrf updates with missing vrfId will have to use existing # vrfId from the instance of the same vrf on DCNM. want["vrfId"] = have["vrfId"] + if skip_keys: + for key in skip_keys: + if key in json_to_dict_have: + json_to_dict_want[key] = json_to_dict_have[key] + want["vrfTemplateConfig"] = json.dumps(json_to_dict_want) create = want else: @@ -1591,6 +1995,7 @@ def get_have(self): have_create = [] have_deploy = {} + chg_deploy = {} curr_vrfs = "" @@ -1679,12 +2084,14 @@ def get_have(self): have_create.append(vrf) upd_vrfs = "" + chg_vrfs = "" for vrf_attach in vrf_attach_objects["DATA"]: if not vrf_attach.get("lanAttachList"): continue attach_list = vrf_attach["lanAttachList"] deploy_vrf = "" + change_vrf = "" for attach in attach_list: attach_state = bool(attach.get("isLanAttached", False)) deploy = attach_state @@ -1700,6 +2107,9 @@ def get_have(self): if deployed: deploy_vrf = attach["vrfName"] + if attach["lanAttachState"] in ("OUT-OF-SYNC", "PENDING"): + change_vrf = attach["vrfName"] + sn = attach["switchSerialNo"] vlan = attach["vlanId"] inst_values = attach.get("instanceValues", None) @@ -1786,14 +2196,21 @@ def get_have(self): if deploy_vrf: upd_vrfs += deploy_vrf + "," + if change_vrf: + chg_vrfs += change_vrf + "," + have_attach = vrf_attach_objects["DATA"] if upd_vrfs: have_deploy.update({"vrfNames": upd_vrfs[:-1]}) + if chg_vrfs: + chg_deploy.update({"vrfNames": chg_vrfs[:-1]}) + self.have_create = have_create self.have_attach = have_attach self.have_deploy = have_deploy + self.chg_deploy = chg_deploy msg = "self.have_create: " msg += f"{json.dumps(self.have_create, indent=4)}" @@ -1809,6 +2226,10 @@ def get_have(self): msg += f"{json.dumps(self.have_deploy, indent=4)}" self.log.debug(msg) + msg = "self.chg_deploy: " + msg += f"{json.dumps(self.chg_deploy, indent=4)}" + self.log.debug(msg) + def get_want(self): method_name = inspect.stack()[0][3] caller = inspect.stack()[1][3] @@ -1838,7 +2259,6 @@ def get_want(self): msg += f"vrf missing mandatory key vrf_name: {vrf}" self.module.fail_json(msg=msg) - all_vrfs.append(vrf_name) vrf_attach = {} vrfs = [] @@ -1852,6 +2272,11 @@ def get_want(self): else: vlan_id = 0 + vrf_deploy = vrf.get("deploy", True) + + if vrf_deploy: + all_vrfs.append(vrf_name) + want_create.append(self.update_create_params(vrf, vlan_id)) if not vrf.get("attach"): @@ -1889,6 +2314,24 @@ def get_want(self): msg += f"{json.dumps(self.want_deploy, indent=4)}" self.log.debug(msg) + def update_want(self): + + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + # If fabric type is multisite_child, no attachments + # are present in want. So copy have_attach to want_attach for + # processing deployments. + if self.action_fabric_type == "multisite_child": + self.want_attach = copy.deepcopy(self.have_attach) + + msg = "self.want_attach: " + msg += f"{json.dumps(self.want_attach, indent=4)}" + self.log.debug(msg) + def get_diff_delete(self): caller = inspect.stack()[1][3] @@ -1924,33 +2367,35 @@ def get_items_to_detach(attach_list): diff_delete.update({want_c["vrfName"]: "DEPLOYED"}) - have_a = self.find_dict_in_list_by_key_value( - search=self.have_attach, key="vrfName", value=want_c["vrfName"] - ) + if self.action_fabric_type != "multisite_child": + have_a = self.find_dict_in_list_by_key_value( + search=self.have_attach, key="vrfName", value=want_c["vrfName"] + ) - if not have_a: - continue + if not have_a: + continue - detach_items = get_items_to_detach(have_a["lanAttachList"]) - if detach_items: - have_a.update({"lanAttachList": detach_items}) - diff_detach.append(have_a) - all_vrfs.append(have_a["vrfName"]) - if len(all_vrfs) != 0: + detach_items = get_items_to_detach(have_a["lanAttachList"]) + if detach_items: + have_a.update({"lanAttachList": detach_items}) + diff_detach.append(have_a) + all_vrfs.append(have_a["vrfName"]) + + if len(all_vrfs) != 0 and self.action_fabric_type != "multisite_child": diff_undeploy.update({"vrfNames": ",".join(all_vrfs)}) else: - - for have_a in self.have_attach: - detach_items = get_items_to_detach(have_a["lanAttachList"]) - if detach_items: - have_a.update({"lanAttachList": detach_items}) - diff_detach.append(have_a) - all_vrfs.append(have_a["vrfName"]) - - diff_delete.update({have_a["vrfName"]: "DEPLOYED"}) - if len(all_vrfs) != 0: - diff_undeploy.update({"vrfNames": ",".join(all_vrfs)}) + if self.action_fabric_type != "multisite_child": + for have_a in self.have_attach: + detach_items = get_items_to_detach(have_a["lanAttachList"]) + if detach_items: + have_a.update({"lanAttachList": detach_items}) + diff_detach.append(have_a) + all_vrfs.append(have_a["vrfName"]) + + diff_delete.update({have_a["vrfName"]: "DEPLOYED"}) + if len(all_vrfs) != 0: + diff_undeploy.update({"vrfNames": ",".join(all_vrfs)}) self.diff_detach = diff_detach self.diff_undeploy = diff_undeploy @@ -1990,21 +2435,22 @@ def get_diff_override(self): detach_list = [] if not found: - for item in have_a["lanAttachList"]: - if "isAttached" in item: - if item["isAttached"]: - del item["isAttached"] - item.update({"deployment": False}) - detach_list.append(item) - - if detach_list: - have_a.update({"lanAttachList": detach_list}) - diff_detach.append(have_a) - all_vrfs.append(have_a["vrfName"]) + if self.action_fabric_type != "multisite_child": + for item in have_a["lanAttachList"]: + if "isAttached" in item: + if item["isAttached"]: + del item["isAttached"] + item.update({"deployment": False}) + detach_list.append(item) + + if detach_list: + have_a.update({"lanAttachList": detach_list}) + diff_detach.append(have_a) + all_vrfs.append(have_a["vrfName"]) diff_delete.update({have_a["vrfName"]: "DEPLOYED"}) - if len(all_vrfs) != 0: + if len(all_vrfs) != 0 and self.action_fabric_type != "multisite_child": diff_undeploy.update({"vrfNames": ",".join(all_vrfs)}) self.diff_delete = diff_delete @@ -2033,6 +2479,7 @@ def get_diff_replace(self): all_vrfs = [] self.get_diff_merge(replace=True) + diff_attach = self.diff_attach diff_deploy = self.diff_deploy @@ -2363,6 +2810,8 @@ def diff_merge_attach(self, replace=False): want_config = self.find_dict_in_list_by_key_value( search=self.config, key="vrf_name", value=want_a["vrfName"] ) + if not want_config: + continue deploy_vrf = "" attach_found = False for have_a in self.have_attach: @@ -2436,6 +2885,42 @@ def diff_merge_attach(self, replace=False): msg += f"{json.dumps(self.diff_deploy, indent=4)}" self.log.debug(msg) + def diff_merge_no_attach(self): + caller = inspect.stack()[1][3] + + msg = "ENTERED. " + msg += f"caller: {caller}. " + self.log.debug(msg) + + diff_deploy = self.diff_deploy + all_vrfs = [] + + if not self.want_deploy or not self.chg_deploy: + msg = "No vrfs to deploy. Returning" + self.log.debug(msg) + return + + for vrf_name in self.want_deploy["vrfNames"].split(","): + msg = f"VRF Name : {vrf_name}" + self.log.debug(msg) + if not self.diff_attach and vrf_name in self.chg_deploy["vrfNames"].split(","): + want_vrf_data = find_dict_in_list_by_key_value(search=self.config, key="vrf_name", value=vrf_name) + if want_vrf_data.get("deploy", True) is True: + all_vrfs.append(vrf_name) + + if all_vrfs: + if not diff_deploy: + diff_deploy.update({"vrfNames": ",".join(all_vrfs)}) + else: + vrfs = self.diff_deploy["vrfNames"] + "," + ",".join(all_vrfs) + diff_deploy.update({"vrfNames": vrfs}) + + self.diff_deploy = diff_deploy + + msg = "self.diff_deploy: " + msg += f"{json.dumps(self.diff_deploy, indent=4)}" + self.log.debug(msg) + def get_diff_merge(self, replace=False): caller = inspect.stack()[1][3] @@ -2453,6 +2938,7 @@ def get_diff_merge(self, replace=False): self.diff_merge_create(replace) self.diff_merge_attach(replace) + self.diff_merge_no_attach() def format_diff(self): caller = inspect.stack()[1][3] @@ -2508,6 +2994,13 @@ def format_diff(self): diff_attach.extend(diff_detach) diff_deploy.extend(diff_undeploy) + # Get the VRF spec to determine which properties should be included in diff + vrf_spec = self.get_vrf_spec() + + msg = "vrf_spec for diff formatting: " + msg += f"{json.dumps(vrf_spec, indent=4, sort_keys=True)}" + self.log.debug(msg) + for want_d in diff_create: msg = "want_d: " @@ -2528,112 +3021,53 @@ def format_diff(self): msg += f"{json.dumps(found_c, indent=4, sort_keys=True)}" self.log.debug(msg) - src = found_c["source"] - found_c.update({"vrf_name": found_c["vrfName"]}) - found_c.update({"vrf_id": found_c["vrfId"]}) - found_c.update({"vrf_template": found_c["vrfTemplate"]}) - found_c.update({"vrf_extension_template": found_c["vrfExtensionTemplate"]}) - del found_c["source"] - found_c.update({"source": src}) - found_c.update({"service_vrf_template": found_c["serviceVrfTemplate"]}) - found_c.update({"attach": []}) - + # Extract template configuration json_to_dict = json.loads(found_c["vrfTemplateConfig"]) - found_c.update({"vrf_vlan_name": json_to_dict.get("vrfVlanName", "")}) - found_c.update( - {"vrf_intf_desc": json_to_dict.get("vrfIntfDescription", "")} - ) - found_c.update({"vrf_description": json_to_dict.get("vrfDescription", "")}) - found_c.update({"vrf_int_mtu": json_to_dict.get("mtu", "")}) - found_c.update({"loopback_route_tag": json_to_dict.get("tag", "")}) - found_c.update({"redist_direct_rmap": json_to_dict.get("vrfRouteMap", "")}) - found_c.update({"v6_redist_direct_rmap": json_to_dict.get("v6VrfRouteMap", "")}) - found_c.update({"max_bgp_paths": json_to_dict.get("maxBgpPaths", "")}) - found_c.update({"max_ibgp_paths": json_to_dict.get("maxIbgpPaths", "")}) - found_c.update( - {"ipv6_linklocal_enable": json_to_dict.get("ipv6LinkLocalFlag", True)} - ) - found_c.update({"l3vni_wo_vlan": json_to_dict.get("enableL3VniNoVlan", False)}) - found_c.update({"trm_enable": json_to_dict.get("trmEnabled", False)}) - found_c.update({"rp_external": json_to_dict.get("isRPExternal", False)}) - found_c.update({"rp_address": json_to_dict.get("rpAddress", "")}) - found_c.update({"rp_loopback_id": json_to_dict.get("loopbackNumber", "")}) - found_c.update( - {"underlay_mcast_ip": json_to_dict.get("L3VniMcastGroup", "")} - ) - found_c.update( - {"overlay_mcast_group": json_to_dict.get("multicastGroup", "")} - ) - found_c.update( - {"trm_bgw_msite": json_to_dict.get("trmBGWMSiteEnabled", False)} - ) - found_c.update( - {"adv_host_routes": json_to_dict.get("advertiseHostRouteFlag", False)} - ) - found_c.update( - { - "adv_default_routes": json_to_dict.get( - "advertiseDefaultRouteFlag", True - ) - } - ) - found_c.update( - { - "static_default_route": json_to_dict.get( - "configureStaticDefaultRouteFlag", True - ) - } - ) - found_c.update({"bgp_password": json_to_dict.get("bgpPassword", "")}) - found_c.update( - {"bgp_passwd_encrypt": json_to_dict.get("bgpPasswordKeyType", "")} - ) - if self.dcnm_version > 11: - found_c.update({"no_rp": json_to_dict.get("isRPAbsent", False)}) - found_c.update( - {"netflow_enable": json_to_dict.get("ENABLE_NETFLOW", True)} - ) - found_c.update({"nf_monitor": json_to_dict.get("NETFLOW_MONITOR", "")}) - found_c.update( - {"disable_rt_auto": json_to_dict.get("disableRtAuto", False)} - ) - found_c.update( - {"import_vpn_rt": json_to_dict.get("routeTargetImport", "")} - ) - found_c.update( - {"export_vpn_rt": json_to_dict.get("routeTargetExport", "")} - ) - found_c.update( - {"import_evpn_rt": json_to_dict.get("routeTargetImportEvpn", "")} - ) - found_c.update( - {"export_evpn_rt": json_to_dict.get("routeTargetExportEvpn", "")} - ) - found_c.update( - {"import_mvpn_rt": json_to_dict.get("routeTargetImportMvpn", "")} - ) - found_c.update( - {"export_mvpn_rt": json_to_dict.get("routeTargetExportMvpn", "")} - ) - del found_c["fabric"] - del found_c["vrfName"] - del found_c["vrfId"] - del found_c["vrfTemplate"] - del found_c["vrfExtensionTemplate"] - del found_c["serviceVrfTemplate"] - del found_c["vrfTemplateConfig"] + # Initialize the output dict with basic required fields + src = found_c["source"] + formatted_vrf = { + "vrf_name": found_c["vrfName"], + "source": src, + } - msg = "found_c: POST_UPDATE: " - msg += f"{json.dumps(found_c, indent=4, sort_keys=True)}" + formatted_vrf.update({"attach": []}) + + # Get property mappings for both template and VRF object properties + template_mappings, vrf_object_mappings = self.get_property_mappings() + + # Process each property defined in the VRF spec + for spec_key in vrf_spec.keys(): + if spec_key in ["vrf_name", "attach", "deploy", "source"]: + continue # These are handled separately + + # Handle template properties + if spec_key in template_mappings: + template_key = template_mappings[spec_key] + if template_key and template_key in json_to_dict: + formatted_vrf[spec_key] = json_to_dict[template_key] + elif "default" in vrf_spec[spec_key]: + formatted_vrf[spec_key] = vrf_spec[spec_key]["default"] + + # Handle VRF object properties + elif spec_key in vrf_object_mappings: + vrf_key = vrf_object_mappings[spec_key] + if vrf_key and vrf_key in found_c: + formatted_vrf[spec_key] = found_c[vrf_key] + elif "default" in vrf_spec[spec_key]: + formatted_vrf[spec_key] = vrf_spec[spec_key]["default"] + + msg = "formatted_vrf: POST_UPDATE: " + msg += f"{json.dumps(formatted_vrf, indent=4, sort_keys=True)}" self.log.debug(msg) - if diff_deploy and found_c["vrf_name"] in diff_deploy: - diff_deploy.remove(found_c["vrf_name"]) + if diff_deploy and formatted_vrf["vrf_name"] in diff_deploy: + diff_deploy.remove(formatted_vrf["vrf_name"]) + if not found_a: - msg = "not found_a. Appending found_c to diff." + msg = "not found_a. Appending formatted_vrf to diff." self.log.debug(msg) - diff.append(found_c) + diff.append(formatted_vrf) continue attach = found_a["lanAttachList"] @@ -2647,13 +3081,12 @@ def format_diff(self): break attach_d.update({"vlan_id": a_w["vlan"]}) attach_d.update({"deploy": a_w["deployment"]}) - found_c["attach"].append(attach_d) + formatted_vrf["attach"].append(attach_d) - msg = "Appending found_c to diff." + msg = "Appending formatted_vrf to diff." self.log.debug(msg) - diff.append(found_c) - + diff.append(formatted_vrf) diff_attach.remove(found_a) for vrf in diff_attach: @@ -2888,8 +3321,9 @@ def push_diff_detach(self, is_rollback=False): msg += f"{json.dumps(self.diff_detach, indent=4, sort_keys=True)}" self.log.debug(msg) - if not self.diff_detach: - msg = "Early return. self.diff_detach is empty." + if not self.diff_detach or self.action_fabric_type == "multisite_child": + msg = f"Early return. Fabric Type:{self.action_fabric_type}" + msg += f"diff_detach: {json.dumps(self.diff_detach, indent=4, sort_keys=True)}" self.log.debug(msg) return @@ -2933,8 +3367,9 @@ def push_diff_undeploy(self, is_rollback=False): msg += f"{json.dumps(self.diff_undeploy, indent=4, sort_keys=True)}" self.log.debug(msg) - if not self.diff_undeploy: - msg = "Early return. self.diff_undeploy is empty." + if not self.diff_undeploy or self.action_fabric_type == "multisite_child": + msg = f"Early return. Fabric Type:{self.action_fabric_type}" + msg += f"diff_deploy: {json.dumps(self.diff_attach, indent=4, sort_keys=True)}" self.log.debug(msg) return @@ -2966,8 +3401,9 @@ def push_diff_delete(self, is_rollback=False): msg += f"{json.dumps(self.diff_delete, indent=4, sort_keys=True)}" self.log.debug(msg) - if not self.diff_delete: - msg = "Early return. self.diff_delete is None." + if not self.diff_delete or self.action_fabric_type == "multisite_child": + msg = f"Early return. Fabric Type:{self.action_fabric_type}" + msg += f"diff_delete: {json.dumps(self.diff_delete, indent=4, sort_keys=True)}" self.log.debug(msg) return @@ -3606,9 +4042,9 @@ def push_diff_attach(self, is_rollback=False): msg += f"{json.dumps(self.diff_attach, indent=4, sort_keys=True)}" self.log.debug(msg) - if not self.diff_attach: - msg = "Early return. self.diff_attach is empty. " - msg += f"{json.dumps(self.diff_attach, indent=4, sort_keys=True)}" + if not self.diff_attach or self.action_fabric_type == "multisite_child": + msg = f"Early return. Fabric Type:{self.action_fabric_type}" + msg += f"diff_attach: {json.dumps(self.diff_attach, indent=4, sort_keys=True)}" self.log.debug(msg) return @@ -3737,7 +4173,8 @@ def push_diff_deploy(self, is_rollback=False): self.log.debug(msg) if not self.diff_deploy: - msg = "Early return. self.diff_deploy is empty." + msg = f"Early return. Fabric Type:{self.action_fabric_type}" + msg += f"diff_deploy: {json.dumps(self.diff_attach, indent=4, sort_keys=True)}" self.log.debug(msg) return @@ -3876,33 +4313,33 @@ def release_orphaned_resources(self, vrf_del_list, is_rollback=False): self.failure(resp) delete_ids = [] - for item in resp["DATA"]: - if "entityName" not in item: - continue - if item["entityName"] not in vrf_del_list: - continue - if item.get("allocatedFlag") is not False: - continue - if item.get("id") is None: - continue - # Resources with no ipAddress or switchName - # are invalid and of Fabric's scope and - # should not be attempted to be deleted here. - if not item.get("ipAddress"): - continue - if not item.get("switchName"): - continue + if resp.get("DATA"): + for item in resp["DATA"]: + if "entityName" not in item: + continue + if item["entityName"] not in vrf_del_list: + continue + if item.get("allocatedFlag") is not False: + continue + if item.get("id") is None: + continue + # Resources with no ipAddress or switchName + # are invalid and of Fabric's scope and + # should not be attempted to be deleted here. + if not item.get("ipAddress"): + continue + if not item.get("switchName"): + continue - msg = f"item {json.dumps(item, indent=4, sort_keys=True)}" - self.log.debug(msg) + msg = f"item {json.dumps(item, indent=4, sort_keys=True)}" + self.log.debug(msg) - delete_ids.append(item["id"]) + delete_ids.append(item["id"]) - if len(delete_ids) == 0: - return - msg = f"Releasing orphaned resources with IDs:{delete_ids}" - self.log.debug(msg) - self.release_resources_by_id(delete_ids) + if len(delete_ids) != 0: + msg = f"Releasing orphaned resources with IDs:{delete_ids}" + self.log.debug(msg) + self.release_resources_by_id(delete_ids) def push_to_remote(self, is_rollback=False): """ @@ -4046,100 +4483,272 @@ def lite_spec(self): return copy.deepcopy(spec) - def vrf_spec(self): - """ - # Summary - - Return the argument spec for VRF parameters - """ - spec = {} - spec["adv_default_routes"] = {"default": True, "type": "bool"} - spec["adv_host_routes"] = {"default": False, "type": "bool"} - - spec["attach"] = {"type": "list"} - spec["bgp_password"] = {"default": "", "type": "str"} - spec["bgp_passwd_encrypt"] = {"choices": [3, 7], "default": 3, "type": "int"} - spec["disable_rt_auto"] = {"default": False, "type": "bool"} - - spec["export_evpn_rt"] = {"default": "", "type": "str"} - spec["export_mvpn_rt"] = {"default": "", "type": "str"} - spec["export_vpn_rt"] = {"default": "", "type": "str"} - - spec["import_evpn_rt"] = {"default": "", "type": "str"} - spec["import_mvpn_rt"] = {"default": "", "type": "str"} - spec["import_vpn_rt"] = {"default": "", "type": "str"} - - spec["ipv6_linklocal_enable"] = {"default": True, "type": "bool"} + def get_vrf_spec(self): + """Return VRF spec based on fabric type and hierarchy""" + base_spec = { + "vrf_name": {"length_max": 32, "required": True, "type": "str"}, + "attach": {"type": "list"}, + } - spec["l3vni_wo_vlan"] = {"default": self.fabric_l3vni_wo_vlan, "type": "bool"} - spec["loopback_route_tag"] = { - "default": 12345, - "range_max": 4294967295, - "type": "int", + # Add deploy spec based on state + if self.state in ("merged", "overridden", "replaced"): + base_spec["deploy"] = {"default": True, "type": "bool"} + else: + base_spec["deploy"] = {"type": "bool"} + + # Add specs based on fabric type + if self.action_fabric_type == 'multisite_parent': + base_spec.update(self.get_parent_msd_specs()) + elif self.action_fabric_type == 'multisite_child': + base_spec.update(self.get_child_msd_specs()) + else: # standalone + base_spec.update(self.get_standalone_specs()) + + return base_spec + + def get_parent_msd_specs(self): + """Return Parent MSD specific VRF parameters""" + spec = { + "vrf_id": {"range_max": 16777214, "type": "int"}, + "vlan_id": {"range_max": 4094, "type": "int"}, + "vrf_template": {"default": "Default_VRF_Universal", "type": "str"}, + "vrf_extension_template": { + "default": "Default_VRF_Extension_Universal", + "type": "str", + }, + "vrf_vlan_name": {"default": "", "type": "str"}, + "vrf_intf_desc": {"default": "", "type": "str"}, + "vrf_description": {"default": "", "type": "str"}, + "vrf_int_mtu": { + "default": 9216, + "range_max": 9216, + "range_min": 68, + "type": "int", + }, + "loopback_route_tag": { + "default": 12345, + "range_max": 4294967295, + "type": "int", + }, + "redist_direct_rmap": { + "default": "FABRIC-RMAP-REDIST-SUBNET", + "type": "str", + }, + "v6_redist_direct_rmap": { + "default": "FABRIC-RMAP-REDIST-SUBNET", + "type": "str", + }, + "max_bgp_paths": { + "default": 1, + "range_max": 64, + "range_min": 1, + "type": "int", + }, + "max_ibgp_paths": { + "default": 2, + "range_max": 64, + "range_min": 1, + "type": "int", + }, + "ipv6_linklocal_enable": {"default": True, "type": "bool"}, + "disable_rt_auto": {"default": False, "type": "bool"}, + "import_vpn_rt": {"default": "", "type": "str"}, + "export_vpn_rt": {"default": "", "type": "str"}, + "import_evpn_rt": {"default": "", "type": "str"}, + "export_evpn_rt": {"default": "", "type": "str"}, + "service_vrf_template": {"default": None, "type": "str"}, + "source": {"default": None, "type": "str"}, } - spec["max_bgp_paths"] = { - "default": 1, - "range_max": 64, - "range_min": 1, - "type": "int", + return spec + + def get_child_msd_specs(self): + """Return Child MSD specific VRF parameters""" + spec = { + "l3vni_wo_vlan": {"default": self.fabric_l3vni_wo_vlan, "type": "bool"}, + "adv_default_routes": {"default": True, "type": "bool"}, + "adv_host_routes": {"default": False, "type": "bool"}, + "static_default_route": {"default": True, "type": "bool"}, + "bgp_password": {"default": "", "type": "str"}, + "bgp_passwd_encrypt": {"choices": [3, 7], "default": 3, "type": "int"}, + "netflow_enable": {"default": False, "type": "bool"}, + "nf_monitor": {"default": "", "type": "str"}, + "trm_enable": {"default": False, "type": "bool"}, + "no_rp": {"default": False, "type": "bool"}, + "rp_address": {"default": "", "type": "str"}, + "rp_external": {"default": False, "type": "bool"}, + "rp_loopback_id": {"default": "", "range_max": 1023, "type": "int"}, + "underlay_mcast_ip": {"default": "", "type": "str"}, + "overlay_mcast_group": {"default": "", "type": "str"}, + "trm_bgw_msite": {"default": False, "type": "bool"}, + "import_mvpn_rt": {"default": "", "type": "str"}, + "export_mvpn_rt": {"default": "", "type": "str"}, } - spec["max_ibgp_paths"] = { - "default": 2, - "range_max": 64, - "range_min": 1, - "type": "int", + return spec + + def get_standalone_specs(self): + """Return standalone (non-MSD) VRF parameters""" + spec = { + "adv_default_routes": {"default": True, "type": "bool"}, + "adv_host_routes": {"default": False, "type": "bool"}, + "bgp_password": {"default": "", "type": "str"}, + "bgp_passwd_encrypt": {"choices": [3, 7], "default": 3, "type": "int"}, + "disable_rt_auto": {"default": False, "type": "bool"}, + "export_evpn_rt": {"default": "", "type": "str"}, + "export_mvpn_rt": {"default": "", "type": "str"}, + "export_vpn_rt": {"default": "", "type": "str"}, + "import_evpn_rt": {"default": "", "type": "str"}, + "import_mvpn_rt": {"default": "", "type": "str"}, + "import_vpn_rt": {"default": "", "type": "str"}, + "ipv6_linklocal_enable": {"default": True, "type": "bool"}, + "l3vni_wo_vlan": {"default": self.fabric_l3vni_wo_vlan, "type": "bool"}, + "loopback_route_tag": { + "default": 12345, + "range_max": 4294967295, + "type": "int", + }, + "max_bgp_paths": { + "default": 1, + "range_max": 64, + "range_min": 1, + "type": "int", + }, + "max_ibgp_paths": { + "default": 2, + "range_max": 64, + "range_min": 1, + "type": "int", + }, + "netflow_enable": {"default": False, "type": "bool"}, + "nf_monitor": {"default": "", "type": "str"}, + "no_rp": {"default": False, "type": "bool"}, + "overlay_mcast_group": {"default": "", "type": "str"}, + "redist_direct_rmap": { + "default": "FABRIC-RMAP-REDIST-SUBNET", + "type": "str", + }, + "v6_redist_direct_rmap": { + "default": "FABRIC-RMAP-REDIST-SUBNET", + "type": "str", + }, + "rp_address": {"default": "", "type": "str"}, + "rp_external": {"default": False, "type": "bool"}, + "rp_loopback_id": {"default": "", "range_max": 1023, "type": "int"}, + "service_vrf_template": {"default": None, "type": "str"}, + "source": {"default": None, "type": "str"}, + "static_default_route": {"default": True, "type": "bool"}, + "trm_bgw_msite": {"default": False, "type": "bool"}, + "trm_enable": {"default": False, "type": "bool"}, + "underlay_mcast_ip": {"default": "", "type": "str"}, + "vlan_id": {"range_max": 4094, "type": "int"}, + "vrf_description": {"default": "", "type": "str"}, + "vrf_id": {"range_max": 16777214, "type": "int"}, + "vrf_intf_desc": {"default": "", "type": "str"}, + "vrf_int_mtu": { + "default": 9216, + "range_max": 9216, + "range_min": 68, + "type": "int", + }, + "vrf_template": {"default": "Default_VRF_Universal", "type": "str"}, + "vrf_extension_template": { + "default": "Default_VRF_Extension_Universal", + "type": "str", + }, + "vrf_vlan_name": {"default": "", "type": "str"}, } - spec["netflow_enable"] = {"default": False, "type": "bool"} - spec["nf_monitor"] = {"default": "", "type": "str"} + return spec + + def get_template_skip_keys(self): + """Return template configuration keys to skip comparison based on fabric type""" + + if self.action_fabric_type == "multisite_parent": + return self.get_child_msd_template_keys() + elif self.action_fabric_type == "multisite_child": + return self.get_parent_msd_template_keys() + else: # standalone + return None + + def get_parent_msd_template_keys(self): + """Return Parent MSD template configuration keys for comparison""" + return [ + "vrfName", "vrfVlanName", "vrfIntfDescription", + "vrfDescription", "mtu", "tag", "vrfRouteMap", "v6VrfRouteMap", + "maxBgpPaths", "maxIbgpPaths", "ipv6LinkLocalFlag", + "disableRtAuto", "routeTargetImport", "routeTargetExport", + "routeTargetImportEvpn", "routeTargetExportEvpn" + ] - spec["no_rp"] = {"default": False, "type": "bool"} - spec["overlay_mcast_group"] = {"default": "", "type": "str"} + def get_child_msd_template_keys(self): + """Return Child MSD template configuration keys for comparison""" + return [ + "enableL3VniNoVlan", "advertiseDefaultRouteFlag", + "advertiseHostRouteFlag", "configureStaticDefaultRouteFlag", "bgpPassword", + "bgpPasswordKeyType", "ENABLE_NETFLOW", "NETFLOW_MONITOR", "trmEnabled", + "isRPAbsent", "rpAddress", "isRPExternal", "loopbackNumber", + "L3VniMcastGroup", "multicastGroup", "trmBGWMSiteEnabled", + "routeTargetImportMvpn", "routeTargetExportMvpn" + ] - spec["redist_direct_rmap"] = { - "default": "FABRIC-RMAP-REDIST-SUBNET", - "type": "str", - } - spec["v6_redist_direct_rmap"] = { - "default": "FABRIC-RMAP-REDIST-SUBNET", - "type": "str", - } - spec["rp_address"] = {"default": "", "type": "str"} - spec["rp_external"] = {"default": False, "type": "bool"} - spec["rp_loopback_id"] = {"default": "", "range_max": 1023, "type": "int"} - - spec["service_vrf_template"] = {"default": None, "type": "str"} - spec["source"] = {"default": None, "type": "str"} - spec["static_default_route"] = {"default": True, "type": "bool"} - - spec["trm_bgw_msite"] = {"default": False, "type": "bool"} - spec["trm_enable"] = {"default": False, "type": "bool"} - - spec["underlay_mcast_ip"] = {"default": "", "type": "str"} - - spec["vlan_id"] = {"range_max": 4094, "type": "int"} - spec["vrf_description"] = {"default": "", "type": "str"} - spec["vrf_id"] = {"range_max": 16777214, "type": "int"} - spec["vrf_intf_desc"] = {"default": "", "type": "str"} - spec["vrf_int_mtu"] = { - "default": 9216, - "range_max": 9216, - "range_min": 68, - "type": "int", - } - spec["vrf_name"] = {"length_max": 32, "required": True, "type": "str"} - spec["vrf_template"] = {"default": "Default_VRF_Universal", "type": "str"} - spec["vrf_extension_template"] = { - "default": "Default_VRF_Extension_Universal", - "type": "str", + def get_property_mappings(self): + """ + Return mappings for both template and VRF object properties. + + Returns: + tuple: (template_mappings, vrf_object_mappings) + """ + # Base properties available in all NDFC versions + template_mappings = { + "vrf_id": "vrfSegmentId", + "vlan_id": "vrfVlanId", + "vrf_vlan_name": "vrfVlanName", + "vrf_intf_desc": "vrfIntfDescription", + "vrf_description": "vrfDescription", + "vrf_int_mtu": "mtu", + "loopback_route_tag": "tag", + "redist_direct_rmap": "vrfRouteMap", + "v6_redist_direct_rmap": "v6VrfRouteMap", + "max_bgp_paths": "maxBgpPaths", + "max_ibgp_paths": "maxIbgpPaths", + "ipv6_linklocal_enable": "ipv6LinkLocalFlag", + "l3vni_wo_vlan": "enableL3VniNoVlan", + "trm_enable": "trmEnabled", + "rp_external": "isRPExternal", + "rp_address": "rpAddress", + "rp_loopback_id": "loopbackNumber", + "underlay_mcast_ip": "L3VniMcastGroup", + "overlay_mcast_group": "multicastGroup", + "trm_bgw_msite": "trmBGWMSiteEnabled", + "adv_host_routes": "advertiseHostRouteFlag", + "adv_default_routes": "advertiseDefaultRouteFlag", + "static_default_route": "configureStaticDefaultRouteFlag", + "bgp_password": "bgpPassword", + "bgp_passwd_encrypt": "bgpPasswordKeyType", } - spec["vrf_vlan_name"] = {"default": "", "type": "str"} - if self.state in ("merged", "overridden", "replaced"): - spec["deploy"] = {"default": True, "type": "bool"} - else: - spec["deploy"] = {"type": "bool"} + # Add ND version 12+ specific properties + if self.dcnm_version > 11: + dcnm_12_mappings = { + "no_rp": "isRPAbsent", + "netflow_enable": "ENABLE_NETFLOW", + "nf_monitor": "NETFLOW_MONITOR", + "disable_rt_auto": "disableRtAuto", + "import_vpn_rt": "routeTargetImport", + "export_vpn_rt": "routeTargetExport", + "import_evpn_rt": "routeTargetImportEvpn", + "export_evpn_rt": "routeTargetExportEvpn", + "import_mvpn_rt": "routeTargetImportMvpn", + "export_mvpn_rt": "routeTargetExportMvpn", + } + template_mappings.update(dcnm_12_mappings) - return copy.deepcopy(spec) + # VRF object properties (same for all versions) + vrf_object_mappings = { + "vrf_template": "vrfTemplate", + "vrf_extension_template": "vrfExtensionTemplate", + "service_vrf_template": "serviceVrfTemplate", + } + + return template_mappings, vrf_object_mappings def validate_input(self): """Parse the playbook values, validate to param specs.""" @@ -4148,7 +4757,7 @@ def validate_input(self): attach_spec = self.attach_spec() lite_spec = self.lite_spec() - vrf_spec = self.vrf_spec() + vrf_spec = self.get_vrf_spec() msg = "attach_spec: " msg += f"{json.dumps(attach_spec, indent=4, sort_keys=True)}" @@ -4176,6 +4785,9 @@ def validate_input(self): if not vrf.get("service_vrf_template"): vrf["service_vrf_template"] = None + if "deploy" not in vrf: + vrf["deploy"] = True + if "vrf_name" not in vrf: fail_msg_list.append( "vrf_name is mandatory under vrf parameters" @@ -4317,14 +4929,12 @@ def handle_response(self, res, op): if op == "attach" and "is in use already" in str(res.values()): fail = True changed = False - if op == "deploy" and "No switches PENDING for deployment" in str(res.values()): - changed = False return fail, changed def failure(self, resp): # Do not Rollback for Multi-site fabrics - if self.fabric_type == "MFD": + if self.fabric_type == "MFD" or self.action_fabric_type != "standalone": self.failed_to_rollback = True self.module.fail_json(msg=resp) return @@ -4386,6 +4996,11 @@ def main(): default="merged", choices=["merged", "replaced", "deleted", "overridden", "query"], ), + _fabric_type=dict( + default="standalone", + choices=['multisite_child', 'multisite_parent', 'standalone'], + required=False, + type="str") ) module = AnsibleModule(argument_spec=element_spec, supports_check_mode=True) @@ -4401,6 +5016,7 @@ def main(): dcnm_vrf.get_want() dcnm_vrf.get_have() + dcnm_vrf.update_want() if module.params["state"] == "merged": dcnm_vrf.get_diff_merge() diff --git a/tests/integration/targets/dcnm_network/tasks/main.yaml b/tests/integration/targets/dcnm_network/tasks/main.yaml index c406aacec..45345a249 100644 --- a/tests/integration/targets/dcnm_network/tasks/main.yaml +++ b/tests/integration/targets/dcnm_network/tasks/main.yaml @@ -39,6 +39,13 @@ delegate_to: localhost tags: always +- name: MAIN - DELETED - delete all networks + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: deleted + tags: always + + - name: MAIN - OVERRIDDEN - Create vrfs required for this test and remove all other vrfs cisco.dcnm.dcnm_vrf: fabric: "{{ test_data_common.fabric }}" @@ -46,19 +53,36 @@ config: "{{ dcnm_network_setup_vrf_conf }}" tags: always -- name: MAIN - Collect DCNM Network Test Cases +- name: MAIN - Set Full Test Case Path + set_fact: + full_testcase_path: "{{ role_path }}/tests/dcnm/{{ testcase }}.yaml" + testcase_is_glob: "{{ testcase.endswith('/*') }}" + testcase_dir: "{{ role_path }}/tests/dcnm/{{ testcase | regex_replace('/\\*$', '') }}" + delegate_to: localhost + tags: always + +- name: MAIN - Check if Test Case File Exists (single file) + stat: + path: "{{ full_testcase_path }}" + register: testcase_stat + delegate_to: localhost + when: not testcase_is_glob + tags: always + +- name: MAIN - Find Test Case Files (glob pattern) find: - paths: ["{{ role_path }}/tests/dcnm", "{{ role_path }}/tests/dcnm/self-contained-tests"] - patterns: "{{ testcase }}.yaml" - connection: local - register: dcnm_cases + paths: "{{ testcase_dir }}" + patterns: "*.yaml" + recurse: no + register: testcase_glob delegate_to: localhost + when: testcase_is_glob tags: always - name: MAIN - Set Test Cases Fact set_fact: test_cases: - files: "{{ dcnm_cases.files }}" + files: "{{ [{'path': full_testcase_path}] if (not testcase_is_glob and testcase_stat.stat.exists) else (testcase_glob.files if testcase_is_glob else []) }}" delegate_to: localhost tags: always @@ -74,6 +98,12 @@ loop_var: test_case_to_run tags: always +- name: MAIN - DELETED - delete all networks + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: deleted + tags: always + - name: MAIN - DELETED - Clean Up Any Existing VRFs cisco.dcnm.dcnm_vrf: fabric: "{{ test_data_common.fabric }}" diff --git a/tests/integration/targets/dcnm_network/templates/msd_deleted/dcnm_network_msd_deleted_multi_conf.j2 b/tests/integration/targets/dcnm_network/templates/msd_deleted/dcnm_network_msd_deleted_multi_conf.j2 new file mode 100644 index 000000000..c552cf78a --- /dev/null +++ b/tests/integration/targets/dcnm_network/templates/msd_deleted/dcnm_network_msd_deleted_multi_conf.j2 @@ -0,0 +1,16 @@ +--- +# This MSD test data structure is auto-generated +# DO NOT EDIT MANUALLY +# + +# ------------------------------ +# Delete multiple MSD Networks +# ------------------------------ +- net_name: "{{ test_data_common.msd_net1 }}" + vrf_name: "{{ test_data_common.net1_vrf }}" + +- net_name: "{{ test_data_common.msd_net2 }}" + vrf_name: "{{ test_data_common.net2_vrf }}" + +- net_name: "{{ test_data_common.msd_l2_net }}" + vrf_name: "{{ test_data_common.net1_vrf }}" \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/templates/msd_deleted/dcnm_network_msd_deleted_net_conf.j2 b/tests/integration/targets/dcnm_network/templates/msd_deleted/dcnm_network_msd_deleted_net_conf.j2 new file mode 100644 index 000000000..5aab50170 --- /dev/null +++ b/tests/integration/targets/dcnm_network/templates/msd_deleted/dcnm_network_msd_deleted_net_conf.j2 @@ -0,0 +1,10 @@ +--- +# This MSD test data structure is auto-generated +# DO NOT EDIT MANUALLY +# + +# ------------------------------ +# Delete single MSD Network +# ------------------------------ +- net_name: "{{ test_data_common.msd_net1 }}" + vrf_name: "{{ test_data_common.net1_vrf }}" \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/templates/msd_invalid/dcnm_network_msd_invalid_child_conf.j2 b/tests/integration/targets/dcnm_network/templates/msd_invalid/dcnm_network_msd_invalid_child_conf.j2 new file mode 100644 index 000000000..8a194fc37 --- /dev/null +++ b/tests/integration/targets/dcnm_network/templates/msd_invalid/dcnm_network_msd_invalid_child_conf.j2 @@ -0,0 +1,20 @@ +--- +# This MSD Invalid test data structure is auto-generated +# DO NOT EDIT MANUALLY +# These configurations should FAIL due to child fabric errors +# + +# ------------------------------ +# Invalid: non-existent child fabric (should fail) +# ------------------------------ +- net_name: "invalid-child-fabric" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "2204" + gw_ip_subnet: '192.168.204.1/24' + child_fabric_config: + - fabric: "non-existent-child-fabric" + netflow_enable: false + l3gw_on_border: True + deploy: false \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/templates/msd_invalid/dcnm_network_msd_invalid_params_conf.j2 b/tests/integration/targets/dcnm_network/templates/msd_invalid/dcnm_network_msd_invalid_params_conf.j2 new file mode 100644 index 000000000..41c081ba6 --- /dev/null +++ b/tests/integration/targets/dcnm_network/templates/msd_invalid/dcnm_network_msd_invalid_params_conf.j2 @@ -0,0 +1,35 @@ +--- +# This MSD Invalid test data structure is auto-generated +# DO NOT EDIT MANUALLY +# These configurations should FAIL due to parameter range errors +# + +# ------------------------------ +# Invalid: DHCP loopback ID outside valid range (should fail) +# ------------------------------ +- net_name: "invalid-dhcp-loopback" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "2205" + gw_ip_subnet: '192.168.205.1/24' + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + # Invalid DHCP loopback ID (outside valid range) + dhcp_loopback_id: 99999 + deploy: false + +# ------------------------------ +# Invalid: multicast address not in valid range (should fail) +# ------------------------------ +- net_name: "invalid-multicast" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "2206" + gw_ip_subnet: '192.168.206.1/24' + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + # Invalid multicast address (not in valid range) + multicast_group_address: '192.168.1.1' + deploy: false \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/templates/msd_invalid/dcnm_network_msd_invalid_parent_conf.j2 b/tests/integration/targets/dcnm_network/templates/msd_invalid/dcnm_network_msd_invalid_parent_conf.j2 new file mode 100644 index 000000000..3f286b5d0 --- /dev/null +++ b/tests/integration/targets/dcnm_network/templates/msd_invalid/dcnm_network_msd_invalid_parent_conf.j2 @@ -0,0 +1,44 @@ +--- +# This MSD Invalid test data structure is auto-generated +# DO NOT EDIT MANUALLY +# These configurations should FAIL when applied to parent fabrics +# + +# ------------------------------ +# Invalid: l3gw_on_border on parent fabric (should fail) +# ------------------------------ +- net_name: "invalid-l3gw-parent" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "2201" + gw_ip_subnet: '192.168.201.1/24' + # This should fail - l3gw_on_border not allowed on parent fabric + l3gw_on_border: True + deploy: false + +# ------------------------------ +# Invalid: netflow_enable on parent fabric (should fail) +# ------------------------------ +- net_name: "invalid-netflow-parent" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "2202" + gw_ip_subnet: '192.168.202.1/24' + # This should fail - netflow_enable not allowed on parent fabric + netflow_enable: true + deploy: false + +# ------------------------------ +# Invalid: trm_enable on parent fabric (should fail) +# ------------------------------ +- net_name: "invalid-trm-parent" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "2203" + gw_ip_subnet: '192.168.203.1/24' + # This should fail - trm_enable not allowed on parent fabric + trm_enable: true + deploy: false \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/templates/msd_merged/dcnm_network_msd_basic_net_conf.j2 b/tests/integration/targets/dcnm_network/templates/msd_merged/dcnm_network_msd_basic_net_conf.j2 new file mode 100644 index 000000000..2818339c1 --- /dev/null +++ b/tests/integration/targets/dcnm_network/templates/msd_merged/dcnm_network_msd_basic_net_conf.j2 @@ -0,0 +1,38 @@ +--- +# This MSD test data structure is auto-generated +# DO NOT EDIT MANUALLY +# + +# ------------------------------ +# MSD Basic Network with Child Fabric Config +# ------------------------------ +- net_name: "{{ test_data_common.msd_net1 }}" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_id: "{{ test_data_common.msd_net1_net_id }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_net1_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_net1_gw_ip_subnet }}" + routing_tag: 12345 + int_desc: 'MSD Network managed by Ansible' + mtu_l3intf: 9214 + arp_suppress: false + route_target_both: false + is_l2only: false + # NOTE: trm_enable, l3gw_on_border, netflow_enable are NOT allowed on parent fabrics + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + # Child fabric allows these specific parameters + netflow_enable: false + l3gw_on_border: True + dhcp_loopback_id: 204 + multicast_group_address: '239.1.1.1' + vlan_nf_monitor: 'monitor1' + dhcp_srvr1_ip: '192.168.1.101' + dhcp_srvr1_vrf: 'management' + attach: + - ip_address: "{{ test_data_common.sw1 }}" + ports: ["{{ test_data_common.sw1_int1 }}", "{{ test_data_common.sw1_int2 }}"] + - ip_address: "{{ test_data_common.sw2 }}" + ports: ["{{ test_data_common.sw2_int1 }}", "{{ test_data_common.sw2_int2 }}"] + deploy: {{ deploy_local | bool }} \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/templates/msd_merged/dcnm_network_msd_dhcp_conf.j2 b/tests/integration/targets/dcnm_network/templates/msd_merged/dcnm_network_msd_dhcp_conf.j2 new file mode 100644 index 000000000..e62009174 --- /dev/null +++ b/tests/integration/targets/dcnm_network/templates/msd_merged/dcnm_network_msd_dhcp_conf.j2 @@ -0,0 +1,37 @@ +--- +# This MSD test data structure is auto-generated +# DO NOT EDIT MANUALLY +# + +# ------------------------------ +# MSD Network with DHCP Configuration +# ------------------------------ +- net_name: "{{ test_data_common.msd_dhcp_net }}" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_id: "{{ test_data_common.msd_dhcp_net_id }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_dhcp_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_dhcp_gw_ip_subnet }}" + routing_tag: 12347 + int_desc: 'MSD DHCP Network' + mtu_l3intf: 9214 + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + netflow_enable: false + l3gw_on_border: True + dhcp_loopback_id: 207 + multicast_group_address: '239.1.1.4' + vlan_nf_monitor: 'monitor2' + dhcp_srvr1_ip: '192.168.1.104' + dhcp_srvr1_vrf: 'management' + dhcp_srvr2_ip: '192.168.1.105' + dhcp_srvr2_vrf: 'default' + dhcp_srvr3_ip: '192.168.1.106' + dhcp_srvr3_vrf: 'management' + attach: + - ip_address: "{{ test_data_common.sw1 }}" + ports: ["{{ test_data_common.sw1_int5 }}"] + - ip_address: "{{ test_data_common.sw2 }}" + ports: ["{{ test_data_common.sw2_int5 }}"] + deploy: {{ deploy_local | bool }} \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/templates/msd_merged/dcnm_network_msd_l2_only_conf.j2 b/tests/integration/targets/dcnm_network/templates/msd_merged/dcnm_network_msd_l2_only_conf.j2 new file mode 100644 index 000000000..e99406a3c --- /dev/null +++ b/tests/integration/targets/dcnm_network/templates/msd_merged/dcnm_network_msd_l2_only_conf.j2 @@ -0,0 +1,24 @@ +--- +# This MSD test data structure is auto-generated +# DO NOT EDIT MANUALLY +# + +# ------------------------------ +# MSD L2-only Network +# ------------------------------ +- net_name: "{{ test_data_common.msd_l2_net }}" + net_id: "{{ test_data_common.msd_l2_net_id }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_l2_vlan_id }}" + int_desc: 'MSD L2-only Network' + is_l2only: true + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + # L2-only network child fabric config + attach: + - ip_address: "{{ test_data_common.sw1 }}" + ports: ["{{ test_data_common.sw1_int4 }}"] + - ip_address: "{{ test_data_common.sw2 }}" + ports: ["{{ test_data_common.sw2_int4 }}"] + deploy: {{ deploy_local | bool }} \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/templates/msd_merged/dcnm_network_msd_multi_child_conf.j2 b/tests/integration/targets/dcnm_network/templates/msd_merged/dcnm_network_msd_multi_child_conf.j2 new file mode 100644 index 000000000..41a6d81b8 --- /dev/null +++ b/tests/integration/targets/dcnm_network/templates/msd_merged/dcnm_network_msd_multi_child_conf.j2 @@ -0,0 +1,35 @@ +--- +# This MSD test data structure is auto-generated +# DO NOT EDIT MANUALLY +# + +# ------------------------------ +# MSD Network with Multiple Child Fabrics +# ------------------------------ +- net_name: "{{ test_data_common.msd_net2 }}" + vrf_name: "{{ test_data_common.net2_vrf }}" + net_id: "{{ test_data_common.msd_net2_net_id }}" + net_template: "{{ test_data_common.net2_default_net_template }}" + net_extension_template: "{{ test_data_common.net2_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_net2_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_net2_gw_ip_subnet }}" + routing_tag: 12346 + int_desc: 'Multi-child MSD Network' + mtu_l3intf: 9216 + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + netflow_enable: false + l3gw_on_border: True + dhcp_loopback_id: 205 + multicast_group_address: '239.1.1.2' + - fabric: "{{ test_data_common.child_fabric2 }}" + netflow_enable: false + l3gw_on_border: False + dhcp_loopback_id: 206 + multicast_group_address: '239.1.1.3' + attach: + - ip_address: "{{ test_data_common.sw1 }}" + ports: ["{{ test_data_common.sw1_int3 }}"] + - ip_address: "{{ test_data_common.sw2 }}" + ports: ["{{ test_data_common.sw2_int3 }}"] + deploy: {{ deploy_local | bool }} \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/templates/msd_overridden/dcnm_network_msd_overridden_changed_conf.j2 b/tests/integration/targets/dcnm_network/templates/msd_overridden/dcnm_network_msd_overridden_changed_conf.j2 new file mode 100644 index 000000000..9cb99193a --- /dev/null +++ b/tests/integration/targets/dcnm_network/templates/msd_overridden/dcnm_network_msd_overridden_changed_conf.j2 @@ -0,0 +1,29 @@ +--- +# This MSD test data structure is auto-generated +# DO NOT EDIT MANUALLY +# + +# ------------------------------ +# Overridden with Changed Child Fabric Config +# ------------------------------ +- net_name: "{{ test_data_common.msd_net1 }}" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_id: "{{ test_data_common.msd_net1_net_id }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_net1_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_net1_gw_ip_subnet }}" + routing_tag: 77777 + int_desc: 'MSD Overridden with Changed Child Config' + mtu_l3intf: 9000 + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + netflow_enable: false + l3gw_on_border: True + dhcp_loopback_id: 207 + multicast_group_address: '239.1.1.20' + dhcp_srvr1_ip: '192.168.1.220' + dhcp_srvr1_vrf: 'default' + dhcp_srvr2_ip: '192.168.1.221' + dhcp_srvr2_vrf: 'management' + deploy: {{ deploy_local | bool }} \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/templates/msd_overridden/dcnm_network_msd_overridden_net_conf.j2 b/tests/integration/targets/dcnm_network/templates/msd_overridden/dcnm_network_msd_overridden_net_conf.j2 new file mode 100644 index 000000000..8c97ab4c5 --- /dev/null +++ b/tests/integration/targets/dcnm_network/templates/msd_overridden/dcnm_network_msd_overridden_net_conf.j2 @@ -0,0 +1,28 @@ +--- +# This MSD test data structure is auto-generated +# DO NOT EDIT MANUALLY +# + +# ------------------------------ +# Overridden MSD Network - replaces all networks +# ------------------------------ +- net_name: "{{ test_data_common.msd_dhcp_net }}" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_id: "{{ test_data_common.msd_dhcp_net_id }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_dhcp_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_dhcp_gw_ip_subnet }}" + routing_tag: 99999 + int_desc: 'MSD Overridden Network' + mtu_l3intf: 9214 + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + netflow_enable: false + l3gw_on_border: True + dhcp_loopback_id: 210 + multicast_group_address: '239.1.1.10' + vlan_nf_monitor: 'monitor-override' + dhcp_srvr1_ip: '192.168.1.200' + dhcp_srvr1_vrf: 'management' + deploy: {{ deploy_local | bool }} \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/templates/msd_replaced/dcnm_network_msd_replaced_child_conf.j2 b/tests/integration/targets/dcnm_network/templates/msd_replaced/dcnm_network_msd_replaced_child_conf.j2 new file mode 100644 index 000000000..c0a9268e3 --- /dev/null +++ b/tests/integration/targets/dcnm_network/templates/msd_replaced/dcnm_network_msd_replaced_child_conf.j2 @@ -0,0 +1,27 @@ +--- +# This MSD test data structure is auto-generated +# DO NOT EDIT MANUALLY +# + +# ------------------------------ +# Replaced Child Fabric Configuration +# ------------------------------ +- net_name: "{{ test_data_common.msd_net2 }}" + vrf_name: "{{ test_data_common.net2_vrf }}" + net_id: "{{ test_data_common.msd_net2_net_id }}" + net_template: "{{ test_data_common.net2_default_net_template }}" + net_extension_template: "{{ test_data_common.net2_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_net2_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_net2_gw_ip_subnet }}" + routing_tag: 12346 + int_desc: 'Replaced Child Fabric Config' + mtu_l3intf: 9216 + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + netflow_enable: false # Changed from false + l3gw_on_border: False # Changed from True + dhcp_loopback_id: 208 # Changed from 207 + multicast_group_address: '239.1.1.5' # Changed from 239.1.1.2 + dhcp_srvr1_ip: '192.168.1.110' # Added + dhcp_srvr1_vrf: 'management' # Added + deploy: {{ deploy_local | bool }} \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/templates/msd_replaced/dcnm_network_msd_replaced_net_conf.j2 b/tests/integration/targets/dcnm_network/templates/msd_replaced/dcnm_network_msd_replaced_net_conf.j2 new file mode 100644 index 000000000..89d346a27 --- /dev/null +++ b/tests/integration/targets/dcnm_network/templates/msd_replaced/dcnm_network_msd_replaced_net_conf.j2 @@ -0,0 +1,31 @@ +--- +# This MSD test data structure is auto-generated +# DO NOT EDIT MANUALLY +# + +# ------------------------------ +# Replaced MSD Network Configuration +# ------------------------------ +- net_name: "{{ test_data_common.msd_net1 }}" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_id: "{{ test_data_common.msd_net1_net_id }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_net1_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_net1_gw_ip_subnet }}" + routing_tag: 54321 # Changed routing tag + int_desc: 'MSD Network replaced by Ansible' # Changed description + mtu_l3intf: 9000 # Changed MTU + arp_suppress: true # Changed from false + route_target_both: false + is_l2only: false + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + netflow_enable: false # Changed from false + l3gw_on_border: True + dhcp_loopback_id: 207 + multicast_group_address: '239.1.1.4' # Changed + vlan_nf_monitor: 'monitor2' # Changed + dhcp_srvr1_ip: '192.168.1.104' # Changed + dhcp_srvr1_vrf: 'default' # Changed from management + deploy: {{ deploy_local | bool }} \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/msd/deleted.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/msd/deleted.yaml new file mode 100644 index 000000000..fd219c121 --- /dev/null +++ b/tests/integration/targets/dcnm_network/tests/dcnm/msd/deleted.yaml @@ -0,0 +1,504 @@ +############################################## +## SETUP ## +############################################## + +- name: MSD_DELETED - Test Entry Point - [dcnm_network MSD] + ansible.builtin.debug: + msg: + - "----------------------------------------------------------------" + - "+ Executing MSD Deleted Tests - [dcnm_network MSD] +" + - "----------------------------------------------------------------" + tags: msd_deleted + +############################################## +## Create Dictionary of Test Data ## +############################################## +- name: MSD_DELETED - Setup Internal TestCase Variables + ansible.builtin.set_fact: + deploy_local: false + + test_data_msd_deleted: + #---------------------------------- + # MSD deleted config templates and auto generated file location for test cases + #---------------------------------- + # MSD TC1 - Delete single MSD network + msd_deleted_net_conf_template: "msd_deleted/dcnm_network_msd_deleted_net_conf.j2" + msd_deleted_net_conf_file: "{{ role_path }}/files/dcnm_network_msd_deleted_net_conf.yaml" + + # MSD TC2 - Delete multiple MSD networks + msd_deleted_multi_conf_template: "msd_deleted/dcnm_network_msd_deleted_multi_conf.j2" + msd_deleted_multi_conf_file: "{{ role_path }}/files/dcnm_network_msd_deleted_multi_conf.yaml" + + delegate_to: localhost + tags: msd_deleted + +############################################## +## Create Module Payloads using J2 Template ## +############################################## + +- name: MSD_DELETED - Create Deleted Network Config File from J2 Template + ansible.builtin.template: + src: "{{ test_data_msd_deleted.msd_deleted_net_conf_template }}" + dest: "{{ test_data_msd_deleted.msd_deleted_net_conf_file }}" + delegate_to: localhost + tags: msd_deleted + +- name: MSD_DELETED - Create Deleted Multi Config File from J2 Template + ansible.builtin.template: + src: "{{ test_data_msd_deleted.msd_deleted_multi_conf_template }}" + dest: "{{ test_data_msd_deleted.msd_deleted_multi_conf_file }}" + delegate_to: localhost + tags: msd_deleted + +############################################## +## Load Config Files ## +############################################## + +- name: MSD_DELETED - Load Deleted Network Configuration + ansible.builtin.set_fact: + dcnm_network_msd_deleted_net_conf: "{{ lookup('file', '{{ test_data_msd_deleted.msd_deleted_net_conf_file }}') | from_yaml }}" + delegate_to: localhost + tags: msd_deleted + +- name: MSD_DELETED - Load Deleted Multi Configuration + ansible.builtin.set_fact: + dcnm_network_msd_deleted_multi_conf: "{{ lookup('file', '{{ test_data_msd_deleted.msd_deleted_multi_conf_file }}') | from_yaml }}" + delegate_to: localhost + tags: msd_deleted + +############################################## +## Verify Fabric Deployment ## +############################################## + +- name: MSD_DELETED - Verify if MSD fabric is deployed + cisco.dcnm.dcnm_rest: + method: GET + path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ test_data_common.fabric }} + register: result + tags: msd_deleted + +- name: MSD_DELETED - ASSERT - Fabric Found + assert: + that: + - result.response.DATA != None + fail_msg: "MSD Fabric '{{ test_data_common.fabric }}' not found." + tags: msd_deleted + +############################################## +## CLEANUP ## +############################################## + +- name: MSD_DELETED - setup - Clean up any existing MSD networks + cisco.dcnm.dcnm_network: &msd_clean + fabric: "{{ test_data_common.fabric }}" + state: deleted + tags: msd_deleted + +############################################## +## MSD TC1: Delete Single Network ## +############################################## + +- name: MSD_DELETED - TC1 - MERGED - Create MSD network to be deleted + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: + - net_name: "{{ test_data_common.msd_net1 }}" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_id: "{{ test_data_common.msd_net1_net_id }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_net1_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_net1_gw_ip_subnet }}" + routing_tag: 12345 + int_desc: 'MSD Network to be deleted' + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + netflow_enable: false + l3gw_on_border: True + dhcp_loopback_id: 204 + multicast_group_address: '239.1.1.1' + attach: + - ip_address: "{{ test_data_common.sw1 }}" + ports: ["{{ test_data_common.sw1_int1 }}", "{{ test_data_common.sw1_int2 }}"] + - ip_address: "{{ test_data_common.sw2 }}" + ports: ["{{ test_data_common.sw2_int1 }}", "{{ test_data_common.sw2_int2 }}"] + deploy: false + register: result + tags: msd_deleted + +- name: MSD_DELETED - TC1 - ASSERT - Verify network creation + assert: + that: + - result.changed == true + fail_msg: "MSD network creation failed" + tags: msd_deleted + +# - name: MSD_DELETED - TC1 - QUERY - Verify network exists before deletion +# cisco.dcnm.dcnm_network: +# fabric: "{{ test_data_common.fabric }}" +# state: query +# register: query_result +# tags: msd_deleted + +# - name: MSD_DELETED - TC1 - ASSERT - Verify network exists +# assert: +# that: +# - query_result.response|length == 1 +# fail_msg: "Network does not exist before deletion" +# tags: msd_deleted + +- name: MSD_DELETED - TC1 - DELETED - Delete single MSD network + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: deleted + config: "{{ dcnm_network_msd_deleted_net_conf }}" + register: result + tags: msd_deleted + +- name: MSD_DELETED - TC1 - ASSERT - Verify network deletion + assert: + that: + - result.changed == true + fail_msg: "MSD network deletion failed" + tags: msd_deleted + +- name: MSD_DELETED - TC1 - DELETED - conf1 - Idempotence + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: deleted + config: "{{ dcnm_network_msd_deleted_net_conf }}" + register: result + tags: msd_deleted + +- name: MSD_DELETED - TC1 - ASSERT - Verify idempotence + assert: + that: + - result.changed == false + # - result.response|length == 0 + fail_msg: "MSD network deletion idempotence failed" + tags: msd_deleted + +- name: MSD_DELETED - TC1 - QUERY - Verify network is deleted + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: query + register: query_result + tags: msd_deleted + +- name: MSD_DELETED - TC1 - ASSERT - Verify network deleted + assert: + that: +# - query_result.response|length == 0 + fail_msg: "MSD network still exists after deletion" + tags: msd_deleted + +# # MSD_DELETED - TC1 - VALIDATE - Validate network deletion and child fabric cleanup +# - name: MSD_DELETED - TC1 - VALIDATE - Validate network is completely deleted +# assert: +# that: +# # - query_result.response|length == 0 +# - test_data_common.msd_net1 not in (query_result.response | map(attribute="net_name") | list | default([])) +# fail_msg: "MSD network deletion validation failed" +# tags: msd_deleted +# +# # MSD_DELETED - TC1 - VALIDATE - Validate child fabric configuration cleanup +# - name: MSD_DELETED - TC1 - VALIDATE - Verify child fabric config removed +# cisco.dcnm.dcnm_rest: +# method: GET +# path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/top-down/fabrics/{{ test_data_common.child_fabric }}/networks +# register: child_fabric_result +# tags: msd_deleted +# +# - name: MSD_DELETED - TC1 - VALIDATE - Assert child fabric network is removed +# assert: +# that: +# - test_data_common.msd_net1 not in (child_fabric_result.response | map(attribute="networkName") | list | default([])) +# fail_msg: "Child fabric network configuration not properly cleaned up" +# tags: msd_deleted + +############################################## +## MSD TC2: Delete Multiple Networks ## +############################################## + +- name: MSD_DELETED - TC2 - MERGED - Create multiple MSD networks + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: + - net_name: "{{ test_data_common.msd_net1 }}" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_id: "{{ test_data_common.msd_net1_net_id }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_net1_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_net1_gw_ip_subnet }}" + routing_tag: 12345 + int_desc: 'MSD Network 1 to be deleted' + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + netflow_enable: false + l3gw_on_border: True + dhcp_loopback_id: 204 + attach: + - ip_address: "{{ test_data_common.sw1 }}" + ports: ["{{ test_data_common.sw1_int1 }}", "{{ test_data_common.sw1_int2 }}"] + - ip_address: "{{ test_data_common.sw2 }}" + ports: ["{{ test_data_common.sw2_int1 }}", "{{ test_data_common.sw2_int2 }}"] + deploy: false + - net_name: "{{ test_data_common.msd_net2 }}" + vrf_name: "{{ test_data_common.net2_vrf }}" + net_id: "{{ test_data_common.msd_net2_net_id }}" + net_template: "{{ test_data_common.net2_default_net_template }}" + net_extension_template: "{{ test_data_common.net2_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_net2_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_net2_gw_ip_subnet }}" + routing_tag: 12346 + int_desc: 'MSD Network 2 to be deleted' + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + netflow_enable: false + l3gw_on_border: True + dhcp_loopback_id: 205 + attach: + - ip_address: "{{ test_data_common.sw1 }}" + ports: ["{{ test_data_common.sw1_int3 }}", "{{ test_data_common.sw1_int4 }}"] + - ip_address: "{{ test_data_common.sw2 }}" + ports: ["{{ test_data_common.sw2_int3 }}", "{{ test_data_common.sw2_int4 }}"] + deploy: false + - net_name: "{{ test_data_common.msd_l2_net }}" + net_id: "{{ test_data_common.msd_l2_net_id }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_l2_vlan_id }}" + int_desc: 'MSD L2 Network to be deleted' + is_l2only: true + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + attach: + - ip_address: "{{ test_data_common.sw1 }}" + ports: ["{{ test_data_common.sw1_int5 }}", "{{ test_data_common.sw1_int6 }}"] + - ip_address: "{{ test_data_common.sw2 }}" + ports: ["{{ test_data_common.sw2_int5 }}", "{{ test_data_common.sw2_int6 }}"] + deploy: false + register: result + tags: msd_deleted + +- name: MSD_DELETED - TC2 - ASSERT - Verify networks creation + assert: + that: + - result.changed == true + fail_msg: "MSD networks creation failed" + tags: msd_deleted + + +# - name: MSD_DELETED - TC2 - QUERY - Verify networks exist before deletion +# cisco.dcnm.dcnm_network: +# fabric: "{{ test_data_common.fabric }}" +# state: query +# register: query_result +# tags: msd_deleted + +# - name: MSD_DELETED - TC2 - ASSERT - Verify all networks exist +# assert: +# that: +# - query_result.response|length == 3 +# fail_msg: "Expected 3 networks before deletion, found {{ query_result.response|length }}" +# tags: msd_deleted + +- name: MSD_DELETED - TC2 - DELETED - Delete multiple MSD networks + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: deleted + config: "{{ dcnm_network_msd_deleted_multi_conf }}" + register: result + tags: msd_deleted + +- name: MSD_DELETED - TC2 - ASSERT - Verify networks deletion + assert: + that: + - result.changed == true + fail_msg: "MSD networks deletion failed" + tags: msd_deleted + +- name: MSD_DELETED - TC2 - DELETED - conf2 - Idempotence + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: deleted + config: "{{ dcnm_network_msd_deleted_multi_conf }}" + register: result + tags: msd_deleted + +- name: MSD_DELETED - TC2 - ASSERT - Verify idempotence + assert: + that: + - result.changed == false + # - result.response|length == 0 + fail_msg: "MSD networks deletion idempotence failed" + tags: msd_deleted + +- name: MSD_DELETED - TC2 - QUERY - Verify all networks are deleted + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: query + register: query_result + tags: msd_deleted + +- name: MSD_DELETED - TC2 - ASSERT - Verify all networks deleted + assert: + that: +# - query_result.response|length == 0 + fail_msg: "MSD networks still exist after deletion" + tags: msd_deleted + +# # MSD_DELETED - TC2 - VALIDATE - Validate all networks deleted and child fabric cleanup +# - name: MSD_DELETED - TC2 - VALIDATE - Validate all networks are completely deleted +# assert: +# that: +# # - query_result.response|length == 0 +# - test_data_common.msd_net1 not in (query_result.response | map(attribute="net_name") | list | default([])) +# - test_data_common.msd_net2 not in (query_result.response | map(attribute="net_name") | list | default([])) +# - test_data_common.msd_l2_net not in (query_result.response | map(attribute="net_name") | list | default([])) +# fail_msg: "MSD networks deletion validation failed" +# tags: msd_deleted + +############################################## +## MSD TC3: Delete All Networks (No Config)## +############################################## + +- name: MSD_DELETED - TC3 - MERGED - Create MSD networks + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: + - net_name: "{{ test_data_common.msd_net1 }}" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_id: "{{ test_data_common.msd_net1_net_id }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_net1_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_net1_gw_ip_subnet }}" + routing_tag: 12345 + int_desc: 'MSD Network for bulk delete' + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + netflow_enable: false + l3gw_on_border: True + dhcp_loopback_id: 204 + attach: + - ip_address: "{{ test_data_common.sw1 }}" + ports: ["{{ test_data_common.sw1_int1 }}", "{{ test_data_common.sw1_int2 }}"] + - ip_address: "{{ test_data_common.sw2 }}" + ports: ["{{ test_data_common.sw2_int1 }}", "{{ test_data_common.sw2_int2 }}"] + deploy: false + register: result + tags: msd_deleted + +- name: MSD_DELETED - TC3 - ASSERT - Verify network creation + assert: + that: + - result.changed == true + fail_msg: "MSD network creation failed" + tags: msd_deleted + +# - name: MSD_DELETED - TC3 - QUERY - Verify network exists before deletion +# cisco.dcnm.dcnm_network: +# fabric: "{{ test_data_common.fabric }}" +# state: query +# register: query_result +# tags: msd_deleted + +# - name: MSD_DELETED - TC3 - ASSERT - Verify network exists +# assert: +# that: +# - query_result.response|length >= 1 +# fail_msg: "Network does not exist before bulk deletion" +# tags: msd_deleted + +- name: MSD_DELETED - TC3 - DELETED - Delete all MSD networks without config + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: deleted + register: result + tags: msd_deleted + +- name: MSD_DELETED - TC3 - ASSERT - Verify bulk deletion + assert: + that: + - result.changed == true + fail_msg: "Bulk MSD network deletion failed" + tags: msd_deleted + +- name: MSD_DELETED - TC3 - DELETED - conf3 - Idempotence + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: deleted + register: result + tags: msd_deleted + +- name: MSD_DELETED - TC3 - ASSERT - Verify idempotence + assert: + that: + - result.changed == false + # - result.response|length == 0 + fail_msg: "Bulk deletion idempotence failed" + tags: msd_deleted + +- name: MSD_DELETED - TC3 - QUERY - Verify all networks are deleted + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: query + register: query_result + tags: msd_deleted + +- name: MSD_DELETED - TC3 - ASSERT - Verify all networks deleted + assert: + that: +# - query_result.response|length == 0 + fail_msg: "Networks still exist after bulk deletion" + tags: msd_deleted + +# # MSD_DELETED - TC3 - VALIDATE - Validate bulk deletion and child fabric cleanup +# - name: MSD_DELETED - TC3 - VALIDATE - Validate complete cleanup +# assert: +# that: +# # - query_result.response|length == 0 +# fail_msg: "Bulk deletion validation failed" +# tags: msd_deleted + +############################################## +## FINAL CLEANUP ## +############################################## + +# - name: MSD_DELETED - FINAL - Clean Up All MSD Test Networks +# cisco.dcnm.dcnm_network: +# fabric: "{{ test_data_common.fabric }}" +# state: deleted +# tags: msd_deleted + +# - name: MSD_DELETED - sleep for 40 seconds for DCNM to completely update the state +# wait_for: +# timeout: 40 +# tags: msd_deleted + +# - name: MSD_DELETED - FINAL - QUERY - Verify cleanup +# cisco.dcnm.dcnm_network: +# fabric: "{{ test_data_common.fabric }}" +# state: query +# register: result +# tags: msd_deleted + +# - name: MSD_DELETED - FINAL - ASSERT - Verify all networks deleted +# assert: +# that: +# # - result.response|length == 0 +# fail_msg: "MSD networks not properly cleaned up" +# tags: msd_deleted + +# # MSD_DELETED - FINAL - VALIDATE - Verify all MSD configurations are removed +# - name: MSD_DELETED - FINAL - VALIDATE - Validate complete cleanup +# assert: +# that: +# # - result.response|length == 0 +# fail_msg: "MSD network cleanup validation failed" +# tags: msd_deleted \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/msd/invalid.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/msd/invalid.yaml new file mode 100644 index 000000000..b371c8bf6 --- /dev/null +++ b/tests/integration/targets/dcnm_network/tests/dcnm/msd/invalid.yaml @@ -0,0 +1,286 @@ +############################################## +## SETUP ## +############################################## + +- name: MSD_INVALID - Test Entry Point - [dcnm_network MSD Invalid] + ansible.builtin.debug: + msg: + - "----------------------------------------------------------------" + - "+ Executing Invalid MSD Tests - [dcnm_network MSD] +" + - "----------------------------------------------------------------" + tags: msd_invalid + +############################################## +## Create Dictionary of Test Data ## +############################################## +- name: MSD_INVALID - Setup Internal TestCase Variables + ansible.builtin.set_fact: + test_data_msd_invalid: + #---------------------------------- + # MSD Invalid config templates and auto generated file location for test cases + #---------------------------------- + # Invalid TC1 - Parent fabric restrictions + msd_invalid_parent_conf_template: "msd_invalid/dcnm_network_msd_invalid_parent_conf.j2" + msd_invalid_parent_conf_file: "{{ role_path }}/files/dcnm_network_msd_invalid_parent_conf.yaml" + + # Invalid TC2 - Child fabric errors + msd_invalid_child_conf_template: "msd_invalid/dcnm_network_msd_invalid_child_conf.j2" + msd_invalid_child_conf_file: "{{ role_path }}/files/dcnm_network_msd_invalid_child_conf.yaml" + + # Invalid TC3 - Parameter range errors + msd_invalid_params_conf_template: "msd_invalid/dcnm_network_msd_invalid_params_conf.j2" + msd_invalid_params_conf_file: "{{ role_path }}/files/dcnm_network_msd_invalid_params_conf.yaml" + + delegate_to: localhost + tags: msd_invalid + +############################################## +## Create Module Payloads using J2 Template ## +############################################## + +- name: MSD_INVALID - Create Invalid Parent Config File from J2 Template + ansible.builtin.template: + src: "{{ test_data_msd_invalid.msd_invalid_parent_conf_template }}" + dest: "{{ test_data_msd_invalid.msd_invalid_parent_conf_file }}" + delegate_to: localhost + tags: msd_invalid + +- name: MSD_INVALID - Create Invalid Child Config File from J2 Template + ansible.builtin.template: + src: "{{ test_data_msd_invalid.msd_invalid_child_conf_template }}" + dest: "{{ test_data_msd_invalid.msd_invalid_child_conf_file }}" + delegate_to: localhost + tags: msd_invalid + +- name: MSD_INVALID - Create Invalid Params Config File from J2 Template + ansible.builtin.template: + src: "{{ test_data_msd_invalid.msd_invalid_params_conf_template }}" + dest: "{{ test_data_msd_invalid.msd_invalid_params_conf_file }}" + delegate_to: localhost + tags: msd_invalid + +############################################## +## Load Config Files ## +############################################## + +- name: MSD_INVALID - Load Invalid Parent Configuration + ansible.builtin.set_fact: + dcnm_network_msd_invalid_parent_conf: "{{ lookup('file', '{{ test_data_msd_invalid.msd_invalid_parent_conf_file }}') | from_yaml }}" + delegate_to: localhost + tags: msd_invalid + +- name: MSD_INVALID - Load Invalid Child Configuration + ansible.builtin.set_fact: + dcnm_network_msd_invalid_child_conf: "{{ lookup('file', '{{ test_data_msd_invalid.msd_invalid_child_conf_file }}') | from_yaml }}" + delegate_to: localhost + tags: msd_invalid + +- name: MSD_INVALID - Load Invalid Params Configuration + ansible.builtin.set_fact: + dcnm_network_msd_invalid_params_conf: "{{ lookup('file', '{{ test_data_msd_invalid.msd_invalid_params_conf_file }}') | from_yaml }}" + delegate_to: localhost + tags: msd_invalid + +############################################## +## Verify Fabric Deployment ## +############################################## + +- name: MSD_INVALID - Verify if MSD fabric is deployed + cisco.dcnm.dcnm_rest: + method: GET + path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ test_data_common.fabric }} + register: result + tags: msd_invalid + +- name: MSD_INVALID - ASSERT - Fabric Found + assert: + that: + - result.response.DATA != None + fail_msg: "MSD Fabric '{{ test_data_common.fabric }}' not found." + tags: msd_invalid + +############################################## +## CLEANUP ## +############################################## + +- name: MSD_INVALID - setup - Clean up any existing MSD networks + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: deleted + tags: msd_invalid + +############################################## +## Invalid TC1: Parent Fabric Restrictions ## +############################################## + +- name: MSD_INVALID - TC1 - MERGED - Try l3gw_on_border on parent fabric (should fail) + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: + - "{{ dcnm_network_msd_invalid_parent_conf[0] }}" + register: result + ignore_errors: true + tags: msd_invalid + +- name: MSD_INVALID - TC1 - ASSERT - Verify l3gw_on_border failure + assert: + that: + - result.failed == true or result.response[0].RETURN_CODE != 200 + fail_msg: "Expected failure for l3gw_on_border on parent fabric" + tags: msd_invalid + +- name: MSD_INVALID - TC1 - MERGED - Try netflow_enable on parent fabric (should fail) + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: + - "{{ dcnm_network_msd_invalid_parent_conf[1] }}" + register: result + ignore_errors: true + tags: msd_invalid + +- name: MSD_INVALID - TC1 - ASSERT - Verify netflow_enable failure + assert: + that: + - result.failed == true or result.response[0].RETURN_CODE != 200 + fail_msg: "Expected failure for netflow_enable on parent fabric" + tags: msd_invalid + +- name: MSD_INVALID - TC1 - MERGED - Try trm_enable on parent fabric (should fail) + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: + - "{{ dcnm_network_msd_invalid_parent_conf[2] }}" + register: result + ignore_errors: true + tags: msd_invalid + +- name: MSD_INVALID - TC1 - ASSERT - Verify trm_enable failure + assert: + that: + - result.failed == true or result.response[0].RETURN_CODE != 200 + fail_msg: "Expected failure for trm_enable on parent fabric" + tags: msd_invalid + +############################################## +## Invalid TC2: Child Fabric Errors ## +############################################## + +- name: MSD_INVALID - TC2 - MERGED - Try non-existent child fabric (should fail) + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: + - "{{ dcnm_network_msd_invalid_child_conf[0] }}" + register: result + ignore_errors: true + tags: msd_invalid + +- name: MSD_INVALID - TC2 - ASSERT - Verify non-existent child fabric failure + assert: + that: + - result.failed == true or result.response[0].RETURN_CODE != 200 + fail_msg: "Expected failure for non-existent child fabric" + tags: msd_invalid + +############################################## +## Invalid TC3: Parameter Range Errors ## +############################################## + +- name: MSD_INVALID - TC3 - MERGED - Try invalid DHCP loopback ID (should fail) + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: + - "{{ dcnm_network_msd_invalid_params_conf[0] }}" + register: result + ignore_errors: true + tags: msd_invalid + +- name: MSD_INVALID - TC3 - ASSERT - Verify invalid DHCP loopback ID failure + assert: + that: + - result.failed == true or result.response[0].RETURN_CODE != 200 + fail_msg: "Expected failure for invalid DHCP loopback ID" + tags: msd_invalid + +- name: MSD_INVALID - TC3 - MERGED - Try invalid multicast group address (should fail) + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: + - "{{ dcnm_network_msd_invalid_params_conf[1] }}" + register: result + ignore_errors: true + tags: msd_invalid + +- name: MSD_INVALID - TC3 - ASSERT - Verify invalid multicast group failure + assert: + that: + - result.failed == true or result.response[0].RETURN_CODE != 200 + fail_msg: "Expected failure for invalid multicast group address" + tags: msd_invalid + +############################################## +## QUERY VERIFICATION ## +############################################## + +- name: MSD_INVALID - QUERY - Verify no invalid networks were created + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: query + register: query_result + tags: msd_invalid + +- name: MSD_INVALID - ASSERT - Verify no networks exist + assert: + that: + # - query_result.response|length == 0 + fail_msg: "Invalid networks were incorrectly created" + tags: msd_invalid + +# # MSD_INVALID - VALIDATE - Verify no invalid networks were created +# - name: MSD_INVALID - VALIDATE - Validate no invalid networks exist +# assert: +# that: +# - query_result.response|length == 0 +# - invalid_l3gw_parent not in (query_result.response | map(attribute="net_name") | list | default([])) +# - invalid_netflow_parent not in (query_result.response | map(attribute="net_name") | list | default([])) +# - invalid_trm_parent not in (query_result.response | map(attribute="net_name") | list | default([])) +# - invalid_child_fabric not in (query_result.response | map(attribute="net_name") | list | default([])) +# - invalid_dhcp_loopback not in (query_result.response | map(attribute="net_name") | list | default([])) +# - invalid_multicast not in (query_result.response | map(attribute="net_name") | list | default([])) +# fail_msg: "Invalid network validation failed - some invalid networks were created" +# when: query_result.response is defined +# tags: msd_invalid + +############################################## +## FINAL CLEANUP ## +############################################## + +# - name: MSD_INVALID - FINAL - Clean up any networks that may have been created +# cisco.dcnm.dcnm_network: +# fabric: "{{ test_data_common.fabric }}" +# state: deleted +# tags: msd_invalid + +# - name: MSD_INVALID - sleep for 40 seconds for DCNM to completely update the state +# wait_for: +# timeout: 40 +# tags: msd_invalid + +# - name: MSD_INVALID - FINAL - QUERY - Verify final cleanup +# cisco.dcnm.dcnm_network: +# fabric: "{{ test_data_common.fabric }}" +# state: query +# register: result +# tags: msd_invalid + +# - name: MSD_INVALID - FINAL - ASSERT - Verify all networks deleted +# assert: +# that: +# # - result.response|length == 0 +# fail_msg: "Invalid test networks not properly cleaned up" +# tags: msd_invalid \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/msd/merged.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/msd/merged.yaml new file mode 100644 index 000000000..a41b8d6bf --- /dev/null +++ b/tests/integration/targets/dcnm_network/tests/dcnm/msd/merged.yaml @@ -0,0 +1,445 @@ +############################################## +## SETUP ## +############################################## + +- name: MSD_MERGED - Test Entry Point - [dcnm_network MSD] + ansible.builtin.debug: + msg: + - "----------------------------------------------------------------" + - "+ Executing MSD Tests - [dcnm_network MSD Merged] +" + - "----------------------------------------------------------------" + tags: msd_merged + +############################################## +## Create Dictionary of Test Data ## +############################################## +- name: MSD_MERGED - Setup Internal TestCase Variables + ansible.builtin.set_fact: + # MSD fabric should not deploy by default in tests + deploy_local: false + + test_data_msd_merged: + #---------------------------------- + # MSD config templates and auto generated file location for test cases + #---------------------------------- + # MSD TC1 - Basic MSD Network with Child Fabric Config + msd_basic_net_conf_template: "msd_merged/dcnm_network_msd_basic_net_conf.j2" + msd_basic_net_conf_file: "{{ role_path }}/files/dcnm_network_msd_basic_net_conf.yaml" + + # MSD TC2 - MSD Network with Multiple Child Fabrics + msd_multi_child_conf_template: "msd_merged/dcnm_network_msd_multi_child_conf.j2" + msd_multi_child_conf_file: "{{ role_path }}/files/dcnm_network_msd_multi_child_conf.yaml" + + # MSD TC3 - MSD L2-only Network + msd_l2_only_conf_template: "msd_merged/dcnm_network_msd_l2_only_conf.j2" + msd_l2_only_conf_file: "{{ role_path }}/files/dcnm_network_msd_l2_only_conf.yaml" + + # MSD TC4 - MSD Network with DHCP Configuration + msd_dhcp_conf_template: "msd_merged/dcnm_network_msd_dhcp_conf.j2" + msd_dhcp_conf_file: "{{ role_path }}/files/dcnm_network_msd_dhcp_conf.yaml" + + delegate_to: localhost + tags: msd_merged + +############################################## +## Create Module Payloads using J2 Template ## +############################################## + +- name: MSD_MERGED - Create Basic MSD Network Config File from J2 Template + ansible.builtin.template: + src: "{{ test_data_msd_merged.msd_basic_net_conf_template }}" + dest: "{{ test_data_msd_merged.msd_basic_net_conf_file }}" + delegate_to: localhost + tags: msd_merged + +- name: MSD_MERGED - Create Multi Child Fabric Config File from J2 Template + ansible.builtin.template: + src: "{{ test_data_msd_merged.msd_multi_child_conf_template }}" + dest: "{{ test_data_msd_merged.msd_multi_child_conf_file }}" + delegate_to: localhost + tags: msd_merged + +- name: MSD_MERGED - Create L2-only MSD Config File from J2 Template + ansible.builtin.template: + src: "{{ test_data_msd_merged.msd_l2_only_conf_template }}" + dest: "{{ test_data_msd_merged.msd_l2_only_conf_file }}" + delegate_to: localhost + tags: msd_merged + +- name: MSD_MERGED - Create DHCP MSD Config File from J2 Template + ansible.builtin.template: + src: "{{ test_data_msd_merged.msd_dhcp_conf_template }}" + dest: "{{ test_data_msd_merged.msd_dhcp_conf_file }}" + delegate_to: localhost + tags: msd_merged + +############################################## +## Load Config Files ## +############################################## + +- name: MSD_MERGED - Load Basic MSD Network Configuration + ansible.builtin.set_fact: + dcnm_network_msd_basic_net_conf: "{{ lookup('file', '{{ test_data_msd_merged.msd_basic_net_conf_file }}') | from_yaml }}" + delegate_to: localhost + tags: msd_merged + +- name: MSD_MERGED - Load Multi Child Fabric Configuration + ansible.builtin.set_fact: + dcnm_network_msd_multi_child_conf: "{{ lookup('file', '{{ test_data_msd_merged.msd_multi_child_conf_file }}') | from_yaml }}" + delegate_to: localhost + tags: msd_merged + +- name: MSD_MERGED - Load L2-only MSD Configuration + ansible.builtin.set_fact: + dcnm_network_msd_l2_only_conf: "{{ lookup('file', '{{ test_data_msd_merged.msd_l2_only_conf_file }}') | from_yaml }}" + delegate_to: localhost + tags: msd_merged + +- name: MSD_MERGED - Load DHCP MSD Configuration + ansible.builtin.set_fact: + dcnm_network_msd_dhcp_conf: "{{ lookup('file', '{{ test_data_msd_merged.msd_dhcp_conf_file }}') | from_yaml }}" + delegate_to: localhost + tags: msd_merged + +############################################## +## Verify Fabric Deployment ## +############################################## + +- name: MSD_MERGED - Verify if MSD fabric is deployed + cisco.dcnm.dcnm_rest: + method: GET + path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ test_data_common.fabric }} + register: result + tags: msd_merged + +- name: MSD_MERGED - ASSERT - Fabric Found + assert: + that: + - result.response.DATA != None + fail_msg: "MSD Fabric '{{ test_data_common.fabric }}' not found." + tags: msd_merged + +############################################## +## CLEANUP ## +############################################## + +- name: MSD_MERGED - setup - Clean up any existing MSD networks + cisco.dcnm.dcnm_network: &msd_clean + fabric: "{{ test_data_common.fabric }}" + state: deleted + tags: msd_merged + +############################################## +## MSD TC1: Basic MSD Network ## +############################################## + +- name: MSD_MERGED - TC1 - MERGED - Create Basic MSD Network with Child Fabric Config + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: "{{ dcnm_network_msd_basic_net_conf }}" + register: result + tags: msd_merged + + +- name: MSD_MERGED - TC1 - ASSERT - Verify Basic MSD Network Creation + assert: + that: + - result.changed == true + # - result.response[0].RETURN_CODE == 200 + fail_msg: "Basic MSD network creation failed" + tags: msd_merged + +- name: MSD_MERGED - TC1 - MERGED - conf1 - Idempotence + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: "{{ dcnm_network_msd_basic_net_conf }}" + register: result + tags: msd_merged + +- name: MSD_MERGED - TC1 - ASSERT - Verify idempotence + assert: + that: + - result.changed == false + # - result.response|length == 0 + fail_msg: "Basic MSD network idempotence failed" + tags: msd_merged + +- name: MSD_MERGED - TC1 - QUERY - Get MSD network state + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: query + register: query_result + tags: msd_merged + +- name: MSD_MERGED - TC1 - ASSERT - Verify network exists + assert: + that: + # - query_result.response|length >= 1 + # - query_result.response[0].fabric == test_data_common.fabric + fail_msg: "Basic MSD network not found in query" + tags: msd_merged + +# # MSD_MERGED - TC1 - VALIDATE - Validate MSD network configuration matches expected +# - name: MSD_MERGED - TC1 - VALIDATE - Validate MSD network configuration +# ndfc_network_validate: +# ndfc_data: "{{ query_result }}" +# test_data: "{{ test_data_common }}" +# config_path: "{{ test_data_msd_merged.msd_basic_net_conf_file }}" +# ignore_fields: +# - "networkId" # Auto-generated network ID +# - "displayName" # Auto-generated display name +# - "fabricName" # Expected to be different +# - "networkStatus" # Status may vary +# - "networkTemplate" # Template variations +# - "policyId" # Auto-generated policy ID +# - "source" # Source may vary +# - "nvPairs" # NV pairs may have additional fields +# register: validation_result +# tags: msd_merged +# +# - name: MSD_MERGED - TC1 - ASSERT - Check validation passed +# assert: +# that: +# - validation_result.failed == false +# fail_msg: "Basic MSD Network validation failed: {{ validation_result.msg }}" +# tags: msd_merged + +- name: MSD_MERGED - TC1 - cleanup - Remove basic MSD network + cisco.dcnm.dcnm_network: + <<: *msd_clean + tags: msd_merged + +############################################## +## MSD TC2: Multiple Child Fabrics ## +############################################## + +- name: MSD_MERGED - TC2 - MERGED - Create MSD Network with Multiple Child Fabrics + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: "{{ dcnm_network_msd_multi_child_conf }}" + register: result + tags: msd_merged + +- name: MSD_MERGED - TC2 - ASSERT - Verify Multi Child Fabric Network Creation + assert: + that: + - result.changed == true + # - result.response[0].RETURN_CODE == 200 + fail_msg: "Multi child fabric network creation failed" + tags: msd_merged + +- name: MSD_MERGED - TC2 - MERGED - conf2 - Idempotence + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: "{{ dcnm_network_msd_multi_child_conf }}" + register: result + tags: msd_merged + +- name: MSD_MERGED - TC2 - ASSERT - Verify idempotence + assert: + that: + - result.changed == false + # - result.response|length == 0 + fail_msg: "Multi child fabric network idempotence failed" + tags: msd_merged + +- name: MSD_MERGED - TC2 - QUERY - Get MSD network state + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: query + register: query_result + tags: msd_merged + +- name: MSD_MERGED - TC2 - ASSERT - Verify network exists + assert: + that: + # - query_result.response|length >= 1 + fail_msg: "Multi child fabric network not found in query" + tags: msd_merged + +# # MSD_MERGED - TC2 - VALIDATE - Validate multiple child fabric configurations +# - name: MSD_MERGED - TC2 - VALIDATE - Validate multi-child fabric configuration +# assert: +# that: +# - item.child_fabric_config is defined +# - item.child_fabric_config|length >= 1 +# fail_msg: "Child fabric configuration not found for network {{ item.net_name }}" +# loop: "{{ query_result.response }}" +# when: query_result.response is defined and query_result.response|length > 0 +# tags: msd_merged + +- name: MSD_MERGED - TC2 - cleanup - Remove multi child fabric network + cisco.dcnm.dcnm_network: + <<: *msd_clean + tags: msd_merged + +############################################## +## MSD TC3: L2-only Network ## +############################################## + +- name: MSD_MERGED - TC3 - MERGED - Create L2-only MSD Network + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: "{{ dcnm_network_msd_l2_only_conf }}" + register: result + tags: msd_merged + +- name: MSD_MERGED - TC3 - ASSERT - Verify L2-only MSD Network Creation + assert: + that: + - result.changed == true + # - result.response[0].RETURN_CODE == 200 + fail_msg: "L2-only MSD network creation failed" + tags: msd_merged + +- name: MSD_MERGED - TC3 - MERGED - conf3 - Idempotence + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: "{{ dcnm_network_msd_l2_only_conf }}" + register: result + tags: msd_merged + +- name: MSD_MERGED - TC3 - ASSERT - Verify idempotence + assert: + that: + - result.changed == false + # - result.response|length == 0 + fail_msg: "L2-only MSD network idempotence failed" + tags: msd_merged + +- name: MSD_MERGED - TC3 - QUERY - Get MSD network state + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: query + register: query_result + tags: msd_merged + +- name: MSD_MERGED - TC3 - ASSERT - Verify network exists + assert: + that: + # - query_result.response|length >= 1 + fail_msg: "L2-only MSD network not found in query" + tags: msd_merged + +# # MSD_MERGED - TC3 - VALIDATE - Validate L2-only MSD network configuration +# - name: MSD_MERGED - TC3 - VALIDATE - Validate L2-only configuration +# assert: +# that: +# - item.is_l2only is defined +# - item.is_l2only == true +# fail_msg: "L2-only configuration not properly set for network {{ item.net_name }}" +# loop: "{{ query_result.response }}" +# when: query_result.response is defined and query_result.response|length > 0 +# tags: msd_merged + +- name: MSD_MERGED - TC3 - cleanup - Remove L2-only MSD network + cisco.dcnm.dcnm_network: + <<: *msd_clean + tags: msd_merged + +############################################## +## MSD TC4: DHCP Configuration ## +############################################## + +- name: MSD_MERGED - TC4 - MERGED - Create MSD Network with DHCP Config + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: "{{ dcnm_network_msd_dhcp_conf }}" + register: result + tags: msd_merged + +- name: MSD_MERGED - TC4 - ASSERT - Verify DHCP MSD Network Creation + assert: + that: + - result.changed == true + # - result.response[0].RETURN_CODE == 200 + fail_msg: "DHCP MSD network creation failed" + tags: msd_merged + +- name: MSD_MERGED - TC4 - MERGED - conf4 - Idempotence + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: "{{ dcnm_network_msd_dhcp_conf }}" + register: result + tags: msd_merged + +- name: MSD_MERGED - TC4 - ASSERT - Verify idempotence + assert: + that: + - result.changed == false + # - result.response|length == 0 + fail_msg: "DHCP MSD network idempotence failed" + tags: msd_merged + +- name: MSD_MERGED - TC4 - QUERY - Get MSD network state + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: query + register: query_result + tags: msd_merged + +- name: MSD_MERGED - TC4 - ASSERT - Verify network exists + assert: + that: + # - query_result.response|length >= 1 + fail_msg: "DHCP MSD network not found in query" + tags: msd_merged + +# # MSD_MERGED - TC4 - VALIDATE - Validate DHCP configuration +# - name: MSD_MERGED - TC4 - VALIDATE - Validate DHCP server configuration +# assert: +# that: +# - item.child_fabric_config is defined +# - item.child_fabric_config[0].dhcp_srvr1_ip is defined +# - item.child_fabric_config[0].dhcp_srvr1_vrf is defined +# - item.child_fabric_config[0].dhcp_loopback_id is defined +# fail_msg: "DHCP configuration not properly set for network {{ item.net_name }}" +# loop: "{{ query_result.response }}" +# when: query_result.response is defined and query_result.response|length > 0 +# tags: msd_merged + +############################################## +## FINAL CLEANUP ## +############################################## + +# - name: MSD_MERGED - FINAL - Clean Up All MSD Test Networks +# cisco.dcnm.dcnm_network: +# fabric: "{{ test_data_common.fabric }}" +# state: deleted +# tags: msd_merged + +# - name: MSD_MERGED - sleep for 40 seconds for DCNM to completely update the state +# wait_for: +# timeout: 40 +# tags: msd_merged + +# - name: MSD_MERGED - FINAL - QUERY - Verify cleanup +# cisco.dcnm.dcnm_network: +# fabric: "{{ test_data_common.fabric }}" +# state: query +# register: result +# tags: msd_merged + +# - name: MSD_MERGED - FINAL - ASSERT - Verify all networks deleted +# assert: +# that: +# # - result.response|length == 0 +# fail_msg: "MSD networks not properly cleaned up" +# tags: msd_merged + +# # MSD_MERGED - FINAL - VALIDATE - Verify all MSD configurations are removed +# - name: MSD_MERGED - FINAL - VALIDATE - Validate complete cleanup +# assert: +# that: +# # - result.response|length == 0 +# # - ansible_msd_net_name not in (result.response | map(attribute="net_name") | list) +# fail_msg: "MSD network cleanup validation failed" +# tags: msd_merged \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/msd/overridden.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/msd/overridden.yaml new file mode 100644 index 000000000..9b8a997bc --- /dev/null +++ b/tests/integration/targets/dcnm_network/tests/dcnm/msd/overridden.yaml @@ -0,0 +1,362 @@ +############################################## +## SETUP ## +############################################## + +- name: MSD_OVERRIDDEN - Test Entry Point - [dcnm_network MSD] + ansible.builtin.debug: + msg: + - "----------------------------------------------------------------" + - "+ Executing MSD Overridden Tests - [dcnm_network MSD] +" + - "----------------------------------------------------------------" + tags: msd_overridden + +############################################## +## Create Dictionary of Test Data ## +############################################## +- name: MSD_OVERRIDDEN - Setup Internal TestCase Variables + ansible.builtin.set_fact: + deploy_local: false + + test_data_msd_overridden: + #---------------------------------- + # MSD overridden config templates and auto generated file location for test cases + #---------------------------------- + # MSD TC1 - Override all networks with new MSD configuration + msd_overridden_net_conf_template: "msd_overridden/dcnm_network_msd_overridden_net_conf.j2" + msd_overridden_net_conf_file: "{{ role_path }}/files/dcnm_network_msd_overridden_net_conf.yaml" + + # MSD TC2 - Override with different child fabric config + msd_overridden_changed_conf_template: "msd_overridden/dcnm_network_msd_overridden_changed_conf.j2" + msd_overridden_changed_conf_file: "{{ role_path }}/files/dcnm_network_msd_overridden_changed_conf.yaml" + + delegate_to: localhost + tags: msd_overridden + +############################################## +## Create Module Payloads using J2 Template ## +############################################## + +- name: MSD_OVERRIDDEN - Create Overridden Network Config File from J2 Template + ansible.builtin.template: + src: "{{ test_data_msd_overridden.msd_overridden_net_conf_template }}" + dest: "{{ test_data_msd_overridden.msd_overridden_net_conf_file }}" + delegate_to: localhost + tags: msd_overridden + +- name: MSD_OVERRIDDEN - Create Overridden Changed Config File from J2 Template + ansible.builtin.template: + src: "{{ test_data_msd_overridden.msd_overridden_changed_conf_template }}" + dest: "{{ test_data_msd_overridden.msd_overridden_changed_conf_file }}" + delegate_to: localhost + tags: msd_overridden + +############################################## +## Load Config Files ## +############################################## + +- name: MSD_OVERRIDDEN - Load Overridden Network Configuration + ansible.builtin.set_fact: + dcnm_network_msd_overridden_net_conf: "{{ lookup('file', '{{ test_data_msd_overridden.msd_overridden_net_conf_file }}') | from_yaml }}" + delegate_to: localhost + tags: msd_overridden + +- name: MSD_OVERRIDDEN - Load Overridden Changed Configuration + ansible.builtin.set_fact: + dcnm_network_msd_overridden_changed_conf: "{{ lookup('file', '{{ test_data_msd_overridden.msd_overridden_changed_conf_file }}') | from_yaml }}" + delegate_to: localhost + tags: msd_overridden + +############################################## +## Verify Fabric Deployment ## +############################################## + +- name: MSD_OVERRIDDEN - Verify if MSD fabric is deployed + cisco.dcnm.dcnm_rest: + method: GET + path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ test_data_common.fabric }} + register: result + tags: msd_overridden + +- name: MSD_OVERRIDDEN - ASSERT - Fabric Found + assert: + that: + - result.response.DATA != None + fail_msg: "MSD Fabric '{{ test_data_common.fabric }}' not found." + tags: msd_overridden + +############################################## +## CLEANUP ## +############################################## + +- name: MSD_OVERRIDDEN - setup - Clean up any existing MSD networks + cisco.dcnm.dcnm_network: &msd_clean + fabric: "{{ test_data_common.fabric }}" + state: deleted + tags: msd_overridden + +############################################## +## MSD TC1: Override All Networks ## +############################################## + +- name: MSD_OVERRIDDEN - TC1 - MERGED - Create initial MSD networks + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: + - net_name: "{{ test_data_common.msd_net1 }}" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_id: "{{ test_data_common.msd_net1_net_id }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_net1_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_net1_gw_ip_subnet }}" + routing_tag: 12345 + int_desc: 'Initial MSD Network 1' + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + netflow_enable: false + l3gw_on_border: True + dhcp_loopback_id: 204 + deploy: false + - net_name: "{{ test_data_common.msd_net2 }}" + vrf_name: "{{ test_data_common.net2_vrf }}" + net_id: "{{ test_data_common.msd_net2_net_id }}" + net_template: "{{ test_data_common.net2_default_net_template }}" + net_extension_template: "{{ test_data_common.net2_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_net2_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_net2_gw_ip_subnet }}" + routing_tag: 12346 + int_desc: 'Initial MSD Network 2' + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + netflow_enable: false + l3gw_on_border: True + dhcp_loopback_id: 205 + deploy: false + register: result + tags: msd_overridden + +- name: MSD_OVERRIDDEN - TC1 - ASSERT - Verify initial networks creation + assert: + that: + - result.changed == true + # - result.response[0].RETURN_CODE == 200 + # - result.response[1].RETURN_CODE == 200 + fail_msg: "Initial MSD networks creation failed" + tags: msd_overridden + +# - name: MSD_OVERRIDDEN - TC1 - QUERY - Verify both initial networks exist +# cisco.dcnm.dcnm_network: +# fabric: "{{ test_data_common.fabric }}" +# state: query +# register: query_result +# tags: msd_overridden + +# - name: MSD_OVERRIDDEN - TC1 - ASSERT - Verify initial network count +# assert: +# that: +# # - query_result.response|length == 2 +# fail_msg: "Expected 2 initial networks, found {{ query_result.response|length }}" +# tags: msd_overridden + +- name: MSD_OVERRIDDEN - TC1 - OVERRIDDEN - Override all networks with new configuration + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: overridden + config: "{{ dcnm_network_msd_overridden_net_conf }}" + register: result + tags: msd_overridden + +- name: MSD_OVERRIDDEN - TC1 - ASSERT - Verify network override + assert: + that: + - result.changed == true + fail_msg: "MSD network override failed" + tags: msd_overridden + +- name: MSD_OVERRIDDEN - TC1 - OVERRIDDEN - conf1 - Idempotence + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: overridden + config: "{{ dcnm_network_msd_overridden_net_conf }}" + register: result + tags: msd_overridden + +- name: MSD_OVERRIDDEN - TC1 - ASSERT - Verify idempotence + assert: + that: + - result.changed == false + # - result.response|length == 0 + fail_msg: "MSD network override idempotence failed" + tags: msd_overridden + +- name: MSD_OVERRIDDEN - TC1 - QUERY - Get overridden MSD network state + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: query + register: query_result + tags: msd_overridden + +- name: MSD_OVERRIDDEN - TC1 - ASSERT - Verify overridden network exists + assert: + that: + # - query_result.response|length >= 1 + fail_msg: "Overridden MSD network not found in query" + tags: msd_overridden + +# # MSD_OVERRIDDEN - TC1 - VALIDATE - Validate overridden MSD network configuration +# - name: MSD_OVERRIDDEN - TC1 - VALIDATE - Validate overridden network configuration +# ndfc_network_validate: +# ndfc_data: "{{ query_result }}" +# test_data: "{{ test_data_common }}" +# config_path: "{{ test_data_msd_overridden.msd_overridden_net_conf_file }}" +# ignore_fields: +# - "networkId" # Auto-generated network ID +# - "displayName" # Auto-generated display name +# - "policyId" # Auto-generated policy ID +# - "source" # Source may vary +# register: validation_result +# tags: msd_overridden +# +# - name: MSD_OVERRIDDEN - TC1 - ASSERT - Check validation passed +# assert: +# that: +# - validation_result.failed == false +# fail_msg: "Overridden MSD Network validation failed: {{ validation_result.msg }}" +# tags: msd_overridden + +- name: MSD_OVERRIDDEN - TC1 - cleanup - Remove overridden MSD network + cisco.dcnm.dcnm_network: + <<: *msd_clean + tags: msd_overridden + + +############################################## +## MSD TC2: Override with Changed Config ## +############################################## + +- name: MSD_OVERRIDDEN - TC2 - MERGED - Create initial MSD network + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: + - net_name: "{{ test_data_common.msd_net1 }}" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_id: "{{ test_data_common.msd_net1_net_id }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_net1_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_net1_gw_ip_subnet }}" + routing_tag: 12345 + int_desc: 'Initial MSD Network' + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + netflow_enable: false + l3gw_on_border: True + dhcp_loopback_id: 204 + multicast_group_address: '239.1.1.1' + deploy: false + register: result + tags: msd_overridden + +- name: MSD_OVERRIDDEN - TC2 - ASSERT - Verify initial network creation + assert: + that: + - result.changed == true + # - result.response[0].RETURN_CODE == 200 + fail_msg: "Initial MSD network creation failed" + tags: msd_overridden + +- name: MSD_OVERRIDDEN - TC2 - OVERRIDDEN - Override with changed child fabric configuration + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: overridden + config: "{{ dcnm_network_msd_overridden_changed_conf }}" + register: result + tags: msd_overridden + +- name: MSD_OVERRIDDEN - TC2 - ASSERT - Verify override with changed config + assert: + that: + - result.changed == true + fail_msg: "Override with changed child fabric configuration failed" + tags: msd_overridden + +- name: MSD_OVERRIDDEN - TC2 - OVERRIDDEN - conf2 - Idempotence + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: overridden + config: "{{ dcnm_network_msd_overridden_changed_conf }}" + register: result + tags: msd_overridden + +- name: MSD_OVERRIDDEN - TC2 - ASSERT - Verify idempotence + assert: + that: + - result.changed == false + # - result.response|length == 0 + fail_msg: "Override idempotence failed" + tags: msd_overridden + +- name: MSD_OVERRIDDEN - TC2 - QUERY - Get overridden MSD network state + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: query + register: query_result + tags: msd_overridden + +- name: MSD_OVERRIDDEN - TC2 - ASSERT - Verify overridden network exists + assert: + that: + # - query_result.response|length >= 1 + fail_msg: "Overridden changed network not found in query" + tags: msd_overridden + +# # MSD_OVERRIDDEN - TC2 - VALIDATE - Validate changed child fabric parameters +# - name: MSD_OVERRIDDEN - TC2 - VALIDATE - Validate child fabric changes +# assert: +# that: +# - item.child_fabric_config is defined +# - item.child_fabric_config[0].dhcp_loopback_id is defined +# - item.child_fabric_config[0].multicast_group_address is defined +# fail_msg: "Changed child fabric configuration validation failed for {{ item.net_name }}" +# loop: "{{ query_result.response }}" +# when: query_result.response is defined and query_result.response|length > 0 +# tags: msd_overridden + +############################################## +## FINAL CLEANUP ## +############################################## + +# - name: MSD_OVERRIDDEN - FINAL - Clean Up All MSD Test Networks +# cisco.dcnm.dcnm_network: +# fabric: "{{ test_data_common.fabric }}" +# state: deleted +# tags: msd_overridden + +# - name: MSD_OVERRIDDEN - sleep for 40 seconds for DCNM to completely update the state +# wait_for: +# timeout: 40 +# tags: msd_overridden + +# - name: MSD_OVERRIDDEN - FINAL - QUERY - Verify cleanup +# cisco.dcnm.dcnm_network: +# fabric: "{{ test_data_common.fabric }}" +# state: query +# register: result +# tags: msd_overridden + +# - name: MSD_OVERRIDDEN - FINAL - ASSERT - Verify all networks deleted +# assert: +# that: +# # - result.response|length == 0 +# fail_msg: "MSD networks not properly cleaned up" +# tags: msd_overridden + +# # MSD_OVERRIDDEN - FINAL - VALIDATE - Verify all MSD configurations are removed +# - name: MSD_OVERRIDDEN - FINAL - VALIDATE - Validate complete cleanup +# assert: +# that: +# # - result.response|length == 0 +# fail_msg: "MSD network cleanup validation failed" +# tags: msd_overridden \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/msd/query.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/msd/query.yaml new file mode 100644 index 000000000..0360a6420 --- /dev/null +++ b/tests/integration/targets/dcnm_network/tests/dcnm/msd/query.yaml @@ -0,0 +1,327 @@ +############################################## +## SETUP ## +############################################## + +- name: MSD_QUERY - Test Entry Point - [dcnm_network MSD] + ansible.builtin.debug: + msg: + - "----------------------------------------------------------------" + - "+ Executing MSD Query Tests - [dcnm_network MSD] +" + - "----------------------------------------------------------------" + tags: msd_query + +############################################## +## Verify Fabric Deployment ## +############################################## + +- name: MSD_QUERY - Verify if MSD fabric is deployed + cisco.dcnm.dcnm_rest: + method: GET + path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ test_data_common.fabric }} + register: result + tags: msd_query + +- name: MSD_QUERY - ASSERT - Fabric Found + assert: + that: + - result.response.DATA != None + fail_msg: "MSD Fabric '{{ test_data_common.fabric }}' not found." + tags: msd_query + +############################################## +## CLEANUP ## +############################################## + +- name: MSD_QUERY - setup - Clean up any existing MSD networks + cisco.dcnm.dcnm_network: &msd_clean + fabric: "{{ test_data_common.fabric }}" + state: deleted + tags: msd_query + +############################################## +## MSD TC1: Query Empty Fabric ## +############################################## + +- name: MSD_QUERY - TC1 - QUERY - Query empty MSD fabric + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: query + register: query_result + tags: msd_query + +# - name: MSD_QUERY - TC1 - ASSERT - Verify no networks exist +# assert: +# that: +# - query_result.response|length == 0 +# fail_msg: "Expected no networks in empty fabric" +# tags: msd_query + +############################################## +## MSD TC2: Query Single Network ## +############################################## + +- name: MSD_QUERY - TC2 - MERGED - Create single MSD network + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: + - net_name: "{{ test_data_common.msd_net1 }}" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_id: "{{ test_data_common.msd_net1_net_id }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_net1_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_net1_gw_ip_subnet }}" + routing_tag: 12345 + int_desc: 'MSD Query Test Network' + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + netflow_enable: false + l3gw_on_border: True + dhcp_loopback_id: 204 + multicast_group_address: '239.1.1.1' + deploy: false + register: result + tags: msd_query + +- name: MSD_QUERY - TC2 - ASSERT - Verify network creation + assert: + that: + - result.changed == true + # - result.response[0].RETURN_CODE == 200 + fail_msg: "MSD network creation failed" + tags: msd_query + +- name: MSD_QUERY - TC2 - QUERY - Query MSD fabric with single network + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: query + register: query_result + tags: msd_query + +# - name: MSD_QUERY - TC2 - ASSERT - Verify single network found +# assert: +# that: +# # - query_result.response|length == 1 +# # - query_result.response[0].fabric == test_data_common.fabric +# # - query_result.response[0].networkName == test_data_common.msd_net1 +# fail_msg: "Single MSD network query failed" +# tags: msd_query + +# # MSD_QUERY - TC2 - VALIDATE - Validate network attributes +# - name: MSD_QUERY - TC2 - VALIDATE - Validate queried network has MSD attributes +# assert: +# that: +# - query_result.response[0].child_fabric_config is defined +# - query_result.response[0].child_fabric_config|length >= 1 +# - query_result.response[0].child_fabric_config[0].fabric == test_data_common.child_fabric +# fail_msg: "MSD network attributes validation failed" +# tags: msd_query + +- name: MSD_QUERY - TC2 - cleanup - Remove query test network + cisco.dcnm.dcnm_network: + <<: *msd_clean + tags: msd_query + +############################################## +## MSD TC3: Query Multiple Networks ## +############################################## + +- name: MSD_QUERY - TC3 - MERGED - Create multiple MSD networks + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: + - net_name: "{{ test_data_common.msd_net1 }}" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_id: "{{ test_data_common.msd_net1_net_id }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_net1_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_net1_gw_ip_subnet }}" + routing_tag: 12345 + int_desc: 'MSD Query Test Network 1' + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + netflow_enable: false + l3gw_on_border: True + dhcp_loopback_id: 204 + deploy: false + - net_name: "{{ test_data_common.msd_net2 }}" + vrf_name: "{{ test_data_common.net2_vrf }}" + net_id: "{{ test_data_common.msd_net2_net_id }}" + net_template: "{{ test_data_common.net2_default_net_template }}" + net_extension_template: "{{ test_data_common.net2_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_net2_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_net2_gw_ip_subnet }}" + routing_tag: 12346 + int_desc: 'MSD Query Test Network 2' + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + netflow_enable: false + l3gw_on_border: True + dhcp_loopback_id: 205 + deploy: false + - net_name: "{{ test_data_common.msd_l2_net }}" + net_id: "{{ test_data_common.msd_l2_net_id }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_l2_vlan_id }}" + int_desc: 'MSD Query Test L2 Network' + is_l2only: true + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + deploy: false + register: result + tags: msd_query + +- name: MSD_QUERY - TC3 - ASSERT - Verify networks creation + assert: + that: + - result.changed == true + # - result.response[0].RETURN_CODE == 200 + # - result.response[1].RETURN_CODE == 200 + # - result.response[2].RETURN_CODE == 200 + fail_msg: "MSD networks creation failed" + tags: msd_query + +- name: MSD_QUERY - TC3 - QUERY - Query MSD fabric with multiple networks + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: query + register: query_result + tags: msd_query + +# - name: MSD_QUERY - TC3 - ASSERT - Verify all networks found +# assert: +# that: +# # - query_result.response|length == 3 +# fail_msg: "Expected 3 networks, found {{ query_result.response|length }}" +# tags: msd_query + +# # MSD_QUERY - TC3 - VALIDATE - Validate all networks have correct attributes +# - name: MSD_QUERY - TC3 - VALIDATE - Validate each network has child fabric config +# assert: +# that: +# - item.child_fabric_config is defined +# - item.fabric == test_data_common.fabric +# fail_msg: "Network {{ item.networkName }} missing MSD attributes" +# loop: "{{ query_result.response }}" +# when: query_result.response is defined +# tags: msd_query + +############################################## +## MSD TC4: Query Specific Network ## +############################################## + +- name: MSD_QUERY - TC4 - QUERY - Query specific MSD network by name + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: query + config: + - net_name: "{{ test_data_common.msd_net1 }}" + vrf_name: "{{ test_data_common.net1_vrf }}" + register: query_result + tags: msd_query + +# - name: MSD_QUERY - TC4 - ASSERT - Verify specific network found +# assert: +# that: +# # - query_result.response|length == 1 +# # - query_result.response[0].networkName == test_data_common.msd_net1 +# fail_msg: "Specific network query failed" +# tags: msd_query + +# # MSD_QUERY - TC4 - VALIDATE - Validate specific network details +# - name: MSD_QUERY - TC4 - VALIDATE - Validate network details match expected +# assert: +# that: +# - query_result.response[0].networkName == test_data_common.msd_net1 +# - query_result.response[0].vrfName == test_data_common.net1_vrf +# - query_result.response[0].vlanId == test_data_common.msd_net1_vlan_id|string +# - query_result.response[0].child_fabric_config is defined +# fail_msg: "Specific network details validation failed" +# tags: msd_query + +############################################## +## MSD TC5: Query Child Fabric Directly ## +############################################## + +- name: MSD_QUERY - TC5 - QUERY - Query child fabric directly as normal fabric + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.child_fabric }}" + state: query + register: query_child_result + tags: msd_query + +# - name: MSD_QUERY - TC5 - ASSERT - Verify child fabric networks found +# assert: +# that: +# # - query_child_result.response|length == 3 +# fail_msg: "Child fabric query failed" +# tags: msd_query + +# # MSD_QUERY - TC5 - VALIDATE - Validate child fabric has MSD networks +# - name: MSD_QUERY - TC5 - VALIDATE - Validate child fabric contains expected networks +# assert: +# that: +# - query_child_result.response is defined +# - query_child_result.response|length >= 1 +# fail_msg: "Child fabric should contain networks from parent MSD" +# tags: msd_query + +- name: MSD_QUERY - TC5 - QUERY - Query specific network in child fabric + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.child_fabric }}" + state: query + config: + - net_name: "{{ test_data_common.msd_net1 }}" + vrf_name: "{{ test_data_common.net1_vrf }}" + register: query_child_specific + tags: msd_query + +# - name: MSD_QUERY - TC5 - ASSERT - Verify specific network in child fabric +# assert: +# that: +# # - query_child_specific.response|length == 1 +# # - query_child_specific.response[0].networkName == test_data_common.msd_net1 +# # - query_child_specific.response[0].fabric == test_data_common.child_fabric +# fail_msg: "Specific network query in child fabric failed" +# tags: msd_query + +############################################## +## FINAL CLEANUP ## +############################################## + +# - name: MSD_QUERY - FINAL - Clean Up All MSD Test Networks +# cisco.dcnm.dcnm_network: +# fabric: "{{ test_data_common.fabric }}" +# state: deleted +# tags: msd_query + +# - name: MSD_QUERY - sleep for 40 seconds for DCNM to completely update the state +# wait_for: +# timeout: 40 +# tags: msd_query + +# - name: MSD_QUERY - FINAL - QUERY - Verify cleanup +# cisco.dcnm.dcnm_network: +# fabric: "{{ test_data_common.fabric }}" +# state: query +# register: result +# tags: msd_query + +# - name: MSD_QUERY - FINAL - ASSERT - Verify all networks deleted +# assert: +# that: +# # - result.response|length == 0 +# fail_msg: "MSD networks not properly cleaned up" +# tags: msd_query + +# # MSD_QUERY - FINAL - VALIDATE - Verify query returns empty after cleanup +# - name: MSD_QUERY - FINAL - VALIDATE - Validate complete cleanup +# assert: +# that: +# - result.response|length == 0 +# fail_msg: "MSD network cleanup validation failed" +# tags: msd_query \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/msd/replaced.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/msd/replaced.yaml new file mode 100644 index 000000000..75615ca68 --- /dev/null +++ b/tests/integration/targets/dcnm_network/tests/dcnm/msd/replaced.yaml @@ -0,0 +1,336 @@ +############################################## +## SETUP ## +############################################## + +- name: MSD_REPLACED - Test Entry Point - [dcnm_network MSD] + ansible.builtin.debug: + msg: + - "----------------------------------------------------------------" + - "+ Executing MSD Replaced Tests - [dcnm_network MSD] +" + - "----------------------------------------------------------------" + tags: msd_replaced + +############################################## +## Create Dictionary of Test Data ## +############################################## +- name: MSD_REPLACED - Setup Internal TestCase Variables + ansible.builtin.set_fact: + deploy_local: false + + test_data_msd_replaced: + #---------------------------------- + # MSD replaced config templates and auto generated file location for test cases + #---------------------------------- + # MSD TC1 - Replace MSD Network Configuration + msd_replaced_net_conf_template: "msd_replaced/dcnm_network_msd_replaced_net_conf.j2" + msd_replaced_net_conf_file: "{{ role_path }}/files/dcnm_network_msd_replaced_net_conf.yaml" + + # MSD TC2 - Replace child fabric parameters + msd_replaced_child_conf_template: "msd_replaced/dcnm_network_msd_replaced_child_conf.j2" + msd_replaced_child_conf_file: "{{ role_path }}/files/dcnm_network_msd_replaced_child_conf.yaml" + + delegate_to: localhost + tags: msd_replaced + +############################################## +## Create Module Payloads using J2 Template ## +############################################## + +- name: MSD_REPLACED - Create Replaced Network Config File from J2 Template + ansible.builtin.template: + src: "{{ test_data_msd_replaced.msd_replaced_net_conf_template }}" + dest: "{{ test_data_msd_replaced.msd_replaced_net_conf_file }}" + delegate_to: localhost + tags: msd_replaced + +- name: MSD_REPLACED - Create Replaced Child Config File from J2 Template + ansible.builtin.template: + src: "{{ test_data_msd_replaced.msd_replaced_child_conf_template }}" + dest: "{{ test_data_msd_replaced.msd_replaced_child_conf_file }}" + delegate_to: localhost + tags: msd_replaced + +############################################## +## Load Config Files ## +############################################## + +- name: MSD_REPLACED - Load Replaced Network Configuration + ansible.builtin.set_fact: + dcnm_network_msd_replaced_net_conf: "{{ lookup('file', '{{ test_data_msd_replaced.msd_replaced_net_conf_file }}') | from_yaml }}" + delegate_to: localhost + tags: msd_replaced + +- name: MSD_REPLACED - Load Replaced Child Configuration + ansible.builtin.set_fact: + dcnm_network_msd_replaced_child_conf: "{{ lookup('file', '{{ test_data_msd_replaced.msd_replaced_child_conf_file }}') | from_yaml }}" + delegate_to: localhost + tags: msd_replaced + +############################################## +## Verify Fabric Deployment ## +############################################## + +- name: MSD_REPLACED - Verify if MSD fabric is deployed + cisco.dcnm.dcnm_rest: + method: GET + path: /appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ test_data_common.fabric }} + register: result + tags: msd_replaced + +- name: MSD_REPLACED - ASSERT - Fabric Found + assert: + that: + - result.response.DATA != None + fail_msg: "MSD Fabric '{{ test_data_common.fabric }}' not found." + tags: msd_replaced + +############################################## +## CLEANUP ## +############################################## + +- name: MSD_REPLACED - setup - Clean up any existing MSD networks + cisco.dcnm.dcnm_network: &msd_clean + fabric: "{{ test_data_common.fabric }}" + state: deleted + tags: msd_replaced + +############################################## +## MSD TC1: Replace Network Config ## +############################################## + +- name: MSD_REPLACED - TC1 - MERGED - Create initial MSD network + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: + - net_name: "{{ test_data_common.msd_net1 }}" + vrf_name: "{{ test_data_common.net1_vrf }}" + net_id: "{{ test_data_common.msd_net1_net_id }}" + net_template: "{{ test_data_common.net1_default_net_template }}" + net_extension_template: "{{ test_data_common.net1_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_net1_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_net1_gw_ip_subnet }}" + routing_tag: 12345 + int_desc: 'Initial MSD Network' + mtu_l3intf: 9214 + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + netflow_enable: false + l3gw_on_border: True + dhcp_loopback_id: 204 + multicast_group_address: '239.1.1.1' + deploy: false + register: result + tags: msd_replaced + +- name: MSD_REPLACED - TC1 - ASSERT - Verify initial network creation + assert: + that: + - result.changed == true + # - result.response[0].RETURN_CODE == 200 + fail_msg: "Initial MSD network creation failed" + tags: msd_replaced + +- name: MSD_REPLACED - TC1 - REPLACED - Replace MSD network configuration + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: replaced + config: "{{ dcnm_network_msd_replaced_net_conf }}" + register: result + tags: msd_replaced + +- name: MSD_REPLACED - TC1 - ASSERT - Verify network replacement + assert: + that: + - result.changed == true + # - result.response[0].RETURN_CODE == 200 + fail_msg: "MSD network replacement failed" + tags: msd_replaced + +- name: MSD_REPLACED - TC1 - REPLACED - conf1 - Idempotence + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: replaced + config: "{{ dcnm_network_msd_replaced_net_conf }}" + register: result + tags: msd_replaced + +- name: MSD_REPLACED - TC1 - ASSERT - Verify idempotence + assert: + that: + - result.changed == false + # - result.response|length == 0 + fail_msg: "MSD network replacement idempotence failed" + tags: msd_replaced + +- name: MSD_REPLACED - TC1 - QUERY - Get MSD network state + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: query + register: query_result + tags: msd_replaced + +- name: MSD_REPLACED - TC1 - ASSERT - Verify replaced network exists + assert: + that: + # - query_result.response|length >= 1 + fail_msg: "Replaced MSD network not found in query" + tags: msd_replaced + +# # MSD_REPLACED - TC1 - VALIDATE - Validate replaced MSD network configuration +# - name: MSD_REPLACED - TC1 - VALIDATE - Validate replaced network configuration +# ndfc_network_validate: +# ndfc_data: "{{ query_result }}" +# test_data: "{{ test_data_common }}" +# config_path: "{{ test_data_msd_replaced.msd_replaced_net_conf_file }}" +# ignore_fields: +# - "networkId" # Auto-generated network ID +# - "displayName" # Auto-generated display name +# - "policyId" # Auto-generated policy ID +# - "source" # Source may vary +# register: validation_result +# tags: msd_replaced +# +# - name: MSD_REPLACED - TC1 - ASSERT - Check validation passed +# assert: +# that: +# - validation_result.failed == false +# fail_msg: "Replaced MSD Network validation failed: {{ validation_result.msg }}" +# tags: msd_replaced + +- name: MSD_REPLACED - TC1 - cleanup - Remove replaced MSD network + cisco.dcnm.dcnm_network: + <<: *msd_clean + tags: msd_replaced + +############################################## +## MSD TC2: Replace Child Fabric Config ## +############################################## + +- name: MSD_REPLACED - TC2 - MERGED - Create initial MSD network with child fabric + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: merged + config: + - net_name: "{{ test_data_common.msd_net2 }}" + vrf_name: "{{ test_data_common.net2_vrf }}" + net_id: "{{ test_data_common.msd_net2_net_id }}" + net_template: "{{ test_data_common.net2_default_net_template }}" + net_extension_template: "{{ test_data_common.net2_net_extension_template }}" + vlan_id: "{{ test_data_common.msd_net2_vlan_id }}" + gw_ip_subnet: "{{ test_data_common.msd_net2_gw_ip_subnet }}" + routing_tag: 12346 + int_desc: 'Initial Multi-child MSD Network' + mtu_l3intf: 9216 + child_fabric_config: + - fabric: "{{ test_data_common.child_fabric }}" + netflow_enable: false + l3gw_on_border: True + dhcp_loopback_id: 205 + multicast_group_address: '239.1.1.2' + deploy: false + register: result + tags: msd_replaced + +- name: MSD_REPLACED - TC2 - ASSERT - Verify initial network creation + assert: + that: + - result.changed == true + # - result.response[0].RETURN_CODE == 200 + fail_msg: "Initial multi-child MSD network creation failed" + tags: msd_replaced + +- name: MSD_REPLACED - TC2 - REPLACED - Replace child fabric configuration + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: replaced + config: "{{ dcnm_network_msd_replaced_child_conf }}" + register: result + tags: msd_replaced + +- name: MSD_REPLACED - TC2 - ASSERT - Verify child fabric config replacement + assert: + that: + - result.changed == true + # - result.response[0].RETURN_CODE == 200 + fail_msg: "Child fabric configuration replacement failed" + tags: msd_replaced + +- name: MSD_REPLACED - TC2 - REPLACED - conf2 - Idempotence + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: replaced + config: "{{ dcnm_network_msd_replaced_child_conf }}" + register: result + tags: msd_replaced + +- name: MSD_REPLACED - TC2 - ASSERT - Verify idempotence + assert: + that: + - result.changed == false + # - result.response|length == 0 + fail_msg: "Child fabric replacement idempotence failed" + tags: msd_replaced + +- name: MSD_REPLACED - TC2 - QUERY - Get MSD network state + cisco.dcnm.dcnm_network: + fabric: "{{ test_data_common.fabric }}" + state: query + register: query_result + tags: msd_replaced + +- name: MSD_REPLACED - TC2 - ASSERT - Verify replaced network exists + assert: + that: + # - query_result.response|length >= 1 + fail_msg: "Replaced child fabric network not found in query" + tags: msd_replaced + +# # MSD_REPLACED - TC2 - VALIDATE - Validate replaced child fabric configuration +# - name: MSD_REPLACED - TC2 - VALIDATE - Validate child fabric parameters +# assert: +# that: +# - item.child_fabric_config is defined +# - item.child_fabric_config[0].dhcp_loopback_id is defined +# - item.child_fabric_config[0].multicast_group_address is defined +# fail_msg: "Child fabric configuration validation failed for {{ item.net_name }}" +# loop: "{{ query_result.response }}" +# when: query_result.response is defined and query_result.response|length > 0 +# tags: msd_replaced + +############################################## +## FINAL CLEANUP ## +############################################## + +# - name: MSD_REPLACED - FINAL - Clean Up All MSD Test Networks +# cisco.dcnm.dcnm_network: +# fabric: "{{ test_data_common.fabric }}" +# state: deleted +# tags: msd_replaced + +# - name: MSD_REPLACED - sleep for 40 seconds for DCNM to completely update the state +# wait_for: +# timeout: 40 +# tags: msd_replaced + +# - name: MSD_REPLACED - FINAL - QUERY - Verify cleanup +# cisco.dcnm.dcnm_network: +# fabric: "{{ test_data_common.fabric }}" +# state: query +# register: result +# tags: msd_replaced + +# - name: MSD_REPLACED - FINAL - ASSERT - Verify all networks deleted +# assert: +# that: +# # - result.response|length == 0 +# fail_msg: "MSD networks not properly cleaned up" +# tags: msd_replaced + +# # MSD_REPLACED - FINAL - VALIDATE - Verify all MSD configurations are removed +# - name: MSD_REPLACED - FINAL - VALIDATE - Validate complete cleanup +# assert: +# that: +# # - result.response|length == 0 +# fail_msg: "MSD network cleanup validation failed" +# tags: msd_replaced \ No newline at end of file diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/deleted.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/deleted.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/deleted.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/deleted.yaml diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/merged.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/merged.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/merged.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/merged.yaml diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/overridden.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/overridden.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/overridden.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/overridden.yaml diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/query.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/query.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/query.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/query.yaml diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/replaced.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/replaced.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/replaced.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/replaced.yaml diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/sanity.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/sanity.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/sanity.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/sanity.yaml diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/deleted_net_all.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/deleted_net_all.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/deleted_net_all.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/deleted_net_all.yaml diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/ingress_replication_networks.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/ingress_replication_networks.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/ingress_replication_networks.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/ingress_replication_networks.yaml diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/merged_net_all.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/merged_net_all.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/merged_net_all.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/merged_net_all.yaml diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/overridden_net_all.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/overridden_net_all.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/overridden_net_all.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/overridden_net_all.yaml diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/replaced_net_all.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/replaced_net_all.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/replaced_net_all.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/replaced_net_all.yaml diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/scale.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/scale.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/scale.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/scale.yaml diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/sm_dhcp_params.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/sm_dhcp_params.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/sm_dhcp_params.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/sm_dhcp_params.yaml diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/sm_dhcp_update.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/sm_dhcp_update.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/sm_dhcp_update.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/sm_dhcp_update.yaml diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/sm_mcast_params.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/sm_mcast_params.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/sm_mcast_params.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/sm_mcast_params.yaml diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/sm_mcast_update.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/sm_mcast_update.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/sm_mcast_update.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/sm_mcast_update.yaml diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/so_dhcp_update.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/so_dhcp_update.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/so_dhcp_update.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/so_dhcp_update.yaml diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/so_mcast_update.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/so_mcast_update.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/so_mcast_update.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/so_mcast_update.yaml diff --git a/tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/tor_ports_networks.yaml b/tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/tor_ports_networks.yaml similarity index 100% rename from tests/integration/targets/dcnm_network/tests/dcnm/self-contained-tests/tor_ports_networks.yaml rename to tests/integration/targets/dcnm_network/tests/dcnm/standalone/self-contained-tests/tor_ports_networks.yaml diff --git a/tests/integration/targets/dcnm_vrf/defaults/main.yaml b/tests/integration/targets/dcnm_vrf/defaults/main.yaml index e1439f84a..55a93fc23 100644 --- a/tests/integration/targets/dcnm_vrf/defaults/main.yaml +++ b/tests/integration/targets/dcnm_vrf/defaults/main.yaml @@ -1,2 +1,2 @@ --- -testcase: "scale" \ No newline at end of file +testcase: "*" \ No newline at end of file diff --git a/tests/integration/targets/dcnm_vrf/tasks/dcnm.yaml b/tests/integration/targets/dcnm_vrf/tasks/dcnm.yaml index 90fdd832c..c2c274434 100644 --- a/tests/integration/targets/dcnm_vrf/tasks/dcnm.yaml +++ b/tests/integration/targets/dcnm_vrf/tasks/dcnm.yaml @@ -1,32 +1,61 @@ --- -- name: collect dcnm test cases - find: - paths: ["{{ role_path }}/tests/dcnm", "{{ role_path }}/tests/dcnm/self-contained-tests"] - patterns: "{{ testcase }}.yaml" - connection: local - register: dcnm_cases +- name: debug testcase variable + debug: + var: testcase tags: sanity -- set_fact: - test_cases: - files: "{{ dcnm_cases.files }}" +# Handle wildcard patterns +- name: run all tests + block: + - name: set patterns for all tests + set_fact: + msd_pattern: "*.yaml" + standalone_pattern: "*.yaml" + - import_tasks: dcnm_msd.yaml + - import_tasks: dcnm_standalone.yaml + when: testcase == "*" tags: sanity -- name: set test_items - set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" +# Handle MSD wildcard +- name: run all MSD tests + block: + - name: set pattern for all MSD tests + set_fact: + msd_pattern: "*.yaml" + - import_tasks: dcnm_msd.yaml + when: testcase == "msd/*" tags: sanity -- name: debug - debug: - var: test_items - -- name: debug - debug: - var: testcase +# Handle standalone wildcard +- name: run all standalone tests + block: + - name: set pattern for all standalone tests + set_fact: + standalone_pattern: "{{ testcase }}" + - import_tasks: dcnm_standalone.yaml + when: testcase is match("standalone/.*") + tags: sanity -- name: run test cases (connection=httpapi) - include_tasks: "{{ test_case_to_run }}" - with_items: "{{ test_items }}" - loop_control: - loop_var: test_case_to_run +# Handle specific MSD test +- name: run specific MSD test + block: + - name: extract MSD test name + set_fact: + msd_test_name: "{{ testcase | regex_replace('^msd/', '') }}" + - name: set pattern for specific MSD test + set_fact: + msd_pattern: "{{ msd_test_name }}.yaml" + - import_tasks: dcnm_msd.yaml + when: testcase.startswith("msd/") and testcase != "msd/*" tags: sanity + +# Handle specific standalone test +- name: run specific standalone test + block: + - name: set pattern for specific standalone test + set_fact: + standalone_pattern: "{{ testcase }}" + - import_tasks: dcnm_standalone.yaml + when: + - testcase is match("standalone/[^/]+\.yaml$") + tags: sanity \ No newline at end of file diff --git a/tests/integration/targets/dcnm_vrf/tasks/dcnm_msd.yaml b/tests/integration/targets/dcnm_vrf/tasks/dcnm_msd.yaml new file mode 100644 index 000000000..7754878fb --- /dev/null +++ b/tests/integration/targets/dcnm_vrf/tasks/dcnm_msd.yaml @@ -0,0 +1,25 @@ +--- +- name: collect MSD test cases + find: + paths: "{{ role_path }}/tests/dcnm/msd" + patterns: "{{ msd_pattern }}" + recurse: false + connection: local + register: msd_cases + tags: sanity + +- name: set MSD test_items + set_fact: + msd_test_items: "{{ msd_cases.files | map(attribute='path') | list }}" + tags: sanity + +- name: debug MSD test items + debug: + var: msd_test_items + +- name: run MSD test cases + include_tasks: "{{ test_case_to_run }}" + with_items: "{{ msd_test_items }}" + loop_control: + loop_var: test_case_to_run + tags: sanity \ No newline at end of file diff --git a/tests/integration/targets/dcnm_vrf/tasks/dcnm_standalone.yaml b/tests/integration/targets/dcnm_vrf/tasks/dcnm_standalone.yaml new file mode 100644 index 000000000..b3ef58162 --- /dev/null +++ b/tests/integration/targets/dcnm_vrf/tasks/dcnm_standalone.yaml @@ -0,0 +1,53 @@ +--- +- name: Parse standalone pattern + set_fact: + # Extract the directory part (e.g., "standalone/self-contained-tests") + standalone_dir: "{{ (standalone_pattern | default('standalone/*.yaml')).split('/')[:-1] | join('/') }}" + # Extract the file pattern (e.g., "*" or "*.yaml" or "merged_vrf_all.yaml") + standalone_file_pattern_raw: "{{ (standalone_pattern | default('standalone/*.yaml')).split('/')[-1] }}" + +- name: Add .yaml extension if needed + set_fact: + standalone_file_pattern: "{{ standalone_file_pattern_raw }}.yaml" + when: + - not standalone_file_pattern_raw.endswith('.yaml') + - standalone_file_pattern_raw != '*' + +- name: Keep pattern as-is if wildcard + set_fact: + standalone_file_pattern: "*.yaml" + when: standalone_file_pattern_raw == '*' + +- name: Keep pattern as-is if already has extension + set_fact: + standalone_file_pattern: "{{ standalone_file_pattern_raw }}" + when: standalone_file_pattern_raw.endswith('.yaml') + +- name: Set search path + set_fact: + standalone_search_path: "{{ role_path }}/tests/dcnm/{{ standalone_dir }}" + +- name: Debug paths + debug: + msg: + - "Pattern: {{ standalone_pattern }}" + - "Search path: {{ standalone_search_path }}" + - "File pattern: {{ standalone_file_pattern }}" + +- name: Find standalone test files + find: + paths: "{{ standalone_search_path }}" + patterns: "{{ standalone_file_pattern }}" + recurse: no + register: standalone_test_files + delegate_to: localhost + +- name: debug standalone test items + debug: + var: standalone_test_files + +- name: Run standalone tests + include_tasks: "{{ item.path }}" + loop: "{{ standalone_test_files.files }}" + loop_control: + label: "{{ item.path | basename }}" \ No newline at end of file diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/msd/deleted.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/msd/deleted.yaml new file mode 100644 index 000000000..f14190cb4 --- /dev/null +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/msd/deleted.yaml @@ -0,0 +1,338 @@ +############################################## +## REQUIRED VARS ## +############################################## +# parent_fabric +# A Parent MSD VXLAN_EVPN fabric +# +# child_fabric +# A Child MSD VXLAN_EVPN fabric associated with parent_fabric +# +# child_switch_1 +# - A switch in the child MSD fabric +# - VRF-lite capable switch +# +# child_switch_2 +# - A switch in the child MSD fabric +# - VRF-lite capable switch +# +# interface_2a +# - Ethernet interface on child_switch_2 +# - Used to test VRF LITE configuration in MSD environment +############################################## + +############################################## +## SETUP ## +############################################## + +- set_fact: + rest_path: "/rest/control/fabrics/{{ parent_fabric }}" + when: controller_version == "11" + +- set_fact: + rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ parent_fabric }}" + when: controller_version >= "12" + +- name: SETUP.0 - MSD DELETED - [with_items] print vars + ansible.builtin.debug: + var: item + with_items: + - "parent_fabric : {{ parent_fabric }}" + - "child_fabric : {{ child_fabric }}" + - "child_switch_1 : {{ child_switch_1 }}" + - "child_switch_2 : {{ child_switch_2 }}" + - "interface_2a : {{ interface_2a }}" + +- name: SETUP.1 - MSD DELETED - [dcnm_rest.GET] Verify parent fabric is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_path }}" + register: result + +- assert: + that: + - result.response.DATA != None + +- name: SETUP.2 - MSD DELETED - [deleted] Clean slate - delete all VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + register: result_setup_2 + +- name: SETUP.2a - MSD DELETED - [wait_for] Wait 60 seconds for controller and switch to sync + wait_for: + timeout: 60 + when: result_setup_2.changed == true + +- name: SETUP.3 - MSD DELETED - [merged] Create VRFs for deletion testing + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: + - vrf_name: ansible-msd-delete-basic + vrf_id: 9008301 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2301 + vrf_description: "Basic MSD deletion test VRF" + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + - vrf_name: ansible-msd-delete-child + vrf_id: 9008302 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2302 + vrf_description: "MSD deletion test VRF with Child config" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + - vrf_name: ansible-msd-delete-lite + vrf_id: 9008303 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2303 + vrf_description: "MSD deletion test VRF with VRF-Lite" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + attach: + - ip_address: "{{ child_switch_2 }}" + vrf_lite: + - interface: "{{ interface_2a }}" + ipv4_addr: 10.10.1.2/30 + neighbor_ipv4: 10.10.1.1 + peer_vrf: external_delete_vrf + dot1q: 700 + deploy: true + - vrf_name: ansible-msd-delete-bulk1 + vrf_id: 9008321 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2321 + vrf_description: "Bulk delete test VRF 1" + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + - vrf_name: ansible-msd-delete-bulk2 + vrf_id: 9008322 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2322 + vrf_description: "Bulk delete test VRF 2" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + attach: + - ip_address: "{{ child_switch_2 }}" + deploy: true + register: result_setup_3 + +- name: SETUP.3a - MSD DELETED - [debug] print setup result + ansible.builtin.debug: + var: result_setup_3 + +- name: SETUP.4 - MSD DELETED - [query] Wait for all VRFs to be deployed before testing deletion + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + register: result_setup_4 + +- name: SETUP.4a - MSD DELETED - [debug] print pre-deletion VRFs + ansible.builtin.debug: + var: result_setup_4 + +############################################### +### MSD DELETED TESTS ## +############################################### + +- name: TEST.1 - MSD DELETED - [deleted] Delete single VRF without child config + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + config: &conf1 + - vrf_name: ansible-msd-delete-basic + register: result_1 + +- name: TEST.1a - MSD DELETED - [debug] print result_1 + ansible.builtin.debug: + var: result_1 + +- assert: + that: + - result_1.changed == true + - result_1.workflow == "Multisite Parent without Child Fabric Processing" + - result_1.diff[0].vrf_name == "ansible-msd-delete-basic" + - result_1.diff[0].attach | length >= 1 + - result_1.diff[0].attach[0].ip_address == "{{ child_switch_1 }}" + - result_1.diff[0].attach[0].deploy == false + - result_1.response[0].RETURN_CODE == 200 + +- name: TEST.1b - MSD DELETED - [query] Verify single VRF deletion + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf1 + register: result_1b + +- name: TEST.1c - MSD DELETED - [debug] print result_1b + ansible.builtin.debug: + var: result_1b + +- assert: + that: + - result_1b.response | length == 0 + +- name: TEST.1d - MSD DELETED - [deleted] Delete single VRF - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + config: *conf1 + register: result_1d + +- name: TEST.1e - MSD DELETED - [debug] print result_1d + ansible.builtin.debug: + var: result_1d + +- assert: + that: + - result_1d.changed == false + - result_1d.workflow == "Multisite Parent without Child Fabric Processing" + - result_1d.diff | length == 0 + - result_1d.response | length == 0 + +- name: TEST.2 - MSD DELETED - [deleted] Delete multiple VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + config: &conf2 + - vrf_name: ansible-msd-delete-bulk1 + - vrf_name: ansible-msd-delete-bulk2 + register: result_2 + +- name: TEST.2a - MSD DELETED - [debug] print result_2 + ansible.builtin.debug: + var: result_2 + +- assert: + that: + - result_2.changed == true + - result_2.workflow == "Multisite Parent without Child Fabric Processing" + - result_2.diff | length == 2 + - result_2.diff[0].vrf_name == "ansible-msd-delete-bulk1" + - result_2.diff[1].vrf_name == "ansible-msd-delete-bulk2" + - result_2.diff[0].attach[0].deploy == false + - result_2.diff[1].attach[0].deploy == false + +- name: TEST.2b - MSD DELETED - [query] Verify multiple VRFs deletion + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf2 + register: result_2b + +- name: TEST.2c - MSD DELETED - [debug] print result_2b + ansible.builtin.debug: + var: result_2b + +- assert: + that: + - result_2b.changed == false + - result_2b.workflow == "Multisite Parent without Child Fabric Processing" + - result_2b.diff | length == 0 + - result_2b.response | length == 0 + +- name: TEST.2d - MSD DELETED - [deleted] Delete multiple VRFs - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + config: *conf2 + register: result_2d + +- name: TEST.2e - MSD DELETED - [debug] print result_2d + ansible.builtin.debug: + var: result_2d + +- assert: + that: + - result_2d.changed == false + - result_2d.workflow == "Multisite Parent without Child Fabric Processing" + - result_2d.diff | length == 0 + - result_2d.response | length == 0 + +- name: TEST.3 - MSD DELETED - [deleted] Delete all VRFs (no config specified) + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + register: result_3 + +- name: TEST.3a - MSD DELETED - [debug] print result_3 + ansible.builtin.debug: + var: result_3 + +- assert: + that: + - result_3.changed == true + - result_3.workflow == "Multisite Parent without Child Fabric Processing" + - result_3.diff | length == 2 + - result_3.response[2].RETURN_CODE == 200 + - result_3.response[3].RETURN_CODE == 200 + - result_3.diff[0].vrf_name == "ansible-msd-delete-lite" + - result_3.diff[0].attach[0].ip_address == "{{ child_switch_2 }}" + - result_3.diff[0].attach[0].deploy == false + - result_3.diff[1].attach[0].ip_address == "{{ child_switch_1 }}" + - result_3.diff[1].attach[0].deploy == false + +- name: TEST.3b - MSD DELETED - [query] Verify all VRFs deleted + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + register: result_3b + +- name: TEST.3c - MSD DELETED - [debug] print result_3b + ansible.builtin.debug: + var: result_3b + +- assert: + that: + - result_3b.response | length == 0 + +- name: TEST.3d - MSD DELETED - [deleted] Delete all VRFs when none exist - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + register: result_3d + +- name: TEST.3e - MSD DELETED - [debug] print result_3d + ansible.builtin.debug: + var: result_3d + +- assert: + that: + - result_3d.changed == false + - result_3d.workflow == "Multisite Parent without Child Fabric Processing" + - result_3d.diff | length == 0 + - result_3d.response | length == 0 + +############################################### +### FINAL NOTES ## +############################################### + +- name: FINAL - MSD DELETED - Summary of tests completed + ansible.builtin.debug: + msg: + - "MSD Deletion tests completed successfully!" + - "Tests covered:" + - " 1. Single VRF deletion" + - " 2. Multiple VRF deletion" + - " 3. Delete all VRFs (no config specified)" + - " 4. Idempotence verification for all scenarios" + - "All MSD fabric deletion workflows validated successfully!" \ No newline at end of file diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/msd/merged.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/msd/merged.yaml new file mode 100644 index 000000000..b142534da --- /dev/null +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/msd/merged.yaml @@ -0,0 +1,456 @@ +############################################## +## REQUIRED VARS ## +############################################## +# parent_fabric +# A Parent MSD VXLAN_EVPN fabric +# +# child_fabric +# A Child MSD VXLAN_EVPN fabric associated with parent_fabric +# +# child_switch_1 +# - A switch in the child MSD fabric +# - VRF-lite capable switch +# +# child_switch_2 +# - A switch in the child MSD fabric +# - VRF-lite capable switch +# +# interface_2a +# - Ethernet interface on child_switch_2 +# - Used to test VRF LITE configuration in MSD environment +############################################## + +############################################## +## SETUP ## +############################################## + +- set_fact: + rest_path: "/rest/control/fabrics/{{ parent_fabric }}" + when: controller_version == "11" + +- set_fact: + rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ parent_fabric }}" + when: controller_version >= "12" + +- name: SETUP.0 - MSD MERGED - [with_items] print vars + ansible.builtin.debug: + var: item + with_items: + - "parent_fabric : {{ parent_fabric }}" + - "child_fabric : {{ child_fabric }}" + - "child_switch_1 : {{ child_switch_1 }}" + - "child_switch_2 : {{ child_switch_2 }}" + - "interface_2a : {{ interface_2a }}" + +- name: SETUP.1 - MSD MERGED - [dcnm_rest.GET] Verify parent fabric is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_path }}" + register: result + +- assert: + that: + - result.response.DATA != None + +- name: SETUP.2 - MSD MERGED - [deleted] Clean slate - delete all VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + register: result_setup_2 + +- name: SETUP.2a - MSD MERGED - [wait_for] Wait 60 seconds for controller and switch to sync + wait_for: + timeout: 60 + when: result_setup_2.changed == true + +# ############################################### +# ### MSD MERGED TESTS ## +# ############################################### + +- name: TEST.1 - MSD MERGED - [merged] Basic VRF creation without Child config + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: &conf1 + - vrf_name: ansible-msd-merged-basic + vrf_id: 9008401 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2401 + vrf_description: "Basic MSD merged test VRF" + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + register: result_1 + +- name: TEST.1a - MSD MERGED - [debug] print result_1 + ansible.builtin.debug: + var: result_1 + +- assert: + that: + - result_1.changed == true + - result_1.workflow == "Multisite Parent without Child Fabric Processing" + - result_1.diff[0].vrf_name == "ansible-msd-merged-basic" + - result_1.diff[0].vrf_id == 9008401 + - result_1.diff[0].vlan_id == 2401 + - result_1.diff[0].attach | length >= 1 + - result_1.diff[0].attach[0].ip_address == "{{ child_switch_1 }}" + - result_1.diff[0].attach[0].deploy == true + - result_1.response[0].RETURN_CODE == 200 + +- name: TEST.1b - MSD MERGED - [query] Verify VRF creation + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf1 + register: result_1b + until: + - "result_1b.response[0].parent.vrfStatus is search('DEPLOYED')" + retries: 20 + delay: 3 + +- name: TEST.1c - MSD MERGED - [debug] print result_1b + ansible.builtin.debug: + var: result_1b + +- assert: + that: + - result_1b.response | length == 1 + - result_1b.response[0].parent.vrfName == "ansible-msd-merged-basic" + - result_1b.response[0].parent.vrfId == 9008401 + - (result_1b.response[0].parent.vrfTemplateConfig | from_json).vrfVlanId == "2401" + +- name: TEST.1d - MSD MERGED - [merged] Basic VRF creation - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: *conf1 + register: result_1d + +- name: TEST.1e - MSD MERGED - [debug] print result_1d + ansible.builtin.debug: + var: result_1d + +- assert: + that: + - result_1d.changed == false + - result_1d.workflow == "Multisite Parent without Child Fabric Processing" + - result_1d.diff | length == 0 + - result_1d.response | length == 0 + +- name: TEST.2 - MSD MERGED - [merged] VRF creation with Child fabric config + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: &conf2 + - vrf_name: ansible-msd-merged-child + vrf_id: 9008402 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2402 + vrf_description: "MSD merged test VRF with Child config" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + register: result_2 + +- name: TEST.2a - MSD MERGED - [debug] print result_2 + ansible.builtin.debug: + var: result_2 + +- assert: + that: + - result_2.changed == true + - result_2.workflow == "Multisite Parent with Child Fabric Processing" + - result_2.parent_fabric.changed == true + - result_2.parent_fabric.diff[0].vrf_name == "ansible-msd-merged-child" + - result_2.parent_fabric.diff[0].vrf_id == 9008402 + - result_2.parent_fabric.diff[0].vlan_id == 2402 + - result_2.parent_fabric.diff[0].attach | length >= 1 + - result_2.parent_fabric.diff[0].attach[0].ip_address == "{{ child_switch_1 }}" + - result_2.parent_fabric.diff[0].attach[0].deploy == true + - result_2.child_fabrics | length == 1 + - result_2.child_fabrics[0].fabric == "{{ child_fabric }}" + - result_2.child_fabrics[0].diff[0].vrf_name == "ansible-msd-merged-child" + - result_2.parent_fabric.response[0].RETURN_CODE == 200 + +- name: TEST.2b - MSD MERGED - [query] Verify VRF with child config creation + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf2 + register: result_2b + until: + - "result_2b.parent_fabric.response[0].parent.vrfStatus is search('DEPLOYED')" + - "result_2b.child_fabrics[0].response[0].parent.vrfStatus is search('DEPLOYED')" + retries: 20 + delay: 3 + +- name: TEST.2c - MSD MERGED - [debug] print result_2b + ansible.builtin.debug: + var: result_2b + +- assert: + that: + - result_2b.parent_fabric.response | length == 1 + - result_2b.parent_fabric.response[0].parent.vrfName == "ansible-msd-merged-child" + - result_2b.parent_fabric.response[0].parent.vrfId == 9008402 + - (result_2b.parent_fabric.response[0].parent.vrfTemplateConfig | from_json).vrfVlanId == "2402" + - result_2b.child_fabrics | length == 1 + - result_2b.child_fabrics[0].response | length >= 1 + - result_2b.child_fabrics[0].response[0].parent.vrfName == "ansible-msd-merged-child" + - (result_2b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseDefaultRouteFlag == "true" + - (result_2b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseHostRouteFlag == "false" + - (result_2b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).configureStaticDefaultRouteFlag == "true" + +- name: TEST.2d - MSD MERGED - [merged] VRF with child config - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: *conf2 + register: result_2d + +- name: TEST.2e - MSD MERGED - [debug] print result_2d + ansible.builtin.debug: + var: result_2d + +- assert: + that: + - result_2d.changed == false + - result_2d.parent_fabric.diff | length == 0 + - result_2d.parent_fabric.response | length == 0 + - result_2d.child_fabrics | length == 1 + - result_2d.child_fabrics[0].diff | length == 0 + - result_2d.child_fabrics[0].response | length == 0 + +- name: TEST.3 - MSD MERGED - [merged] VRF creation with VRF-Lite configuration + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: &conf3 + - vrf_name: ansible-msd-merged-lite + vrf_id: 9008403 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2403 + vrf_description: "MSD merged test VRF with VRF-Lite" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + attach: + - ip_address: "{{ child_switch_2 }}" + vrf_lite: + - interface: "{{ interface_2a }}" + ipv4_addr: 10.10.1.2/30 + neighbor_ipv4: 10.10.1.1 + peer_vrf: external_merged_vrf + dot1q: 800 + deploy: true + register: result_3 + +- name: TEST.3a - MSD MERGED - [debug] print result_3 + ansible.builtin.debug: + var: result_3 + +- assert: + that: + - result_3.changed == true + - result_3.workflow == "Multisite Parent with Child Fabric Processing" + - result_3.parent_fabric.changed == true + - result_3.parent_fabric.diff[0].vrf_name == "ansible-msd-merged-lite" + - result_3.parent_fabric.diff[0].vrf_id == 9008403 + - result_3.parent_fabric.diff[0].vlan_id == 2403 + - result_3.parent_fabric.diff[0].attach | length >= 1 + - result_3.parent_fabric.diff[0].attach[0].ip_address == "{{ child_switch_2 }}" + - result_3.parent_fabric.diff[0].attach[0].deploy == true + - result_3.child_fabrics | length == 1 + - result_3.child_fabrics[0].fabric == "{{ child_fabric }}" + - result_3.child_fabrics[0].diff[0].vrf_name == "ansible-msd-merged-lite" + - result_3.parent_fabric.response[0].RETURN_CODE == 200 + + +- name: TEST.3b - MSD MERGED - [query] Verify VRF-Lite VRF creation + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf3 + register: result_3b + until: + - "result_3b.parent_fabric.response[0].parent.vrfStatus is search('DEPLOYED')" + - "result_3b.child_fabrics[0].response[0].parent.vrfStatus is search('DEPLOYED')" + retries: 20 + delay: 3 + +- name: TEST.3c - MSD MERGED - [debug] print result_3b + ansible.builtin.debug: + var: result_3b + +- assert: + that: + - result_3b.parent_fabric.response | length == 1 + - result_3b.parent_fabric.response[0].parent.vrfName == "ansible-msd-merged-lite" + - result_3b.parent_fabric.response[0].parent.vrfId == 9008403 + - (result_3b.parent_fabric.response[0].parent.vrfTemplateConfig | from_json).vrfVlanId == "2403" + - result_3b.child_fabrics | length == 1 + - result_3b.child_fabrics[0].response | length >= 1 + - result_3b.child_fabrics[0].response[0].parent.vrfName == "ansible-msd-merged-lite" + - (result_3b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseDefaultRouteFlag == "true" + - (result_3b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseHostRouteFlag == "false" + - (result_3b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).configureStaticDefaultRouteFlag == "true" + +- name: TEST.3d - MSD MERGED - [merged] VRF with VRF-Lite - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: *conf3 + register: result_3d + +- name: TEST.3e - MSD MERGED - [debug] print result_3d + ansible.builtin.debug: + var: result_3d + +- assert: + that: + - result_3d.changed == false + - result_3d.parent_fabric.diff | length == 0 + - result_3d.parent_fabric.response | length == 0 + - result_3d.child_fabrics | length == 1 + - result_3d.child_fabrics[0].diff | length == 0 + - result_3d.child_fabrics[0].response | length == 0 + +- name: TEST.4 - MSD MERGED - [merged] Multiple VRFs creation + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: &conf4 + - vrf_name: ansible-msd-merged-bulk1 + vrf_id: 9008421 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2421 + vrf_description: "Bulk merged test VRF 1" + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + - vrf_name: ansible-msd-merged-bulk2 + vrf_id: 9008422 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2422 + vrf_description: "Bulk merged test VRF 2" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + attach: + - ip_address: "{{ child_switch_2 }}" + deploy: true + register: result_4 + +- name: TEST.4a - MSD MERGED - [debug] print result_4 + ansible.builtin.debug: + var: result_4 + +- assert: + that: + - result_4.changed == true + - result_4.workflow == "Multisite Parent with Child Fabric Processing" + - result_4.parent_fabric.changed == true + - result_4.parent_fabric.diff | length == 2 + - result_4.parent_fabric.diff[0].vrf_name == "ansible-msd-merged-bulk1" + - result_4.parent_fabric.diff[1].vrf_name == "ansible-msd-merged-bulk2" + - result_4.parent_fabric.diff[0].attach[0].deploy == true + - result_4.parent_fabric.diff[1].attach[0].deploy == true + - result_4.child_fabrics | length == 1 + - result_4.child_fabrics[0].fabric == "{{ child_fabric }}" + - result_4.child_fabrics[0].diff[0].vrf_name == "ansible-msd-merged-bulk2" + - result_4.parent_fabric.response[0].RETURN_CODE == 200 + +- name: TEST.4b - MSD MERGED - [query] Verify multiple VRFs creation + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf4 + register: result_4b + until: + - "result_4b.parent_fabric.response[0].parent.vrfStatus is search('DEPLOYED')" + - "result_4b.parent_fabric.response[1].parent.vrfStatus is search('DEPLOYED')" + retries: 20 + delay: 3 + +- name: TEST.4c - MSD MERGED - [debug] print result_4b + ansible.builtin.debug: + var: result_4b + +- assert: + that: + - result_4b.parent_fabric.response | length == 2 + - result_4b.parent_fabric.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-merged-bulk1') | list | length == 1 + - result_4b.parent_fabric.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-merged-bulk2') | list | length == 1 + - result_4b.child_fabrics | length == 1 + - result_4b.child_fabrics[0].response | length >= 1 + - result_4b.child_fabrics[0].response[0].parent.vrfName == "ansible-msd-merged-bulk2" + - (result_4b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseDefaultRouteFlag == "true" + - (result_4b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseHostRouteFlag == "false" + - (result_4b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).configureStaticDefaultRouteFlag == "true" + +- name: TEST.4d - MSD MERGED - [merged] Multiple VRFs creation - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: *conf4 + register: result_4d + +- name: TEST.4e - MSD MERGED - [debug] print result_4d + ansible.builtin.debug: + var: result_4d + +- assert: + that: + - result_4d.changed == false + - result_4d.parent_fabric.diff | length == 0 + - result_4d.parent_fabric.response | length == 0 + - result_4d.child_fabrics | length == 1 + - result_4d.child_fabrics[0].diff | length == 0 + - result_4d.child_fabrics[0].response | length == 0 + +############################################### +### FINAL CLEANUP ## +############################################### + +- name: CLEANUP.1 - MSD MERGED - [deleted] Delete all VRFs for cleanup + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + register: result_cleanup_1 + +- name: CLEANUP.2 - MSD MERGED - [wait_for] Wait 60 seconds for cleanup to complete + wait_for: + timeout: 60 + when: result_cleanup_1.changed == true + +############################################### +### FINAL NOTES ## +############################################### + +- name: FINAL - MSD MERGED - Summary of tests completed + ansible.builtin.debug: + msg: + - "MSD Merged tests completed successfully!" + - "Tests covered:" + - " 1. Basic VRF creation (Parent-only)" + - " 2. VRF creation with Child fabric config" + - " 3. VRF-Lite configuration creation" + - " 4. Multiple VRF creation" + - " 6. Comprehensive query validation" + - " 7. Idempotence verification for all scenarios" + - "All MSD fabric merged workflows validated successfully!" \ No newline at end of file diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/msd/overridden.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/msd/overridden.yaml new file mode 100644 index 000000000..bb0ed1da4 --- /dev/null +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/msd/overridden.yaml @@ -0,0 +1,454 @@ +############################################## +## REQUIRED VARS ## +############################################## +# parent_fabric +# A Parent MSD VXLAN_EVPN fabric +# +# child_fabric +# A Child MSD VXLAN_EVPN fabric associated with parent_fabric +# +# child_switch_1 +# - A switch in the child MSD fabric +# - VRF-lite capable switch +# +# child_switch_2 +# - A switch in the child MSD fabric +# - VRF-lite capable switch +# +# interface_2a +# - Ethernet interface on child_switch_2 +# - Used to test VRF LITE configuration in MSD environment +############################################## + +############################################## +## SETUP ## +############################################## + +- set_fact: + rest_path: "/rest/control/fabrics/{{ parent_fabric }}" + when: controller_version == "11" + +- set_fact: + rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ parent_fabric }}" + when: controller_version >= "12" + +- name: SETUP.0 - MSD OVERRIDDEN - [with_items] print vars + ansible.builtin.debug: + var: item + with_items: + - "parent_fabric : {{ parent_fabric }}" + - "child_fabric : {{ child_fabric }}" + - "child_switch_1 : {{ child_switch_1 }}" + - "child_switch_2 : {{ child_switch_2 }}" + - "interface_2a : {{ interface_2a }}" + +- name: SETUP.1 - MSD OVERRIDDEN - [dcnm_rest.GET] Verify parent fabric is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_path }}" + register: result + +- assert: + that: + - result.response.DATA != None + +- name: SETUP.2 - MSD OVERRIDDEN - [deleted] Clean slate - delete all VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + register: result_setup_2 + +- name: SETUP.2a - MSD OVERRIDDEN - [wait_for] Wait 60 seconds for controller and switch to sync + wait_for: + timeout: 60 + when: result_setup_2.changed == true + +- name: SETUP.3 - MSD OVERRIDDEN - [merged] Create initial VRFs for override testing + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: + - vrf_name: ansible-msd-override-initial1 + vrf_id: 9008501 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2501 + vrf_description: "Initial VRF 1 for override testing" + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + - vrf_name: ansible-msd-override-initial2 + vrf_id: 9008502 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2502 + vrf_description: "Initial VRF 2 for override testing" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + attach: + - ip_address: "{{ child_switch_2 }}" + deploy: true + - vrf_name: ansible-msd-override-initial3 + vrf_id: 9008503 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2503 + vrf_description: "Initial VRF 3 for override testing" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: false + adv_host_routes: true + static_default_route: false + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + register: result_setup_3 + +- name: SETUP.3a - MSD OVERRIDDEN - [debug] print setup result + ansible.builtin.debug: + var: result_setup_3 + +- name: SETUP.4 - MSD OVERRIDDEN - [query] Wait for initial VRFs to be deployed before testing override + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + register: result_setup_4 + until: + - "result_setup_4.response | selectattr('parent.vrfStatus', 'search', 'DEPLOYED') | list | length == 3" + retries: 20 + delay: 3 + +- name: SETUP.4a - MSD OVERRIDDEN - [debug] print pre-override VRFs + ansible.builtin.debug: + var: result_setup_4 + +############################################### +### MSD OVERRIDDEN TESTS ## +############################################### + +- name: TEST.1 - MSD OVERRIDDEN - [overridden] Basic override without Child config + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: overridden + config: &conf1 + - vrf_name: ansible-msd-override-basic + vrf_id: 9008511 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2511 + vrf_description: "Basic MSD override test VRF" + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + register: result_1 + +- name: TEST.1a - MSD OVERRIDDEN - [debug] print result_1 + ansible.builtin.debug: + var: result_1 + +- assert: + that: + - result_1.changed == true + - result_1.workflow == "Multisite Parent without Child Fabric Processing" + - result_1.diff[0].vrf_name == "ansible-msd-override-basic" + - result_1.diff[1].vrf_name == "ansible-msd-override-initial1" + - result_1.diff[2].vrf_name == "ansible-msd-override-initial2" + - result_1.diff[3].vrf_name == "ansible-msd-override-initial3" + - result_1.response[0].RETURN_CODE == 200 + +- name: TEST.1b - MSD OVERRIDDEN - [query] Verify override result + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf1 + register: result_1b + +- name: TEST.1c - MSD OVERRIDDEN - [debug] print result_1b + ansible.builtin.debug: + var: result_1b + +- assert: + that: + - result_1b.response | length == 1 + - result_1b.response[0].parent.vrfName == "ansible-msd-override-basic" + - result_1b.response[0].parent.vrfId == 9008511 + - (result_1b.response[0].parent.vrfTemplateConfig | from_json).vrfVlanId == "2511" + - (result_1b.response[0].parent.vrfTemplateConfig | from_json).vrfDescription == "Basic MSD override test VRF" + +- name: TEST.1d - MSD OVERRIDDEN - [overridden] Basic override - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: overridden + config: *conf1 + register: result_1d + +- name: TEST.1e - MSD OVERRIDDEN - [debug] print result_1d + ansible.builtin.debug: + var: result_1d + +- assert: + that: + - result_1d.changed == false + - result_1d.workflow == "Multisite Parent without Child Fabric Processing" + - result_1d.diff | length == 0 + - result_1d.response | length == 0 + +- name: TEST.2 - MSD OVERRIDDEN - [overridden] Override with Child fabric config + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: overridden + config: &conf2 + - vrf_name: ansible-msd-override-child + vrf_id: 9008512 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2512 + vrf_description: "MSD override test VRF with Child config" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + register: result_2 + +- name: TEST.2a - MSD OVERRIDDEN - [debug] print result_2 + ansible.builtin.debug: + var: result_2 + +- assert: + that: + - result_2.changed == true + - result_2.workflow == "Multisite Parent with Child Fabric Processing" + - result_2.parent_fabric.changed == true + - result_2.parent_fabric.diff[0].vrf_name == "ansible-msd-override-child" + - result_2.parent_fabric.diff[1].vrf_name == "ansible-msd-override-basic" + - result_2.child_fabrics[0].fabric == "AK-RT" + - result_2.child_fabrics[0].diff[0].vrf_name == "ansible-msd-override-child" + - result_2.parent_fabric.response[0].RETURN_CODE == 200 + +- name: TEST.2b - MSD OVERRIDDEN - [query] Verify override with child config + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf2 + register: result_2b + +- name: TEST.2c - MSD OVERRIDDEN - [debug] print result_2b + ansible.builtin.debug: + var: result_2b + +- assert: + that: + - result_2b.parent_fabric.response | length == 1 + - result_2b.parent_fabric.response[0].parent.vrfName == "ansible-msd-override-child" + - result_2b.parent_fabric.response[0].parent.vrfId == 9008512 + - (result_2b.parent_fabric.response[0].parent.vrfTemplateConfig | from_json).vrfVlanId == "2512" + - (result_2b.parent_fabric.response[0].parent.vrfTemplateConfig | from_json).vrfDescription == "MSD override test VRF with Child config" + - result_2b.child_fabrics | length == 1 + - result_2b.child_fabrics[0].response | length == 1 + - result_2b.child_fabrics[0].response[0].parent.vrfName == "ansible-msd-override-child" + - (result_2b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseDefaultRouteFlag == "true" + - (result_2b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseHostRouteFlag == "false" + - (result_2b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).configureStaticDefaultRouteFlag == "true" + +- name: TEST.2d - MSD OVERRIDDEN - [overridden] Override with child config - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: overridden + config: *conf2 + register: result_2d + +- name: TEST.2e - MSD OVERRIDDEN - [debug] print result_2d + ansible.builtin.debug: + var: result_2d + +- assert: + that: + - result_2d.changed == false + - result_2d.workflow == "Multisite Parent with Child Fabric Processing" + - result_2d.parent_fabric.diff | length == 0 + - result_2d.parent_fabric.response | length == 0 + - result_2d.child_fabrics | length == 1 + - result_2d.child_fabrics[0].diff | length == 0 + - result_2d.child_fabrics[0].response | length == 0 + +- name: TEST.3 - MSD OVERRIDDEN - [overridden] Override with multiple VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: overridden + config: &conf3 + - vrf_name: ansible-msd-override-bulk1 + vrf_id: 9008521 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2521 + vrf_description: "Bulk override test VRF 1" + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + - vrf_name: ansible-msd-override-bulk2 + vrf_id: 9008522 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2522 + vrf_description: "Bulk override test VRF 2" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + attach: + - ip_address: "{{ child_switch_2 }}" + deploy: true + register: result_3 + +- name: TEST.3a - MSD OVERRIDDEN - [debug] print result_3 + ansible.builtin.debug: + var: result_3 + +- assert: + that: + - result_3.changed == true + - result_3.workflow == "Multisite Parent with Child Fabric Processing" + - result_3.parent_fabric.changed == true + - result_3.parent_fabric.diff | length == 3 + - result_3.parent_fabric.diff | selectattr('vrf_name', 'equalto', 'ansible-msd-override-bulk1') | list | length == 1 + - result_3.parent_fabric.diff | selectattr('vrf_name', 'equalto', 'ansible-msd-override-bulk2') | list | length == 1 + - result_3.parent_fabric.diff | selectattr('vrf_name', 'equalto', 'ansible-msd-override-child') | selectattr('attach', 'defined') | list | length == 1 + - result_3.child_fabrics | length == 1 + - result_3.child_fabrics[0].fabric == "AK-RT" + - result_3.child_fabrics[0].diff[0].vrf_name == "ansible-msd-override-bulk2" + - result_3.parent_fabric.response[0].RETURN_CODE == 200 + +- name: TEST.3b - MSD OVERRIDDEN - [query] Verify multiple VRFs override + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf3 + register: result_3b + +- name: TEST.3c - MSD OVERRIDDEN - [debug] print result_3b + ansible.builtin.debug: + var: result_3b + +- assert: + that: + - result_3b.parent_fabric.response | length == 2 + - result_3b.parent_fabric.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-override-bulk1') | list | length == 1 + - result_3b.parent_fabric.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-override-bulk2') | list | length == 1 + - result_3b.child_fabrics | length == 1 + - result_3b.child_fabrics[0].response | length == 1 + - result_3b.child_fabrics[0].response[0].parent.vrfName == "ansible-msd-override-bulk2" + - (result_3b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseDefaultRouteFlag == "true" + - (result_3b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseHostRouteFlag == "false" + - (result_3b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).configureStaticDefaultRouteFlag == "true" + +- name: TEST.3d - MSD OVERRIDDEN - [overridden] Override with multiple VRFs - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: overridden + config: *conf3 + register: result_3d + +- name: TEST.3e - MSD OVERRIDDEN - [debug] print result_3d + ansible.builtin.debug: + var: result_3d + +- assert: + that: + - result_3d.changed == false + - result_3d.workflow == "Multisite Parent with Child Fabric Processing" + - result_3d.parent_fabric.diff | length == 0 + - result_3d.parent_fabric.response | length == 0 + - result_3d.child_fabrics | length == 1 + - result_3d.child_fabrics[0].diff | length == 0 + - result_3d.child_fabrics[0].response | length == 0 + +- name: TEST.4 - MSD OVERRIDDEN - [overridden] Override with empty config (delete all) + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: overridden + config: [] + register: result_4 + +- name: TEST.4a - MSD OVERRIDDEN - [debug] print result_4 + ansible.builtin.debug: + var: result_4 + +- assert: + that: + - result_4.changed == true + - result_4.workflow == "Multisite Parent without Child Fabric Processing" + - result_4.diff | length == 2 + - result_4.diff | selectattr('vrf_name', 'equalto', 'ansible-msd-override-bulk1') | list | length == 1 + - result_4.diff | selectattr('vrf_name', 'equalto', 'ansible-msd-override-bulk2') | list | length == 1 + - result_4.response[0].RETURN_CODE == 200 + +- name: TEST.4b - MSD OVERRIDDEN - [query] Verify all VRFs deleted by empty override + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + register: result_4b + +- name: TEST.4c - MSD OVERRIDDEN - [debug] print result_4b + ansible.builtin.debug: + var: result_4b + +- assert: + that: + - result_4b.response | length == 0 + +- name: TEST.4d - MSD OVERRIDDEN - [overridden] Override with empty config - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: overridden + config: [] + register: result_4d + +- name: TEST.4e - MSD OVERRIDDEN - [debug] print result_4d + ansible.builtin.debug: + var: result_4d + +- assert: + that: + - result_4d.changed == false + - result_4d.workflow == "Multisite Parent without Child Fabric Processing" + - result_4d.diff | length == 0 + - result_4d.response | length == 0 + +############################################### +### FINAL CLEANUP ## +############################################### + +- name: CLEANUP.1 - MSD OVERRIDDEN - [deleted] Ensure all VRFs are deleted + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + register: result_cleanup_1 + +- name: CLEANUP.2 - MSD OVERRIDDEN - [wait_for] Wait 60 seconds for cleanup to complete + wait_for: + timeout: 60 + when: result_cleanup_1.changed == true + +############################################### +### FINAL NOTES ## +############################################### + +- name: FINAL - MSD OVERRIDDEN - Summary of tests completed + ansible.builtin.debug: + msg: + - "MSD Overridden tests completed successfully!" + - "Tests covered:" + - " 1. Basic VRF override (Parent-only)" + - " 2. VRF override with Child fabric config" + - " 3. Multiple VRF override" + - " 4. Empty config override (delete all)" + - " 5. Idempotence verification for all scenarios" + - "All MSD fabric override workflows validated successfully!" \ No newline at end of file diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/msd/query.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/msd/query.yaml new file mode 100644 index 000000000..e2c90ea0b --- /dev/null +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/msd/query.yaml @@ -0,0 +1,410 @@ +############################################## +## REQUIRED VARS ## +############################################## +# parent_fabric +# A Parent MSD VXLAN_EVPN fabric +# +# child_fabric +# A Child MSD VXLAN_EVPN fabric associated with parent_fabric +# +# child_switch_1 +# - A switch in the child MSD fabric +# - VRF-lite capable switch +# +############################################## +## REQUIRED VARS ## +############################################## +# parent_fabric +# A Parent MSD VXLAN_EVPN fabric +# +# child_fabric +# A Child MSD VXLAN_EVPN fabric associated with parent_fabric +# +# child_switch_1 +# - A switch in the child MSD fabric +# - VRF-lite capable switch +# +# child_switch_2 +# - A switch in the child MSD fabric +# - VRF-lite capable switch +# +# interface_2a +# - Ethernet interface on child_switch_2 +# - Used to test VRF LITE configuration in MSD environment +############################################## + +############################################## +## SETUP ## +############################################## + +- set_fact: + rest_path: "/rest/control/fabrics/{{ parent_fabric }}" + when: controller_version == "11" + +- set_fact: + rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ parent_fabric }}" + when: controller_version >= "12" + +- name: SETUP.0 - MSD QUERY - [with_items] print vars + ansible.builtin.debug: + var: item + with_items: + - "parent_fabric : {{ parent_fabric }}" + - "child_fabric : {{ child_fabric }}" + - "child_switch_1 : {{ child_switch_1 }}" + - "child_switch_2 : {{ child_switch_2 }}" + - "interface_2a : {{ interface_2a }}" + +- name: SETUP.1 - MSD QUERY - [dcnm_rest.GET] Verify parent fabric is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_path }}" + register: result + +- assert: + that: + - result.response.DATA != None + +- name: SETUP.2 - MSD QUERY - [deleted] Clean slate - delete all VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + register: result_setup_2 + +- name: SETUP.2a - MSD QUERY - [wait_for] Wait 60 seconds for controller and switch to sync + wait_for: + timeout: 60 + when: result_setup_2.changed == true + +- name: SETUP.3 - MSD QUERY - [merged] Create VRFs for query testing + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: + - vrf_name: ansible-msd-query-basic + vrf_id: 9008201 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2201 + vrf_description: "Basic MSD query test VRF" + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + - vrf_name: ansible-msd-query-child + vrf_id: 9008202 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2202 + vrf_description: "MSD query test VRF with Child config" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + - vrf_name: ansible-msd-query-lite + vrf_id: 9008203 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2203 + vrf_description: "MSD query test VRF with VRF-Lite" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + attach: + - ip_address: "{{ child_switch_2 }}" + vrf_lite: + - interface: "{{ interface_2a }}" + ipv4_addr: 10.10.1.2/30 + neighbor_ipv4: 10.10.1.1 + peer_vrf: external_query_vrf + dot1q: 600 + deploy: true + - vrf_name: ansible-msd-query-multiple-1 + vrf_id: 9008221 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2221 + vrf_description: "Query test VRF 1 for multiple query tests" + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + - vrf_name: ansible-msd-query-multiple-2 + vrf_id: 9008222 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2222 + vrf_description: "Query test VRF 2 for multiple query tests" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + attach: + - ip_address: "{{ child_switch_2 }}" + deploy: true + register: result_setup_3 + +- name: SETUP.3a - MSD QUERY - [wait_for] Wait for VRFs to be deployed + wait_for: + timeout: 60 + +- name: SETUP.3b - MSD QUERY - [debug] print setup result + ansible.builtin.debug: + var: result_setup_3 + +############################################### +### MSD QUERY TESTS ## +############################################### + +- name: TEST.1 - MSD QUERY - [query] Basic VRF query without Child config + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: + - vrf_name: ansible-msd-query-basic + register: result_1 + +- name: TEST.1a - MSD QUERY - [debug] print result_1 + ansible.builtin.debug: + var: result_1 + +- assert: + that: + - result_1.changed == false + - result_1.workflow == "Multisite Parent without Child Fabric Processing" + - result_1.response | length == 1 + - result_1.response[0].parent.vrfName == "ansible-msd-query-basic" + - result_1.response[0].parent.vrfId == 9008201 + - result_1.response[0].parent.fabric == "{{ parent_fabric }}" + - (result_1.response[0].parent.vrfTemplateConfig | from_json).vrfVlanId == "2201" + - result_1.response[0].attach | length >= 1 + - result_1.response[0].attach[0].vrfName == "ansible-msd-query-basic" + +- name: TEST.2 - MSD QUERY - [query] VRF query with Child config + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: + - vrf_name: ansible-msd-query-child + register: result_2 + +- name: TEST.2a - MSD QUERY - [debug] print result_2 + ansible.builtin.debug: + var: result_2 + +- assert: + that: + - result_2.changed == false + - result_2.workflow == "Multisite Parent without Child Fabric Processing" + - result_2.response | length == 1 + - result_2.response[0].parent.vrfName == "ansible-msd-query-child" + - result_2.response[0].parent.vrfId == 9008202 + - result_2.response[0].parent.fabric == "{{ parent_fabric }}" + - (result_2.response[0].parent.vrfTemplateConfig | from_json).vrfVlanId == "2202" + - result_2.response[0].attach | length >= 1 + +- name: TEST.3 - MSD QUERY - [query] Query multiple specific VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: + - vrf_name: ansible-msd-query-basic + child_fabric_config: + - fabric: "{{ child_fabric }}" + - vrf_name: ansible-msd-query-child + child_fabric_config: + - fabric: "{{ child_fabric }}" + - vrf_name: ansible-msd-query-lite + register: result_3 + +- name: TEST.3a - MSD QUERY - [debug] print result_3 + ansible.builtin.debug: + var: result_3 + +- assert: + that: + - result_3.changed == false + - result_3.workflow == "Multisite Parent with Child Fabric Processing" + - result_3.parent_fabric.fabric == "{{ parent_fabric }}" + - result_3.parent_fabric.response | length == 3 + - result_3.parent_fabric.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-query-basic') | list | length == 1 + - result_3.parent_fabric.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-query-child') | list | length == 1 + - result_3.parent_fabric.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-query-lite') | list | length == 1 + - result_3.parent_fabric.response | selectattr('parent.vrfId', 'equalto', 9008201) | list | length == 1 + - result_3.parent_fabric.response | selectattr('parent.vrfId', 'equalto', 9008202) | list | length == 1 + - result_3.parent_fabric.response | selectattr('parent.vrfId', 'equalto', 9008203) | list | length == 1 + - result_3.child_fabrics | length == 1 + - result_3.child_fabrics[0].fabric == "{{ child_fabric }}" + - result_3.child_fabrics[0].response | length == 2 + +- name: TEST.4 - MSD QUERY - [query] Query all VRFs (no config specified - MSD Parent) + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + register: result_4 + +- name: TEST.4a - MSD QUERY - [debug] print result_4 + ansible.builtin.debug: + var: result_4 + +- assert: + that: + - result_4.changed == false + - result_4.workflow == "Multisite Parent without Child Fabric Processing" + - result_4.response | length == 5 # Should return all 5 VRFs created + - result_4.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-query-basic') | list | length == 1 + - result_4.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-query-child') | list | length == 1 + - result_4.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-query-lite') | list | length == 1 + - result_4.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-query-multiple-1') | list | length == 1 + - result_4.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-query-multiple-2') | list | length == 1 + - result_4.response[0].parent.vrfId == 9008221 + - (result_4.response[0].parent.vrfTemplateConfig | from_json).vrfVlanId == "2221" + - result_4.diff | length == 0 + +- name: TEST.5 - MSD QUERY - [query] Query all VRFs (no config specified - MSD Parent) + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + register: result_5 + +- name: TEST.5a - MSD QUERY - [debug] print result_5 + ansible.builtin.debug: + var: result_5 + +- assert: + that: + - result_5.changed == false + - result_5.workflow == "Multisite Parent without Child Fabric Processing" + - result_5.response | length == 5 # Should return all 5 VRFs created + - result_5.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-query-basic') | list | length == 1 + - result_5.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-query-child') | list | length == 1 + - result_5.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-query-lite') | list | length == 1 + - result_5.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-query-multiple-1') | list | length == 1 + - result_5.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-query-multiple-2') | list | length == 1 + - result_5.diff | length == 0 + +- name: TEST.6 - MSD QUERY - [query] Query all VRFs (no config specified - MSD Child) + cisco.dcnm.dcnm_vrf: + fabric: "{{ child_fabric }}" + state: query + register: result_6 + +- name: TEST.6a - MSD QUERY - [debug] print result_6 + ansible.builtin.debug: + var: result_6 + +- assert: + that: + - result_6.changed == false + - result_6.workflow == "Multisite Child VRF Processing" + - result_6.response | length == 5 + - result_6.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-query-basic') | list | length == 1 + - result_6.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-query-child') | list | length == 1 + - result_6.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-query-lite') | list | length == 1 + - result_6.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-query-multiple-1') | list | length == 1 + - result_6.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-query-multiple-2') | list | length == 1 + - result_6.diff | length == 0 + +- name: TEST.7 - MSD QUERY - [query] Query non-existent VRF (MSD Parent) + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: + - vrf_name: ansible-msd-nonexistent + register: result_7 + +- name: TEST.7a - MSD QUERY - [debug] print result_7 + ansible.builtin.debug: + var: result_7 + +- assert: + that: + - result_7.changed == false + - result_7.workflow == "Multisite Parent without Child Fabric Processing" + - result_7.response | length == 0 # Should return no VRFs + - result_7.diff | length == 0 + +- name: TEST.8 - MSD QUERY - [query] Query non-existent VRF (MSD Child) + cisco.dcnm.dcnm_vrf: + fabric: "{{ child_fabric }}" + state: query + config: + - vrf_name: ansible-msd-nonexistent + register: result_8 + +- name: TEST.8a - MSD QUERY - [debug] print result_8 + ansible.builtin.debug: + var: result_8 + +- assert: + that: + - result_8.changed == false + - result_8.workflow == "Multisite Child VRF Processing" + - result_8.response | length == 0 # Should return no VRFs + - result_8.diff | length == 0 + +############################################### +### CLEANUP ## +############################################### + +- name: CLEANUP.1 - MSD QUERY - [deleted] Delete all VRFs from Parent MSD fabric + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + register: result_cleanup_1 + +- name: CLEANUP.1a - MSD QUERY - [debug] print cleanup result + ansible.builtin.debug: + var: result_cleanup_1 + +- assert: + that: + - result_cleanup_1.changed == true # Should delete the 5 VRFs we created + - result_cleanup_1.workflow == "Multisite Parent without Child Fabric Processing" + - result_cleanup_1.diff | length >= 5 + +- name: CLEANUP.2 - MSD QUERY - [wait_for] Wait 60 seconds for cleanup to complete + wait_for: + timeout: 60 + +- name: CLEANUP.3 - MSD QUERY - [query] Verify all VRFs were deleted + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + register: result_cleanup_3 + +- name: CLEANUP.3a - MSD QUERY - [debug] print final query result + ansible.builtin.debug: + var: result_cleanup_3 + +- assert: + that: + - result_cleanup_3.changed == false + - result_cleanup_3.workflow == "Multisite Parent without Child Fabric Processing" + - result_cleanup_3.response | length == 0 # No VRFs should remain + - result_cleanup_3.diff | length == 0 + +############################################### +### FINAL NOTES ## +############################################### + +- name: FINAL - MSD QUERY - Summary of tests completed + ansible.builtin.debug: + msg: + - "MSD Query tests completed successfully!" + - "Tests covered:" + - " 1. Basic VRF query (Parent-only)" + - " 2. VRF query with Child fabric config" + - " 3. VRF query with VRF-Lite configuration" + - " 4. Multiple specific VRF queries" + - " 5. Query all VRFs (no config specified)" + - " 6. Query non-existent VRF" + - " 7. Query mixed VRFs (existing and non-existing)" + - " 8. Query multiple VRFs for bulk query tests" + - " 9. Idempotence verification for all query operations" + - "All MSD fabric query workflows validated successfully!" \ No newline at end of file diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/msd/replaced.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/msd/replaced.yaml new file mode 100644 index 000000000..dbaeb7fc9 --- /dev/null +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/msd/replaced.yaml @@ -0,0 +1,626 @@ +############################################## +## REQUIRED VARS ## +############################################## +# parent_fabric +# A Parent MSD VXLAN_EVPN fabric +# +# child_fabric +# A Child MSD VXLAN_EVPN fabric associated with parent_fabric +# +# child_switch_1 +# - A switch in the child MSD fabric +# - VRF-lite capable switch +# +# child_switch_2 +# - A switch in the child MSD fabric +# - VRF-lite capable switch +# +# interface_2a +# - Ethernet interface on child_switch_2 +# - Used to test VRF LITE configuration in MSD environment +############################################## + +############################################## +## SETUP ## +############################################## + +- set_fact: + rest_path: "/rest/control/fabrics/{{ parent_fabric }}" + when: controller_version == "11" + +- set_fact: + rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ parent_fabric }}" + when: controller_version >= "12" + +- name: SETUP.0 - MSD REPLACED - [with_items] print vars + ansible.builtin.debug: + var: item + with_items: + - "parent_fabric : {{ parent_fabric }}" + - "child_fabric : {{ child_fabric }}" + - "child_switch_1 : {{ child_switch_1 }}" + - "child_switch_2 : {{ child_switch_2 }}" + - "interface_2a : {{ interface_2a }}" + +- name: SETUP.1 - MSD REPLACED - [dcnm_rest.GET] Verify parent fabric is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_path }}" + register: result + +- assert: + that: + - result.response.DATA != None + +- name: SETUP.2 - MSD REPLACED - [deleted] Delete all VRFs from Parent MSD fabric + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + register: result_setup_2 + +- name: SETUP.2a - MSD REPLACED - [wait_for] Wait 60 seconds for controller and switch to sync + wait_for: + timeout: 60 + when: result_setup_2.changed == true + +- name: SETUP.3 - MSD REPLACED - [merged] Create initial VRFs for replacement testing + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: + - vrf_name: ansible-msd-replace-1 + vrf_id: 9008021 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2021 + vrf_description: "Initial VRF for replacement test" + vrf_int_mtu: 9000 + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + - vrf_name: ansible-msd-replace-2 + vrf_id: 9008022 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2022 + vrf_description: "Second VRF for replacement test" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: false + adv_host_routes: true + static_default_route: false + l3vni_wo_vlan: true + attach: + - ip_address: "{{ child_switch_1 }}" + - ip_address: "{{ child_switch_2 }}" + deploy: true + register: result_setup_3 + +- name: SETUP.3a - MSD REPLACED - [debug] print setup result + ansible.builtin.debug: + var: result_setup_3 + +############################################### +### MSD REPLACED TESTS ## +############################################### + +- name: TEST.1 - MSD REPLACED - [replaced] Replace VRF properties and Child config + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: replaced + config: &conf1 + - vrf_name: ansible-msd-replace-1 + vrf_id: 9008021 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2021 + vrf_description: "Updated VRF description after replacement" + vrf_int_mtu: 9216 # Changed from 9000 + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: false # Changed from true + adv_host_routes: true # Changed from false + static_default_route: false # Changed from true + l3vni_wo_vlan: true # New parameter + netflow_enable: false # New parameter + attach: + - ip_address: "{{ child_switch_1 }}" + - ip_address: "{{ child_switch_2 }}" # Added new attachment + deploy: true + register: result_1 + +- name: TEST.1a - MSD REPLACED - [debug] print result_1 + ansible.builtin.debug: + var: result_1 + +- assert: + that: + - result_1.changed == true + - result_1.workflow == "Multisite Parent with Child Fabric Processing" + - result_1.parent_fabric.fabric == "{{ parent_fabric }}" + - result_1.parent_fabric.changed == true + - result_1.parent_fabric.diff[0].vrf_id == 9008021 + - result_1.parent_fabric.diff[0].vrf_name == "ansible-msd-replace-1" + - result_1.parent_fabric.diff[0].vrf_description == "Updated VRF description after replacement" + - result_1.parent_fabric.diff[0].vrf_int_mtu == 9216 + - result_1.parent_fabric.diff[0].attach | length == 1 + - result_1.parent_fabric.diff[0].attach[0].ip_address == "{{ child_switch_2 }}" + - result_1.parent_fabric.diff[0].attach[0].deploy == true + - result_1.child_fabrics | length == 1 + - result_1.child_fabrics[0].fabric == "{{ child_fabric }}" + - result_1.child_fabrics[0].diff[0].vrf_name == "ansible-msd-replace-1" + - result_1.child_fabrics[0].diff[0].adv_default_routes == false + - result_1.child_fabrics[0].diff[0].adv_host_routes == true + - result_1.child_fabrics[0].diff[0].static_default_route == false + - result_1.child_fabrics[0].diff[0].l3vni_wo_vlan == true + - result_1.child_fabrics[0].response[0].RETURN_CODE == 200 + +- name: TEST.1b - MSD REPLACED - [query] Query replaced VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf1 + register: result_1b + until: + - "result_1b.parent_fabric.response[0].parent.vrfStatus is search('DEPLOYED')" + - "result_1b.child_fabrics[0].response[0].parent.vrfStatus is search('DEPLOYED')" + retries: 20 + delay: 3 + +- name: TEST.1c - MSD REPLACED - [debug] print result_1b + ansible.builtin.debug: + var: result_1b + +- assert: + that: + - result_1b.parent_fabric.response | length == 1 + - result_1b.parent_fabric.response[0].parent.vrfName == "ansible-msd-replace-1" + - result_1b.parent_fabric.response[0].parent.vrfStatus == "DEPLOYED" + - (result_1b.parent_fabric.response[0].parent.vrfTemplateConfig | from_json).mtu == "9216" + - result_1b.child_fabrics | length == 1 + - result_1b.child_fabrics[0].response | length == 1 + - result_1b.child_fabrics[0].response[0].parent.vrfName == "ansible-msd-replace-1" + - result_1b.child_fabrics[0].response[0].parent.vrfStatus == "DEPLOYED" + - (result_1b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseDefaultRouteFlag == "false" + - (result_1b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseHostRouteFlag == "true" + - (result_1b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).configureStaticDefaultRouteFlag == "false" + - (result_1b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).enableL3VniNoVlan == "true" + +- name: TEST.1d - MSD REPLACED - [replaced] Replace VRF - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: replaced + config: *conf1 + register: result_1d + +- name: TEST.1e - MSD REPLACED - [debug] print result_1d + ansible.builtin.debug: + var: result_1d + +- assert: + that: + - result_1d.changed == false + - result_1d.workflow == "Multisite Parent with Child Fabric Processing" + - result_1d.parent_fabric.diff | length == 0 + - result_1d.parent_fabric.response | length == 0 + - result_1d.child_fabrics | length == 1 + - result_1d.child_fabrics[0].diff | length == 0 + - result_1d.child_fabrics[0].response | length == 0 + +- name: TEST.2 - MSD REPLACED - [replaced] Replace VRF - Remove one attachment + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: replaced + config: &conf2 + - vrf_name: ansible-msd-replace-2 + vrf_id: 9008022 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2022 + vrf_description: "VRF with removed Child config" + attach: + - ip_address: "{{ child_switch_1 }}" + # Removed child_switch_2 attachment + deploy: true + register: result_2 + +- name: TEST.2a - MSD REPLACED - [debug] print result_2 + ansible.builtin.debug: + var: result_2 + +- assert: + that: + - result_2.changed == true + - result_2.workflow == "Multisite Parent without Child Fabric Processing" + - result_2.changed == true + - result_2.diff[0].vrf_name == "ansible-msd-replace-2" + - result_2.diff[0].attach | length == 1 + - result_2.diff[0].attach[0].ip_address == "{{ child_switch_2 }}" + - result_2.diff[0].attach[0].deploy == false + - result_2.response[0].RETURN_CODE == 200 + +- name: TEST.2b - MSD REPLACED - [query] Query replaced VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf2 + register: result_2b + until: + - "result_2b.response[0].parent.vrfStatus is search('DEPLOYED')" + retries: 20 + delay: 3 + +- name: TEST.2c - MSD REPLACED - [debug] print result_2b + ansible.builtin.debug: + var: result_2b + +- assert: + that: + - result_2b.response | length == 1 + - result_2b.response[0].parent.vrfName == "ansible-msd-replace-2" + - result_2b.response[0].parent.vrfStatus == "DEPLOYED" + - (result_2b.response[0].parent.vrfTemplateConfig | from_json).vrfDescription == "VRF with removed Child config" + - result_2b.workflow == "Multisite Parent without Child Fabric Processing" + +- name: TEST.2d - MSD REPLACED - [replaced] Replace VRF - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: replaced + config: *conf2 + register: result_2d + +- name: TEST.2e - MSD REPLACED - [debug] print result_2d + ansible.builtin.debug: + var: result_2d + +- assert: + that: + - result_2d.changed == false + - result_2d.workflow == "Multisite Parent without Child Fabric Processing" + - result_2d.diff | length == 0 + - result_2d.response | length == 0 + +- name: TEST.3 - MSD REPLACED - [replaced] Replace VRF with Child fabric config + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: replaced + config: &conf3 + - vrf_name: ansible-msd-replace-2 + vrf_id: 9008022 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2022 + vrf_description: "VRF with new Child config" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + attach: + - ip_address: "{{ child_switch_1 }}" + - ip_address: "{{ child_switch_2 }}" + deploy: true + register: result_3 + +- name: TEST.3a - MSD REPLACED - [debug] print result_3 + ansible.builtin.debug: + var: result_3 + +- assert: + that: + - result_3.changed == true + - result_3.workflow == "Multisite Parent with Child Fabric Processing" + - result_3.parent_fabric.fabric == "{{ parent_fabric }}" + - result_3.parent_fabric.changed == true + - result_3.parent_fabric.diff[0].vrf_id == 9008022 + - result_3.parent_fabric.diff[0].vrf_name == "ansible-msd-replace-2" + - result_3.parent_fabric.diff[0].vrf_description == "VRF with new Child config" + - result_3.parent_fabric.diff[0].attach | length == 1 + - result_3.parent_fabric.diff[0].attach[0].ip_address == "{{ child_switch_2 }}" + - result_3.parent_fabric.response[0].RETURN_CODE == 200 + - result_3.child_fabrics | length == 1 + - result_3.child_fabrics[0].fabric == "{{ child_fabric }}" + - result_3.child_fabrics[0].diff[0].vrf_name == "ansible-msd-replace-2" + - result_3.child_fabrics[0].diff[0].adv_default_routes == true + - result_3.child_fabrics[0].diff[0].adv_host_routes == false + - result_3.child_fabrics[0].diff[0].static_default_route == true + +- name: TEST.3b - MSD REPLACED - [query] Query replaced VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf3 + register: result_3b + until: + - "result_3b.parent_fabric.response[0].parent.vrfStatus is search('DEPLOYED')" + - "result_3b.child_fabrics[0].response[0].parent.vrfStatus is search('DEPLOYED')" + retries: 20 + delay: 3 + +- name: TEST.3c - MSD REPLACED - [debug] print result_3b + ansible.builtin.debug: + var: result_3b + +- assert: + that: + - result_3b.parent_fabric.response | length == 1 + - result_3b.parent_fabric.response[0].parent.vrfName == "ansible-msd-replace-2" + - result_3b.parent_fabric.response[0].parent.vrfStatus == "DEPLOYED" + - (result_3b.parent_fabric.response[0].parent.vrfTemplateConfig | from_json).vrfDescription == "VRF with new Child config" + - result_3b.child_fabrics | length == 1 + - result_3b.child_fabrics[0].response | length == 1 + - result_3b.child_fabrics[0].response[0].parent.vrfName == "ansible-msd-replace-2" + - result_3b.child_fabrics[0].response[0].parent.vrfStatus == "DEPLOYED" + - (result_3b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseDefaultRouteFlag == "true" + - (result_3b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseHostRouteFlag == "false" + - (result_3b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).configureStaticDefaultRouteFlag == "true" + - result_3b.workflow == "Multisite Parent with Child Fabric Processing" + +- name: TEST.3d - MSD REPLACED - [replaced] Replace VRF - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: replaced + config: *conf3 + register: result_3d + +- name: TEST.3e - MSD REPLACED - [debug] print result_3d + ansible.builtin.debug: + var: result_3d + +- assert: + that: + - result_3d.changed == false + - result_3d.workflow == "Multisite Parent with Child Fabric Processing" + - result_3d.parent_fabric.changed == false + - result_3d.parent_fabric.diff | length == 0 + - result_3d.child_fabrics[0].diff | length == 0 + - result_3d.child_fabrics[0].response | length == 0 + +- name: TEST.4 - MSD REPLACED - [replaced] Replace VRF with VRF-Lite configuration + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: replaced + config: &conf4 + - vrf_name: ansible-msd-replace-1 + vrf_id: 9008021 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2021 + vrf_description: "VRF with VRF-Lite after replacement" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + attach: + - ip_address: "{{ child_switch_1 }}" + - ip_address: "{{ child_switch_2 }}" + vrf_lite: + - interface: "{{ interface_2a }}" + ipv4_addr: 10.10.1.2/30 + neighbor_ipv4: 10.10.1.1 + peer_vrf: external_replace_vrf + dot1q: 200 + deploy: true + register: result_4 + +- name: TEST.4a - MSD REPLACED - [debug] print result_4 + ansible.builtin.debug: + var: result_4 + +- assert: + that: + - result_4.changed == true + - result_4.workflow == "Multisite Parent with Child Fabric Processing" + - result_4.parent_fabric.fabric == "{{ parent_fabric }}" + - result_4.parent_fabric.changed == true + - result_4.parent_fabric.diff[0].vrf_id == 9008021 + - result_4.parent_fabric.diff[0].vrf_name == "ansible-msd-replace-1" + - result_4.parent_fabric.diff[0].vrf_description == "VRF with VRF-Lite after replacement" + - result_4.parent_fabric.diff[0].attach | length == 1 + - result_4.parent_fabric.diff[0].attach[0].ip_address == "{{ child_switch_2 }}" + - result_4.parent_fabric.response[0].RETURN_CODE == 200 + - result_4.child_fabrics | length == 1 + - result_4.child_fabrics[0].fabric == "{{ child_fabric }}" + - result_4.child_fabrics[0].diff[0].vrf_name == "ansible-msd-replace-1" + - result_4.child_fabrics[0].response[0].RETURN_CODE == 200 + +- name: TEST.4b - MSD REPLACED - [query] Query replaced VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf4 + register: result_4b + until: + - "result_4b.parent_fabric.response[0].parent.vrfStatus is search('DEPLOYED')" + - "result_4b.child_fabrics[0].response[0].parent.vrfStatus is search('DEPLOYED')" + retries: 20 + delay: 3 + +- name: TEST.4c - MSD REPLACED - [debug] print result_4b + ansible.builtin.debug: + var: result_4b + +- assert: + that: + - result_4b.parent_fabric.response | length == 1 + - result_4b.parent_fabric.response[0].parent.vrfName == "ansible-msd-replace-1" + - result_4b.parent_fabric.response[0].parent.vrfId == 9008021 + - (result_4b.parent_fabric.response[0].parent.vrfTemplateConfig | from_json).vrfDescription == "VRF with VRF-Lite after replacement" + - result_4b.child_fabrics | length == 1 + - result_4b.child_fabrics[0].response | length == 1 + - result_4b.child_fabrics[0].response[0].parent.vrfName == "ansible-msd-replace-1" + - result_4b.child_fabrics[0].response[0].parent.vrfId == 9008021 + - (result_4b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseDefaultRouteFlag == "true" + - result_4b.workflow == "Multisite Parent with Child Fabric Processing" + +- name: TEST.4d - MSD REPLACED - [replaced] Replace VRF - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: replaced + config: *conf4 + register: result_4d + +- name: TEST.4e - MSD REPLACED - [debug] print result_4d + ansible.builtin.debug: + var: result_4d + +- assert: + that: + - result_4d.changed == true + - result_4d.workflow == "Multisite Parent with Child Fabric Processing" + - result_4d.parent_fabric.changed == false + - result_4d.parent_fabric.diff | length == 0 + - result_4d.parent_fabric.response | length == 0 + - result_4d.child_fabrics[0].diff | length == 0 + - result_4d.child_fabrics[0].response | length == 0 + +- name: TEST.5 - MSD REPLACED - [replaced] Replace multiple VRFs simultaneously + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: replaced + config: &conf5 + - vrf_name: ansible-msd-replace-1 + vrf_id: 9008021 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2021 + vrf_description: "Multi-replace VRF 1" + vrf_int_mtu: 9000 + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: false + adv_host_routes: true + static_default_route: false + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + - vrf_name: ansible-msd-replace-2 + vrf_id: 9008022 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2022 + vrf_description: "Multi-replace VRF 2" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: false + adv_host_routes: true + static_default_route: false + l3vni_wo_vlan: false + attach: + - ip_address: "{{ child_switch_2 }}" + deploy: true + register: result_5 + +- name: TEST.5a - MSD REPLACED - [debug] print result_5 + ansible.builtin.debug: + var: result_5 + +- assert: + that: + - result_5.changed == true + - result_5.workflow == "Multisite Parent with Child Fabric Processing" + - result_5.parent_fabric.fabric == "{{ parent_fabric }}" + - result_5.parent_fabric.changed == true + - result_5.parent_fabric.diff | length == 4 + - result_5.parent_fabric.diff[0].vrf_name == "ansible-msd-replace-1" + - result_5.parent_fabric.diff[0].vrf_int_mtu == 9000 + - result_5.parent_fabric.diff[0].attach[0].deploy == false + - result_5.parent_fabric.diff[1].vrf_name == "ansible-msd-replace-2" + - result_5.parent_fabric.diff[1].attach[0].deploy == false + - result_5.parent_fabric.response[0].RETURN_CODE == 200 + - result_5.child_fabrics | length == 1 + - result_5.child_fabrics[0].fabric == "{{ child_fabric }}" + - result_5.child_fabrics[0].diff | length == 2 + - result_5.child_fabrics[0].diff[0].vrf_name == "ansible-msd-replace-1" + - result_5.child_fabrics[0].diff[0].adv_default_routes == false + - result_5.child_fabrics[0].diff[0].adv_host_routes == true + - result_5.child_fabrics[0].diff[1].vrf_name == "ansible-msd-replace-2" + - result_5.child_fabrics[0].response[0].RETURN_CODE == 200 + +- name: TEST.5b - MSD REPLACED - [query] Query replaced VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf5 + register: result_5b + until: + - "result_5b.parent_fabric.response[0].parent.vrfStatus is search('DEPLOYED')" + - "result_5b.parent_fabric.response[1].parent.vrfStatus is search('DEPLOYED')" + - "result_5b.child_fabrics[0].response[0].parent.vrfStatus is search('DEPLOYED')" + - "result_5b.child_fabrics[0].response[1].parent.vrfStatus is search('DEPLOYED')" + retries: 20 + delay: 3 + +- name: TEST.5c - MSD REPLACED - [debug] print result_5b + ansible.builtin.debug: + var: result_5b + +- assert: + that: + - result_5b.parent_fabric.response | length == 2 + - result_5b.parent_fabric.response[0].parent.vrfName == "ansible-msd-replace-1" + - result_5b.parent_fabric.response[0].parent.vrfId == 9008021 + - result_5b.parent_fabric.response[0].parent.vrfStatus == "DEPLOYED" + - result_5b.parent_fabric.response[1].parent.vrfName == "ansible-msd-replace-2" + - result_5b.parent_fabric.response[1].parent.vrfId == 9008022 + - result_5b.parent_fabric.response[1].parent.vrfStatus == "DEPLOYED" + - (result_5b.parent_fabric.response[0].parent.vrfTemplateConfig | from_json).mtu == "9000" + - result_5b.child_fabrics | length == 1 + - result_5b.child_fabrics[0].response | length == 2 + - result_5b.child_fabrics[0].response[0].parent.vrfName == "ansible-msd-replace-1" + - result_5b.child_fabrics[0].response[0].parent.vrfStatus == "DEPLOYED" + - result_5b.child_fabrics[0].response[1].parent.vrfName == "ansible-msd-replace-2" + - result_5b.child_fabrics[0].response[1].parent.vrfStatus == "DEPLOYED" + - (result_5b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseDefaultRouteFlag == "false" + - (result_5b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseHostRouteFlag == "true" + - result_5b.workflow == "Multisite Parent with Child Fabric Processing" + +- name: TEST.5d - MSD REPLACED - [replaced] Replace multiple VRFs - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: replaced + config: *conf5 + register: result_5d + +- name: TEST.5e - MSD REPLACED - [debug] print result_5d + ansible.builtin.debug: + var: result_5d + +- assert: + that: + - result_5d.changed == false + - result_5d.workflow == "Multisite Parent with Child Fabric Processing" + - result_5d.parent_fabric.changed == false + - result_5d.parent_fabric.diff | length == 0 + - result_5d.parent_fabric.response | length == 0 + - result_5d.child_fabrics[0].diff | length == 0 + - result_5d.child_fabrics[0].response | length == 0 + +############################################### +### CLEANUP ## +############################################### + +- name: CLEANUP.1 - MSD REPLACED - [deleted] Delete all VRFs from Parent MSD fabric + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + register: result_cleanup_1 + +- name: CLEANUP.1a - MSD REPLACED - [debug] print cleanup result + ansible.builtin.debug: + var: result_cleanup_1 + +- assert: + that: + - result_cleanup_1.changed in [true, false] + +- name: CLEANUP.2 - MSD REPLACED - [wait_for] Wait 60 seconds for cleanup to complete + wait_for: + timeout: 60 + when: result_cleanup_1.changed == true \ No newline at end of file diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/msd/sanity.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/msd/sanity.yaml new file mode 100644 index 000000000..8c4df2503 --- /dev/null +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/msd/sanity.yaml @@ -0,0 +1,823 @@ +############################################## +## REQUIRED VARS ## +############################################## +# parent_fabric +# A Parent MSD VXLAN_EVPN fabric +# +# child_fabric +# A Child MSD VXLAN_EVPN fabric associated with parent_fabric +# +# child_switch_1 +# - A switch in the child MSD fabric +# - VRF-lite capable switch +# +# child_switch_2 +# - A switch in the child MSD fabric +# - VRF-lite capable switch +# +# interface_2a +# - Ethernet interface on child_switch_2 +# - Used to test VRF LITE configuration in MSD environment +############################################## + +############################################## +## SETUP ## +############################################## + +- set_fact: + rest_path: "/rest/control/fabrics/{{ parent_fabric }}" + when: controller_version == "11" + +- set_fact: + rest_path: "/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/{{ parent_fabric }}" + when: controller_version >= "12" + +- name: SETUP.0 - MSD SANITY - [with_items] print vars + ansible.builtin.debug: + var: item + with_items: + - "parent_fabric : {{ parent_fabric }}" + - "child_fabric : {{ child_fabric }}" + - "child_switch_1 : {{ child_switch_1 }}" + - "child_switch_2 : {{ child_switch_2 }}" + - "interface_2a : {{ interface_2a }}" + +- name: SETUP.1 - MSD SANITY - [dcnm_rest.GET] Verify parent fabric is deployed. + cisco.dcnm.dcnm_rest: + method: GET + path: "{{ rest_path }}" + register: result + +- assert: + that: + - result.response.DATA != None + +- name: SETUP.2 - MSD SANITY - [deleted] Clean slate - delete all VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + register: result_setup_2 + +- name: SETUP.2a - MSD SANITY - [wait_for] Wait 60 seconds for controller and switch to sync + wait_for: + timeout: 60 + when: result_setup_2.changed == true + +############################################### +### MSD SANITY TESTS ## +############################################### + +- name: TEST.1 - MSD SANITY - [merged] Basic VRF creation without Child config + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: + - vrf_name: ansible-msd-sanity-basic + vrf_id: 9008201 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2201 + vrf_description: "Basic MSD sanity test VRF" + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + register: result_1 + +- name: TEST.1a - MSD SANITY - [debug] print result_1 + ansible.builtin.debug: + var: result_1 + +- assert: + that: + - result_1.changed == true + - result_1.workflow == "Multisite Parent without Child Fabric Processing" + - result_1.diff[0].vrf_id == 9008201 + - result_1.diff[0].vrf_name == "ansible-msd-sanity-basic" + - result_1.diff[0].attach | length >= 1 + - result_1.diff[0].attach[0].ip_address == "{{ child_switch_1 }}" + - result_1.diff[0].attach[0].deploy == true + - result_1.response[0].RETURN_CODE == 200 + - result_1.response[1].RETURN_CODE == 200 + - result_1.response[2].RETURN_CODE == 200 + - (result_1.response[1].DATA|dict2items)[0].value == "SUCCESS" + +- name: TEST.1b - MSD SANITY - [query] Verify VRF state + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + register: result_1b + until: + - "result_1b.response[0].parent.vrfStatus is search('DEPLOYED')" + retries: 20 + delay: 3 + +- name: TEST.1c - MSD SANITY - [debug] print result_1b + ansible.builtin.debug: + var: result_1b + +- assert: + that: + - result_1b.response | length >= 1 + - result_1b.response[0].parent.vrfName == "ansible-msd-sanity-basic" + - result_1b.response[0].parent.vrfId == 9008201 + - (result_1b.response[0].parent.vrfTemplateConfig | from_json).vrfVlanId == "2201" + +- name: TEST.1d - MSD SANITY - [merged] Basic VRF creation - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: + - vrf_name: ansible-msd-sanity-basic + vrf_id: 9008201 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2201 + vrf_description: "Basic MSD sanity test VRF" + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + register: result_1d + +- name: TEST.1e - MSD SANITY - [debug] print result_1d + ansible.builtin.debug: + var: result_1d + +- assert: + that: + - result_1d.changed == false + - result_1d.diff | length == 0 + - result_1d.response | length == 0 + +- name: TEST.2 - MSD SANITY - [merged] VRF creation with Child config + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: &conf2 + - vrf_name: ansible-msd-sanity-child + vrf_id: 9008202 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2202 + vrf_description: "MSD sanity test VRF with Child config" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + register: result_2 + +- name: TEST.2a - MSD SANITY - [debug] print result_2 + ansible.builtin.debug: + var: result_2 + +- assert: + that: + - result_2.changed == true + - result_2.workflow == "Multisite Parent with Child Fabric Processing" + - result_2.parent_fabric.fabric == "{{ parent_fabric }}" + - result_2.parent_fabric.changed == true + - result_2.parent_fabric.diff[0].vrf_id == 9008202 + - result_2.parent_fabric.diff[0].vrf_name == "ansible-msd-sanity-child" + - result_2.parent_fabric.diff[0].attach | length >= 1 + - result_2.parent_fabric.diff[0].attach[0].ip_address == "{{ child_switch_1 }}" + - result_2.parent_fabric.diff[0].attach[0].deploy == true + - result_2.child_fabrics | length == 1 + - result_2.child_fabrics[0].fabric == "{{ child_fabric }}" + - result_2.child_fabrics[0].diff[0].vrf_name == "ansible-msd-sanity-child" + - result_2.child_fabrics[0].diff[0].adv_default_routes == true + - result_2.child_fabrics[0].diff[0].adv_host_routes == false + - result_2.child_fabrics[0].diff[0].static_default_route == true + +- name: TEST.2b - MSD SANITY - [query] Query created VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf2 + register: result_2b + until: + - "result_2b.parent_fabric.response[0].parent.vrfStatus is search('DEPLOYED')" + - "result_2b.child_fabrics[0].response[0].parent.vrfStatus is search('DEPLOYED')" + retries: 20 + delay: 3 + +- name: TEST.2c - MSD SANITY - [debug] print result_2b + ansible.builtin.debug: + var: result_2b + +- assert: + that: + - result_2b.parent_fabric.response | length >= 1 + - result_2b.parent_fabric.response[0].parent.vrfName == "ansible-msd-sanity-child" + - result_2b.parent_fabric.response[0].parent.vrfId == 9008202 + - (result_2b.parent_fabric.response[0].parent.vrfTemplateConfig | from_json).vrfVlanId == "2202" + - result_2b.child_fabrics | length == 1 + - result_2b.child_fabrics[0].response | length >= 1 + - result_2b.child_fabrics[0].response[0].parent.vrfName == "ansible-msd-sanity-child" + - result_2b.child_fabrics[0].response[0].parent.vrfId == 9008202 + - (result_2b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).vrfVlanId == "2202" + - (result_2b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseDefaultRouteFlag == "true" + - (result_2b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseHostRouteFlag == "false" + - (result_2b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).configureStaticDefaultRouteFlag == "true" + +- name: TEST.2d - MSD SANITY - [merged] VRF creation with Child config - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: *conf2 + register: result_2d + +- name: TEST.2e - MSD SANITY - [debug] print result_2d + ansible.builtin.debug: + var: result_2d + +- assert: + that: + - result_2d.changed == false + - result_2d.parent_fabric.diff | length == 0 + - result_2d.parent_fabric.response | length == 0 + - result_2d.child_fabrics | length == 1 + - result_2d.child_fabrics[0].changed == false + - result_2d.child_fabrics[0].diff | length == 0 + - result_2d.child_fabrics[0].response | length == 0 + +- name: TEST.3 - MSD SANITY - [replaced] Replace VRF configuration + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: replaced + config: &conf3 + - vrf_name: ansible-msd-sanity-basic + vrf_id: 9008201 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2206 # Changed from 2201 + vrf_description: "Updated basic MSD sanity test VRF" + vrf_int_mtu: 9000 # Changed from default + attach: + - ip_address: "{{ child_switch_1 }}" + - ip_address: "{{ child_switch_2 }}" # Added attachment + deploy: true + register: result_3 + +- name: TEST.3a - MSD SANITY - [debug] print result_3 + ansible.builtin.debug: + var: result_3 + +- assert: + that: + - result_3.changed == true + - result_3.workflow == "Multisite Parent without Child Fabric Processing" + - result_3.diff[0].vrf_id == 9008201 + - result_3.diff[0].vrf_name == "ansible-msd-sanity-basic" + - result_3.diff[0].vrf_description == "Updated basic MSD sanity test VRF" + - result_3.diff[0].vrf_int_mtu == 9000 + - result_3.diff[0].attach | length == 1 + - result_3.diff[0].attach[0].ip_address == "{{ child_switch_2 }}" + - result_3.response[0].RETURN_CODE == 200 + +- name: TEST.3b - MSD SANITY - [query] Query created VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf3 + register: result_3b + until: + - "result_3b.response[0].parent.vrfStatus is search('DEPLOYED')" + retries: 20 + delay: 3 + +- name: TEST.3c - MSD SANITY - [debug] print result_3b + ansible.builtin.debug: + var: result_3b + +- assert: + that: + - result_3b.response | length >= 1 + - result_3b.response[0].parent.vrfName == "ansible-msd-sanity-basic" + - result_3b.response[0].parent.vrfId == 9008201 + - (result_3b.response[0].parent.vrfTemplateConfig | from_json).vrfVlanId == "2206" + - (result_3b.response[0].parent.vrfTemplateConfig | from_json).mtu == "9000" + +- name: TEST.3d - MSD SANITY - [replaced] Replace VRF configuration - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: replaced + config: *conf3 + register: result_3d + +- name: TEST.3e - MSD SANITY - [debug] print result_3d + ansible.builtin.debug: + var: result_3d + +- assert: + that: + - result_3d.changed == false + - result_3d.diff | length == 0 + - result_3d.response | length == 0 + +- name: TEST.4 - MSD SANITY - [replaced] Replace VRF with Child config + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: replaced + config: &conf4 + - vrf_name: ansible-msd-sanity-child + vrf_id: 9008202 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2209 + vrf_description: "Updated MSD sanity test VRF with Child config" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: false # Changed + adv_host_routes: true # Changed + static_default_route: false # Changed + # l3vni_wo_vlan: true # Added + attach: + - ip_address: "{{ child_switch_1 }}" + - ip_address: "{{ child_switch_2 }}" # Added attachment + deploy: true + register: result_4 + +- name: TEST.4a - MSD SANITY - [debug] print result_4 + ansible.builtin.debug: + var: result_4 + +- assert: + that: + - result_4.changed == true + - result_4.workflow == "Multisite Parent with Child Fabric Processing" + - result_4.parent_fabric.fabric == "{{ parent_fabric }}" + - result_4.parent_fabric.changed == true + - result_4.parent_fabric.diff[0].vrf_id == 9008202 + - result_4.parent_fabric.diff[0].vrf_name == "ansible-msd-sanity-child" + - result_4.parent_fabric.diff[0].vrf_description == "Updated MSD sanity test VRF with Child config" + - result_4.parent_fabric.diff[0].attach | length == 1 + - result_4.parent_fabric.diff[0].attach[0].ip_address == "{{ child_switch_2 }}" + - result_4.child_fabrics | length == 1 + - result_4.child_fabrics[0].fabric == "{{ child_fabric }}" + - result_4.child_fabrics[0].diff[0].vrf_name == "ansible-msd-sanity-child" + - result_4.child_fabrics[0].diff[0].adv_default_routes == false + - result_4.child_fabrics[0].diff[0].adv_host_routes == true + - result_4.child_fabrics[0].diff[0].static_default_route == false + # - result_4.child_fabrics[0].diff[0].l3vni_wo_vlan == true + +- name: TEST.4b - MSD SANITY - [query] Query created VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf4 + register: result_4b + until: + - "result_4b.parent_fabric.response[0].parent.vrfStatus is search('DEPLOYED')" + - "result_4b.child_fabrics[0].response[0].parent.vrfStatus is search('DEPLOYED')" + retries: 20 + delay: 3 + +- name: TEST.4c - MSD SANITY - [debug] print result_4b + ansible.builtin.debug: + var: result_4b + +- assert: + that: + - result_4b.parent_fabric.response | length >= 1 + - result_4b.parent_fabric.response[0].parent.vrfName == "ansible-msd-sanity-child" + - result_4b.parent_fabric.response[0].parent.vrfId == 9008202 + - result_4b.child_fabrics | length == 1 + - result_4b.child_fabrics[0].response | length >= 1 + - result_4b.child_fabrics[0].response[0].parent.vrfName == "ansible-msd-sanity-child" + - result_4b.child_fabrics[0].response[0].parent.vrfId == 9008202 + - (result_4b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseDefaultRouteFlag == "false" + - (result_4b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).advertiseHostRouteFlag == "true" + - (result_4b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).configureStaticDefaultRouteFlag == "false" + # - (result_4b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).enableL3VniNoVlan == "true" + +- name: TEST.4d - MSD SANITY - [replaced] Replace VRF with Child config - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: replaced + config: *conf4 + register: result_4d + +- name: TEST.4e - MSD SANITY - [debug] print result_4d + ansible.builtin.debug: + var: result_4d + +- assert: + that: + - result_4d.changed == false + - result_4d.parent_fabric.diff | length == 0 + - result_4d.parent_fabric.response | length == 0 + - result_4d.child_fabrics | length == 1 + - result_4d.child_fabrics[0].changed == false + - result_4d.child_fabrics[0].diff | length == 0 + - result_4d.child_fabrics[0].response | length == 0 + +- name: TEST.5 - MSD SANITY - [merged] Create VRF with VRF-Lite + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: &conf5 + - vrf_name: ansible-msd-sanity-lite + vrf_id: 9008203 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2203 + vrf_description: "MSD sanity test VRF with VRF-Lite" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + attach: + - ip_address: "{{ child_switch_2 }}" + vrf_lite: + - interface: "{{ interface_2a }}" + ipv4_addr: 10.10.1.2/30 + neighbor_ipv4: 10.10.1.1 + peer_vrf: external_sanity_vrf + dot1q: 600 + deploy: true + register: result_5 + +- name: TEST.5a - MSD SANITY - [debug] print result_5 + ansible.builtin.debug: + var: result_5 + +- assert: + that: + - result_5.changed == true + - result_5.workflow == "Multisite Parent with Child Fabric Processing" + - result_5.parent_fabric.fabric == "{{ parent_fabric }}" + - result_5.parent_fabric.changed == true + - result_5.parent_fabric.diff[0].vrf_id == 9008203 + - result_5.parent_fabric.diff[0].vrf_name == "ansible-msd-sanity-lite" + - result_5.parent_fabric.diff[0].attach | length == 1 + - result_5.parent_fabric.diff[0].attach[0].ip_address == "{{ child_switch_2 }}" + - result_5.child_fabrics | length == 1 + - result_5.child_fabrics[0].fabric == "{{ child_fabric }}" + - result_5.child_fabrics[0].changed == true + - result_5.child_fabrics[0].diff[0].vrf_name == "ansible-msd-sanity-lite" + +- name: TEST.5b - MSD SANITY - [query] Query created VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf5 + register: result_5b + until: + - "result_5b.parent_fabric.response[0].parent.vrfStatus is search('DEPLOYED')" + - "result_5b.child_fabrics[0].response[0].parent.vrfStatus is search('DEPLOYED')" + retries: 20 + delay: 3 + +- name: TEST.5c - MSD SANITY - [debug] print result_5b + ansible.builtin.debug: + var: result_5b + +- assert: + that: + - result_5b.parent_fabric.response | length >= 1 + - result_5b.parent_fabric.response[0].parent.vrfName == "ansible-msd-sanity-lite" + - result_5b.parent_fabric.response[0].parent.vrfId == 9008203 + - (result_5b.parent_fabric.response[0].parent.vrfTemplateConfig | from_json).vrfVlanId == "2203" + - result_5b.parent_fabric.response[0].attach | length >= 1 + - result_5b.child_fabrics | length == 1 + - result_5b.child_fabrics[0].response | length >= 1 + - result_5b.child_fabrics[0].response[0].parent.vrfName == "ansible-msd-sanity-lite" + - result_5b.child_fabrics[0].response[0].parent.vrfId == 9008203 + - (result_5b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).vrfVlanId == "2203" + +- name: TEST.5d - MSD SANITY - [merged] Create VRF with VRF-Lite - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: *conf5 + register: result_5d + +- name: TEST.5e - MSD SANITY - [debug] print result_5d + ansible.builtin.debug: + var: result_5d + +- assert: + that: + - result_5d.changed == false + - result_5d.parent_fabric.diff | length == 0 + - result_5d.parent_fabric.response | length == 0 + - result_5d.child_fabrics | length == 1 + - result_5d.child_fabrics[0].changed == false + - result_5d.child_fabrics[0].diff | length == 0 + - result_5d.child_fabrics[0].response | length == 0 + +- name: TEST.6 - MSD SANITY - [overridden] Override all VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: overridden + config: &conf6 + - vrf_name: ansible-msd-sanity-override + vrf_id: 9008210 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2210 + vrf_description: "MSD sanity override test VRF" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + bgp_password: "SANITYPASS123" + bgp_passwd_encrypt: 7 + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + register: result_6 + +- name: TEST.6a - MSD SANITY - [debug] print result_6 + ansible.builtin.debug: + var: result_6 + +- assert: + that: + - result_6.changed == true + - result_6.workflow == "Multisite Parent with Child Fabric Processing" + - result_6.parent_fabric.fabric == "{{ parent_fabric }}" + - result_6.parent_fabric.changed == true + - result_6.parent_fabric.diff[0].vrf_id == 9008210 + - result_6.parent_fabric.diff[0].vrf_name == "ansible-msd-sanity-override" + - result_6.parent_fabric.diff[0].attach | length == 1 + - result_6.parent_fabric.diff[0].attach[0].ip_address == "{{ child_switch_1 }}" + - result_6.child_fabrics | length == 1 + - result_6.child_fabrics[0].fabric == "{{ child_fabric }}" + - result_6.child_fabrics[0].diff[0].vrf_name == "ansible-msd-sanity-override" + - result_6.child_fabrics[0].diff[0].adv_default_routes == true + - result_6.child_fabrics[0].diff[0].adv_host_routes == false + - result_6.child_fabrics[0].diff[0].static_default_route == true + - result_6.child_fabrics[0].diff[0].bgp_password == "SANITYPASS123" + - result_6.child_fabrics[0].diff[0].bgp_passwd_encrypt == 7 + +- name: TEST.6b - MSD SANITY - [query] Verify override worked + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf6 + register: result_6b + until: + - "result_6b.parent_fabric.response[0].parent.vrfStatus is search('DEPLOYED')" + - "result_6b.child_fabrics[0].response[0].parent.vrfStatus is search('DEPLOYED')" + retries: 20 + delay: 3 + +- name: TEST.6c - MSD SANITY - [debug] print result_6b + ansible.builtin.debug: + var: result_6b + +- assert: + that: + - result_6b.parent_fabric.response | length >= 1 + - result_6b.parent_fabric.response[0].parent.vrfName == "ansible-msd-sanity-override" + - result_6b.parent_fabric.response[0].parent.vrfId == 9008210 + - (result_6b.parent_fabric.response[0].parent.vrfTemplateConfig | from_json).vrfVlanId == "2210" + - result_6b.child_fabrics | length == 1 + - result_6b.child_fabrics[0].response | length >= 1 + - result_6b.child_fabrics[0].response[0].parent.vrfName == "ansible-msd-sanity-override" + - result_6b.child_fabrics[0].response[0].parent.vrfId == 9008210 + - (result_6b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).vrfVlanId == "2210" + - (result_6b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).bgpPassword != "" + - (result_6b.child_fabrics[0].response[0].parent.vrfTemplateConfig | from_json).bgpPasswordKeyType == "7" + +- name: TEST.6d - MSD SANITY - [overridden] Override all VRFs - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: overridden + config: *conf6 + register: result_6d + +- name: TEST.6e - MSD SANITY - [debug] print result_6d + ansible.builtin.debug: + var: result_6d + +- assert: + that: + - result_6d.changed == false + - result_6d.parent_fabric.diff | length == 0 + - result_6d.parent_fabric.response | length == 0 + - result_6d.child_fabrics | length == 1 + - result_6d.child_fabrics[0].diff | length == 0 + - result_6d.child_fabrics[0].response | length == 0 + +- name: TEST.7 - MSD SANITY - [deleted] Delete specific VRF + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + config: &conf7 + - vrf_name: ansible-msd-sanity-override + register: result_7 + +- name: TEST.7a - MSD SANITY - [debug] print result_7 + ansible.builtin.debug: + var: result_7 + +- assert: + that: + - result_7.changed == true + - result_7.workflow == "Multisite Parent without Child Fabric Processing" + - result_7.diff[0].attach[0].ip_address == "{{ child_switch_1 }}" + - result_7.diff[0].attach[0].deploy == false + - result_7.diff[0].vrf_name == "ansible-msd-sanity-override" + - result_7.response[0].RETURN_CODE == 200 + - result_7.response[2].RETURN_CODE == 200 + +- name: TEST.7b - MSD SANITY - [query] Verify deletion worked + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf7 + register: result_7b + +- name: TEST.7c - MSD SANITY - [debug] print result_7b + ansible.builtin.debug: + var: result_7b + +- assert: + that: + - result_7b.workflow == "Multisite Parent without Child Fabric Processing" + - result_7b.diff | length == 0 + - result_7b.response | length == 0 + +- name: TEST.7d - MSD SANITY - [deleted] Delete specific VRF - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + config: *conf7 + register: result_7d + +- name: TEST.7e - MSD SANITY - [debug] print result_7d + ansible.builtin.debug: + var: result_7d + +- assert: + that: + - result_7d.workflow == "Multisite Parent without Child Fabric Processing" + - result_7d.changed == false + - result_7d.diff | length == 0 + - result_7d.response | length == 0 + +- name: TEST.8 - MSD SANITY - [merged] Create multiple VRFs for bulk delete test + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: &conf8 + - vrf_name: ansible-msd-bulk-1 + vrf_id: 9008221 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2221 + vrf_description: "Bulk delete test VRF 1" + attach: + - ip_address: "{{ child_switch_1 }}" + deploy: true + - vrf_name: ansible-msd-bulk-2 + vrf_id: 9008222 + vrf_template: Default_VRF_Universal + vrf_extension_template: Default_VRF_Extension_Universal + vlan_id: 2222 + vrf_description: "Bulk delete test VRF 2" + child_fabric_config: + - fabric: "{{ child_fabric }}" + adv_default_routes: true + adv_host_routes: false + static_default_route: true + attach: + - ip_address: "{{ child_switch_2 }}" + deploy: true + register: result_8 + +- name: TEST.8a - MSD SANITY - [debug] print result_8 + ansible.builtin.debug: + var: result_8 + +- assert: + that: + - result_8.changed == true + - result_8.workflow == "Multisite Parent with Child Fabric Processing" + - result_8.parent_fabric.fabric == "{{ parent_fabric }}" + - result_8.parent_fabric.changed == true + - result_8.parent_fabric.diff | length == 2 + - result_8.parent_fabric.diff[0].vrf_name == "ansible-msd-bulk-1" + - result_8.parent_fabric.diff[0].vrf_id == 9008221 + - result_8.parent_fabric.diff[1].vrf_name == "ansible-msd-bulk-2" + - result_8.parent_fabric.diff[1].vrf_id == 9008222 + - result_8.child_fabrics | length == 1 + - result_8.child_fabrics[0].fabric == "{{ child_fabric }}" + - result_8.child_fabrics[0].diff[0].vrf_name == "ansible-msd-bulk-2" + +- name: TEST.8b - MSD SANITY - [query] Verify VRFs created + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: query + config: *conf8 + register: result_8b + until: + - "result_8b.parent_fabric.response | length == 2" + - "result_8b.parent_fabric.response | selectattr('parent.vrfStatus', 'search', 'DEPLOYED') | list | length == 2" + - "result_8b.child_fabrics[0].response | length >= 1" + - "result_8b.child_fabrics[0].response[0].parent.vrfStatus is search('DEPLOYED')" + retries: 20 + delay: 3 + +- name: TEST.8c - MSD SANITY - [debug] print result_8b + ansible.builtin.debug: + var: result_8b + +- assert: + that: + - result_8b.parent_fabric.response | length == 2 + - result_8b.parent_fabric.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-bulk-1') | list | length == 1 + - result_8b.parent_fabric.response | selectattr('parent.vrfName', 'equalto', 'ansible-msd-bulk-2') | list | length == 1 + - result_8b.parent_fabric.response | selectattr('parent.vrfId', 'equalto', 9008221) | list | length == 1 + - result_8b.parent_fabric.response | selectattr('parent.vrfId', 'equalto', 9008222) | list | length == 1 + - result_8b.child_fabrics | length == 1 + - result_8b.child_fabrics[0].response | length >= 1 + - result_8b.child_fabrics[0].response[0].parent.vrfName == "ansible-msd-bulk-2" + - result_8b.child_fabrics[0].response[0].parent.vrfId == 9008222 + +- name: TEST.8d - MSD SANITY - [merged] Create multiple VRFs for bulk delete test - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: merged + config: *conf8 + register: result_8d + +- name: TEST.8e - MSD SANITY - [debug] print result_8d + ansible.builtin.debug: + var: result_8d + +- assert: + that: + - result_8d.changed == false + - result_8d.parent_fabric.diff | length == 0 + - result_8d.parent_fabric.response | length == 0 + - result_8d.child_fabrics | length == 1 + - result_8d.child_fabrics[0].diff | length == 0 + - result_8d.child_fabrics[0].response | length == 0 + +- name: TEST.9 - MSD SANITY - [deleted] Clean up all test VRFs + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + register: result_9 + +- name: TEST.9a - MSD SANITY - [debug] print cleanup result + ansible.builtin.debug: + var: result_9 + +- assert: + that: + - result_9.changed == true + - result_9.workflow == "Multisite Parent without Child Fabric Processing" + - result_9.diff[0].attach[0].ip_address == "{{ child_switch_1 }}" + - result_9.diff[0].attach[0].deploy == false + - result_9.diff[0].vrf_name == "ansible-msd-bulk-1" + - result_9.diff[1].attach[0].ip_address == "{{ child_switch_2 }}" + - result_9.diff[1].attach[0].deploy == false + - result_9.diff[1].vrf_name == "ansible-msd-bulk-2" + - result_9.response[0].RETURN_CODE == 200 + - result_9.response[2].RETURN_CODE == 200 + - result_9.response[3].RETURN_CODE == 200 + +- name: TEST.9b - MSD SANITY - [deleted] Clean all VRFs - Idempotence + cisco.dcnm.dcnm_vrf: + fabric: "{{ parent_fabric }}" + state: deleted + register: result_9b + +- name: TEST.9c - MSD SANITY - [debug] print result_9b + ansible.builtin.debug: + var: result_9b + +- assert: + that: + - result_9b.workflow == "Multisite Parent without Child Fabric Processing" + - result_9b.changed == false + - result_9b.diff | length == 0 + - result_9b.response | length == 0 + +############################################### +### FINAL NOTES ## +############################################### + +- name: FINAL - MSD SANITY - Summary of tests completed + ansible.builtin.debug: + msg: + - "MSD Sanity tests completed successfully!" + - "Tests covered:" + - " 1. Basic VRF creation (Parent-only)" + - " 2. VRF creation with Child fabric config" + - " 3. Query operations" + - " 4. Replace operations (with and without Child config)" + - " 5. VRF-Lite configuration" + - " 6. Override operations" + - " 7. Delete operations (specific and bulk)" + - " 8. Idempotence verification" + - "All MSD fabric workflows validated successfully!" \ No newline at end of file diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/deleted.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/deleted.yaml similarity index 100% rename from tests/integration/targets/dcnm_vrf/tests/dcnm/deleted.yaml rename to tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/deleted.yaml diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/merged.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/merged.yaml similarity index 99% rename from tests/integration/targets/dcnm_vrf/tests/dcnm/merged.yaml rename to tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/merged.yaml index e5f1033f1..692ef3085 100644 --- a/tests/integration/targets/dcnm_vrf/tests/dcnm/merged.yaml +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/merged.yaml @@ -284,7 +284,7 @@ vrf_id: 9008011 vrf_template: Default_VRF_Universal vrf_extension_template: Default_VRF_Extension_Universal - vlan_id: 500 + vlan_id: 501 attach: - ip_address: "{{ switch_1 }}" - ip_address: "{{ switch_2 }}" diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/overridden.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/overridden.yaml similarity index 96% rename from tests/integration/targets/dcnm_vrf/tests/dcnm/overridden.yaml rename to tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/overridden.yaml index fa7b2a448..d54a88743 100644 --- a/tests/integration/targets/dcnm_vrf/tests/dcnm/overridden.yaml +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/overridden.yaml @@ -154,10 +154,11 @@ - 'result_1.response[3].RETURN_CODE == 200' - 'result_1.response[4].RETURN_CODE == 200' - 'result_1.response[5].RETURN_CODE == 200' + - 'result_1.response[6].RETURN_CODE == 200' - '(result_1.response[0].DATA|dict2items)[0].value == "SUCCESS"' - '(result_1.response[0].DATA|dict2items)[1].value == "SUCCESS"' - - '(result_1.response[5].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_1.response[5].DATA|dict2items)[1].value == "SUCCESS"' + - '(result_1.response[6].DATA|dict2items)[0].value == "SUCCESS"' + - '(result_1.response[6].DATA|dict2items)[1].value == "SUCCESS"' - 'result_1.diff[0].vrf_name == "ansible-vrf-int2"' - 'result_1.diff[1].attach[0].deploy == false' - 'result_1.diff[1].attach[1].deploy == false' @@ -268,10 +269,11 @@ - 'result_2c.response[4].RETURN_CODE == 200' - 'result_2c.response[5].RETURN_CODE == 200' - 'result_2c.response[6].RETURN_CODE == 200' + - 'result_2c.response[7].RETURN_CODE == 200' - '(result_2c.response[0].DATA|dict2items)[0].value == "SUCCESS"' - '(result_2c.response[0].DATA|dict2items)[1].value == "SUCCESS"' - - '(result_2c.response[5].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_2c.response[5].DATA|dict2items)[1].value == "SUCCESS"' + - '(result_2c.response[6].DATA|dict2items)[0].value == "SUCCESS"' + - '(result_2c.response[6].DATA|dict2items)[1].value == "SUCCESS"' - 'result_2c.diff[0].attach[0].deploy == true' - 'result_2c.diff[0].attach[1].deploy == true' - 'result_2c.diff[0].vrf_name == "ansible-vrf-int2"' @@ -345,11 +347,8 @@ - 'result_3.response[1].RETURN_CODE == 200' - 'result_3.response[2].RETURN_CODE == 200' - '(result_3.response[1].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_3.response[1].DATA|dict2items)[1].value == "SUCCESS"' - 'result_3.diff[0].attach[0].deploy == true' - - 'result_3.diff[0].attach[1].deploy == true' - '"{{ switch_1 }}" in result_3.diff[0].attach[0].ip_address' - - '"{{ switch_2 }}" in result_3.diff[0].attach[1].ip_address' - 'result_3.diff[0].vrf_name == "ansible-vrf-int2"' - name: TEST.4 - OVERRIDDEN - [overridden] Override vrf_lite extension with new dot1q value @@ -495,10 +494,11 @@ - 'result_5.response[4].RETURN_CODE == 200' - 'result_5.response[5].RETURN_CODE == 200' - 'result_5.response[6].RETURN_CODE == 200' + - 'result_5.response[7].RETURN_CODE == 200' - '(result_5.response[0].DATA|dict2items)[0].value == "SUCCESS"' - '(result_5.response[0].DATA|dict2items)[1].value == "SUCCESS"' - - '(result_5.response[5].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_5.response[5].DATA|dict2items)[1].value == "SUCCESS"' + - '(result_5.response[6].DATA|dict2items)[0].value == "SUCCESS"' + - '(result_5.response[6].DATA|dict2items)[1].value == "SUCCESS"' - 'result_5.response[2].METHOD == "DELETE"' - 'result_5.diff[0].attach[0].deploy == true' - 'result_5.diff[0].attach[1].deploy == true' diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/query.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/query.yaml similarity index 100% rename from tests/integration/targets/dcnm_vrf/tests/dcnm/query.yaml rename to tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/query.yaml diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/replaced.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/replaced.yaml similarity index 100% rename from tests/integration/targets/dcnm_vrf/tests/dcnm/replaced.yaml rename to tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/replaced.yaml diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/sanity.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/sanity.yaml similarity index 99% rename from tests/integration/targets/dcnm_vrf/tests/dcnm/sanity.yaml rename to tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/sanity.yaml index 52aa69b34..40b28bebc 100644 --- a/tests/integration/targets/dcnm_vrf/tests/dcnm/sanity.yaml +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/sanity.yaml @@ -490,10 +490,11 @@ - 'result_3c.response[3].RETURN_CODE == 200' - 'result_3c.response[4].RETURN_CODE == 200' - 'result_3c.response[5].RETURN_CODE == 200' + - 'result_3c.response[6].RETURN_CODE == 200' - '(result_3c.response[0].DATA|dict2items)[0].value == "SUCCESS"' - '(result_3c.response[0].DATA|dict2items)[1].value == "SUCCESS"' - - '(result_3c.response[5].DATA|dict2items)[0].value == "SUCCESS"' - - '(result_3c.response[5].DATA|dict2items)[1].value == "SUCCESS"' + - '(result_3c.response[6].DATA|dict2items)[0].value == "SUCCESS"' + - '(result_3c.response[6].DATA|dict2items)[1].value == "SUCCESS"' - 'result_3c.diff[0].attach[0].deploy == true' - 'result_3c.diff[0].attach[1].deploy == true' - 'result_3c.diff[0].vrf_name == "ansible-vrf-int2"' diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/self-contained-tests/deleted_vrf_all.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/self-contained-tests/deleted_vrf_all.yaml similarity index 100% rename from tests/integration/targets/dcnm_vrf/tests/dcnm/self-contained-tests/deleted_vrf_all.yaml rename to tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/self-contained-tests/deleted_vrf_all.yaml diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/self-contained-tests/merged_vrf_all.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/self-contained-tests/merged_vrf_all.yaml similarity index 100% rename from tests/integration/targets/dcnm_vrf/tests/dcnm/self-contained-tests/merged_vrf_all.yaml rename to tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/self-contained-tests/merged_vrf_all.yaml diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/self-contained-tests/overridden_vrf_all.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/self-contained-tests/overridden_vrf_all.yaml similarity index 79% rename from tests/integration/targets/dcnm_vrf/tests/dcnm/self-contained-tests/overridden_vrf_all.yaml rename to tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/self-contained-tests/overridden_vrf_all.yaml index 11b543b57..17bf1b6af 100644 --- a/tests/integration/targets/dcnm_vrf/tests/dcnm/self-contained-tests/overridden_vrf_all.yaml +++ b/tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/self-contained-tests/overridden_vrf_all.yaml @@ -153,29 +153,30 @@ retries: 30 delay: 2 -# - assert: -# that: -# - 'result.changed == true' -# - 'result.response[0].RETURN_CODE == 200' -# - 'result.response[1].RETURN_CODE == 200' -# - 'result.response[2].RETURN_CODE == 200' -# - 'result.response[3].RETURN_CODE == 200' -# - 'result.response[4].RETURN_CODE == 200' -# - 'result.response[5].RETURN_CODE == 200' -# - '(result.response[0].DATA|dict2items)[0].value == "SUCCESS"' -# - '(result.response[0].DATA|dict2items)[1].value == "SUCCESS"' -# - '(result.response[4].DATA|dict2items)[0].value == "SUCCESS"' -# - '(result.response[4].DATA|dict2items)[1].value == "SUCCESS"' -# - 'result.diff[0].attach[0].deploy == true' -# - 'result.diff[0].attach[1].deploy == true' -# - 'result.diff[1].attach[0].deploy == false' -# - 'result.diff[1].attach[1].deploy == false' -# - '"{{ switch_1 }}" or "{{ switch_2 }}" in result.diff[0].attach[0].ip_address' -# - '"{{ switch_2 }}" or "{{ switch_1 }}" in result.diff[0].attach[1].ip_address' -# - '"{{ switch_1 }}" or "{{ switch_2 }}" in result.diff[1].attach[0].ip_address' -# - '"{{ switch_2 }}" or "{{ switch_1 }}" in result.diff[1].attach[1].ip_address' -# - 'result.diff[0].vrf_name == "ansible-vrf-int2"' -# - 'result.diff[1].vrf_name == "ansible-vrf-int1"' +- assert: + that: + - 'result.changed == true' + - 'result.response[0].RETURN_CODE == 200' + - 'result.response[1].RETURN_CODE == 200' + - 'result.response[2].RETURN_CODE == 200' + - 'result.response[3].RETURN_CODE == 200' + - 'result.response[4].RETURN_CODE == 200' + - 'result.response[5].RETURN_CODE == 200' + - 'result.response[6].RETURN_CODE == 200' + - '(result.response[0].DATA|dict2items)[0].value == "SUCCESS"' + - '(result.response[0].DATA|dict2items)[1].value == "SUCCESS"' + - '(result.response[5].DATA|dict2items)[0].value == "SUCCESS"' + - '(result.response[5].DATA|dict2items)[1].value == "SUCCESS"' + - 'result.diff[0].attach[0].deploy == true' + - 'result.diff[0].attach[1].deploy == true' + - 'result.diff[1].attach[0].deploy == false' + - 'result.diff[1].attach[1].deploy == false' + - '"{{ switch_1 }}" or "{{ switch_2 }}" in result.diff[0].attach[0].ip_address' + - '"{{ switch_2 }}" or "{{ switch_1 }}" in result.diff[0].attach[1].ip_address' + - '"{{ switch_1 }}" or "{{ switch_2 }}" in result.diff[1].attach[0].ip_address' + - '"{{ switch_2 }}" or "{{ switch_1 }}" in result.diff[1].attach[1].ip_address' + - 'result.diff[0].vrf_name == "ansible-vrf-int2"' + - 'result.diff[1].vrf_name == "ansible-vrf-int1"' - name: OVERRIDDEN_ALL - conf1 - Idempotence cisco.dcnm.dcnm_vrf: *conf1 diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/self-contained-tests/replaced_vrf_all.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/self-contained-tests/replaced_vrf_all.yaml similarity index 100% rename from tests/integration/targets/dcnm_vrf/tests/dcnm/self-contained-tests/replaced_vrf_all.yaml rename to tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/self-contained-tests/replaced_vrf_all.yaml diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/self-contained-tests/scale.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/self-contained-tests/scale.yaml similarity index 100% rename from tests/integration/targets/dcnm_vrf/tests/dcnm/self-contained-tests/scale.yaml rename to tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/self-contained-tests/scale.yaml diff --git a/tests/integration/targets/dcnm_vrf/tests/dcnm/self-contained-tests/vrf_lite.yaml b/tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/self-contained-tests/vrf_lite.yaml similarity index 100% rename from tests/integration/targets/dcnm_vrf/tests/dcnm/self-contained-tests/vrf_lite.yaml rename to tests/integration/targets/dcnm_vrf/tests/dcnm/standalone/self-contained-tests/vrf_lite.yaml diff --git a/tests/unit/modules/dcnm/dcnm_module.py b/tests/unit/modules/dcnm/dcnm_module.py index ac2624477..eb0c7fa5d 100644 --- a/tests/unit/modules/dcnm/dcnm_module.py +++ b/tests/unit/modules/dcnm/dcnm_module.py @@ -31,6 +31,9 @@ def set_module_args(args): + """Store module args for potential action plugin use.""" + if hasattr(TestDcnmModule, '_last_module_args'): + TestDcnmModule._last_module_args = args return _set_module_args(args) @@ -73,6 +76,9 @@ def load_fixture(module_name, name, device=""): class TestDcnmModule(ModuleTestCase): + # Class variable to store last module args for action plugin execution + _last_module_args = None + def execute_module_devices( self, failed=False, changed=False, response=None, sort=True, defaults=False ): @@ -96,17 +102,20 @@ def execute_module_devices( return retvals def execute_module( - self, failed=False, changed=False, response=None, sort=True, device="" + self, failed=False, changed=False, response=None, sort=True, device="", use_action_plugin=False ): self.load_fixtures(response, device=device) - if failed: - result = self.failed() - self.assertTrue(result["failed"], result) + if use_action_plugin: + result = self._execute_via_action_plugin(failed, changed) else: - result = self.changed(changed) - self.assertEqual(result["changed"], changed, result) + if failed: + result = self.failed() + self.assertTrue(result["failed"], result) + else: + result = self.changed(changed) + self.assertEqual(result["changed"], changed, result) if response is not None: if sort: @@ -136,3 +145,111 @@ def changed(self, changed=False): def load_fixtures(self, response=None, device=""): pass + + def _execute_via_action_plugin(self, failed=False, changed=False): + """ + Execute the module via its action plugin instead of directly. + + This method simulates Ansible's action plugin execution flow by: + 1. Importing the action plugin for the module + 2. Creating a mock ActionBase execution context + 3. Calling the action plugin's run() method + 4. The action plugin then calls the module internally + + Args: + failed (bool): Whether the execution is expected to fail + changed (bool): Whether the execution is expected to result in changes + + Returns: + dict: The result dictionary from action plugin execution + """ + from unittest.mock import Mock, patch + from ansible.playbook.task import Task + import importlib + + # Get module name from the test class's module attribute + module_name = self.module.__name__.rsplit(".", 1)[1] + + # Construct the action plugin name (should be same as module name) + action_plugin_name = f"cisco.dcnm.{module_name}" + + # Try to import the action plugin directly instead of using action_loader + try: + action_module = importlib.import_module(f"ansible_collections.cisco.dcnm.plugins.action.{module_name}") + action_plugin = action_module.ActionModule + except (ImportError, AttributeError): + action_plugin = None + + if action_plugin is None: + # If no action plugin exists, fall back to direct module execution + if failed: + result = self.failed() + self.assertTrue(result["failed"], result) + else: + result = self.changed(changed) + self.assertEqual(result["changed"], changed, result) + return result + + # Create mock objects for action plugin execution context + mock_connection = Mock() + mock_play_context = Mock() + mock_loader = Mock() + mock_templar = Mock() + mock_shared_loader_obj = Mock() + + # Create a mock task with the module arguments + mock_task = Mock(spec=Task) + mock_task.args = self._last_module_args if self._last_module_args else {} + mock_task.async_val = 0 + mock_task.action = action_plugin_name + + # Instantiate the action plugin + action = action_plugin( + task=mock_task, + connection=mock_connection, + play_context=mock_play_context, + loader=mock_loader, + templar=mock_templar, + shared_loader_obj=mock_shared_loader_obj + ) + + # Mock the _execute_module method on the action plugin to call our module directly + # This preserves the existing test mocking behavior + original_execute_module = action._execute_module + + def mock_execute_module(module_name=None, module_args=None, task_vars=None, tmp=None, **kwargs): + # Handle fabric associations API call from action plugin + if module_name == "cisco.dcnm.dcnm_rest": + # Return the mocked fabric_associations response directly + path = module_args.get('path', '') if module_args else '' + if '/fabric-associations' in path: + if hasattr(self, 'fabric_associations'): + return {'response': self.fabric_associations, 'failed': False} + elif 'vrfs' in path: + if '/vrfs' in path or 'top-down' in path: + return {'response': self.vrf_ready_data, 'failed': False} + return {'failed': True, 'msg': 'Rest Module Mocks not provided'} + + # Set the module args if provided for dcnm_network module + if module_args: + set_module_args(module_args) + + # Execute the module using the standard test flow + if failed: + return self.failed() + else: + return self.changed(changed) + + # Patch _execute_module to use our mock + with patch.object(action, '_execute_module', side_effect=mock_execute_module): + # Execute the action plugin's run method + # The action plugin will call _execute_module internally + result = action.run(tmp=None, task_vars={}) + + # Validate results + if failed: + self.assertTrue(result.get("failed", False), result) + else: + self.assertEqual(result.get("changed", False), changed, result) + + return result diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_network.json b/tests/unit/modules/dcnm/fixtures/dcnm_network.json index d5d097b03..6ecd86e24 100644 --- a/tests/unit/modules/dcnm/fixtures/dcnm_network.json +++ b/tests/unit/modules/dcnm/fixtures/dcnm_network.json @@ -8,6 +8,46 @@ "10.10.10.227": "XYZKSJHSMK4", "10.10.10.228": "XYZKSJHSMK5" }, + "fabric_associations": { + "RETURN_CODE": 200, + "METHOD": "GET", + "REQUEST_PATH": "https://mock-ndfc/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/msd/fabric-associations", + "MESSAGE": "OK", + "DATA": [ + { + "fabricId": 1, + "fabricName": "test_network", + "fabricType": "Switch_Fabric", + "fabricState": "standalone", + "fabricParent": "None", + "fabricTechnology": "VXLANFabric" + }, + { + "fabricId": 100, + "fabricName": "msd-parent", + "fabricType": "MFD", + "fabricState": "msd", + "fabricParent": "None", + "fabricTechnology": "VXLANFabric" + }, + { + "fabricId": 101, + "fabricName": "msd-child-1", + "fabricType": "Switch_Fabric", + "fabricState": "member", + "fabricParent": "msd-parent", + "fabricTechnology": "VXLANFabric" + }, + { + "fabricId": 102, + "fabricName": "msd-child-2", + "fabricType": "Switch_Fabric", + "fabricState": "member", + "fabricParent": "msd-parent", + "fabricTechnology": "VXLANFabric" + } + ] + }, "playbook_config": [ { "net_name": "test_network", @@ -632,6 +672,12 @@ "METHOD": "POST", "RETURN_CODE": 200 }, + "empty_network_list": { + "DATA": [], + "MESSAGE": "OK", + "METHOD": "GET", + "RETURN_CODE": 200 + }, "get_have_failure": { "DATA": "Invalid JSON response: Invalid Fabric: demo-fabric-123", "ERROR": "Not Found", @@ -1128,5 +1174,543 @@ "vrfStatus": "DEPLOYED" } ] + }, + "playbook_msd_config": [ + { + "net_name": "ansible-msd-net1", + "vrf_name": "Tenant-1", + "net_id": "8001", + "net_template": "Default_Network_Universal", + "net_extension_template": "Default_Network_Extension_Universal", + "vlan_id": "2101", + "gw_ip_subnet": "192.168.101.1/24", + "routing_tag": 12345, + "int_desc": "MSD Network managed by Ansible", + "mtu_l3intf": 9214, + "arp_suppress": false, + "route_target_both": false, + "is_l2only": false, + "child_fabric_config": [ + { + "fabric": "msd-child-1", + "netflow_enable": false, + "l3gw_on_border": true, + "dhcp_loopback_id": 204, + "multicast_group_address": "239.1.1.1", + "dhcp_srvr1_ip": "192.168.1.101", + "dhcp_srvr1_vrf": "management" + } + ], + "attach": [ + { + "ip_address": "192.168.10.203", + "ports": [ + "Ethernet1/15", + "Ethernet1/16" + ] + }, + { + "ip_address": "192.168.10.204", + "ports": [ + "Ethernet1/15", + "Ethernet1/16" + ] + } + ], + "deploy": true + } + ], + "mock_msd_fabric_details": { + "RETURN_CODE": 200, + "MESSAGE": "OK", + "DATA": { + "id": 100, + "fabricId": "FABRIC-100", + "fabricName": "msd-parent", + "fabricType": "MFD", + "fabricTypeFriendly": "VXLAN EVPN Multi-Site", + "fabricTechnology": "VXLANFabric", + "templateFabricType": "VXLAN EVPN Multi-Site", + "fabricTechnologyFriendly": "VXLAN EVPN", + "provisionMode": "DCNMTopDown", + "deviceType": "n9k", + "replicationMode": "IngressReplication", + "operStatus": "CRITICAL", + "templateName": "MSD_Fabric", + "nvPairs": { + "FABRIC_NAME": "msd-parent", + "BGP_RP_ASN": "", + "FABRIC_TYPE": "MFD" + }, + "vrfTemplate": "Default_VRF_Universal", + "networkTemplate": "Default_Network_Universal", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "networkExtensionTemplate": "Default_Network_Extension_Universal" + } + }, + "mock_msd_child_fabric_details": { + "RETURN_CODE": 200, + "MESSAGE": "OK", + "DATA": { + "id": 101, + "fabricId": "FABRIC-101", + "fabricName": "msd-child-1", + "fabricType": "Switch_Fabric", + "fabricTypeFriendly": "Switch Fabric", + "fabricTechnology": "VXLANFabric", + "templateFabricType": "Data Center VXLAN EVPN", + "fabricTechnologyFriendly": "VXLAN EVPN", + "provisionMode": "DCNMTopDown", + "deviceType": "n9k", + "replicationMode": "Multicast", + "operStatus": "MINOR", + "asn": "40000", + "siteId": "101", + "templateName": "Easy_Fabric", + "vrfTemplate": "Default_VRF_Universal", + "networkTemplate": "Default_Network_Universal", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "networkExtensionTemplate": "Default_Network_Extension_Universal" + } + }, + "mock_msd_ip_sn": { + "192.168.10.203": { + "ipAddress": "192.168.10.203", + "logicalName": "leaf3", + "serialNumber": "9R518K2AT3R", + "switchRole": "leaf" + }, + "192.168.10.204": { + "ipAddress": "192.168.10.204", + "logicalName": "leaf4", + "serialNumber": "915KQ8P3NS8", + "switchRole": "leaf" + } + }, + "mock_msd_vrf_object": { + "MESSAGE": "OK", + "METHOD": "GET", + "RETURN_CODE": 200, + "DATA": [ + { + "fabric": "msd-parent", + "vrfName": "Tenant-1", + "enforce": null, + "defaultSGTag": null, + "vrfTemplate": "Default_VRF_Universal", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfTemplateConfig": "{\"routeTargetExportEvpn\":\"\",\"routeTargetImport\":\"\",\"vrfVlanId\":\"501\",\"vrfDescription\":\"\",\"disableRtAuto\":\"false\",\"v6VrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"vrfSegmentId\":\"9008012\",\"maxBgpPaths\":\"1\",\"maxIbgpPaths\":\"2\",\"routeTargetExport\":\"\",\"ipv6LinkLocalFlag\":\"true\",\"mtu\":\"9216\",\"vrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"vrfVlanName\":\"\",\"tag\":\"12345\",\"nveId\":\"1\",\"vrfIntfDescription\":\"\",\"vrfName\":\"Tenant-1\",\"routeTargetImportEvpn\":\"\"}", + "tenantName": null, + "id": 19932, + "vrfId": 9008012, + "serviceVrfTemplate": null, + "source": null, + "vrfStatus": "DEPLOYED", + "hierarchicalKey": "msd-parent" + } + ] + }, + "mock_msd_net_create_response": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": { + "Network Id": 8001, + "Network Name": "ansible-msd-net1" + } + }, + "mock_msd_net_attach_response": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": { + "ansible-msd-net1-[9R518K2AT3R/leaf3]": "SUCCESS", + "ansible-msd-net1-[915KQ8P3NS8/leaf4]": "SUCCESS" + } + }, + "mock_msd_child_net_object": { + "MESSAGE": "OK", + "METHOD": "GET", + "RETURN_CODE": 200, + "DATA": [ + { + "fabric": "msd-child-1", + "networkName": "ansible-msd-net1", + "displayName": "ansible-msd-net1", + "networkId": 8001, + "networkTemplate": "Default_Network_Universal", + "networkExtensionTemplate": "Default_Network_Extension_Universal", + "networkTemplateConfig": "{\"vlanId\":\"2101\",\"gatewayIpAddress\":\"192.168.101.1/24\",\"isLayer2Only\":false,\"tag\":\"12345\",\"vlanName\":\"\",\"intfDescription\":\"MSD Network managed by Ansible\",\"mtu\":\"9214\",\"suppressArp\":false,\"dhcpServerAddr1\":\"192.168.1.101\",\"dhcpServerAddr2\":\"\",\"dhcpServerAddr3\":\"\",\"vrfDhcp\":\"management\",\"vrfDhcp2\":\"\",\"vrfDhcp3\":\"\",\"loopbackId\":\"204\",\"mcastGroup\":\"239.1.1.1\",\"gatewayIpV6Address\":\"\",\"secondaryGW1\":\"\",\"secondaryGW2\":\"\",\"secondaryGW3\":\"\",\"secondaryGW4\":\"\",\"trmEnabled\":false,\"rtBothAuto\":false,\"enableL3OnBorder\":true,\"ENABLE_NETFLOW\":false,\"SVI_NETFLOW_MONITOR\":\"\",\"VLAN_NETFLOW_MONITOR\":\"monitor1\"}", + "vrf": "Tenant-1", + "tenantName": null, + "serviceNetworkTemplate": null, + "source": null, + "interfaceGroups": null, + "networkStatus": "DEPLOYED" + } + ] + }, + "mock_msd_child_net_attach_object": { + "MESSAGE": "OK", + "METHOD": "GET", + "RETURN_CODE": 200, + "DATA": [ + { + "networkName": "ansible-msd-net1", + "lanAttachList": [ + { + "networkName": "ansible-msd-net1", + "fabricName": "msd-child-1", + "serialNumber": "9R518K2AT3R", + "switchPorts": "Ethernet1/15,Ethernet1/16", + "ipAddress": "10.10.10.217", + "lanAttachState": "OUT-OF-SYNC", + "isLanAttached": true, + "deployment": false + }, + { + "networkName": "ansible-msd-net1", + "fabricName": "msd-child-1", + "serialNumber": "915KQ8P3NS8", + "switchPorts": "Ethernet1/15,Ethernet1/16", + "ipAddress": "10.10.10.218", + "lanAttachState": "OUT-OF-SYNC", + "isLanAttached": true, + "deployment": false + } + ] + } + ] + }, + "mock_msd_child_net_update_response": { + "MESSAGE": "OK", + "METHOD": "PUT", + "RETURN_CODE": 200, + "DATA": {} + }, + "playbook_msd_dhcp_config": [ + { + "net_name": "ansible-msd-dhcp-net", + "vrf_name": "Tenant-1", + "net_id": "8004", + "net_template": "Default_Network_Universal", + "net_extension_template": "Default_Network_Extension_Universal", + "vlan_id": "2104", + "gw_ip_subnet": "192.168.104.1/24", + "routing_tag": 12347, + "int_desc": "MSD DHCP Network", + "mtu_l3intf": 9214, + "route_target_both": false, + "is_l2only": false, + "child_fabric_config": [ + { + "fabric": "msd-child-1", + "netflow_enable": false, + "l3gw_on_border": true, + "dhcp_loopback_id": 207, + "multicast_group_address": "239.1.1.4", + "dhcp_srvr1_ip": "192.168.1.102", + "dhcp_srvr1_vrf": "management", + "dhcp_srvr2_ip": "192.168.1.105", + "dhcp_srvr2_vrf": "default", + "dhcp_srvr3_ip": "192.168.1.106", + "dhcp_srvr3_vrf": "management" + } + ], + "attach": [ + { + "ip_address": "192.168.10.203", + "ports": [ + "Ethernet1/17", + "Ethernet1/18" + ] + }, + { + "ip_address": "192.168.10.204", + "ports": [ + "Ethernet1/17", + "Ethernet1/18" + ] + } + ], + "deploy": true + } + ], + "mock_msd_dhcp_net_create_response": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": { + "Network Id": 8004, + "Network Name": "ansible-msd-dhcp-net" + } + }, + "mock_msd_dhcp_net_attach_response": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": { + "ansible-msd-dhcp-net--9R518K2AT3R(leaf3)": "SUCCESS", + "ansible-msd-dhcp-net--915KQ8P3NS8(leaf4)": "SUCCESS" + } + }, + "mock_msd_dhcp_child_net_object": { + "MESSAGE": "OK", + "METHOD": "GET", + "RETURN_CODE": 200, + "DATA": [ + { + "displayName": "ansible-msd-dhcp-net", + "fabric": "msd-child-1", + "networkId": "8004", + "networkName": "ansible-msd-dhcp-net", + "networkTemplate": "Default_Network_Universal", + "networkExtensionTemplate": "Default_Network_Extension_Universal", + "networkTemplateConfig": "{\"suppressArp\":\"false\",\"enableIR\":\"false\",\"trmEnabled\":\"false\",\"rtBothAuto\":\"false\",\"enableL3OnBorder\":\"true\",\"mtu\":\"9214\",\"tag\":\"12347\",\"vrfDhcp\":\"management\",\"vrfDhcp2\":\"default\",\"vrfDhcp3\":\"management\",\"loopbackId\":\"207\",\"dhcpServerAddr1\":\"192.168.1.104\",\"dhcpServerAddr2\":\"192.168.1.105\",\"dhcpServerAddr3\":\"192.168.1.106\",\"vrfName\":\"Tenant-1\",\"isLayer2Only\":\"false\",\"nveId\":\"1\",\"vlanId\":\"2104\",\"segmentId\":\"8004\",\"gatewayIpAddress\":\"192.168.104.1/24\",\"gatewayIpV6Address\":\"\",\"vlanName\":\"\",\"intfDescription\":\"MSD DHCP Network\",\"mcastGroup\":\"239.1.1.4\",\"ENABLE_NETFLOW\":\"false\",\"SVI_NETFLOW_MONITOR\":\"\",\"VLAN_NETFLOW_MONITOR\":\"monitor2\",\"secondaryGW1\":\"\",\"secondaryGW2\":\"\",\"secondaryGW3\":\"\",\"secondaryGW4\":\"\"}", + "vrf": "Tenant-1", + "serviceNetworkTemplate": null, + "source": null, + "interfaceGroups": null, + "networkStatus": "DEPLOYED" + } + ] + }, + "mock_msd_dhcp_child_net_attach_object": { + "MESSAGE": "OK", + "METHOD": "GET", + "RETURN_CODE": 200, + "DATA": [ + { + "fabric": "msd-child-1", + "networkName": "ansible-msd-dhcp-net", + "serialNumber": "9R518K2AT3R", + "switchName": "leaf3", + "ipAddress": "192.168.10.203", + "lanAttachedState": "DEPLOYED", + "isLanAttached": true, + "deployment": true + }, + { + "fabric": "msd-child-1", + "networkName": "ansible-msd-dhcp-net", + "serialNumber": "915KQ8P3NS8", + "switchName": "leaf4", + "ipAddress": "192.168.10.204", + "lanAttachedState": "DEPLOYED", + "isLanAttached": true, + "deployment": true + } + ] + }, + "mock_msd_dhcp_child_net_update_response": { + "MESSAGE": "OK", + "METHOD": "PUT", + "RETURN_CODE": 200, + "DATA": {} + }, + "playbook_msd_override_config": [ + { + "net_name": "ansible-msd-dhcp-net", + "vrf_name": "Tenant-1", + "net_id": "8004", + "net_template": "Default_Network_Universal", + "net_extension_template": "Default_Network_Extension_Universal", + "vlan_id": "2104", + "gw_ip_subnet": "192.168.104.1/24", + "routing_tag": 12347, + "int_desc": "MSD DHCP Network", + "mtu_l3intf": 9216, + "route_target_both": false, + "is_l2only": false, + "child_fabric_config": [ + { + "fabric": "msd-child-1" + } + ], + "attach": [ + { + "ip_address": "192.168.10.203", + "ports": [ + "Ethernet1/16", + "Ethernet1/17" + ] + }, + { + "ip_address": "192.168.10.204", + "ports": [ + "Ethernet1/16", + "Ethernet1/17" + ] + } + ], + "deploy": true + } + ], + "mock_msd_override_parent_net_object": { + "MESSAGE": "OK", + "METHOD": "GET", + "RETURN_CODE": 200, + "DATA": [ + { + "id": 3277, + "fabric": "msd-parent", + "networkName": "ansible-msd-dhcp-net", + "displayName": "ansible-msd-dhcp-net", + "networkId": 8004, + "networkTemplate": "Default_Network_Universal", + "networkExtensionTemplate": "Default_Network_Extension_Universal", + "networkTemplateConfig": "{\"suppressArp\":\"false\",\"secondaryGW3\":\"\",\"secondaryGW2\":\"\",\"secondaryGW1\":\"\",\"vlanId\":\"2104\",\"gatewayIpAddress\":\"192.168.104.1/24\",\"vlanName\":\"\",\"type\":\"Normal\",\"mtu\":\"9216\",\"rtBothAuto\":\"false\",\"isLayer2Only\":\"false\",\"intfDescription\":\"MSD DHCP Network\",\"segmentId\":\"8004\",\"gatewayIpV6Address\":\"\",\"tag\":\"12347\",\"nveId\":\"1\",\"secondaryGW4\":\"\",\"vrfName\":\"Tenant-1\"}", + "vrf": "Tenant-1", + "tenantName": null, + "serviceNetworkTemplate": null, + "source": null, + "interfaceGroups": null, + "primaryNetworkId": -1, + "type": "Normal", + "primaryNetworkName": null, + "vlanId": null, + "vlanName": null, + "networkStatus": "DEPLOYED", + "hierarchicalKey": "msd-parent" + } + ] + }, + "mock_msd_override_parent_net_attach_object": { + "MESSAGE": "OK", + "METHOD": "GET", + "RETURN_CODE": 200, + "DATA": [ + { + "networkName": "ansible-msd-dhcp-net", + "lanAttachList": [ + { + "networkName": "ansible-msd-dhcp-net", + "displayName": "ansible-msd-dhcp-net", + "switchName": "leaf3", + "switchRole": "leaf", + "fabricName": "msd-child-1", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "portNames": "Ethernet1/17,Ethernet1/18", + "switchSerialNo": "9R518K2AT3R", + "peerSerialNo": null, + "switchDbId": 1552090, + "ipAddress": "192.168.10.203", + "networkId": 8004, + "vlanId": 2104, + "instanceValues": "{\"isVPC\":\"false\"}", + "entityName": "ansible-msd-dhcp-net", + "interfaceGroups": null + }, + { + "networkName": "ansible-msd-dhcp-net", + "displayName": "ansible-msd-dhcp-net", + "switchName": "leaf4", + "switchRole": "leaf", + "fabricName": "msd-child-1", + "lanAttachState": "DEPLOYED", + "isLanAttached": true, + "portNames": "Ethernet1/17,Ethernet1/18", + "switchSerialNo": "915KQ8P3NS8", + "peerSerialNo": null, + "switchDbId": 1403960, + "ipAddress": "192.168.10.204", + "networkId": 8004, + "vlanId": 2104, + "instanceValues": "{\"isVPC\":\"false\"}", + "entityName": "ansible-msd-dhcp-net", + "interfaceGroups": null + } + ] + } + ] + }, + "mock_msd_override_attach_response": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": { + "ansible-msd-dhcp-net-[915KQ8P3NS8/leaf4]": "SUCCESS", + "ansible-msd-dhcp-net-[9R518K2AT3R/leaf3]": "SUCCESS" + } + }, + "mock_msd_override_child_net_object": { + "MESSAGE": "OK", + "METHOD": "GET", + "RETURN_CODE": 200, + "DATA": [ + { + "id": 3278, + "fabric": "msd-child-1", + "networkName": "ansible-msd-dhcp-net", + "displayName": "ansible-msd-dhcp-net", + "networkId": 8004, + "networkTemplate": "Default_Network_Universal", + "networkExtensionTemplate": "Default_Network_Extension_Universal", + "networkTemplateConfig": "{\"suppressArp\":\"false\",\"secondaryGW3\":\"\",\"secondaryGW2\":\"\",\"loopbackId\":\"\",\"secondaryGW1\":\"\",\"enableL3OnBorder\":\"false\",\"networkName\":\"ansible-msd-dhcp-net\",\"enableL3OnBorderVpcBgw\":\"false\",\"SVI_NETFLOW_MONITOR\":\"\",\"type\":\"Normal\",\"enableIR\":\"false\",\"rtBothAuto\":\"false\",\"isLayer2Only\":\"false\",\"trmV6Enabled\":\"\",\"ENABLE_NETFLOW\":\"false\",\"segmentId\":\"8004\",\"dhcpServerAddr3\":\"\",\"gatewayIpV6Address\":\"\",\"dhcpServerAddr2\":\"\",\"tag\":\"12347\",\"nveId\":\"1\",\"vrfDhcp\":\"\",\"secondaryGW4\":\"\",\"vlanId\":\"2104\",\"gatewayIpAddress\":\"192.168.104.1/24\",\"vlanName\":\"\",\"mtu\":\"9216\",\"intfDescription\":\"MSD DHCP Network\",\"mcastGroup\":\"239.1.1.1\",\"igmpVersion\":\"2\",\"trmEnabled\":\"false\",\"VLAN_NETFLOW_MONITOR\":\"\",\"dhcpServers\":\"\",\"vrfName\":\"Tenant-1\"}", + "vrf": "Tenant-1", + "tenantName": null, + "serviceNetworkTemplate": null, + "source": null, + "interfaceGroups": "", + "primaryNetworkId": -1, + "type": "Normal", + "primaryNetworkName": null, + "vlanId": null, + "vlanName": null, + "networkStatus": "IN PROGRESS", + "hierarchicalKey": "msd-child-1" + } + ] + }, + "mock_msd_override_child_net_attach_object": { + "MESSAGE": "OK", + "METHOD": "GET", + "RETURN_CODE": 200, + "DATA": [ + { + "networkName": "ansible-msd-dhcp-net", + "lanAttachList": [ + { + "networkName": "ansible-msd-dhcp-net", + "displayName": "ansible-msd-dhcp-net", + "switchName": "leaf3", + "switchRole": "leaf", + "fabricName": "msd-child-1", + "lanAttachState": "IN PROGRESS", + "isLanAttached": true, + "portNames": "Ethernet1/16,Ethernet1/17", + "switchSerialNo": "9R518K2AT3R", + "peerSerialNo": null, + "switchDbId": 1552090, + "ipAddress": "192.168.10.203", + "networkId": 8004, + "vlanId": 2104, + "instanceValues": "{\"isVPC\":\"false\"}", + "entityName": "ansible-msd-dhcp-net", + "interfaceGroups": null + }, + { + "networkName": "ansible-msd-dhcp-net", + "displayName": "ansible-msd-dhcp-net", + "switchName": "leaf4", + "switchRole": "leaf", + "fabricName": "msd-child-1", + "lanAttachState": "IN PROGRESS", + "isLanAttached": true, + "portNames": "Ethernet1/16,Ethernet1/17", + "switchSerialNo": "915KQ8P3NS8", + "peerSerialNo": null, + "switchDbId": 1403960, + "ipAddress": "192.168.10.204", + "networkId": 8004, + "vlanId": 2104, + "instanceValues": "{\"isVPC\":\"false\"}", + "entityName": "ansible-msd-dhcp-net", + "interfaceGroups": null + } + ] + } + ] } } \ No newline at end of file diff --git a/tests/unit/modules/dcnm/fixtures/dcnm_vrf.json b/tests/unit/modules/dcnm/fixtures/dcnm_vrf.json index a3fc3ae46..f2dc3632f 100644 --- a/tests/unit/modules/dcnm/fixtures/dcnm_vrf.json +++ b/tests/unit/modules/dcnm/fixtures/dcnm_vrf.json @@ -7,18 +7,22 @@ "10.10.10.228": "XYZKSJHSMK5" }, "mock_ip_fab" : { - "10.10.10.224": "test_fabric", - "10.10.10.225": "test_fabric", - "10.10.10.226": "test_fabric", - "10.10.10.227": "test_fabric", - "10.10.10.228": "test_fabric" + "10.10.10.224": "standalone_fabric", + "10.10.10.225": "standalone_fabric", + "10.10.10.226": "standalone_fabric", + "10.10.10.227": "standalone_fabric", + "10.10.10.228": "standalone_fabric" }, "mock_sn_fab" : { - "XYZKSJHSMK1": "test_fabric", - "XYZKSJHSMK2": "test_fabric", - "XYZKSJHSMK3": "test_fabric", - "XYZKSJHSMK4": "test_fabric", - "XYZKSJHSMK5": "test_fabric" + "XYZKSJHSMK1": "standalone_fabric", + "XYZKSJHSMK2": "standalone_fabric", + "XYZKSJHSMK3": "standalone_fabric", + "XYZKSJHSMK4": "standalone_fabric", + "XYZKSJHSMK5": "standalone_fabric" + }, + "mock_sn_fab_msd" : { + "XYZKSJHSMK1": "parent_fabric", + "XYZKSJHSMK2": "parent_fabric" }, "playbook_config_input_validation" : [ { @@ -749,6 +753,279 @@ "service_vrf_template": "None" } ], + "playbook_msd_config" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "2000", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224" + } + ], + "child_fabric_config": [ + { + "adv_host_routes": true, + "adv_default_routes": true, + "l3vni_wo_vlan": false, + "fabric": "child_fabric" + } + ], + "deploy": true + } + ], + "playbook_msd_replace_config" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vrf_int_mtu": 1500, + "attach": [ + { + "ip_address": "10.10.10.224" + } + ], + "child_fabric_config": [ + { + "fabric": "child_fabric", + "static_default_route": true, + "l3vni_wo_vlan": true + } + ], + "deploy": true + } + ], + "playbook_msd_config_no_child" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "2000", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224" + } + ], + "deploy": true + } + ], + "playbook_msd_config_misconfig_1" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "2000", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224" + } + ], + "child_fabric_config": [ + { + "fabric_name": "k_fab", + "static_default_route": true, + "l3vni_wo_vlan": true + } + ], + "deploy": true + } + ], + "playbook_msd_config_misconfig_2" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "2000", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224" + } + ], + "child_fabric_config": [ + { + "fabric": "k_fab", + "static_default_route": true, + "l3vni_wo_vlan": true + } + ], + "deploy": true + } + ], + "playbook_msd_config_misconfig_3" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "2000", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224" + } + ], + "child_fabric_config": [ + ], + "deploy": true + } + ], + "playbook_msd_delete_config" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "2000", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224" + } + ], + "deploy": true + } + ], + "playbook_msd_override_config": [ + { + "vrf_name": "test_vrf_2", + "vrf_id": "9008012", + "vrf_template": "Default_VRF_Universal", + "vrf_extension_template": "Default_VRF_Extension_Universal", + "vlan_id": "2001", + "source": "None", + "service_vrf_template": "None", + "attach": [ + { + "ip_address": "10.10.10.224", + "deploy": true + } + ], + "child_fabric_config": [ + { + "fabric": "child_fabric", + "adv_host_routes": true, + "adv_default_routes": true, + "l3vni_wo_vlan": false + } + ], + "deploy": true + } + ], + "playbook_msd_query_config" : [ + { + "vrf_name": "test_vrf_1", + "vrf_id": "9008011", + "vlan_id": "2000", + "attach": [ + { + "ip_address": "10.10.10.224" + } + ], + "child_fabric_config": [ + { + "adv_host_routes": true, + "adv_default_routes": true, + "l3vni_wo_vlan": false, + "fabric": "child_fabric" + } + ], + "deploy": true + } + ], + "playbook_msd_child_config" : [ + { + "vrf_name": "test_vrf_1", + "adv_host_routes": true, + "adv_default_routes": true, + "l3vni_wo_vlan": false + } + ], + "mock_fabric_associations": { + "DATA": [ + { + "fabricId": 38, + "fabricName": "parent_fabric", + "fabricParent": "None", + "fabricState": "msd", + "fabricTechnology": "VXLANFabric", + "fabricType": "MSD" + }, + { + "fabricId": 40, + "fabricName": "child_fabric", + "fabricParent": "parent_fabric", + "fabricState": "member", + "fabricTechnology": "VXLANFabric", + "fabricType": "Switch_Fabric" + }, + { + "fabricId": 11, + "fabricName": "standalone_fabric", + "fabricParent": "None", + "fabricState": "standalone", + "fabricTechnology": "VXLANFabric", + "fabricType": "Switch_Fabric" + } + ], + "MESSAGE": "OK", + "METHOD": "GET", + "REQUEST_PATH": "https://mock.ndfc/appcenter/cisco/ndfc/api/v1/lan-fabric/rest/control/fabrics/msd/fabric-associations", + "RETURN_CODE": 200 + }, + "mock_vrf_get_object": { + "RETURN_CODE": 200, + "MESSAGE": "OK", + "DATA":[ + { + "fabric": "child_fabric", + "vrfName": "test_vrf_1", + "enforce": null, + "defaultSGTag": null, + "vrfTemplate": "Default_VRF_Universal", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfTemplateConfig": "{\"routeTargetExportEvpn\":\"\",\"routeTargetImport\":\"\",\"vrfVlanId\":\"2000\",\"vrfDescription\":\"\",\"disableRtAuto\":\"false\",\"v6VrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"maxBgpPaths\":\"1\",\"maxIbgpPaths\":\"2\",\"vrfSegmentId\":\"50000\",\"routeTargetExport\":\"\",\"ipv6LinkLocalFlag\":\"true\",\"mtu\":\"9216\",\"vrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"vrfVlanName\":\"\",\"tag\":\"12345\",\"nveId\":\"1\",\"vrfIntfDescription\":\"\",\"routeTargetImportEvpn\":\"\",\"vrfName\":\"test_vrf_1\"}", + "tenantName": null, + "id": 19899, + "vrfId": 9008011, + "serviceVrfTemplate": null, + "source": null, + "vrfStatus": "DEPLOYED", + "hierarchicalKey": "child_fabric" + }, + { + "fabric": "child_fabric", + "vrfName": "test_vrf_2", + "enforce": null, + "defaultSGTag": null, + "vrfTemplate": "Default_VRF_Universal", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfTemplateConfig": "{\"routeTargetExportEvpn\":\"\",\"routeTargetImport\":\"\",\"vrfVlanId\":\"2001\",\"vrfDescription\":\"\",\"disableRtAuto\":\"false\",\"v6VrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"maxBgpPaths\":\"1\",\"maxIbgpPaths\":\"2\",\"vrfSegmentId\":\"50000\",\"routeTargetExport\":\"\",\"ipv6LinkLocalFlag\":\"true\",\"mtu\":\"9216\",\"vrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"vrfVlanName\":\"\",\"tag\":\"12345\",\"nveId\":\"1\",\"vrfIntfDescription\":\"\",\"routeTargetImportEvpn\":\"\",\"vrfName\":\"test_vrf_2\"}", + "tenantName": null, + "id": 19899, + "vrfId": 9008012, + "serviceVrfTemplate": null, + "source": null, + "vrfStatus": "DEPLOYED", + "hierarchicalKey": "child_fabric" + } + ] + }, "mock_vrf_attach_object_del_not_ready": { "ERROR": "", "RETURN_CODE": 200, @@ -769,63 +1046,286 @@ } ] }, - "mock_vrf_attach_object_del_oos": { - "ERROR": "", - "RETURN_CODE": 200, + "mock_msd_vrf_attach_object_del_not_ready": { + "ERROR": "", + "RETURN_CODE": 200, + "MESSAGE": "OK", + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "lanAttachState": "DEPLOYED", + "isLanAttached": false + } + ] + } + ] + }, + "mock_vrf_attach_object_del_oos": { + "ERROR": "", + "RETURN_CODE": 200, + "MESSAGE": "OK", + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "lanAttachState": "OUT-OF-SYNC" + }, + { + "lanAttachState": "OUT-OF-SYNC" + } + ] + } + ] + }, + "mock_vrf_attach_object_del_ready": { + "ERROR": "", + "RETURN_CODE": 200, + "MESSAGE": "OK", + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "lanAttachState": "NA", + "isLanAttached": false + }, + { + "lanAttachState": "NA", + "isLanAttached": false + } + ] + } + ] + }, + "mock_msd_vrf_attach_object_del_ready": { + "ERROR": "", + "RETURN_CODE": 200, + "MESSAGE": "OK", + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "lanAttachState": "NA", + "isLanAttached": false + } + ] + } + ] + }, + "mock_vrf_object": { + "ERROR": "", + "RETURN_CODE": 200, + "MESSAGE":"OK", + "DATA": [ + { + "fabric": "standalone_fabric", + "serviceVrfTemplate": "None", + "source": "None", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfId": 9008011, + "vrfName": "test_vrf_1", + "vrfTemplate": "Default_VRF_Universal", + "vrfTemplateConfig": "{\"advertiseDefaultRouteFlag\":\"true\",\"vrfVlanId\":\"202\",\"isRPExternal\":\"false\",\"vrfDescription\":\"\",\"L3VniMcastGroup\":\"\",\"maxBgpPaths\":\"1\",\"maxIbgpPaths\":\"2\",\"vrfSegmentId\":\"9008013\",\"ipv6LinkLocalFlag\":\"true\",\"vrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"v6VrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"configureStaticDefaultRouteFlag\":\"true\",\"trmBGWMSiteEnabled\":\"false\",\"tag\":\"12345\",\"rpAddress\":\"\",\"nveId\":\"1\",\"bgpPasswordKeyType\":\"3\",\"bgpPassword\":\"\",\"mtu\":\"9216\",\"multicastGroup\":\"\",\"advertiseHostRouteFlag\":\"false\",\"vrfVlanName\":\"\",\"trmEnabled\":\"false\",\"loopbackNumber\":\"\",\"asn\":\"34343\",\"vrfIntfDescription\":\"\",\"vrfName\":\"test_vrf_1\"}", + "vrfStatus": "DEPLOYED" + } + ] + }, + "mock_msd_parent_vrf_object": { + "ERROR": "", + "RETURN_CODE": 200, + "MESSAGE":"OK", + "DATA": [ + { + "fabric": "parent_fabric", + "serviceVrfTemplate": "None", + "source": "None", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfId": 9008011, + "vrfName": "test_vrf_1", + "vrfTemplate": "Default_VRF_Universal", + "vrfTemplateConfig": "{\"advertiseDefaultRouteFlag\":\"true\",\"vrfVlanId\":\"2000\",\"isRPExternal\":\"false\",\"vrfDescription\":\"\",\"L3VniMcastGroup\":\"\",\"maxBgpPaths\":\"1\",\"maxIbgpPaths\":\"2\",\"vrfSegmentId\":\"9008013\",\"ipv6LinkLocalFlag\":\"true\",\"vrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"v6VrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"configureStaticDefaultRouteFlag\":\"true\",\"trmBGWMSiteEnabled\":\"false\",\"tag\":\"12345\",\"rpAddress\":\"\",\"nveId\":\"1\",\"bgpPasswordKeyType\":\"3\",\"bgpPassword\":\"\",\"mtu\":\"9216\",\"multicastGroup\":\"\",\"advertiseHostRouteFlag\":\"false\",\"vrfVlanName\":\"\",\"trmEnabled\":\"false\",\"loopbackNumber\":\"\",\"asn\":\"34343\",\"vrfIntfDescription\":\"\",\"vrfName\":\"test_vrf_1\"}", + "vrfStatus": "DEPLOYED" + } + ] + }, + "mock_msd_vrf_object": { + "ERROR": "", + "RETURN_CODE": 200, + "MESSAGE":"OK", + "DATA": [ + { + "fabric": "child_fabric", + "serviceVrfTemplate": "None", + "source": "None", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfId": 9008011, + "vrfName": "test_vrf_1", + "vrfTemplate": "Default_VRF_Universal", + "vrfTemplateConfig": "{\"advertiseDefaultRouteFlag\":\"true\",\"vrfVlanId\":\"2000\",\"isRPExternal\":\"false\",\"vrfDescription\":\"\",\"L3VniMcastGroup\":\"\",\"maxBgpPaths\":\"1\",\"maxIbgpPaths\":\"2\",\"vrfSegmentId\":\"9008013\",\"ipv6LinkLocalFlag\":\"true\",\"vrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"v6VrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"configureStaticDefaultRouteFlag\":\"true\",\"trmBGWMSiteEnabled\":\"false\",\"tag\":\"12345\",\"rpAddress\":\"\",\"nveId\":\"1\",\"bgpPasswordKeyType\":\"3\",\"bgpPassword\":\"\",\"mtu\":\"9216\",\"multicastGroup\":\"\",\"advertiseHostRouteFlag\":\"false\",\"vrfVlanName\":\"\",\"trmEnabled\":\"false\",\"loopbackNumber\":\"\",\"asn\":\"34343\",\"vrfIntfDescription\":\"\",\"vrfName\":\"test_vrf_1\"}", + "vrfStatus": "DEPLOYED" + } + ] + }, + "mock_msd_vrf_object_2": { + "ERROR": "", + "RETURN_CODE": 200, + "MESSAGE":"OK", + "DATA": [ + { + "fabric": "child_fabric", + "serviceVrfTemplate": "None", + "source": "None", + "vrfExtensionTemplate": "Default_VRF_Extension_Universal", + "vrfId": 9008012, + "vrfName": "test_vrf_2", + "vrfTemplate": "Default_VRF_Universal", + "vrfTemplateConfig": "{\"advertiseDefaultRouteFlag\":\"true\",\"vrfVlanId\":\"2001\",\"isRPExternal\":\"false\",\"vrfDescription\":\"\",\"L3VniMcastGroup\":\"\",\"maxBgpPaths\":\"1\",\"maxIbgpPaths\":\"2\",\"vrfSegmentId\":\"9008013\",\"ipv6LinkLocalFlag\":\"true\",\"vrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"v6VrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"configureStaticDefaultRouteFlag\":\"true\",\"trmBGWMSiteEnabled\":\"false\",\"tag\":\"12345\",\"rpAddress\":\"\",\"nveId\":\"1\",\"bgpPasswordKeyType\":\"3\",\"bgpPassword\":\"\",\"mtu\":\"9216\",\"multicastGroup\":\"\",\"advertiseHostRouteFlag\":\"false\",\"vrfVlanName\":\"\",\"trmEnabled\":\"false\",\"loopbackNumber\":\"\",\"asn\":\"34343\",\"vrfIntfDescription\":\"\",\"vrfName\":\"test_vrf_2\"}", + "vrfStatus": "DEPLOYED" + } + ] + }, + "mock_vrf_attach_object" : { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf1", + "lanAttachState": "DEPLOYED", + "instanceValues": "{\"loopbackId\":\"\",\"loopbackIpAddress\":\"\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"5000:100\",\"switchRouteTargetExportEvpn\":\"5000:100\"}", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK1", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.224", + "vlanId": "202", + "vrfId": "9008011" + }, + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf2", + "lanAttachState": "DEPLOYED", + "instanceValues": "{\"loopbackId\":\"\",\"loopbackIpAddress\":\"\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"5000:100\",\"switchRouteTargetExportEvpn\":\"5000:100\"}", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK2", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.225", + "vlanId": "202", + "vrfId": "9008011" + } + ] + } + ] + }, + "mock_msd_vrf_attach_object" : { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf1", + "lanAttachState": "DEPLOYED", + "instanceValues": "{\"loopbackId\":\"\",\"loopbackIpAddress\":\"\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"\",\"switchRouteTargetExportEvpn\":\"\"}", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK1", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.224", + "vlanId": "2000", + "vrfId": "9008011" + } + ] + } + ] + }, + "mock_msd_vrf_child_attach_object" : { "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, "DATA": [ { "vrfName": "test_vrf_1", "lanAttachList": [ { - "lanAttachState": "OUT-OF-SYNC" - }, - { - "lanAttachState": "OUT-OF-SYNC" + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf1", + "lanAttachState": "DEPLOYED", + "instanceValues": "{\"loopbackId\":\"\",\"loopbackIpAddress\":\"\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"\",\"switchRouteTargetExportEvpn\":\"\"}", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK1", + "switchRole": "leaf", + "fabricName": "child_fabric", + "ipAddress": "10.10.10.224", + "vlanId": "2000", + "vrfId": "9008011" } ] } ] }, - "mock_vrf_attach_object_del_ready": { - "ERROR": "", - "RETURN_CODE": 200, + "mock_msd_vrf_child_attach_object_2" : { "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, "DATA": [ { - "vrfName": "test_vrf_1", + "vrfName": "test_vrf_2", "lanAttachList": [ { - "lanAttachState": "NA", - "isLanAttached": false - }, - { - "lanAttachState": "NA", - "isLanAttached": false + "vrfName": "test_vrf_2", + "switchName": "n9kv_leaf1", + "lanAttachState": "DEPLOYED", + "instanceValues": "{\"loopbackId\":\"\",\"loopbackIpAddress\":\"\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"\",\"switchRouteTargetExportEvpn\":\"\"}", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK1", + "switchRole": "leaf", + "fabricName": "child_fabric", + "ipAddress": "10.10.10.224", + "vlanId": "2001", + "vrfId": "9008012" } ] } ] }, - "mock_vrf_object": { - "ERROR": "", + "mock_msd_vrf_get_attach_object_replaced_parent" : { + "METHOD": "GET", "RETURN_CODE": 200, - "MESSAGE":"OK", "DATA": [ { - "fabric": "test_fabric", - "serviceVrfTemplate": "None", - "source": "None", - "vrfExtensionTemplate": "Default_VRF_Extension_Universal", - "vrfId": 9008011, "vrfName": "test_vrf_1", - "vrfTemplate": "Default_VRF_Universal", - "vrfTemplateConfig": "{\"advertiseDefaultRouteFlag\":\"true\",\"vrfVlanId\":\"202\",\"isRPExternal\":\"false\",\"vrfDescription\":\"\",\"L3VniMcastGroup\":\"\",\"maxBgpPaths\":\"1\",\"maxIbgpPaths\":\"2\",\"vrfSegmentId\":\"9008013\",\"ipv6LinkLocalFlag\":\"true\",\"vrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"v6VrfRouteMap\":\"FABRIC-RMAP-REDIST-SUBNET\",\"configureStaticDefaultRouteFlag\":\"true\",\"trmBGWMSiteEnabled\":\"false\",\"tag\":\"12345\",\"rpAddress\":\"\",\"nveId\":\"1\",\"bgpPasswordKeyType\":\"3\",\"bgpPassword\":\"\",\"mtu\":\"9216\",\"multicastGroup\":\"\",\"advertiseHostRouteFlag\":\"false\",\"vrfVlanName\":\"\",\"trmEnabled\":\"false\",\"loopbackNumber\":\"\",\"asn\":\"34343\",\"vrfIntfDescription\":\"\",\"vrfName\":\"test_vrf_1\"}", - "vrfStatus": "DEPLOYED" + "lanAttachList": [ + { + "vrfName": "test_vrf_1", + "switchName": "n9kv_leaf1", + "lanAttachState": "DEPLOYED", + "instanceValues": "{\"loopbackId\":\"\",\"loopbackIpAddress\":\"\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"\",\"switchRouteTargetExportEvpn\":\"\"}", + "isLanAttached": true, + "switchSerialNo": "XYZKSJHSMK1", + "switchRole": "leaf", + "fabricName": "test-fabric", + "ipAddress": "10.10.10.224", + "vlanId": "2000", + "vrfId": "9008011" + } + ] } ] }, - "mock_vrf_attach_object" : { + "mock_vrf_attach_object_query" : { "MESSAGE": "OK", "METHOD": "POST", "RETURN_CODE": 200, @@ -837,7 +1337,6 @@ "vrfName": "test_vrf_1", "switchName": "n9kv_leaf1", "lanAttachState": "DEPLOYED", - "instanceValues": "{\"loopbackId\":\"\",\"loopbackIpAddress\":\"\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"5000:100\",\"switchRouteTargetExportEvpn\":\"5000:100\"}", "isLanAttached": true, "switchSerialNo": "XYZKSJHSMK1", "switchRole": "leaf", @@ -850,7 +1349,6 @@ "vrfName": "test_vrf_1", "switchName": "n9kv_leaf2", "lanAttachState": "DEPLOYED", - "instanceValues": "{\"loopbackId\":\"\",\"loopbackIpAddress\":\"\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"5000:100\",\"switchRouteTargetExportEvpn\":\"5000:100\"}", "isLanAttached": true, "switchSerialNo": "XYZKSJHSMK2", "switchRole": "leaf", @@ -863,7 +1361,7 @@ } ] }, - "mock_vrf_attach_object_query" : { + "mock_msd_vrf_parent_attach_object_query" : { "MESSAGE": "OK", "METHOD": "POST", "RETURN_CODE": 200, @@ -878,21 +1376,33 @@ "isLanAttached": true, "switchSerialNo": "XYZKSJHSMK1", "switchRole": "leaf", - "fabricName": "test-fabric", + "fabricName": "parent_fabric", "ipAddress": "10.10.10.224", - "vlanId": "202", + "vlanId": "2000", "vrfId": "9008011" - }, + } + ] + } + ] + }, + "mock_msd_vrf_child_attach_object_query" : { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "vrfName": "test_vrf_1", + "lanAttachList": [ { "vrfName": "test_vrf_1", - "switchName": "n9kv_leaf2", + "switchName": "n9kv_leaf1", "lanAttachState": "DEPLOYED", "isLanAttached": true, - "switchSerialNo": "XYZKSJHSMK2", + "switchSerialNo": "XYZKSJHSMK1", "switchRole": "leaf", - "fabricName": "test-fabric", - "ipAddress": "10.10.10.225", - "vlanId": "202", + "fabricName": "child_fabric", + "ipAddress": "10.10.10.224", + "vlanId": "2000", "vrfId": "9008011" } ] @@ -982,7 +1492,7 @@ { "deployment": true, "extensionValues": "", - "fabric": "test_fabric", + "fabric": "standalone_fabric", "freeformConfig": "", "instanceValues": "", "serialNumber": "XYZKSJHSMK1", @@ -993,7 +1503,7 @@ { "deployment": true, "extensionValues": "{\"VRF_LITE_CONN\":\"{\\\"VRF_LITE_CONN\\\":[{\\\"IF_NAME\\\":\\\"Ethernet1/16\\\",\\\"DOT1Q_ID\\\":\\\"2\\\",\\\"IP_MASK\\\":\\\"10.33.0.2/30\\\",\\\"NEIGHBOR_IP\\\":\\\"10.33.0.1\\\",\\\"NEIGHBOR_ASN\\\":\\\"65535\\\",\\\"IPV6_MASK\\\":\\\"2010::10:34:0:7/64\\\",\\\"IPV6_NEIGHBOR\\\":\\\"2010::10:34:0:3\\\",\\\"AUTO_VRF_LITE_FLAG\\\":\\\"false\\\",\\\"PEER_VRF_NAME\\\":\\\"ansible-vrf-int1\\\",\\\"VRF_LITE_JYTHON_TEMPLATE\\\":\\\"Ext_VRF_Lite_Jython\\\"}]}\",\"MULTISITE_CONN\":\"{\\\"MULTISITE_CONN\\\":[]}\"}", - "fabric": "test_fabric", + "fabric": "standalone_fabric", "freeformConfig": "", "instanceValues": "{\"loopbackId\":\"\",\"loopbackIpAddress\":\"\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"5000:100\",\"switchRouteTargetExportEvpn\":\"5000:100\"}", "serialNumber": "XYZKSJHSMK4", @@ -1046,7 +1556,7 @@ "MESSAGE": "OK", "DATA": [ { - "fabric": "test_fabric", + "fabric": "standalone_fabric", "vrfName": "test_vrf_dcnm", "vrfTemplate": "Default_VRF_Universal", "vrfExtensionTemplate": "Default_VRF_Extension_Universal", @@ -1212,6 +1722,34 @@ } ] }, + "mock_msd_vrf_attach_get_ext_object_merge_att1_only": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [], + "extensionValues": "", + "freeformConfig": "", + "instanceValues": "", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "leaf", + "serialNumber": "XYZKSJHSMK1", + "switchName": "n9kv_leaf1", + "vlan": "2000", + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Universal", + "vrfName": "test_vrf_1" + } + ] + }, "mock_vrf_attach_get_ext_object_ov_att1_only": { "MESSAGE": "OK", @@ -1334,8 +1872,62 @@ } ] }, - - + "mock_vrf_msd_attach_get_ext_object_dcnm_att1_only": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [], + "extensionValues": "", + "freeformConfig": "", + "instanceValues": "{\"loopbackId\":\"\",\"loopbackIpAddress\":\"\",\"loopbackIpV6Address\":\"\",\"switchRouteTargetImportEvpn\":\"\",\"switchRouteTargetExportEvpn\":\"\"}", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "leaf", + "serialNumber": "XYZKSJHSMK1", + "switchName": "n9kv_leaf1", + "vlan": "2000", + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Universal", + "vrfName": "test_vrf_dcnm" + } + ] + }, + "mock_vrf_msd_attach_get_ext_object_ov_att1_only": { + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200, + "DATA": [ + { + "switchDetailsList": [ + { + "errorMessage": null, + "extensionPrototypeValues": [], + "extensionValues": "", + "freeformConfig": "", + "instanceValues": "", + "islanAttached": true, + "lanAttachedState": "DEPLOYED", + "peerSerialNumber": null, + "role": "leaf", + "serialNumber": "XYZKSJHSMK1", + "switchName": "n9kv_leaf1", + "vlan": "2001", + "vlanModifiable": true + } + ], + "templateName": "Default_VRF_Universal", + "vrfName": "test_vrf_1" + } + ] + }, "attach_success_resp": { "DATA": { "test-vrf-1--XYZKSJHSMK1(leaf1)": "SUCCESS", @@ -1345,6 +1937,22 @@ "METHOD": "POST", "RETURN_CODE": 200 }, + "msd_attach_success_resp": { + "DATA": { + "test-vrf-1--XYZKSJHSMK1(leaf1)": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200 + }, + "msd_attach_success_resp_2": { + "DATA": { + "test-vrf-2--XYZKSJHSMK1(leaf1)": "SUCCESS" + }, + "MESSAGE": "OK", + "METHOD": "POST", + "RETURN_CODE": 200 + }, "attach_success_resp2": { "DATA": { "test-vrf-2--XYZKSJHSMK2(leaf2)": "SUCCESS", @@ -1374,6 +1982,11 @@ "METHOD": "POST", "RETURN_CODE": 200 }, + "update_data": { + "MESSAGE": "OK", + "METHOD": "PUT", + "RETURN_CODE": 200 + }, "get_have_failure": { "DATA": "Invalid JSON response: Invalid Fabric: demo-fabric-123", "ERROR": "Not Found", @@ -1445,7 +2058,7 @@ }, "fabric_details_mfd": { "id": 4, - "fabricId": "FABRIC-4", + "fabricId": "standalone_fabric_mfd", "fabricName": "MSD", "fabricType": "MFD", "fabricTypeFriendly": "VXLAN EVPN Multi-Site", @@ -1929,7 +2542,7 @@ "MESSAGE":"OK", "DATA": [ { - "fabric": "test_fabric", + "fabric": "standalone_fabric", "serviceVrfTemplate": "None", "source": "None", "vrfExtensionTemplate": "Default_VRF_Extension_Universal", @@ -1971,6 +2584,36 @@ } ] }, + "mock_pools_top_down_dot1q": { + "ERROR": "", + "RETURN_CODE": 200, + "MESSAGE":"OK", + "DATA": [ + { + "allocatedFlag": true, + "allocatedIp": "5", + "allocatedOn": 1734051260507, + "allocatedScopeValue": "XYZKSJHSMK1", + "entityName": "test_vrf_1", + "entityType": "DeviceInterface", + "hierarchicalKey": "0", + "id": 6613, + "ipAddress": "10.10.10.224", + "resourcePool": { + "dynamicSubnetRange": null, + "fabricName": "parent_fabric", + "hierarchicalKey": "parent_fabric", + "id": 0, + "overlapAllowed": false, + "poolName": "TOP_DOWN_L3_DOT1Q", + "poolType": "ID_POOL", + "targetSubnet": 0, + "vrfName": "VRF_1" + }, + "switchName": "n9kv_leaf1" + } + ] + }, "mock_vrf_lite_obj": { "RETURN_CODE":200, "METHOD":"GET", @@ -2013,5 +2656,77 @@ ] } ] + }, + "mock_msd_vrf_lite_obj": { + "RETURN_CODE":200, + "METHOD":"GET", + "MESSAGE":"OK", + "DATA": [ + { + "vrfName":"test_vrf_1", + "templateName":"Default_VRF_Extension_Universal", + "switchDetailsList":[ + { + "switchName":"leaf1", + "vlan":2000, + "serialNumber":"XYZKSJHSMK1", + "peerSerialNumber": "None", + "extensionValues":"{}", + "extensionPrototypeValues":[ + { + "interfaceName":"Ethernet1/3", + "extensionType":"VRF_LITE", + "extensionValues":"{\"PEER_VRF_NAME\": \"\", \"NEIGHBOR_IP\": \"10.33.0.1\", \"VRF_LITE_JYTHON_TEMPLATE\": \"Ext_VRF_Lite_Jython\", \"enableBorderExtension\": \"VRF_LITE\", \"AUTO_VRF_LITE_FLAG\":\"false\", \"IP_MASK\": \"10.33.0.2/30\", \"MTU\": \"9216\", \"NEIGHBOR_ASN\": \"23132\", \"IF_NAME\": \"Ethernet1/3\", \"IPV6_NEIGHBOR\": \"\", \"IPV6_MASK\": \"\", \"DOT1Q_ID\": \"2\", \"asn\": \"52125\"}", + "destInterfaceName":"Ethernet1/1", + "destSwitchName":"poap-import-static" + } + ], + "islanAttached":false, + "lanAttachedState":"DEPLOYED", + "errorMessage":"None", + "instanceValues": "{\"loopbackIpV6Address\":\"\",\"loopbackId\":\"\",\"deviceSupportL3VniNoVlan\":\"false\",\"switchRouteTargetImportEvpn\":\"\",\"loopbackIpAddress\":\"\",\"switchRouteTargetExportEvpn\":\"\"}", + "freeformConfig":"", + "role":"leaf", + "vlanModifiable":true + } + ] + } + ] + }, + "mock_msd_vrf_lite_obj_2": { + "RETURN_CODE":200, + "METHOD":"GET", + "MESSAGE":"OK", + "DATA": [ + { + "vrfName":"test_vrf_2", + "templateName":"Default_VRF_Extension_Universal", + "switchDetailsList":[ + { + "switchName":"leaf1", + "vlan":2001, + "serialNumber":"XYZKSJHSMK1", + "peerSerialNumber": "None", + "extensionValues":"{}", + "extensionPrototypeValues":[ + { + "interfaceName":"Ethernet1/3", + "extensionType":"VRF_LITE", + "extensionValues":"{\"PEER_VRF_NAME\": \"\", \"NEIGHBOR_IP\": \"10.33.0.1\", \"VRF_LITE_JYTHON_TEMPLATE\": \"Ext_VRF_Lite_Jython\", \"enableBorderExtension\": \"VRF_LITE\", \"AUTO_VRF_LITE_FLAG\":\"false\", \"IP_MASK\": \"10.33.0.2/30\", \"MTU\": \"9216\", \"NEIGHBOR_ASN\": \"23132\", \"IF_NAME\": \"Ethernet1/3\", \"IPV6_NEIGHBOR\": \"\", \"IPV6_MASK\": \"\", \"DOT1Q_ID\": \"2\", \"asn\": \"52125\"}", + "destInterfaceName":"Ethernet1/1", + "destSwitchName":"poap-import-static" + } + ], + "islanAttached":false, + "lanAttachedState":"DEPLOYED", + "errorMessage":"None", + "instanceValues": "{\"loopbackIpV6Address\":\"\",\"loopbackId\":\"\",\"deviceSupportL3VniNoVlan\":\"false\",\"switchRouteTargetImportEvpn\":\"\",\"loopbackIpAddress\":\"\",\"switchRouteTargetExportEvpn\":\"\"}", + "freeformConfig":"", + "role":"leaf", + "vlanModifiable":true + } + ] + } + ] } -} +} \ No newline at end of file diff --git a/tests/unit/modules/dcnm/test_dcnm_network.py b/tests/unit/modules/dcnm/test_dcnm_network.py index 8e592c73b..90ed8488c 100644 --- a/tests/unit/modules/dcnm/test_dcnm_network.py +++ b/tests/unit/modules/dcnm/test_dcnm_network.py @@ -41,6 +41,7 @@ class TestDcnmNetworkModule(TestDcnmModule): net_inv_data = test_data.get("net_inv_data") fabric_details = test_data.get("fabric_details") fabric_details_vxlan_fabric = test_data.get("fabric_details_vxlan_fabric") + fabric_associations = test_data.get("fabric_associations") playbook_config = test_data.get("playbook_config") playbook_config_incorrect_netid = test_data.get("playbook_config_incorrect_netid") @@ -69,6 +70,35 @@ class TestDcnmNetworkModule(TestDcnmModule): delete_success_resp = test_data.get("delete_success_resp") blank_data = test_data.get("blank_data") + empty_network_list = test_data.get("empty_network_list") + + # MSD test data + playbook_msd_config = test_data.get("playbook_msd_config") + mock_msd_fabric_details = test_data.get("mock_msd_fabric_details") + mock_msd_child_fabric_details = test_data.get("mock_msd_child_fabric_details") + mock_msd_ip_sn = test_data.get("mock_msd_ip_sn") + mock_msd_vrf_object = test_data.get("mock_msd_vrf_object") + mock_msd_net_create_response = test_data.get("mock_msd_net_create_response") + mock_msd_net_attach_response = test_data.get("mock_msd_net_attach_response") + mock_msd_child_net_object = test_data.get("mock_msd_child_net_object") + mock_msd_child_net_attach_object = test_data.get("mock_msd_child_net_attach_object") + mock_msd_child_net_update_response = test_data.get("mock_msd_child_net_update_response") + + # MSD DHCP test data + playbook_msd_dhcp_config = test_data.get("playbook_msd_dhcp_config") + mock_msd_dhcp_net_create_response = test_data.get("mock_msd_dhcp_net_create_response") + mock_msd_dhcp_net_attach_response = test_data.get("mock_msd_dhcp_net_attach_response") + mock_msd_dhcp_child_net_object = test_data.get("mock_msd_dhcp_child_net_object") + mock_msd_dhcp_child_net_attach_object = test_data.get("mock_msd_dhcp_child_net_attach_object") + mock_msd_dhcp_child_net_update_response = test_data.get("mock_msd_dhcp_child_net_update_response") + + # MSD Override test data + playbook_msd_override_config = test_data.get("playbook_msd_override_config") + mock_msd_override_parent_net_object = test_data.get("mock_msd_override_parent_net_object") + mock_msd_override_parent_net_attach_object = test_data.get("mock_msd_override_parent_net_attach_object") + mock_msd_override_attach_response = test_data.get("mock_msd_override_attach_response") + mock_msd_override_child_net_object = test_data.get("mock_msd_override_child_net_object") + mock_msd_override_child_net_attach_object = test_data.get("mock_msd_override_child_net_attach_object") def init_data(self): # Some of the mock data is re-initialized after each test as previous test might have altered portions @@ -423,6 +453,75 @@ def load_fixtures(self, response=None, device=""): self.attach_success_resp, self.deploy_success_resp, ] + + elif "_merged_msd_basic" in self._testMethodName: + self.init_data() + self.run_dcnm_fabric_details.side_effect = [ + self.mock_msd_fabric_details, + self.mock_msd_child_fabric_details, + ] + + self.run_dcnm_ip_sn.side_effect = [ + self.mock_msd_ip_sn, + self.mock_msd_ip_sn, + ] + self.run_dcnm_send.side_effect = [ + self.mock_msd_vrf_object, + self.blank_data, + self.mock_msd_net_create_response, + self.mock_msd_net_attach_response, + self.deploy_success_resp, + self.mock_msd_vrf_object, + self.mock_msd_child_net_object, + self.mock_msd_child_net_attach_object, + self.mock_msd_child_net_update_response, + self.deploy_success_resp, + ] + + elif "_merged_msd_dhcp" in self._testMethodName: + self.init_data() + self.run_dcnm_fabric_details.side_effect = [ + self.mock_msd_fabric_details, + self.mock_msd_child_fabric_details, + ] + + self.run_dcnm_ip_sn.side_effect = [ + self.mock_msd_ip_sn, + self.mock_msd_ip_sn, + ] + self.run_dcnm_send.side_effect = [ + self.mock_msd_vrf_object, + self.blank_data, + self.mock_msd_dhcp_net_create_response, + self.mock_msd_dhcp_net_attach_response, + self.deploy_success_resp, + self.mock_msd_vrf_object, + self.mock_msd_dhcp_child_net_object, + self.mock_msd_dhcp_child_net_attach_object, + self.mock_msd_dhcp_child_net_update_response, + self.deploy_success_resp, + ] + + elif "_msd_override_with_different_attachments" in self._testMethodName: + self.init_data() + self.run_dcnm_fabric_details.side_effect = [ + self.mock_msd_fabric_details, + self.mock_msd_child_fabric_details, + ] + + self.run_dcnm_ip_sn.side_effect = [ + self.mock_msd_ip_sn, + self.mock_msd_ip_sn, + ] + self.run_dcnm_send.side_effect = [ + self.mock_msd_vrf_object, + self.mock_msd_override_parent_net_object, + self.mock_msd_override_attach_response, + self.deploy_success_resp, + self.mock_msd_vrf_object, + self.mock_msd_override_child_net_object, + self.mock_msd_override_child_net_attach_object, + ] else: pass @@ -430,17 +529,17 @@ def test_dcnm_net_blank_fabric(self): set_module_args( dict(state="merged", fabric="test_network", config=self.playbook_config) ) - result = self.execute_module(changed=False, failed=True) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertEqual( result.get("msg"), - "Fabric test_network missing on DCNM or does not have any switches", + "Fabric test_network missing on ND or does not have any switches", ) def test_dcnm_net_get_have_failure(self): set_module_args( dict(state="merged", fabric="test_network", config=self.playbook_config) ) - result = self.execute_module(changed=False, failed=True) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertEqual(result.get("msg"), "Fabric test_network not present on DCNM") def test_dcnm_net_check_mode(self): @@ -452,7 +551,7 @@ def test_dcnm_net_check_mode(self): config=self.playbook_config, ) ) - result = self.execute_module(changed=False, failed=False) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) self.assertTrue(result.get("diff")) self.assertFalse(result.get("response")) @@ -466,7 +565,7 @@ def test_dcnm_net_12check_mode(self): config=self.playbook_config, ) ) - result = self.execute_module(changed=False, failed=False) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) self.version = 11 self.assertTrue(result.get("diff")) self.assertFalse(result.get("response")) @@ -475,7 +574,7 @@ def test_dcnm_net_merged_new(self): set_module_args( dict(state="merged", fabric="test_network", config=self.playbook_config) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual( @@ -487,7 +586,7 @@ def test_dcnm_net_12merged_new(self): set_module_args( dict(state="merged", fabric="test_network", config=self.playbook_config) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.version = 11 self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) @@ -499,7 +598,7 @@ def test_dcnm_net_merged_novlan_new(self): set_module_args( dict(state="merged", fabric="test_network", config=self.playbook_config_novlan) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual( @@ -510,7 +609,7 @@ def test_dcnm_net_error1(self): set_module_args( dict(state="merged", fabric="test_network", config=self.playbook_config) ) - result = self.execute_module(changed=False, failed=True) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertEqual(result["msg"]["RETURN_CODE"], 400) self.assertEqual(result["msg"]["ERROR"], "There is an error") @@ -518,7 +617,7 @@ def test_dcnm_net_error2(self): set_module_args( dict(state="merged", fabric="test_network", config=self.playbook_config) ) - result = self.execute_module(changed=False, failed=True) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertIn( "Entered Network VLAN ID 203 is in use already", str(result["msg"]["DATA"].values()), @@ -528,7 +627,7 @@ def test_dcnm_net_error3(self): set_module_args( dict(state="merged", fabric="test_network", config=self.playbook_config) ) - result = self.execute_module(changed=False, failed=False) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) self.assertEqual( result["response"][2]["DATA"], "No switches PENDING for deployment" ) @@ -537,7 +636,7 @@ def test_dcnm_net_merged_duplicate(self): set_module_args( dict(state="merged", fabric="test_network", config=self.playbook_config) ) - result = self.execute_module(changed=False, failed=False) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")) def test_dcnm_net_merged_with_incorrect_netid(self): @@ -548,7 +647,7 @@ def test_dcnm_net_merged_with_incorrect_netid(self): config=self.playbook_config_incorrect_netid, ) ) - result = self.execute_module(changed=False, failed=True) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertEqual( result.get("msg"), "networkId can not be updated on existing network: test_network", @@ -562,7 +661,7 @@ def test_dcnm_net_merged_with_incorrect_vrf(self): config=self.playbook_config_incorrect_vrf, ) ) - result = self.execute_module(changed=False, failed=True) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertEqual( result.get("msg"), "VRF: ansible-vrf-int2 is missing in fabric: test_network", @@ -574,7 +673,7 @@ def test_dcnm_net_merged_with_update(self): state="merged", fabric="test_network", config=self.playbook_config_update ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual( @@ -593,7 +692,7 @@ def test_dcnm_net_replace_with_changes(self): config=self.playbook_config_replace, ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertEqual(result.get("diff")[0]["vlan_id"], 203) self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) @@ -614,7 +713,7 @@ def test_dcnm_net_replace_with_no_atch(self): config=self.playbook_config_replace_no_atch, ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual(result.get("diff")[0]["net_name"], "test_network") @@ -637,7 +736,7 @@ def test_dcnm_net_replace_with_no_atch(self): # set_module_args( # dict(state="replaced", fabric="test_network", config=self.playbook_config) # ) - # result = self.execute_module(changed=False, failed=False) + # result = self.execute_module(changed=False, failed=False, use_action_plugin=True) # self.assertFalse(result.get("diff")) # self.assertFalse(result.get("response")) @@ -645,14 +744,14 @@ def test_dcnm_vrf_merged_redeploy(self): set_module_args( dict(state="merged", fabric="test_network", config=self.playbook_config) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertEqual(result.get("diff")[0]["net_name"], "test_network") def test_dcnm_net_override_with_additions(self): set_module_args( dict(state="overridden", fabric="test_network", config=self.playbook_config) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual( @@ -680,7 +779,7 @@ def test_dcnm_net_override_with_additions(self): # set_module_args( # dict(state="overridden", fabric="test_network", config=self.playbook_config) # ) - # result = self.execute_module(changed=False, failed=False) + # result = self.execute_module(changed=False, failed=False, use_action_plugin=True) # self.assertFalse(result.get("diff")) # self.assertFalse(result.get("response")) @@ -692,7 +791,7 @@ def test_dcnm_net_override_with_deletions(self): config=self.playbook_config_override, ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual(result.get("diff")[0]["vlan_id"], 303) @@ -722,7 +821,7 @@ def test_dcnm_net_delete_std(self): set_module_args( dict(state="deleted", fabric="test_network", config=self.playbook_config) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual(result.get("diff")[0]["net_name"], "test_network") @@ -739,7 +838,7 @@ def test_dcnm_net_delete_std(self): def test_dcnm_net_delete_without_config(self): set_module_args(dict(state="deleted", fabric="test_network", config=[])) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual(result.get("diff")[0]["net_name"], "test_network") @@ -758,7 +857,7 @@ def test_dcnm_net_query_with_config(self): set_module_args( dict(state="query", fabric="test_network", config=self.playbook_config) ) - result = self.execute_module(changed=False, failed=False) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")) self.assertEqual(result.get("response")[0]["parent"]["networkName"], "test_network") self.assertEqual(result.get("response")[0]["parent"]["networkId"], 9008011) @@ -781,7 +880,7 @@ def test_dcnm_net_query_without_config(self): set_module_args( dict(state="query", fabric="test_network", config=[]) ) - result = self.execute_module(changed=False, failed=False) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")) self.assertEqual(result.get("response")[0]["parent"]["networkName"], "test_network") self.assertEqual(result.get("response")[0]["parent"]["networkId"], 9008011) @@ -805,7 +904,7 @@ def test_dcnm_net_merged_torport_new(self): set_module_args( dict(state="merged", fabric="test_network", config=self.playbook_tor_config) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.version = 11 self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) @@ -817,7 +916,7 @@ def test_dcnm_net_merged_torport_vererror(self): set_module_args( dict(state="merged", fabric="test_network", config=self.playbook_tor_config) ) - result = self.execute_module(changed=False, failed=True) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertEqual( result.get("msg"), "Invalid parameters in playbook: tor_ports configurations are supported only on NDFC", @@ -828,7 +927,7 @@ def test_dcnm_net_merged_torport_roleerror(self): set_module_args( dict(state="merged", fabric="test_network", config=self.playbook_tor_roleerr_config) ) - result = self.execute_module(changed=False, failed=True) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.version = 11 self.assertEqual( result.get("msg"), @@ -842,7 +941,7 @@ def test_dcnm_net_merged_tor_with_update(self): state="merged", fabric="test_network", config=self.playbook_tor_config_update ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.version = 11 self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) @@ -861,7 +960,7 @@ def test_dcnm_net_replace_tor_ports(self): state="replaced", fabric="test_network", config=self.playbook_tor_config_update ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.version = 11 self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) @@ -886,7 +985,7 @@ def test_dcnm_net_override_tor_ports(self): state="overridden", fabric="test_network", config=self.playbook_tor_config_update ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.version = 11 self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) @@ -903,3 +1002,358 @@ def test_dcnm_net_override_tor_ports(self): result.get("diff")[0]["attach"][1]["tor_ports"], "dt-n9k7(Ethernet1/13,Ethernet1/14)" ) self.assertEqual(result.get("diff")[0]["vrf_name"], "ansible-vrf-int1") + + def test_dcnm_net_merged_msd_basic(self): + """ + Test MSD network creation with child fabric configuration. + + This test verifies: + - MSD network creation at parent MSD fabric level + - Network attachment configuration in child fabric + - Child fabric network configuration with DHCP and other parameters + - Proper output structure with parent_fabric and child_fabrics sections + """ + self.version = 12 + set_module_args( + dict(state="merged", fabric="msd-parent", config=self.playbook_msd_config) + ) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) + + # Verify overall result structure + self.assertTrue(result.get("changed")) + self.assertIn("parent_fabric", result) + self.assertIn("child_fabrics", result) + self.assertEqual(result.get("workflow"), "Parent MSD with Child Fabric Processing") + + # Verify parent fabric section + parent = result.get("parent_fabric") + self.assertTrue(parent.get("changed")) + self.assertFalse(parent.get("failed")) + self.assertEqual(parent.get("fabric_name"), "msd-parent") + self.assertIn("diff", parent) + self.assertIn("response", parent) + + # Verify parent fabric diff + parent_diff = parent.get("diff")[0] + self.assertEqual(parent_diff["net_name"], "ansible-msd-net1") + self.assertEqual(parent_diff["vrf_name"], "Tenant-1") + self.assertEqual(parent_diff["net_id"], 8001) + self.assertEqual(parent_diff["vlan_id"], 2101) + self.assertEqual(parent_diff["gw_ip_subnet"], "192.168.101.1/24") + self.assertEqual(parent_diff["int_desc"], "MSD Network managed by Ansible") + self.assertEqual(parent_diff["mtu_l3intf"], 9214) + self.assertFalse(parent_diff["is_l2only"]) + + # Verify parent fabric attachments + self.assertEqual(len(parent_diff["attach"]), 2) + self.assertTrue(parent_diff["attach"][0]["deploy"]) + self.assertTrue(parent_diff["attach"][1]["deploy"]) + + # Verify parent fabric response (create, attach, deploy) + parent_response = parent.get("response") + self.assertEqual(len(parent_response), 3) + + # Verify create response + self.assertEqual(parent_response[0]["RETURN_CODE"], 200) + self.assertEqual(parent_response[0]["METHOD"], "POST") + self.assertIn("Network Id", parent_response[0]["DATA"]) + self.assertEqual(parent_response[0]["DATA"]["Network Id"], 8001) + self.assertEqual(parent_response[0]["DATA"]["Network Name"], "ansible-msd-net1") + + # Verify attachment response + self.assertEqual(parent_response[1]["RETURN_CODE"], 200) + self.assertEqual(parent_response[1]["METHOD"], "POST") + self.assertIn("ansible-msd-net1", str(parent_response[1]["DATA"])) + + # Verify deploy response + self.assertEqual(parent_response[2]["RETURN_CODE"], 200) + self.assertEqual(parent_response[2]["METHOD"], "POST") + + # Verify child fabrics section + child_fabrics = result.get("child_fabrics") + self.assertEqual(len(child_fabrics), 1) + + child = child_fabrics[0] + self.assertTrue(child.get("changed")) + self.assertFalse(child.get("failed")) + self.assertEqual(child.get("fabric_name"), "msd-child-1") + self.assertIn("diff", child) + self.assertIn("response", child) + + # Verify child fabric diff + child_diff = child.get("diff")[0] + self.assertEqual(child_diff["net_name"], "ansible-msd-net1") + self.assertEqual(child_diff["vrf_name"], "Tenant-1") + self.assertEqual(child_diff["net_id"], 8001) + self.assertEqual(child_diff["vlan_id"], 2101) + self.assertEqual(child_diff["gw_ip_subnet"], "192.168.101.1/24") + + # Verify child fabric specific configuration + self.assertEqual(child_diff["dhcp_srvr1_ip"], "192.168.1.101") + self.assertEqual(child_diff["dhcp_srvr1_vrf"], "management") + self.assertEqual(child_diff["dhcp_loopback_id"], 204) + self.assertEqual(child_diff["multicast_group_address"], "239.1.1.1") + self.assertTrue(child_diff["l3gw_on_border"]) + self.assertEqual(child_diff["vlan_nf_monitor"], "monitor1") + + # Verify child fabric response (query response showing OUT-OF-SYNC state) + child_response = child.get("response") + self.assertEqual(len(child_response), 1) + self.assertEqual(child_response[0]["RETURN_CODE"], 200) + self.assertEqual(child_response[0]["METHOD"], "GET") + + # Verify the child fabric response contains network attach information + data = child_response[0]["DATA"] + self.assertIsInstance(data, list) + self.assertEqual(len(data), 1) + self.assertEqual(data[0]["networkName"], "ansible-msd-net1") + self.assertIn("lanAttachList", data[0]) + self.assertEqual(len(data[0]["lanAttachList"]), 2) + + # Verify attachment states + lan_attach = data[0]["lanAttachList"] + self.assertEqual(lan_attach[0]["lanAttachState"], "OUT-OF-SYNC") + self.assertEqual(lan_attach[1]["lanAttachState"], "OUT-OF-SYNC") + + def test_dcnm_net_merged_msd_dhcp(self): + """ + Test MSD network creation with DHCP configuration in child fabric. + + This test verifies: + - MSD network creation with multiple DHCP servers + - DHCP server configuration (IP, VRF) in child fabric + - DHCP loopback ID configuration + - Additional parameters like l3gw_on_border, multicast_group_address, netflow_enable, vlan_nf_monitor + - Proper output structure with parent_fabric and child_fabrics sections + """ + self.version = 12 + set_module_args( + dict(state="merged", fabric="msd-parent", config=self.playbook_msd_dhcp_config) + ) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) + + # Verify overall result structure + self.assertTrue(result.get("changed")) + self.assertIn("parent_fabric", result) + self.assertIn("child_fabrics", result) + self.assertEqual(result.get("workflow"), "Parent MSD with Child Fabric Processing") + + # Verify parent fabric section + parent = result.get("parent_fabric") + self.assertTrue(parent.get("changed")) + self.assertFalse(parent.get("failed")) + self.assertEqual(parent.get("fabric_name"), "msd-parent") + + # Verify parent fabric diff - key network parameters + parent_diff = parent.get("diff")[0] + self.assertEqual(parent_diff["net_name"], "ansible-msd-dhcp-net") + self.assertEqual(parent_diff["vrf_name"], "Tenant-1") + self.assertEqual(parent_diff["net_id"], 8004) + self.assertEqual(parent_diff["vlan_id"], 2104) + self.assertEqual(parent_diff["gw_ip_subnet"], "192.168.104.1/24") + self.assertEqual(parent_diff["int_desc"], "MSD DHCP Network") + self.assertEqual(parent_diff["mtu_l3intf"], 9214) + + # Verify parent fabric does NOT have DHCP config (should be empty/false) + self.assertEqual(parent_diff["dhcp_srvr1_ip"], "") + self.assertEqual(parent_diff["dhcp_loopback_id"], "") + self.assertFalse(parent_diff["l3gw_on_border"]) + + # Verify parent fabric attachments + self.assertEqual(len(parent_diff["attach"]), 2) + self.assertEqual(parent_diff["attach"][0]["ip_address"], "192.168.10.203") + self.assertEqual(parent_diff["attach"][1]["ip_address"], "192.168.10.204") + + # Verify parent fabric response + parent_response = parent.get("response") + self.assertEqual(len(parent_response), 3) + self.assertEqual(parent_response[0]["DATA"]["Network Id"], 8004) + self.assertEqual(parent_response[0]["DATA"]["Network Name"], "ansible-msd-dhcp-net") + self.assertIn("9R518K2AT3R", str(parent_response[1]["DATA"])) + self.assertIn("915KQ8P3NS8", str(parent_response[1]["DATA"])) + + # Verify child fabrics section + child_fabrics = result.get("child_fabrics") + self.assertEqual(len(child_fabrics), 1) + + child = child_fabrics[0] + self.assertTrue(child.get("changed")) + self.assertFalse(child.get("failed")) + self.assertEqual(child.get("fabric_name"), "msd-child-1") + + # Verify child fabric diff - DHCP configuration is present + child_diff = child.get("diff")[0] + self.assertEqual(child_diff["net_name"], "ansible-msd-dhcp-net") + self.assertEqual(child_diff["vrf_name"], "Tenant-1") + self.assertEqual(child_diff["net_id"], "8004") + self.assertEqual(child_diff["vlan_id"], 2104) + + # Verify DHCP servers configuration + self.assertEqual(child_diff["dhcp_srvr1_ip"], "192.168.1.102") + self.assertEqual(child_diff["dhcp_srvr1_vrf"], "management") + self.assertEqual(child_diff["dhcp_srvr2_ip"], "192.168.1.105") + self.assertEqual(child_diff["dhcp_srvr2_vrf"], "default") + self.assertEqual(child_diff["dhcp_srvr3_ip"], "192.168.1.106") + self.assertEqual(child_diff["dhcp_srvr3_vrf"], "management") + + # Verify child-specific parameters + self.assertEqual(child_diff["dhcp_loopback_id"], 207) + self.assertEqual(child_diff["multicast_group_address"], "239.1.1.4") + self.assertTrue(child_diff["l3gw_on_border"]) + self.assertFalse(child_diff["netflow_enable"]) + self.assertEqual(child_diff["vlan_nf_monitor"], "monitor2") + + # Verify child fabric response + child_response = child.get("response") + self.assertEqual(len(child_response), 1) + self.assertEqual(child_response[0]["RETURN_CODE"], 200) + self.assertEqual(child_response[0]["METHOD"], "GET") + + # Verify child fabric attachment data + attach_data = child_response[0]["DATA"] + self.assertEqual(len(attach_data), 2) + self.assertEqual(attach_data[0]["serialNumber"], "9R518K2AT3R") + self.assertEqual(attach_data[0]["ipAddress"], "192.168.10.203") + self.assertEqual(attach_data[0]["lanAttachedState"], "DEPLOYED") + self.assertEqual(attach_data[1]["serialNumber"], "915KQ8P3NS8") + self.assertEqual(attach_data[1]["ipAddress"], "192.168.10.204") + self.assertEqual(attach_data[1]["lanAttachedState"], "DEPLOYED") + + def test_dcnm_net_msd_override_with_different_attachments(self): + """ + Test MSD network override with different attachments. + + This test verifies: + - MSD network exists with old attachments (Ethernet1/17,Ethernet1/18) + - Override state updates attachments to new ports (Ethernet1/16,Ethernet1/17) + - Parent fabric processes attachment updates correctly + - Child fabric reflects the updated attachment configuration + - Proper detachment of old ports and attachment of new ports + """ + self.version = 12 + set_module_args( + dict(state="overridden", fabric="msd-parent", config=self.playbook_msd_override_config) + ) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) + + # Verify overall result structure + self.assertTrue(result.get("changed")) + self.assertIn("parent_fabric", result) + self.assertIn("child_fabrics", result) + self.assertEqual(result.get("workflow"), "Parent MSD with Child Fabric Processing") + + # Verify parent fabric section + parent = result.get("parent_fabric") + self.assertTrue(parent.get("changed")) + self.assertFalse(parent.get("failed")) + self.assertEqual(parent.get("fabric_name"), "msd-parent") + self.assertIn("diff", parent) + self.assertIn("response", parent) + + # Verify parent fabric diff shows updated attachments (minimal structure) + parent_diff = parent.get("diff")[0] + self.assertEqual(parent_diff["net_name"], "ansible-msd-dhcp-net") + self.assertIn("attach", parent_diff) + + # Verify parent fabric updated attachments - NEW PORTS + self.assertEqual(len(parent_diff["attach"]), 2) + + # Check first switch attachment (leaf3) has new ports + attach1 = parent_diff["attach"][0] + self.assertEqual(attach1["ip_address"], "192.168.10.203") + self.assertEqual(attach1["ports"], "Ethernet1/16,Ethernet1/17") + self.assertTrue(attach1["deploy"]) + + # Check second switch attachment (leaf4) has new ports + attach2 = parent_diff["attach"][1] + self.assertEqual(attach2["ip_address"], "192.168.10.204") + self.assertEqual(attach2["ports"], "Ethernet1/16,Ethernet1/17") + self.assertTrue(attach2["deploy"]) + + # Verify parent fabric response includes attachment update and deploy + parent_response = parent.get("response") + self.assertEqual(len(parent_response), 2) + + # Verify attachment update response (index 0) + attach_resp = parent_response[0] + self.assertEqual(attach_resp["RETURN_CODE"], 200) + self.assertEqual(attach_resp["METHOD"], "POST") + self.assertIn("ansible-msd-dhcp-net-[9R518K2AT3R/leaf3]", attach_resp["DATA"]) + self.assertEqual(attach_resp["DATA"]["ansible-msd-dhcp-net-[9R518K2AT3R/leaf3]"], "SUCCESS") + self.assertIn("ansible-msd-dhcp-net-[915KQ8P3NS8/leaf4]", attach_resp["DATA"]) + self.assertEqual(attach_resp["DATA"]["ansible-msd-dhcp-net-[915KQ8P3NS8/leaf4]"], "SUCCESS") + + # Verify deploy response (index 1) + deploy_resp = parent_response[1] + self.assertEqual(deploy_resp["RETURN_CODE"], 200) + self.assertEqual(deploy_resp["METHOD"], "POST") + self.assertIn("status", deploy_resp["DATA"]) + + # Verify child fabrics section + child_fabrics = result.get("child_fabrics") + self.assertEqual(len(child_fabrics), 1) + + child = child_fabrics[0] + self.assertTrue(child.get("changed")) + self.assertFalse(child.get("failed")) + self.assertEqual(child.get("fabric_name"), "msd-child-1") + self.assertIn("diff", child) + self.assertIn("response", child) + + # Verify child fabric diff - includes all network parameters + child_diff = child.get("diff")[0] + self.assertEqual(child_diff["net_name"], "ansible-msd-dhcp-net") + self.assertEqual(child_diff["vrf_name"], "Tenant-1") + self.assertEqual(child_diff["net_id"], 8004) + self.assertEqual(child_diff["vlan_id"], 2104) + self.assertEqual(child_diff["gw_ip_subnet"], "192.168.104.1/24") + self.assertEqual(child_diff["net_template"], "Default_Network_Universal") + self.assertEqual(child_diff["net_extension_template"], "Default_Network_Extension_Universal") + self.assertFalse(child_diff["is_l2only"]) + self.assertEqual(child_diff["int_desc"], "MSD DHCP Network") + self.assertEqual(child_diff["mtu_l3intf"], 9216) + self.assertFalse(child_diff["route_target_both"]) + self.assertFalse(child_diff["l3gw_on_border"]) + + # Verify child fabric has empty attach list (attachments shown in response only) + self.assertEqual(len(child_diff["attach"]), 0) + + # Verify child fabric response shows IN PROGRESS state + child_response = child.get("response") + self.assertEqual(len(child_response), 1) + self.assertEqual(child_response[0]["RETURN_CODE"], 200) + self.assertEqual(child_response[0]["METHOD"], "GET") + + # Verify child fabric attachment data reflects new port configuration + child_attach_data = child_response[0]["DATA"] + self.assertEqual(len(child_attach_data), 1) + self.assertEqual(child_attach_data[0]["networkName"], "ansible-msd-dhcp-net") + self.assertIn("lanAttachList", child_attach_data[0]) + + # Verify lanAttachList has 2 switches + lan_attach_list = child_attach_data[0]["lanAttachList"] + self.assertEqual(len(lan_attach_list), 2) + + # Verify first switch in child fabric (leaf3) + leaf3_attach = lan_attach_list[0] + self.assertEqual(leaf3_attach["switchSerialNo"], "9R518K2AT3R") + self.assertEqual(leaf3_attach["ipAddress"], "192.168.10.203") + self.assertEqual(leaf3_attach["switchName"], "leaf3") + self.assertEqual(leaf3_attach["fabricName"], "msd-child-1") + self.assertEqual(leaf3_attach["networkId"], 8004) + self.assertEqual(leaf3_attach["vlanId"], 2104) + self.assertEqual(leaf3_attach["portNames"], "Ethernet1/16,Ethernet1/17") + self.assertEqual(leaf3_attach["lanAttachState"], "IN PROGRESS") + self.assertTrue(leaf3_attach["isLanAttached"]) + + # Verify second switch in child fabric (leaf4) + leaf4_attach = lan_attach_list[1] + self.assertEqual(leaf4_attach["switchSerialNo"], "915KQ8P3NS8") + self.assertEqual(leaf4_attach["ipAddress"], "192.168.10.204") + self.assertEqual(leaf4_attach["switchName"], "leaf4") + self.assertEqual(leaf4_attach["fabricName"], "msd-child-1") + self.assertEqual(leaf4_attach["networkId"], 8004) + self.assertEqual(leaf4_attach["vlanId"], 2104) + self.assertEqual(leaf4_attach["portNames"], "Ethernet1/16,Ethernet1/17") + self.assertEqual(leaf4_attach["lanAttachState"], "IN PROGRESS") + self.assertTrue(leaf4_attach["isLanAttached"]) diff --git a/tests/unit/modules/dcnm/test_dcnm_vrf.py b/tests/unit/modules/dcnm/test_dcnm_vrf.py index c437bb866..4c2215f16 100644 --- a/tests/unit/modules/dcnm/test_dcnm_vrf.py +++ b/tests/unit/modules/dcnm/test_dcnm_vrf.py @@ -42,6 +42,8 @@ class TestDcnmVrfModule(TestDcnmModule): fabric_details = test_data.get("fabric_details") fabric_details_mfd = test_data.get("fabric_details_mfd") fabric_details_vxlan = test_data.get("fabric_details_vxlan") + fabric_associations = test_data.get("mock_fabric_associations") + vrf_ready_data = test_data.get("mock_vrf_get_object") mock_net_from_vrf_empty = test_data.get("mock_net_from_vrf_empty") mock_vrf_attach_object_del_not_ready = test_data.get( @@ -49,7 +51,12 @@ class TestDcnmVrfModule(TestDcnmModule): ) mock_vrf_attach_object_del_oos = test_data.get("mock_vrf_attach_object_del_oos") mock_vrf_attach_object_del_ready = test_data.get("mock_vrf_attach_object_del_ready") + mock_msd_vrf_attach_object_del_not_ready = test_data.get("mock_msd_vrf_attach_object_del_not_ready") + mock_msd_vrf_attach_object_del_ready = test_data.get("mock_msd_vrf_attach_object_del_ready") + msd_attach_success_resp = test_data.get("msd_attach_success_resp") + msd_attach_success_resp_2 = test_data.get("msd_attach_success_resp_2") + update_success_rep = test_data.get("update_data") attach_success_resp = test_data.get("attach_success_resp") attach_success_resp2 = test_data.get("attach_success_resp2") attach_success_resp3 = test_data.get("attach_success_resp3") @@ -61,16 +68,41 @@ class TestDcnmVrfModule(TestDcnmModule): delete_success_resp = test_data.get("delete_success_resp") blank_data = test_data.get("blank_data") + # Action plugin fixtures + # mock_fabric_associations = action_test_data.get("mock_fabric_associations") + # mock_fabric_associations_dict = action_test_data.get("mock_fabric_associations_dict") + # mock_standalone_fabric_details = action_test_data.get("mock_standalone_fabric_details") + # mock_parent_fabric_details = action_test_data.get("mock_parent_fabric_details") + # mock_child_fabric_details = action_test_data.get("mock_child_fabric_details") def init_data(self): # Some of the mock data is re-initialized after each test as previous test might have altered portions # of the mock data. self.mock_sn_fab_dict = copy.deepcopy(self.test_data.get("mock_sn_fab")) + self.mock_sn_fab_msd_dict = copy.deepcopy(self.test_data.get("mock_sn_fab_msd")) self.mock_vrf_object = copy.deepcopy(self.test_data.get("mock_vrf_object")) self.mock_vrf12_object = copy.deepcopy(self.test_data.get("mock_vrf12_object")) + self.mock_msd_vrf_object = copy.deepcopy( + self.test_data.get("mock_msd_vrf_object") + ) + self.mock_msd_vrf_object_2 = copy.deepcopy( + self.test_data.get("mock_msd_vrf_object_2") + ) + self.mock_msd_parent_vrf_object = copy.deepcopy( + self.test_data.get("mock_msd_parent_vrf_object") + ) self.mock_vrf_attach_object = copy.deepcopy( self.test_data.get("mock_vrf_attach_object") ) + self.mock_msd_vrf_attach_object = copy.deepcopy( + self.test_data.get("mock_msd_vrf_attach_object") + ) + self.mock_msd_vrf_child_attach_object = copy.deepcopy( + self.test_data.get("mock_msd_vrf_child_attach_object") + ) + self.mock_msd_vrf_child_attach_object_2 = copy.deepcopy( + self.test_data.get("mock_msd_vrf_child_attach_object_2") + ) self.mock_vrf_attach_object_query = copy.deepcopy( self.test_data.get("mock_vrf_attach_object_query") ) @@ -80,6 +112,12 @@ def init_data(self): self.mock_vrf_attach_object2_query = copy.deepcopy( self.test_data.get("mock_vrf_attach_object2_query") ) + self.mock_msd_vrf_parent_attach_object_query = copy.deepcopy( + self.test_data.get("mock_msd_vrf_parent_attach_object_query") + ) + self.mock_msd_vrf_child_attach_object_query = copy.deepcopy( + self.test_data.get("mock_msd_vrf_child_attach_object_query") + ) self.mock_vrf_attach_object_pending = copy.deepcopy( self.test_data.get("mock_vrf_attach_object_pending") ) @@ -107,19 +145,33 @@ def init_data(self): self.mock_vrf_attach_get_ext_object_merge_att4_only = copy.deepcopy( self.test_data.get("mock_vrf_attach_get_ext_object_merge_att4_only") ) + self.mock_vrf_msd_attach_get_ext_object_dcnm_att1_only = copy.deepcopy( + self.test_data.get("mock_vrf_msd_attach_get_ext_object_dcnm_att1_only") + ) self.mock_vrf_attach_get_ext_object_ov_att1_only = copy.deepcopy( self.test_data.get("mock_vrf_attach_get_ext_object_ov_att1_only") ) self.mock_vrf_attach_get_ext_object_ov_att2_only = copy.deepcopy( self.test_data.get("mock_vrf_attach_get_ext_object_ov_att2_only") ) + self.mock_vrf_msd_attach_get_ext_object_ov_att1_only = copy.deepcopy( + self.test_data.get("mock_vrf_msd_attach_get_ext_object_ov_att1_only") + ) + self.mock_msd_vrf_attach_get_ext_object_merge_att1_only = copy.deepcopy( + self.test_data.get("mock_msd_vrf_attach_get_ext_object_merge_att1_only") + ) self.mock_vrf_attach_lite_object = copy.deepcopy( self.test_data.get("mock_vrf_attach_lite_object") ) self.mock_vrf_lite_obj = copy.deepcopy(self.test_data.get("mock_vrf_lite_obj")) + self.mock_msd_vrf_lite_obj = copy.deepcopy(self.test_data.get("mock_msd_vrf_lite_obj")) + self.mock_msd_vrf_lite_obj_2 = copy.deepcopy(self.test_data.get("mock_msd_vrf_lite_obj_2")) self.mock_pools_top_down_vrf_vlan = copy.deepcopy( self.test_data.get("mock_pools_top_down_vrf_vlan") ) + self.mock_pools_top_down_dot1q = copy.deepcopy( + self.test_data.get("mock_pools_top_down_dot1q") + ) def setUp(self): super(TestDcnmVrfModule, self).setUp() @@ -172,9 +224,9 @@ def load_fixtures(self, response=None, device=""): if "vrf_blank_fabric" in self._testMethodName: self.run_dcnm_ip_sn.side_effect = [{}] else: - self.run_dcnm_ip_sn.side_effect = [self.vrf_inv_data] + self.run_dcnm_ip_sn.side_effect = [self.vrf_inv_data, self.vrf_inv_data] - self.run_dcnm_fabric_details.side_effect = [self.fabric_details] + self.run_dcnm_fabric_details.side_effect = [self.fabric_details, self.fabric_details] if "get_have_failure" in self._testMethodName: self.run_dcnm_send.side_effect = [self.get_have_failure] @@ -459,6 +511,7 @@ def load_fixtures(self, response=None, device=""): self.mock_vrf_attach_object_del_ready, self.delete_success_resp, self.mock_pools_top_down_vrf_vlan, + self.mock_pools_top_down_dot1q, self.blank_data, self.attach_success_resp2, self.deploy_success_resp, @@ -497,6 +550,7 @@ def load_fixtures(self, response=None, device=""): self.mock_vrf_attach_object_del_ready, self.delete_success_resp, self.mock_pools_top_down_vrf_vlan, + self.mock_pools_top_down_dot1q, ] elif "delete_std_lite" in self._testMethodName: @@ -550,9 +604,10 @@ def load_fixtures(self, response=None, device=""): obj2, self.delete_success_resp, self.mock_pools_top_down_vrf_vlan, + self.mock_pools_top_down_dot1q, ] - elif "query" in self._testMethodName: + elif "vrf_query" in self._testMethodName: self.init_data() self.run_dcnm_get_url.side_effect = [self.mock_vrf_attach_object] self.run_dcnm_send.side_effect = [ @@ -610,30 +665,150 @@ def load_fixtures(self, response=None, device=""): self.deploy_success_resp, ] + elif "_msd_merged" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_msd_dict, self.mock_sn_fab_msd_dict] + self.run_dcnm_get_url.side_effect = [self.mock_msd_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.blank_data, + self.blank_data, + self.msd_attach_success_resp, + self.deploy_success_resp, + self.mock_msd_vrf_object, + self.mock_msd_vrf_lite_obj, + self.update_success_rep, + self.deploy_success_resp + ] + + elif "_msd_replaced" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_msd_dict, self.mock_sn_fab_msd_dict] + self.run_dcnm_get_url.side_effect = [self.mock_msd_vrf_attach_object, self.mock_msd_vrf_child_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_msd_vrf_object, + self.mock_msd_vrf_lite_obj, + self.update_success_rep, + self.deploy_success_resp, + self.mock_msd_vrf_object, + self.mock_msd_vrf_lite_obj, + self.update_success_rep, + self.deploy_success_resp + ] + + elif "_msd_merged_nochild" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_msd_dict] + # self.run_dcnm_get_url.side_effect = [self.mock_msd_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.blank_data, + self.blank_data, + self.msd_attach_success_resp, + self.deploy_success_resp + ] + + elif "msd_delete" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_msd_dict] + self.run_dcnm_get_url.side_effect = [self.mock_msd_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_msd_parent_vrf_object, + self.mock_vrf_msd_attach_get_ext_object_dcnm_att1_only, + self.mock_net_from_vrf_empty, + self.msd_attach_success_resp, + self.deploy_success_resp, + self.mock_msd_vrf_attach_object_del_not_ready, + self.mock_msd_vrf_attach_object_del_ready, + self.delete_success_resp, + self.mock_pools_top_down_vrf_vlan, + self.mock_pools_top_down_dot1q, + ] + + elif "msd_override" in self._testMethodName: + self.init_data() + self.run_dcnm_sn_fab.side_effect = [self.mock_sn_fab_msd_dict, self.mock_sn_fab_msd_dict] + self.run_dcnm_get_url.side_effect = [self.mock_msd_vrf_attach_object, self.mock_msd_vrf_child_attach_object_2] + self.run_dcnm_send.side_effect = [ + self.mock_msd_parent_vrf_object, + self.mock_vrf_msd_attach_get_ext_object_ov_att1_only, + self.mock_net_from_vrf_empty, + self.msd_attach_success_resp, + self.deploy_success_resp, + self.mock_msd_vrf_attach_object_del_not_ready, + self.mock_msd_vrf_attach_object_del_ready, + self.delete_success_resp, + self.mock_pools_top_down_vrf_vlan, + self.mock_pools_top_down_dot1q, + self.blank_data, + self.msd_attach_success_resp_2, + self.deploy_success_resp, + self.mock_msd_vrf_object_2, + self.mock_msd_vrf_lite_obj_2, + self.update_success_rep, + self.deploy_success_resp + ] + + elif "nochild_msd_query" in self._testMethodName: + self.init_data() + self.run_dcnm_get_url.side_effect = [self.mock_msd_vrf_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_msd_parent_vrf_object, + self.mock_msd_vrf_attach_get_ext_object_merge_att1_only, + self.mock_msd_parent_vrf_object, + self.mock_msd_vrf_parent_attach_object_query, + self.mock_msd_vrf_attach_get_ext_object_merge_att1_only, + ] + + elif "vrf_msd_query" in self._testMethodName: + self.init_data() + self.run_dcnm_get_url.side_effect = [self.mock_msd_vrf_attach_object, self.mock_msd_vrf_child_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_msd_parent_vrf_object, + self.mock_msd_vrf_attach_get_ext_object_merge_att1_only, + self.mock_msd_parent_vrf_object, + self.mock_msd_vrf_parent_attach_object_query, + self.mock_msd_vrf_attach_get_ext_object_merge_att1_only, + self.mock_msd_vrf_object, + self.mock_msd_vrf_attach_get_ext_object_merge_att1_only, + self.mock_msd_vrf_object, + self.mock_msd_vrf_child_attach_object_query, + self.mock_msd_vrf_attach_get_ext_object_merge_att1_only, + ] + + elif "child_msd_query" in self._testMethodName: + self.init_data() + self.run_dcnm_get_url.side_effect = [self.mock_msd_vrf_child_attach_object] + self.run_dcnm_send.side_effect = [ + self.mock_msd_vrf_object, + self.mock_msd_vrf_attach_get_ext_object_merge_att1_only, + self.mock_msd_vrf_object, + self.mock_msd_vrf_child_attach_object_query, + self.mock_msd_vrf_attach_get_ext_object_merge_att1_only, + ] + else: pass def test_dcnm_vrf_blank_fabric(self): playbook = self.test_data.get("playbook_config") - set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) - result = self.execute_module(changed=False, failed=True) + set_module_args(dict(state="merged", fabric="test_fabrics", config=playbook)) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertEqual( result.get("msg"), - "Fabric test_fabric missing on the controller or does not have any switches", + "Fabric 'test_fabrics' not found in NDFC.", ) def test_dcnm_vrf_get_have_failure(self): playbook = self.test_data.get("playbook_config") - set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) - result = self.execute_module(changed=False, failed=True) + set_module_args(dict(state="merged", fabric="standalone_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertEqual( - result.get("msg"), "caller: get_have. Fabric test_fabric not present on the controller" + result.get("msg"), "caller: get_have. Fabric standalone_fabric not present on the controller" ) def test_dcnm_vrf_merged_redeploy(self): playbook = self.test_data.get("playbook_config") - set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) - result = self.execute_module(changed=True, failed=False) + set_module_args(dict(state="merged", fabric="standalone_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") def test_dcnm_vrf_merged_lite_redeploy_interface_with_extensions(self): @@ -643,11 +818,11 @@ def test_dcnm_vrf_merged_lite_redeploy_interface_with_extensions(self): set_module_args( dict( state="merged", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") def test_dcnm_vrf_merged_lite_redeploy_interface_without_extensions(self): @@ -657,11 +832,11 @@ def test_dcnm_vrf_merged_lite_redeploy_interface_without_extensions(self): set_module_args( dict( state="merged", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=False, failed=True) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertFalse(result.get("changed")) self.assertTrue(result.get("failed")) @@ -671,18 +846,18 @@ def test_dcnm_vrf_check_mode(self): dict( _ansible_check_mode=True, state="merged", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=False, failed=False) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")) self.assertFalse(result.get("response")) def test_dcnm_vrf_merged_new(self): playbook = self.test_data.get("playbook_config") - set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) - result = self.execute_module(changed=True, failed=False) + set_module_args(dict(state="merged", fabric="standalone_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual( @@ -708,11 +883,11 @@ def test_dcnm_vrf_merged_lite_new_interface_with_extensions(self): set_module_args( dict( state="merged", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual( @@ -738,18 +913,18 @@ def test_dcnm_vrf_merged_lite_new_interface_without_extensions(self): set_module_args( dict( state="merged", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=False, failed=True) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertFalse(result.get("changed")) self.assertTrue(result.get("failed")) def test_dcnm_vrf_merged_duplicate(self): playbook = self.test_data.get("playbook_config") - set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) - result = self.execute_module(changed=False, failed=False) + set_module_args(dict(state="merged", fabric="standalone_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")) def test_dcnm_vrf_merged_lite_duplicate(self): @@ -757,11 +932,11 @@ def test_dcnm_vrf_merged_lite_duplicate(self): set_module_args( dict( state="merged", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=False, failed=False) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")) def test_dcnm_vrf_merged_with_incorrect_vrfid(self): @@ -769,14 +944,14 @@ def test_dcnm_vrf_merged_with_incorrect_vrfid(self): set_module_args( dict( state="merged", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=False, failed=True) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertEqual( result.get("msg"), - "DcnmVrf.diff_for_create: vrf_id for vrf test_vrf_1 cannot be updated to a different value", + "Pre-validation failed", ) def test_dcnm_vrf_merged_lite_invalidrole(self): @@ -784,11 +959,11 @@ def test_dcnm_vrf_merged_lite_invalidrole(self): set_module_args( dict( state="merged", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=False, failed=True) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) msg = "DcnmVrf.update_attach_params_extension_values: " msg += "caller: update_attach_params. " msg += "VRF LITE attachments are appropriate only for switches " @@ -799,8 +974,8 @@ def test_dcnm_vrf_merged_lite_invalidrole(self): def test_dcnm_vrf_merged_with_update(self): playbook = self.test_data.get("playbook_config_update") - set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) - result = self.execute_module(changed=True, failed=False) + set_module_args(dict(state="merged", fabric="standalone_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertEqual( result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.226" @@ -814,11 +989,11 @@ def test_dcnm_vrf_merged_lite_update_interface_with_extensions(self): set_module_args( dict( state="merged", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertEqual( result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.228" @@ -832,11 +1007,11 @@ def test_dcnm_vrf_merged_lite_update_interface_without_extensions(self): set_module_args( dict( state="merged", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=False, failed=True) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertFalse(result.get("changed")) self.assertTrue(result.get("failed")) @@ -845,11 +1020,11 @@ def test_dcnm_vrf_merged_with_update_vlan(self): set_module_args( dict( state="merged", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual( @@ -877,11 +1052,11 @@ def test_dcnm_vrf_merged_lite_vlan_update_interface_with_extensions(self): set_module_args( dict( state="merged", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertEqual( result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.228" @@ -904,25 +1079,25 @@ def test_dcnm_vrf_merged_lite_vlan_update_interface_without_extensions(self): set_module_args( dict( state="merged", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=False, failed=True) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertFalse(result.get("changed")) self.assertTrue(result.get("failed")) def test_dcnm_vrf_error1(self): playbook = self.test_data.get("playbook_config") - set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) - result = self.execute_module(changed=False, failed=True) + set_module_args(dict(state="merged", fabric="standalone_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertEqual(result["msg"]["RETURN_CODE"], 400) self.assertEqual(result["msg"]["ERROR"], "There is an error") def test_dcnm_vrf_error2(self): playbook = self.test_data.get("playbook_config") - set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) - result = self.execute_module(changed=False, failed=True) + set_module_args(dict(state="merged", fabric="standalone_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertIn( "Entered VRF VLAN ID 203 is in use already", str(result["msg"]["DATA"].values()), @@ -930,8 +1105,8 @@ def test_dcnm_vrf_error2(self): def test_dcnm_vrf_error3(self): playbook = self.test_data.get("playbook_config") - set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) - result = self.execute_module(changed=False, failed=False) + set_module_args(dict(state="merged", fabric="standalone_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertEqual( result["response"][2]["DATA"], "No switches PENDING for deployment" ) @@ -941,11 +1116,11 @@ def test_dcnm_vrf_replace_with_changes(self): set_module_args( dict( state="replaced", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 203) @@ -966,11 +1141,11 @@ def test_dcnm_vrf_replace_lite_changes_interface_with_extension_values(self): set_module_args( dict( state="replaced", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 202) @@ -989,11 +1164,11 @@ def test_dcnm_vrf_replace_lite_changes_interface_without_extensions(self): set_module_args( dict( state="replaced", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=False, failed=True) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertFalse(result.get("changed")) self.assertTrue(result.get("failed")) @@ -1002,11 +1177,11 @@ def test_dcnm_vrf_replace_with_no_atch(self): set_module_args( dict( state="replaced", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], "202") @@ -1027,11 +1202,11 @@ def test_dcnm_vrf_replace_lite_no_atch(self): set_module_args( dict( state="replaced", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], "202") @@ -1049,8 +1224,8 @@ def test_dcnm_vrf_replace_lite_no_atch(self): def test_dcnm_vrf_replace_without_changes(self): playbook = self.test_data.get("playbook_config") - set_module_args(dict(state="replaced", fabric="test_fabric", config=playbook)) - result = self.execute_module(changed=False, failed=False) + set_module_args(dict(state="replaced", fabric="standalone_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")) self.assertFalse(result.get("response")) @@ -1059,11 +1234,11 @@ def test_dcnm_vrf_replace_lite_without_changes(self): set_module_args( dict( state="replaced", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=False, failed=False) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")) self.assertFalse(result.get("response")) @@ -1074,11 +1249,11 @@ def test_dcnm_vrf_lite_override_with_additions_interface_with_extensions(self): set_module_args( dict( state="overridden", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual( @@ -1104,11 +1279,11 @@ def test_dcnm_vrf_lite_override_with_additions_interface_without_extensions(self set_module_args( dict( state="overridden", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=False, failed=True) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertFalse(result.get("changed")) self.assertTrue(result.get("failed")) @@ -1117,11 +1292,11 @@ def test_dcnm_vrf_override_with_deletions(self): set_module_args( dict( state="overridden", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 303) @@ -1144,10 +1319,10 @@ def test_dcnm_vrf_override_with_deletions(self): self.assertEqual(result["response"][1]["DATA"]["status"], "") self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) self.assertEqual( - result["response"][5]["DATA"]["test-vrf-2--XYZKSJHSMK2(leaf2)"], "SUCCESS" + result["response"][6]["DATA"]["test-vrf-2--XYZKSJHSMK2(leaf2)"], "SUCCESS" ) self.assertEqual( - result["response"][5]["DATA"]["test-vrf-2--XYZKSJHSMK3(leaf3)"], "SUCCESS" + result["response"][6]["DATA"]["test-vrf-2--XYZKSJHSMK3(leaf3)"], "SUCCESS" ) def test_dcnm_vrf_lite_override_with_deletions_interface_with_extensions(self): @@ -1157,11 +1332,11 @@ def test_dcnm_vrf_lite_override_with_deletions_interface_with_extensions(self): set_module_args( dict( state="overridden", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], 202) @@ -1183,18 +1358,18 @@ def test_dcnm_vrf_lite_override_with_deletions_interface_without_extensions(self set_module_args( dict( state="overridden", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=False, failed=True) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) self.assertFalse(result.get("changed")) self.assertTrue(result.get("failed")) def test_dcnm_vrf_override_without_changes(self): playbook = self.test_data.get("playbook_config") - set_module_args(dict(state="overridden", fabric="test_fabric", config=playbook)) - result = self.execute_module(changed=False, failed=False) + set_module_args(dict(state="overridden", fabric="standalone_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")) self.assertFalse(result.get("response")) @@ -1203,18 +1378,18 @@ def test_dcnm_vrf_override_no_changes_lite(self): set_module_args( dict( state="overridden", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=False, failed=False) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")) self.assertFalse(result.get("response")) def test_dcnm_vrf_delete_std(self): playbook = self.test_data.get("playbook_config") - set_module_args(dict(state="deleted", fabric="test_fabric", config=playbook)) - result = self.execute_module(changed=True, failed=False) + set_module_args(dict(state="deleted", fabric="standalone_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], "202") @@ -1236,11 +1411,11 @@ def test_dcnm_vrf_delete_std_lite(self): set_module_args( dict( state="deleted", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=True, failed=False) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], "202") @@ -1258,8 +1433,8 @@ def test_dcnm_vrf_delete_std_lite(self): self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) def test_dcnm_vrf_delete_dcnm_only(self): - set_module_args(dict(state="deleted", fabric="test_fabric", config=[])) - result = self.execute_module(changed=True, failed=False) + set_module_args(dict(state="deleted", fabric="standalone_fabric", config=[])) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) self.assertFalse(result.get("diff")[0]["attach"][1]["deploy"]) self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], "402") @@ -1278,15 +1453,15 @@ def test_dcnm_vrf_delete_dcnm_only(self): def test_dcnm_vrf_delete_failure(self): playbook = self.test_data.get("playbook_config") - set_module_args(dict(state="deleted", fabric="test_fabric", config=playbook)) - result = self.execute_module(changed=False, failed=True) + set_module_args(dict(state="deleted", fabric="standalone_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) msg = "DcnmVrf.push_diff_delete: Deletion of vrfs test_vrf_1 has failed" self.assertEqual(result["msg"]["response"][2], msg) def test_dcnm_vrf_query(self): playbook = self.test_data.get("playbook_config") - set_module_args(dict(state="query", fabric="test_fabric", config=playbook)) - result = self.execute_module(changed=False, failed=False) + set_module_args(dict(state="query", fabric="standalone_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")) self.assertEqual(result.get("response")[0]["parent"]["vrfName"], "test_vrf_1") self.assertEqual(result.get("response")[0]["parent"]["vrfId"], 9008011) @@ -1316,11 +1491,11 @@ def test_dcnm_vrf_query_vrf_lite(self): set_module_args( dict( state="query", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=False, failed=False) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")) self.assertEqual(result.get("response")[0]["parent"]["vrfName"], "test_vrf_1") self.assertEqual(result.get("response")[0]["parent"]["vrfId"], 9008011) @@ -1358,8 +1533,8 @@ def test_dcnm_vrf_query_vrf_lite(self): ) def test_dcnm_vrf_query_lite_without_config(self): - set_module_args(dict(state="query", fabric="test_fabric", config=[])) - result = self.execute_module(changed=False, failed=False) + set_module_args(dict(state="query", fabric="standalone_fabric", config=[])) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) self.assertFalse(result.get("diff")) self.assertEqual(result.get("response")[0]["parent"]["vrfName"], "test_vrf_1") self.assertEqual(result.get("response")[0]["parent"]["vrfId"], 9008011) @@ -1401,19 +1576,16 @@ def test_dcnm_vrf_validation(self): set_module_args( dict( state="merged", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=False, failed=True) - msg = "DcnmVrf.validate_input: " - msg += "vrf_name is mandatory under vrf parameters," - msg += "ip_address is mandatory under attach parameters" - self.assertEqual(result["msg"], msg) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) + self.assertEqual(result["msg"], "Pre-validation failed") def test_dcnm_vrf_validation_no_config(self): - set_module_args(dict(state="merged", fabric="test_fabric", config=[])) - result = self.execute_module(changed=False, failed=True) + set_module_args(dict(state="merged", fabric="standalone_fabric", config=[])) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) msg = "DcnmVrf.validate_input: config element is mandatory for merged state" self.assertEqual(result["msg"], msg) @@ -1424,11 +1596,11 @@ def test_dcnm_vrf_12check_mode(self): dict( _ansible_check_mode=True, state="merged", - fabric="test_fabric", + fabric="standalone_fabric", config=playbook, ) ) - result = self.execute_module(changed=False, failed=False) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) self.version = 11 self.assertFalse(result.get("diff")) self.assertFalse(result.get("response")) @@ -1436,8 +1608,8 @@ def test_dcnm_vrf_12check_mode(self): def test_dcnm_vrf_12merged_new(self): self.version = 12 playbook = self.test_data.get("playbook_config") - set_module_args(dict(state="merged", fabric="test_fabric", config=playbook)) - result = self.execute_module(changed=True, failed=False) + set_module_args(dict(state="merged", fabric="standalone_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) self.version = 11 self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) self.assertTrue(result.get("diff")[0]["attach"][1]["deploy"]) @@ -1456,3 +1628,203 @@ def test_dcnm_vrf_12merged_new(self): ) self.assertEqual(result["response"][2]["DATA"]["status"], "") self.assertEqual(result["response"][2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_msd_merged(self): + self.version = 12 + playbook = self.test_data.get("playbook_msd_config") + set_module_args(dict(state="merged", fabric="parent_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) + self.assertTrue(result.get("workflow"), "Multisite Parent with Child Fabric Processing") + self.assertTrue(result.get("parent_fabric").get("diff")[0]["attach"][0]["deploy"]) + self.assertTrue(result.get("parent_fabric").get("diff")[0]["attach"][0]["ip_address"], "10.10.10.224") + self.assertTrue(result.get("parent_fabric").get("diff")[0]["vrf_id"], 9008011) + self.assertTrue(result.get("parent_fabric").get("response")[1]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS") + self.assertEqual(result.get("parent_fabric").get("response")[2]["DATA"]["status"], "") + self.assertEqual(result.get("parent_fabric").get("response")[2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + self.assertTrue(result.get("child_fabrics")[0]["diff"][0]["adv_default_routes"]) + self.assertTrue(result.get("child_fabrics")[0]["diff"][0]["adv_host_routes"]) + self.assertTrue(result.get("child_fabrics")[0]["response"][0]["MESSAGE"], "OK") + self.assertEqual(result.get("child_fabrics")[0]["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_msd_replaced(self): + self.version = 12 + playbook = self.test_data.get("playbook_msd_replace_config") + set_module_args(dict(state="replaced", fabric="parent_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) + self.assertTrue(result.get("workflow"), "Multisite Parent with Child Fabric Processing") + self.assertTrue(result.get("parent_fabric").get("diff")[0]["vrf_int_mtu"], 1500) + self.assertEqual(result.get("parent_fabric").get("response")[0]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + self.assertEqual(result.get("parent_fabric").get("response")[1]["DATA"]["status"], "") + self.assertEqual(result.get("parent_fabric").get("response")[1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + self.assertTrue(result.get("child_fabrics")[0]["diff"][0]["adv_default_routes"]) + self.assertFalse(result.get("child_fabrics")[0]["diff"][0]["adv_host_routes"]) + self.assertTrue(result.get("child_fabrics")[0]["diff"][0]["static_default_route"]) + self.assertTrue(result.get("child_fabrics")[0]["response"][0]["MESSAGE"], "OK") + self.assertEqual(result.get("child_fabrics")[0]["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_msd_merged_nochild(self): + self.version = 12 + playbook = self.test_data.get("playbook_msd_config_no_child") + set_module_args(dict(state="merged", fabric="parent_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) + self.assertTrue(result.get("workflow"), "Multisite Parent without Child Fabric Processing") + self.assertTrue(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertEqual( + result.get("diff")[0]["attach"][0]["ip_address"], "10.10.10.224" + ) + self.assertEqual(result.get("diff")[0]["vrf_id"], 9008011) + self.assertEqual( + result["response"][1]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS" + ) + self.assertEqual(result["response"][2]["DATA"]["status"], "") + self.assertEqual(result["response"][2]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_msd_merged_misconfig_1(self): + self.version = 12 + playbook = self.test_data.get("playbook_msd_config_misconfig_1") + set_module_args(dict(state="merged", fabric="parent_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) + self.assertEqual(result["msg"], "Config[1].child_fabric_config[1]: fabric is required") + + def test_dcnm_vrf_msd_merged_misconfig_2(self): + self.version = 12 + playbook = self.test_data.get("playbook_msd_config_misconfig_2") + set_module_args(dict(state="merged", fabric="parent_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) + self.assertEqual(result["msg"], "Multisite Child-Parent fabric validation failed: k_fab -> parent_fabric") + + def test_dcnm_vrf_msd_merged_misconfig_3(self): + self.version = 12 + playbook = self.test_data.get("playbook_msd_config_misconfig_3") + set_module_args(dict(state="merged", fabric="parent_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=True, use_action_plugin=True) + self.assertEqual(result["msg"], + "Config[1]: child_fabric_config is required for Multisite Parent fabrics. It can be optionally removed when state is query/deleted.") + + def test_dcnm_vrf_msd_delete(self): + playbook = self.test_data.get("playbook_msd_delete_config") + set_module_args(dict(state="deleted", fabric="parent_fabric", config=playbook)) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) + self.assertTrue(result.get("workflow"), "Multisite Parent without Child Fabric Processing") + self.assertFalse(result.get("diff")[0]["attach"][0]["deploy"]) + self.assertEqual(result.get("diff")[0]["attach"][0]["vlan_id"], "2000") + self.assertEqual(result.get("diff")[0]["vrf_name"], "test_vrf_1") + self.assertNotIn("vrf_id", result.get("diff")[0]) + self.assertEqual( + result["response"][0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS" + ) + self.assertEqual(result["response"][1]["DATA"]["status"], "") + self.assertEqual(result["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_msd_override(self): + playbook = self.test_data.get("playbook_msd_override_config") + set_module_args( + dict( + state="overridden", + fabric="parent_fabric", + config=playbook, + ) + ) + result = self.execute_module(changed=True, failed=False, use_action_plugin=True) + self.assertTrue(result.get("workflow"), "Multisite Parent with Child Fabric Processing") + self.assertTrue(result.get("parent_fabric").get("diff")[0]["attach"][0]["deploy"]) + self.assertTrue(result.get("parent_fabric").get("diff")[0]["attach"][0]["ip_address"], "10.10.10.224") + self.assertEqual(result.get("parent_fabric").get("diff")[0]["attach"][0]["vlan_id"], 2001) + self.assertEqual(result.get("parent_fabric").get("diff")[0]["vrf_name"], "test_vrf_2") + self.assertEqual(result.get("parent_fabric").get("diff")[0]["vrf_id"], 9008012) + + self.assertFalse(result.get("parent_fabric").get("diff")[1]["attach"][0]["deploy"]) + self.assertEqual(result.get("parent_fabric").get("diff")[1]["attach"][0]["vlan_id"], "2000") + self.assertTrue(result.get("parent_fabric").get("diff")[1]["attach"][0]["ip_address"], "10.10.10.224") + self.assertEqual(result.get("parent_fabric").get("diff")[1]["vrf_name"], "test_vrf_1") + self.assertNotIn("vrf_id", result.get("parent_fabric").get("diff")[1]) + + self.assertEqual( + result.get("parent_fabric").get("response")[0]["DATA"]["test-vrf-1--XYZKSJHSMK1(leaf1)"], "SUCCESS" + ) + self.assertEqual(result.get("parent_fabric").get("response")[1]["DATA"]["status"], "") + self.assertEqual(result.get("parent_fabric").get("response")[1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + self.assertEqual( + result.get("parent_fabric").get("response")[6]["DATA"]["test-vrf-2--XYZKSJHSMK1(leaf1)"], "SUCCESS" + ) + self.assertEqual(result.get("child_fabrics")[0]["diff"][0]["vrf_name"], "test_vrf_2") + self.assertTrue(result.get("child_fabrics")[0]["diff"][0]["adv_default_routes"]) + self.assertTrue(result.get("child_fabrics")[0]["diff"][0]["adv_host_routes"]) + self.assertFalse(result.get("child_fabrics")[0]["diff"][0]["l3vni_wo_vlan"]) + self.assertEqual(result.get("child_fabrics")[0]["response"][1]["DATA"]["status"], "") + self.assertEqual(result.get("child_fabrics")[0]["response"][1]["RETURN_CODE"], self.SUCCESS_RETURN_CODE) + + def test_dcnm_vrf_nochild_msd_query(self): + playbook = self.test_data.get("playbook_msd_config_no_child") + set_module_args(dict(state="query", fabric="parent_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) + self.assertFalse(result.get("diff")) + self.assertTrue(result.get("workflow"), "Multisite Parent without Child Fabric Processing") + self.assertEqual(result.get("response")[0]["parent"]["vrfName"], "test_vrf_1") + self.assertEqual(result.get("response")[0]["parent"]["vrfId"], 9008011) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0][ + "lanAttachedState" + ], + "DEPLOYED", + ) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0]["vlan"], + "2000", + ) + + def test_dcnm_vrf_msd_query(self): + playbook = self.test_data.get("playbook_msd_query_config") + set_module_args(dict(state="query", fabric="parent_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) + self.assertTrue(result.get("workflow"), "Multisite Parent with Child Fabric Processing") + self.assertFalse(result.get("parent_fabric").get("diff")) + self.assertEqual(result.get("parent_fabric").get("response")[0]["parent"]["vrfName"], "test_vrf_1") + self.assertEqual(result.get("parent_fabric").get("response")[0]["parent"]["vrfId"], 9008011) + self.assertEqual( + result.get("parent_fabric").get("response")[0]["attach"][0]["switchDetailsList"][0][ + "lanAttachedState" + ], + "DEPLOYED", + ) + self.assertEqual( + result.get("parent_fabric").get("response")[0]["attach"][0]["switchDetailsList"][0]["vlan"], + "2000", + ) + self.assertFalse(result.get("child_fabrics")[0]["diff"]) + self.assertEqual(result.get("child_fabrics")[0]["response"][0]["parent"]["vrfName"], "test_vrf_1") + self.assertEqual(result.get("child_fabrics")[0]["response"][0]["parent"]["vrfId"], 9008011) + self.assertEqual( + result.get("child_fabrics")[0]["response"][0]["attach"][0]["switchDetailsList"][0][ + "lanAttachedState" + ], + "DEPLOYED", + ) + self.assertEqual( + result.get("child_fabrics")[0]["response"][0]["attach"][0]["switchDetailsList"][0]["vlan"], + "2000", + ) + + def test_dcnm_vrf_child_msd_query(self): + playbook = self.test_data.get("playbook_msd_child_config") + set_module_args(dict(state="query", fabric="child_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) + self.assertTrue(result.get("workflow"), "Multisite Child VRF Processing") + self.assertEqual(result.get("response")[0]["parent"]["vrfName"], "test_vrf_1") + self.assertEqual(result.get("response")[0]["parent"]["vrfId"], 9008011) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0][ + "lanAttachedState" + ], + "DEPLOYED", + ) + self.assertEqual( + result.get("response")[0]["attach"][0]["switchDetailsList"][0]["vlan"], + "2000", + ) + + def test_dcnm_vrf_child_msd_invalid_config(self): + playbook = self.test_data.get("playbook_msd_child_config") + set_module_args(dict(state="merged", fabric="child_fabric", config=playbook)) + result = self.execute_module(changed=False, failed=False, use_action_plugin=True) + self.assertTrue(result.get("msg"), "Attempted task on Child Multisite fabric 'child_fabric'. State 'query' is only allowed.")