Files
story/cmd/sim-device/firmware_ab.go
T

299 lines
12 KiB
Go
Raw Normal View History

2026-08-26 06:41:07 +08:00
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
const (
firmwareBootStaged = "STAGED"
firmwareBootRebootPending = "REBOOT_PENDING"
firmwareBootConfirmed = "CONFIRMED"
firmwareBootRolledBack = "ROLLED_BACK"
)
type simulatedFirmwareBootState struct {
Phase string `json:"phase"`
CommandID string `json:"command_id"`
ReleaseID string `json:"release_id"`
TargetVersion string `json:"target_version"`
PreviousVersion string `json:"previous_version"`
Digest string `json:"sha256"`
ActiveSlot string `json:"active_slot"`
PreviousSlot string `json:"previous_slot"`
TargetSlot string `json:"target_slot"`
FailedSlot string `json:"failed_slot,omitempty"`
BootAttemptID string `json:"boot_attempt_id,omitempty"`
TerminalResultCode string `json:"terminal_result_code,omitempty"`
HealthDeadline time.Time `json:"health_deadline,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
}
func simulatedABFirmwareEnabled() (bool, error) {
value := strings.ToLower(strings.TrimSpace(os.Getenv("SIM_FIRMWARE_AB_UPDATE")))
if value == "" || value == "false" {
return false, nil
}
if value != "true" {
return false, errors.New("SIM_FIRMWARE_AB_UPDATE must be true or false")
}
statePath := strings.TrimSpace(os.Getenv("SIM_FIRMWARE_STATE_FILE"))
if statePath == "" || !filepath.IsAbs(statePath) {
return false, errors.New("SIM_FIRMWARE_STATE_FILE must be an absolute path when A/B firmware simulation is enabled")
}
evidencePath := strings.TrimSpace(os.Getenv("SIM_FIRMWARE_EVIDENCE_FILE"))
if evidencePath != "" && !filepath.IsAbs(evidencePath) {
return false, errors.New("SIM_FIRMWARE_EVIDENCE_FILE must be an absolute path")
}
return true, nil
}
func simulatedFirmwareHealthWindow() (time.Duration, error) {
value := strings.TrimSpace(os.Getenv("SIM_FIRMWARE_BOOT_HEALTH_WINDOW"))
if value == "" {
return 2 * time.Minute, nil
}
duration, err := time.ParseDuration(value)
if err != nil || duration < time.Second || duration > 30*time.Minute {
return 0, errors.New("SIM_FIRMWARE_BOOT_HEALTH_WINDOW must be between 1s and 30m")
}
return duration, nil
}
func simulatedFirmwareInitialSlot() (string, error) {
value := strings.ToUpper(env("SIM_FIRMWARE_ACTIVE_SLOT", "A"))
if value != "A" && value != "B" {
return "", errors.New("SIM_FIRMWARE_ACTIVE_SLOT must be A or B")
}
return value, nil
}
func inactiveFirmwareSlot(active string) string {
if active == "A" {
return "B"
}
return "A"
}
func loadSimulatedFirmwareBootState() (*simulatedFirmwareBootState, error) {
path := strings.TrimSpace(os.Getenv("SIM_FIRMWARE_STATE_FILE"))
content, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("read simulated firmware state: %w", err)
}
var state simulatedFirmwareBootState
decoder := json.NewDecoder(strings.NewReader(string(content)))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&state); err != nil {
return nil, fmt.Errorf("decode simulated firmware state: %w", err)
}
if state.ActiveSlot != "A" && state.ActiveSlot != "B" {
return nil, errors.New("simulated firmware state has an invalid active slot")
}
switch state.Phase {
case firmwareBootStaged, firmwareBootRebootPending, firmwareBootConfirmed, firmwareBootRolledBack:
default:
return nil, errors.New("simulated firmware state has an invalid phase")
}
return &state, nil
}
func saveSimulatedFirmwareBootState(state simulatedFirmwareBootState) error {
path := strings.TrimSpace(os.Getenv("SIM_FIRMWARE_STATE_FILE"))
if !filepath.IsAbs(path) {
return errors.New("SIM_FIRMWARE_STATE_FILE must be an absolute path")
}
state.UpdatedAt = time.Now().UTC()
content, err := json.MarshalIndent(state, "", " ")
if err != nil {
return err
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
if err != nil {
return fmt.Errorf("open simulated firmware state: %w", err)
}
if _, err = file.Write(content); err == nil {
err = file.Sync()
}
closeErr := file.Close()
if err != nil {
return fmt.Errorf("write simulated firmware state: %w", err)
}
if closeErr != nil {
return fmt.Errorf("close simulated firmware state: %w", closeErr)
}
return nil
}
func (s *simulator) resumeABFirmwareCommand(ctx context.Context, command simulatedDeviceCommand, manifest simulatedFirmwareManifest, fault string, delay time.Duration) (bool, firmwareExecutionOutcome, error) {
state, err := loadSimulatedFirmwareBootState()
if err != nil || state == nil {
return false, firmwareFailed, err
}
if state.Phase == firmwareBootConfirmed || state.Phase == firmwareBootRolledBack {
if state.CommandID != command.ID {
return false, firmwareCompleted, nil
}
if state.Phase == firmwareBootConfirmed {
err := s.ackDeviceCommand(ctx, command.ID, "SUCCEEDED", map[string]any{
"phase": "confirmed", "progress_percent": 100, "result_code": "SIMULATED_AB_BOOT_CONFIRMED", "installed_version": manifest.Version,
"verified_sha256": manifest.Digest, "verified_signing_key_id": manifest.SigningKeyID, "active_slot": state.ActiveSlot, "previous_slot": state.PreviousSlot, "boot_attempt_id": state.BootAttemptID,
})
return true, firmwareCompleted, err
}
resultCode := state.TerminalResultCode
if resultCode == "" {
resultCode = "BOOT_HEALTH_FAILED_ROLLED_BACK"
}
err := s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{
"phase": "rolled_back", "progress_percent": 95, "result_code": resultCode, "rollback_applied": true,
"active_slot": state.ActiveSlot, "failed_slot": state.FailedSlot, "rollback_version": state.TargetVersion, "boot_attempt_id": state.BootAttemptID,
})
return true, firmwareFailed, err
}
if state.CommandID != command.ID || state.ReleaseID != manifest.ReleaseID || state.TargetVersion != manifest.Version || state.Digest != manifest.Digest {
return true, firmwareFailed, s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{"phase": "rolled_back", "progress_percent": 0, "result_code": "LOCAL_UPDATE_STATE_CONFLICT", "rollback_applied": false})
}
if state.Phase == firmwareBootStaged {
outcome, err := s.armSimulatedFirmwareBoot(ctx, command.ID, *state)
return true, outcome, err
}
bootState := *state
bootState.ActiveSlot = state.TargetSlot
if err := recordSimulatedFirmwareEvidence(command, manifest, "boot_verifying", bootState, "BOOT_HEALTH_CHECK_STARTED"); err != nil {
return true, firmwareFailed, err
}
if err := s.ackDeviceCommand(ctx, command.ID, "ACKNOWLEDGED", map[string]any{
"phase": "boot_verifying", "progress_percent": 95, "result_code": "BOOT_HEALTH_CHECK_STARTED",
"active_slot": state.TargetSlot, "previous_slot": state.PreviousSlot, "boot_attempt_id": state.BootAttemptID, "boot_health_deadline": state.HealthDeadline,
}); err != nil {
return true, firmwareFailed, err
}
if fault == "power_loss_during_boot" {
if err := recordSimulatedFirmwareEvidence(command, manifest, "power_loss_during_boot", *state, "SIMULATED_POWER_LOSS"); err != nil {
return true, firmwareFailed, err
}
return true, firmwareInterrupted, nil
}
rollbackCode := ""
if fault == "boot_health_failed" {
rollbackCode = "BOOT_HEALTH_FAILED_ROLLED_BACK"
} else if !state.HealthDeadline.IsZero() && time.Now().After(state.HealthDeadline) {
rollbackCode = "BOOT_HEALTH_TIMEOUT_ROLLED_BACK"
}
if rollbackCode != "" {
failedSlot := state.TargetSlot
state.Phase = firmwareBootRolledBack
state.ActiveSlot = state.PreviousSlot
state.FailedSlot = failedSlot
state.TerminalResultCode = rollbackCode
state.TargetSlot = ""
state.TargetVersion = state.PreviousVersion
if err := saveSimulatedFirmwareBootState(*state); err != nil {
return true, firmwareFailed, err
}
if err := recordSimulatedFirmwareEvidence(command, manifest, "rolled_back", *state, rollbackCode); err != nil {
return true, firmwareFailed, err
}
if err := s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{
"phase": "rolled_back", "progress_percent": 95, "result_code": rollbackCode, "rollback_applied": true,
"active_slot": state.ActiveSlot, "failed_slot": failedSlot, "rollback_version": state.PreviousVersion, "boot_attempt_id": state.BootAttemptID,
}); err != nil {
return true, firmwareFailed, err
}
return true, firmwareFailed, nil
}
if err := waitForContext(ctx, delay); err != nil {
return true, firmwareInterrupted, err
}
confirmedSlot := state.TargetSlot
state.Phase = firmwareBootConfirmed
state.ActiveSlot = confirmedSlot
state.TerminalResultCode = "SIMULATED_AB_BOOT_CONFIRMED"
state.TargetSlot = ""
state.PreviousVersion = state.TargetVersion
if err := saveSimulatedFirmwareBootState(*state); err != nil {
return true, firmwareFailed, err
}
if err := recordSimulatedFirmwareEvidence(command, manifest, "confirmed", *state, "SIMULATED_AB_BOOT_CONFIRMED"); err != nil {
return true, firmwareFailed, err
}
if err := s.ackDeviceCommand(ctx, command.ID, "SUCCEEDED", map[string]any{
"phase": "confirmed", "progress_percent": 100, "result_code": "SIMULATED_AB_BOOT_CONFIRMED", "installed_version": manifest.Version,
"verified_sha256": manifest.Digest, "verified_signing_key_id": manifest.SigningKeyID, "active_slot": confirmedSlot, "previous_slot": state.PreviousSlot, "boot_attempt_id": state.BootAttemptID,
}); err != nil {
return true, firmwareFailed, err
}
return true, firmwareCompleted, nil
}
func (s *simulator) stageABFirmwareCommand(ctx context.Context, command simulatedDeviceCommand, manifest simulatedFirmwareManifest, fault string) (firmwareExecutionOutcome, error) {
current, err := loadSimulatedFirmwareBootState()
if err != nil {
return firmwareFailed, err
}
activeSlot, err := simulatedFirmwareInitialSlot()
if err != nil {
return firmwareFailed, err
}
previousVersion := env("SIM_FIRMWARE_CURRENT_VERSION", "0.2.0-sim")
if current != nil && (current.Phase == firmwareBootConfirmed || current.Phase == firmwareBootRolledBack) {
activeSlot = current.ActiveSlot
previousVersion = current.TargetVersion
}
state := simulatedFirmwareBootState{
Phase: firmwareBootStaged, CommandID: command.ID, ReleaseID: manifest.ReleaseID, TargetVersion: manifest.Version,
PreviousVersion: previousVersion, Digest: manifest.Digest, ActiveSlot: activeSlot, PreviousSlot: activeSlot, TargetSlot: inactiveFirmwareSlot(activeSlot),
}
if err := saveSimulatedFirmwareBootState(state); err != nil {
return firmwareFailed, err
}
if err := recordSimulatedFirmwareEvidence(command, manifest, "staging", state, "INACTIVE_SLOT_STAGED"); err != nil {
return firmwareFailed, err
}
if err := s.ackDeviceCommand(ctx, command.ID, "ACKNOWLEDGED", map[string]any{
"phase": "staging", "progress_percent": 85, "result_code": "INACTIVE_SLOT_STAGED", "active_slot": state.ActiveSlot, "target_slot": state.TargetSlot,
}); err != nil {
return firmwareFailed, err
}
if fault == "power_loss_after_stage" {
if err := recordSimulatedFirmwareEvidence(command, manifest, "power_loss_after_stage", state, "SIMULATED_POWER_LOSS"); err != nil {
return firmwareFailed, err
}
return firmwareInterrupted, nil
}
return s.armSimulatedFirmwareBoot(ctx, command.ID, state)
}
func (s *simulator) armSimulatedFirmwareBoot(ctx context.Context, commandID string, state simulatedFirmwareBootState) (firmwareExecutionOutcome, error) {
window, err := simulatedFirmwareHealthWindow()
if err != nil {
return firmwareFailed, err
}
state.Phase = firmwareBootRebootPending
state.BootAttemptID = nonce()
state.HealthDeadline = time.Now().UTC().Add(window)
if err := saveSimulatedFirmwareBootState(state); err != nil {
return firmwareFailed, err
}
if err := recordSimulatedFirmwareEvidence(simulatedDeviceCommand{ID: commandID}, simulatedFirmwareManifest{ReleaseID: state.ReleaseID, Version: state.TargetVersion, Digest: state.Digest}, "reboot_pending", state, "REBOOT_ARMED"); err != nil {
return firmwareFailed, err
}
if err := s.ackDeviceCommand(ctx, commandID, "ACKNOWLEDGED", map[string]any{
"phase": "reboot_pending", "progress_percent": 90, "result_code": "REBOOT_ARMED", "active_slot": state.ActiveSlot,
"target_slot": state.TargetSlot, "boot_attempt_id": state.BootAttemptID, "boot_health_deadline": state.HealthDeadline,
}); err != nil {
return firmwareFailed, err
}
return firmwareRebootRequired, nil
}