feat: add download/checksum and burn execution stub
This commit is contained in:
parent
b500241b8c
commit
910d01d22a
@ -72,17 +72,17 @@ func burnCmd(args []string) {
|
||||
log.Fatalf("--node and --device are required")
|
||||
}
|
||||
inv := loadInventory(*invPath)
|
||||
p, err := plan.Build(inv, *node, *device, *cache)
|
||||
p, err := plan.Execute(inv, *node, *device, *cache, *confirm)
|
||||
if err != nil {
|
||||
log.Fatalf("build plan: %v", err)
|
||||
log.Fatalf("burn: %v", err)
|
||||
}
|
||||
fmt.Printf("Plan for %s to %s:\n", p.Node, p.Device)
|
||||
for _, a := range p.Actions {
|
||||
fmt.Printf("- [%s] %s\n", a.Type, a.Detail)
|
||||
}
|
||||
if !*confirm {
|
||||
fmt.Printf("\nDry run. Re-run with --yes to execute (not yet implemented).\n")
|
||||
fmt.Printf("\nDry run. Re-run with --yes to execute.\n")
|
||||
return
|
||||
}
|
||||
log.Fatalf("burn execution not yet implemented; follow plan commands manually")
|
||||
fmt.Println("\nBurn complete. Safely eject and insert the SD into the node.")
|
||||
}
|
||||
|
||||
79
pkg/image/download.go
Normal file
79
pkg/image/download.go
Normal file
@ -0,0 +1,79 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Download fetches url into dest if dest does not exist.
|
||||
func Download(url, dest string) error {
|
||||
if _, err := os.Stat(dest); err == nil {
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.HasPrefix(url, "file://") {
|
||||
src := strings.TrimPrefix(url, "file://")
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
_, err = io.Copy(out, in)
|
||||
return err
|
||||
}
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("download failed: %s", resp.Status)
|
||||
}
|
||||
out, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
// VerifyChecksum checks sha256 in the form "sha256:<hex>".
|
||||
func VerifyChecksum(path, checksum string) error {
|
||||
if checksum == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.SplitN(checksum, ":", 2)
|
||||
if len(parts) != 2 || parts[0] != "sha256" {
|
||||
return errors.New("unsupported checksum format; use sha256:<hex>")
|
||||
}
|
||||
expected := strings.ToLower(parts[1])
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return err
|
||||
}
|
||||
sum := hex.EncodeToString(h.Sum(nil))
|
||||
if sum != expected {
|
||||
return fmt.Errorf("checksum mismatch: expected %s got %s", expected, sum)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
47
pkg/plan/burn.go
Normal file
47
pkg/plan/burn.go
Normal file
@ -0,0 +1,47 @@
|
||||
package plan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"metis/pkg/image"
|
||||
"metis/pkg/inventory"
|
||||
)
|
||||
|
||||
// Execute performs a burn if confirm is true. With confirm=false, it only downloads/verifies and returns the plan.
|
||||
func Execute(inv *inventory.Inventory, nodeName, device, cacheDir string, confirm bool) (*Plan, error) {
|
||||
p, err := Build(inv, nodeName, device, cacheDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cacheImage := filepath.Join(cacheDir, filepath.Base(p.Image))
|
||||
if err := image.Download(p.Image, cacheImage); err != nil {
|
||||
return p, fmt.Errorf("download image: %w", err)
|
||||
}
|
||||
if err := image.VerifyChecksum(cacheImage, checksumFromInventory(inv, nodeName)); err != nil {
|
||||
return p, err
|
||||
}
|
||||
if !confirm {
|
||||
return p, nil
|
||||
}
|
||||
if device == "" || device == "/dev/sdX" {
|
||||
return p, fmt.Errorf("refusing to write to placeholder device")
|
||||
}
|
||||
ddCmd := []string{"dd", fmt.Sprintf("if=%s", cacheImage), fmt.Sprintf("of=%s", device), "bs=4M", "status=progress", "conv=fsync"}
|
||||
cmd := exec.Command(ddCmd[0], ddCmd[1:]...)
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
if err := cmd.Run(); err != nil {
|
||||
return p, fmt.Errorf("dd failed: %w", err)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func checksumFromInventory(inv *inventory.Inventory, node string) string {
|
||||
_, cls, err := inv.FindNode(node)
|
||||
if err != nil || cls == nil {
|
||||
return ""
|
||||
}
|
||||
return cls.Checksum
|
||||
}
|
||||
24
pkg/util/run.go
Normal file
24
pkg/util/run.go
Normal file
@ -0,0 +1,24 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// Run executes a command and returns combined output or error.
|
||||
func Run(cmd string, args ...string) error {
|
||||
c := exec.Command(cmd, args...)
|
||||
c.Stdout = nil
|
||||
c.Stderr = nil
|
||||
return c.Run()
|
||||
}
|
||||
|
||||
// RunLogged returns stdout/stderr for logging while failing on error.
|
||||
func RunLogged(cmd string, args ...string) (string, error) {
|
||||
c := exec.Command(cmd, args...)
|
||||
out, err := c.CombinedOutput()
|
||||
if err != nil {
|
||||
return string(out), fmt.Errorf("%s %v failed: %w: %s", cmd, args, err, string(out))
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user