From 131a72e1f58f3050cb0a3ffcbc948d526cf3f010 Mon Sep 17 00:00:00 2001 From: Gaspard Micol Date: Wed, 20 May 2026 05:27:24 -0400 Subject: [PATCH] [minor_change] Add module Routing Policies Route Maps. --- plugins/module_utils/endpoints/mixins.py | 6 + .../endpoints/v1/manage/manage_route_maps.py | 473 +++++++++++++++++ .../models/manage_route_map/__init__.py | 3 + .../models/manage_route_map/enums.py | 36 ++ .../manage_route_map/manage_route_map.py | 493 ++++++++++++++++++ .../orchestrators/manage_route_map.py | 169 ++++++ plugins/modules/nd_manage_route_map.py | 422 +++++++++++++++ 7 files changed, 1602 insertions(+) create mode 100644 plugins/module_utils/endpoints/v1/manage/manage_route_maps.py create mode 100644 plugins/module_utils/models/manage_route_map/__init__.py create mode 100644 plugins/module_utils/models/manage_route_map/enums.py create mode 100644 plugins/module_utils/models/manage_route_map/manage_route_map.py create mode 100644 plugins/module_utils/orchestrators/manage_route_map.py create mode 100644 plugins/modules/nd_manage_route_map.py diff --git a/plugins/module_utils/endpoints/mixins.py b/plugins/module_utils/endpoints/mixins.py index e7f0620c9..cf29774bd 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 RouteMapNameMixin(BaseModel): + """Mixin for endpoints that require route_map_name parameter.""" + + route_map_name: Optional[str] = Field(default=None, min_length=1, max_length=115, description="Route map name") + + class VrfNameMixin(BaseModel): """Mixin for endpoints that require vrf_name parameter.""" diff --git a/plugins/module_utils/endpoints/v1/manage/manage_route_maps.py b/plugins/module_utils/endpoints/v1/manage/manage_route_maps.py new file mode 100644 index 000000000..f11a23732 --- /dev/null +++ b/plugins/module_utils/endpoints/v1/manage/manage_route_maps.py @@ -0,0 +1,473 @@ +# -*- 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 Route Maps endpoint models. + +This module contains endpoint definitions for route-map-related operations +in the ND Manage API. + +## Endpoints + +- `EpManageRouteMapsGet` - Get a specific route map by name + (GET /api/v1/manage/fabrics/{fabricName}/routeMaps/{routeMapName}) +- `EpManageRouteMapsListGet` - List all route maps for a fabric + (GET /api/v1/manage/fabrics/{fabricName}/routeMaps) +- `EpManageRouteMapsPost` - Bulk-create route maps for a fabric + (POST /api/v1/manage/fabrics/{fabricName}/routeMaps) +- `EpManageRouteMapsPut` - Update a specific route map + (PUT /api/v1/manage/fabrics/{fabricName}/routeMaps/{routeMapName}) +- `EpManageRouteMapsDelete` - Delete a specific route map + (DELETE /api/v1/manage/fabrics/{fabricName}/routeMaps/{routeMapName}) +- `EpManageRouteMapsBulkDelete` - Bulk-delete route maps by name + (POST /api/v1/manage/fabrics/{fabricName}/routeMapActions/remove) +""" + +from __future__ import annotations + +__metaclass__ = type + +from typing import ClassVar, Literal, Optional + +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, RouteMapNameMixin +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 RouteMapsEndpointParams(EndpointQueryParams): + """ + # Summary + + Query parameters shared by single-item route map endpoints. + + ## Parameters + + - cluster_name: Name of the target Nexus Dashboard cluster (multi-cluster deployments) + + ## Usage + + ```python + params = RouteMapsEndpointParams(cluster_name="cluster1") + query_string = params.to_query_string() + # Returns: "clusterName=cluster1" + ``` + """ + + 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 RouteMapsListEndpointParams(EndpointQueryParams): + """ + # Summary + + Query parameters for the GET /fabrics/{fabricName}/routeMaps list endpoint. + + ## Parameters + + - cluster_name: Name of the target Nexus Dashboard cluster (multi-cluster deployments) + - filter: Lucene-format filter string + - max: Maximum number of records to return + - offset: Number of records to skip for pagination + - sort: Sort field with optional ``:desc`` suffix + + ## Usage + + ```python + params = RouteMapsListEndpointParams(max=10, offset=0) + query_string = params.to_query_string() + # Returns: "max=10&offset=0" + ``` + """ + + 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", + ) + + +class _EpManageRouteMapsBase(RouteMapNameMixin, FabricNameMixin, NDEndpointBaseModel): + """ + Base class for ND Manage Route Maps endpoints. + + Provides common functionality for all HTTP methods on the + /api/v1/manage/fabrics/{fabricName}/routeMaps endpoint. + + Subclasses may override: + - ``_require_fabric_name``: set to ``False`` if fabric name is not required. + - ``_require_route_map_name``: set to ``False`` for collection-level endpoints + (list, bulk-create) that do not include a route map name in the path. + """ + + _require_fabric_name: ClassVar[bool] = True + _require_route_map_name: ClassVar[bool] = True + + endpoint_params: EndpointQueryParams = Field(default_factory=EndpointQueryParams, description="Endpoint-specific query parameters") + + def set_identifiers(self, identifier: IdentifierKey = None) -> None: + """Set the route_map_name from the identifier value.""" + self.route_map_name = identifier + + @property + def path(self) -> str: + """ + # Summary + + Build the endpoint path for route maps. + + ## Returns + + - Complete endpoint path string including fabric name, optional route map name, + and query string. + + ## Raises + + - ``ValueError`` if ``fabric_name`` is required but not set. + - ``ValueError`` if ``route_map_name`` is required but not set. + """ + if self._require_fabric_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_route_map_name and self.route_map_name is None: + raise ValueError(f"{type(self).__name__}.path: route_map_name must be set before accessing path.") + + segments = ["fabrics"] + if self.fabric_name is not None: + segments.append(self.fabric_name) + segments.append("routeMaps") + if self.route_map_name is not None: + segments.append(self.route_map_name) + + base_path = BasePath.path(*segments) + query_string = self.endpoint_params.to_query_string() + if query_string: + return f"{base_path}?{query_string}" + return base_path + + +class EpManageRouteMapsGet(_EpManageRouteMapsBase): + """ + # Summary + + ND Manage Route Maps GET Endpoint + + ## Description + + Endpoint to retrieve a specific route map by name from a fabric. + Both ``fabric_name`` and ``route_map_name`` are required path parameters. + + ## Path + + - ``/api/v1/manage/fabrics/{fabricName}/routeMaps/{routeMapName}`` + - ``/api/v1/manage/fabrics/{fabricName}/routeMaps/{routeMapName}?clusterName=cluster1`` + + ## Verb + + - GET + + ## Raises + + - ``ValueError`` if ``fabric_name`` or ``route_map_name`` is not set when accessing ``path`` + + ## Usage + + ```python + ep = EpManageRouteMapsGet() + ep.fabric_name = "my-fabric" + ep.route_map_name = "my-route-map" + path = ep.path + verb = ep.verb + ``` + """ + + class_name: Literal["EpManageRouteMapsGet"] = Field(default="EpManageRouteMapsGet", description="Class name for backward compatibility") + + endpoint_params: RouteMapsEndpointParams = Field(default_factory=RouteMapsEndpointParams, description="Endpoint-specific query parameters") + + @property + def verb(self) -> HttpVerbEnum: + """Return the HTTP verb for this endpoint.""" + return HttpVerbEnum.GET + + +class EpManageRouteMapsListGet(_EpManageRouteMapsBase): + """ + # Summary + + ND Manage Route Maps List GET Endpoint + + ## Description + + Endpoint to list all route maps for a given fabric. + Supports optional query parameters for filtering, pagination, and sorting. + + ## Path + + - ``/api/v1/manage/fabrics/{fabricName}/routeMaps`` + - ``/api/v1/manage/fabrics/{fabricName}/routeMaps?max=10&offset=0`` + + ## Verb + + - GET + + ## Raises + + - ``ValueError`` if ``fabric_name`` is not set when accessing ``path`` + + ## Usage + + ```python + ep = EpManageRouteMapsListGet() + ep.fabric_name = "my-fabric" + path = ep.path + verb = ep.verb + ``` + """ + + _require_route_map_name: ClassVar[bool] = False + + class_name: Literal["EpManageRouteMapsListGet"] = Field(default="EpManageRouteMapsListGet", description="Class name for backward compatibility") + + endpoint_params: RouteMapsListEndpointParams = Field(default_factory=RouteMapsListEndpointParams, description="Endpoint-specific query parameters") + + @property + def verb(self) -> HttpVerbEnum: + """Return the HTTP verb for this endpoint.""" + return HttpVerbEnum.GET + + +class EpManageRouteMapsPost(_EpManageRouteMapsBase): + """ + # Summary + + ND Manage Route Maps POST Endpoint + + ## Description + + Endpoint to bulk-create route maps for a given fabric. + The request body must conform to the ``CreateRouteMapsRequest`` schema: + ``{"routeMaps": [{"name": ..., "entries": [...]}, ...]}``. + + ## Path + + - ``/api/v1/manage/fabrics/{fabricName}/routeMaps`` + + ## Verb + + - POST + + ## Request Body + + ``CreateRouteMapsRequest`` schema -- ``{"routeMaps": [RouteMap, ...]}``. + + ## Raises + + - ``ValueError`` if ``fabric_name`` is not set when accessing ``path`` + + ## Usage + + ```python + ep = EpManageRouteMapsPost() + ep.fabric_name = "my-fabric" + rest_send.path = ep.path + rest_send.verb = ep.verb + rest_send.payload = {"routeMaps": [{"name": "rm1", "entries": [...]}]} + ``` + """ + + _require_route_map_name: ClassVar[bool] = False + + class_name: Literal["EpManageRouteMapsPost"] = Field(default="EpManageRouteMapsPost", description="Class name for backward compatibility") + + endpoint_params: RouteMapsEndpointParams = Field(default_factory=RouteMapsEndpointParams, description="Endpoint-specific query parameters") + + @property + def verb(self) -> HttpVerbEnum: + """Return the HTTP verb for this endpoint.""" + return HttpVerbEnum.POST + + +class EpManageRouteMapsPut(_EpManageRouteMapsBase): + """ + # Summary + + ND Manage Route Maps PUT Endpoint + + ## Description + + Endpoint to update a specific route map in a fabric. + Both ``fabric_name`` and ``route_map_name`` are required path parameters. + The request body must conform to the ``RouteMap`` schema. + + ## Path + + - ``/api/v1/manage/fabrics/{fabricName}/routeMaps/{routeMapName}`` + + ## Verb + + - PUT + + ## Request Body + + ``RouteMap`` schema -- ``{"name": ..., "entries": [...]}``. + + ## Raises + + - ``ValueError`` if ``fabric_name`` or ``route_map_name`` is not set when accessing ``path`` + + ## Usage + + ```python + ep = EpManageRouteMapsPut() + ep.fabric_name = "my-fabric" + ep.route_map_name = "my-route-map" + rest_send.path = ep.path + rest_send.verb = ep.verb + rest_send.payload = {"name": "my-route-map", "entries": [...]} + ``` + """ + + class_name: Literal["EpManageRouteMapsPut"] = Field(default="EpManageRouteMapsPut", description="Class name for backward compatibility") + + endpoint_params: RouteMapsEndpointParams = Field(default_factory=RouteMapsEndpointParams, description="Endpoint-specific query parameters") + + @property + def verb(self) -> HttpVerbEnum: + """Return the HTTP verb for this endpoint.""" + return HttpVerbEnum.PUT + + +class EpManageRouteMapsDelete(_EpManageRouteMapsBase): + """ + # Summary + + ND Manage Route Maps DELETE Endpoint + + ## Description + + Endpoint to delete a specific route map from a fabric. + Both ``fabric_name`` and ``route_map_name`` are required path parameters. + + ## Path + + - ``/api/v1/manage/fabrics/{fabricName}/routeMaps/{routeMapName}`` + + ## Verb + + - DELETE + + ## Raises + + - ``ValueError`` if ``fabric_name`` or ``route_map_name`` is not set when accessing ``path`` + + ## Usage + + ```python + ep = EpManageRouteMapsDelete() + ep.fabric_name = "my-fabric" + ep.route_map_name = "my-route-map" + rest_send.path = ep.path + rest_send.verb = ep.verb + ``` + """ + + class_name: Literal["EpManageRouteMapsDelete"] = Field(default="EpManageRouteMapsDelete", description="Class name for backward compatibility") + + endpoint_params: RouteMapsEndpointParams = Field(default_factory=RouteMapsEndpointParams, description="Endpoint-specific query parameters") + + @property + def verb(self) -> HttpVerbEnum: + """Return the HTTP verb for this endpoint.""" + return HttpVerbEnum.DELETE + + +class EpManageRouteMapsBulkDelete(FabricNameMixin, NDEndpointBaseModel): + """ + # Summary + + ND Manage Route Maps Bulk DELETE Endpoint + + ## Description + + Endpoint to bulk-delete multiple route maps from a fabric using the + ``routeMapActions/remove`` sub-resource. The request body must conform to + the ``RouteMapDeleteRequest`` schema: + ``{"routeMapNames": ["rm1", "rm2", ...]}``. + + ## Path + + - ``/api/v1/manage/fabrics/{fabricName}/routeMapActions/remove`` + + ## Verb + + - POST + + ## Request Body + + ``RouteMapDeleteRequest`` schema -- ``{"routeMapNames": [str, ...]}``. + + ## Raises + + - ``ValueError`` if ``fabric_name`` is not set when accessing ``path`` + + ## Usage + + ```python + ep = EpManageRouteMapsBulkDelete() + ep.fabric_name = "my-fabric" + rest_send.path = ep.path + rest_send.verb = ep.verb + rest_send.payload = {"routeMapNames": ["MY-BGP-ROUTEMAP-1", "MY-BGP-ROUTEMAP-2"]} + ``` + """ + + class_name: Literal["EpManageRouteMapsBulkDelete"] = Field(default="EpManageRouteMapsBulkDelete", description="Class name for backward compatibility") + + @property + def path(self) -> str: + """ + Build the bulk-delete endpoint path. + + ## Raises + + - ``ValueError`` if ``fabric_name`` is not set. + """ + 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, "routeMapActions", "remove") + + @property + def verb(self) -> HttpVerbEnum: + """Return the HTTP verb for this endpoint.""" + return HttpVerbEnum.POST diff --git a/plugins/module_utils/models/manage_route_map/__init__.py b/plugins/module_utils/models/manage_route_map/__init__.py new file mode 100644 index 000000000..1de5fa344 --- /dev/null +++ b/plugins/module_utils/models/manage_route_map/__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_route_map/enums.py b/plugins/module_utils/models/manage_route_map/enums.py new file mode 100644 index 000000000..e01cb0b2f --- /dev/null +++ b/plugins/module_utils/models/manage_route_map/enums.py @@ -0,0 +1,36 @@ +# -*- 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 Route Map management. +""" + +from __future__ import absolute_import, division, print_function + +from enum import Enum + + +class ActionEnum(str, Enum): + """Action values for a route map entry.""" + + PERMIT = "permit" + DENY = "deny" + + +class RuleTypeEnum(str, Enum): + """Rule type discriminator values for route map rule entries.""" + + MATCH_IPV4_ACL = "matchIpv4Acl" + MATCH_IPV6_ACL = "matchIpv6Acl" + MATCH_IPV4_PREFIX_LIST = "matchIpv4PrefixList" + MATCH_IPV6_PREFIX_LIST = "matchIpv6PrefixList" + MATCH_COMMUNITY = "matchCommunity" + MATCH_EXTENDED_COMMUNITY = "matchExtendedCommunity" + MATCH_TAG = "matchTag" + SET_COMMUNITY = "setCommunity" + SET_EXTENDED_COMMUNITY_LIST = "setExtendedCommunityList" + SET_LOCAL_PREFERENCE = "setLocalPreference" + SET_IPV4_NEXT_HOP = "setIpv4NextHop" + SET_IPV6_NEXT_HOP = "setIpv6NextHop" diff --git a/plugins/module_utils/models/manage_route_map/manage_route_map.py b/plugins/module_utils/models/manage_route_map/manage_route_map.py new file mode 100644 index 000000000..da0c82075 --- /dev/null +++ b/plugins/module_utils/models/manage_route_map/manage_route_map.py @@ -0,0 +1,493 @@ +# -*- 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 Route Map management via Nexus Dashboard. + +This module provides Pydantic models for creating, updating, and deleting +route maps through the Nexus Dashboard Fabric Controller (NDFC) Manage API. + +## Models Overview + +- ``RouteMapRuleEntryModel`` - Flat model for all rule entry types + (match/set conditions discriminated by ``ruleType``) +- ``RouteMapEntryModel`` - A single route map entry (sequence + action + rules) +- ``RouteMapModel`` - Complete route map (name + entries list) + +## Usage + +```python +# Create a route map model from Ansible config +rm = RouteMapModel.from_config({ + "name": "MY-BGP-ROUTEMAP-1", + "entries": [ + { + "sequence_number": 10, + "action": "permit", + "rule_entries": [ + {"rule_type": "matchIpv4PrefixList", "prefix_list_names": ["PL-1"]}, + ], + } + ], +}) +payload = rm.to_payload() +# {"name": "MY-BGP-ROUTEMAP-1", "entries": [{"sequenceNumber": 10, "action": "permit", +# "ruleEntries": [{"ruleType": "matchIpv4PrefixList", "prefixListNames": ["PL-1"]}]}]} +``` +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from typing import Any, ClassVar, Dict, List, Literal, Optional, Set + +from ansible_collections.cisco.nd.plugins.module_utils.common.pydantic_compat import Field +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_route_map.enums import ActionEnum, RuleTypeEnum + +# Rule type choices list (used in argument_spec) +RULE_TYPE_CHOICES = [e.value for e in RuleTypeEnum] + + +class RouteMapRuleEntryModel(NDNestedModel): + """ + # Summary + + Flat Pydantic model for a single route map rule entry. + + A rule entry is a match or set condition identified by ``ruleType``. + All variant-specific fields are Optional; only the fields relevant to the + active ``ruleType`` are serialised into the API payload + (``exclude_none=True`` in ``to_payload``). + + ## Supported ruleType values + + - ``matchIpv4Acl`` - ``accessControlListName`` (required) + - ``matchIpv6Acl`` - ``accessControlListName`` (required) + - ``matchIpv4PrefixList`` - ``prefixListNames`` (required) + - ``matchIpv6PrefixList`` - ``prefixListNames`` (required) + - ``matchCommunity`` - ``communityListNames`` (required), ``exactMatch`` + - ``matchExtendedCommunity`` - ``extendedCommunityListNames`` (required), ``exactMatch`` + - ``matchTag`` - ``tags`` (required) + - ``setCommunity`` - ``communityNumbers`` (required), ``additive``, + ``gracefulRestartShutdownCommunity``, + ``noAdvertiseCommunity``, ``noExportCommunity``, + ``localAsCommunity``, ``internetCommunity`` + - ``setExtendedCommunityList`` - ``extendedCommunityListName`` (required) + - ``setLocalPreference`` - ``value`` (required) + - ``setIpv4NextHop`` - ``nextHopIpCollection``, ``dropOnFail``, + ``loadShare``, ``enforceOrder``, + ``verifyAvailability``, ``usePeerAddress``, + ``redistributeUnchanged``, ``unchanged``, + ``trackId`` + - ``setIpv6NextHop`` - same optional fields as ``setIpv4NextHop`` + """ + + # --- Discriminator (required for every rule entry) --- + + rule_type: str = Field(alias="ruleType", description="Rule type discriminator.") + + # --- matchIpv4Acl / matchIpv6Acl --- + + access_control_list_name: Optional[str] = Field( + default=None, + alias="accessControlListName", + description="Name of the access control list to match.", + ) + + # --- matchIpv4PrefixList / matchIpv6PrefixList --- + + prefix_list_names: Optional[List[str]] = Field( + default=None, + alias="prefixListNames", + description="Names of the prefix lists to match.", + ) + + # --- matchCommunity --- + + community_list_names: Optional[List[str]] = Field( + default=None, + alias="communityListNames", + description="Names of the community lists to match.", + ) + + # --- matchExtendedCommunity --- + + extended_community_list_names: Optional[List[str]] = Field( + default=None, + alias="extendedCommunityListNames", + description="Names of the extended community lists to match.", + ) + + # --- matchCommunity / matchExtendedCommunity --- + + exact_match: Optional[bool] = Field( + default=None, + alias="exactMatch", + description="Require an exact match for the (extended) community lists.", + ) + + # --- matchTag --- + + tags: Optional[List[int]] = Field( + default=None, + alias="tags", + description="List of integer tags to match (0-4294967295).", + ) + + # --- setCommunity --- + + community_numbers: Optional[List[str]] = Field( + default=None, + alias="communityNumbers", + description="Community numbers in ASN2:NN format (e.g. '65000:100').", + ) + + additive: Optional[bool] = Field( + default=None, + alias="additive", + description="Add communities without replacing existing ones.", + ) + + graceful_restart_shutdown_community: Optional[bool] = Field( + default=None, + alias="gracefulRestartShutdownCommunity", + description="Set the graceful-restart shutdown community.", + ) + + no_advertise_community: Optional[bool] = Field( + default=None, + alias="noAdvertiseCommunity", + description="Set the no-advertise community.", + ) + + no_export_community: Optional[bool] = Field( + default=None, + alias="noExportCommunity", + description="Set the no-export community.", + ) + + local_as_community: Optional[bool] = Field( + default=None, + alias="localAsCommunity", + description="Set the local-AS community.", + ) + + internet_community: Optional[bool] = Field( + default=None, + alias="internetCommunity", + description="Set the internet community.", + ) + + # --- setExtendedCommunityList --- + + extended_community_list_name: Optional[str] = Field( + default=None, + alias="extendedCommunityListName", + description="Name of the extended community list to set.", + ) + + # --- setLocalPreference --- + + value: Optional[int] = Field( + default=None, + alias="value", + description="Local preference value (0-4294967295).", + ) + + # --- setIpv4NextHop / setIpv6NextHop --- + + next_hop_ip_collection: Optional[List[str]] = Field( + default=None, + alias="nextHopIpCollection", + description="List of next-hop IP addresses.", + ) + + drop_on_fail: Optional[bool] = Field( + default=None, + alias="dropOnFail", + description="Drop the packet if the next hop is unavailable.", + ) + + load_share: Optional[bool] = Field( + default=None, + alias="loadShare", + description="Enable load sharing across multiple next hops.", + ) + + enforce_order: Optional[bool] = Field( + default=None, + alias="enforceOrder", + description="Enforce the order of next-hop IPs.", + ) + + verify_availability: Optional[bool] = Field( + default=None, + alias="verifyAvailability", + description="Ensure the next hop is reachable before using it.", + ) + + use_peer_address: Optional[bool] = Field( + default=None, + alias="usePeerAddress", + description="Use the peer address as the next hop.", + ) + + redistribute_unchanged: Optional[bool] = Field( + default=None, + alias="redistributeUnchanged", + description="Redistribute routes without changing the next hop.", + ) + + unchanged: Optional[bool] = Field( + default=None, + alias="unchanged", + description="Keep the next hop unchanged.", + ) + + track_id: Optional[int] = Field( + default=None, + alias="trackId", + description="Tracking subsystem object ID (1-512).", + ) + + +class RouteMapEntryModel(NDNestedModel): + """ + # Summary + + A single route map entry (one sequence block). + + Each entry consists of a sequence number, a permit/deny action, and a list + of rule entries (match/set conditions). + """ + + sequence_number: int = Field( + alias="sequenceNumber", + ge=0, + le=65535, + description="Route map sequence number (0-65535).", + ) + + action: ActionEnum = Field( + default=ActionEnum.PERMIT, + alias="action", + description="Action for this entry: permit or deny.", + ) + + rule_entries: List[RouteMapRuleEntryModel] = Field( + alias="ruleEntries", + description="List of match or set rule conditions.", + ) + + +class RouteMapModel(NDBaseModel): + """ + # Summary + + Route map configuration for a Nexus Dashboard fabric. + + ## Identifier + + ``name`` (single) - the route map name within its fabric. + + ## Serialization Notes + + - ``last_update_timestamp`` is a read-only field returned by the API. + It is excluded from payload output and diff comparisons. + - ``fabric_name`` is managed at the orchestrator level and is NOT part of + this model; path construction is handled by the endpoint classes. + """ + + # --- Identifier Configuration --- + + identifiers: ClassVar[Optional[List[str]]] = ["name"] + identifier_strategy: ClassVar[Optional[Literal["single", "composite", "hierarchical", "singleton"]]] = "single" + + # --- Serialization Configuration --- + + exclude_from_diff: ClassVar[Set[str]] = {"last_update_timestamp"} + payload_exclude_fields: ClassVar[Set[str]] = {"last_update_timestamp"} + unwanted_keys: ClassVar[List] = [] + + # --- Fields --- + + name: str = Field( + alias="name", + min_length=1, + max_length=115, + description="Name of the route map (pattern: ^[a-zA-Z0-9~_-]+$).", + ) + + last_update_timestamp: Optional[str] = Field( + default=None, + alias="lastUpdateTimestamp", + description="Timestamp of the last update (read-only, set by ND).", + ) + + entries: List[RouteMapEntryModel] = Field( + alias="entries", + description="List of route map entries (sequence + action + rule conditions).", + ) + + # --- 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( + name=dict( + type="str", + required=True, + ), + entries=dict( + type="list", + elements="dict", + required=True, + options=dict( + sequence_number=dict( + type="int", + default=10, + ), + action=dict( + type="str", + default="permit", + choices=["permit", "deny"], + ), + rule_entries=dict( + type="list", + elements="dict", + required=True, + options=dict( + rule_type=dict( + type="str", + required=True, + choices=RULE_TYPE_CHOICES, + aliases=["ruleType"], + ), + # matchIpv4Acl / matchIpv6Acl + access_control_list_name=dict( + type="str", + aliases=["accessControlListName"], + ), + # matchIpv4PrefixList / matchIpv6PrefixList + prefix_list_names=dict( + type="list", + elements="str", + aliases=["prefixListNames"], + ), + # matchCommunity + community_list_names=dict( + type="list", + elements="str", + aliases=["communityListNames"], + ), + # matchExtendedCommunity + extended_community_list_names=dict( + type="list", + elements="str", + aliases=["extendedCommunityListNames"], + ), + # matchCommunity / matchExtendedCommunity + exact_match=dict( + type="bool", + aliases=["exactMatch"], + ), + # matchTag + tags=dict( + type="list", + elements="int", + ), + # setCommunity + community_numbers=dict( + type="list", + elements="str", + aliases=["communityNumbers"], + ), + additive=dict(type="bool"), + graceful_restart_shutdown_community=dict( + type="bool", + aliases=["gracefulRestartShutdownCommunity"], + ), + no_advertise_community=dict( + type="bool", + aliases=["noAdvertiseCommunity"], + ), + no_export_community=dict( + type="bool", + aliases=["noExportCommunity"], + ), + local_as_community=dict( + type="bool", + aliases=["localAsCommunity"], + ), + internet_community=dict( + type="bool", + aliases=["internetCommunity"], + ), + # setExtendedCommunityList + extended_community_list_name=dict( + type="str", + aliases=["extendedCommunityListName"], + ), + # setLocalPreference + value=dict(type="int"), + # setIpv4NextHop / setIpv6NextHop + next_hop_ip_collection=dict( + type="list", + elements="str", + aliases=["nextHopIpCollection"], + ), + drop_on_fail=dict( + type="bool", + aliases=["dropOnFail"], + ), + load_share=dict( + type="bool", + aliases=["loadShare"], + ), + enforce_order=dict( + type="bool", + aliases=["enforceOrder"], + ), + verify_availability=dict( + type="bool", + aliases=["verifyAvailability"], + ), + use_peer_address=dict( + type="bool", + aliases=["usePeerAddress"], + ), + redistribute_unchanged=dict( + type="bool", + aliases=["redistributeUnchanged"], + ), + unchanged=dict(type="bool"), + track_id=dict( + type="int", + aliases=["trackId"], + ), + ), + ), + ), + ), + ), + ), + state=dict( + type="str", + default="merged", + choices=["merged", "replaced", "overridden", "deleted"], + ), + ) diff --git a/plugins/module_utils/orchestrators/manage_route_map.py b/plugins/module_utils/orchestrators/manage_route_map.py new file mode 100644 index 000000000..8a939bcba --- /dev/null +++ b/plugins/module_utils/orchestrators/manage_route_map.py @@ -0,0 +1,169 @@ +# -*- 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, List, 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_route_maps import ( + EpManageRouteMapsDelete, + EpManageRouteMapsGet, + EpManageRouteMapsListGet, + EpManageRouteMapsPost, + EpManageRouteMapsPut, + EpManageRouteMapsBulkDelete, +) +from ansible_collections.cisco.nd.plugins.module_utils.models.base import NDBaseModel +from ansible_collections.cisco.nd.plugins.module_utils.models.manage_route_map.manage_route_map import RouteMapModel +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 ManageRouteMapOrchestrator(NDBaseOrchestrator[RouteMapModel]): + """ + Orchestrator for Route Map CRUD operations. + + This orchestrator manages route maps for a single fabric identified by + ``fabric_name``. Because the API only supports bulk-create + (``POST /routeMaps`` with ``{"routeMaps": [...]}``) and bulk-delete + (``POST /routeMapActions/remove`` with ``{"routeMapNames": [...]}``) + those two operations are implemented as custom ``create_bulk`` / + ``delete_bulk`` methods. Individual updates are performed via the + standard ``PUT /routeMaps/{routeMapName}`` endpoint. + + The ``fabric_name`` field is mandatory and must be supplied when + constructing the orchestrator: + + ```python + orchestrator = ManageRouteMapOrchestrator( + rest_send=rest_send, + fabric_name="my-fabric", + ) + ``` + """ + + model_class: ClassVar[Type[NDBaseModel]] = RouteMapModel + + supports_bulk_create: ClassVar[bool] = True + supports_bulk_delete: ClassVar[bool] = True + + # Fabric context -- required at construction time + fabric_name: str = Field(description="Name of the fabric that owns these route maps.") + + # Standard endpoint references (single-item operations) + create_endpoint: Type[NDEndpointBaseModel] = EpManageRouteMapsPost + update_endpoint: Type[NDEndpointBaseModel] = EpManageRouteMapsPut + delete_endpoint: Type[NDEndpointBaseModel] = EpManageRouteMapsDelete + query_one_endpoint: Type[NDEndpointBaseModel] = EpManageRouteMapsGet + query_all_endpoint: Type[NDEndpointBaseModel] = EpManageRouteMapsListGet + + # Bulk endpoint references + create_bulk_endpoint: Type[NDEndpointBaseModel] = EpManageRouteMapsPost + delete_bulk_endpoint: Type[NDEndpointBaseModel] = EpManageRouteMapsBulkDelete + + # ------------------------------------------------------------------------- + # Query helpers + # ------------------------------------------------------------------------- + + def query_all(self) -> ResponseType: + """ + List all route maps for the configured fabric. + + The API response is wrapped under the ``"routeMaps"`` key; this method + extracts and returns the list directly. + """ + try: + api_endpoint = self.query_all_endpoint() + api_endpoint.fabric_name = self.fabric_name + result = self._request(path=api_endpoint.path, verb=api_endpoint.verb, not_found_ok=True) + return result.get("routeMaps", []) or [] + except Exception as e: + raise Exception(f"Query all failed: {e}") from e + + def query_one(self, model_instance: RouteMapModel, **kwargs) -> ResponseType: + """Retrieve a single route map by name.""" + try: + api_endpoint = self.query_one_endpoint() + 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 + # ------------------------------------------------------------------------- + + def create(self, model_instance: RouteMapModel, **kwargs) -> ResponseType: + """ + Create a single route map via the bulk-create endpoint. + + Wraps the single model payload in ``{"routeMaps": [...]}``. + """ + 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: RouteMapModel, **kwargs) -> ResponseType: + """Update an existing route map via the PUT endpoint.""" + try: + api_endpoint = self.update_endpoint() + 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: RouteMapModel, **kwargs) -> ResponseType: + """ + Delete a single route map via the bulk-delete endpoint. + + Wraps the single name in ``{"routeMapNames": [...]}``. + """ + 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[RouteMapModel], **kwargs) -> ResponseType: + """ + Bulk-create route maps. + + Sends ``POST /fabrics/{fabricName}/routeMaps`` with body: + ``{"routeMaps": [, ...]}``. + """ + try: + api_endpoint = self.create_bulk_endpoint() + api_endpoint.fabric_name = self.fabric_name + payload = {"routeMaps": [item.to_payload() for item in model_instances]} + return self._request(path=api_endpoint.path, verb=api_endpoint.verb, data=payload) + except Exception as e: + raise Exception(f"Bulk create failed: {e}") from e + + def delete_bulk(self, model_instances: List[RouteMapModel], **kwargs) -> ResponseType: + """ + Bulk-delete route maps. + + Sends ``POST /fabrics/{fabricName}/routeMapActions/remove`` with body: + ``{"routeMapNames": ["name1", "name2", ...]}``. + """ + try: + api_endpoint = self.delete_bulk_endpoint() + api_endpoint.fabric_name = self.fabric_name + route_map_names = [item.name for item in model_instances] + payload = {"routeMapNames": route_map_names} + return self._request(path=api_endpoint.path, verb=api_endpoint.verb, data=payload) + except Exception as e: + raise Exception(f"Bulk delete failed: {e}") from e diff --git a/plugins/modules/nd_manage_route_map.py b/plugins/modules/nd_manage_route_map.py new file mode 100644 index 000000000..731d06f98 --- /dev/null +++ b/plugins/modules/nd_manage_route_map.py @@ -0,0 +1,422 @@ +# -*- 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_route_map +version_added: "1.6.0" +short_description: Manage route maps on Cisco Nexus Dashboard fabrics +description: +- Manage route maps on a Cisco Nexus Dashboard (ND) fabric. +- It supports creating, updating, and deleting route maps. +- Route maps are created and deleted in bulk via dedicated bulk-API endpoints. +- Each route map contains one or more entries, each entry containing + a sequence number, a permit/deny action, and a list of match/set rule conditions. +author: +- Gaspard Micol (@gmicol) +options: + fabric_name: + description: + - The name of the fabric that owns the route maps. + - This is a required parameter for all operations. + type: str + required: true + config: + description: + - The list of route maps to configure. + type: list + elements: dict + required: True + suboptions: + name: + description: + - The name of the route map. + - Allowed characters are C([a-zA-Z0-9~_-]). + - Maximum length is 115 characters (63 for the default tenant). + type: str + required: true + entries: + description: + - The list of route map entries. + - Each entry defines a sequence block with a permit/deny action + and one or more match/set rule conditions. + type: list + elements: dict + required: true + suboptions: + sequence_number: + description: + - The sequence number of the route map entry (0-65535). + - Lower sequence numbers are evaluated first. + type: int + default: 10 + action: + description: + - The action to take when the entry matches. + type: str + default: permit + choices: [ permit, deny ] + rule_entries: + description: + - The list of match or set rule conditions for this entry. + - Each rule is discriminated by O(config.entries.rule_entries.rule_type). + type: list + elements: dict + required: true + suboptions: + rule_type: + description: + - The type of the rule entry. + - Determines which additional fields are required. + type: str + required: true + choices: + - matchIpv4Acl + - matchIpv6Acl + - matchIpv4PrefixList + - matchIpv6PrefixList + - matchCommunity + - matchExtendedCommunity + - matchTag + - setCommunity + - setExtendedCommunityList + - setLocalPreference + - setIpv4NextHop + - setIpv6NextHop + aliases: [ ruleType ] + access_control_list_name: + description: + - Name of the access control list to match. + - Required when O(config.entries.rule_entries.rule_type=matchIpv4Acl) + or O(config.entries.rule_entries.rule_type=matchIpv6Acl). + type: str + aliases: [ accessControlListName ] + prefix_list_names: + description: + - Names of the prefix lists to match. + - Required when O(config.entries.rule_entries.rule_type=matchIpv4PrefixList) + or O(config.entries.rule_entries.rule_type=matchIpv6PrefixList). + type: list + elements: str + aliases: [ prefixListNames ] + community_list_names: + description: + - Names of the community lists to match. + - Required when O(config.entries.rule_entries.rule_type=matchCommunity). + type: list + elements: str + aliases: [ communityListNames ] + extended_community_list_names: + description: + - Names of the extended community lists to match. + - Required when O(config.entries.rule_entries.rule_type=matchExtendedCommunity). + type: list + elements: str + aliases: [ extendedCommunityListNames ] + exact_match: + description: + - Require an exact match for (extended) community list comparisons. + - Used with C(matchCommunity) and C(matchExtendedCommunity). + type: bool + aliases: [ exactMatch ] + tags: + description: + - List of integer tags to match (0-4294967295). + - Required when O(config.entries.rule_entries.rule_type=matchTag). + type: list + elements: int + community_numbers: + description: + - Community numbers to set, each in ASN2:NN format (e.g. C(65000:100)). + - Required when O(config.entries.rule_entries.rule_type=setCommunity). + type: list + elements: str + aliases: [ communityNumbers ] + additive: + description: + - Add communities without replacing existing ones. + - Used with C(setCommunity). + type: bool + graceful_restart_shutdown_community: + description: + - Set the graceful-restart shutdown community. + - Used with C(setCommunity). + type: bool + aliases: [ gracefulRestartShutdownCommunity ] + no_advertise_community: + description: + - Set the no-advertise community. + - Used with C(setCommunity). + type: bool + aliases: [ noAdvertiseCommunity ] + no_export_community: + description: + - Set the no-export community. + - Used with C(setCommunity). + type: bool + aliases: [ noExportCommunity ] + local_as_community: + description: + - Set the local-AS community. + - Used with C(setCommunity). + type: bool + aliases: [ localAsCommunity ] + internet_community: + description: + - Set the internet community. + - Used with C(setCommunity). + type: bool + aliases: [ internetCommunity ] + extended_community_list_name: + description: + - Name of the extended community list to set. + - Required when O(config.entries.rule_entries.rule_type=setExtendedCommunityList). + type: str + aliases: [ extendedCommunityListName ] + value: + description: + - Local preference value (0-4294967295). + - Required when O(config.entries.rule_entries.rule_type=setLocalPreference). + type: int + next_hop_ip_collection: + description: + - List of next-hop IP addresses to set. + - Used with C(setIpv4NextHop) and C(setIpv6NextHop). + type: list + elements: str + aliases: [ nextHopIpCollection ] + drop_on_fail: + description: + - Drop the packet if the next hop is unavailable. + - Used with C(setIpv4NextHop) and C(setIpv6NextHop). + type: bool + aliases: [ dropOnFail ] + load_share: + description: + - Enable load sharing across multiple next hops. + - Used with C(setIpv4NextHop) and C(setIpv6NextHop). + type: bool + aliases: [ loadShare ] + enforce_order: + description: + - Enforce the order of next-hop IPs. + - Used with C(setIpv4NextHop) and C(setIpv6NextHop). + type: bool + aliases: [ enforceOrder ] + verify_availability: + description: + - Ensure the next hop is reachable before using it. + - Used with C(setIpv4NextHop) and C(setIpv6NextHop). + type: bool + aliases: [ verifyAvailability ] + use_peer_address: + description: + - Use the peer address as the next hop. + - Used with C(setIpv4NextHop) and C(setIpv6NextHop). + type: bool + aliases: [ usePeerAddress ] + redistribute_unchanged: + description: + - Redistribute routes without changing the next hop. + - Used with C(setIpv4NextHop) and C(setIpv6NextHop). + type: bool + aliases: [ redistributeUnchanged ] + unchanged: + description: + - Keep the next hop unchanged. + - Used with C(setIpv4NextHop) and C(setIpv6NextHop). + type: bool + track_id: + description: + - Tracking subsystem object ID (1-512). + - Used with C(setIpv4NextHop) and C(setIpv6NextHop). + type: int + aliases: [ trackId ] + state: + description: + - The desired state of the route map resources on Cisco Nexus Dashboard. + - Use O(state=merged) to create new route maps and update existing ones + as defined in your configuration. + Route maps on ND that are not specified in the configuration are left unchanged. + - Use O(state=replaced) to replace the route maps specified in the configuration. + - Use O(state=overridden) to enforce the configuration as the single source of truth. + Route maps on ND that are not in the configuration will be deleted. Use with caution. + - Use O(state=deleted) to remove the route maps specified in the configuration + from 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.2.1 or higher. +- Route maps are created and deleted in bulk via the API. + When O(state=overridden) is used, all route maps not present in the configuration + will be deleted in a single bulk-delete API call. +""" + +EXAMPLES = r""" +- name: Create route maps with prefix-list and community match rules + cisco.nd.nd_manage_route_map: + fabric_name: my-fabric + config: + - name: MY-BGP-ROUTEMAP-1 + entries: + - sequence_number: 10 + action: permit + rule_entries: + - rule_type: matchIpv4PrefixList + prefix_list_names: + - PL-IPV4-1 + - PL-IPV4-2 + - sequence_number: 20 + action: deny + rule_entries: + - rule_type: matchCommunity + community_list_names: + - COMM-LIST-1 + exact_match: true + - name: MY-BGP-ROUTEMAP-2 + entries: + - sequence_number: 10 + action: permit + rule_entries: + - rule_type: setLocalPreference + value: 200 + state: merged + +- name: Create route map with next-hop set rule + cisco.nd.nd_manage_route_map: + fabric_name: my-fabric + config: + - name: MY-BGP-ROUTEMAP-3 + entries: + - sequence_number: 10 + action: permit + rule_entries: + - rule_type: setIpv4NextHop + next_hop_ip_collection: + - 192.168.1.1 + - 192.168.1.2 + drop_on_fail: true + load_share: true + state: merged + +- name: Update a route map (replace its entries) + cisco.nd.nd_manage_route_map: + fabric_name: my-fabric + config: + - name: MY-BGP-ROUTEMAP-1 + entries: + - sequence_number: 10 + action: permit + rule_entries: + - rule_type: matchIpv6PrefixList + prefix_list_names: + - PL-IPV6-1 + state: replaced + +- name: Delete specific route maps + cisco.nd.nd_manage_route_map: + fabric_name: my-fabric + config: + - name: MY-BGP-ROUTEMAP-1 + entries: + - sequence_number: 10 + action: permit + rule_entries: + - rule_type: matchIpv4Acl + access_control_list_name: dummy-acl + - name: MY-BGP-ROUTEMAP-2 + entries: + - sequence_number: 10 + action: permit + rule_entries: + - rule_type: matchIpv4Acl + access_control_list_name: dummy-acl + state: deleted + +- name: Override -- enforce exact set of route maps (delete all others) + cisco.nd.nd_manage_route_map: + fabric_name: my-fabric + config: + - name: MY-BGP-ROUTEMAP-FINAL + entries: + - sequence_number: 10 + action: permit + rule_entries: + - rule_type: setLocalPreference + value: 100 + 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_route_map.manage_route_map import RouteMapModel +from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.manage_route_map import ManageRouteMapOrchestrator +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(RouteMapModel.get_argument_spec()) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + require_pydantic(module) + + nd_state_machine = None + try: + # Build REST infrastructure and inject fabric_name into the orchestrator. + # The orchestrator is instantiated manually so that fabric_name (a top-level + # module parameter) can be passed at construction time. + 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 = ManageRouteMapOrchestrator( + 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()