package main import ( "context" "encoding/json" "flag" "fmt" "os" "os/exec" "path/filepath" "strings" "time" "metis/pkg/service" "metis/pkg/writer" ) type flashPartitionTable struct { PartitionTable struct { Partitions []flashTablePartition `json:"partitions"` } `json:"partitiontable"` } type flashTablePartition struct { Type string `json:"type"` } type flashBlockDevicePayload struct { Blockdevices []flashBlockDevice `json:"blockdevices"` } type flashBlockDevice struct { Path string `json:"path"` Type string `json:"type"` FSType string `json:"fstype"` Label string `json:"label"` Children []flashBlockDevice `json:"children"` } var requiredBootFiles = []string{"config.txt", "cmdline.txt", "boot.scr"} func remoteFlashCmd(args []string) { fs := flag.NewFlagSet("remote-flash", flag.ExitOnError) node := fs.String("node", "", "target node") device := fs.String("device", "", "target device path or test sink") artifactRef := fs.String("artifact-ref", "", "harbor artifact ref without tag") workDir := fs.String("work-dir", filepath.Join(os.TempDir(), "metis-flash"), "working directory") harborRegistry := fs.String("harbor-registry", getenvOr("METIS_HARBOR_REGISTRY", "registry.bstein.dev"), "harbor registry host") harborUsername := fs.String("harbor-username", getenvOr("METIS_HARBOR_USERNAME", ""), "harbor username") harborPassword := fs.String("harbor-password", getenvOr("METIS_HARBOR_PASSWORD", ""), "harbor password") hostTmpDir := fs.String("host-tmp-dir", "/host-tmp", "mounted host tmp dir for test writes") fs.Parse(args) if *node == "" || *device == "" || *artifactRef == "" { fatalf("--node, --device, and --artifact-ref are required") } if err := os.MkdirAll(*workDir, 0o755); err != nil { fatalf("mkdir workdir: %v", err) } emitStageProgress("flash_pull", 84, fmt.Sprintf("Pulling the latest Harbor artifact for %s", *node)) if err := orasLogin(*harborRegistry, *harborUsername, *harborPassword); err != nil { fatalf("oras login: %v", err) } if err := orasPull(fmt.Sprintf("%s:latest", *artifactRef), *workDir); err != nil { fatalf("oras pull: %v", err) } emitStageProgress("flash_prepare", 88, fmt.Sprintf("Preparing the downloaded image for %s", *node)) imagePath, compressed, err := resolvePulledArtifact(*workDir) if err != nil { fatalf("resolve artifact: %v", err) } rawImage := imagePath if compressed { emitStageProgress("flash_unpack", 90, fmt.Sprintf("Decompressing the image for %s before writing", *node)) rawImage = filepath.Join(*workDir, fmt.Sprintf("%s.img", *node)) cmd := exec.Command("sh", "-lc", fmt.Sprintf("xz -dc '%s' > '%s'", imagePath, rawImage)) if out, err := cmd.CombinedOutput(); err != nil { fatalf("xz stream decompress: %v: %s", err, strings.TrimSpace(string(out))) } } destPath := *device if strings.HasPrefix(destPath, "hosttmp://") { if err := os.MkdirAll(*hostTmpDir, 0o755); err != nil { fatalf("mkdir host tmp dir: %v", err) } destPath = filepath.Join(*hostTmpDir, fmt.Sprintf("%s.img", *node)) } info, err := os.Stat(rawImage) if err != nil { fatalf("stat raw image: %v", err) } imageSize := info.Size() emitStageProgress("flash_write", 92, fmt.Sprintf("Writing the latest image for %s to %s", *node, destPath)) writeEmitter := newProgressEmitter("flash_write", 92, 98, fmt.Sprintf("Writing the latest image for %s", *node), true) if err := writer.WriteImageWithProgress(context.Background(), rawImage, destPath, writeEmitter); err != nil { fatalf("write image: %v", err) } flushFlashTarget(destPath, *node) result, err := verifyFlashDestination(destPath) if err != nil { fatalf("verify flash output: %v", err) } result.Node = *node result.Device = *device result.DestPath = destPath result.SizeBytes = imageSize writeStructuredResult(result) } func flushFlashTarget(destPath, node string) { emitStageProgress("flash_flush", 98.5, fmt.Sprintf("Flushing the finished image for %s", node)) _ = exec.Command("sync").Run() if !strings.HasPrefix(destPath, "/dev/") { return } _ = exec.Command("blockdev", "--flushbufs", destPath).Run() _ = exec.Command("blockdev", "--rereadpt", destPath).Run() _ = exec.Command("partprobe", destPath).Run() _ = exec.Command("udevadm", "settle", "--timeout=10").Run() } func verifyFlashDestination(destPath string) (service.RemoteFlashResult, error) { emitStageProgress("flash_verify", 99.2, fmt.Sprintf("Verifying the flashed recovery media at %s", destPath)) if strings.HasPrefix(destPath, "/dev/") { return verifyBlockDeviceFlash(destPath) } return verifyImageFileFlash(destPath) } func verifyImageFileFlash(destPath string) (service.RemoteFlashResult, error) { table, err := readFlashPartitionTable(destPath) if err != nil { return service.RemoteFlashResult{}, err } hasBoot := false hasRoot := false for _, part := range table.PartitionTable.Partitions { if isBootPartitionType(part.Type) { hasBoot = true } if isLinuxPartitionType(part.Type) { hasRoot = true } } if !hasBoot || !hasRoot { return service.RemoteFlashResult{}, fmt.Errorf("image %s does not expose the expected boot and writable partitions", destPath) } return service.RemoteFlashResult{ Verified: true, VerificationKind: "image-file", VerificationSummary: fmt.Sprintf("Verified image layout at %s; boot and writable partitions are present.", destPath), }, nil } func verifyBlockDeviceFlash(destPath string) (service.RemoteFlashResult, error) { deadline := time.Now().Add(15 * time.Second) var lastErr error for time.Now().Before(deadline) { parts, err := readBlockDevicePartitions(destPath) if err == nil { boot, root, classifyErr := classifyFlashPartitions(parts) if classifyErr == nil { checkedFiles, verifyErr := verifyBootPartitionFiles(boot.Path) if verifyErr == nil { bootLabel := firstNonEmpty(boot.Label, filepath.Base(boot.Path)) rootLabel := firstNonEmpty(root.Label, filepath.Base(root.Path)) return service.RemoteFlashResult{ Verified: true, VerificationKind: "block-device", VerificationSummary: fmt.Sprintf("Verified %s; %s and %s are present and the boot files look correct.", destPath, bootLabel, rootLabel), BootPartition: boot.Path, RootPartition: root.Path, BootLabel: boot.Label, RootLabel: root.Label, BootFSType: boot.FSType, RootFSType: root.FSType, CheckedFiles: checkedFiles, }, nil } lastErr = verifyErr } else { lastErr = classifyErr } } else { lastErr = err } _ = exec.Command("blockdev", "--rereadpt", destPath).Run() _ = exec.Command("partprobe", destPath).Run() _ = exec.Command("udevadm", "settle", "--timeout=10").Run() time.Sleep(time.Second) } if lastErr == nil { lastErr = fmt.Errorf("timed out waiting for the flashed partitions on %s", destPath) } return service.RemoteFlashResult{}, lastErr } func readFlashPartitionTable(destPath string) (flashPartitionTable, error) { out, err := exec.Command("sfdisk", "-J", destPath).CombinedOutput() if err != nil { return flashPartitionTable{}, fmt.Errorf("sfdisk -J %s: %v: %s", destPath, err, strings.TrimSpace(string(out))) } var table flashPartitionTable if err := json.Unmarshal(out, &table); err != nil { return flashPartitionTable{}, fmt.Errorf("decode partition table for %s: %w", destPath, err) } return table, nil } func readBlockDevicePartitions(destPath string) ([]flashBlockDevice, error) { out, err := exec.Command("lsblk", "-J", "-o", "PATH,TYPE,FSTYPE,LABEL", destPath).CombinedOutput() if err != nil { return nil, fmt.Errorf("lsblk %s: %v: %s", destPath, err, strings.TrimSpace(string(out))) } var payload flashBlockDevicePayload if err := json.Unmarshal(out, &payload); err != nil { return nil, fmt.Errorf("decode lsblk output for %s: %w", destPath, err) } for _, device := range payload.Blockdevices { if device.Path == destPath { return device.Children, nil } } if len(payload.Blockdevices) == 1 { return payload.Blockdevices[0].Children, nil } return nil, fmt.Errorf("lsblk did not report partitions for %s", destPath) } func classifyFlashPartitions(parts []flashBlockDevice) (flashBlockDevice, flashBlockDevice, error) { var boot flashBlockDevice var root flashBlockDevice for _, part := range parts { if part.Type != "part" { continue } normalizedLabel := strings.ToLower(strings.TrimSpace(part.Label)) normalizedFS := strings.ToLower(strings.TrimSpace(part.FSType)) if boot.Path == "" && (normalizedLabel == "system-boot" || normalizedFS == "vfat" || normalizedFS == "fat" || normalizedFS == "fat32") { boot = part continue } if root.Path == "" && (normalizedLabel == "writable" || normalizedFS == "ext4") { root = part } } if boot.Path == "" { return flashBlockDevice{}, flashBlockDevice{}, fmt.Errorf("could not find a boot partition with a FAT filesystem") } if root.Path == "" { return flashBlockDevice{}, flashBlockDevice{}, fmt.Errorf("could not find a writable ext4 partition") } return boot, root, nil } func verifyBootPartitionFiles(partitionPath string) ([]string, error) { mountDir, err := os.MkdirTemp("", "metis-boot-") if err != nil { return nil, err } defer os.RemoveAll(mountDir) cmd := exec.Command("mount", "-o", "ro", partitionPath, mountDir) if out, err := cmd.CombinedOutput(); err != nil { return nil, fmt.Errorf("mount %s read-only: %v: %s", partitionPath, err, strings.TrimSpace(string(out))) } defer exec.Command("umount", mountDir).Run() checked := make([]string, 0, len(requiredBootFiles)) for _, name := range requiredBootFiles { if _, err := os.Stat(filepath.Join(mountDir, name)); err != nil { return nil, fmt.Errorf("boot partition %s is missing %s", partitionPath, name) } checked = append(checked, name) } return checked, nil } func isBootPartitionType(partType string) bool { normalized := strings.ToLower(strings.TrimSpace(partType)) switch normalized { case "b", "c", "0b", "0c", "ef", "ef00": return true } return normalized == "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" } func isLinuxPartitionType(partType string) bool { normalized := strings.ToLower(strings.TrimSpace(partType)) switch normalized { case "83", "8300": return true } return normalized == "0fc63daf-8483-4772-8e79-3d69d8477de4" } func firstNonEmpty(values ...string) string { for _, value := range values { if trimmed := strings.TrimSpace(value); trimmed != "" { return trimmed } } return "" }