From a7b4b2edd4c9f9faf85ae8ce1b6632a0ab80ad45 Mon Sep 17 00:00:00 2001 From: Matt Tarkington Date: Sat, 11 Apr 2026 06:30:29 -0400 Subject: [PATCH] initial ndb fabric type --- .../models/manage_fabric/enums.py | 9 +- .../models/manage_fabric/manage_fabric_ndb.py | 227 ++++++++ .../orchestrators/manage_fabric_ndb.py | 46 ++ plugins/modules/nd_manage_fabric_ndb.py | 352 ++++++++++++ .../nd_manage_fabric/tasks/fabric_ndb.yaml | 536 ++++++++++++++++++ .../targets/nd_manage_fabric/tasks/main.yaml | 3 + .../targets/nd_manage_fabric/vars/main.yaml | 18 + 7 files changed, 1188 insertions(+), 3 deletions(-) create mode 100644 plugins/module_utils/models/manage_fabric/manage_fabric_ndb.py create mode 100644 plugins/module_utils/orchestrators/manage_fabric_ndb.py create mode 100644 plugins/modules/nd_manage_fabric_ndb.py create mode 100644 tests/integration/targets/nd_manage_fabric/tasks/fabric_ndb.yaml diff --git a/plugins/module_utils/models/manage_fabric/enums.py b/plugins/module_utils/models/manage_fabric/enums.py index 8bb17076..157d22fd 100644 --- a/plugins/module_utils/models/manage_fabric/enums.py +++ b/plugins/module_utils/models/manage_fabric/enums.py @@ -29,13 +29,16 @@ class FabricTypeEnum(str, Enum): ## Values - - `VXLAN_IBGP` - VXLAN fabric with iBGP overlay + - `DATA_BROKER` - Data Broker (NDB) fabric + - `EXTERNAL_CONNECTIVITY` - External connectivity fabric - `VXLAN_EBGP` - VXLAN fabric with eBGP overlay + - `VXLAN_IBGP` - VXLAN fabric with iBGP overlay """ - VXLAN_IBGP = "vxlanIbgp" - VXLAN_EBGP = "vxlanEbgp" + DATA_BROKER = "dataBroker" EXTERNAL_CONNECTIVITY = "externalConnectivity" + VXLAN_EBGP = "vxlanEbgp" + VXLAN_IBGP = "vxlanIbgp" class AlertSuspendEnum(str, Enum): diff --git a/plugins/module_utils/models/manage_fabric/manage_fabric_ndb.py b/plugins/module_utils/models/manage_fabric/manage_fabric_ndb.py new file mode 100644 index 00000000..8bbc3cfc --- /dev/null +++ b/plugins/module_utils/models/manage_fabric/manage_fabric_ndb.py @@ -0,0 +1,227 @@ +# -*- 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 Dict, List, 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.enums import ( + FabricTypeEnum, + AlertSuspendEnum, + LicenseTierEnum, + TelemetryCollectionTypeEnum, + TelemetryStreamingProtocolEnum, +) +from ansible_collections.cisco.nd.plugins.module_utils.models.manage_fabric.manage_fabric_common import ( + LocationModel, + TelemetrySettingsModel, + ExternalStreamingSettingsModel, +) + +""" +# Pydantic models for Data Broker (NDB) fabric management via Nexus Dashboard + +This module provides Pydantic models for creating, updating, and deleting +Data Broker (Nexus Dashboard Data Broker) fabrics through the Nexus Dashboard +Fabric Controller (NDFC) API. + +## Models Overview + +- `DataBrokerManagementModel` - Data Broker specific management settings (minimal — type only) +- `FabricDataBrokerModel` - Complete fabric creation model + +## Usage + +```python +fabric_data = { + "name": "MyNdbFabric", + "management": { + "type": "dataBroker", + } +} +fabric = FabricDataBrokerModel(**fabric_data) +``` +""" + + +class DataBrokerManagementModel(NDNestedModel): + """ + # Summary + + Data Broker fabric management configuration. + + The dataBroker management schema is minimal — it contains only the type + discriminator field. Unlike eBGP/iBGP fabrics, there are no additional + fabric-level management parameters. + + ## Raises + + None + """ + + model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True, populate_by_name=True, extra="allow") + + # Fabric Type (required for discriminated union) + type: Literal[FabricTypeEnum.DATA_BROKER] = Field( + description="Fabric management type", + default=FabricTypeEnum.DATA_BROKER, + ) + + +class FabricDataBrokerModel(NDBaseModel): + """ + # Summary + + Complete model for creating a new Data Broker (NDB) fabric. + + This model combines all necessary components for fabric creation including + basic fabric properties, the minimal dataBroker management settings, + telemetry, and streaming configuration. + + ## 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 Properties + category: Literal["fabric"] = Field(description="Resource category", default="fabric") + fabric_name: str = Field(alias="name", description="Fabric name", min_length=1, max_length=64) + location: Optional[LocationModel] = Field(description="Geographic location of the fabric", default=None) + + # License and Operations + license_tier: LicenseTierEnum = Field( + alias="licenseTier", + description="License Tier value of a fabric.", + default=LicenseTierEnum.PREMIER, + ) + alert_suspend: AlertSuspendEnum = Field( + alias="alertSuspend", + description="Alert Suspend state configured on the fabric", + default=AlertSuspendEnum.DISABLED, + ) + telemetry_collection: bool = Field( + alias="telemetryCollection", + description="Enable telemetry collection", + default=False, + ) + telemetry_collection_type: TelemetryCollectionTypeEnum = Field( + alias="telemetryCollectionType", + description="Telemetry collection method.", + default=TelemetryCollectionTypeEnum.OUT_OF_BAND, + ) + telemetry_streaming_protocol: TelemetryStreamingProtocolEnum = Field( + alias="telemetryStreamingProtocol", + description="Telemetry Streaming Protocol.", + default=TelemetryStreamingProtocolEnum.IPV4, + ) + telemetry_source_interface: str = Field( + alias="telemetrySourceInterface", + description="Telemetry Source Interface (VLAN id or Loopback id) only valid if Telemetry Collection is set to inBand", + default="", + ) + telemetry_source_vrf: str = Field( + alias="telemetrySourceVrf", + description="VRF over which telemetry is streamed, valid only if telemetry collection is set to inband", + default="", + ) + security_domain: str = Field( + alias="securityDomain", + description="Security Domain associated with the fabric", + default="all", + ) + + # Core Management Configuration (minimal for dataBroker) + management: Optional[DataBrokerManagementModel] = Field( + description="Data Broker management configuration", + default=None, + ) + + # Optional Advanced Settings + telemetry_settings: Optional[TelemetrySettingsModel] = Field( + alias="telemetrySettings", + description="Telemetry configuration", + default=None, + ) + external_streaming_settings: ExternalStreamingSettingsModel = Field( + alias="externalStreamingSettings", + description="External streaming settings", + default_factory=ExternalStreamingSettingsModel, + ) + + @field_validator("fabric_name") + @classmethod + def validate_fabric_name(cls, value: str) -> str: + """ + # Summary + + Validate fabric name format and characters. + + ## Raises + + - `ValueError` - If name contains invalid characters or format + """ + if not re.match(r"^[a-zA-Z0-9_-]+$", value): + raise ValueError( + f"Fabric name can only contain letters, numbers, underscores, and hyphens, got: {value}" + ) + return value + + @model_validator(mode="after") + def validate_fabric_consistency(self) -> "FabricDataBrokerModel": + """ + # Summary + + Validate consistency between fabric settings and management configuration. + + ## Raises + + - `ValueError` - If fabric settings are inconsistent + """ + # Ensure management type matches model type + if self.management is not None and self.management.type != FabricTypeEnum.DATA_BROKER: + raise ValueError(f"Management type must be {FabricTypeEnum.DATA_BROKER}") + + # Validate telemetry consistency + if self.telemetry_collection and self.telemetry_settings is None: + self.telemetry_settings = TelemetrySettingsModel() + + return self + + @classmethod + def get_argument_spec(cls) -> Dict: + return dict( + state={ + "type": "str", + "default": "merged", + "choices": ["merged", "replaced", "deleted", "overridden"], + }, + config={"required": False, "type": "list", "elements": "dict"}, + ) + + +__all__ = [ + "DataBrokerManagementModel", + "FabricDataBrokerModel", +] diff --git a/plugins/module_utils/orchestrators/manage_fabric_ndb.py b/plugins/module_utils/orchestrators/manage_fabric_ndb.py new file mode 100644 index 00000000..686f8c09 --- /dev/null +++ b/plugins/module_utils/orchestrators/manage_fabric_ndb.py @@ -0,0 +1,46 @@ +# -*- 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.manage_fabric_ndb import FabricDataBrokerModel +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 ManageNdbFabricOrchestrator(NDBaseOrchestrator): + model_class: Type[NDBaseModel] = FabricDataBrokerModel + + 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 dataBroker fabric types. + """ + 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("management", {}).get("type") == "dataBroker"] + except Exception as e: + raise Exception(f"Query all failed: {e}") from e diff --git a/plugins/modules/nd_manage_fabric_ndb.py b/plugins/modules/nd_manage_fabric_ndb.py new file mode 100644 index 00000000..1e7b189d --- /dev/null +++ b/plugins/modules/nd_manage_fabric_ndb.py @@ -0,0 +1,352 @@ +#!/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_ndb +version_added: "1.4.0" +short_description: Manage Data Broker (NDB) fabrics on Cisco Nexus Dashboard +description: +- Manage Nexus Dashboard Data Broker (NDB) fabrics on Cisco Nexus Dashboard (ND). +- It supports creating, updating, replacing, and deleting Data Broker fabrics. +- The Data Broker fabric type (C(dataBroker)) has minimal management settings compared to other fabric types. +author: +- Matt Tarkington (@mtarking) +options: + config: + description: + - The list of Data Broker fabrics to configure. + type: list + elements: dict + suboptions: + fabric_name: + description: + - The name of the fabric. + - Only letters, numbers, underscores, and hyphens are allowed. + - The O(config.fabric_name) must be defined when creating, updating or deleting a fabric. + type: str + required: true + category: + description: + - The resource category. + type: str + default: fabric + location: + description: + - The geographic location of the fabric. + type: dict + suboptions: + latitude: + description: + - Latitude coordinate of the fabric location (-90 to 90). + type: float + required: true + longitude: + description: + - Longitude coordinate of the fabric location (-180 to 180). + type: float + required: true + license_tier: + description: + - License Tier value of a fabric. + type: str + default: premier + choices: [ essentials, advantage, premier ] + alert_suspend: + description: + - Alert Suspend state configured on the fabric. + type: str + default: disabled + choices: [ enabled, disabled ] + telemetry_collection: + description: + - Enable telemetry collection for the fabric. + type: bool + default: false + telemetry_collection_type: + description: + - Telemetry collection method. + type: str + default: outOfBand + choices: [ inBand, outOfBand ] + telemetry_streaming_protocol: + description: + - Telemetry Streaming Protocol. + type: str + default: ipv4 + choices: [ ipv4, ipv6 ] + telemetry_source_interface: + description: + - Telemetry Source Interface (VLAN id or Loopback id) only valid if Telemetry Collection is set to inBand. + type: str + default: "" + telemetry_source_vrf: + description: + - VRF over which telemetry is streamed, valid only if telemetry collection is set to inband. + type: str + default: "" + security_domain: + description: + - Security Domain associated with the fabric. + type: str + default: all + management: + description: + - The Data Broker management configuration for the fabric. + - The Data Broker fabric type has minimal management settings — only the C(type) discriminator. + type: dict + suboptions: + type: + description: + - The fabric management type. Must be C(dataBroker) for Data Broker fabrics. + type: str + default: dataBroker + choices: [ dataBroker ] + telemetry_settings: + description: + - Telemetry configuration for the fabric. + type: dict + suboptions: + flow_collection: + description: + - Flow collection settings. + type: dict + suboptions: + traffic_analytics: + description: + - Traffic analytics state. + type: str + default: enabled + traffic_analytics_scope: + description: + - Traffic analytics scope. + type: str + default: intraFabric + operating_mode: + description: + - Operating mode. + type: str + default: flowTelemetry + udp_categorization: + description: + - UDP categorization. + type: str + default: enabled + microburst: + description: + - Microburst detection settings. + type: dict + suboptions: + microburst: + description: + - Enable microburst detection. + type: bool + default: false + sensitivity: + description: + - Microburst sensitivity level. + type: str + default: low + analysis_settings: + description: + - Analysis settings. + type: dict + suboptions: + is_enabled: + description: + - Enable telemetry analysis. + type: bool + default: false + nas: + description: + - NAS telemetry configuration. + type: dict + suboptions: + server: + description: + - NAS server address. + type: str + default: "" + export_settings: + description: + - NAS export settings. + type: dict + suboptions: + export_type: + description: + - Export type. + type: str + default: full + export_format: + description: + - Export format. + type: str + default: json + energy_management: + description: + - Energy management settings. + type: dict + suboptions: + cost: + description: + - Energy cost per unit. + type: float + default: 1.2 + external_streaming_settings: + description: + - External streaming settings for the fabric. + type: dict + suboptions: + email: + description: + - Email streaming configuration. + type: list + elements: dict + message_bus: + description: + - Message bus configuration. + type: list + elements: dict + syslog: + description: + - Syslog streaming configuration. + type: dict + webhooks: + description: + - Webhook configuration. + type: list + elements: dict + state: + description: + - The desired state of the fabric resources on the Cisco Nexus Dashboard. + - Use O(state=merged) to create new fabrics 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 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 existing on ND but not present in the configuration will be deleted. Use with extra caution. + - Use O(state=deleted) to remove the fabrics specified in the configuration from the Cisco Nexus Dashboard. + type: str + default: merged + choices: [ merged, replaced, overridden, deleted ] +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 Data Broker fabric type (C(dataBroker)) is supported by this module. +- The Data Broker management configuration is minimal — it contains only the C(type) discriminator field. +""" + +EXAMPLES = r""" +- name: Create a Data Broker fabric using state merged + cisco.nd.nd_manage_fabric_ndb: + state: merged + config: + - fabric_name: my_ndb_fabric + category: fabric + location: + latitude: 37.7749 + longitude: -122.4194 + license_tier: premier + alert_suspend: disabled + security_domain: all + telemetry_collection: false + management: + type: dataBroker + register: result + +- name: Update location on an existing Data Broker fabric using state merged + cisco.nd.nd_manage_fabric_ndb: + state: merged + config: + - fabric_name: my_ndb_fabric + location: + latitude: 40.7128 + longitude: -74.0060 + register: result + +- name: Replace a Data Broker fabric configuration using state replaced + cisco.nd.nd_manage_fabric_ndb: + state: replaced + config: + - fabric_name: my_ndb_fabric + category: fabric + location: + latitude: 37.7749 + longitude: -122.4194 + license_tier: advantage + alert_suspend: enabled + security_domain: all + telemetry_collection: true + telemetry_collection_type: inBand + management: + type: dataBroker + register: result + +- name: Delete a Data Broker fabric using state deleted + cisco.nd.nd_manage_fabric_ndb: + state: deleted + config: + - fabric_name: my_ndb_fabric + register: result + +- name: Delete multiple Data Broker fabrics in a single task + cisco.nd.nd_manage_fabric_ndb: + state: deleted + config: + - fabric_name: ndb_fabric_east + - fabric_name: ndb_fabric_west + 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.manage_fabric_ndb import FabricDataBrokerModel +from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.manage_fabric_ndb import ManageNdbFabricOrchestrator +from ansible_collections.cisco.nd.plugins.module_utils.common.exceptions import NDStateMachineError + + +def main(): + argument_spec = nd_argument_spec() + argument_spec.update(FabricDataBrokerModel.get_argument_spec()) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + try: + # Initialize StateMachine + nd_state_machine = NDStateMachine( + module=module, + model_orchestrator=ManageNdbFabricOrchestrator, + ) + + # Manage state + 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/tasks/fabric_ndb.yaml b/tests/integration/targets/nd_manage_fabric/tasks/fabric_ndb.yaml new file mode 100644 index 00000000..13d12e10 --- /dev/null +++ b/tests/integration/targets/nd_manage_fabric/tasks/fabric_ndb.yaml @@ -0,0 +1,536 @@ +--- +# Test code for the nd_manage_fabric_ndb 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 fabrics before starting tests + cisco.nd.nd_manage_fabric_ndb: + <<: *nd_info + state: deleted + config: + - fabric_name: "{{ ndb_test_fabric_merged }}" + - fabric_name: "{{ ndb_test_fabric_replaced }}" + - fabric_name: "{{ ndb_test_fabric_deleted }}" + tags: always + +############################################################################# +# TEST 1: STATE MERGED - Create fabric using merged state +############################################################################# +- name: "TEST 1a: Create NDB fabric using state merged (first run)" + cisco.nd.nd_manage_fabric_ndb: + <<: *nd_info + state: merged + config: + - "{{ {'fabric_name': ndb_test_fabric_merged} | combine(fabric_config_ndb) }}" + register: ndb_merged_result_1 + tags: [test_merged, test_merged_create] + +- name: "TEST 1a: Verify NDB fabric was created using merged state" + assert: + that: + - ndb_merged_result_1 is changed + - ndb_merged_result_1 is not failed + fail_msg: "NDB fabric creation with state merged failed" + success_msg: "NDB fabric successfully created with state merged" + tags: [test_merged, test_merged_create] + +- name: "TEST 1b: Create NDB fabric using state merged (second run - idempotency test)" + cisco.nd.nd_manage_fabric_ndb: + <<: *nd_info + state: merged + config: + - "{{ {'fabric_name': ndb_test_fabric_merged} | combine(fabric_config_ndb) }}" + register: ndb_merged_result_2 + tags: [test_merged, test_merged_idempotent] + +- name: "TEST 1b: Verify merged state is idempotent" + assert: + that: + - ndb_merged_result_2 is not changed + - ndb_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_merged, test_merged_idempotent] + +- name: "TEST 1c: Update NDB fabric using state merged (modify existing)" + cisco.nd.nd_manage_fabric_ndb: + <<: *nd_info + state: merged + config: + - fabric_name: "{{ ndb_test_fabric_merged }}" + category: fabric + location: + latitude: 40.7128 + longitude: -74.0060 + license_tier: advantage # Changed from premier + alert_suspend: enabled # Changed from disabled + security_domain: admin # Changed from all + management: + type: dataBroker + register: ndb_merged_result_3 + tags: [test_merged, test_merged_update] + +- name: "TEST 1c: Verify NDB fabric was updated using merged state" + assert: + that: + - ndb_merged_result_3 is changed + - ndb_merged_result_3 is not failed + fail_msg: "NDB fabric update with state merged failed" + success_msg: "NDB fabric successfully updated with state merged" + tags: [test_merged, test_merged_update] + +############################################################################# +# VALIDATION: Query ndb_test_fabric_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_merged, test_merged_validation] + delegate_to: localhost + +- name: "VALIDATION 1: Query ndb_test_fabric_merged configuration from ND" + ansible.builtin.uri: + url: "https://{{ ansible_host }}:{{ ansible_httpapi_port | default(443) }}/api/v1/manage/fabrics/{{ ndb_test_fabric_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: ndb_merged_fabric_query + tags: [test_merged, test_merged_validation] + delegate_to: localhost + +- name: "VALIDATION 1: Parse fabric configuration response" + set_fact: + ndb_merged_fabric_config: "{{ ndb_merged_fabric_query.json }}" + tags: [test_merged, test_merged_validation] + +- name: "VALIDATION 1: Verify management type is dataBroker" + assert: + that: + - ndb_merged_fabric_config.management.type == "dataBroker" + fail_msg: "Management type validation failed. Expected: dataBroker, Actual: {{ ndb_merged_fabric_config.management.type }}" + success_msg: "✓ Management type correctly set to dataBroker" + tags: [test_merged, test_merged_validation] + +- name: "VALIDATION 1: Verify license tier was updated to advantage" + assert: + that: + - ndb_merged_fabric_config.licenseTier == "advantage" + fail_msg: "License tier validation failed. Expected: advantage, Actual: {{ ndb_merged_fabric_config.licenseTier }}" + success_msg: "✓ License tier correctly updated to advantage" + tags: [test_merged, test_merged_validation] + +- name: "VALIDATION 1: Verify alert suspend was updated to enabled" + assert: + that: + - ndb_merged_fabric_config.alertSuspend == "enabled" + fail_msg: "Alert suspend validation failed. Expected: enabled, Actual: {{ ndb_merged_fabric_config.alertSuspend }}" + success_msg: "✓ Alert suspend correctly updated to enabled" + tags: [test_merged, test_merged_validation] + +- name: "VALIDATION 1: Display successful validation summary for ndb_test_fabric_merged" + debug: + msg: | + ======================================== + VALIDATION SUMMARY for ndb_test_fabric_merged: + ======================================== + ✓ Management Type: {{ ndb_merged_fabric_config.management.type }} + ✓ License Tier: {{ ndb_merged_fabric_config.licenseTier }} + ✓ Alert Suspend: {{ ndb_merged_fabric_config.alertSuspend }} + + All expected changes validated successfully! + ======================================== + tags: [test_merged, test_merged_validation] + +############################################################################# +# TEST 2: STATE REPLACED - Create and manage fabric using replaced state +############################################################################# +- name: "TEST 2a: Create NDB fabric using state replaced (first run)" + cisco.nd.nd_manage_fabric_ndb: + <<: *nd_info + state: replaced + config: + - fabric_name: "{{ ndb_test_fabric_replaced }}" + category: fabric + location: + latitude: 37.7749 + longitude: -122.4194 + license_tier: advantage # Different from default (premier) + alert_suspend: enabled # Different from default (disabled) + security_domain: admin # Different from default (all) + telemetry_collection: true # Different from default (false) + telemetry_collection_type: inBand # Different from default (outOfBand) + management: + type: dataBroker + register: ndb_replaced_result_1 + tags: [test_replaced, test_replaced_create] + +- name: "TEST 2a: Verify NDB fabric was created using replaced state" + assert: + that: + - ndb_replaced_result_1 is changed + - ndb_replaced_result_1 is not failed + fail_msg: "NDB fabric creation with state replaced failed" + success_msg: "NDB fabric successfully created with state replaced" + tags: [test_replaced, test_replaced_create] + +- name: "TEST 2b: Create NDB fabric using state replaced (second run - idempotency test)" + cisco.nd.nd_manage_fabric_ndb: + <<: *nd_info + state: replaced + config: + - fabric_name: "{{ ndb_test_fabric_replaced }}" + category: fabric + location: + latitude: 37.7749 + longitude: -122.4194 + license_tier: advantage + alert_suspend: enabled + security_domain: admin + telemetry_collection: true + telemetry_collection_type: inBand + management: + type: dataBroker + register: ndb_replaced_result_2 + tags: [test_replaced, test_replaced_idempotent] + +- name: "TEST 2b: Verify replaced state is idempotent" + assert: + that: + - ndb_replaced_result_2 is not changed + - ndb_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_replaced, test_replaced_idempotent] + +- name: "TEST 2c: Update NDB fabric using state replaced (complete replacement with minimal config)" + cisco.nd.nd_manage_fabric_ndb: + <<: *nd_info + state: replaced + config: + - fabric_name: "{{ ndb_test_fabric_replaced }}" + category: fabric + management: + type: dataBroker + register: ndb_replaced_result_3 + tags: [test_replaced, test_replaced_update] + +- name: "TEST 2c: Verify NDB fabric was completely replaced" + assert: + that: + - ndb_replaced_result_3 is changed + - ndb_replaced_result_3 is not failed + fail_msg: "NDB fabric replacement with state replaced failed" + success_msg: "NDB fabric successfully replaced with state replaced" + tags: [test_replaced, test_replaced_update] + +############################################################################# +# VALIDATION: Query ndb_test_fabric_replaced and validate defaults restored +############################################################################# +- 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_replaced, test_replaced_validation] + delegate_to: localhost + +- name: "VALIDATION 2: Query ndb_test_fabric_replaced configuration from ND" + ansible.builtin.uri: + url: "https://{{ ansible_host }}:{{ ansible_httpapi_port | default(443) }}/api/v1/manage/fabrics/{{ ndb_test_fabric_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: ndb_replaced_fabric_query + tags: [test_replaced, test_replaced_validation] + delegate_to: localhost + +- name: "VALIDATION 2: Parse fabric configuration response" + set_fact: + ndb_replaced_fabric_config: "{{ ndb_replaced_fabric_query.json }}" + tags: [test_replaced, test_replaced_validation] + +- name: "VALIDATION 2: Verify license tier was restored to default (premier)" + assert: + that: + - ndb_replaced_fabric_config.licenseTier == "premier" + fail_msg: "License tier validation failed. Expected: premier, Actual: {{ ndb_replaced_fabric_config.licenseTier }}" + success_msg: "✓ License tier correctly restored to default (premier)" + tags: [test_replaced, test_replaced_validation] + +- name: "VALIDATION 2: Verify alert suspend was restored to default (disabled)" + assert: + that: + - ndb_replaced_fabric_config.alertSuspend == "disabled" + fail_msg: "Alert suspend validation failed. Expected: disabled, Actual: {{ ndb_replaced_fabric_config.alertSuspend }}" + success_msg: "✓ Alert suspend correctly restored to default (disabled)" + tags: [test_replaced, test_replaced_validation] + +- name: "VALIDATION 2: Verify security domain was restored to default (all)" + assert: + that: + - ndb_replaced_fabric_config.securityDomain == "all" + fail_msg: "Security domain validation failed. Expected: all, Actual: {{ ndb_replaced_fabric_config.securityDomain }}" + success_msg: "✓ Security domain correctly restored to default (all)" + tags: [test_replaced, test_replaced_validation] + +- name: "VALIDATION 2: Verify telemetry collection was restored to default (false)" + assert: + that: + - ndb_replaced_fabric_config.telemetryCollection == false + fail_msg: "Telemetry collection validation failed. Expected: false, Actual: {{ ndb_replaced_fabric_config.telemetryCollection }}" + success_msg: "✓ Telemetry collection correctly restored to default (false)" + tags: [test_replaced, test_replaced_validation] + +- name: "VALIDATION 2: Display successful validation summary for ndb_test_fabric_replaced" + debug: + msg: | + ======================================== + VALIDATION SUMMARY for ndb_test_fabric_replaced: + ======================================== + ✓ License Tier: {{ ndb_replaced_fabric_config.licenseTier }} + ✓ Alert Suspend: {{ ndb_replaced_fabric_config.alertSuspend }} + ✓ Security Domain: {{ ndb_replaced_fabric_config.securityDomain }} + ✓ Telemetry Collection: {{ ndb_replaced_fabric_config.telemetryCollection }} + + All defaults correctly restored after replaced with minimal config! + ======================================== + tags: [test_replaced, test_replaced_validation] + +############################################################################# +# TEST 3: Demonstrate difference between merged and replaced states +############################################################################# +- name: "TEST 3: Create NDB fabric for merged vs replaced comparison" + cisco.nd.nd_manage_fabric_ndb: + <<: *nd_info + state: replaced + config: + - "{{ {'fabric_name': ndb_test_fabric_deleted} | combine(fabric_config_ndb) }}" + register: ndb_comparison_fabric_creation + tags: [test_comparison] + +- name: "TEST 3a: Partial update using merged state (should merge changes)" + cisco.nd.nd_manage_fabric_ndb: + <<: *nd_info + state: merged + config: + - fabric_name: "{{ ndb_test_fabric_deleted }}" + category: fabric + license_tier: advantage # Only updating license tier + register: ndb_merged_partial_result + tags: [test_comparison, test_merged_partial] + +- name: "TEST 3a: Verify merged state preserves existing configuration" + assert: + that: + - ndb_merged_partial_result is changed + - ndb_merged_partial_result is not failed + fail_msg: "Partial update with merged state failed" + success_msg: "Merged state successfully performed partial update" + tags: [test_comparison, test_merged_partial] + +- name: "TEST 3b: Partial update using replaced state (should replace entire config)" + cisco.nd.nd_manage_fabric_ndb: + <<: *nd_info + state: replaced + config: + - fabric_name: "{{ ndb_test_fabric_deleted }}" + category: fabric + management: + type: dataBroker + register: ndb_replaced_partial_result + tags: [test_comparison, test_replaced_partial] + +- name: "TEST 3b: Verify replaced state performs complete replacement" + assert: + that: + - ndb_replaced_partial_result is changed + - ndb_replaced_partial_result is not failed + fail_msg: "Partial replacement with replaced state failed" + success_msg: "Replaced state successfully performed complete replacement" + tags: [test_comparison, test_replaced_partial] + +############################################################################# +# TEST 4: STATE DELETED - Delete fabrics +############################################################################# +- name: "TEST 4a: Delete NDB fabric using state deleted" + cisco.nd.nd_manage_fabric_ndb: + <<: *nd_info + state: deleted + config: + - fabric_name: "{{ ndb_test_fabric_deleted }}" + register: ndb_deleted_result_1 + tags: [test_deleted, test_deleted_delete] + +- name: "TEST 4a: Verify NDB fabric was deleted" + assert: + that: + - ndb_deleted_result_1 is changed + - ndb_deleted_result_1 is not failed + fail_msg: "NDB fabric deletion with state deleted failed" + success_msg: "NDB fabric successfully deleted with state deleted" + tags: [test_deleted, test_deleted_delete] + +- name: "TEST 4b: Delete NDB fabric using state deleted (second run - idempotency test)" + cisco.nd.nd_manage_fabric_ndb: + <<: *nd_info + state: deleted + config: + - fabric_name: "{{ ndb_test_fabric_deleted }}" + register: ndb_deleted_result_2 + tags: [test_deleted, test_deleted_idempotent] + +- name: "TEST 4b: Verify deleted state is idempotent" + assert: + that: + - ndb_deleted_result_2 is not changed + - ndb_deleted_result_2 is not failed + fail_msg: "Deleted state is not idempotent - should not change when deleting non-existent fabric" + success_msg: "Deleted state is idempotent - no changes when deleting non-existent fabric" + tags: [test_deleted, test_deleted_idempotent] + +############################################################################# +# TEST 5: Multiple fabric operations in single task +############################################################################# +- name: "TEST 5: Multiple NDB fabric operations in single task" + cisco.nd.nd_manage_fabric_ndb: + <<: *nd_info + state: merged + config: + - fabric_name: "ndb_multi_fabric_1" + category: fabric + location: + latitude: 37.7749 + longitude: -122.4194 + license_tier: premier + alert_suspend: disabled + security_domain: all + telemetry_collection: false + management: + type: dataBroker + - fabric_name: "ndb_multi_fabric_2" + category: fabric + location: + latitude: 40.7128 + longitude: -74.0060 + license_tier: advantage + alert_suspend: disabled + security_domain: all + telemetry_collection: false + management: + type: dataBroker + register: ndb_multi_fabric_result + tags: [test_multi, test_multi_create] + +- name: "TEST 5: Verify multiple NDB fabrics were created" + assert: + that: + - ndb_multi_fabric_result is changed + - ndb_multi_fabric_result is not failed + fail_msg: "Multiple NDB fabric creation failed" + success_msg: "Multiple NDB fabrics successfully created" + tags: [test_multi, test_multi_create] + +############################################################################# +# FINAL CLEANUP - Clean up all test fabrics +############################################################################# +- name: "CLEANUP: Delete all NDB test fabrics" + cisco.nd.nd_manage_fabric_ndb: + <<: *nd_info + state: deleted + config: + - fabric_name: "{{ ndb_test_fabric_merged }}" + - fabric_name: "{{ ndb_test_fabric_replaced }}" + - fabric_name: "{{ ndb_test_fabric_deleted }}" + - fabric_name: "ndb_multi_fabric_1" + - fabric_name: "ndb_multi_fabric_2" + ignore_errors: true + tags: [cleanup, always] + +############################################################################# +# TEST SUMMARY +############################################################################# +- name: "TEST SUMMARY: Display NDB test results" + debug: + msg: | + ======================================================== + TEST SUMMARY for cisco.nd.nd_manage_fabric_ndb module: + ======================================================== + ✓ TEST 1: STATE MERGED + - Create fabric: {{ 'PASSED' if ndb_merged_result_1 is changed else 'FAILED' }} + - Idempotency: {{ 'PASSED' if ndb_merged_result_2 is not changed else 'FAILED' }} + - Update fabric: {{ 'PASSED' if ndb_merged_result_3 is changed else 'FAILED' }} + + ✓ TEST 2: STATE REPLACED + - Create fabric: {{ 'PASSED' if ndb_replaced_result_1 is changed else 'FAILED' }} + - Idempotency: {{ 'PASSED' if ndb_replaced_result_2 is not changed else 'FAILED' }} + - Replace fabric: {{ 'PASSED' if ndb_replaced_result_3 is changed else 'FAILED' }} + + ✓ TEST 3: MERGED vs REPLACED Comparison + - Merged partial: {{ 'PASSED' if ndb_merged_partial_result is changed else 'FAILED' }} + - Replaced partial: {{ 'PASSED' if ndb_replaced_partial_result is changed else 'FAILED' }} + + ✓ TEST 4: STATE DELETED + - Delete fabric: {{ 'PASSED' if ndb_deleted_result_1 is changed else 'FAILED' }} + - Idempotency: {{ 'PASSED' if ndb_deleted_result_2 is not changed else 'FAILED' }} + + ✓ TEST 5: MULTIPLE FABRICS + - Multi-create: {{ 'PASSED' if ndb_multi_fabric_result is changed else 'FAILED' }} + + All tests validate: + - State merged: Creates and updates NDB fabrics by merging changes + - State replaced: Creates and completely replaces NDB fabric configuration + - State deleted: Removes NDB fabrics + - Idempotency: All operations are idempotent when run multiple times + - Difference: Merged preserves existing config, replaced overwrites completely + ======================================== + tags: [summary, always] diff --git a/tests/integration/targets/nd_manage_fabric/tasks/main.yaml b/tests/integration/targets/nd_manage_fabric/tasks/main.yaml index eacc3be3..f87f3553 100644 --- a/tests/integration/targets/nd_manage_fabric/tasks/main.yaml +++ b/tests/integration/targets/nd_manage_fabric/tasks/main.yaml @@ -7,3 +7,6 @@ - name: Run nd_manage_fabric External Connectivity tests ansible.builtin.include_tasks: fabric_external.yaml + +- name: Run nd_manage_fabric Data Broker (NDB) tests + ansible.builtin.include_tasks: fabric_ndb.yaml diff --git a/tests/integration/targets/nd_manage_fabric/vars/main.yaml b/tests/integration/targets/nd_manage_fabric/vars/main.yaml index 893b17bb..1d778c8a 100644 --- a/tests/integration/targets/nd_manage_fabric/vars/main.yaml +++ b/tests/integration/targets/nd_manage_fabric/vars/main.yaml @@ -12,6 +12,10 @@ ext_test_fabric_merged: "ext_test_fabric_merged" ext_test_fabric_replaced: "ext_test_fabric_replaced" ext_test_fabric_deleted: "ext_test_fabric_deleted" +ndb_test_fabric_merged: "ndb_test_fabric_merged" +ndb_test_fabric_replaced: "ndb_test_fabric_replaced" +ndb_test_fabric_deleted: "ndb_test_fabric_deleted" + # Common fabric configuration for all tests # common_fabric_config: fabric_config_ibgp: @@ -245,6 +249,20 @@ fabric_config_external: management_gateway: "" management_ipv4_prefix: 24 +# Common Data Broker (NDB) fabric configuration for all NDB tests +# common_ndb_fabric_config: +fabric_config_ndb: + category: fabric + location: + latitude: 37.7749 + longitude: -122.4194 + license_tier: premier + alert_suspend: disabled + security_domain: all + telemetry_collection: false + management: + type: dataBroker + # Common eBGP fabric configuration for all eBGP tests # common_ebgp_fabric_config: fabric_config_ebgp: