metropolis: use new OS image format for install

This switches the USB and SSH installation methods to the new OS image
format based on OCI artifacts.

When stored on disk, the new format consists of a directory containing
an OCI layout, instead of a single file. This means that all steps which
copy or upload an image now need to handle a tree of files.

Change-Id: I526d32f5c50bd74f513f785118768a56b2655fa0
Reviewed-on: https://review.monogon.dev/c/monogon/+/4090
Reviewed-by: Tim Windelschmidt <tim@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/cli/metroctl/cmd_install_ssh.go b/metropolis/cli/metroctl/cmd_install_ssh.go
index b52c353..3dec0d4 100644
--- a/metropolis/cli/metroctl/cmd_install_ssh.go
+++ b/metropolis/cli/metroctl/cmd_install_ssh.go
@@ -24,6 +24,7 @@
 	"google.golang.org/protobuf/proto"
 
 	"source.monogon.dev/osbase/net/sshtakeover"
+	"source.monogon.dev/osbase/oci"
 )
 
 // progressbarUpdater wraps a [progressbar.ProgressBar] with an improved
@@ -93,7 +94,7 @@
 var sshCmd = &cobra.Command{
 	Use:     "ssh --disk=<disk> <target>",
 	Short:   "Installs Metropolis on a Linux system accessible via SSH.",
-	Example: "metroctl install --bundle=metropolis-v0.1.zip --takeover=takeover ssh --disk=nvme0n1 root@ssh-enabled-server.example",
+	Example: "metroctl install --image=metropolis-v0.1 --takeover=takeover ssh --disk=nvme0n1 root@ssh-enabled-server.example",
 	Args:    cobra.ExactArgs(1), // One positional argument: the target
 	RunE: func(cmd *cobra.Command, args []string) error {
 		user, address, err := parseSSHAddr(args[0])
@@ -185,22 +186,36 @@
 		}
 
 		const takeoverTargetPath = "/root/takeover"
-		const bundleTargetPath = "/root/bundle.zip"
-		bundle, err := external("bundle", "_main/metropolis/node/bundle.zip", bundlePath)
+		const imageTargetPath = "/root/osimage"
+
+		imagePathResolved, err := external("image", "_main/metropolis/node/oci_image", imagePath)
 		if err != nil {
 			return err
 		}
+		image, err := oci.ReadLayout(imagePathResolved)
+		if err != nil {
+			return fmt.Errorf("failed to read OS image: %w", err)
+		}
+		imageLayout, err := oci.CreateLayout(image)
+		if err != nil {
+			return fmt.Errorf("failed to read OS image: %w", err)
+		}
 		takeoverPath, err := cmd.Flags().GetString("takeover")
 		if err != nil {
 			return err
 		}
-		takeover, err := external("takeover", "_main/metropolis/cli/takeover/takeover_bin_/takeover_bin", &takeoverPath)
+		takeover, err := externalFile("takeover", "_main/metropolis/cli/takeover/takeover_bin_/takeover_bin", &takeoverPath)
 		if err != nil {
 			return err
 		}
 
 		log.Println("Uploading files to target host.")
-		totalSize := takeover.Size() + bundle.Size()
+		totalSize := takeover.Size()
+		for _, entry := range imageLayout.Walk() {
+			if entry.Mode.IsRegular() {
+				totalSize += entry.Content.Size()
+			}
+		}
 		barUpdater := startProgressbarUpdater(progressbar.DefaultBytes(totalSize))
 		defer barUpdater.stop()
 		conn.SetProgress(barUpdater.add)
@@ -215,14 +230,9 @@
 			return fmt.Errorf("error while uploading %q: %w", takeoverTargetPath, err)
 		}
 
-		bundleContent, err := bundle.Open()
+		err = conn.UploadTree(ctx, imageTargetPath, imageLayout)
 		if err != nil {
-			return err
-		}
-		err = conn.Upload(ctx, bundleTargetPath, bundleContent)
-		bundleContent.Close()
-		if err != nil {
-			return fmt.Errorf("error while uploading %q: %w", bundleTargetPath, err)
+			return fmt.Errorf("error while uploading OS image: %w", err)
 		}
 
 		barUpdater.stop()