From 1b80689880e7e0545a48fca078687051cdf9b1d4 Mon Sep 17 00:00:00 2001 From: Allen Robel Date: Fri, 10 Apr 2026 13:42:54 -1000 Subject: [PATCH] Replace NDModule (nd.py) with RestSend in orchestrator framework Remove the legacy NDModule dependency from NDBaseOrchestrator and NDStateMachine, wiring RestSend + Sender + ResponseHandler directly. - NDBaseOrchestrator: replace `sender: NDModule` field with `rest_send: RestSend`, add `_request()` and `_query_obj()` helper methods that encapsulate the commit-and-extract-DATA pattern. - NDStateMachine: create Sender, ResponseHandler, and RestSend in __init__ instead of NDModule; pass rest_send to orchestrator. - Update all orchestrator subclasses (local_user, manage_fabric_ebgp, manage_fabric_external, manage_fabric_ibgp) to use `_query_obj()` instead of `self.sender.query_obj()`. Co-authored-by: Gaspard Micol <@gmicol> Co-Authored-By: Claude Opus 4.6 --- plugins/module_utils/nd_state_machine.py | 27 +++++-- plugins/module_utils/orchestrators/base.py | 76 ++++++++++++++++--- .../module_utils/orchestrators/local_user.py | 17 +++-- .../orchestrators/manage_fabric_ebgp.py | 13 ++-- .../orchestrators/manage_fabric_external.py | 13 ++-- .../orchestrators/manage_fabric_ibgp.py | 13 ++-- 6 files changed, 117 insertions(+), 42 deletions(-) diff --git a/plugins/module_utils/nd_state_machine.py b/plugins/module_utils/nd_state_machine.py index 7990468e..de6df2fe 100644 --- a/plugins/module_utils/nd_state_machine.py +++ b/plugins/module_utils/nd_state_machine.py @@ -5,15 +5,18 @@ from __future__ import absolute_import, division, print_function -from typing import Type, Union, List, Any, Callable, Optional +from typing import Any, Callable, List, Optional, Type, Union + from ansible.module_utils.basic import AnsibleModule -from ansible_collections.cisco.nd.plugins.module_utils.nd import NDModule -from ansible_collections.cisco.nd.plugins.module_utils.nd_output import NDOutput +from ansible_collections.cisco.nd.plugins.module_utils.common.exceptions import NDStateMachineError from ansible_collections.cisco.nd.plugins.module_utils.models.base import NDBaseModel from ansible_collections.cisco.nd.plugins.module_utils.nd_config_collection import NDConfigCollection +from ansible_collections.cisco.nd.plugins.module_utils.nd_output import NDOutput from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.base import NDBaseOrchestrator from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.types import ResponseType -from ansible_collections.cisco.nd.plugins.module_utils.common.exceptions import NDStateMachineError +from ansible_collections.cisco.nd.plugins.module_utils.rest.response_handler_nd import ResponseHandler +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 class NDStateMachine: @@ -26,7 +29,19 @@ def __init__(self, module: AnsibleModule, model_orchestrator: Union[Type[NDBaseO Initialize the ND State Machine. """ self.module = module - self.nd_module = NDModule(self.module) + + # REST infrastructure + sender = Sender() + sender.ansible_module = self.module + + self.rest_send = RestSend( + { + "check_mode": self.module.check_mode, + "state": self.module.params.get("state"), + } + ) + self.rest_send.sender = sender + self.rest_send.response_handler = ResponseHandler() # Operation tracking self.output = NDOutput(output_level=module.params.get("output_level", "normal")) @@ -34,7 +49,7 @@ def __init__(self, module: AnsibleModule, model_orchestrator: Union[Type[NDBaseO # Configuration # Accept either an orchestrator instance or a class. if isinstance(model_orchestrator, type) and issubclass(model_orchestrator, NDBaseOrchestrator): - self.model_orchestrator = model_orchestrator(sender=self.nd_module) + self.model_orchestrator = model_orchestrator(rest_send=self.rest_send) elif isinstance(model_orchestrator, NDBaseOrchestrator): self.model_orchestrator = model_orchestrator else: diff --git a/plugins/module_utils/orchestrators/base.py b/plugins/module_utils/orchestrators/base.py index 36582b04..7dd95a60 100644 --- a/plugins/module_utils/orchestrators/base.py +++ b/plugins/module_utils/orchestrators/base.py @@ -5,12 +5,14 @@ from __future__ import absolute_import, division, print_function from functools import wraps +from typing import Any, ClassVar, Dict, Generic, List, Optional, Type, TypeVar + from ansible_collections.cisco.nd.plugins.module_utils.common.pydantic_compat import BaseModel, ConfigDict, model_validator -from typing import ClassVar, Type, Optional, Generic, TypeVar, List -from ansible_collections.cisco.nd.plugins.module_utils.models.base import NDBaseModel -from ansible_collections.cisco.nd.plugins.module_utils.nd import NDModule from ansible_collections.cisco.nd.plugins.module_utils.endpoints.base import NDEndpointBaseModel +from ansible_collections.cisco.nd.plugins.module_utils.enums import HttpVerbEnum +from ansible_collections.cisco.nd.plugins.module_utils.models.base import NDBaseModel from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.types import ResponseType +from ansible_collections.cisco.nd.plugins.module_utils.rest.rest_send import RestSend ModelType = TypeVar("ModelType", bound=NDBaseModel) @@ -53,14 +55,68 @@ class NDBaseOrchestrator(BaseModel, Generic[ModelType]): create_bulk_endpoint: Optional[Type[NDEndpointBaseModel]] = None delete_bulk_endpoint: Optional[Type[NDEndpointBaseModel]] = None - # NOTE: Module Field is always required - sender: NDModule + # REST infrastructure + rest_send: RestSend + + def _request(self, path: str, verb: HttpVerbEnum, data: Optional[Dict[str, Any]] = None) -> ResponseType: + """ + # Summary + + Send a REST request via RestSend and return the response DATA. + + ## Raises + + ### Exception + + - If the request fails (non-success result from the controller). + """ + self.rest_send.path = path + self.rest_send.verb = verb + if data is not None: + self.rest_send.payload = data + self.rest_send.commit() + + result = self.rest_send.result_current + if not result.get("success", False): + response = self.rest_send.response_current + msg = response.get("MESSAGE", "Unknown error") + code = response.get("RETURN_CODE", -1) + raise Exception(f"Request failed ({code}): {msg}") + + return self.rest_send.response_current.get("DATA", {}) + + def _query_obj(self, path: str) -> Dict[str, Any]: + """ + # Summary + + GET the given path and return the DATA dict, or empty dict if not found. + + ## Raises + + ### Exception + + - If the request fails with a non-404 error. + """ + self.rest_send.path = path + self.rest_send.verb = HttpVerbEnum.GET + self.rest_send.commit() + + result = self.rest_send.result_current + if not result.get("success", False): + response = self.rest_send.response_current + if response.get("RETURN_CODE") == 404: + return {} + msg = response.get("MESSAGE", "Unknown error") + code = response.get("RETURN_CODE", -1) + raise Exception(f"Query failed ({code}): {msg}") + + return self.rest_send.response_current.get("DATA", {}) # NOTE: Generic CRUD API operations for simple endpoints with single identifier (e.g. "api/v1/infra/aaa/LocalUsers/{loginID}") def create(self, model_instance: ModelType, **kwargs) -> ResponseType: try: api_endpoint = self.create_endpoint() - return self.sender.request(path=api_endpoint.path, method=api_endpoint.verb, data=model_instance.to_payload()) + return self._request(path=api_endpoint.path, verb=api_endpoint.verb, data=model_instance.to_payload()) except Exception as e: raise Exception(f"Create failed for {model_instance.get_identifier_value()}: {e}") from e @@ -68,7 +124,7 @@ def update(self, model_instance: ModelType, **kwargs) -> ResponseType: try: api_endpoint = self.update_endpoint() api_endpoint.set_identifiers(model_instance.get_identifier_value()) - return self.sender.request(path=api_endpoint.path, method=api_endpoint.verb, data=model_instance.to_payload()) + 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 @@ -76,7 +132,7 @@ def delete(self, model_instance: ModelType, **kwargs) -> ResponseType: try: api_endpoint = self.delete_endpoint() api_endpoint.set_identifiers(model_instance.get_identifier_value()) - return self.sender.request(path=api_endpoint.path, method=api_endpoint.verb) + return self._request(path=api_endpoint.path, verb=api_endpoint.verb) except Exception as e: raise Exception(f"Delete failed for {model_instance.get_identifier_value()}: {e}") from e @@ -84,14 +140,14 @@ def query_one(self, model_instance: ModelType, **kwargs) -> ResponseType: try: api_endpoint = self.query_one_endpoint() api_endpoint.set_identifiers(model_instance.get_identifier_value()) - return self.sender.request(path=api_endpoint.path, method=api_endpoint.verb) + 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 def query_all(self, model_instance: Optional[ModelType] = None, **kwargs) -> ResponseType: try: api_endpoint = self.query_all_endpoint() - result = self.sender.query_obj(api_endpoint.path) + result = self._query_obj(api_endpoint.path) return result or [] except Exception as e: raise Exception(f"Query all failed: {e}") from e diff --git a/plugins/module_utils/orchestrators/local_user.py b/plugins/module_utils/orchestrators/local_user.py index e95a3003..000a2555 100644 --- a/plugins/module_utils/orchestrators/local_user.py +++ b/plugins/module_utils/orchestrators/local_user.py @@ -4,18 +4,19 @@ from __future__ import absolute_import, division, print_function -from typing import Type, ClassVar -from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.base import NDBaseOrchestrator -from ansible_collections.cisco.nd.plugins.module_utils.models.base import NDBaseModel -from ansible_collections.cisco.nd.plugins.module_utils.models.local_user.local_user import LocalUserModel +from typing import ClassVar, Type + from ansible_collections.cisco.nd.plugins.module_utils.endpoints.base import NDEndpointBaseModel -from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.types import ResponseType from ansible_collections.cisco.nd.plugins.module_utils.endpoints.v1.infra.aaa_local_users import ( - EpInfraAaaLocalUsersPost, - EpInfraAaaLocalUsersPut, EpInfraAaaLocalUsersDelete, EpInfraAaaLocalUsersGet, + EpInfraAaaLocalUsersPost, + EpInfraAaaLocalUsersPut, ) +from ansible_collections.cisco.nd.plugins.module_utils.models.base import NDBaseModel +from ansible_collections.cisco.nd.plugins.module_utils.models.local_user.local_user import LocalUserModel +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 LocalUserOrchestrator(NDBaseOrchestrator[LocalUserModel]): @@ -33,7 +34,7 @@ def query_all(self) -> ResponseType: """ try: api_endpoint = self.query_all_endpoint() - result = self.sender.query_obj(api_endpoint.path) + result = self._query_obj(api_endpoint.path) return result.get("localusers", []) or [] except Exception as e: raise Exception(f"Query all failed: {e}") from e diff --git a/plugins/module_utils/orchestrators/manage_fabric_ebgp.py b/plugins/module_utils/orchestrators/manage_fabric_ebgp.py index 2171189a..47331bd6 100644 --- a/plugins/module_utils/orchestrators/manage_fabric_ebgp.py +++ b/plugins/module_utils/orchestrators/manage_fabric_ebgp.py @@ -9,18 +9,19 @@ __metaclass__ = type from typing import Type -from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.base import NDBaseOrchestrator -from ansible_collections.cisco.nd.plugins.module_utils.models.base import NDBaseModel -from ansible_collections.cisco.nd.plugins.module_utils.models.manage_fabric.manage_fabric_ebgp import FabricEbgpModel + from ansible_collections.cisco.nd.plugins.module_utils.endpoints.base import NDEndpointBaseModel -from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.types import ResponseType from ansible_collections.cisco.nd.plugins.module_utils.endpoints.v1.manage.manage_fabrics import ( + EpManageFabricsDelete, EpManageFabricsGet, EpManageFabricsListGet, EpManageFabricsPost, EpManageFabricsPut, - EpManageFabricsDelete, ) +from ansible_collections.cisco.nd.plugins.module_utils.models.base import NDBaseModel +from ansible_collections.cisco.nd.plugins.module_utils.models.manage_fabric.manage_fabric_ebgp import FabricEbgpModel +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 ManageEbgpFabricOrchestrator(NDBaseOrchestrator): @@ -39,7 +40,7 @@ def query_all(self) -> ResponseType: """ try: api_endpoint = self.query_all_endpoint() - result = self.sender.query_obj(api_endpoint.path) + result = self._query_obj(api_endpoint.path) fabrics = result.get("fabrics", []) or [] return [f for f in fabrics if f.get("management", {}).get("type") == "vxlanEbgp"] except Exception as e: diff --git a/plugins/module_utils/orchestrators/manage_fabric_external.py b/plugins/module_utils/orchestrators/manage_fabric_external.py index d370315a..d32a53f4 100644 --- a/plugins/module_utils/orchestrators/manage_fabric_external.py +++ b/plugins/module_utils/orchestrators/manage_fabric_external.py @@ -9,18 +9,19 @@ __metaclass__ = type from typing import Type -from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.base import NDBaseOrchestrator -from ansible_collections.cisco.nd.plugins.module_utils.models.base import NDBaseModel -from ansible_collections.cisco.nd.plugins.module_utils.models.manage_fabric.manage_fabric_external import FabricExternalConnectivityModel + from ansible_collections.cisco.nd.plugins.module_utils.endpoints.base import NDEndpointBaseModel -from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.types import ResponseType from ansible_collections.cisco.nd.plugins.module_utils.endpoints.v1.manage.manage_fabrics import ( + EpManageFabricsDelete, EpManageFabricsGet, EpManageFabricsListGet, EpManageFabricsPost, EpManageFabricsPut, - EpManageFabricsDelete, ) +from ansible_collections.cisco.nd.plugins.module_utils.models.base import NDBaseModel +from ansible_collections.cisco.nd.plugins.module_utils.models.manage_fabric.manage_fabric_external import FabricExternalConnectivityModel +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 ManageExternalFabricOrchestrator(NDBaseOrchestrator): @@ -39,7 +40,7 @@ def query_all(self) -> ResponseType: """ try: api_endpoint = self.query_all_endpoint() - result = self.sender.query_obj(api_endpoint.path) + result = self._query_obj(api_endpoint.path) fabrics = result.get("fabrics", []) or [] return [f for f in fabrics if f.get("management", {}).get("type") == "externalConnectivity"] except Exception as e: diff --git a/plugins/module_utils/orchestrators/manage_fabric_ibgp.py b/plugins/module_utils/orchestrators/manage_fabric_ibgp.py index 9fb5da78..82669c87 100644 --- a/plugins/module_utils/orchestrators/manage_fabric_ibgp.py +++ b/plugins/module_utils/orchestrators/manage_fabric_ibgp.py @@ -9,18 +9,19 @@ __metaclass__ = type from typing import Type -from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.base import NDBaseOrchestrator -from ansible_collections.cisco.nd.plugins.module_utils.models.base import NDBaseModel -from ansible_collections.cisco.nd.plugins.module_utils.models.manage_fabric.manage_fabric_ibgp import FabricIbgpModel + from ansible_collections.cisco.nd.plugins.module_utils.endpoints.base import NDEndpointBaseModel -from ansible_collections.cisco.nd.plugins.module_utils.orchestrators.types import ResponseType from ansible_collections.cisco.nd.plugins.module_utils.endpoints.v1.manage.manage_fabrics import ( + EpManageFabricsDelete, EpManageFabricsGet, EpManageFabricsListGet, EpManageFabricsPost, EpManageFabricsPut, - EpManageFabricsDelete, ) +from ansible_collections.cisco.nd.plugins.module_utils.models.base import NDBaseModel +from ansible_collections.cisco.nd.plugins.module_utils.models.manage_fabric.manage_fabric_ibgp import FabricIbgpModel +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 ManageIbgpFabricOrchestrator(NDBaseOrchestrator): @@ -39,7 +40,7 @@ def query_all(self) -> ResponseType: """ try: api_endpoint = self.query_all_endpoint() - result = self.sender.query_obj(api_endpoint.path) + result = self._query_obj(api_endpoint.path) fabrics = result.get("fabrics", []) or [] return [f for f in fabrics if f.get("management", {}).get("type") == "vxlanIbgp"] except Exception as e: