chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user