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
1 change: 1 addition & 0 deletions .typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ extend-ignore-re = [
[default.extend-words]
ser = "ser"
otu = "otu"
ISSU = "ISSU"
# Typo in name used by Cisco NX-OS for a configurable property.
# See: https://pubhub.devnetcloud.com/media/dme-docs-10-4-3/docs/System/snmp%3ACommSecP/#configurable-properties
acess = "acess"
Expand Down
12 changes: 12 additions & 0 deletions api/core/v1alpha1/groupversion_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ const VRFLabel = "networking.metal.ironcore.dev/vrf-name"
// to trigger certain disruptive operations, such as reboots or firmware upgrades.
const DeviceMaintenanceAnnotation = "networking.metal.ironcore.dev/maintenance"

// DeviceMaintenanceFirmwareTargetAnnotation specifies the target firmware image for a firmware upgrade.
// It also includes the MD5 checksum of the firmware image if available.
// The value format is {"url": "<url>", "md5": "<md5>"}
const DeviceMaintenanceFirmwareTargetAnnotation = "networking.metal.ironcore.dev/maintenance-target-firmware"

// PhysicalInterfaceNeighborLabel identifies the peer Interface resource on the other end of a physical link.
// The value must be the name of another Interface resource in the same namespace.
// This label is only valid for interfaces of type Physical.
Expand Down Expand Up @@ -113,6 +118,10 @@ const (
// spec.provisioning is defined.
// The annotation is always consumed once the device reaches Running.
DeviceMaintenanceSkipProvisioning = "skip-provisioning"
// DeviceMaintenanceFirmwareUpgrade triggers a firmware upgrade on the device. The provider initiates
// the upgrade workflow, which will apply the new firmware and reboot the device as necessary.
// The target firmware image is specified by the DeviceMaintenanceFirmwareTargetAnnotation.
DeviceMaintenanceFirmwareUpgrade = "firmware-upgrade"
)

// Condition types that are used across different objects.
Expand Down Expand Up @@ -252,6 +261,9 @@ const (
const (
// MaintenanceFailedReason indicates that a requested maintenance operation (e.g., reboot or factory reset) failed.
MaintenanceFailedReason = "MaintenanceFailed"
// MaintenanceInProgressReason indicates that a long-running maintenance
// operation (e.g., firmware upgrade) is still in progress and will be retried.
MaintenanceInProgressReason = "MaintenanceInProgress"
)

// Reasons that are specific to [RoutingPolicy] objects.
Expand Down
91 changes: 90 additions & 1 deletion internal/controller/core/device_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ package core
import (
"cmp"
"context"
"encoding/json"
"errors"
"fmt"
"math/rand/v2"
"net/url"
"regexp"
"slices"
"strings"
Expand Down Expand Up @@ -246,6 +248,12 @@ func (r *DeviceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ c
}

if err := r.reconcileMaintenance(ctx, obj, conn); err != nil {
// ErrUpgradeInProgress signals that a long-running maintenance step was
// issued and the operation must be resumed on a subsequent reconcile, so
// it must requeue rather than terminate.
if errors.Is(err, provider.ErrUpgradeInProgress) {
return ctrl.Result{}, err
}
return ctrl.Result{}, reconcile.TerminalError(err)
}

Expand Down Expand Up @@ -468,7 +476,8 @@ func (r *DeviceReconciler) reconcileMaintenance(ctx context.Context, obj *v1alph

case v1alpha1.DeviceMaintenanceReboot,
v1alpha1.DeviceMaintenanceFactoryReset,
v1alpha1.DeviceMaintenanceReprovision:
v1alpha1.DeviceMaintenanceReprovision,
v1alpha1.DeviceMaintenanceFirmwareUpgrade:

prov := r.Provider()
if err := prov.Connect(ctx, conn); err != nil {
Expand Down Expand Up @@ -538,6 +547,39 @@ func (r *DeviceReconciler) reconcileMaintenance(ctx context.Context, obj *v1alph
return fmt.Errorf("failed to prepare device for reprovisioning: %w", err)
}
obj.Status.Phase = v1alpha1.DevicePhasePending

case v1alpha1.DeviceMaintenanceFirmwareUpgrade:
mp, ok := prov.(provider.MaintenanceProvider)
if !ok {
r.Recorder.Eventf(obj, nil, "Warning", "MaintenanceUnsupported", "Maintenance", "Provider does not support firmware upgrade operation: %s", action)
return nil
}
targetFirmware, err := r.getTargetFirmware(obj)
if err != nil {
return err
}

err = mp.UpgradeFirmware(ctx, conn, targetFirmware)
if errors.Is(err, provider.ErrUpgradeInProgress) {
conditions.Set(obj, metav1.Condition{
Type: v1alpha1.ReadyCondition,
Status: metav1.ConditionFalse,
Reason: v1alpha1.MaintenanceInProgressReason,
Message: "Firmware upgrade in progress",
})
r.Recorder.Eventf(obj, nil, "Normal", "FirmwareUpgradeInProgress", "Maintenance", "Device firmware upgrade is in progress")
return err
}
if err != nil {
conditions.Set(obj, metav1.Condition{
Type: v1alpha1.ReadyCondition,
Status: metav1.ConditionFalse,
Reason: v1alpha1.MaintenanceFailedReason,
Message: fmt.Sprintf("Failed to upgrade firmware: %v", err),
})
r.Recorder.Eventf(obj, nil, "Warning", "FirmwareUpgradeFailed", "Maintenance", "Device firmware upgrade has failed: %v", err)
return fmt.Errorf("failed to upgrade firmware: %w", err)
}
}

default:
Expand All @@ -551,6 +593,53 @@ func (r *DeviceReconciler) reconcileMaintenance(ctx context.Context, obj *v1alph
return nil
}

// getTargetFirmware retrieves the target firmware information from the device's annotations
func (r *DeviceReconciler) getTargetFirmware(obj *v1alpha1.Device) (provider.TargetFirmware, error) {
targetFirmwareJSON, ok := obj.Annotations[v1alpha1.DeviceMaintenanceFirmwareTargetAnnotation]
if !ok {
r.Recorder.Eventf(obj, nil, "Warning", "MaintenanceMissingFirmwareTarget", "Maintenance", "Firmware upgrade requested but no target firmware specified")
return provider.TargetFirmware{}, errors.New("firmware upgrade requested but no target firmware specified")
}

var targetFirmware provider.TargetFirmware
if err := json.Unmarshal([]byte(targetFirmwareJSON), &targetFirmware); err != nil {
r.Recorder.Eventf(obj, nil, "Warning", "MaintenanceInvalidFirmwareTarget", "Maintenance", "Invalid firmware target specified: %v", err)
return provider.TargetFirmware{}, fmt.Errorf("failed to parse firmware target: %w", err)
}

// Validate the URL to ensure it is a well-formed absolute HTTP(S) URL and reject
// characters commonly used for shell/control injection if this value is later passed on.
parsedURL, err := url.ParseRequestURI(targetFirmware.URL)
if err != nil {
r.Recorder.Eventf(obj, nil, "Warning", "MaintenanceInvalidFirmwareTarget", "Maintenance", "Invalid firmware target specified: url is invalid: %v", err)
return provider.TargetFirmware{}, fmt.Errorf("invalid firmware target: url is invalid: %w", err)
}
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
r.Recorder.Eventf(obj, nil, "Warning", "MaintenanceInvalidFirmwareTarget", "Maintenance", "Invalid firmware target specified: url scheme must be http or https")
return provider.TargetFirmware{}, errors.New("invalid firmware target: url scheme must be http or https")
}
if parsedURL.Host == "" {
r.Recorder.Eventf(obj, nil, "Warning", "MaintenanceInvalidFirmwareTarget", "Maintenance", "Invalid firmware target specified: url host is required")
return provider.TargetFirmware{}, errors.New("invalid firmware target: url host is required")
}
if parsedURL.User != nil {
r.Recorder.Eventf(obj, nil, "Warning", "MaintenanceInvalidFirmwareTarget", "Maintenance", "Invalid firmware target specified: url must not contain user info")
return provider.TargetFirmware{}, errors.New("invalid firmware target: url must not contain user info")
}
if strings.ContainsAny(targetFirmware.URL, "\r\n\t`$\\<>|;&()") {
r.Recorder.Eventf(obj, nil, "Warning", "MaintenanceInvalidFirmwareTarget", "Maintenance", "Invalid firmware target specified: url contains forbidden characters")
return provider.TargetFirmware{}, errors.New("invalid firmware target: url contains forbidden characters")
}

// Validate the MD5 checksum if provided. It must be alphanumeric.
if targetFirmware.MD5 != "" && !regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString(targetFirmware.MD5) {
r.Recorder.Eventf(obj, nil, "Warning", "MaintenanceInvalidFirmwareTarget", "Maintenance", "Invalid firmware target specified: md5 must be alphanumeric")
return provider.TargetFirmware{}, errors.New("invalid firmware target: md5 must be alphanumeric")
}

return targetFirmware, nil
}

// secretToDevices is a [handler.MapFunc] to be used to enqueue requests for reconciliation
// for a Device to update when one of its referenced Secrets gets updated.
func (r *DeviceReconciler) secretToDevices(ctx context.Context, obj client.Object) []ctrl.Request {
Expand Down
15 changes: 15 additions & 0 deletions internal/controller/core/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ type Provider struct {
sync.Mutex

ConnectError error // if non-nil, Connect returns this error
UpgradeError error // if non-nil, UpgradeFirmware returns this error
LastRebootTime time.Time

Ports sets.Set[string]
Expand Down Expand Up @@ -568,6 +569,20 @@ func (p *Provider) FactoryReset(ctx context.Context, conn *deviceutil.Connection
return nil
}

func (p *Provider) UpgradeFirmware(ctx context.Context, conn *deviceutil.Connection, target provider.TargetFirmware) error {
p.Lock()
defer p.Unlock()
return p.UpgradeError
}

// SetUpgradeError sets the error that UpgradeFirmware returns on subsequent
// calls. Pass nil to clear it.
func (p *Provider) SetUpgradeError(err error) {
p.Lock()
defer p.Unlock()
p.UpgradeError = err
}

func (p *Provider) Reprovision(ctx context.Context, conn *deviceutil.Connection) (reterr error) {
return nil
}
Expand Down
6 changes: 6 additions & 0 deletions internal/paused/paused.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ func computeCondition(device *v1alpha1.Device, obj Object) metav1.Condition {
condition.Message = "Device is not reachable: " + cond.Message
return condition
}
if device.GetAnnotations()[v1alpha1.DeviceMaintenanceAnnotation] == v1alpha1.DeviceMaintenanceFirmwareUpgrade {
condition.Status = metav1.ConditionTrue
condition.Reason = v1alpha1.PausedReason
condition.Message = "Device is in maintenance"
return condition
}
}
}

Expand Down
Loading
Loading