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/BUILD.bazel b/metropolis/cli/metroctl/BUILD.bazel
index 8829be4..e1d724b 100644
--- a/metropolis/cli/metroctl/BUILD.bazel
+++ b/metropolis/cli/metroctl/BUILD.bazel
@@ -50,6 +50,7 @@
"//osbase/logtree",
"//osbase/logtree/proto",
"//osbase/net/sshtakeover",
+ "//osbase/oci",
"//osbase/structfs",
"//version",
"@com_github_adrg_xdg//:xdg",
@@ -78,7 +79,7 @@
"//conditions:default": [
"//metropolis/cli/takeover",
"//metropolis/installer:kernel",
- "//metropolis/node:bundle",
+ "//metropolis/node:oci_image",
],
}),
embed = [":metroctl_lib"],
diff --git a/metropolis/cli/metroctl/cmd_install.go b/metropolis/cli/metroctl/cmd_install.go
index 1b9b8cb..3fd4802 100644
--- a/metropolis/cli/metroctl/cmd_install.go
+++ b/metropolis/cli/metroctl/cmd_install.go
@@ -36,7 +36,7 @@
var bootstrap = installCmd.PersistentFlags().Bool("bootstrap", false, "Create a bootstrap installer image.")
var bootstrapTPMMode = flagdefs.TPMModePflag(installCmd.PersistentFlags(), "bootstrap-tpm-mode", cpb.ClusterConfiguration_TPM_MODE_REQUIRED, "TPM mode to set on cluster")
var bootstrapStorageSecurityPolicy = flagdefs.StorageSecurityPolicyPflag(installCmd.PersistentFlags(), "bootstrap-storage-security", cpb.ClusterConfiguration_STORAGE_SECURITY_POLICY_NEEDS_ENCRYPTION_AND_AUTHENTICATION, "Storage security policy to set on cluster")
-var bundlePath = installCmd.PersistentFlags().StringP("bundle", "b", "", "Path to the Metropolis bundle to be installed")
+var imagePath = installCmd.PersistentFlags().StringP("image", "", "", "Path to the OCI layout directory containing the Metropolis OS image to be installed")
var nodeParamPath = installCmd.PersistentFlags().String("node-params", "", "Path to the metropolis.proto.api.NodeParameters prototext file (advanced usage only)")
func makeNodeParams() (*api.NodeParameters, error) {
@@ -109,19 +109,26 @@
return params, nil
}
-func external(name, datafilePath string, flag *string) (structfs.Blob, error) {
+func external(name, datafilePath string, flag *string) (string, error) {
if flag == nil || *flag == "" {
rPath, err := runfiles.Rlocation(datafilePath)
if err != nil {
- return nil, fmt.Errorf("no %s specified", name)
+ return "", fmt.Errorf("no %s specified", name)
}
- return structfs.OSPathBlob(rPath)
+ return rPath, nil
}
- f, err := structfs.OSPathBlob(*flag)
+ return *flag, nil
+}
+
+func externalFile(name, datafilePath string, flag *string) (structfs.Blob, error) {
+ path, err := external(name, datafilePath, flag)
+ if err != nil {
+ return nil, err
+ }
+ f, err := structfs.OSPathBlob(path)
if err != nil {
return nil, fmt.Errorf("failed to open specified %s: %w", name, err)
}
-
return f, nil
}
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()
diff --git a/metropolis/cli/metroctl/cmd_install_usb.go b/metropolis/cli/metroctl/cmd_install_usb.go
index 38c1501..4453ed7 100644
--- a/metropolis/cli/metroctl/cmd_install_usb.go
+++ b/metropolis/cli/metroctl/cmd_install_usb.go
@@ -11,12 +11,13 @@
"github.com/spf13/cobra"
"source.monogon.dev/metropolis/cli/metroctl/core"
+ "source.monogon.dev/osbase/oci"
)
var genusbCmd = &cobra.Command{
Use: "genusb target",
Short: "Generates a Metropolis installer disk or image.",
- Example: "metroctl install --bundle=metropolis-v0.1.zip genusb /dev/sdx",
+ Example: "metroctl install --image=metropolis-v0.1 genusb /dev/sdx",
Args: PrintUsageOnWrongArgs(cobra.ExactArgs(1)), // One positional argument: the target
RunE: func(cmd *cobra.Command, args []string) error {
params, err := makeNodeParams()
@@ -29,20 +30,24 @@
return err
}
- installer, err := external("installer", "_main/metropolis/installer/kernel.efi", &installerPath)
+ installer, err := externalFile("installer", "_main/metropolis/installer/kernel.efi", &installerPath)
if err != nil {
return err
}
- bundle, err := external("bundle", "_main/metropolis/node/bundle.zip", bundlePath)
+ 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)
+ }
installerImageArgs := core.MakeInstallerImageArgs{
TargetPath: args[0],
Installer: installer,
NodeParams: params,
- Bundle: bundle,
+ Image: image,
}
log.Printf("Generating installer image (this can take a while, see issues/92).")
diff --git a/metropolis/cli/metroctl/core/BUILD.bazel b/metropolis/cli/metroctl/core/BUILD.bazel
index 73527d5..cd95ac1 100644
--- a/metropolis/cli/metroctl/core/BUILD.bazel
+++ b/metropolis/cli/metroctl/core/BUILD.bazel
@@ -21,6 +21,7 @@
"//osbase/blockdev",
"//osbase/fat32",
"//osbase/gpt",
+ "//osbase/oci",
"//osbase/structfs",
"@io_k8s_client_go//pkg/apis/clientauthentication/v1:clientauthentication",
"@io_k8s_client_go//tools/clientcmd",
diff --git a/metropolis/cli/metroctl/core/install.go b/metropolis/cli/metroctl/core/install.go
index 5d43a89..92e8303 100644
--- a/metropolis/cli/metroctl/core/install.go
+++ b/metropolis/cli/metroctl/core/install.go
@@ -15,6 +15,7 @@
"source.monogon.dev/osbase/blockdev"
"source.monogon.dev/osbase/fat32"
"source.monogon.dev/osbase/gpt"
+ "source.monogon.dev/osbase/oci"
"source.monogon.dev/osbase/structfs"
)
@@ -28,13 +29,13 @@
// Optional NodeParameters to be embedded for use by the installer.
NodeParams *api.NodeParameters
- // Optional Reader for a Metropolis bundle for use by the installer.
- Bundle structfs.Blob
+ // Optional OS image for use by the installer.
+ Image *oci.Image
}
// MakeInstallerImage generates an installer disk image containing a Table
// partition table and a single FAT32 partition with an installer and optionally
-// with a bundle and/or Node Parameters.
+// with an OS image and/or Node Parameters.
func MakeInstallerImage(args MakeInstallerImageArgs) error {
if args.Installer == nil {
return errors.New("installer is mandatory")
@@ -58,8 +59,12 @@
return err
}
}
- if args.Bundle != nil {
- if err := espRoot.PlaceFile("metropolis-installer/bundle.bin", args.Bundle); err != nil {
+ if args.Image != nil {
+ imageLayout, err := oci.CreateLayout(args.Image)
+ if err != nil {
+ return err
+ }
+ if err := espRoot.PlaceDir("metropolis-installer/osimage", imageLayout); err != nil {
return err
}
}