Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion plugins/module_utils/endpoints/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@

from typing import Optional

from ansible_collections.cisco.nd.plugins.module_utils.enums import BooleanStringEnum
from ansible_collections.cisco.nd.plugins.module_utils.common.pydantic_compat import (
BaseModel,
Field,
)
from ansible_collections.cisco.nd.plugins.module_utils.enums import BooleanStringEnum


class ClusterNameMixin(BaseModel):
Expand Down Expand Up @@ -86,6 +86,12 @@ class SwitchSerialNumberMixin(BaseModel):
switch_sn: Optional[str] = Field(default=None, min_length=1, description="Switch serial number")


class UpdateGroupNameMixin(BaseModel):
"""Mixin for endpoints that require update_group_name parameter."""

update_group_name: Optional[str] = Field(default=None, min_length=1, description="Update group name")


class VrfNameMixin(BaseModel):
"""Mixin for endpoints that require vrf_name parameter."""

Expand Down
180 changes: 180 additions & 0 deletions plugins/module_utils/endpoints/v1/manage/fabric_update_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Copyright: (c) 2026, Allen Robel (@allenrobel) <arobel@cisco.com>

# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
ND Manage Fabric Update Group endpoint models.
This module contains endpoint definitions for fabric update group operations
under the ND Manage Fabric Software Management API.
## Endpoints
- `EpFabricUpdateGroupListGet` - List update groups in a fabric
(GET /api/v1/manage/fabrics/{fabric_name}/updateGroups)
- `EpFabricUpdateGroupGet` - Get a specific update group by name
(GET /api/v1/manage/fabrics/{fabric_name}/updateGroups/{update_group_name})
- `EpFabricUpdateGroupPut` - Update an existing update group
(PUT /api/v1/manage/fabrics/{fabric_name}/updateGroups/{update_group_name})
- `EpFabricUpdateGroupDelete` - Delete an update group
(DELETE /api/v1/manage/fabrics/{fabric_name}/updateGroups/{update_group_name})
"""

from __future__ import annotations

from typing import ClassVar, Literal
from urllib.parse import quote

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.mixins import FabricNameMixin, UpdateGroupNameMixin
from ansible_collections.cisco.nd.plugins.module_utils.endpoints.v1.manage.base_path import BasePath
from ansible_collections.cisco.nd.plugins.module_utils.enums import HttpVerbEnum
from ansible_collections.cisco.nd.plugins.module_utils.types import IdentifierKey


class _EpFabricUpdateGroupBase(UpdateGroupNameMixin, FabricNameMixin, NDEndpointBaseModel):
"""
Base class for ND Manage Fabric Update Group endpoints.
Provides common functionality for all HTTP methods on the
/api/v1/manage/fabrics/{fabric_name}/updateGroups endpoint.
Subclasses may override:
- ``_require_update_group_name``: set to ``False`` for collection-level endpoints
(list, create) that do not include an update group name in the path.
"""

_require_update_group_name: ClassVar[bool] = True

def set_identifiers(self, identifier: IdentifierKey = None):
self.update_group_name = identifier

@property
def path(self) -> str:
"""
# Summary
Build the endpoint path with required fabric_name and optional update_group_name.
## Raises
### ValueError
- If `fabric_name` is not set when accessing `path`
- If `update_group_name` is required but not set
"""
if self.fabric_name is None:
raise ValueError(f"{type(self).__name__}.path: fabric_name must be set before accessing path.")
if self._require_update_group_name and self.update_group_name is None:
raise ValueError(f"{type(self).__name__}.path: update_group_name must be set before accessing path.")
segments = ["fabrics", quote(self.fabric_name, safe=""), "updateGroups"]
if self.update_group_name is not None:
segments.append(quote(self.update_group_name, safe=""))
return BasePath.path(*segments)


class EpFabricUpdateGroupListGet(_EpFabricUpdateGroupBase):
"""
# Summary
ND Manage Fabric Update Group List GET endpoint.
## Path
- `/api/v1/manage/fabrics/{fabric_name}/updateGroups`
## Verb
- GET
## Usage
```python
ep = EpFabricUpdateGroupListGet()
ep.fabric_name = "SITE1"
rest_send.path = ep.path
rest_send.verb = ep.verb
```
"""

_require_update_group_name: ClassVar[bool] = False

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

@property
def verb(self) -> HttpVerbEnum:
"""Return the HTTP verb for this endpoint."""
return HttpVerbEnum.GET


class EpFabricUpdateGroupGet(_EpFabricUpdateGroupBase):
"""
# Summary
ND Manage Fabric Update Group GET endpoint.
## Path
- `/api/v1/manage/fabrics/{fabric_name}/updateGroups/{update_group_name}`
## Verb
- GET
"""

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

@property
def verb(self) -> HttpVerbEnum:
"""Return the HTTP verb for this endpoint."""
return HttpVerbEnum.GET


class EpFabricUpdateGroupPut(_EpFabricUpdateGroupBase):
"""
# Summary
ND Manage Fabric Update Group PUT endpoint.
## Path
- `/api/v1/manage/fabrics/{fabric_name}/updateGroups/{update_group_name}`
## Verb
- PUT
"""

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

@property
def verb(self) -> HttpVerbEnum:
"""Return the HTTP verb for this endpoint."""
return HttpVerbEnum.PUT


class EpFabricUpdateGroupDelete(_EpFabricUpdateGroupBase):
"""
# Summary
ND Manage Fabric Update Group DELETE endpoint.
## Path
- `/api/v1/manage/fabrics/{fabric_name}/updateGroups/{update_group_name}`
## Verb
- DELETE
"""

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

@property
def verb(self) -> HttpVerbEnum:
"""Return the HTTP verb for this endpoint."""
return HttpVerbEnum.DELETE
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Copyright: (c) 2026, Allen Robel (@allenrobel) <arobel@cisco.com>

# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
ND Manage Fabric Software Management switch-centric action endpoint models.
These endpoints back the GUI's switch-centric update-group flow. Unlike the group-centric
`updateGroups` CRUD endpoints, they are ghost-safe by construction: `attachGroup` requires at least
one switch, and `detachGroup` auto-deletes a group server-side once its last switch is removed.
All three endpoints share the same shape - a POST to
`/api/v1/manage/fabrics/{fabric_name}/softwareUpdatePlan/actions/{action}` - so the path and verb
live on a common `_EpFabricSoftwareUpdatePlanActionBase`; each concrete endpoint just sets its
`_action` segment.
## Endpoints
- `EpFabricSoftwareUpdatePlanAttachGroup` - Create an update group and assign switches to it
(POST /api/v1/manage/fabrics/{fabric_name}/softwareUpdatePlan/actions/attachGroup)
- `EpFabricSoftwareUpdatePlanDetachGroup` - Detach switches from an update group
(POST /api/v1/manage/fabrics/{fabric_name}/softwareUpdatePlan/actions/detachGroup)
- `EpFabricSoftwareUpdatePlanPropose` - Auto-assign update groups fabric-wide by algorithm
(POST /api/v1/manage/fabrics/{fabric_name}/softwareUpdatePlan/actions/propose)
"""

