metropolis/cli/metroctl: implement install ssh
This implements another way of installing metropolis via ssh. It does
this by uploading the files to the target machine and then doing a kexec
into the install environment. If it fails at any point it will print the
error and reboot.
Change-Id: I1ac6538896709c386b053a84903fa04940c1f012
Reviewed-on: https://review.monogon.dev/c/monogon/+/2079
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/cli/takeover/install.go b/metropolis/cli/takeover/install.go
new file mode 100644
index 0000000..50679d0
--- /dev/null
+++ b/metropolis/cli/takeover/install.go
@@ -0,0 +1,115 @@
+package main
+
+import (
+ "archive/zip"
+ "bytes"
+ _ "embed"
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+
+ "source.monogon.dev/go/logging"
+ "source.monogon.dev/osbase/blockdev"
+ "source.monogon.dev/osbase/build/mkimage/osimage"
+ "source.monogon.dev/osbase/efivarfs"
+)
+
+//go:embed metropolis/node/core/abloader/abloader_bin.efi
+var abloader []byte
+
+// FileSizedReader is a small adapter from fs.File to fs.SizedReader
+// Panics on Stat() failure, so should only be used with sources where Stat()
+// cannot fail.
+type FileSizedReader struct {
+ fs.File
+}
+
+func (f FileSizedReader) Size() int64 {
+ stat, err := f.Stat()
+ if err != nil {
+ panic(err)
+ }
+ return stat.Size()
+}
+
+// EnvInstallTarget environment variable which tells the takeover binary where
+// to install to
+const EnvInstallTarget = "TAKEOVER_INSTALL_TARGET"
+
+func installMetropolis(l logging.Leveled) error {
+ // Validate we are running via EFI.
+ if _, err := os.Stat("/sys/firmware/efi"); os.IsNotExist(err) {
+ //nolint:ST1005
+ return fmt.Errorf("Monogon OS can only be installed on EFI-booted machines, this one is not")
+ }
+
+ metropolisSpecRaw, err := os.ReadFile("/params.pb")
+ if err != nil {
+ return err
+ }
+
+ bundleRaw, err := os.Open("/bundle.zip")
+ if err != nil {
+ return err
+ }
+
+ bundleStat, err := bundleRaw.Stat()
+ if err != nil {
+ return err
+ }
+
+ bundle, err := zip.NewReader(bundleRaw, bundleStat.Size())
+ if err != nil {
+ return fmt.Errorf("failed to open node bundle: %w", err)
+ }
+
+ installParams, err := setupOSImageParams(bundle, metropolisSpecRaw, os.Getenv(EnvInstallTarget))
+ if err != nil {
+ return err
+ }
+
+ be, err := osimage.Write(installParams)
+ if err != nil {
+ return fmt.Errorf("failed to apply installation: %w", err)
+ }
+ bootEntryIdx, err := efivarfs.AddBootEntry(be)
+ if err != nil {
+ return fmt.Errorf("error creating EFI boot entry: %w", err)
+ }
+ if err := efivarfs.SetBootOrder(efivarfs.BootOrder{uint16(bootEntryIdx)}); err != nil {
+ return fmt.Errorf("error setting EFI boot order: %w", err)
+ }
+ l.Info("Metropolis installation completed")
+ return nil
+}
+
+func setupOSImageParams(bundle *zip.Reader, metropolisSpecRaw []byte, installTarget string) (*osimage.Params, error) {
+ rootDev, err := blockdev.Open(filepath.Join("/dev", installTarget))
+ if err != nil {
+ return nil, fmt.Errorf("failed to open root device: %w", err)
+ }
+
+ efiPayload, err := bundle.Open("kernel_efi.efi")
+ if err != nil {
+ return nil, fmt.Errorf("invalid bundle: %w", err)
+ }
+
+ systemImage, err := bundle.Open("verity_rootfs.img")
+ if err != nil {
+ return nil, fmt.Errorf("invalid bundle: %w", err)
+ }
+
+ return &osimage.Params{
+ PartitionSize: osimage.PartitionSizeInfo{
+ ESP: 384,
+ System: 4096,
+ Data: 128,
+ },
+ SystemImage: systemImage,
+ EFIPayload: FileSizedReader{efiPayload},
+ ABLoader: bytes.NewReader(abloader),
+ NodeParameters: bytes.NewReader(metropolisSpecRaw),
+ Output: rootDev,
+ }, nil
+}