Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
(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)
- `EpFabricSoftwareUpdatePlanStage` - Stage and validate images for update groups
(POST /api/v1/manage/fabrics/{fabric_name}/softwareUpdatePlan/actions/stage)
"""

from __future__ import annotations
Expand Down Expand Up @@ -166,3 +168,59 @@ class EpFabricSoftwareUpdatePlanPropose(_EpFabricSoftwareUpdatePlanActionBase):
def verb(self) -> HttpVerbEnum:
"""Return the HTTP verb for this endpoint."""
return HttpVerbEnum.POST


class EpFabricSoftwareUpdatePlanStage(FabricNameMixin, NDEndpointBaseModel):
"""
# Summary

Stage and validate images for one or more update groups (the GUI "Prepare" action).

ND copies the configured image to each switch's bootflash, runs `show install all impact`, and
generates pre-reports. The action is asynchronous: it returns HTTP 202 with an empty body, and
progress is observed via the `softwareUpdatePlan/summary` endpoint.

- Path: `/api/v1/manage/fabrics/{fabric_name}/softwareUpdatePlan/actions/stage`
- Verb: POST
- Body: `{"updateGroupNames": ["...", "..."]}`

## Raises

### ValueError

- Via `path` property if `fabric_name` is not set.
"""

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

@property
def path(self) -> str:
"""
# Summary

Build the stage 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", "stage")

@property
def verb(self) -> HttpVerbEnum:
"""
# Summary

Return `HttpVerbEnum.POST`.

## Raises

None
"""
return HttpVerbEnum.POST
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# 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 software update plan summary endpoint model.

## Endpoints

- `EpFabricSoftwareUpdatePlanSummary` - Software update plan summary for a fabric
(GET /api/v1/manage/fabrics/{fabric_name}/softwareUpdatePlan/summary)
"""

from __future__ import annotations

from typing import 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.query_params import EndpointQueryParams
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 SoftwareUpdatePlanSummaryEndpointParams(EndpointQueryParams):
"""
# Summary

Endpoint-specific query parameters for the software update plan summary endpoint.

`update_group_name` is rendered as the API's `updateGroupName` query parameter (snake_case ->
camelCase is automatic via `EndpointQueryParams`). When set, Nexus Dashboard scopes the summary
to that single update group instead of returning the fabric-wide plan.

## Raises

None
"""

update_group_name: str | None = Field(default=None, min_length=1, description="Scope the summary to a single update group")


class EpFabricSoftwareUpdatePlanSummary(FabricNameMixin, NDEndpointBaseModel):
"""
# Summary

Software update plan summary for a fabric.

Returns the fabric-wide software update plan: every update group with its per-switch stage /
validate / install status. Used to drive the `nd_fabric_prepare_update` pre-flight role check
and to poll for staging completion.

Set `endpoint_params.update_group_name` to scope the summary to a single update group via the
API's optional `updateGroupName` query parameter; leave it unset for the fabric-wide summary.

- Path: `/api/v1/manage/fabrics/{fabric_name}/softwareUpdatePlan/summary`
- Verb: GET

## Raises

### ValueError

- Via `path` property if `fabric_name` is not set.
"""

class_name: Literal["EpFabricSoftwareUpdatePlanSummary"] = Field(
default="EpFabricSoftwareUpdatePlanSummary", frozen=True, description="Class name for backward compatibility"
)
endpoint_params: SoftwareUpdatePlanSummaryEndpointParams = Field(
default_factory=SoftwareUpdatePlanSummaryEndpointParams, description="Endpoint-specific query parameters"
)

@property
def path(self) -> str:
"""
# Summary

Build the software update plan summary endpoint path. `fabric_name` is percent-encoded with `safe=""`. When
`endpoint_params.update_group_name` is set, the `updateGroupName` query string is appended to scope the summary.

## 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.")
base_path = BasePath.path("fabrics", quote(self.fabric_name, safe=""), "softwareUpdatePlan", "summary")
query_string = self.endpoint_params.to_query_string()
if query_string:
return f"{base_path}?{query_string}"
return base_path

