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
+}