Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
5c8b04d
[ignore] Add generic logger facility through the Log class and loggin…
allenrobel Mar 2, 2026
756bdcc
[ignore] Add Endpoints framework for ND API v1 (#186)
allenrobel Mar 11, 2026
f5b1655
[ignore] Add RestSend framework, enums, and shared unit test infrastr…
allenrobel Mar 12, 2026
06633f6
[ignore] Fix broken imports after nd42_rest_send restructuring
allenrobel Mar 13, 2026
1c3b3c6
[minor_change] Add nd_local_user as a new network resource module for…
gmicol Aug 19, 2025
c70a93f
[ignore] First Pydantic implementation: Add Pydantic Models for nd_lo…
gmicol Jan 15, 2026
267e2a8
[ignore] Second Pydantic Implementation: Create a NDBaseModel to be i…
gmicol Jan 20, 2026
9b6bebd
[ignore] Pydantic Models: Modify and Clean both local_user.py and bas…
gmicol Jan 22, 2026
019c8e3
[ignore] Pydantic ND base models and local_user models: Final proposi…
gmicol Jan 23, 2026
427f33f
[ignore] Pydantic ND Config Collection: Final proposition of core des…
gmicol Jan 23, 2026
6fe3bbf
[ignore] Pydantic Base ND Network Resource Module: Final proposition …
gmicol Jan 23, 2026
0b36b2d
[ignore] Modify nd_local_user based on Pydantic implementation and ch…
gmicol Jan 23, 2026
e37636f
[ignore] Add api_endpoints for configuring endpoints and orchestrator…
gmicol Feb 17, 2026
fcde8c9
[ignore] Modifiy models/local_user to take full advantage of Pydantic…
gmicol Feb 18, 2026
5cf2a48
[ignore] Adapt the Network Resource Module architecture for ND to sma…
gmicol Feb 24, 2026
6c411bc
[ignore] Default to none and update condition for regarding in mod…
gmicol Feb 25, 2026
8ed627c
[ignore] Add choice for when no identifier is needed. Add quick com…
gmicol Feb 26, 2026
1ddd995
[ignore] Complete orchestrators/base.py by making simple CRUD operati…
gmicol Feb 26, 2026
aa99bbf
[ignore] Fix and in nd_config_collections.py. Move to utils.py.
gmicol Feb 26, 2026
04c73ff
[ignore] Rename NDNetworkResourceModule to NDStateMachine. Add file f…
gmicol Feb 26, 2026
85c36e8
[ignore] Make a small change to NDModule request function.
gmicol Feb 26, 2026
034b49f
[ignore] Modify nd_state_machine to work with orchestrators/models/ap…
gmicol Mar 2, 2026
b6ddee4
[ignore] Add proper path dependencies and Ran black formatting.
gmicol Mar 3, 2026
1d96db1
[ignore] Clean code for sanity purposes (except Pydantic import checks.
gmicol Mar 3, 2026
5d1f52f
[ignore] Restructure api_endpoints folder into endpoints -> v1. Fix s…
gmicol Mar 3, 2026
039103e
[ignore] Remove NDModule inheritence from NDStateMachine. Add first i…
gmicol Mar 3, 2026
c1774d1
[ignore] Rename NDBaseSmartEndpoint to NDBaseEndpoint. Fix importatio…
gmicol Mar 3, 2026
872b5f4
[ignore] Replace all pydantic imports with pydantic_compat. Fix sanit…
gmicol Mar 4, 2026
24c0686
[ignore] Add NDOutput class. Modify NDStateMachine and nd_local_user …
gmicol Mar 6, 2026
dedc958
[ignore] Update NDOutput class. Remove all fail_json dependencies in …
gmicol Mar 10, 2026
2d472d9
[ignore] Fix serialization of model with minimal changes to base.py …
gmicol Mar 11, 2026
de81d56
[ignore] Complete nd_local_user integration test for creation and upd…
gmicol Mar 11, 2026
401deed
[ignore] Finish integration test file for nd_local_user module. Remov…
gmicol Mar 12, 2026
a325955
[ignore] Fix sanity issues by enhancing pydantic_compat.py. Fix Black…
gmicol Mar 12, 2026
ea06769
[ignore] Remove all TODO comments.
gmicol Mar 12, 2026
47af5f4
[ignore] Update endpoints to match latest nd42_integration branch. Up…
gmicol Mar 12, 2026
2f6de8a
[ignore] Update pydantic_compat.py to support extra Pydantic methods …
gmicol Mar 12, 2026
79dc000
[ignore] Remove Python 2.7 compatibilities.
gmicol Mar 17, 2026
2c7ec78
[ignore] Fix comments and docstrings. made and static methods for …
gmicol Mar 17, 2026
7142c43
[ignore] Slightly modify Exceptions handling in NDStateMachine. Remov…
gmicol Mar 18, 2026
bc5cfb0
[ignore] Rename aaa_local_users.py to infra_aaa_local_users.py. Move …
gmicol Mar 18, 2026
ed33a5a
[ignore] Update integration tests for nd_local_user module.
gmicol Mar 19, 2026
a859d13
[ignore] Revert local users endpoints filename to aaa_local_users.py.
gmicol Mar 19, 2026
96a492d
[ignore] Change in NDStateMachine initialization to take advantage o…
gmicol Mar 19, 2026
4c593df
[ignore] Remove ValidationError import from nd_state_machine.py.
gmicol Mar 19, 2026
f3a0eff
[ignore] Add function to nd_local_user module. Slighty fix Documenta…
gmicol Mar 24, 2026
a8073b6
[ignore] Make NDBaseOrchestrator a Generic class.
gmicol Mar 24, 2026
3c54a5c
[ignore] Enable CI unit tests
samiib Mar 17, 2026
0af63e6
[ignore] Add pydantic to requirements.txt
samiib Mar 23, 2026
e0476f8
merge develop
mtarking Apr 2, 2026
932a186
temp update to pr branch list for ci
mtarking Apr 2, 2026
ed218e9
Ansible ND 4.X Fabric Modules for iBGP, eBGP and External Fabric Type…
mikewiebe Apr 8, 2026
9667163
initial tor module & tests
mtarking Apr 9, 2026
d5d65fb
Merge branch 'develop' into issue-226
mtarking Apr 9, 2026
e22ef86
update actions
mtarking Apr 9, 2026
af2bfd7
Merge branch 'develop' into issue-226
mtarking Apr 9, 2026
fdb585e
remove nd42_integration from actions
mtarking Apr 9, 2026
2416614
update lint errors
mtarking Apr 9, 2026
e7c37b9
resolve lint errors
mtarking Apr 9, 2026
74b6954
Merge branch 'develop' into issue-226
mtarking Apr 10, 2026
2409e69
refactor for bulk methods
mtarking Apr 10, 2026
07976c1
update query for tor associations
mtarking Apr 10, 2026
952d34c
update query functionality
mtarking Apr 10, 2026
ff2c186
update integration tests
mtarking Apr 10, 2026
7a39614
Merge branch 'issue-226' of github.com:CiscoDevNet/ansible-nd into is…
mtarking Apr 10, 2026
a0f6536
update association filtering
mtarking Apr 10, 2026
e80b89e
test new filter
mtarking Apr 10, 2026
d0be310
fix delete issue
mtarking Apr 10, 2026
ca8bd67
update nd to accept 207
mtarking Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions plugins/module_utils/endpoints/v1/manage/manage_tor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Copyright: (c) 2026, Matt Tarkington (@mtarking)

# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
ND Manage Access/ToR Association endpoint models.

Endpoints for access or ToR switch association operations
in the ND Manage API.

Endpoints:
- EpManageTorAssociatePost - Associate access/ToR switches
(POST /api/v1/manage/fabrics/{fabricName}/accessAssociationActions/associate)
- EpManageTorDisassociatePost - Disassociate access/ToR switches
(POST /api/v1/manage/fabrics/{fabricName}/accessAssociationActions/disassociate)
- EpManageTorAssociationsGet - List access/ToR associations
(GET /api/v1/manage/fabrics/{fabricName}/accessAssociations)
"""

from __future__ import absolute_import, annotations, division, print_function

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
from ansible_collections.cisco.nd.plugins.module_utils.endpoints.base import NDEndpointBaseModel
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 _EpManageTorBase(FabricNameMixin, NDEndpointBaseModel):
"""
Base class for ND Manage Access/ToR Association endpoints.

All ToR association endpoints require a fabric_name path parameter.
"""

_path_suffix: ClassVar[Optional[str]] = None

def set_identifiers(self, identifier: IdentifierKey = None):
if isinstance(identifier, tuple) and len(identifier) >= 1:
self.fabric_name = identifier[0]
elif isinstance(identifier, str):
self.fabric_name = identifier

def _build_path(self, *segments: str) -> 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, *segments)


class EpManageTorAssociatePost(_EpManageTorBase):
"""
POST /api/v1/manage/fabrics/{fabricName}/accessAssociationActions/associate

Associate access or ToR switches with aggregation/leaf switches or VPC pairs.
Request body is an array of accessPairWithResources objects.
"""

class_name: Literal["EpManageTorAssociatePost"] = Field(
default="EpManageTorAssociatePost",
frozen=True,
description="Class name for backward compatibility",
)

@property
def path(self) -> str:
return self._build_path("accessAssociationActions", "associate")

@property
def verb(self) -> HttpVerbEnum:
return HttpVerbEnum.POST


class EpManageTorDisassociatePost(_EpManageTorBase):
"""
POST /api/v1/manage/fabrics/{fabricName}/accessAssociationActions/disassociate

Disassociate access or ToR switches from aggregation/leaf switches or VPC pairs.
Request body is an array of aggregationAccessSwitchIds objects.
"""

class_name: Literal["EpManageTorDisassociatePost"] = Field(
default="EpManageTorDisassociatePost",
frozen=True,
description="Class name for backward compatibility",
)

@property
def path(self) -> str:
return self._build_path("accessAssociationActions", "disassociate")

@property
def verb(self) -> HttpVerbEnum:
return HttpVerbEnum.POST


class EpManageTorAssociationsGet(_EpManageTorBase):
"""
GET /api/v1/manage/fabrics/{fabricName}/accessAssociations

List access or ToR switch associations for a fabric.
"""

class_name: Literal["EpManageTorAssociationsGet"] = Field(
default="EpManageTorAssociationsGet",
frozen=True,
description="Class name for backward compatibility",
)

@property
def path(self) -> str:
return self._build_path("accessAssociations")

@property
def verb(self) -> HttpVerbEnum:
return HttpVerbEnum.GET
135 changes: 135 additions & 0 deletions plugins/module_utils/models/manage_tor/manage_tor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Copyright: (c) 2026, Matt Tarkington (@mtarking)

# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function

from typing import List, Dict, Any, Optional, ClassVar, Literal, Set

from ansible_collections.cisco.nd.plugins.module_utils.common.pydantic_compat import (
Field,
model_validator,
)
from ansible_collections.cisco.nd.plugins.module_utils.models.base import NDBaseModel


class ManageTorModel(NDBaseModel):
"""
Access/ToR switch association configuration for Nexus Dashboard.

Identifier: composite (fabric_name, access_or_tor_switch_id, aggregation_or_leaf_switch_id)

Serialization notes:
- fabric_name is excluded from API payload (path parameter only).
- Port channel and VPC ID fields are nested under "resources" in
payload mode but remain flat in config mode.
"""

# --- Identifier Configuration ---

identifiers: ClassVar[Optional[List[str]]] = [
"fabric_name",
"access_or_tor_switch_id",
"aggregation_or_leaf_switch_id",
]
identifier_strategy: ClassVar[Optional[Literal["single", "composite", "hierarchical", "singleton"]]] = "composite"

# --- Serialization Configuration ---

# fabric_name is a path parameter; hostname fields are read-only from API responses
payload_exclude_fields: ClassVar[Set[str]] = {
"fabric_name",
"access_or_tor_switch_name",
"access_or_tor_peer_switch_name",
}
exclude_from_diff: ClassVar[Set[str]] = {
"access_or_tor_switch_name",
"access_or_tor_peer_switch_name",
}

# In payload mode, nest these fields under "resources"
payload_nested_fields: ClassVar[Dict[str, List[str]]] = {
"resources": [
"access_or_tor_port_channel_id",
"aggregation_or_leaf_port_channel_id",
"access_or_tor_peer_port_channel_id",
"access_or_tor_vpc_id",
"aggregation_or_leaf_peer_port_channel_id",
"aggregation_or_leaf_vpc_id",
],
}

# --- Fields ---

# Path parameter / scope
fabric_name: str = Field(alias="fabricName")

# Required switch identifiers
access_or_tor_switch_id: str = Field(alias="accessOrTorSwitchId")
aggregation_or_leaf_switch_id: str = Field(alias="aggregationOrLeafSwitchId")

# Optional VPC peer switch identifiers
access_or_tor_peer_switch_id: Optional[str] = Field(default=None, alias="accessOrTorPeerSwitchId")
aggregation_or_leaf_peer_switch_id: Optional[str] = Field(default=None, alias="aggregationOrLeafPeerSwitchId")

# Read-only hostname fields (returned by API, never sent in payloads)
access_or_tor_switch_name: Optional[str] = Field(default=None, alias="accessOrTorSwitchName")
access_or_tor_peer_switch_name: Optional[str] = Field(default=None, alias="accessOrTorPeerSwitchName")

# Resource fields (nested under "resources" in API payload)
access_or_tor_port_channel_id: Optional[int] = Field(default=None, alias="accessOrTorPortChannelId")
aggregation_or_leaf_port_channel_id: Optional[int] = Field(default=None, alias="aggregationOrLeafPortChannelId")
access_or_tor_peer_port_channel_id: Optional[int] = Field(default=None, alias="accessOrTorPeerPortChannelId")
access_or_tor_vpc_id: Optional[int] = Field(default=None, alias="accessOrTorVpcId")
aggregation_or_leaf_peer_port_channel_id: Optional[int] = Field(default=None, alias="aggregationOrLeafPeerPortChannelId")
aggregation_or_leaf_vpc_id: Optional[int] = Field(default=None, alias="aggregationOrLeafVpcId")

# --- Validators (Deserialization) ---

@model_validator(mode="before")
@classmethod
def flatten_resources(cls, data: Any) -> Any:
"""
Flatten nested resources from API response into top-level fields.
This is the inverse of the payload_nested_fields nesting.
"""
if not isinstance(data, dict):
return data

resources = data.pop("resources", None)
if isinstance(resources, dict):
for key, val in resources.items():
data.setdefault(key, val)

return data

# --- Argument Spec ---

@classmethod
def get_argument_spec(cls) -> Dict:
return dict(
fabric_name=dict(type="str", required=True),
config=dict(
type="list",
elements="dict",
options=dict(
access_or_tor_switch_id=dict(type="str"),
aggregation_or_leaf_switch_id=dict(type="str", required=True),
access_or_tor_peer_switch_id=dict(type="str"),
aggregation_or_leaf_peer_switch_id=dict(type="str"),
access_or_tor_port_channel_id=dict(type="int"),
aggregation_or_leaf_port_channel_id=dict(type="int"),
access_or_tor_peer_port_channel_id=dict(type="int"),
access_or_tor_vpc_id=dict(type="int"),
aggregation_or_leaf_peer_port_channel_id=dict(type="int"),
aggregation_or_leaf_vpc_id=dict(type="int"),
),
),
state=dict(
type="str",
default="merged",
choices=["merged", "deleted", "gathered"],
),
# gathered also requires config to provide aggregation_or_leaf_switch_id
# for the ND API query parameter (enforced via required_if in the module).
)
4 changes: 2 additions & 2 deletions plugins/module_utils/nd.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,8 @@ def request(
elif info.get("modified") == "true":
self.result["changed"] = True

# 200: OK, 201: Created, 202: Accepted, 204: No Content
if self.status in (200, 201, 202, 204):
# 200: OK, 201: Created, 202: Accepted, 204: No Content, 207: Multi-Status
if self.status in (200, 201, 202, 204, 207):
if output_format == "raw":
return info.get("raw")
return info.get("body")
Expand Down
Loading
Loading