diff --git a/api/pkg/api/handler/rack.go b/api/pkg/api/handler/rack.go index e6acc5320..bb2583128 100644 --- a/api/pkg/api/handler/rack.go +++ b/api/pkg/api/handler/rack.go @@ -1150,7 +1150,7 @@ func (furh UpdateRackFirmwareHandler) Handle(c echo.Context) error { } flowResp, err := common.ExecuteFirmwareUpdateWorkflow(ctx, c, logger, stc, targetSpec, apiRequest.Version, - fmt.Sprintf("rack-firmware-update-%s", rackStrID), "Rack") + nil, fmt.Sprintf("rack-firmware-update-%s", rackStrID), "Rack") if err != nil { return err } @@ -1268,7 +1268,7 @@ func (furbh BatchUpdateRackFirmwareHandler) Handle(c echo.Context) error { targetSpec := request.Filter.ToTargetSpec() flowResp, err := common.ExecuteFirmwareUpdateWorkflow(ctx, c, logger, stc, targetSpec, request.Version, - fmt.Sprintf("rack-firmware-batch-update-%s", common.RequestHash(request.Filter)), "Rack") + nil, fmt.Sprintf("rack-firmware-batch-update-%s", common.RequestHash(request.Filter)), "Rack") if err != nil { return err } diff --git a/api/pkg/api/handler/tray.go b/api/pkg/api/handler/tray.go index 3dbb102e5..c085326bd 100644 --- a/api/pkg/api/handler/tray.go +++ b/api/pkg/api/handler/tray.go @@ -1144,7 +1144,7 @@ func (futh UpdateTrayFirmwareHandler) Handle(c echo.Context) error { } flowResp, err := common.ExecuteFirmwareUpdateWorkflow(ctx, c, logger, stc, targetSpec, apiRequest.Version, - fmt.Sprintf("tray-firmware-update-%s", trayStrID), "Tray") + apiRequest.Targets, fmt.Sprintf("tray-firmware-update-%s", trayStrID), "Tray") if err != nil { return err } @@ -1259,7 +1259,7 @@ func (futbh BatchUpdateTrayFirmwareHandler) Handle(c echo.Context) error { targetSpec := request.Filter.ToTargetSpec() flowResp, err := common.ExecuteFirmwareUpdateWorkflow(ctx, c, logger, stc, targetSpec, request.Version, - fmt.Sprintf("tray-firmware-batch-update-%s", common.RequestHash(request.Filter)), "Tray") + request.Targets, fmt.Sprintf("tray-firmware-batch-update-%s", common.RequestHash(request.Filter)), "Tray") if err != nil { return err } diff --git a/api/pkg/api/handler/util/common/common.go b/api/pkg/api/handler/util/common/common.go index 88161b179..92c295e0e 100644 --- a/api/pkg/api/handler/util/common/common.go +++ b/api/pkg/api/handler/util/common/common.go @@ -1762,6 +1762,13 @@ func ExecuteBringUpRackWorkflow( // ExecuteFirmwareUpdateWorkflow builds an UpgradeFirmwareRequest, executes the UpgradeFirmware // workflow via Temporal, and returns the raw SubmitTaskResponse. +// +// targets, when non-empty, restricts the upgrade to the listed firmware +// sub-parts within each targeted tray (e.g. ["bmc", "nvos"] for switch +// trays). An empty/nil slice keeps the historical "update everything in +// the bundle" behavior. Names are passed through verbatim to Flow as +// `sub_targets`, which resolves them against the tray-type-specific +// component-manager enums (see flow/pkg/common/firmwarecomponents). func ExecuteFirmwareUpdateWorkflow( ctx context.Context, c echo.Context, @@ -1769,12 +1776,14 @@ func ExecuteFirmwareUpdateWorkflow( stc tclient.Client, targetSpec *flowv1.OperationTargetSpec, version *string, + targets []string, workflowID string, entityName string, ) (*flowv1.SubmitTaskResponse, error) { flowRequest := &flowv1.UpgradeFirmwareRequest{ TargetSpec: targetSpec, TargetVersion: version, + SubTargets: targets, Description: fmt.Sprintf("API firmware update %s", entityName), } diff --git a/api/pkg/api/model/firmware.go b/api/pkg/api/model/firmware.go index 894e33f7b..5f1d8d0d0 100644 --- a/api/pkg/api/model/firmware.go +++ b/api/pkg/api/model/firmware.go @@ -29,6 +29,22 @@ import ( type APIUpdateFirmwareRequest struct { SiteID string `json:"siteId"` Version *string `json:"version,omitempty"` + // Targets, when non-empty, restricts the update to a subset of + // firmware sub-parts within the targeted tray (e.g. ["bmc", "nvos"] + // for switch trays). Names are lowercase. The authoritative supported + // set per tray type is derived from the Flow service's NICo proto + // bindings (mirroring Core's per-tray-type enums in + // carbide-core/crates/rpc/proto/forge.proto); see + // flow/pkg/common/firmwarecomponents for the resolution logic and + // helpers like SupportedNICoNVSwitchNames. + // Empty/nil means "update everything in the bundle". When non-empty, + // requires Version. + // + // REST surface intentionally calls these "targets" to avoid confusion + // with carbide's tray-level "Component" vocabulary; the downstream + // Flow proto field is named `sub_targets` and represents the same + // enum subset. + Targets []string `json:"targets,omitempty"` } // Validate validates the firmware update request @@ -36,7 +52,7 @@ func (r *APIUpdateFirmwareRequest) Validate() error { if r.SiteID == "" { return fmt.Errorf("siteId is required") } - return nil + return validateFirmwareTargets(r.Targets, r.Version) } // ========== Firmware Update Response ========== @@ -89,6 +105,10 @@ type APIBatchTrayFirmwareUpdateRequest struct { SiteID string `json:"siteId"` Filter *TrayFilter `json:"filter,omitempty"` Version *string `json:"version,omitempty"` + // Targets, when non-empty, restricts the update to a subset of + // firmware sub-parts within each matched tray. Same semantics as the + // single-tray variant. When non-empty, requires Version. + Targets []string `json:"targets,omitempty"` } // Validate checks required fields and filter constraints. @@ -96,5 +116,29 @@ func (r *APIBatchTrayFirmwareUpdateRequest) Validate() error { if r.SiteID == "" { return fmt.Errorf("siteId is required") } - return r.Filter.Validate() + if r.Filter != nil { + if err := r.Filter.Validate(); err != nil { + return err + } + } + return validateFirmwareTargets(r.Targets, r.Version) +} + +// validateFirmwareTargets enforces the cross-field constraint that a +// firmware-target subset selection is only meaningful when a target version +// is also supplied. Per-tray-type name validation is delegated to Flow, +// where the mapping from string to component-manager enum lives. +func validateFirmwareTargets(targets []string, version *string) error { + if len(targets) == 0 { + return nil + } + for _, t := range targets { + if t == "" { + return fmt.Errorf("targets must not contain empty strings") + } + } + if version == nil || *version == "" { + return fmt.Errorf("targets requires version to be set") + } + return nil } diff --git a/api/pkg/api/model/firmware_test.go b/api/pkg/api/model/firmware_test.go index c5d8b93ae..d094d6487 100644 --- a/api/pkg/api/model/firmware_test.go +++ b/api/pkg/api/model/firmware_test.go @@ -45,6 +45,97 @@ func TestAPIUpdateFirmwareRequest_Validate(t *testing.T) { request: APIUpdateFirmwareRequest{Version: strPtr("24.11.0")}, wantErr: true, }, + { + name: "valid - targets with version", + request: APIUpdateFirmwareRequest{ + SiteID: "site-1", + Version: strPtr("24.11.0"), + Targets: []string{"bmc", "nvos"}, + }, + wantErr: false, + }, + { + name: "invalid - targets without version", + request: APIUpdateFirmwareRequest{ + SiteID: "site-1", + Targets: []string{"bmc"}, + }, + wantErr: true, + }, + { + name: "invalid - targets with empty version string", + request: APIUpdateFirmwareRequest{ + SiteID: "site-1", + Version: strPtr(""), + Targets: []string{"bmc"}, + }, + wantErr: true, + }, + { + name: "invalid - targets contains empty string", + request: APIUpdateFirmwareRequest{ + SiteID: "site-1", + Version: strPtr("24.11.0"), + Targets: []string{"bmc", ""}, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.request.Validate() + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestAPIBatchTrayFirmwareUpdateRequest_Validate(t *testing.T) { + tests := []struct { + name string + request APIBatchTrayFirmwareUpdateRequest + wantErr bool + }{ + { + name: "valid - siteId only", + request: APIBatchTrayFirmwareUpdateRequest{SiteID: "site-1"}, + wantErr: false, + }, + { + name: "valid - with filter and version", + request: APIBatchTrayFirmwareUpdateRequest{ + SiteID: "site-1", + Filter: &TrayFilter{IDs: []string{"550e8400-e29b-41d4-a716-446655440000"}}, + Version: strPtr("24.11.0"), + }, + wantErr: false, + }, + { + name: "valid - targets with version", + request: APIBatchTrayFirmwareUpdateRequest{ + SiteID: "site-1", + Version: strPtr("24.11.0"), + Targets: []string{"bmc"}, + }, + wantErr: false, + }, + { + name: "invalid - targets without version", + request: APIBatchTrayFirmwareUpdateRequest{ + SiteID: "site-1", + Targets: []string{"bmc"}, + }, + wantErr: true, + }, + { + name: "invalid - missing siteId", + request: APIBatchTrayFirmwareUpdateRequest{}, + wantErr: true, + }, } for _, tt := range tests { diff --git a/docs/index.html b/docs/index.html index a2bd4b3da..4892973f5 100644 --- a/docs/index.html +++ b/docs/index.html @@ -7700,8 +7700,46 @@
ID of the Rack
| siteId required | string <uuid> ID of the Site - | ||
| version | string or null version | string or null Target firmware version. + | |
| targets | Array of strings[ items non-empty ] Items Enum: "bmc" "cpld" "bios" "nvos" "pmc" "psu" Optional subset of firmware targets to update within the targeted tray.
+Names are lowercase and select sub-parts of the tray
+(BMC, BIOS, etc.). The accepted set per tray type comes from
+the Flow service's NICo proto bindings (which mirror Core's
+per-tray-type enums in
|
| taskIds | Array of strings <uuid> [ items <uuid > ] Typical API Call Flow for Tenant
" class="sc-iJuXkV sc-cBNeAB iNuSsz dyntKg"> Filter by component ID. Requires 'type'. |
| ids | Array of strings <uuid> [ items <uuid > ] Filter by tray UUID - |
Target firmware version.
+Optional subset of firmware targets to update within each matched tray.
+Names are lowercase and select sub-parts of the tray
+(BMC, BIOS, etc.). The accepted set per tray type comes from
+the Flow service's NICo proto bindings (which mirror Core's
+per-tray-type enums in carbide-core/crates/rpc/proto/forge.proto),
+so the supported values track Core as new sub-parts are added:
version to be set.| taskIds | Array of strings <uuid> [ items <uuid > ] Typical API Call Flow for Tenant
" class="sc-iJuXkV sc-cBNeAB iNuSsz dyntKg"> ID of the Tray |
| siteId required | string <uuid> ID of the Site - | ||
| version | string or null version | string or null Target firmware version. + | |
| targets | Array of strings[ items non-empty ] Items Enum: "bmc" "cpld" "bios" "nvos" "pmc" "psu" Optional subset of firmware targets to update within the targeted tray.
+Names are lowercase and select sub-parts of the tray
+(BMC, BIOS, etc.). The accepted set per tray type comes from
+the Flow service's NICo proto bindings (which mirror Core's
+per-tray-type enums in
|
| taskIds | Array of strings <uuid> [ items <uuid > ] Typical API Call Flow for Tenant
|