From a3ccfecc2d334228b639807d98cc4ad018d1e15b Mon Sep 17 00:00:00 2001 From: Matt Tarkington Date: Thu, 9 Apr 2026 16:31:55 -0400 Subject: [PATCH 1/6] initial msd fabric group module --- .../models/manage_fabric_group/common.py | 28 + .../models/manage_fabric_group/enums.py | 97 ++++ .../manage_fabric_group_vxlan.py | 459 +++++++++++++++ .../manage_fabric_group_vxlan.py | 50 ++ .../modules/nd_manage_fabric_group_vxlan.py | 543 ++++++++++++++++++ .../tasks/main.yaml | 511 ++++++++++++++++ .../vars/main.yaml | 51 ++ ...points_api_v1_manage_fabric_group_vxlan.py | 322 +++++++++++ 8 files changed, 2061 insertions(+) create mode 100644 plugins/module_utils/models/manage_fabric_group/common.py create mode 100644 plugins/module_utils/models/manage_fabric_group/enums.py create mode 100644 plugins/module_utils/models/manage_fabric_group/manage_fabric_group_vxlan.py create mode 100644 plugins/module_utils/orchestrators/manage_fabric_group_vxlan.py create mode 100644 plugins/modules/nd_manage_fabric_group_vxlan.py create mode 100644 tests/integration/targets/nd_manage_fabric_group_vxlan/tasks/main.yaml create mode 100644 tests/integration/targets/nd_manage_fabric_group_vxlan/vars/main.yaml create mode 100644 tests/unit/module_utils/endpoints/test_endpoints_api_v1_manage_fabric_group_vxlan.py diff --git a/plugins/module_utils/models/manage_fabric_group/common.py b/plugins/module_utils/models/manage_fabric_group/common.py new file mode 100644 index 00000000..6214e63c --- /dev/null +++ b/plugins/module_utils/models/manage_fabric_group/common.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2026, Matt Tarkington (@mtarking) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +# Summary + +Common constants and patterns for VXLAN Fabric Group models. +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import re + +# Regex from OpenAPI schema: bgpAsn accepts plain integers (1-4294967295) and +# dotted four-byte ASN notation (1-65535).(0-65535) +BGP_ASN_RE = re.compile( + r"^(([1-9]{1}[0-9]{0,8}|[1-3]{1}[0-9]{1,9}|[4]{1}([0-1]{1}[0-9]{8}" + r"|[2]{1}([0-8]{1}[0-9]{7}|[9]{1}([0-3]{1}[0-9]{6}|[4]{1}([0-8]{1}[0-9]{5}" + r"|[9]{1}([0-5]{1}[0-9]{4}|[6]{1}([0-6]{1}[0-9]{3}|[7]{1}([0-1]{1}[0-9]{2}" + r"|[2]{1}([0-8]{1}[0-9]{1}|[9]{1}[0-5]{1})))))))))" + r"|([1-5]\d{4}|[1-9]\d{0,3}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])" + r"(\.([1-5]\d{4}|[1-9]\d{0,3}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5]|0))?)$" +) diff --git a/plugins/module_utils/models/manage_fabric_group/enums.py b/plugins/module_utils/models/manage_fabric_group/enums.py new file mode 100644 index 00000000..87fe2309 --- /dev/null +++ b/plugins/module_utils/models/manage_fabric_group/enums.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# pylint: disable=wrong-import-position +# pylint: disable=missing-module-docstring +# Copyright: (c) 2026, Matt Tarkington (@mtarking) +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +# Summary + +Enum definitions for VXLAN Fabric Group (MSD) modules. + +## Enums + +- FabricGroupTypeEnum: Fabric group type discriminator. +- BgpAuthenticationKeyTypeEnum: BGP authentication key encryption types. +- MultisiteOverlayInterConnectTypeEnum: Multi-Site Overlay Interconnect type options. +- CloudSecAlgorithmEnum: CloudSec encryption algorithm options. +- CloudSecEnforcementEnum: CloudSec enforcement type options. +- SecurityGroupTagEnum: Security Group Tag enforcement options. +""" + +from __future__ import annotations + +__metaclass__ = type + +from enum import Enum + + +class FabricGroupTypeEnum(str, Enum): + """ + # Summary + + Enumeration of supported fabric group types for discriminated union. + + ## Values + + - `VXLAN` - VXLAN fabric group (MSD) + """ + + VXLAN = "vxlan" + + +class BgpAuthenticationKeyTypeEnum(str, Enum): + """ + # Summary + + Enumeration for BGP authentication key encryption types. + """ + + THREE_DES = "3des" + TYPE6 = "type6" + TYPE7 = "type7" + + +class MultisiteOverlayInterConnectTypeEnum(str, Enum): + """ + # Summary + + Enumeration for Multi-Site Overlay Interconnect type options. + """ + + MANUAL = "manual" + ROUTE_SERVER = "routeServer" + DIRECT_PEERING = "directPeering" + + +class CloudSecAlgorithmEnum(str, Enum): + """ + # Summary + + Enumeration for CloudSec encryption algorithm options. + """ + + AES_128_CMAC = "AES_128_CMAC" + AES_256_CMAC = "AES_256_CMAC" + + +class CloudSecEnforcementEnum(str, Enum): + """ + # Summary + + Enumeration for CloudSec enforcement type options. + """ + + STRICT = "strict" + LOOSE = "loose" + + +class SecurityGroupTagEnum(str, Enum): + """ + # Summary + + Enumeration for Security Group Tag enforcement options (fabric group level). + """ + + OFF = "off" + LOOSE = "loose" + STRICT = "strict" diff --git a/plugins/module_utils/models/manage_fabric_group/manage_fabric_group_vxlan.py b/plugins/module_utils/models/manage_fabric_group/manage_fabric_group_vxlan.py new file mode 100644 index 00000000..1d640083 --- /dev/null +++ b/plugins/module_utils/models/manage_fabric_group/manage_fabric_group_vxlan.py @@ -0,0 +1,459 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2026, Matt Tarkington (@mtarking) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import re +from typing import List, Dict, Any, Optional, ClassVar, Literal + +from ansible_collections.cisco.nd.plugins.module_utils.models.base import NDBaseModel +from ansible_collections.cisco.nd.plugins.module_utils.models.nested import NDNestedModel +from ansible_collections.cisco.nd.plugins.module_utils.common.pydantic_compat import ( + ConfigDict, + Field, + field_validator, + model_validator, +) +from ansible_collections.cisco.nd.plugins.module_utils.models.manage_fabric_group.enums import ( + FabricGroupTypeEnum, + BgpAuthenticationKeyTypeEnum, + CloudSecAlgorithmEnum, + CloudSecEnforcementEnum, + MultisiteOverlayInterConnectTypeEnum, + SecurityGroupTagEnum, +) +from ansible_collections.cisco.nd.plugins.module_utils.models.manage_fabric_group.common import ( + BGP_ASN_RE, +) + +""" +# Pydantic models for VXLAN Fabric Group (MSD) management via Nexus Dashboard + +This module provides Pydantic models for creating, updating, and deleting +VXLAN Fabric Groups (MSD - Multi-Site Domain) through the Nexus Dashboard +Fabric Controller (NDFC) API. + +## Models Overview + +- `RouteServerModel` - Route server configuration for multi-site overlay +- `VxlanFabricGroupManagementModel` - VXLAN fabric group management settings +- `FabricGroupVxlanModel` - Complete fabric group creation model + +## Usage + +```python +fabric_group_data = { + "name": "MyFabricGroup", + "category": "fabricGroup", + "management": { + "type": "vxlan", + "l2VniRange": "30000-49000", + "l3VniRange": "50000-59000", + "anycastGatewayMac": "2020.0000.00aa", + } +} +fabric_group = FabricGroupVxlanModel(**fabric_group_data) +``` +""" + + +class RouteServerModel(NDNestedModel): + """ + # Summary + + Route server configuration for multi-site overlay interconnect. + + ## Raises + + - `ValueError` - If IP address or ASN format is invalid + """ + + model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True, populate_by_name=True, extra="allow") + + route_server_ip: str = Field(alias="routeServerIp", description="Route Server IP Address") + route_server_asn: str = Field(alias="routeServerAsn", description="Autonomous system number 1-4294967295 | 1-65535[.0-65535]") + + @field_validator("route_server_asn") + @classmethod + def validate_asn(cls, value: str) -> str: + if not BGP_ASN_RE.match(value): + raise ValueError(f"Invalid BGP ASN format: {value}") + return value + + +class VxlanFabricGroupManagementModel(NDNestedModel): + """ + # Summary + + VXLAN Fabric Group (MSD) management configuration. + + This model contains all settings specific to VXLAN fabric group types including + multi-site overlay/underlay configuration, CloudSec, and security group settings. + + ## Raises + + - `ValueError` - If VNI ranges, IP ranges, or MAC addresses are invalid + - `TypeError` - If required string fields are not provided + """ + + model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True, populate_by_name=True, extra="allow") + + # Fabric Group Type (required for discriminated union) + type: Literal[FabricGroupTypeEnum.VXLAN] = Field( + description="Type of the fabric group", default=FabricGroupTypeEnum.VXLAN + ) + + # VNI Ranges + l2_vni_range: str = Field( + alias="l2VniRange", + description="Overlay network identifier range (minimum: 1, maximum: 16777214)", + default="30000-49000", + ) + l3_vni_range: str = Field( + alias="l3VniRange", + description="Overlay VRF identifier range (minimum: 1, maximum: 16777214)", + default="50000-59000", + ) + downstream_vni: bool = Field( + alias="downstreamVni", + description="Enable unique per-fabric virtual network identifier (VNI)", + default=False, + ) + downstream_l2_vni_range: str = Field( + alias="downstreamL2VniRange", + description="Unique Range for L2VNI when downstream VNI is enabled (min: 1, max: 16777214)", + default="60000-69000", + ) + downstream_l3_vni_range: str = Field( + alias="downstreamL3VniRange", + description="Unique Range for L3VNI when downstream VNI is enabled (min: 1, max: 16777214)", + default="80000-89000", + ) + + # Underlay + underlay_ipv6: bool = Field( + alias="underlayIpv6", + description="If not enabled, IPv4 underlay is used", + default=False, + ) + + # Templates + vrf_template: str = Field( + alias="vrfTemplate", + description="Default overlay VRF template for leafs", + default="Default_VRF_Universal", + ) + network_template: str = Field( + alias="networkTemplate", + description="Default overlay network template for leafs", + default="Default_Network_Universal", + ) + vrf_extension_template: str = Field( + alias="vrfExtensionTemplate", + description="Default overlay VRF template for borders", + default="Default_VRF_Extension_Universal", + ) + network_extension_template: str = Field( + alias="networkExtensionTemplate", + description="Default overlay network template for borders", + default="Default_Network_Extension_Universal", + ) + + # PVLAN + private_vlan: bool = Field( + alias="privateVlan", + description="Enable PVLAN on switches except spines and super spines", + default=False, + ) + default_private_vlan_secondary_network_template: str = Field( + alias="defaultPrivateVlanSecondaryNetworkTemplate", + description="Default PVLAN secondary network template", + default="Pvlan_Secondary_Network", + ) + + # Anycast Gateway + anycast_gateway_mac: str = Field( + alias="anycastGatewayMac", + description="Shared anycast gateway MAC address for all VTEPs", + default="2020.0000.00aa", + ) + + # Multi-Site Overlay + multisite_overlay_inter_connect_type: MultisiteOverlayInterConnectTypeEnum = Field( + alias="multisiteOverlayInterConnectType", + description="Type of Multi-Site Overlay Interconnect", + default=MultisiteOverlayInterConnectTypeEnum.MANUAL, + ) + route_server_collection: Optional[List[RouteServerModel]] = Field( + alias="routeServerCollection", + description="Multi-Site Route-Servers", + default=None, + ) + route_server_redistribute_direct_route_map: bool = Field( + alias="routeServerRedistributeDirectRouteMap", + description="Redistribute direct on route servers for auto-created Multi-Site overlay IFC links", + default=False, + ) + route_server_routing_tag: int = Field( + alias="routeServerRoutingTag", + description="Routing tag associated with Route Server IP for redistribute direct (0-4294967295)", + ge=0, + le=4294967295, + default=54321, + ) + enable_ms_overlay_ifc_bgp_desc: bool = Field( + alias="enableMsOverlayIfcBgpDesc", + description="Generate BGP neighbor description for auto-created Multi-Site overlay IFC links", + default=True, + ) + + # Multi-Site Underlay + auto_multisite_underlay_inter_connect: bool = Field( + alias="autoMultisiteUnderlayInterConnect", + description="Auto-configures Multi-Site underlay Inter-Fabric links", + default=False, + ) + bgp_send_community: bool = Field( + alias="bgpSendCommunity", + description="For auto-created Multi-Site Underlay Inter-Fabric links", + default=False, + ) + bgp_log_neighbor_change: bool = Field( + alias="bgpLogNeighborChange", + description="For auto-created Multi-Site Underlay Inter-Fabric links", + default=False, + ) + bgp_bfd: bool = Field( + alias="bgpBfd", + description="For auto-created Multi-Site Underlay Inter-Fabric links", + default=False, + ) + multisite_delay_restore: int = Field( + alias="multisiteDelayRestore", + description="Multi-Site underlay and overlay control plane convergence time in seconds", + ge=30, + le=1000, + default=300, + ) + multisite_inter_connect_bgp_authentication: bool = Field( + alias="multisiteInterConnectBgpAuthentication", + description="Enables or disables the BGP authentication for inter-site links", + default=False, + ) + multisite_inter_connect_bgp_auth_key_type: BgpAuthenticationKeyTypeEnum = Field( + alias="multisiteInterConnectBgpAuthKeyType", + description="BGP key encryption type: 3 - 3DES, 6 - Cisco type 6, 7 - Cisco type 7", + default=BgpAuthenticationKeyTypeEnum.THREE_DES, + ) + multisite_inter_connect_bgp_key: Optional[str] = Field( + alias="multisiteInterConnectBgpKey", + description="Encrypted BGP authentication key based on type", + min_length=1, + max_length=256, + default=None, + ) + multisite_loopback_id: int = Field( + alias="multisiteLoopbackId", + description="Loopback ID for multi-site (typically Loopback100)", + ge=0, + le=1023, + default=100, + ) + border_gateway_routing_tag: int = Field( + alias="borderGatewayRoutingTag", + description="Routing tag associated with IP address of loopback and DCI interfaces (0-4294967295)", + ge=0, + le=4294967295, + default=54321, + ) + + # Multi-Site IP Ranges + multisite_loopback_ip_range: str = Field( + alias="multisiteLoopbackIpRange", + description="Typically Loopback100 IP Address Range", + default="10.10.0.0/24", + ) + multisite_underlay_subnet_range: str = Field( + alias="multisiteUnderlaySubnetRange", + description="Address range to assign P2P DCI Links", + default="10.10.1.0/24", + ) + multisite_underlay_subnet_target_mask: int = Field( + alias="multisiteUnderlaySubnetTargetMask", + description="Target Mask for Subnet Range", + ge=8, + le=31, + default=30, + ) + multisite_loopback_ipv6_range: str = Field( + alias="multisiteLoopbackIpv6Range", + description="Typically Loopback100 IPv6 Address Range", + default="fd00::a10:0/120", + ) + multisite_underlay_ipv6_subnet_range: str = Field( + alias="multisiteUnderlayIpv6SubnetRange", + description="Address range to assign P2P DCI IPv6 Links", + default="fd00::a11:0/120", + ) + multisite_underlay_ipv6_subnet_target_mask: int = Field( + alias="multisiteUnderlayIpv6SubnetTargetMask", + description="Target IPv6 Mask for Subnet Range", + ge=120, + le=127, + default=126, + ) + + # Tenant Routed Multicast + tenant_routed_multicast_v4_v6: bool = Field( + alias="tenantRoutedMulticastV4V6", + description="If enabled, MVPN VRI IDs are tracked in MSD fabric to ensure uniqueness within MSD", + default=False, + ) + + # Security Groups + security_group_tag: SecurityGroupTagEnum = Field( + alias="securityGroupTag", + description="If set to strict, only security groups enabled child fabrics will be allowed", + default=SecurityGroupTagEnum.OFF, + ) + security_group_tag_prefix: str = Field( + alias="securityGroupTagPrefix", + description="Prefix to be used when a new security group is created", + min_length=1, + max_length=10, + default="SG_", + ) + security_group_tag_mac_segmentation: bool = Field( + alias="securityGroupTagMacSegmentation", + description="Enable MAC based segmentation for security groups", + default=False, + ) + security_group_tag_id_range: str = Field( + alias="securityGroupTagIdRange", + description="Security group tag (SGT) identifier range (min: 16, max: 65535)", + default="10000-14000", + ) + security_group_tag_preprovision: bool = Field( + alias="securityGroupTagPreprovision", + description="Generate security groups configuration for non-enforced VRFs", + default=False, + ) + + # CloudSec + auto_configure_cloud_sec: bool = Field( + alias="autoConfigureCloudSec", + description="Auto Config CloudSec on Border Gateways", + default=False, + ) + cloud_sec_key: Optional[str] = Field( + alias="cloudSecKey", + description="Cisco Type 7 Encrypted Octet String", + min_length=1, + max_length=130, + default=None, + ) + cloud_sec_algorithm: CloudSecAlgorithmEnum = Field( + alias="cloudSecAlgorithm", + description="CloudSec Encryption Algorithm", + default=CloudSecAlgorithmEnum.AES_128_CMAC, + ) + cloud_sec_enforcement: CloudSecEnforcementEnum = Field( + alias="cloudSecEnforcement", + description="Enforcement type. If set strict, data across site must be encrypted", + default=CloudSecEnforcementEnum.STRICT, + ) + cloud_sec_report_timer: int = Field( + alias="cloudSecReportTimer", + description="CloudSec Operational Status periodic report timer in minutes", + ge=5, + le=60, + default=5, + ) + + # Configuration Backup + scheduled_backup: Optional[bool] = Field( + alias="scheduledBackup", + description="Enable backup at the specified time daily", + default=None, + ) + scheduled_backup_time: Optional[str] = Field( + alias="scheduledBackupTime", + description="Time (UTC) in 24 hour format to take a daily backup (00:00 to 23:59)", + default=None, + ) + + @field_validator("anycast_gateway_mac") + @classmethod + def validate_mac(cls, value: str) -> str: + if not re.match(r"^[0-9a-fA-F]{4}\.[0-9a-fA-F]{4}\.[0-9a-fA-F]{4}$", value): + raise ValueError(f"Invalid MAC address format, expected xxxx.xxxx.xxxx, got: {value}") + return value.lower() + + +class FabricGroupVxlanModel(NDBaseModel): + """ + # Summary + + Complete model for creating a VXLAN Fabric Group (MSD). + + ## Raises + + - `ValueError` - If required fields are missing or invalid + - `TypeError` - If field types don't match expected types + """ + + model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True, populate_by_name=True, extra="allow") + + identifiers: ClassVar[Optional[List[str]]] = ["fabric_name"] + identifier_strategy: ClassVar[Optional[Literal["single", "composite", "hierarchical", "singleton"]]] = "single" + + # Basic Fabric Group Properties + category: Literal["fabricGroup"] = Field(description="Resource category", default="fabricGroup") + fabric_name: str = Field(alias="name", description="Fabric group name", min_length=1, max_length=64) + + # Core Management Configuration + management: Optional[VxlanFabricGroupManagementModel] = Field( + description="VXLAN fabric group management configuration", default=None + ) + + @field_validator("fabric_name") + @classmethod + def validate_fabric_name(cls, value: str) -> str: + if not re.match(r"^[a-zA-Z0-9_-]+$", value): + raise ValueError(f"Fabric group name can only contain letters, numbers, underscores, and hyphens, got: {value}") + return value + + @model_validator(mode="after") + def validate_fabric_group_consistency(self) -> "FabricGroupVxlanModel": + if self.management is not None and self.management.type != FabricGroupTypeEnum.VXLAN: + raise ValueError(f"Management type must be {FabricGroupTypeEnum.VXLAN}") + return self + + @classmethod + def get_argument_spec(cls) -> Dict: + return dict( + state={ + "type": "str", + "default": "merged", + "choices": ["merged", "replaced", "deleted", "overridden", "gathered"], + }, + config={"required": False, "type": "list", "elements": "dict"}, + ) + + +# Export all models for external use +__all__ = [ + "RouteServerModel", + "VxlanFabricGroupManagementModel", + "FabricGroupVxlanModel", + "FabricGroupTypeEnum", + "MultisiteOverlayInterConnectTypeEnum", + "CloudSecAlgorithmEnum", + "CloudSecEnforcementEnum", + "SecurityGroupTagEnum", +] diff --git a/plugins/module_utils/orchestrators/manage_fabric_group_vxlan.py b/plugins/module_utils/orchestrators/manage_fabric_group_vxlan.py new file mode 100644 index 00000000..78c813b2 --- /dev/null +++ b/plugins/module_utils/orchestrators/manage_fabric_group_vxlan.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2026, Matt Tarkington (@mtarking) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from typing import Type +from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.base import NDBaseOrchestrator +from ansible_collections.cisco.nd.plugins.module_utils.models.base import NDBaseModel +from ansible_collections.cisco.nd.plugins.module_utils.models.manage_fabric_group.manage_fabric_group_vxlan import FabricGroupVxlanModel +from ansible_collections.cisco.nd.plugins.module_utils.endpoints.base import NDEndpointBaseModel +from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.types import ResponseType +from ansible_collections.cisco.nd.plugins.module_utils.endpoints.v1.manage.manage_fabrics import ( + EpManageFabricsGet, + EpManageFabricsListGet, + EpManageFabricsPost, + EpManageFabricsPut, + EpManageFabricsDelete, +) + + +class ManageFabricGroupVxlanOrchestrator(NDBaseOrchestrator): + model_class: Type[NDBaseModel] = FabricGroupVxlanModel + + create_endpoint: Type[NDEndpointBaseModel] = EpManageFabricsPost + update_endpoint: Type[NDEndpointBaseModel] = EpManageFabricsPut + delete_endpoint: Type[NDEndpointBaseModel] = EpManageFabricsDelete + query_one_endpoint: Type[NDEndpointBaseModel] = EpManageFabricsGet + query_all_endpoint: Type[NDEndpointBaseModel] = EpManageFabricsListGet + + def query_all(self) -> ResponseType: + """ + Custom query_all action to extract 'fabrics' from response, + filtered to only VXLAN fabric group types (category=fabricGroup, management.type=vxlan). + """ + try: + api_endpoint = self.query_all_endpoint() + result = self.sender.query_obj(api_endpoint.path) + fabrics = result.get("fabrics", []) or [] + return [ + f + for f in fabrics + if f.get("category") == "fabricGroup" and f.get("management", {}).get("type") == "vxlan" + ] + except Exception as e: + raise Exception(f"Query all failed: {e}") from e diff --git a/plugins/modules/nd_manage_fabric_group_vxlan.py b/plugins/modules/nd_manage_fabric_group_vxlan.py new file mode 100644 index 00000000..39bab9f2 --- /dev/null +++ b/plugins/modules/nd_manage_fabric_group_vxlan.py @@ -0,0 +1,543 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2026, Matt Tarkington (@mtarking) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = {"metadata_version": "1.1", "status": ["preview"], "supported_by": "community"} + +DOCUMENTATION = r""" +--- +module: nd_manage_fabric_group_vxlan +version_added: "1.5.0" +short_description: Manage VXLAN Fabric Groups (MSD) on Cisco Nexus Dashboard +description: +- Manage VXLAN Fabric Groups (Multi-Site Domain) on Cisco Nexus Dashboard (ND). +- It supports creating, updating, replacing, deleting, and gathering VXLAN fabric groups. +- Fabric groups aggregate multiple member fabrics for multi-site operations. +author: +- Matt Tarkington (@mtarking) +options: + config: + description: + - The list of VXLAN fabric groups to configure. + type: list + elements: dict + suboptions: + fabric_name: + description: + - The name of the fabric group. + - Only letters, numbers, underscores, and hyphens are allowed. + - The O(config.fabric_name) must be defined when creating, updating or deleting a fabric group. + type: str + required: true + category: + description: + - The resource category. Must be C(fabricGroup) for fabric groups. + type: str + default: fabricGroup + management: + description: + - The VXLAN fabric group management configuration. + - Properties control multi-site overlay/underlay, CloudSec, security groups, and VNI ranges. + type: dict + suboptions: + # General + type: + description: + - The fabric group management type. Must be C(vxlan) for VXLAN fabric groups. + type: str + default: vxlan + choices: [ vxlan ] + l2_vni_range: + description: + - The Layer 2 VNI range (minimum 1, maximum 16777214). + type: str + default: "30000-49000" + l3_vni_range: + description: + - The Layer 3 VNI range (minimum 1, maximum 16777214). + type: str + default: "50000-59000" + downstream_vni: + description: + - Enable unique per-fabric virtual network identifier (VNI). + type: bool + default: false + downstream_l2_vni_range: + description: + - Unique Range for L2VNI when downstream VNI is enabled (min 1, max 16777214). + - Should not conflict with any VNI already used in member fabric. + type: str + default: "60000-69000" + downstream_l3_vni_range: + description: + - Unique Range for L3VNI when downstream VNI is enabled (min 1, max 16777214). + - Should not conflict with any VNI already used in member fabric. + type: str + default: "80000-89000" + underlay_ipv6: + description: + - Enable IPv6 underlay. If not enabled, IPv4 underlay is used. + type: bool + default: false + + # Templates + vrf_template: + description: + - Default overlay VRF template for leafs. + type: str + default: Default_VRF_Universal + network_template: + description: + - Default overlay network template for leafs. + type: str + default: Default_Network_Universal + vrf_extension_template: + description: + - Default overlay VRF template for borders. + type: str + default: Default_VRF_Extension_Universal + network_extension_template: + description: + - Default overlay network template for borders. + type: str + default: Default_Network_Extension_Universal + + # PVLAN + private_vlan: + description: + - Enable PVLAN on switches except spines and super spines. + type: bool + default: false + default_private_vlan_secondary_network_template: + description: + - Default PVLAN secondary network template. + type: str + default: Pvlan_Secondary_Network + + # Anycast Gateway + anycast_gateway_mac: + description: + - Shared anycast gateway MAC address for all VTEPs in xxxx.xxxx.xxxx format. + type: str + default: 2020.0000.00aa + + # Multi-Site Overlay + multisite_overlay_inter_connect_type: + description: + - Type of Multi-Site Overlay Interconnect. + type: str + default: manual + choices: [ manual, routeServer, directPeering ] + route_server_collection: + description: + - List of Multi-Site Route-Servers. + - Each entry requires a route server IP address and BGP ASN. + type: list + elements: dict + suboptions: + route_server_ip: + description: + - Route Server IP Address. + type: str + required: true + route_server_asn: + description: + - Autonomous system number (1-4294967295 or dotted notation). + type: str + required: true + route_server_redistribute_direct_route_map: + description: + - Redistribute direct on route servers for auto-created Multi-Site overlay IFC links. + - Applicable only when deployment method is centralizedToRouteServers. + type: bool + default: false + route_server_routing_tag: + description: + - Routing tag associated with Route Server IP for redistribute direct (0-4294967295). + type: int + default: 54321 + enable_ms_overlay_ifc_bgp_desc: + description: + - Generate BGP neighbor description for auto-created Multi-Site overlay IFC links. + type: bool + default: true + + # Multi-Site Underlay + auto_multisite_underlay_inter_connect: + description: + - Auto-configures Multi-Site underlay Inter-Fabric links. + type: bool + default: false + bgp_send_community: + description: + - Send community for auto-created Multi-Site Underlay Inter-Fabric links. + type: bool + default: false + bgp_log_neighbor_change: + description: + - Log neighbor change for auto-created Multi-Site Underlay Inter-Fabric links. + type: bool + default: false + bgp_bfd: + description: + - BFD for auto-created Multi-Site Underlay Inter-Fabric links. + type: bool + default: false + multisite_delay_restore: + description: + - Multi-Site underlay and overlay control plane convergence time in seconds (30-1000). + type: int + default: 300 + multisite_inter_connect_bgp_authentication: + description: + - Enables or disables the BGP authentication for inter-site links. + type: bool + default: false + multisite_inter_connect_bgp_auth_key_type: + description: + - "BGP key encryption type: 3 - 3DES, 6 - Cisco type 6, 7 - Cisco type 7." + type: str + default: 3des + choices: [ 3des, type6, type7 ] + multisite_inter_connect_bgp_key: + description: + - Encrypted BGP authentication key based on type. + type: str + multisite_loopback_id: + description: + - Loopback ID for multi-site, typically Loopback100 (0-1023). + type: int + default: 100 + border_gateway_routing_tag: + description: + - Routing tag associated with IP address of loopback and DCI interfaces (0-4294967295). + type: int + default: 54321 + + # Multi-Site IP Ranges + multisite_loopback_ip_range: + description: + - Typically Loopback100 IP Address Range. + type: str + default: "10.10.0.0/24" + multisite_underlay_subnet_range: + description: + - Address range to assign P2P DCI Links. + type: str + default: "10.10.1.0/24" + multisite_underlay_subnet_target_mask: + description: + - Target Mask for Subnet Range (8-31). + type: int + default: 30 + multisite_loopback_ipv6_range: + description: + - Typically Loopback100 IPv6 Address Range. + type: str + default: "fd00::a10:0/120" + multisite_underlay_ipv6_subnet_range: + description: + - Address range to assign P2P DCI IPv6 Links. + type: str + default: "fd00::a11:0/120" + multisite_underlay_ipv6_subnet_target_mask: + description: + - Target IPv6 Mask for Subnet Range (120-127). + type: int + default: 126 + + # Tenant Routed Multicast + tenant_routed_multicast_v4_v6: + description: + - If enabled, MVPN VRI IDs are tracked in MSD fabric to ensure uniqueness within MSD. + type: bool + default: false + + # Security Groups + security_group_tag: + description: + - Security Group Tag enforcement. If set to C(strict), only security groups enabled child fabrics will be allowed. + type: str + default: "off" + choices: [ "off", loose, strict ] + security_group_tag_prefix: + description: + - Prefix to be used when a new security group is created. + type: str + default: SG_ + security_group_tag_mac_segmentation: + description: + - Enable MAC based segmentation for security groups. + type: bool + default: false + security_group_tag_id_range: + description: + - Security group tag (SGT) identifier range (min 16, max 65535). + type: str + default: "10000-14000" + security_group_tag_preprovision: + description: + - Generate security groups configuration for non-enforced VRFs. + type: bool + default: false + + # CloudSec + auto_configure_cloud_sec: + description: + - Auto Config CloudSec on Border Gateways. + type: bool + default: false + cloud_sec_key: + description: + - Cisco Type 7 Encrypted Octet String for CloudSec. + type: str + cloud_sec_algorithm: + description: + - CloudSec Encryption Algorithm. + type: str + default: AES_128_CMAC + choices: [ AES_128_CMAC, AES_256_CMAC ] + cloud_sec_enforcement: + description: + - CloudSec enforcement type. If set C(strict), data across site must be encrypted. + type: str + default: strict + choices: [ strict, loose ] + cloud_sec_report_timer: + description: + - CloudSec Operational Status periodic report timer in minutes (5-60). + type: int + default: 5 + + # Configuration Backup + scheduled_backup: + description: + - Enable backup at the specified time daily. + type: bool + scheduled_backup_time: + description: + - Time (UTC) in 24 hour format to take a daily backup if enabled (00:00 to 23:59). + type: str + state: + description: + - The desired state of the fabric group resources on the Cisco Nexus Dashboard. + - Use O(state=merged) to create new fabric groups and update existing ones as defined in the configuration. + Resources on ND that are not specified in the configuration will be left unchanged. + - Use O(state=replaced) to replace the fabric group configuration specified in the configuration. + Any settings not explicitly provided will revert to their defaults. + - Use O(state=overridden) to enforce the configuration as the single source of truth. + Any fabric group existing on ND but not present in the configuration will be deleted. Use with extra caution. + - Use O(state=deleted) to remove the fabric groups specified in the configuration from the Cisco Nexus Dashboard. + - Use O(state=gathered) to query the current state of fabric groups from ND without making any changes. + type: str + default: merged + choices: [ merged, replaced, overridden, deleted, gathered ] +extends_documentation_fragment: +- cisco.nd.modules +- cisco.nd.check_mode +notes: +- This module is only supported on Nexus Dashboard having version 4.1.0 or higher. +- Only VXLAN fabric group type (C(vxlan)) is supported by this module. +- When using O(state=replaced) with only required fields, all optional management settings revert to their defaults. +- Fabric group member management (add/remove members) is not handled by this module. Use a dedicated member module. +""" + +EXAMPLES = r""" +- name: Create a VXLAN fabric group (MSD) using state merged + cisco.nd.nd_manage_fabric_group_vxlan: + state: merged + config: + - fabric_name: my_fabric_group + category: fabricGroup + management: + type: vxlan + l2_vni_range: "30000-49000" + l3_vni_range: "50000-59000" + anycast_gateway_mac: "2020.0000.00aa" + multisite_overlay_inter_connect_type: manual + multisite_loopback_ip_range: "10.10.0.0/24" + multisite_underlay_subnet_range: "10.10.1.0/24" + multisite_underlay_subnet_target_mask: 30 + multisite_delay_restore: 300 + register: result + +- name: Update specific fields on an existing fabric group using state merged (partial update) + cisco.nd.nd_manage_fabric_group_vxlan: + state: merged + config: + - fabric_name: my_fabric_group + management: + anycast_gateway_mac: "2020.0000.00bb" + auto_multisite_underlay_inter_connect: true + multisite_delay_restore: 600 + register: result + +- name: Create a VXLAN fabric group with route servers + cisco.nd.nd_manage_fabric_group_vxlan: + state: merged + config: + - fabric_name: my_fabric_group + category: fabricGroup + management: + type: vxlan + l2_vni_range: "30000-49000" + l3_vni_range: "50000-59000" + anycast_gateway_mac: "2020.0000.00aa" + multisite_overlay_inter_connect_type: routeServer + route_server_collection: + - route_server_ip: "10.1.1.1" + route_server_asn: "65000" + - route_server_ip: "10.1.1.2" + route_server_asn: "65001" + multisite_loopback_ip_range: "10.10.0.0/24" + multisite_underlay_subnet_range: "10.10.1.0/24" + multisite_underlay_subnet_target_mask: 30 + multisite_delay_restore: 300 + register: result + +- name: Create a VXLAN fabric group with CloudSec enabled + cisco.nd.nd_manage_fabric_group_vxlan: + state: merged + config: + - fabric_name: my_secure_group + category: fabricGroup + management: + type: vxlan + l2_vni_range: "30000-49000" + l3_vni_range: "50000-59000" + anycast_gateway_mac: "2020.0000.00aa" + auto_configure_cloud_sec: true + cloud_sec_algorithm: AES_256_CMAC + cloud_sec_enforcement: strict + cloud_sec_report_timer: 10 + register: result + +- name: Create or fully replace a VXLAN fabric group using state replaced + cisco.nd.nd_manage_fabric_group_vxlan: + state: replaced + config: + - fabric_name: my_fabric_group + category: fabricGroup + management: + type: vxlan + l2_vni_range: "40000-59000" + l3_vni_range: "60000-69000" + anycast_gateway_mac: "2020.0000.00cc" + multisite_overlay_inter_connect_type: directPeering + multisite_loopback_ip_range: "10.20.0.0/24" + multisite_underlay_subnet_range: "10.20.1.0/24" + multisite_underlay_subnet_target_mask: 30 + multisite_delay_restore: 500 + downstream_vni: true + downstream_l2_vni_range: "60000-69000" + downstream_l3_vni_range: "80000-89000" + register: result + +- name: Replace fabric group with only required fields (all optional settings revert to defaults) + cisco.nd.nd_manage_fabric_group_vxlan: + state: replaced + config: + - fabric_name: my_fabric_group + category: fabricGroup + management: + type: vxlan + register: result + +- name: Enforce exact fabric group inventory using state overridden (deletes unlisted groups) + cisco.nd.nd_manage_fabric_group_vxlan: + state: overridden + config: + - fabric_name: group_east + category: fabricGroup + management: + type: vxlan + l2_vni_range: "30000-49000" + l3_vni_range: "50000-59000" + anycast_gateway_mac: "2020.0000.0010" + multisite_loopback_ip_range: "10.10.0.0/24" + multisite_underlay_subnet_range: "10.10.1.0/24" + - fabric_name: group_west + category: fabricGroup + management: + type: vxlan + l2_vni_range: "30000-49000" + l3_vni_range: "50000-59000" + anycast_gateway_mac: "2020.0000.0020" + multisite_loopback_ip_range: "10.20.0.0/24" + multisite_underlay_subnet_range: "10.20.1.0/24" + register: result + +- name: Delete a specific fabric group using state deleted + cisco.nd.nd_manage_fabric_group_vxlan: + state: deleted + config: + - fabric_name: my_fabric_group + register: result + +- name: Delete multiple fabric groups in a single task + cisco.nd.nd_manage_fabric_group_vxlan: + state: deleted + config: + - fabric_name: group_east + - fabric_name: group_west + - fabric_name: group_old + register: result + +- name: Gather current state of all VXLAN fabric groups + cisco.nd.nd_manage_fabric_group_vxlan: + state: gathered + register: result +""" + +RETURN = r""" +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.cisco.nd.plugins.module_utils.nd import nd_argument_spec +from ansible_collections.cisco.nd.plugins.module_utils.nd_state_machine import NDStateMachine +from ansible_collections.cisco.nd.plugins.module_utils.models.manage_fabric_group.manage_fabric_group_vxlan import FabricGroupVxlanModel +from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.manage_fabric_group_vxlan import ManageFabricGroupVxlanOrchestrator +from ansible_collections.cisco.nd.plugins.module_utils.common.exceptions import NDStateMachineError + + +def main(): + argument_spec = nd_argument_spec() + argument_spec.update(FabricGroupVxlanModel.get_argument_spec()) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + try: + # Initialize StateMachine + nd_state_machine = NDStateMachine( + module=module, + model_orchestrator=ManageFabricGroupVxlanOrchestrator, + ) + + state = module.params.get("state") + + if state == "gathered": + # Gathered state: return current config without changes + module.exit_json(changed=False, **nd_state_machine.output.format()) + else: + # Manage state for merged, replaced, overridden, deleted + nd_state_machine.manage_state() + module.exit_json(**nd_state_machine.output.format()) + + except NDStateMachineError as e: + module.fail_json(msg=str(e)) + except Exception as e: + module.fail_json(msg=f"Module execution failed: {str(e)}") + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/nd_manage_fabric_group_vxlan/tasks/main.yaml b/tests/integration/targets/nd_manage_fabric_group_vxlan/tasks/main.yaml new file mode 100644 index 00000000..6367b155 --- /dev/null +++ b/tests/integration/targets/nd_manage_fabric_group_vxlan/tasks/main.yaml @@ -0,0 +1,511 @@ +--- +# Test code for the nd_manage_fabric_group_vxlan module +# Copyright: (c) 2026, Matt Tarkington (@mtarking) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +- name: Test that we have a Nexus Dashboard host, username and password + ansible.builtin.fail: + msg: 'Please define the following variables: ansible_host, ansible_user and ansible_password.' + when: ansible_host is not defined or ansible_user is not defined or ansible_password is not defined + +- name: Set vars + ansible.builtin.set_fact: + nd_info: &nd_info + output_level: '{{ api_key_output_level | default("debug") }}' + +############################################################################# +# CLEANUP - Ensure clean state before tests +############################################################################# +- name: Clean up any existing test fabric groups before starting tests + cisco.nd.nd_manage_fabric_group_vxlan: + <<: *nd_info + state: deleted + config: + - fabric_name: "{{ fg_vxlan_test_merged }}" + - fabric_name: "{{ fg_vxlan_test_replaced }}" + tags: always + ignore_errors: true + +############################################################################# +# TEST 1: STATE MERGED - Create fabric group using merged state +############################################################################# +- name: "TEST 1a: Create fabric group using state merged (first run)" + cisco.nd.nd_manage_fabric_group_vxlan: + <<: *nd_info + state: merged + config: + - "{{ {'fabric_name': fg_vxlan_test_merged} | combine(fabric_config_fg_vxlan) }}" + register: fg_merged_result_1 + tags: [test_fg_merged, test_fg_merged_create] + +- name: "TEST 1a: Verify fabric group was created using merged state" + assert: + that: + - fg_merged_result_1 is changed + - fg_merged_result_1 is not failed + fail_msg: "Fabric group creation with state merged failed" + success_msg: "Fabric group successfully created with state merged" + tags: [test_fg_merged, test_fg_merged_create] + +- name: "TEST 1b: Create fabric group using state merged (second run - idempotency test)" + cisco.nd.nd_manage_fabric_group_vxlan: + <<: *nd_info + state: merged + config: + - "{{ {'fabric_name': fg_vxlan_test_merged} | combine(fabric_config_fg_vxlan) }}" + register: fg_merged_result_2 + tags: [test_fg_merged, test_fg_merged_idempotent] + +- name: "TEST 1b: Verify merged state is idempotent" + assert: + that: + - fg_merged_result_2 is not changed + - fg_merged_result_2 is not failed + fail_msg: "Merged state is not idempotent - should not change when run twice with same config" + success_msg: "Merged state is idempotent - no changes on second run" + tags: [test_fg_merged, test_fg_merged_idempotent] + +- name: "TEST 1c: Update fabric group using state merged (modify existing)" + cisco.nd.nd_manage_fabric_group_vxlan: + <<: *nd_info + state: merged + config: + - fabric_name: "{{ fg_vxlan_test_merged }}" + category: fabricGroup + management: + type: vxlan + anycast_gateway_mac: "2020.0000.00bb" + multisite_delay_restore: 600 + auto_multisite_underlay_inter_connect: true + bgp_bfd: true + downstream_vni: true + downstream_l2_vni_range: "70000-79000" + downstream_l3_vni_range: "90000-99000" + register: fg_merged_result_3 + tags: [test_fg_merged, test_fg_merged_update] + +- name: "TEST 1c: Verify fabric group was updated using merged state" + assert: + that: + - fg_merged_result_3 is changed + - fg_merged_result_3 is not failed + fail_msg: "Fabric group update with state merged failed" + success_msg: "Fabric group successfully updated with state merged" + tags: [test_fg_merged, test_fg_merged_update] + +############################################################################# +# VALIDATION: Query fg_vxlan_test_merged and validate expected changes +############################################################################# +- name: "VALIDATION 1: Authenticate with ND to get token" + ansible.builtin.uri: + url: "https://{{ ansible_host }}:{{ ansible_httpapi_port | default(443) }}/login" + method: POST + headers: + Content-Type: "application/json" + body_format: json + body: + domain: "{{ ansible_httpapi_login_domain | default('local') }}" + userName: "{{ ansible_user }}" + userPasswd: "{{ ansible_password }}" + validate_certs: false + return_content: true + status_code: + - 200 + register: nd_auth_response + tags: [test_fg_merged, test_fg_merged_validation] + delegate_to: localhost + +- name: "VALIDATION 1: Query fg_vxlan_test_merged configuration from ND" + ansible.builtin.uri: + url: "https://{{ ansible_host }}:{{ ansible_httpapi_port | default(443) }}/api/v1/manage/fabrics/{{ fg_vxlan_test_merged }}" + method: GET + headers: + Authorization: "Bearer {{ nd_auth_response.json.jwttoken }}" + Content-Type: "application/json" + validate_certs: false + return_content: true + status_code: + - 200 + - 404 + register: fg_merged_query + tags: [test_fg_merged, test_fg_merged_validation] + delegate_to: localhost + +- name: "VALIDATION 1: Parse fabric group configuration response" + set_fact: + fg_merged_config: "{{ fg_merged_query.json }}" + tags: [test_fg_merged, test_fg_merged_validation] + +# +# Category 1: Properties CHANGED by TEST 1c merge +# +- name: "VALIDATION 1a: Verify changed properties after merge" + assert: + that: + - fg_merged_config.management.anycastGatewayMac == "2020.0000.00bb" + - fg_merged_config.management.multisiteDelayRestore == 600 + - fg_merged_config.management.autoMultisiteUnderlayInterConnect == true + - fg_merged_config.management.bgpBfd == true + - fg_merged_config.management.downstreamVni == true + - fg_merged_config.management.downstreamL2VniRange == "70000-79000" + - fg_merged_config.management.downstreamL3VniRange == "90000-99000" + fail_msg: >- + Changed properties validation failed. + success_msg: "All 7 changed properties updated correctly" + tags: [test_fg_merged, test_fg_merged_validation] + +# +# Category 2: Properties NOT in TEST 1c - MUST be preserved from original create +# +- name: "VALIDATION 1b: Verify preserved VNI ranges (not in merge task)" + assert: + that: + - fg_merged_config.management.l2VniRange == "30000-49000" + - fg_merged_config.management.l3VniRange == "50000-59000" + fail_msg: "Preserved VNI ranges validation failed" + success_msg: "Preserved VNI ranges retained correctly" + tags: [test_fg_merged, test_fg_merged_validation] + +- name: "VALIDATION 1b: Verify preserved templates (not in merge task)" + assert: + that: + - fg_merged_config.management.vrfTemplate == "Default_VRF_Universal" + - fg_merged_config.management.networkTemplate == "Default_Network_Universal" + - fg_merged_config.management.vrfExtensionTemplate == "Default_VRF_Extension_Universal" + - fg_merged_config.management.networkExtensionTemplate == "Default_Network_Extension_Universal" + fail_msg: "Preserved template settings validation failed" + success_msg: "Preserved template settings retained correctly" + tags: [test_fg_merged, test_fg_merged_validation] + +- name: "VALIDATION 1b: Verify preserved multi-site overlay settings (not in merge task)" + assert: + that: + - fg_merged_config.management.multisiteOverlayInterConnectType == "manual" + - fg_merged_config.management.enableMsOverlayIfcBgpDesc == true + - fg_merged_config.management.multisiteLoopbackId == 100 + - fg_merged_config.management.borderGatewayRoutingTag == 54321 + - fg_merged_config.management.multisiteLoopbackIpRange == "10.10.0.0/24" + - fg_merged_config.management.multisiteUnderlaySubnetRange == "10.10.1.0/24" + - fg_merged_config.management.multisiteUnderlaySubnetTargetMask == 30 + fail_msg: "Preserved multi-site settings validation failed" + success_msg: "Preserved multi-site settings retained correctly" + tags: [test_fg_merged, test_fg_merged_validation] + +- name: "VALIDATION 1b: Verify preserved security/CloudSec settings (not in merge task)" + assert: + that: + - fg_merged_config.management.securityGroupTag == "off" + - fg_merged_config.management.autoConfigureCloudSec == false + - fg_merged_config.management.tenantRoutedMulticastV4V6 == false + fail_msg: "Preserved security/CloudSec settings validation failed" + success_msg: "Preserved security/CloudSec settings retained correctly" + tags: [test_fg_merged, test_fg_merged_validation] + +############################################################################# +# TEST 2: STATE REPLACED - Create and manage fabric group using replaced state +############################################################################# +- name: "TEST 2a: Create fabric group using state replaced (first run)" + cisco.nd.nd_manage_fabric_group_vxlan: + <<: *nd_info + state: replaced + config: + - fabric_name: "{{ fg_vxlan_test_replaced }}" + category: fabricGroup + management: + type: vxlan + l2_vni_range: "40000-59000" + l3_vni_range: "60000-69000" + anycast_gateway_mac: "2020.0000.00cc" + multisite_overlay_inter_connect_type: directPeering + auto_multisite_underlay_inter_connect: true + multisite_delay_restore: 500 + multisite_loopback_ip_range: "10.20.0.0/24" + multisite_underlay_subnet_range: "10.20.1.0/24" + multisite_underlay_subnet_target_mask: 28 + border_gateway_routing_tag: 12345 + security_group_tag: strict + register: fg_replaced_result_1 + tags: [test_fg_replaced, test_fg_replaced_create] + +- name: "TEST 2a: Verify fabric group was created using replaced state" + assert: + that: + - fg_replaced_result_1 is changed + - fg_replaced_result_1 is not failed + fail_msg: "Fabric group creation with state replaced failed" + success_msg: "Fabric group successfully created with state replaced" + tags: [test_fg_replaced, test_fg_replaced_create] + +- name: "TEST 2b: Create fabric group using state replaced (second run - idempotency test)" + cisco.nd.nd_manage_fabric_group_vxlan: + <<: *nd_info + state: replaced + config: + - fabric_name: "{{ fg_vxlan_test_replaced }}" + category: fabricGroup + management: + type: vxlan + l2_vni_range: "40000-59000" + l3_vni_range: "60000-69000" + anycast_gateway_mac: "2020.0000.00cc" + multisite_overlay_inter_connect_type: directPeering + auto_multisite_underlay_inter_connect: true + multisite_delay_restore: 500 + multisite_loopback_ip_range: "10.20.0.0/24" + multisite_underlay_subnet_range: "10.20.1.0/24" + multisite_underlay_subnet_target_mask: 28 + border_gateway_routing_tag: 12345 + security_group_tag: strict + register: fg_replaced_result_2 + tags: [test_fg_replaced, test_fg_replaced_idempotent] + +- name: "TEST 2b: Verify replaced state is idempotent" + assert: + that: + - fg_replaced_result_2 is not changed + - fg_replaced_result_2 is not failed + fail_msg: "Replaced state is not idempotent - should not change when run twice with same config" + success_msg: "Replaced state is idempotent - no changes on second run" + tags: [test_fg_replaced, test_fg_replaced_idempotent] + +- name: "TEST 2c: Update fabric group using state replaced (complete replacement with minimal fields)" + cisco.nd.nd_manage_fabric_group_vxlan: + <<: *nd_info + state: replaced + config: + - fabric_name: "{{ fg_vxlan_test_replaced }}" + category: fabricGroup + management: + type: vxlan + register: fg_replaced_result_3 + tags: [test_fg_replaced, test_fg_replaced_update] + +- name: "TEST 2c: Verify fabric group was completely replaced" + assert: + that: + - fg_replaced_result_3 is changed + - fg_replaced_result_3 is not failed + fail_msg: "Fabric group replace with minimal fields failed" + success_msg: "Fabric group successfully replaced with minimal fields" + tags: [test_fg_replaced, test_fg_replaced_update] + +############################################################################# +# VALIDATION: Query fg_vxlan_test_replaced and validate defaults +############################################################################# +- name: "VALIDATION 2: Authenticate with ND to get token" + ansible.builtin.uri: + url: "https://{{ ansible_host }}:{{ ansible_httpapi_port | default(443) }}/login" + method: POST + headers: + Content-Type: "application/json" + body_format: json + body: + domain: "{{ ansible_httpapi_login_domain | default('local') }}" + userName: "{{ ansible_user }}" + userPasswd: "{{ ansible_password }}" + validate_certs: false + return_content: true + status_code: + - 200 + register: nd_auth_response_2 + tags: [test_fg_replaced, test_fg_replaced_validation] + delegate_to: localhost + +- name: "VALIDATION 2: Query fg_vxlan_test_replaced configuration from ND" + ansible.builtin.uri: + url: "https://{{ ansible_host }}:{{ ansible_httpapi_port | default(443) }}/api/v1/manage/fabrics/{{ fg_vxlan_test_replaced }}" + method: GET + headers: + Authorization: "Bearer {{ nd_auth_response_2.json.jwttoken }}" + Content-Type: "application/json" + validate_certs: false + return_content: true + status_code: + - 200 + - 404 + register: fg_replaced_query + tags: [test_fg_replaced, test_fg_replaced_validation] + delegate_to: localhost + +- name: "VALIDATION 2: Parse fabric group configuration response" + set_fact: + fg_replaced_config: "{{ fg_replaced_query.json }}" + tags: [test_fg_replaced, test_fg_replaced_validation] + +# +# After replacing with minimal fields, all optional settings should revert to defaults +# +- name: "VALIDATION 2a: Verify VNI ranges reverted to defaults after replace" + assert: + that: + - fg_replaced_config.management.l2VniRange == "30000-49000" + - fg_replaced_config.management.l3VniRange == "50000-59000" + fail_msg: "VNI ranges did not revert to defaults after replace" + success_msg: "VNI ranges reverted to defaults correctly" + tags: [test_fg_replaced, test_fg_replaced_validation] + +- name: "VALIDATION 2a: Verify multi-site settings reverted to defaults after replace" + assert: + that: + - fg_replaced_config.management.multisiteOverlayInterConnectType == "manual" + - fg_replaced_config.management.autoMultisiteUnderlayInterConnect == false + - fg_replaced_config.management.multisiteDelayRestore == 300 + - fg_replaced_config.management.multisiteLoopbackIpRange == "10.10.0.0/24" + - fg_replaced_config.management.multisiteUnderlaySubnetRange == "10.10.1.0/24" + - fg_replaced_config.management.multisiteUnderlaySubnetTargetMask == 30 + - fg_replaced_config.management.borderGatewayRoutingTag == 54321 + fail_msg: "Multi-site settings did not revert to defaults after replace" + success_msg: "Multi-site settings reverted to defaults correctly" + tags: [test_fg_replaced, test_fg_replaced_validation] + +- name: "VALIDATION 2a: Verify security settings reverted to defaults after replace" + assert: + that: + - fg_replaced_config.management.securityGroupTag == "off" + - fg_replaced_config.management.anycastGatewayMac == "2020.0000.00aa" + fail_msg: "Security settings did not revert to defaults after replace" + success_msg: "Security settings reverted to defaults correctly" + tags: [test_fg_replaced, test_fg_replaced_validation] + +############################################################################# +# TEST 3: STATE DELETED - Delete fabric groups +############################################################################# +- name: "TEST 3a: Delete fabric group using state deleted" + cisco.nd.nd_manage_fabric_group_vxlan: + <<: *nd_info + state: deleted + config: + - fabric_name: "{{ fg_vxlan_test_replaced }}" + register: fg_deleted_result_1 + tags: [test_fg_deleted, test_fg_deleted_delete] + +- name: "TEST 3a: Verify fabric group was deleted" + assert: + that: + - fg_deleted_result_1 is changed + - fg_deleted_result_1 is not failed + fail_msg: "Fabric group deletion with state deleted failed" + success_msg: "Fabric group successfully deleted with state deleted" + tags: [test_fg_deleted, test_fg_deleted_delete] + +- name: "TEST 3b: Delete fabric group using state deleted (second run - idempotency test)" + cisco.nd.nd_manage_fabric_group_vxlan: + <<: *nd_info + state: deleted + config: + - fabric_name: "{{ fg_vxlan_test_replaced }}" + register: fg_deleted_result_2 + tags: [test_fg_deleted, test_fg_deleted_idempotent] + +- name: "TEST 3b: Verify deleted state is idempotent" + assert: + that: + - fg_deleted_result_2 is not changed + - fg_deleted_result_2 is not failed + fail_msg: "Deleted state is not idempotent - should not change when deleting non-existent fabric group" + success_msg: "Deleted state is idempotent - no changes when deleting non-existent fabric group" + tags: [test_fg_deleted, test_fg_deleted_idempotent] + +############################################################################# +# TEST 4: STATE GATHERED - Gather current fabric group configuration +############################################################################# +- name: "TEST 4a: Gather all VXLAN fabric groups" + cisco.nd.nd_manage_fabric_group_vxlan: + <<: *nd_info + state: gathered + register: fg_gathered_result_1 + tags: [test_fg_gathered] + +- name: "TEST 4a: Verify gathered state returns without changes" + assert: + that: + - fg_gathered_result_1 is not changed + - fg_gathered_result_1 is not failed + fail_msg: "Gathered state should not report changes" + success_msg: "Gathered state returned successfully without changes" + tags: [test_fg_gathered] + +############################################################################# +# TEST 5: Multiple fabric group operations in single task +############################################################################# +- name: "TEST 5: Create multiple fabric groups in single task" + cisco.nd.nd_manage_fabric_group_vxlan: + <<: *nd_info + state: merged + config: + - fabric_name: "multi_fg_1" + category: fabricGroup + management: + type: vxlan + l2_vni_range: "30000-49000" + l3_vni_range: "50000-59000" + anycast_gateway_mac: "2020.0000.0001" + multisite_loopback_ip_range: "10.10.0.0/24" + multisite_underlay_subnet_range: "10.10.1.0/24" + - fabric_name: "multi_fg_2" + category: fabricGroup + management: + type: vxlan + l2_vni_range: "30000-49000" + l3_vni_range: "50000-59000" + anycast_gateway_mac: "2020.0000.0002" + multisite_loopback_ip_range: "10.20.0.0/24" + multisite_underlay_subnet_range: "10.20.1.0/24" + register: fg_multi_result + tags: [test_fg_multi] + +- name: "TEST 5: Verify multiple fabric groups were created" + assert: + that: + - fg_multi_result is changed + - fg_multi_result is not failed + fail_msg: "Multiple fabric group creation failed" + success_msg: "Multiple fabric groups successfully created" + tags: [test_fg_multi] + +############################################################################# +# FINAL CLEANUP - Clean up all test fabric groups +############################################################################# +- name: "CLEANUP: Delete all test fabric groups" + cisco.nd.nd_manage_fabric_group_vxlan: + <<: *nd_info + state: deleted + config: + - fabric_name: "{{ fg_vxlan_test_merged }}" + - fabric_name: "{{ fg_vxlan_test_replaced }}" + - fabric_name: "multi_fg_1" + - fabric_name: "multi_fg_2" + ignore_errors: true + tags: [cleanup, always] + +############################################################################# +# TEST SUMMARY +############################################################################# +- name: "TEST SUMMARY: Display test results" + debug: + msg: | + ======================================================== + TEST SUMMARY for cisco.nd.nd_manage_fabric_group_vxlan module: + ======================================================== + TEST 1: STATE MERGED + - Create fabric group: {{ 'PASSED' if fg_merged_result_1 is changed else 'FAILED' }} + - Idempotency: {{ 'PASSED' if fg_merged_result_2 is not changed else 'FAILED' }} + - Update fabric group: {{ 'PASSED' if fg_merged_result_3 is changed else 'FAILED' }} + + TEST 2: STATE REPLACED + - Create fabric group: {{ 'PASSED' if fg_replaced_result_1 is changed else 'FAILED' }} + - Idempotency: {{ 'PASSED' if fg_replaced_result_2 is not changed else 'FAILED' }} + - Replace fabric group: {{ 'PASSED' if fg_replaced_result_3 is changed else 'FAILED' }} + + TEST 3: STATE DELETED + - Delete fabric group: {{ 'PASSED' if fg_deleted_result_1 is changed else 'FAILED' }} + - Idempotency: {{ 'PASSED' if fg_deleted_result_2 is not changed else 'FAILED' }} + + TEST 4: STATE GATHERED + - Gather all: {{ 'PASSED' if fg_gathered_result_1 is not changed else 'FAILED' }} + + TEST 5: MULTI-CREATE + - Multiple fabric groups: {{ 'PASSED' if fg_multi_result is changed else 'FAILED' }} + ======================================================== + tags: [test_summary, always] diff --git a/tests/integration/targets/nd_manage_fabric_group_vxlan/vars/main.yaml b/tests/integration/targets/nd_manage_fabric_group_vxlan/vars/main.yaml new file mode 100644 index 00000000..1f04858a --- /dev/null +++ b/tests/integration/targets/nd_manage_fabric_group_vxlan/vars/main.yaml @@ -0,0 +1,51 @@ +--- + +fg_vxlan_test_merged: "fg_vxlan_test_merged" +fg_vxlan_test_replaced: "fg_vxlan_test_replaced" +fg_vxlan_test_deleted: "fg_vxlan_test_deleted" + +# Common VXLAN Fabric Group (MSD) configuration for all fabric group tests +fabric_config_fg_vxlan: + category: fabricGroup + management: + type: vxlan + l2_vni_range: "30000-49000" + l3_vni_range: "50000-59000" + downstream_vni: false + downstream_l2_vni_range: "60000-69000" + downstream_l3_vni_range: "80000-89000" + underlay_ipv6: false + vrf_template: Default_VRF_Universal + network_template: Default_Network_Universal + vrf_extension_template: Default_VRF_Extension_Universal + network_extension_template: Default_Network_Extension_Universal + private_vlan: false + anycast_gateway_mac: "2020.0000.00aa" + multisite_overlay_inter_connect_type: manual + route_server_redistribute_direct_route_map: false + route_server_routing_tag: 54321 + enable_ms_overlay_ifc_bgp_desc: true + auto_multisite_underlay_inter_connect: false + bgp_send_community: false + bgp_log_neighbor_change: false + bgp_bfd: false + multisite_delay_restore: 300 + multisite_inter_connect_bgp_authentication: false + multisite_loopback_id: 100 + border_gateway_routing_tag: 54321 + multisite_loopback_ip_range: "10.10.0.0/24" + multisite_underlay_subnet_range: "10.10.1.0/24" + multisite_underlay_subnet_target_mask: 30 + multisite_loopback_ipv6_range: "fd00::a10:0/120" + multisite_underlay_ipv6_subnet_range: "fd00::a11:0/120" + multisite_underlay_ipv6_subnet_target_mask: 126 + tenant_routed_multicast_v4_v6: false + security_group_tag: "off" + security_group_tag_prefix: SG_ + security_group_tag_mac_segmentation: false + security_group_tag_id_range: "10000-14000" + security_group_tag_preprovision: false + auto_configure_cloud_sec: false + cloud_sec_algorithm: AES_128_CMAC + cloud_sec_enforcement: strict + cloud_sec_report_timer: 5 diff --git a/tests/unit/module_utils/endpoints/test_endpoints_api_v1_manage_fabric_group_vxlan.py b/tests/unit/module_utils/endpoints/test_endpoints_api_v1_manage_fabric_group_vxlan.py new file mode 100644 index 00000000..ab847805 --- /dev/null +++ b/tests/unit/module_utils/endpoints/test_endpoints_api_v1_manage_fabric_group_vxlan.py @@ -0,0 +1,322 @@ +# Copyright: (c) 2026, Matt Tarkington (@mtarking) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +Unit tests for manage_fabric_group_vxlan endpoint usage and orchestrator wiring. + +Tests that the ManageFabricGroupVxlanOrchestrator is correctly wired to the +shared EpManageFabrics* endpoints and that the custom query_all filtering +correctly selects only VXLAN fabric group resources. +""" + +from __future__ import absolute_import, annotations, division, print_function + +# pylint: disable=invalid-name +__metaclass__ = type +# pylint: enable=invalid-name + +import pytest +from unittest.mock import MagicMock + +from ansible_collections.cisco.nd.plugins.module_utils.endpoints.v1.manage.manage_fabrics import ( + EpManageFabricsDelete, + EpManageFabricsGet, + EpManageFabricsListGet, + EpManageFabricsPost, + EpManageFabricsPut, +) +from ansible_collections.cisco.nd.plugins.module_utils.enums import HttpVerbEnum +from ansible_collections.cisco.nd.plugins.module_utils.nd import NDModule +from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.manage_fabric_group_vxlan import ( + ManageFabricGroupVxlanOrchestrator, +) +from ansible_collections.cisco.nd.tests.unit.module_utils.common_utils import ( + does_not_raise, +) + + +# ============================================================================= +# Test: Orchestrator Endpoint Wiring +# ============================================================================= + + +def test_manage_fabric_group_vxlan_endpoints_00010(): + """ + # Summary + + Verify orchestrator is wired to the correct endpoint classes. + + ## Test + + - create_endpoint is EpManageFabricsPost + - update_endpoint is EpManageFabricsPut + - delete_endpoint is EpManageFabricsDelete + - query_one_endpoint is EpManageFabricsGet + - query_all_endpoint is EpManageFabricsListGet + """ + mock_sender = MagicMock(spec=NDModule) + with does_not_raise(): + orch = ManageFabricGroupVxlanOrchestrator(sender=mock_sender) + assert orch.create_endpoint is EpManageFabricsPost + assert orch.update_endpoint is EpManageFabricsPut + assert orch.delete_endpoint is EpManageFabricsDelete + assert orch.query_one_endpoint is EpManageFabricsGet + assert orch.query_all_endpoint is EpManageFabricsListGet + + +def test_manage_fabric_group_vxlan_endpoints_00020(): + """ + # Summary + + Verify create endpoint produces correct path and verb for fabric groups. + + ## Test + + - EpManageFabricsPost path is /api/v1/manage/fabrics + - verb is POST + """ + with does_not_raise(): + ep = EpManageFabricsPost() + assert ep.path == "/api/v1/manage/fabrics" + assert ep.verb == HttpVerbEnum.POST + + +def test_manage_fabric_group_vxlan_endpoints_00030(): + """ + # Summary + + Verify query_one endpoint produces correct path for a fabric group name. + + ## Test + + - EpManageFabricsGet with fabric_name "my-fg" returns /api/v1/manage/fabrics/my-fg + - verb is GET + """ + with does_not_raise(): + ep = EpManageFabricsGet() + ep.fabric_name = "my-fg" + assert ep.path == "/api/v1/manage/fabrics/my-fg" + assert ep.verb == HttpVerbEnum.GET + + +def test_manage_fabric_group_vxlan_endpoints_00040(): + """ + # Summary + + Verify update endpoint produces correct path for a fabric group name. + + ## Test + + - EpManageFabricsPut with fabric_name "my-fg" returns /api/v1/manage/fabrics/my-fg + - verb is PUT + """ + with does_not_raise(): + ep = EpManageFabricsPut() + ep.fabric_name = "my-fg" + assert ep.path == "/api/v1/manage/fabrics/my-fg" + assert ep.verb == HttpVerbEnum.PUT + + +def test_manage_fabric_group_vxlan_endpoints_00050(): + """ + # Summary + + Verify delete endpoint produces correct path for a fabric group name. + + ## Test + + - EpManageFabricsDelete with fabric_name "my-fg" returns /api/v1/manage/fabrics/my-fg + - verb is DELETE + """ + with does_not_raise(): + ep = EpManageFabricsDelete() + ep.fabric_name = "my-fg" + assert ep.path == "/api/v1/manage/fabrics/my-fg" + assert ep.verb == HttpVerbEnum.DELETE + + +def test_manage_fabric_group_vxlan_endpoints_00060(): + """ + # Summary + + Verify query_all endpoint produces correct path (list all fabrics). + + ## Test + + - EpManageFabricsListGet path is /api/v1/manage/fabrics + - verb is GET + """ + with does_not_raise(): + ep = EpManageFabricsListGet() + assert ep.path == "/api/v1/manage/fabrics" + assert ep.verb == HttpVerbEnum.GET + + +# ============================================================================= +# Test: Orchestrator query_all Filtering +# ============================================================================= + + +def test_manage_fabric_group_vxlan_endpoints_00100(): + """ + # Summary + + Verify query_all returns only VXLAN fabric groups from mixed results. + + ## Test + + - API returns fabrics of mixed types (fabricGroup/vxlan, fabric/vxlanIbgp, fabricGroup/other) + - query_all filters to only category=fabricGroup AND management.type=vxlan + """ + mock_sender = MagicMock(spec=NDModule) + mock_sender.query_obj.return_value = { + "fabrics": [ + {"name": "fg1", "category": "fabricGroup", "management": {"type": "vxlan"}}, + {"name": "f1", "category": "fabric", "management": {"type": "vxlanIbgp"}}, + {"name": "fg2", "category": "fabricGroup", "management": {"type": "vxlan"}}, + {"name": "fg3", "category": "fabricGroup", "management": {"type": "other"}}, + {"name": "f2", "category": "fabric", "management": {"type": "vxlanEbgp"}}, + ] + } + orch = ManageFabricGroupVxlanOrchestrator(sender=mock_sender) + result = orch.query_all() + assert len(result) == 2 + assert result[0]["name"] == "fg1" + assert result[1]["name"] == "fg2" + + +def test_manage_fabric_group_vxlan_endpoints_00110(): + """ + # Summary + + Verify query_all returns empty list when no VXLAN fabric groups exist. + + ## Test + + - API returns fabrics but none are fabricGroup/vxlan + - query_all returns empty list + """ + mock_sender = MagicMock(spec=NDModule) + mock_sender.query_obj.return_value = { + "fabrics": [ + {"name": "f1", "category": "fabric", "management": {"type": "vxlanIbgp"}}, + {"name": "f2", "category": "fabric", "management": {"type": "vxlanEbgp"}}, + ] + } + orch = ManageFabricGroupVxlanOrchestrator(sender=mock_sender) + result = orch.query_all() + assert result == [] + + +def test_manage_fabric_group_vxlan_endpoints_00120(): + """ + # Summary + + Verify query_all returns empty list when API returns empty fabrics list. + + ## Test + + - API returns {"fabrics": []} + - query_all returns empty list + """ + mock_sender = MagicMock(spec=NDModule) + mock_sender.query_obj.return_value = {"fabrics": []} + orch = ManageFabricGroupVxlanOrchestrator(sender=mock_sender) + result = orch.query_all() + assert result == [] + + +def test_manage_fabric_group_vxlan_endpoints_00130(): + """ + # Summary + + Verify query_all handles missing 'fabrics' key gracefully. + + ## Test + + - API returns {} (no fabrics key) + - query_all returns empty list + """ + mock_sender = MagicMock(spec=NDModule) + mock_sender.query_obj.return_value = {} + orch = ManageFabricGroupVxlanOrchestrator(sender=mock_sender) + result = orch.query_all() + assert result == [] + + +def test_manage_fabric_group_vxlan_endpoints_00140(): + """ + # Summary + + Verify query_all handles None fabrics value gracefully. + + ## Test + + - API returns {"fabrics": None} + - query_all returns empty list + """ + mock_sender = MagicMock(spec=NDModule) + mock_sender.query_obj.return_value = {"fabrics": None} + orch = ManageFabricGroupVxlanOrchestrator(sender=mock_sender) + result = orch.query_all() + assert result == [] + + +def test_manage_fabric_group_vxlan_endpoints_00150(): + """ + # Summary + + Verify query_all excludes fabrics with missing management key. + + ## Test + + - API returns a fabric with category=fabricGroup but no management key + - That fabric is excluded from results + """ + mock_sender = MagicMock(spec=NDModule) + mock_sender.query_obj.return_value = { + "fabrics": [ + {"name": "fg-no-mgmt", "category": "fabricGroup"}, + {"name": "fg-ok", "category": "fabricGroup", "management": {"type": "vxlan"}}, + ] + } + orch = ManageFabricGroupVxlanOrchestrator(sender=mock_sender) + result = orch.query_all() + assert len(result) == 1 + assert result[0]["name"] == "fg-ok" + + +def test_manage_fabric_group_vxlan_endpoints_00160(): + """ + # Summary + + Verify query_all raises exception when sender raises an error. + + ## Test + + - sender.query_obj raises an exception + - query_all wraps and re-raises it + """ + mock_sender = MagicMock(spec=NDModule) + mock_sender.query_obj.side_effect = ConnectionError("API unreachable") + orch = ManageFabricGroupVxlanOrchestrator(sender=mock_sender) + with pytest.raises(Exception, match="Query all failed"): + orch.query_all() + + +def test_manage_fabric_group_vxlan_endpoints_00170(): + """ + # Summary + + Verify query_all calls sender.query_obj with the correct path. + + ## Test + + - query_all invokes sender.query_obj with /api/v1/manage/fabrics + """ + mock_sender = MagicMock(spec=NDModule) + mock_sender.query_obj.return_value = {"fabrics": []} + orch = ManageFabricGroupVxlanOrchestrator(sender=mock_sender) + orch.query_all() + mock_sender.query_obj.assert_called_once_with("/api/v1/manage/fabrics") From 442d953f2cc66ec09489e0ec31c413282128ac5b Mon Sep 17 00:00:00 2001 From: Matt Tarkington Date: Thu, 9 Apr 2026 16:48:35 -0400 Subject: [PATCH 2/6] reuse existing fabric model --- .../models/manage_fabric_group/common.py | 28 ------------------- .../models/manage_fabric_group/enums.py | 13 --------- .../manage_fabric_group_vxlan.py | 6 ++-- 3 files changed, 4 insertions(+), 43 deletions(-) delete mode 100644 plugins/module_utils/models/manage_fabric_group/common.py diff --git a/plugins/module_utils/models/manage_fabric_group/common.py b/plugins/module_utils/models/manage_fabric_group/common.py deleted file mode 100644 index 6214e63c..00000000 --- a/plugins/module_utils/models/manage_fabric_group/common.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright: (c) 2026, Matt Tarkington (@mtarking) - -# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) - -""" -# Summary - -Common constants and patterns for VXLAN Fabric Group models. -""" - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - -import re - -# Regex from OpenAPI schema: bgpAsn accepts plain integers (1-4294967295) and -# dotted four-byte ASN notation (1-65535).(0-65535) -BGP_ASN_RE = re.compile( - r"^(([1-9]{1}[0-9]{0,8}|[1-3]{1}[0-9]{1,9}|[4]{1}([0-1]{1}[0-9]{8}" - r"|[2]{1}([0-8]{1}[0-9]{7}|[9]{1}([0-3]{1}[0-9]{6}|[4]{1}([0-8]{1}[0-9]{5}" - r"|[9]{1}([0-5]{1}[0-9]{4}|[6]{1}([0-6]{1}[0-9]{3}|[7]{1}([0-1]{1}[0-9]{2}" - r"|[2]{1}([0-8]{1}[0-9]{1}|[9]{1}[0-5]{1})))))))))" - r"|([1-5]\d{4}|[1-9]\d{0,3}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])" - r"(\.([1-5]\d{4}|[1-9]\d{0,3}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5]|0))?)$" -) diff --git a/plugins/module_utils/models/manage_fabric_group/enums.py b/plugins/module_utils/models/manage_fabric_group/enums.py index 87fe2309..22418b2c 100644 --- a/plugins/module_utils/models/manage_fabric_group/enums.py +++ b/plugins/module_utils/models/manage_fabric_group/enums.py @@ -11,7 +11,6 @@ ## Enums - FabricGroupTypeEnum: Fabric group type discriminator. -- BgpAuthenticationKeyTypeEnum: BGP authentication key encryption types. - MultisiteOverlayInterConnectTypeEnum: Multi-Site Overlay Interconnect type options. - CloudSecAlgorithmEnum: CloudSec encryption algorithm options. - CloudSecEnforcementEnum: CloudSec enforcement type options. @@ -39,18 +38,6 @@ class FabricGroupTypeEnum(str, Enum): VXLAN = "vxlan" -class BgpAuthenticationKeyTypeEnum(str, Enum): - """ - # Summary - - Enumeration for BGP authentication key encryption types. - """ - - THREE_DES = "3des" - TYPE6 = "type6" - TYPE7 = "type7" - - class MultisiteOverlayInterConnectTypeEnum(str, Enum): """ # Summary diff --git a/plugins/module_utils/models/manage_fabric_group/manage_fabric_group_vxlan.py b/plugins/module_utils/models/manage_fabric_group/manage_fabric_group_vxlan.py index 1d640083..2834967a 100644 --- a/plugins/module_utils/models/manage_fabric_group/manage_fabric_group_vxlan.py +++ b/plugins/module_utils/models/manage_fabric_group/manage_fabric_group_vxlan.py @@ -21,13 +21,15 @@ ) from ansible_collections.cisco.nd.plugins.module_utils.models.manage_fabric_group.enums import ( FabricGroupTypeEnum, - BgpAuthenticationKeyTypeEnum, CloudSecAlgorithmEnum, CloudSecEnforcementEnum, MultisiteOverlayInterConnectTypeEnum, SecurityGroupTagEnum, ) -from ansible_collections.cisco.nd.plugins.module_utils.models.manage_fabric_group.common import ( +from ansible_collections.cisco.nd.plugins.module_utils.models.manage_fabric.enums import ( + BgpAuthenticationKeyTypeEnum, +) +from ansible_collections.cisco.nd.plugins.module_utils.models.manage_fabric.manage_fabric_common import ( BGP_ASN_RE, ) From e004625bbc07b89edd0ca7b7e2c388796fd16bbd Mon Sep 17 00:00:00 2001 From: Matt Tarkington Date: Thu, 9 Apr 2026 17:08:24 -0400 Subject: [PATCH 3/6] update gathered --- .../modules/nd_manage_fabric_group_vxlan.py | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/plugins/modules/nd_manage_fabric_group_vxlan.py b/plugins/modules/nd_manage_fabric_group_vxlan.py index 39bab9f2..8ee90cbc 100644 --- a/plugins/modules/nd_manage_fabric_group_vxlan.py +++ b/plugins/modules/nd_manage_fabric_group_vxlan.py @@ -502,6 +502,9 @@ from ansible.module_utils.basic import AnsibleModule from ansible_collections.cisco.nd.plugins.module_utils.nd import nd_argument_spec from ansible_collections.cisco.nd.plugins.module_utils.nd_state_machine import NDStateMachine +from ansible_collections.cisco.nd.plugins.module_utils.nd_output import NDOutput +from ansible_collections.cisco.nd.plugins.module_utils.nd_config_collection import NDConfigCollection +from ansible_collections.cisco.nd.plugins.module_utils.nd import NDModule from ansible_collections.cisco.nd.plugins.module_utils.models.manage_fabric_group.manage_fabric_group_vxlan import FabricGroupVxlanModel from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.manage_fabric_group_vxlan import ManageFabricGroupVxlanOrchestrator from ansible_collections.cisco.nd.plugins.module_utils.common.exceptions import NDStateMachineError @@ -516,27 +519,34 @@ def main(): supports_check_mode=True, ) - try: - # Initialize StateMachine - nd_state_machine = NDStateMachine( - module=module, - model_orchestrator=ManageFabricGroupVxlanOrchestrator, - ) - - state = module.params.get("state") + state = module.params["state"] + try: if state == "gathered": - # Gathered state: return current config without changes - module.exit_json(changed=False, **nd_state_machine.output.format()) + # Handle gathered state: query and return without changes + nd_module = NDModule(module) + orchestrator = ManageFabricGroupVxlanOrchestrator(sender=nd_module) + response_data = orchestrator.query_all() + gathered = NDConfigCollection.from_api_response( + response_data=response_data, + model_class=FabricGroupVxlanModel, + ) + output = NDOutput(output_level=module.params.get("output_level", "normal")) + output.assign(before=gathered, after=gathered) + module.exit_json(**output.format()) else: - # Manage state for merged, replaced, overridden, deleted + # Handle merged/replaced/overridden/deleted states via the state machine + nd_state_machine = NDStateMachine( + module=module, + model_orchestrator=ManageFabricGroupVxlanOrchestrator, + ) nd_state_machine.manage_state() module.exit_json(**nd_state_machine.output.format()) except NDStateMachineError as e: module.fail_json(msg=str(e)) except Exception as e: - module.fail_json(msg=f"Module execution failed: {str(e)}") + module.fail_json(msg="Module execution failed: {0}".format(str(e))) if __name__ == "__main__": From 594b454ecd4a3b33eac49fccfa8016bce8f603a3 Mon Sep 17 00:00:00 2001 From: Matt Tarkington Date: Thu, 9 Apr 2026 17:26:29 -0400 Subject: [PATCH 4/6] fix lint error --- .../models/manage_fabric_group/manage_fabric_group_vxlan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/module_utils/models/manage_fabric_group/manage_fabric_group_vxlan.py b/plugins/module_utils/models/manage_fabric_group/manage_fabric_group_vxlan.py index 2834967a..9c540470 100644 --- a/plugins/module_utils/models/manage_fabric_group/manage_fabric_group_vxlan.py +++ b/plugins/module_utils/models/manage_fabric_group/manage_fabric_group_vxlan.py @@ -9,7 +9,7 @@ __metaclass__ = type import re -from typing import List, Dict, Any, Optional, ClassVar, Literal +from typing import List, Dict, Optional, ClassVar, Literal from ansible_collections.cisco.nd.plugins.module_utils.models.base import NDBaseModel from ansible_collections.cisco.nd.plugins.module_utils.models.nested import NDNestedModel From 8cb75b1658babf656d23c9254e1e6c3219d5f236 Mon Sep 17 00:00:00 2001 From: Matt Tarkington Date: Thu, 9 Apr 2026 17:49:32 -0400 Subject: [PATCH 5/6] black corrections --- .../manage_fabric_group/manage_fabric_group_vxlan.py | 8 ++------ .../orchestrators/manage_fabric_group_vxlan.py | 6 +----- .../test_endpoints_api_v1_manage_fabric_group_vxlan.py | 1 - 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/plugins/module_utils/models/manage_fabric_group/manage_fabric_group_vxlan.py b/plugins/module_utils/models/manage_fabric_group/manage_fabric_group_vxlan.py index 9c540470..e0008d0f 100644 --- a/plugins/module_utils/models/manage_fabric_group/manage_fabric_group_vxlan.py +++ b/plugins/module_utils/models/manage_fabric_group/manage_fabric_group_vxlan.py @@ -106,9 +106,7 @@ class VxlanFabricGroupManagementModel(NDNestedModel): model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True, populate_by_name=True, extra="allow") # Fabric Group Type (required for discriminated union) - type: Literal[FabricGroupTypeEnum.VXLAN] = Field( - description="Type of the fabric group", default=FabricGroupTypeEnum.VXLAN - ) + type: Literal[FabricGroupTypeEnum.VXLAN] = Field(description="Type of the fabric group", default=FabricGroupTypeEnum.VXLAN) # VNI Ranges l2_vni_range: str = Field( @@ -419,9 +417,7 @@ class FabricGroupVxlanModel(NDBaseModel): fabric_name: str = Field(alias="name", description="Fabric group name", min_length=1, max_length=64) # Core Management Configuration - management: Optional[VxlanFabricGroupManagementModel] = Field( - description="VXLAN fabric group management configuration", default=None - ) + management: Optional[VxlanFabricGroupManagementModel] = Field(description="VXLAN fabric group management configuration", default=None) @field_validator("fabric_name") @classmethod diff --git a/plugins/module_utils/orchestrators/manage_fabric_group_vxlan.py b/plugins/module_utils/orchestrators/manage_fabric_group_vxlan.py index 78c813b2..11f22d73 100644 --- a/plugins/module_utils/orchestrators/manage_fabric_group_vxlan.py +++ b/plugins/module_utils/orchestrators/manage_fabric_group_vxlan.py @@ -41,10 +41,6 @@ def query_all(self) -> ResponseType: api_endpoint = self.query_all_endpoint() result = self.sender.query_obj(api_endpoint.path) fabrics = result.get("fabrics", []) or [] - return [ - f - for f in fabrics - if f.get("category") == "fabricGroup" and f.get("management", {}).get("type") == "vxlan" - ] + return [f for f in fabrics if f.get("category") == "fabricGroup" and f.get("management", {}).get("type") == "vxlan"] except Exception as e: raise Exception(f"Query all failed: {e}") from e diff --git a/tests/unit/module_utils/endpoints/test_endpoints_api_v1_manage_fabric_group_vxlan.py b/tests/unit/module_utils/endpoints/test_endpoints_api_v1_manage_fabric_group_vxlan.py index ab847805..a807286f 100644 --- a/tests/unit/module_utils/endpoints/test_endpoints_api_v1_manage_fabric_group_vxlan.py +++ b/tests/unit/module_utils/endpoints/test_endpoints_api_v1_manage_fabric_group_vxlan.py @@ -35,7 +35,6 @@ does_not_raise, ) - # ============================================================================= # Test: Orchestrator Endpoint Wiring # ============================================================================= From cbe6cc3e5196c8333fd579efd7b4f1565664440a Mon Sep 17 00:00:00 2001 From: Matt Tarkington Date: Thu, 9 Apr 2026 17:54:42 -0400 Subject: [PATCH 6/6] update minimum ND version --- plugins/modules/nd_manage_fabric_group_vxlan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/nd_manage_fabric_group_vxlan.py b/plugins/modules/nd_manage_fabric_group_vxlan.py index 8ee90cbc..f45855cd 100644 --- a/plugins/modules/nd_manage_fabric_group_vxlan.py +++ b/plugins/modules/nd_manage_fabric_group_vxlan.py @@ -343,7 +343,7 @@ - cisco.nd.modules - cisco.nd.check_mode notes: -- This module is only supported on Nexus Dashboard having version 4.1.0 or higher. +- This module is only supported on Nexus Dashboard having version 4.2.1 or higher. - Only VXLAN fabric group type (C(vxlan)) is supported by this module. - When using O(state=replaced) with only required fields, all optional management settings revert to their defaults. - Fabric group member management (add/remove members) is not handled by this module. Use a dedicated member module.