chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,408 @@
|
||||
package firmwareconformance
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
DeviceEvidenceSchema = "firmware-device-evidence-v1"
|
||||
ReportSchema = "firmware-conformance-report-v1"
|
||||
)
|
||||
|
||||
var digestPattern = regexp.MustCompile(`^[0-9a-f]{64}$`)
|
||||
|
||||
type CloudRollout struct {
|
||||
ID string `json:"id"`
|
||||
ReleaseID string `json:"release_id"`
|
||||
Status string `json:"status"`
|
||||
Assignments []CloudAssignment `json:"assignments"`
|
||||
}
|
||||
|
||||
type CloudAssignment struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
DeviceModel string `json:"device_model"`
|
||||
PreviousVersion string `json:"previous_version"`
|
||||
Status string `json:"status"`
|
||||
Events []CloudEvent `json:"events"`
|
||||
}
|
||||
|
||||
type CloudEvent struct {
|
||||
ID string `json:"id"`
|
||||
CommandID string `json:"command_id"`
|
||||
CommandStatus string `json:"command_status"`
|
||||
AssignmentStatus string `json:"assignment_status"`
|
||||
Phase string `json:"phase"`
|
||||
ProgressPercent *int `json:"progress_percent"`
|
||||
ResultCode string `json:"result_code"`
|
||||
ActiveSlot string `json:"active_slot"`
|
||||
TargetSlot string `json:"target_slot"`
|
||||
PreviousSlot string `json:"previous_slot"`
|
||||
FailedSlot string `json:"failed_slot"`
|
||||
BootAttemptID string `json:"boot_attempt_id"`
|
||||
RollbackApplied *bool `json:"rollback_applied"`
|
||||
RollbackVersion string `json:"rollback_version"`
|
||||
InstalledVersion string `json:"installed_version"`
|
||||
VerifiedSHA256 string `json:"verified_sha256"`
|
||||
VerifiedSigningKeyID string `json:"verified_signing_key_id"`
|
||||
ResultDigest string `json:"result_digest"`
|
||||
ReportedAt time.Time `json:"reported_at"`
|
||||
}
|
||||
|
||||
type DeviceEvidence struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
EvidenceSource string `json:"evidence_source"`
|
||||
CandidateVersion string `json:"candidate_version"`
|
||||
BuildDigest string `json:"build_digest"`
|
||||
DeviceID string `json:"device_id"`
|
||||
DeviceModel string `json:"device_model"`
|
||||
HardwareRevision string `json:"hardware_revision"`
|
||||
ReleaseID string `json:"release_id"`
|
||||
CommandID string `json:"command_id"`
|
||||
Scenario string `json:"scenario"`
|
||||
BootloaderVersion string `json:"bootloader_version"`
|
||||
PartitionTableSHA256 string `json:"partition_table_sha256"`
|
||||
SecureBootEnabled bool `json:"secure_boot_enabled"`
|
||||
ArtifactSHA256 string `json:"artifact_sha256"`
|
||||
SigningKeyID string `json:"signing_key_id"`
|
||||
InitialSlot string `json:"initial_slot"`
|
||||
TargetSlot string `json:"target_slot"`
|
||||
FinalSlot string `json:"final_slot"`
|
||||
InitialVersion string `json:"initial_version"`
|
||||
TargetVersion string `json:"target_version"`
|
||||
FinalVersion string `json:"final_version"`
|
||||
BootAttemptID string `json:"boot_attempt_id"`
|
||||
CollectedAt time.Time `json:"collected_at"`
|
||||
Events []DeviceEvent `json:"events"`
|
||||
}
|
||||
|
||||
type DeviceEvent struct {
|
||||
Phase string `json:"phase"`
|
||||
OccurredAt time.Time `json:"occurred_at"`
|
||||
ActiveSlot string `json:"active_slot,omitempty"`
|
||||
TargetSlot string `json:"target_slot,omitempty"`
|
||||
ResultCode string `json:"result_code,omitempty"`
|
||||
BootAttemptID string `json:"boot_attempt_id,omitempty"`
|
||||
}
|
||||
|
||||
type Check struct {
|
||||
Code string `json:"code"`
|
||||
Status string `json:"status"`
|
||||
Detail string `json:"detail"`
|
||||
}
|
||||
|
||||
type Report struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
Result string `json:"result"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
CandidateVersion string `json:"candidate_version"`
|
||||
BuildDigest string `json:"build_digest"`
|
||||
EvidenceScope string `json:"evidence_scope"`
|
||||
ExternalValidationRequired bool `json:"external_validation_required"`
|
||||
DeviceID string `json:"device_id"`
|
||||
ReleaseID string `json:"release_id"`
|
||||
RolloutID string `json:"rollout_id"`
|
||||
CommandID string `json:"command_id"`
|
||||
Scenario string `json:"scenario"`
|
||||
CloudEventCount int `json:"cloud_event_count"`
|
||||
DeviceEventCount int `json:"device_event_count"`
|
||||
InputSHA256 map[string]string `json:"input_sha256"`
|
||||
Checks []Check `json:"checks"`
|
||||
Disclaimer string `json:"disclaimer"`
|
||||
}
|
||||
|
||||
func ParseCloud(content []byte) (CloudRollout, error) {
|
||||
var envelope struct {
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(content, &envelope); err != nil {
|
||||
return CloudRollout{}, fmt.Errorf("decode cloud rollout: %w", err)
|
||||
}
|
||||
payload := content
|
||||
if len(envelope.Data) > 0 && string(envelope.Data) != "null" {
|
||||
payload = envelope.Data
|
||||
}
|
||||
var rollout CloudRollout
|
||||
if err := json.Unmarshal(payload, &rollout); err != nil {
|
||||
return CloudRollout{}, fmt.Errorf("decode cloud rollout data: %w", err)
|
||||
}
|
||||
if rollout.ID == "" || rollout.ReleaseID == "" {
|
||||
return CloudRollout{}, errors.New("cloud rollout is missing id or release_id")
|
||||
}
|
||||
return rollout, nil
|
||||
}
|
||||
|
||||
func ParseDeviceEvidence(content []byte) (DeviceEvidence, error) {
|
||||
var evidence DeviceEvidence
|
||||
decoder := json.NewDecoder(bytes.NewReader(content))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&evidence); err != nil {
|
||||
return DeviceEvidence{}, fmt.Errorf("decode device evidence: %w", err)
|
||||
}
|
||||
var trailing any
|
||||
if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
|
||||
return DeviceEvidence{}, errors.New("device evidence contains trailing JSON data")
|
||||
}
|
||||
return evidence, nil
|
||||
}
|
||||
|
||||
// ParseReport reads the exact report format emitted by cmd/firmware-conformance.
|
||||
// Strict decoding prevents an unreviewed producer extension from being treated as
|
||||
// release evidence by downstream packaging tools.
|
||||
func ParseReport(content []byte) (Report, error) {
|
||||
var report Report
|
||||
decoder := json.NewDecoder(bytes.NewReader(content))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&report); err != nil {
|
||||
return Report{}, fmt.Errorf("decode firmware conformance report: %w", err)
|
||||
}
|
||||
var trailing any
|
||||
if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
|
||||
return Report{}, errors.New("firmware conformance report contains trailing JSON data")
|
||||
}
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func Evaluate(cloud CloudRollout, device DeviceEvidence, generatedAt time.Time) Report {
|
||||
report := Report{
|
||||
SchemaVersion: ReportSchema, Result: "PASS", GeneratedAt: generatedAt.UTC(), CandidateVersion: device.CandidateVersion,
|
||||
BuildDigest: device.BuildDigest, EvidenceScope: "PROTOCOL_SIMULATION", ExternalValidationRequired: true,
|
||||
DeviceID: device.DeviceID, ReleaseID: device.ReleaseID, RolloutID: cloud.ID, CommandID: device.CommandID, Scenario: device.Scenario,
|
||||
CloudEventCount: 0, DeviceEventCount: len(device.Events), InputSHA256: map[string]string{}, Checks: []Check{},
|
||||
Disclaimer: "一致性通过只证明所提供设备证据与云端固件时间线相互匹配;不替代真实Bootloader、闪存掉电一致性、硬件安全或生产发布验收。",
|
||||
}
|
||||
if device.EvidenceSource == "HARDWARE" {
|
||||
report.EvidenceScope = "HARDWARE_CAPTURE"
|
||||
}
|
||||
add := func(code string, ok bool, detail string) {
|
||||
status := "PASS"
|
||||
if !ok {
|
||||
status = "FAIL"
|
||||
report.Result = "FAIL"
|
||||
}
|
||||
report.Checks = append(report.Checks, Check{Code: code, Status: status, Detail: detail})
|
||||
}
|
||||
|
||||
add("DEVICE_EVIDENCE_SCHEMA", device.SchemaVersion == DeviceEvidenceSchema, "设备证据必须使用"+DeviceEvidenceSchema)
|
||||
add("EVIDENCE_SOURCE", device.EvidenceSource == "SIMULATOR" || device.EvidenceSource == "HARDWARE", "证据来源必须明确为SIMULATOR或HARDWARE")
|
||||
buildOK := strings.TrimSpace(device.CandidateVersion) != "" && strings.HasPrefix(device.BuildDigest, "sha256:") && digestPattern.MatchString(strings.TrimPrefix(device.BuildDigest, "sha256:"))
|
||||
add("CANDIDATE_IDENTITY", buildOK, "候选版本和sha256构建摘要必须完整")
|
||||
identityOK := validUUID(device.DeviceID) && validUUID(device.ReleaseID) && validUUID(device.CommandID) && device.DeviceModel != "" && device.HardwareRevision != ""
|
||||
add("DEVICE_IDENTITY", identityOK, "设备、发布和命令必须使用公开UUID,并提供型号与硬件版本")
|
||||
platformOK := device.BootloaderVersion != "" && digestPattern.MatchString(device.PartitionTableSHA256)
|
||||
add("DEVICE_PLATFORM_METADATA", platformOK, fmt.Sprintf("Bootloader=%s,安全启动=%t,分区表摘要必须为SHA-256", device.BootloaderVersion, device.SecureBootEnabled))
|
||||
artifactOK := digestPattern.MatchString(device.ArtifactSHA256) && device.SigningKeyID != ""
|
||||
add("ARTIFACT_IDENTITY", artifactOK, "设备证据必须绑定完整制品摘要和签名密钥编号")
|
||||
|
||||
assignment, found := findAssignment(cloud.Assignments, device.DeviceID)
|
||||
add("CLOUD_ASSIGNMENT", found, "云端批次必须包含目标设备且只能匹配一条分配")
|
||||
add("RELEASE_BINDING", cloud.ReleaseID == device.ReleaseID, "设备证据发布编号必须与云端批次一致")
|
||||
if found {
|
||||
report.CloudEventCount = len(assignment.Events)
|
||||
}
|
||||
cloudArtifactBound := false
|
||||
if found {
|
||||
for _, event := range assignment.Events {
|
||||
if event.VerifiedSHA256 == device.ArtifactSHA256 && event.VerifiedSigningKeyID == device.SigningKeyID {
|
||||
cloudArtifactBound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
add("CLOUD_ARTIFACT_BINDING", cloudArtifactBound, "云端阶段时间线必须包含与设备一致的已验证摘要和签名密钥")
|
||||
commandBound := found && len(assignment.Events) > 0
|
||||
if commandBound {
|
||||
for _, event := range assignment.Events {
|
||||
if event.CommandID != device.CommandID {
|
||||
commandBound = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
add("COMMAND_BINDING", commandBound, "全部云端阶段事件必须绑定同一设备命令")
|
||||
cloudTimelineOK := found && validateCloudTimeline(assignment.Events)
|
||||
add("CLOUD_TIMELINE_INTEGRITY", cloudTimelineOK, "事件编号、结果摘要哈希、接收时间和顺序必须有效")
|
||||
deviceTimelineOK := validateDeviceTimeline(device.Events, device.CollectedAt)
|
||||
add("DEVICE_TIMELINE_INTEGRITY", deviceTimelineOK, "设备事件必须按时间升序且不晚于证据采集时间")
|
||||
|
||||
terminalPhase, scenarioOK := scenarioTerminalPhase(device.Scenario)
|
||||
add("SCENARIO_SUPPORTED", scenarioOK, "场景必须为AB_SUCCESS、AB_ROLLBACK、POWER_LOSS_AFTER_STAGE或POWER_LOSS_DURING_BOOT")
|
||||
required := []string{"staging", "reboot_pending", "boot_verifying", terminalPhase}
|
||||
cloudPhasesOK := scenarioOK && found && containsPhaseSequence(cloudPhases(assignment.Events), required)
|
||||
deviceRequired := append([]string{}, required...)
|
||||
if device.Scenario == "POWER_LOSS_AFTER_STAGE" {
|
||||
deviceRequired = []string{"staging", "power_loss_after_stage", "reboot_pending", "boot_verifying", terminalPhase}
|
||||
}
|
||||
if device.Scenario == "POWER_LOSS_DURING_BOOT" {
|
||||
deviceRequired = []string{"staging", "reboot_pending", "boot_verifying", "power_loss_during_boot", "boot_verifying", terminalPhase}
|
||||
}
|
||||
devicePhasesOK := scenarioOK && containsPhaseSequence(devicePhases(device.Events), deviceRequired)
|
||||
add("CLOUD_PHASE_SEQUENCE", cloudPhasesOK, "云端必须按顺序留存写入、待重启、健康确认和终态")
|
||||
add("DEVICE_PHASE_SEQUENCE", devicePhasesOK, "设备侧必须提供与场景一致的本地阶段顺序")
|
||||
|
||||
bootAttemptOK := found && device.BootAttemptID != ""
|
||||
if bootAttemptOK {
|
||||
seen := false
|
||||
for _, event := range assignment.Events {
|
||||
if event.BootAttemptID == "" {
|
||||
continue
|
||||
}
|
||||
seen = true
|
||||
if event.BootAttemptID != device.BootAttemptID {
|
||||
bootAttemptOK = false
|
||||
}
|
||||
}
|
||||
bootAttemptOK = bootAttemptOK && seen
|
||||
}
|
||||
add("BOOT_ATTEMPT_BINDING", bootAttemptOK, "设备和云端必须引用同一启动尝试编号")
|
||||
|
||||
phaseTimeOK, maxSkew := phaseTimeCorrelation(assignment.Events, device.Events)
|
||||
add("PHASE_TIME_CORRELATION", found && phaseTimeOK, fmt.Sprintf("同名设备/云端阶段最大时间差为%s,允许10分钟", maxSkew.Round(time.Second)))
|
||||
|
||||
slotOK := device.InitialSlot != "" && device.TargetSlot != "" && device.InitialSlot != device.TargetSlot
|
||||
versionOK := device.InitialVersion != "" && device.TargetVersion != "" && device.FinalVersion != ""
|
||||
outcomeOK := false
|
||||
if found && scenarioOK && len(assignment.Events) > 0 {
|
||||
finalEvent := assignment.Events[len(assignment.Events)-1]
|
||||
if terminalPhase == "rolled_back" {
|
||||
outcomeOK = assignment.Status == "FAILED" && finalEvent.CommandStatus == "FAILED" && finalEvent.Phase == "rolled_back" && finalEvent.RollbackApplied != nil && *finalEvent.RollbackApplied && finalEvent.RollbackVersion == device.InitialVersion && device.FinalSlot == device.InitialSlot && device.FinalVersion == device.InitialVersion
|
||||
slotOK = slotOK && device.FinalSlot == device.InitialSlot
|
||||
versionOK = versionOK && device.FinalVersion == device.InitialVersion
|
||||
} else {
|
||||
outcomeOK = assignment.Status == "CONFIRMED" && finalEvent.CommandStatus == "SUCCEEDED" && finalEvent.Phase == "confirmed" && finalEvent.InstalledVersion == device.TargetVersion && finalEvent.VerifiedSHA256 == device.ArtifactSHA256 && finalEvent.VerifiedSigningKeyID == device.SigningKeyID && device.FinalSlot == device.TargetSlot && device.FinalVersion == device.TargetVersion
|
||||
slotOK = slotOK && device.FinalSlot == device.TargetSlot
|
||||
versionOK = versionOK && device.FinalVersion == device.TargetVersion
|
||||
}
|
||||
}
|
||||
add("SLOT_TRANSITION", slotOK, "目标分区必须不同于初始分区,成功或回退后的活动分区必须符合场景")
|
||||
add("VERSION_TRANSITION", versionOK, "最终版本必须与成功升级或自动回退结果一致")
|
||||
add("TERMINAL_OUTCOME", outcomeOK, "云端命令、分配终态、设备分区和最终版本必须一致")
|
||||
return report
|
||||
}
|
||||
|
||||
func SHA256(content []byte) string {
|
||||
digest := sha256.Sum256(content)
|
||||
return hex.EncodeToString(digest[:])
|
||||
}
|
||||
|
||||
func validUUID(value string) bool {
|
||||
_, err := uuid.Parse(value)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func findAssignment(items []CloudAssignment, deviceID string) (CloudAssignment, bool) {
|
||||
var result CloudAssignment
|
||||
count := 0
|
||||
for _, item := range items {
|
||||
if item.DeviceID == deviceID {
|
||||
result = item
|
||||
count++
|
||||
}
|
||||
}
|
||||
return result, count == 1
|
||||
}
|
||||
|
||||
func validateCloudTimeline(events []CloudEvent) bool {
|
||||
if len(events) == 0 {
|
||||
return false
|
||||
}
|
||||
for index, event := range events {
|
||||
if !validUUID(event.ID) || !validUUID(event.CommandID) || !digestPattern.MatchString(event.ResultDigest) || event.ReportedAt.IsZero() {
|
||||
return false
|
||||
}
|
||||
if index > 0 && event.ReportedAt.Before(events[index-1].ReportedAt) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validateDeviceTimeline(events []DeviceEvent, collectedAt time.Time) bool {
|
||||
if len(events) == 0 || collectedAt.IsZero() {
|
||||
return false
|
||||
}
|
||||
for index, event := range events {
|
||||
if event.Phase == "" || event.OccurredAt.IsZero() || event.OccurredAt.After(collectedAt) {
|
||||
return false
|
||||
}
|
||||
if index > 0 && event.OccurredAt.Before(events[index-1].OccurredAt) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func scenarioTerminalPhase(scenario string) (string, bool) {
|
||||
switch scenario {
|
||||
case "AB_SUCCESS", "POWER_LOSS_AFTER_STAGE", "POWER_LOSS_DURING_BOOT":
|
||||
return "confirmed", true
|
||||
case "AB_ROLLBACK":
|
||||
return "rolled_back", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func cloudPhases(events []CloudEvent) []string {
|
||||
result := make([]string, 0, len(events))
|
||||
for _, event := range events {
|
||||
result = append(result, event.Phase)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func devicePhases(events []DeviceEvent) []string {
|
||||
result := make([]string, 0, len(events))
|
||||
for _, event := range events {
|
||||
result = append(result, event.Phase)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func containsPhaseSequence(actual, required []string) bool {
|
||||
position := 0
|
||||
for _, value := range actual {
|
||||
if position < len(required) && value == required[position] {
|
||||
position++
|
||||
}
|
||||
}
|
||||
return position == len(required)
|
||||
}
|
||||
|
||||
func phaseTimeCorrelation(cloud []CloudEvent, device []DeviceEvent) (bool, time.Duration) {
|
||||
maximum := time.Duration(0)
|
||||
matched := 0
|
||||
for _, local := range device {
|
||||
closest := time.Duration(1<<63 - 1)
|
||||
for _, remote := range cloud {
|
||||
if remote.Phase != local.Phase {
|
||||
continue
|
||||
}
|
||||
difference := remote.ReportedAt.Sub(local.OccurredAt)
|
||||
if difference < 0 {
|
||||
difference = -difference
|
||||
}
|
||||
if difference < closest {
|
||||
closest = difference
|
||||
}
|
||||
}
|
||||
if closest == time.Duration(1<<63-1) {
|
||||
continue
|
||||
}
|
||||
matched++
|
||||
if closest > maximum {
|
||||
maximum = closest
|
||||
}
|
||||
}
|
||||
return matched >= 4 && maximum <= 10*time.Minute, maximum
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package firmwareconformance
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestEvaluateABRollbackEvidence(t *testing.T) {
|
||||
cloud, device := rollbackFixture()
|
||||
report := Evaluate(cloud, device, time.Date(2026, 8, 25, 2, 0, 0, 0, time.UTC))
|
||||
if report.Result != "PASS" || report.EvidenceScope != "PROTOCOL_SIMULATION" || report.CloudEventCount != 4 || len(report.Checks) < 10 {
|
||||
t.Fatalf("report=%+v", report)
|
||||
}
|
||||
device.FinalVersion = "3.1.0"
|
||||
report = Evaluate(cloud, device, time.Now())
|
||||
if report.Result != "FAIL" {
|
||||
t.Fatalf("mismatched rollback version passed: %+v", report)
|
||||
}
|
||||
found := false
|
||||
for _, check := range report.Checks {
|
||||
if check.Code == "VERSION_TRANSITION" && check.Status == "FAIL" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("version mismatch was not identified")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCloudEnvelopeAndStrictDeviceEvidence(t *testing.T) {
|
||||
cloud, device := rollbackFixture()
|
||||
cloudJSON, _ := json.Marshal(map[string]any{"request_id": "request-1", "data": cloud})
|
||||
parsed, err := ParseCloud(cloudJSON)
|
||||
if err != nil || parsed.ID != cloud.ID {
|
||||
t.Fatalf("parsed cloud=%+v err=%v", parsed, err)
|
||||
}
|
||||
deviceJSON, _ := json.Marshal(device)
|
||||
if _, err := ParseDeviceEvidence(deviceJSON); err != nil {
|
||||
t.Fatalf("valid evidence error=%v", err)
|
||||
}
|
||||
var payload map[string]any
|
||||
_ = json.Unmarshal(deviceJSON, &payload)
|
||||
payload["unexpected_secret"] = "must-not-be-accepted"
|
||||
invalid, _ := json.Marshal(payload)
|
||||
if _, err := ParseDeviceEvidence(invalid); err == nil {
|
||||
t.Fatal("unknown device evidence field was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseReportRejectsUnknownFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
if _, err := ParseReport([]byte(`{"schema_version":"firmware-conformance-report-v1","unknown":true}`)); err == nil {
|
||||
t.Fatal("unknown firmware report field was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func rollbackFixture() (CloudRollout, DeviceEvidence) {
|
||||
base := time.Date(2026, 8, 25, 1, 0, 0, 0, time.UTC)
|
||||
deviceID := "10000000-0000-0000-0000-000000000511"
|
||||
releaseID := "20000000-0000-0000-0000-000000000001"
|
||||
commandID := "30000000-0000-0000-0000-000000000001"
|
||||
bootAttemptID := "boot-attempt-1"
|
||||
phases := []string{"staging", "reboot_pending", "boot_verifying", "rolled_back"}
|
||||
cloudEvents := make([]CloudEvent, 0, len(phases))
|
||||
deviceEvents := make([]DeviceEvent, 0, len(phases))
|
||||
for index, phase := range phases {
|
||||
commandStatus, assignmentStatus := "ACKNOWLEDGED", "INSTALLING"
|
||||
if phase == "rolled_back" {
|
||||
commandStatus, assignmentStatus = "FAILED", "FAILED"
|
||||
}
|
||||
reported := base.Add(time.Duration(index) * time.Minute)
|
||||
event := CloudEvent{
|
||||
ID: "40000000-0000-0000-0000-00000000000" + string(rune('1'+index)), CommandID: commandID,
|
||||
CommandStatus: commandStatus, AssignmentStatus: assignmentStatus, Phase: phase, ResultDigest: strings.Repeat(string(rune('a'+index)), 64),
|
||||
ReportedAt: reported, BootAttemptID: bootAttemptID,
|
||||
}
|
||||
if phase == "staging" {
|
||||
event.ActiveSlot, event.TargetSlot = "A", "B"
|
||||
event.VerifiedSHA256, event.VerifiedSigningKeyID = strings.Repeat("d", 64), "sim-key"
|
||||
}
|
||||
if phase == "rolled_back" {
|
||||
rollbackApplied := true
|
||||
event.RollbackApplied = &rollbackApplied
|
||||
event.RollbackVersion = "0.2.0-sim"
|
||||
event.ActiveSlot, event.FailedSlot = "A", "B"
|
||||
}
|
||||
cloudEvents = append(cloudEvents, event)
|
||||
deviceEvents = append(deviceEvents, DeviceEvent{Phase: phase, OccurredAt: reported.Add(-2 * time.Second), ActiveSlot: event.ActiveSlot, TargetSlot: event.TargetSlot, BootAttemptID: bootAttemptID})
|
||||
}
|
||||
cloud := CloudRollout{
|
||||
ID: "50000000-0000-0000-0000-000000000001", ReleaseID: releaseID, Status: "PAUSED",
|
||||
Assignments: []CloudAssignment{{DeviceID: deviceID, DeviceModel: "simulated-terminal", PreviousVersion: "0.2.0-sim", Status: "FAILED", Events: cloudEvents}},
|
||||
}
|
||||
device := DeviceEvidence{
|
||||
SchemaVersion: DeviceEvidenceSchema, EvidenceSource: "SIMULATOR", CandidateVersion: "candidate-1", BuildDigest: "sha256:" + strings.Repeat("b", 64),
|
||||
DeviceID: deviceID, DeviceModel: "simulated-terminal", HardwareRevision: "sim-rev-a", ReleaseID: releaseID, CommandID: commandID, Scenario: "AB_ROLLBACK",
|
||||
BootloaderVersion: "sim-boot-1", PartitionTableSHA256: strings.Repeat("c", 64), SecureBootEnabled: false,
|
||||
ArtifactSHA256: strings.Repeat("d", 64), SigningKeyID: "sim-key", InitialSlot: "A", TargetSlot: "B", FinalSlot: "A",
|
||||
InitialVersion: "0.2.0-sim", TargetVersion: "3.1.0", FinalVersion: "0.2.0-sim", BootAttemptID: bootAttemptID,
|
||||
CollectedAt: base.Add(10 * time.Minute), Events: deviceEvents,
|
||||
}
|
||||
return cloud, device
|
||||
}
|
||||
Reference in New Issue
Block a user