From fc479ecbd33d54caac1e38fd43a265e593515825 Mon Sep 17 00:00:00 2001 From: Gaspard Micol Date: Wed, 20 May 2026 05:52:16 -0400 Subject: [PATCH] [minor_change] Add module for Routing Policies Prefix Lists. --- plugins/module_utils/endpoints/mixins.py | 6 + .../v1/manage/manage_prefix_lists.py | 402 ++++++++++++++++++ .../models/manage_prefix_list/__init__.py | 3 + .../models/manage_prefix_list/enums.py | 26 ++ .../manage_prefix_list/manage_prefix_list.py | 363 ++++++++++++++++ .../orchestrators/manage_prefix_list.py | 237 +++++++++++ plugins/modules/nd_manage_prefix_list.py | 286 +++++++++++++ 7 files changed, 1323 insertions(+) create mode 100644 plugins/module_utils/endpoints/v1/manage/manage_prefix_lists.py create mode 100644 plugins/module_utils/models/manage_prefix_list/__init__.py create mode 100644 plugins/module_utils/models/manage_prefix_list/enums.py create mode 100644 plugins/module_utils/models/manage_prefix_list/manage_prefix_list.py create mode 100644 plugins/module_utils/orchestrators/manage_prefix_list.py create mode 100644 plugins/modules/nd_manage_prefix_list.py diff --git a/plugins/module_utils/endpoints/mixins.py b/plugins/module_utils/endpoints/mixins.py index e7f0620c9..ac7ab66c1 100644 --- a/plugins/module_utils/endpoints/mixins.py +++ b/plugins/module_utils/endpoints/mixins.py @@ -80,6 +80,12 @@ class SwitchSerialNumberMixin(BaseModel): switch_sn: Optional[str] = Field(default=None, min_length=1, description="Switch serial number") +class PrefixListNameMixin(BaseModel): + """Mixin for endpoints that require prefix_list_name parameter.""" + + prefix_list_name: Optional[str] = Field(default=None, min_length=1, max_length=115, description="Prefix list name") + + class VrfNameMixin(BaseModel): """Mixin for endpoints that require vrf_name parameter.""" diff --git a/plugins/module_utils/endpoints/v1/manage/manage_prefix_lists.py b/plugins/module_utils/endpoints/v1/manage/manage_prefix_lists.py new file mode 100644 index 000000000..001bb7602 --- /dev/null +++ b/plugins/module_utils/endpoints/v1/manage/manage_prefix_lists.py @@ -0,0 +1,402 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2026, Gaspard Micol (@gmicol) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +ND Manage Prefix Lists endpoint models (IPv4 and IPv6). + +This module contains endpoint definitions for prefix-list operations +in the ND Manage API, covering both IPv4 and IPv6 prefix lists. + +## IPv4 Endpoints + +- `EpManageIpv4PrefixListsGet` - Get a specific IPv4 prefix list by name + (GET /api/v1/manage/fabrics/{fabricName}/ipv4PrefixLists/{ipv4PrefixListName}) +- `EpManageIpv4PrefixListsListGet` - List all IPv4 prefix lists for a fabric + (GET /api/v1/manage/fabrics/{fabricName}/ipv4PrefixLists) +- `EpManageIpv4PrefixListsPost` - Bulk-create IPv4 prefix lists + (POST /api/v1/manage/fabrics/{fabricName}/ipv4PrefixLists) +- `EpManageIpv4PrefixListsPut` - Update a specific IPv4 prefix list + (PUT /api/v1/manage/fabrics/{fabricName}/ipv4PrefixLists/{ipv4PrefixListName}) +- `EpManageIpv4PrefixListsDelete` - Delete a specific IPv4 prefix list + (DELETE /api/v1/manage/fabrics/{fabricName}/ipv4PrefixLists/{ipv4PrefixListName}) +- `EpManageIpv4PrefixListsBulkDelete` - Bulk-delete IPv4 prefix lists + (POST /api/v1/manage/fabrics/{fabricName}/ipv4PrefixListActions/remove) + +## IPv6 Endpoints + +- `EpManageIpv6PrefixListsGet` - Get a specific IPv6 prefix list by name + (GET /api/v1/manage/fabrics/{fabricName}/ipv6PrefixLists/{ipv6PrefixListName}) +- `EpManageIpv6PrefixListsListGet` - List all IPv6 prefix lists for a fabric + (GET /api/v1/manage/fabrics/{fabricName}/ipv6PrefixLists) +- `EpManageIpv6PrefixListsPost` - Bulk-create IPv6 prefix lists + (POST /api/v1/manage/fabrics/{fabricName}/ipv6PrefixLists) +- `EpManageIpv6PrefixListsPut` - Update a specific IPv6 prefix list + (PUT /api/v1/manage/fabrics/{fabricName}/ipv6PrefixLists/{ipv6PrefixListName}) +- `EpManageIpv6PrefixListsDelete` - Delete a specific IPv6 prefix list + (DELETE /api/v1/manage/fabrics/{fabricName}/ipv6PrefixLists/{ipv6PrefixListName}) +- `EpManageIpv6PrefixListsBulkDelete` - Bulk-delete IPv6 prefix lists + (POST /api/v1/manage/fabrics/{fabricName}/ipv6PrefixListActions/remove) +""" + +from __future__ import annotations + +__metaclass__ = type + +from typing import ClassVar, Literal, Optional, Tuple, Union + +from ansible_collections.cisco.nd.plugins.module_utils.enums import HttpVerbEnum +from ansible_collections.cisco.nd.plugins.module_utils.endpoints.v1.manage.base_path import BasePath +from ansible_collections.cisco.nd.plugins.module_utils.endpoints.mixins import FabricNameMixin, PrefixListNameMixin +from ansible_collections.cisco.nd.plugins.module_utils.endpoints.base import NDEndpointBaseModel +from ansible_collections.cisco.nd.plugins.module_utils.endpoints.query_params import EndpointQueryParams +from ansible_collections.cisco.nd.plugins.module_utils.common.pydantic_compat import Field +from ansible_collections.cisco.nd.plugins.module_utils.types import IdentifierKey + + +class PrefixListsEndpointParams(EndpointQueryParams): + """ + Query parameters shared by single-item prefix list endpoints. + + - cluster_name: Name of the target Nexus Dashboard cluster (multi-cluster deployments) + """ + + cluster_name: Optional[str] = Field( + default=None, + min_length=1, + description="Name of the target Nexus Dashboard cluster to execute this API, in a multi-cluster deployment", + ) + + +class PrefixListsListEndpointParams(EndpointQueryParams): + """ + Query parameters for prefix list collection (list) endpoints. + + - cluster_name: multi-cluster target cluster name + - filter: Lucene-format filter string + - max: maximum number of records + - offset: records to skip for pagination + - sort: sort field with optional ``:desc`` suffix + """ + + cluster_name: Optional[str] = Field( + default=None, + min_length=1, + description="Name of the target Nexus Dashboard cluster to execute this API, in a multi-cluster deployment", + ) + + filter: Optional[str] = Field( + default=None, + description="Lucene format filter - Filter the response based on this filter field", + ) + + max: Optional[int] = Field( + default=None, + ge=1, + description="Number of records to return", + ) + + offset: Optional[int] = Field( + default=None, + ge=0, + description="Number of records to skip for pagination", + ) + + sort: Optional[str] = Field( + default=None, + description="Sort the records by the declared fields in either ascending (default) or descending (:desc) order", + ) + + +# --------------------------------------------------------------------------- +# IPv4 base class +# --------------------------------------------------------------------------- + + +class _EpManageIpv4PrefixListsBase(PrefixListNameMixin, FabricNameMixin, NDEndpointBaseModel): + """ + Base class for IPv4 prefix list endpoints. + + Path: ``/api/v1/manage/fabrics/{fabricName}/ipv4PrefixLists[/{name}]`` + + Subclasses set ``_require_prefix_list_name = False`` for collection-level + operations (list, bulk-create). + """ + + _require_prefix_list_name: ClassVar[bool] = True + + endpoint_params: EndpointQueryParams = Field(default_factory=EndpointQueryParams, description="Endpoint-specific query parameters") + + def set_identifiers(self, identifier: IdentifierKey = None) -> None: + """ + Accept either a plain name or a composite tuple ``("ipv4", name)`` + and assign the prefix list name. + """ + if isinstance(identifier, tuple): + self.prefix_list_name = identifier[1] + else: + self.prefix_list_name = identifier + + @property + def path(self) -> str: + if self._require_prefix_list_name and self.fabric_name is None: + raise ValueError(f"{type(self).__name__}.path: fabric_name must be set before accessing path.") + if self._require_prefix_list_name and self.prefix_list_name is None: + raise ValueError(f"{type(self).__name__}.path: prefix_list_name must be set before accessing path.") + segments = ["fabrics"] + if self.fabric_name is not None: + segments.append(self.fabric_name) + segments.append("ipv4PrefixLists") + if self.prefix_list_name is not None: + segments.append(self.prefix_list_name) + base = BasePath.path(*segments) + qs = self.endpoint_params.to_query_string() + return f"{base}?{qs}" if qs else base + + +class EpManageIpv4PrefixListsGet(_EpManageIpv4PrefixListsBase): + """ + GET /api/v1/manage/fabrics/{fabricName}/ipv4PrefixLists/{ipv4PrefixListName} + + Retrieve a specific IPv4 prefix list by name. + """ + + class_name: Literal["EpManageIpv4PrefixListsGet"] = Field(default="EpManageIpv4PrefixListsGet", description="Class name for backward compatibility") + endpoint_params: PrefixListsEndpointParams = Field(default_factory=PrefixListsEndpointParams) + + @property + def verb(self) -> HttpVerbEnum: + return HttpVerbEnum.GET + + +class EpManageIpv4PrefixListsListGet(_EpManageIpv4PrefixListsBase): + """ + GET /api/v1/manage/fabrics/{fabricName}/ipv4PrefixLists + + List all IPv4 prefix lists for a fabric. + """ + + _require_prefix_list_name: ClassVar[bool] = False + + class_name: Literal["EpManageIpv4PrefixListsListGet"] = Field(default="EpManageIpv4PrefixListsListGet", description="Class name for backward compatibility") + endpoint_params: PrefixListsListEndpointParams = Field(default_factory=PrefixListsListEndpointParams) + + @property + def verb(self) -> HttpVerbEnum: + return HttpVerbEnum.GET + + +class EpManageIpv4PrefixListsPost(_EpManageIpv4PrefixListsBase): + """ + POST /api/v1/manage/fabrics/{fabricName}/ipv4PrefixLists + + Bulk-create IPv4 prefix lists. + Request body: ``{"ipv4PrefixLists": [...]}``. + """ + + _require_prefix_list_name: ClassVar[bool] = False + + class_name: Literal["EpManageIpv4PrefixListsPost"] = Field(default="EpManageIpv4PrefixListsPost", description="Class name for backward compatibility") + endpoint_params: PrefixListsEndpointParams = Field(default_factory=PrefixListsEndpointParams) + + @property + def verb(self) -> HttpVerbEnum: + return HttpVerbEnum.POST + + +class EpManageIpv4PrefixListsPut(_EpManageIpv4PrefixListsBase): + """ + PUT /api/v1/manage/fabrics/{fabricName}/ipv4PrefixLists/{ipv4PrefixListName} + + Update a specific IPv4 prefix list. + Request body: ``ipv4PrefixListItem`` schema. + """ + + class_name: Literal["EpManageIpv4PrefixListsPut"] = Field(default="EpManageIpv4PrefixListsPut", description="Class name for backward compatibility") + endpoint_params: PrefixListsEndpointParams = Field(default_factory=PrefixListsEndpointParams) + + @property + def verb(self) -> HttpVerbEnum: + return HttpVerbEnum.PUT + + +class EpManageIpv4PrefixListsDelete(_EpManageIpv4PrefixListsBase): + """ + DELETE /api/v1/manage/fabrics/{fabricName}/ipv4PrefixLists/{ipv4PrefixListName} + + Delete a specific IPv4 prefix list. + """ + + class_name: Literal["EpManageIpv4PrefixListsDelete"] = Field(default="EpManageIpv4PrefixListsDelete", description="Class name for backward compatibility") + endpoint_params: PrefixListsEndpointParams = Field(default_factory=PrefixListsEndpointParams) + + @property + def verb(self) -> HttpVerbEnum: + return HttpVerbEnum.DELETE + + +class EpManageIpv4PrefixListsBulkDelete(FabricNameMixin, NDEndpointBaseModel): + """ + POST /api/v1/manage/fabrics/{fabricName}/ipv4PrefixListActions/remove + + Bulk-delete IPv4 prefix lists. + Request body: ``{"ipv4PrefixListNames": ["name1", ...]}``. + """ + + class_name: Literal["EpManageIpv4PrefixListsBulkDelete"] = Field(default="EpManageIpv4PrefixListsBulkDelete", description="Class name for backward compatibility") + + @property + def path(self) -> str: + if self.fabric_name is None: + raise ValueError(f"{type(self).__name__}.path: fabric_name must be set before accessing path.") + return BasePath.path("fabrics", self.fabric_name, "ipv4PrefixListActions", "remove") + + @property + def verb(self) -> HttpVerbEnum: + return HttpVerbEnum.POST + + +# --------------------------------------------------------------------------- +# IPv6 base class +# --------------------------------------------------------------------------- + + +class _EpManageIpv6PrefixListsBase(PrefixListNameMixin, FabricNameMixin, NDEndpointBaseModel): + """ + Base class for IPv6 prefix list endpoints. + + Path: ``/api/v1/manage/fabrics/{fabricName}/ipv6PrefixLists[/{name}]`` + """ + + _require_prefix_list_name: ClassVar[bool] = True + + endpoint_params: EndpointQueryParams = Field(default_factory=EndpointQueryParams, description="Endpoint-specific query parameters") + + def set_identifiers(self, identifier: IdentifierKey = None) -> None: + """ + Accept either a plain name or a composite tuple ``("ipv6", name)`` + and assign the prefix list name. + """ + if isinstance(identifier, tuple): + self.prefix_list_name = identifier[1] + else: + self.prefix_list_name = identifier + + @property + def path(self) -> str: + if self._require_prefix_list_name and self.fabric_name is None: + raise ValueError(f"{type(self).__name__}.path: fabric_name must be set before accessing path.") + if self._require_prefix_list_name and self.prefix_list_name is None: + raise ValueError(f"{type(self).__name__}.path: prefix_list_name must be set before accessing path.") + segments = ["fabrics"] + if self.fabric_name is not None: + segments.append(self.fabric_name) + segments.append("ipv6PrefixLists") + if self.prefix_list_name is not None: + segments.append(self.prefix_list_name) + base = BasePath.path(*segments) + qs = self.endpoint_params.to_query_string() + return f"{base}?{qs}" if qs else base + + +class EpManageIpv6PrefixListsGet(_EpManageIpv6PrefixListsBase): + """ + GET /api/v1/manage/fabrics/{fabricName}/ipv6PrefixLists/{ipv6PrefixListName} + + Retrieve a specific IPv6 prefix list by name. + """ + + class_name: Literal["EpManageIpv6PrefixListsGet"] = Field(default="EpManageIpv6PrefixListsGet", description="Class name for backward compatibility") + endpoint_params: PrefixListsEndpointParams = Field(default_factory=PrefixListsEndpointParams) + + @property + def verb(self) -> HttpVerbEnum: + return HttpVerbEnum.GET + + +class EpManageIpv6PrefixListsListGet(_EpManageIpv6PrefixListsBase): + """ + GET /api/v1/manage/fabrics/{fabricName}/ipv6PrefixLists + + List all IPv6 prefix lists for a fabric. + """ + + _require_prefix_list_name: ClassVar[bool] = False + + class_name: Literal["EpManageIpv6PrefixListsListGet"] = Field(default="EpManageIpv6PrefixListsListGet", description="Class name for backward compatibility") + endpoint_params: PrefixListsListEndpointParams = Field(default_factory=PrefixListsListEndpointParams) + + @property + def verb(self) -> HttpVerbEnum: + return HttpVerbEnum.GET + + +class EpManageIpv6PrefixListsPost(_EpManageIpv6PrefixListsBase): + """ + POST /api/v1/manage/fabrics/{fabricName}/ipv6PrefixLists + + Bulk-create IPv6 prefix lists. + Request body: ``{"ipv6PrefixLists": [...]}``. + """ + + _require_prefix_list_name: ClassVar[bool] = False + + class_name: Literal["EpManageIpv6PrefixListsPost"] = Field(default="EpManageIpv6PrefixListsPost", description="Class name for backward compatibility") + endpoint_params: PrefixListsEndpointParams = Field(default_factory=PrefixListsEndpointParams) + + @property + def verb(self) -> HttpVerbEnum: + return HttpVerbEnum.POST + + +class EpManageIpv6PrefixListsPut(_EpManageIpv6PrefixListsBase): + """ + PUT /api/v1/manage/fabrics/{fabricName}/ipv6PrefixLists/{ipv6PrefixListName} + + Update a specific IPv6 prefix list. + Request body: ``ipv6PrefixListItem`` schema. + """ + + class_name: Literal["EpManageIpv6PrefixListsPut"] = Field(default="EpManageIpv6PrefixListsPut", description="Class name for backward compatibility") + endpoint_params: PrefixListsEndpointParams = Field(default_factory=PrefixListsEndpointParams) + + @property + def verb(self) -> HttpVerbEnum: + return HttpVerbEnum.PUT + + +class EpManageIpv6PrefixListsDelete(_EpManageIpv6PrefixListsBase): + """ + DELETE /api/v1/manage/fabrics/{fabricName}/ipv6PrefixLists/{ipv6PrefixListName} + + Delete a specific IPv6 prefix list. + """ + + class_name: Literal["EpManageIpv6PrefixListsDelete"] = Field(default="EpManageIpv6PrefixListsDelete", description="Class name for backward compatibility") + endpoint_params: PrefixListsEndpointParams = Field(default_factory=PrefixListsEndpointParams) + + @property + def verb(self) -> HttpVerbEnum: + return HttpVerbEnum.DELETE + + +class EpManageIpv6PrefixListsBulkDelete(FabricNameMixin, NDEndpointBaseModel): + """ + POST /api/v1/manage/fabrics/{fabricName}/ipv6PrefixListActions/remove + + Bulk-delete IPv6 prefix lists. + Request body: ``{"ipv6PrefixListNames": ["name1", ...]}``. + """ + + class_name: Literal["EpManageIpv6PrefixListsBulkDelete"] = Field(default="EpManageIpv6PrefixListsBulkDelete", description="Class name for backward compatibility") + + @property + def path(self) -> str: + if self.fabric_name is None: + raise ValueError(f"{type(self).__name__}.path: fabric_name must be set before accessing path.") + return BasePath.path("fabrics", self.fabric_name, "ipv6PrefixListActions", "remove") + + @property + def verb(self) -> HttpVerbEnum: + return HttpVerbEnum.POST diff --git a/plugins/module_utils/models/manage_prefix_list/__init__.py b/plugins/module_utils/models/manage_prefix_list/__init__.py new file mode 100644 index 000000000..1de5fa344 --- /dev/null +++ b/plugins/module_utils/models/manage_prefix_list/__init__.py @@ -0,0 +1,3 @@ +# Copyright: (c) 2026, Gaspard Micol (@gmicol) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) diff --git a/plugins/module_utils/models/manage_prefix_list/enums.py b/plugins/module_utils/models/manage_prefix_list/enums.py new file mode 100644 index 000000000..a9e66243a --- /dev/null +++ b/plugins/module_utils/models/manage_prefix_list/enums.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2026, Gaspard Micol (@gmicol) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +Enumerations for Prefix List management. +""" + +from __future__ import absolute_import, division, print_function + +from enum import Enum + + +class PrefixListActionEnum(str, Enum): + """Permit/deny action for a prefix list entry.""" + + PERMIT = "permit" + DENY = "deny" + + +class IpVersionEnum(str, Enum): + """IP version discriminator for a prefix list.""" + + IPV4 = "ipv4" + IPV6 = "ipv6" diff --git a/plugins/module_utils/models/manage_prefix_list/manage_prefix_list.py b/plugins/module_utils/models/manage_prefix_list/manage_prefix_list.py new file mode 100644 index 000000000..3b9ea4ff4 --- /dev/null +++ b/plugins/module_utils/models/manage_prefix_list/manage_prefix_list.py @@ -0,0 +1,363 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2026, Gaspard Micol (@gmicol) + +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +Pydantic models for Prefix List management (IPv4 and IPv6) via Nexus Dashboard. + +This module provides a unified set of models for creating, updating, and +deleting both IPv4 and IPv6 prefix lists through the Nexus Dashboard Fabric +Controller (NDFC) Manage API. + +## Models Overview + +- ``PrefixListEntryModel`` - A single prefix list entry + (sequenceNumber + action + prefix + optional length constraints) +- ``PrefixListModel`` - Complete prefix list + (ip_version + name + entries) + +## Design Notes + +IPv4 and IPv6 prefix lists share the same JSON schema structure and are +distinguished solely by the ``ip_version`` field (values: ``"ipv4"`` / +``"ipv6"``). This Ansible-only field is: + - Injected by the orchestrator during ``query_all`` (not present in API + responses). + - Excluded from API payloads via ``payload_exclude_fields``. + - Used as part of the composite identifier ``(ip_version, name)`` so that + an IPv4 "PL-1" and an IPv6 "PL-1" coexist in the same collection. + +Entry-level validation checks that ``prefix`` and ``mask`` (when supplied) +match the declared ``ip_version`` of the containing prefix list. + +## Usage + +```python +pl = PrefixListModel.from_config({ + "ip_version": "ipv4", + "name": "PL-IPV4-1", + "entries": [ + {"sequence_number": 10, "action": "permit", "prefix": "10.0.0.0/8"}, + {"sequence_number": 20, "action": "deny", "prefix": "192.168.0.0/16", + "min_prefix_length": 24, "max_prefix_length": 32}, + ], +}) +payload = pl.to_payload() +# {"name": "PL-IPV4-1", "entries": [{"sequenceNumber": 10, "action": "permit", +# "prefix": "10.0.0.0/8"}, ...]} +``` +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import ipaddress +import re +from typing import Any, ClassVar, Dict, List, Literal, Optional, Set, Tuple + +from ansible_collections.cisco.nd.plugins.module_utils.common.pydantic_compat import ( + Field, + field_validator, + model_validator, +) +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.models.manage_prefix_list.enums import ( + IpVersionEnum, + PrefixListActionEnum, +) + +# Allowed characters for prefix list / tenant names (from OpenAPI pattern) +_NAME_RE = re.compile(r"^[a-zA-Z0-9~_-]+$") +_TENANT_RE = re.compile(r"^[A-Za-z0-9_-]+$") + + +class PrefixListEntryModel(NDNestedModel): + """ + A single entry in a prefix list. + + Required fields: ``sequence_number``, ``action``, ``prefix``. + Optional length constraints: ``exact_length``, ``min_prefix_length``, + ``max_prefix_length``, ``mask``. + + Length-constraint ranges (validated at the PrefixListModel level): + - IPv4: 1-32 + - IPv6: 1-128 + """ + + sequence_number: int = Field( + alias="sequenceNumber", + ge=1, + le=4294967294, + description="Sequence number of the entry (1-4294967294).", + ) + + action: PrefixListActionEnum = Field( + alias="action", + description="Action for matching prefixes: permit or deny.", + ) + + prefix: str = Field( + alias="prefix", + description="IP prefix in CIDR notation (e.g. '10.1.1.0/24' or '2001:db8::/32').", + ) + + exact_length: Optional[int] = Field( + default=None, + alias="exactLength", + description="Exact prefix length to match.", + ) + + # NOTE: Python field names avoid collision with Pydantic's built-in + # ``min_length`` / ``max_length`` Field constraints. + min_prefix_length: Optional[int] = Field( + default=None, + alias="minLength", + description="Minimum prefix length to match.", + ) + + max_prefix_length: Optional[int] = Field( + default=None, + alias="maxLength", + description="Maximum prefix length to match.", + ) + + mask: Optional[str] = Field( + default=None, + alias="mask", + description="Network mask in dotted-decimal (IPv4) or full IPv6 format.", + ) + + +class PrefixListModel(NDBaseModel): + """ + Prefix list configuration (IPv4 or IPv6) for a Nexus Dashboard fabric. + + ## Identifier + + ``composite`` strategy using ``(ip_version, name)``, which allows an + IPv4 prefix list and an IPv6 prefix list with the same name to coexist + in the same ``NDConfigCollection``. + + ## Serialization Notes + + - ``ip_version`` is an Ansible-only field; it is excluded from API payloads. + - ``last_update_timestamp`` is read-only and excluded from payloads and diffs. + - Validation ensures that ``entries[*].prefix`` and ``entries[*].mask`` + (when set) are syntactically valid CIDRs/addresses for the declared + ``ip_version``. + """ + + # --- Identifier Configuration --- + + identifiers: ClassVar[Optional[List[str]]] = ["ip_version", "name"] + identifier_strategy: ClassVar[Optional[Literal["single", "composite", "hierarchical", "singleton"]]] = "composite" + + # --- Serialization Configuration --- + + exclude_from_diff: ClassVar[Set[str]] = {"last_update_timestamp"} + payload_exclude_fields: ClassVar[Set[str]] = {"ip_version", "last_update_timestamp"} + unwanted_keys: ClassVar[List] = [] + + # --- Fields --- + + ip_version: IpVersionEnum = Field( + alias="ipVersion", + description="IP version of this prefix list: 'ipv4' or 'ipv6'. " + "This is an Ansible-only field and is not sent to the API.", + ) + + name: str = Field( + alias="name", + min_length=1, + max_length=115, + description="Name of the prefix list (pattern: ^[a-zA-Z0-9~_-]+$).", + ) + + description: Optional[str] = Field( + default=None, + alias="description", + max_length=90, + description="Description of the prefix list.", + ) + + last_update_timestamp: Optional[str] = Field( + default=None, + alias="lastUpdateTimestamp", + description="Timestamp of the last update (read-only, set by ND).", + ) + + tenant_name: Optional[str] = Field( + default=None, + alias="tenantName", + max_length=63, + description="Name of the tenant (pattern: ^[A-Za-z0-9_-]+$).", + ) + + entries: List[PrefixListEntryModel] = Field( + alias="entries", + description="List of prefix list entries.", + ) + + # --- Field Validators --- + + @field_validator("name") + @classmethod + def validate_name(cls, value: str) -> str: + """Enforce API name pattern: ^[a-zA-Z0-9~_-]+$.""" + if not _NAME_RE.match(value): + raise ValueError( + f"Prefix list name '{value}' is invalid. " + "Only alphanumeric characters and '~', '_', '-' are allowed." + ) + return value + + @field_validator("tenant_name") + @classmethod + def validate_tenant_name(cls, value: Optional[str]) -> Optional[str]: + """Enforce tenant name pattern: ^[A-Za-z0-9_-]+$.""" + if value is not None and not _TENANT_RE.match(value): + raise ValueError( + f"Tenant name '{value}' is invalid. " + "Only alphanumeric characters, '_', and '-' are allowed." + ) + return value + + # --- Model Validators (cross-field) --- + + @model_validator(mode="after") + def validate_entries_for_ip_version(self) -> "PrefixListModel": + """ + Validate that every entry's ``prefix`` and ``mask`` (when set) are + syntactically correct for the declared ``ip_version``, and that + length constraints are within the valid range. + """ + version = str(self.ip_version) + max_len = 32 if version == "ipv4" else 128 + + for idx, entry in enumerate(self.entries): + # Validate prefix CIDR + try: + network = ipaddress.ip_network(entry.prefix, strict=False) + except ValueError: + raise ValueError( + f"entries[{idx}].prefix '{entry.prefix}' is not a valid IP network in CIDR notation." + ) + if version == "ipv4" and not isinstance(network, ipaddress.IPv4Network): + raise ValueError( + f"entries[{idx}].prefix '{entry.prefix}' must be an IPv4 CIDR (ip_version='ipv4')." + ) + if version == "ipv6" and not isinstance(network, ipaddress.IPv6Network): + raise ValueError( + f"entries[{idx}].prefix '{entry.prefix}' must be an IPv6 CIDR (ip_version='ipv6')." + ) + + # Validate mask (when present) + if entry.mask is not None: + try: + addr = ipaddress.ip_address(entry.mask) + except ValueError: + raise ValueError( + f"entries[{idx}].mask '{entry.mask}' is not a valid IP address." + ) + if version == "ipv4" and not isinstance(addr, ipaddress.IPv4Address): + raise ValueError( + f"entries[{idx}].mask '{entry.mask}' must be an IPv4 address (ip_version='ipv4')." + ) + if version == "ipv6" and not isinstance(addr, ipaddress.IPv6Address): + raise ValueError( + f"entries[{idx}].mask '{entry.mask}' must be an IPv6 address (ip_version='ipv6')." + ) + + # Validate length constraints + for field_name, label in [ + ("exact_length", "exactLength"), + ("min_prefix_length", "minLength"), + ("max_prefix_length", "maxLength"), + ]: + val = getattr(entry, field_name, None) + if val is not None and not (1 <= val <= max_len): + raise ValueError( + f"entries[{idx}].{label}={val} is out of range " + f"(1-{max_len} for ip_version='{version}')." + ) + + return self + + # --- Argument Spec --- + + @classmethod + def get_argument_spec(cls) -> Dict[str, Any]: + return dict( + fabric_name=dict( + type="str", + required=True, + ), + config=dict( + type="list", + elements="dict", + required=True, + options=dict( + ip_version=dict( + type="str", + required=True, + choices=["ipv4", "ipv6"], + aliases=["ipVersion"], + ), + name=dict( + type="str", + required=True, + ), + description=dict( + type="str", + ), + tenant_name=dict( + type="str", + aliases=["tenantName"], + ), + entries=dict( + type="list", + elements="dict", + required=True, + options=dict( + sequence_number=dict( + type="int", + required=True, + aliases=["sequenceNumber"], + ), + action=dict( + type="str", + required=True, + choices=["permit", "deny"], + ), + prefix=dict( + type="str", + required=True, + ), + exact_length=dict( + type="int", + aliases=["exactLength"], + ), + min_prefix_length=dict( + type="int", + aliases=["minLength"], + ), + max_prefix_length=dict( + type="int", + aliases=["maxLength"], + ), + mask=dict( + type="str", + ), + ), + ), + ), + ), + state=dict( + type="str", + default="merged", + choices=["merged", "replaced", "overridden", "deleted"], + ), + ) diff --git a/plugins/module_utils/orchestrators/manage_prefix_list.py b/plugins/module_utils/orchestrators/manage_prefix_list.py new file mode 100644 index 000000000..bb941c2d3 --- /dev/null +++ b/plugins/module_utils/orchestrators/manage_prefix_list.py @@ -0,0 +1,237 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2026, Gaspard Micol (@gmicol) + +# 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 ClassVar, Dict, List, Optional, Tuple, Type + +from ansible_collections.cisco.nd.plugins.module_utils.common.pydantic_compat import Field +from ansible_collections.cisco.nd.plugins.module_utils.endpoints.base import NDEndpointBaseModel +from ansible_collections.cisco.nd.plugins.module_utils.endpoints.v1.manage.manage_prefix_lists import ( + EpManageIpv4PrefixListsBulkDelete, + EpManageIpv4PrefixListsDelete, + EpManageIpv4PrefixListsGet, + EpManageIpv4PrefixListsListGet, + EpManageIpv4PrefixListsPost, + EpManageIpv4PrefixListsPut, + EpManageIpv6PrefixListsBulkDelete, + EpManageIpv6PrefixListsDelete, + EpManageIpv6PrefixListsGet, + EpManageIpv6PrefixListsListGet, + EpManageIpv6PrefixListsPost, + EpManageIpv6PrefixListsPut, +) +from ansible_collections.cisco.nd.plugins.module_utils.models.base import NDBaseModel +from ansible_collections.cisco.nd.plugins.module_utils.models.manage_prefix_list.manage_prefix_list import PrefixListModel +from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.base import NDBaseOrchestrator +from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.types import ResponseType + + +class ManagePrefixListOrchestrator(NDBaseOrchestrator[PrefixListModel]): + """ + Orchestrator for Prefix List (IPv4 and IPv6) CRUD operations. + + Both IPv4 and IPv6 prefix lists are managed through a single orchestrator. + The ``ip_version`` field on the model determines which set of API endpoints + to use for each operation. + + **Creation and deletion** are performed via bulk API endpoints: + - IPv4 create: ``POST /fabrics/{fabricName}/ipv4PrefixLists`` + with ``{"ipv4PrefixLists": [...]}``. + - IPv6 create: ``POST /fabrics/{fabricName}/ipv6PrefixLists`` + with ``{"ipv6PrefixLists": [...]}``. + - IPv4 bulk delete: ``POST /fabrics/{fabricName}/ipv4PrefixListActions/remove`` + with ``{"ipv4PrefixListNames": [...]}``. + - IPv6 bulk delete: ``POST /fabrics/{fabricName}/ipv6PrefixListActions/remove`` + with ``{"ipv6PrefixListNames": [...]}``. + + ``query_all`` fetches both IPv4 and IPv6 prefix lists and injects the + ``ipVersion`` key into each raw API response dict so ``PrefixListModel`` + can deserialise them correctly. + + The ``fabric_name`` field must be supplied at construction time: + + ```python + orchestrator = ManagePrefixListOrchestrator( + rest_send=rest_send, + fabric_name="my-fabric", + ) + ``` + """ + + model_class: ClassVar[Type[NDBaseModel]] = PrefixListModel + + supports_bulk_create: ClassVar[bool] = True + supports_bulk_delete: ClassVar[bool] = True + + # Fabric context + fabric_name: str = Field(description="Name of the fabric that owns these prefix lists.") + + # Required stubs -- the orchestrator overrides every operation, so these + # just need to satisfy NDBaseOrchestrator's required fields. + create_endpoint: Type[NDEndpointBaseModel] = EpManageIpv4PrefixListsPost + update_endpoint: Type[NDEndpointBaseModel] = EpManageIpv4PrefixListsPut + delete_endpoint: Type[NDEndpointBaseModel] = EpManageIpv4PrefixListsDelete + query_one_endpoint: Type[NDEndpointBaseModel] = EpManageIpv4PrefixListsGet + query_all_endpoint: Type[NDEndpointBaseModel] = EpManageIpv4PrefixListsListGet + + create_bulk_endpoint: Type[NDEndpointBaseModel] = EpManageIpv4PrefixListsPost + delete_bulk_endpoint: Type[NDEndpointBaseModel] = EpManageIpv4PrefixListsBulkDelete + + # ------------------------------------------------------------------------- + # Internal helpers + # ------------------------------------------------------------------------- + + def _endpoint_classes_for_version(self, version: str) -> Dict[str, Type[NDEndpointBaseModel]]: + """Return the correct endpoint class mapping for the given ip_version.""" + if version == "ipv4": + return { + "get": EpManageIpv4PrefixListsGet, + "list": EpManageIpv4PrefixListsListGet, + "post": EpManageIpv4PrefixListsPost, + "put": EpManageIpv4PrefixListsPut, + "delete": EpManageIpv4PrefixListsDelete, + "bulk_delete": EpManageIpv4PrefixListsBulkDelete, + } + else: + return { + "get": EpManageIpv6PrefixListsGet, + "list": EpManageIpv6PrefixListsListGet, + "post": EpManageIpv6PrefixListsPost, + "put": EpManageIpv6PrefixListsPut, + "delete": EpManageIpv6PrefixListsDelete, + "bulk_delete": EpManageIpv6PrefixListsBulkDelete, + } + + def _bulk_create_for_version(self, version: str, items: List[PrefixListModel]) -> ResponseType: + """Send a single bulk-create request for all items of the given ip_version.""" + eps = self._endpoint_classes_for_version(version) + api_endpoint = eps["post"]() + api_endpoint.fabric_name = self.fabric_name + list_key = "ipv4PrefixLists" if version == "ipv4" else "ipv6PrefixLists" + payload = {list_key: [item.to_payload() for item in items]} + return self._request(path=api_endpoint.path, verb=api_endpoint.verb, data=payload) + + def _bulk_delete_for_version(self, version: str, names: List[str]) -> ResponseType: + """Send a single bulk-delete request for the given prefix list names.""" + eps = self._endpoint_classes_for_version(version) + api_endpoint = eps["bulk_delete"]() + api_endpoint.fabric_name = self.fabric_name + names_key = "ipv4PrefixListNames" if version == "ipv4" else "ipv6PrefixListNames" + payload = {names_key: names} + return self._request(path=api_endpoint.path, verb=api_endpoint.verb, data=payload) + + # ------------------------------------------------------------------------- + # Query helpers + # ------------------------------------------------------------------------- + + def query_all(self) -> ResponseType: + """ + Fetch all IPv4 and IPv6 prefix lists and combine them into a single list. + + The ``ipVersion`` key is injected into each raw response dict so that + ``PrefixListModel.from_response()`` can populate the ``ip_version`` field. + """ + try: + results = [] + for version in ("ipv4", "ipv6"): + eps = self._endpoint_classes_for_version(version) + api_endpoint = eps["list"]() + api_endpoint.fabric_name = self.fabric_name + raw = self._request(path=api_endpoint.path, verb=api_endpoint.verb, not_found_ok=True) + list_key = "ipv4PrefixLists" if version == "ipv4" else "ipv6PrefixLists" + for item in raw.get(list_key, []) or []: + item["ipVersion"] = version + results.append(item) + return results + except Exception as e: + raise Exception(f"Query all failed: {e}") from e + + def query_one(self, model_instance: PrefixListModel, **kwargs) -> ResponseType: + """Retrieve a single prefix list by name and ip_version.""" + try: + version = str(model_instance.ip_version) + eps = self._endpoint_classes_for_version(version) + api_endpoint = eps["get"]() + api_endpoint.fabric_name = self.fabric_name + api_endpoint.set_identifiers(model_instance.get_identifier_value()) + return self._request(path=api_endpoint.path, verb=api_endpoint.verb) + except Exception as e: + raise Exception(f"Query failed for {model_instance.get_identifier_value()}: {e}") from e + + # ------------------------------------------------------------------------- + # Write operations -- single item (delegate to bulk) + # ------------------------------------------------------------------------- + + def create(self, model_instance: PrefixListModel, **kwargs) -> ResponseType: + """Create a single prefix list via the bulk endpoint.""" + try: + return self.create_bulk([model_instance]) + except Exception as e: + raise Exception(f"Create failed for {model_instance.get_identifier_value()}: {e}") from e + + def update(self, model_instance: PrefixListModel, **kwargs) -> ResponseType: + """Update an existing prefix list via the PUT endpoint.""" + try: + version = str(model_instance.ip_version) + eps = self._endpoint_classes_for_version(version) + api_endpoint = eps["put"]() + api_endpoint.fabric_name = self.fabric_name + api_endpoint.set_identifiers(model_instance.get_identifier_value()) + return self._request(path=api_endpoint.path, verb=api_endpoint.verb, data=model_instance.to_payload()) + except Exception as e: + raise Exception(f"Update failed for {model_instance.get_identifier_value()}: {e}") from e + + def delete(self, model_instance: PrefixListModel, **kwargs) -> ResponseType: + """Delete a single prefix list via the bulk-delete endpoint.""" + try: + return self.delete_bulk([model_instance]) + except Exception as e: + raise Exception(f"Delete failed for {model_instance.get_identifier_value()}: {e}") from e + + # ------------------------------------------------------------------------- + # Write operations -- bulk + # ------------------------------------------------------------------------- + + def create_bulk(self, model_instances: List[PrefixListModel], **kwargs) -> ResponseType: + """ + Bulk-create prefix lists, split by ip_version. + + Items are grouped into IPv4 and IPv6 subsets, each sent in a + separate API request. + """ + try: + ipv4_items = [m for m in model_instances if str(m.ip_version) == "ipv4"] + ipv6_items = [m for m in model_instances if str(m.ip_version) == "ipv6"] + result = {} + if ipv4_items: + result["ipv4"] = self._bulk_create_for_version("ipv4", ipv4_items) + if ipv6_items: + result["ipv6"] = self._bulk_create_for_version("ipv6", ipv6_items) + return result + except Exception as e: + raise Exception(f"Bulk create failed: {e}") from e + + def delete_bulk(self, model_instances: List[PrefixListModel], **kwargs) -> ResponseType: + """ + Bulk-delete prefix lists, split by ip_version. + + Names are grouped into IPv4 and IPv6 subsets, each sent in a + separate API request. + """ + try: + ipv4_names = [m.name for m in model_instances if str(m.ip_version) == "ipv4"] + ipv6_names = [m.name for m in model_instances if str(m.ip_version) == "ipv6"] + result = {} + if ipv4_names: + result["ipv4"] = self._bulk_delete_for_version("ipv4", ipv4_names) + if ipv6_names: + result["ipv6"] = self._bulk_delete_for_version("ipv6", ipv6_names) + return result + except Exception as e: + raise Exception(f"Bulk delete failed: {e}") from e diff --git a/plugins/modules/nd_manage_prefix_list.py b/plugins/modules/nd_manage_prefix_list.py new file mode 100644 index 000000000..32a4a4399 --- /dev/null +++ b/plugins/modules/nd_manage_prefix_list.py @@ -0,0 +1,286 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2026, Gaspard Micol (@gmicol) + +# 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 + +ANSIBLE_METADATA = {"metadata_version": "1.1", "status": ["preview"], "supported_by": "community"} + +DOCUMENTATION = r""" +--- +module: nd_manage_prefix_list +version_added: "1.6.0" +short_description: Manage IPv4 and IPv6 prefix lists on Cisco Nexus Dashboard fabrics +description: +- Manage IPv4 and IPv6 routing-policy prefix lists on a Cisco Nexus Dashboard (ND) fabric. +- Both address families are handled by this single module; set O(config.ip_version) + to C(ipv4) or C(ipv6) for each prefix list. +- Prefix lists are created and deleted in bulk via dedicated API endpoints. +- Prefix lists with the same name but different O(config.ip_version) values are treated + as distinct resources. +author: +- Gaspard Micol (@gmicol) +options: + fabric_name: + description: + - The name of the fabric that owns the prefix lists. + - Required for all operations. + type: str + required: true + config: + description: + - The list of prefix lists to configure. + type: list + elements: dict + required: True + suboptions: + ip_version: + description: + - The IP address family of the prefix list. + - Use C(ipv4) for IPv4 prefix lists and C(ipv6) for IPv6 prefix lists. + - A prefix list named C(PL-1) with C(ip_version=ipv4) and another with + C(ip_version=ipv6) are treated as independent resources. + type: str + required: true + choices: [ ipv4, ipv6 ] + aliases: [ ipVersion ] + name: + description: + - The name of the prefix list. + - Allowed characters are C([a-zA-Z0-9~_-]). + - Maximum length is 115 characters (63 for the default tenant). + type: str + required: true + description: + description: + - A human-readable description of the prefix list. + - Maximum length is 90 characters. + type: str + tenant_name: + description: + - The tenant that owns this prefix list. + - When omitted, the default tenant is used. + - Allowed characters are C([A-Za-z0-9_-]). + type: str + aliases: [ tenantName ] + entries: + description: + - The list of prefix list entries. + - Each entry defines a permit/deny action for a specific IP prefix. + type: list + elements: dict + required: true + suboptions: + sequence_number: + description: + - The sequence number of this entry (1-4294967294). + - Entries are evaluated in ascending sequence order. + type: int + required: true + aliases: [ sequenceNumber ] + action: + description: + - The action to take when the prefix matches. + type: str + required: true + choices: [ permit, deny ] + prefix: + description: + - The IP prefix in CIDR notation. + - Must be an IPv4 CIDR (e.g. C(10.0.0.0/8)) when + O(config.ip_version=ipv4). + - Must be an IPv6 CIDR (e.g. C(2001:db8::/32)) when + O(config.ip_version=ipv6). + type: str + required: true + exact_length: + description: + - Exact prefix-length to match. + - Range 1-32 for IPv4, 1-128 for IPv6. + type: int + aliases: [ exactLength ] + min_prefix_length: + description: + - Minimum prefix-length to match (inclusive). + - Range 1-32 for IPv4, 1-128 for IPv6. + type: int + aliases: [ minLength ] + max_prefix_length: + description: + - Maximum prefix-length to match (inclusive). + - Range 1-32 for IPv4, 1-128 for IPv6. + type: int + aliases: [ maxLength ] + mask: + description: + - Network mask in dotted-decimal format for IPv4 + (e.g. C(255.255.255.0)) or explicit match mask in IPv6 + format (e.g. C(ffff:ffff::)). + - Must be a valid IPv4 address when O(config.ip_version=ipv4). + - Must be a valid IPv6 address when O(config.ip_version=ipv6). + type: str + state: + description: + - The desired state of the prefix list resources on Cisco Nexus Dashboard. + - Use O(state=merged) to create new prefix lists and update existing ones + as defined in the configuration. + Prefix lists on ND that are not specified in the configuration are left unchanged. + - Use O(state=replaced) to replace the prefix lists specified in the configuration. + - Use O(state=overridden) to enforce the configuration as the single source of truth. + All prefix lists (both IPv4 and IPv6) on ND not present in the configuration + will be deleted. Use with caution. + - Use O(state=deleted) to remove the prefix lists specified in the configuration. + 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.2.1 or higher. +- IPv4 and IPv6 prefix lists are created and deleted in bulk via separate API endpoints. + A single task may contain a mix of IPv4 and IPv6 entries. +- O(config.entries.prefix) is validated locally to match the declared O(config.ip_version). +- O(config.entries.exact_length), O(config.entries.min_prefix_length), and + O(config.entries.max_prefix_length) are validated to be within the + address-family-appropriate range (1-32 for IPv4, 1-128 for IPv6). +""" + +EXAMPLES = r""" +- name: Create IPv4 and IPv6 prefix lists + cisco.nd.nd_manage_prefix_list: + fabric_name: my-fabric + config: + - ip_version: ipv4 + name: PL-IPV4-BORDERS + description: Border router IPv4 prefixes + entries: + - sequence_number: 10 + action: permit + prefix: 10.0.0.0/8 + - sequence_number: 20 + action: permit + prefix: 172.16.0.0/12 + min_prefix_length: 24 + max_prefix_length: 32 + - sequence_number: 30 + action: deny + prefix: 0.0.0.0/0 + - ip_version: ipv6 + name: PL-IPV6-DATACENTER + description: Datacenter IPv6 prefixes + entries: + - sequence_number: 10 + action: permit + prefix: 2001:db8::/32 + exact_length: 48 + - sequence_number: 20 + action: deny + prefix: ::/0 + state: merged + +- name: Update an IPv4 prefix list + cisco.nd.nd_manage_prefix_list: + fabric_name: my-fabric + config: + - ip_version: ipv4 + name: PL-IPV4-BORDERS + entries: + - sequence_number: 10 + action: permit + prefix: 192.168.0.0/16 + min_prefix_length: 24 + state: replaced + +- name: Delete specific prefix lists + cisco.nd.nd_manage_prefix_list: + fabric_name: my-fabric + config: + - ip_version: ipv4 + name: PL-IPV4-BORDERS + entries: + - sequence_number: 10 + action: permit + prefix: 10.0.0.0/8 + - ip_version: ipv6 + name: PL-IPV6-DATACENTER + entries: + - sequence_number: 10 + action: permit + prefix: 2001:db8::/32 + state: deleted + +- name: Override -- enforce exact set of prefix lists (delete all others) + cisco.nd.nd_manage_prefix_list: + fabric_name: my-fabric + config: + - ip_version: ipv4 + name: PL-IPV4-FINAL + entries: + - sequence_number: 10 + action: permit + prefix: 10.0.0.0/8 + state: overridden +""" + +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.common.pydantic_compat import require_pydantic +from ansible_collections.cisco.nd.plugins.module_utils.models.manage_prefix_list.manage_prefix_list import PrefixListModel +from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.manage_prefix_list import ManagePrefixListOrchestrator +from ansible_collections.cisco.nd.plugins.module_utils.rest.rest_send import RestSend +from ansible_collections.cisco.nd.plugins.module_utils.rest.sender_nd import Sender +from ansible_collections.cisco.nd.plugins.module_utils.rest.response_handler_nd import ResponseHandler + + +def main(): + argument_spec = nd_argument_spec() + argument_spec.update(PrefixListModel.get_argument_spec()) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + require_pydantic(module) + + nd_state_machine = None + try: + sender = Sender() + sender.ansible_module = module + + rest_send = RestSend( + { + "check_mode": module.check_mode, + "state": module.params.get("state"), + } + ) + rest_send.sender = sender + rest_send.response_handler = ResponseHandler() + + orchestrator = ManagePrefixListOrchestrator( + rest_send=rest_send, + fabric_name=module.params["fabric_name"], + ) + + nd_state_machine = NDStateMachine( + module=module, + model_orchestrator=orchestrator, + ) + + nd_state_machine.manage_state() + + module.exit_json(**nd_state_machine.output.format()) + + except Exception as e: + output = nd_state_machine.output.format() if nd_state_machine is not None else {} + module.fail_json(msg=f"Module execution failed: {str(e)}", **output) + + +if __name__ == "__main__": + main()