from __future__ import annotations

from typing import ClassVar, Literal
from urllib.parse import quote

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.mixins import FabricNameMixin
from ansible_collections.cisco.nd.plugins.module_utils.endpoints.v1.manage.base_path import BasePath
from ansible_collections.cisco.nd.plugins.module_utils.enums import HttpVerbEnum


class _EpFabricSoftwareUpdatePlanActionBase(FabricNameMixin, NDEndpointBaseModel):
"""
# Summary
Base class for the switch-centric Fabric Software Management action endpoints. Every action is a
POST to `/api/v1/manage/fabrics/{fabric_name}/softwareUpdatePlan/actions/{_action}`; subclasses
set the `_action` segment, the `verb`, and their own `class_name`.
`verb` is intentionally left abstract here (rather than defined as POST on the base) so the
endpoint metaclass keeps treating this base as abstract and does not require it to carry a
`class_name` field - mirroring the `_EpFabricUpdateGroupBase` pattern in this package.
## Raises
### ValueError
- Via `path` property if `fabric_name` is not set.
"""

# Action path segment (e.g. "attachGroup"). Overridden per subclass. Accessed via `self._action`
# (instance access) - the leading-underscore ClassVar trap that bites Pydantic v2 on Python 3.10
# only fires on CLASS-level access, which this never does.
_action: ClassVar[str] = ""

@property
def path(self) -> str:
"""
# Summary
Build the action endpoint path. `fabric_name` is percent-encoded with `safe=""`.
## Raises
### ValueError
- If `fabric_name` is not set before accessing `path`.
"""
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", quote(self.fabric_name, safe=""), "softwareUpdatePlan", "actions", self._action)


class EpFabricSoftwareUpdatePlanAttachGroup(_EpFabricSoftwareUpdatePlanActionBase):
"""
# Summary
Create an update group and assign switches to it (switch-centric, ghost-safe).
- Path: `/api/v1/manage/fabrics/{fabric_name}/softwareUpdatePlan/actions/attachGroup`
- Verb: POST
- Body: `{"attachUpdateGroups": [{"updateGroupName": "...", "switchIds": ["..."], "forceCreated": false}]}`
## Raises
### ValueError
- Via `path` property if `fabric_name` is not set.
"""

_action: ClassVar[str] = "attachGroup"

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

@property
def verb(self) -> HttpVerbEnum:
"""Return the HTTP verb for this endpoint."""
return HttpVerbEnum.POST


class EpFabricSoftwareUpdatePlanDetachGroup(_EpFabricSoftwareUpdatePlanActionBase):
"""
# Summary
Detach switches from an update group (switch-centric). Removing a group's last switch
auto-deletes the group server-side.
- Path: `/api/v1/manage/fabrics/{fabric_name}/softwareUpdatePlan/actions/detachGroup`
- Verb: POST
- Body: `{"detachUpdateGroups": [{"updateGroupName": "...", "switchIds": ["..."]}]}`
## Raises
### ValueError
- Via `path` property if `fabric_name` is not set.
"""

_action: ClassVar[str] = "detachGroup"

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

@property
def verb(self) -> HttpVerbEnum:
"""Return the HTTP verb for this endpoint."""
return HttpVerbEnum.POST


class EpFabricSoftwareUpdatePlanPropose(_EpFabricSoftwareUpdatePlanActionBase):
"""
# Summary
Auto-assign update groups fabric-wide (the GUI "Auto-generate groups" action).
ND generates the update groups itself based on the requested algorithm and applies the result
immediately - it is not a preview.
- Path: `/api/v1/manage/fabrics/{fabric_name}/softwareUpdatePlan/actions/propose`
- Verb: POST
- Body: `{"algorithm": "roleBased"}` # or "evenOdd"
## Raises
### ValueError
- Via `path` property if `fabric_name` is not set.
"""

_action: ClassVar[str] = "propose"

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

@property
def verb(self) -> HttpVerbEnum:
"""Return the HTTP verb for this endpoint."""
return HttpVerbEnum.POST
Empty file.
Loading
Loading