@property
def verb(self) -> HttpVerbEnum:
"""
# Summary

Return `HttpVerbEnum.GET`.

## Raises

None
"""
return HttpVerbEnum.GET
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# 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)
"""
Pydantic models for parsing the ND `softwareUpdatePlan/summary` response.

The summary endpoint returns the fabric-wide software update plan: every update group with its
per-switch stage / validate / install status. `nd_fabric_prepare_update` uses these models for the
pre-flight switch-role check and to poll for staging completion. The models are response-only -
they parse the wire shape and are never serialized back to ND.
"""

from __future__ import annotations

from typing import Literal, TypeAlias

from ansible_collections.cisco.nd.plugins.module_utils.common.pydantic_compat import Field
from ansible_collections.cisco.nd.plugins.module_utils.models.nested import NDNestedModel

# Known per-switch stage / validate statuses on ND 4.2.1. The `str` fallback in `ImageStatus` lets
# an unrecognized status from a newer ND release parse rather than raising during a poll; type
# checkers collapse the union to `str`, so the Literal is documentation of the known vocabulary.
KnownImageStatus: TypeAlias = Literal["none", "inProgress", "success", "failed", "skipped"]
ImageStatus: TypeAlias = KnownImageStatus | str


class UpdateGroupWarningModel(NDNestedModel):
"""
# Summary

A single advisory warning ND attaches to an update group in the software update plan summary
(for example, that upgrading the selected switches would impact every switch of a role).

## Raises

None
"""

message: str | None = Field(default=None, alias="message")
switch_name: str | None = Field(default=None, alias="switchName")


class SwitchStageStatusModel(NDNestedModel):
"""
# Summary

Per-switch stage / validate status within an update group, as reported by the
`softwareUpdatePlan/summary` endpoint.

`image_staged_status` and `image_validated_status` are typed `ImageStatus`
(`KnownImageStatus | str`): the `Literal` half documents the known vocabulary while the `str`
fallback lets an unrecognized status from a newer ND release parse rather than raising during
a poll.

## Raises

None
"""

switch_id: str | None = Field(default=None, alias="switchId")
switch_name: str | None = Field(default=None, alias="switchName")
switch_role: str | None = Field(default=None, alias="switchRole")
switch_management_ip: str | None = Field(default=None, alias="switchManagementIP")
switch_version: str | None = Field(default=None, alias="switchVersion")
selected_version: str | None = Field(default=None, alias="selectedVersion")
image_staged_status: ImageStatus | None = Field(default=None, alias="imageStagedStatus")
image_validated_status: ImageStatus | None = Field(default=None, alias="imageValidatedStatus")
switch_stage_validate_percentage: int | None = Field(default=None, alias="switchStageValidatePercentage")


class UpdateGroupStatusModel(NDNestedModel):
"""
# Summary

A single update group's status within the software update plan summary, including its member
switches and any advisory warnings ND raises for the group.

The `warnings` and `update_group_switches` lists default to empty: a key absent from the
response body parses the same as an explicitly empty list, since every consumer treats the two
identically.

`update_group_status` is kept as a free-form string (not a `Literal`) for the same reason as
the per-switch statuses in `SwitchStageStatusModel`: an unrecognized status from a newer ND
release must parse rather than raise during a poll. Its vocabulary is not lab-confirmed, so no
known-values alias is provided.

## Raises

None
"""

update_group_name: str | None = Field(default=None, alias="updateGroupName")
update_group_status: str | None = Field(default=None, alias="updateGroupStatus")
Comment thread
akinross marked this conversation as resolved.
update_type: str | None = Field(default=None, alias="updateType")
stage_validate_percentage: int | None = Field(default=None, alias="stageValidatePercentage")
switch_count: int | None = Field(default=None, alias="switchCount")
warnings: list[UpdateGroupWarningModel] = Field(default_factory=list, alias="warnings")
update_group_switches: list[SwitchStageStatusModel] = Field(default_factory=list, alias="updateGroupSwitches")


class SoftwareUpdatePlanSummaryModel(NDNestedModel):
"""
# Summary

Top-level parse of the `softwareUpdatePlan/summary` response. Only the `updateGroups` list is
modeled; the `softwareUpdateSummary` and `tableHeaders` blocks are ignored (`extra="ignore"`).
`update_groups` defaults to empty, so a response missing the key parses the same as an
explicitly empty plan.

## Raises

None
"""

update_groups: list[UpdateGroupStatusModel] = Field(default_factory=list, alias="updateGroups")
Loading
Loading