m/c/metroctl: add MVP disk/image-only installer
This adds a very minimalist metroctl install command.
Only supports boostrapping and a single owner key.
But good enough to set up a node.
Change-Id: I9306a054b9540a3a0c70621f3f5d9cb34fc18d14
Reviewed-on: https://review.monogon.dev/c/monogon/+/417
Reviewed-by: Mateusz Zalega <mateusz@monogon.tech>
diff --git a/metropolis/cli/metroctl/install.go b/metropolis/cli/metroctl/install.go
new file mode 100644
index 0000000..ab31610
--- /dev/null
+++ b/metropolis/cli/metroctl/install.go
@@ -0,0 +1,128 @@
+package main
+
+import (
+ "crypto/ed25519"
+ "crypto/rand"
+ "encoding/pem"
+ "io/ioutil"
+ "log"
+ "os"
+ "path/filepath"
+
+ "github.com/adrg/xdg"
+ "github.com/spf13/cobra"
+
+ "source.monogon.dev/metropolis/cli/metroctl/core"
+ "source.monogon.dev/metropolis/proto/api"
+)
+
+var installCmd = &cobra.Command{
+ Short: "Contains subcommands to install Metropolis over different mediums.",
+ Use: "install",
+}
+
+// install flags
+var installer *string
+var bundle *string
+
+var genusbCmd = &cobra.Command{
+ Use: "genusb target --installer=inst.efi --bundle=bundle.bin",
+ Short: "Generates a Metropolis installer disk or image.",
+ Example: "metroctl install genusb /dev/sdx --installer=installer_x86_64.efi --bundle=metropolis_dev_x86_64.tar.xz",
+ Args: cobra.ExactArgs(1), // One positional argument: the target
+ Run: doGenUSB,
+}
+
+// A PEM block type for a Metropolis initial owner private key
+const ownerKeyType = "METROPOLIS INITIAL OWNER PRIVATE KEY"
+
+func doGenUSB(cmd *cobra.Command, args []string) {
+ installerFile, err := os.Open(*installer)
+ if err != nil {
+ log.Fatalf("Failed to open installer: %v", err)
+ }
+ installerFileStat, err := installerFile.Stat()
+ if err != nil {
+ log.Fatalf("Failed to stat installer: %v", err)
+ }
+ var bundleFile *os.File
+ var bundleFileStat os.FileInfo
+ if bundle != nil && *bundle != "" {
+ bundleFile, err = os.Open(*bundle)
+ if err != nil {
+ log.Fatalf("Failed to open bundle: %v", err)
+ }
+ bundleFileStat, err = bundleFile.Stat()
+ if err != nil {
+ log.Fatalf("Failed to stat bundle: %v", err)
+ }
+ }
+
+ // TODO(lorenz): Have a key management story for this
+ if err := os.MkdirAll(filepath.Join(xdg.ConfigHome, "metroctl"), 0700); err != nil {
+ log.Fatalf("Failed to create config directory: %v", err)
+ }
+ var ownerPublicKey ed25519.PublicKey
+ ownerPrivateKeyPEM, err := ioutil.ReadFile(filepath.Join(xdg.ConfigHome, "metroctl/owner-key.pem"))
+ if os.IsNotExist(err) {
+ pub, priv, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ log.Fatalf("Failed to generate owner private key: %v", err)
+ }
+ pemPriv := pem.EncodeToMemory(&pem.Block{Type: ownerKeyType, Bytes: priv})
+ if err := ioutil.WriteFile(filepath.Join(xdg.ConfigHome, "metroctl/owner-key.pem"), pemPriv, 0600); err != nil {
+ log.Fatalf("Failed to store owner private key: %v", err)
+ }
+ ownerPublicKey = pub
+ } else if err != nil {
+ log.Fatalf("Failed to load owner private key: %v", err)
+ } else {
+ block, _ := pem.Decode(ownerPrivateKeyPEM)
+ if block == nil {
+ log.Fatalf("owner-key.pem contains invalid PEM")
+ }
+ if block.Type != ownerKeyType {
+ log.Fatalf("owner-key.pem contains a PEM block that's not a %v", ownerKeyType)
+ }
+ if len(block.Bytes) != ed25519.PrivateKeySize {
+ log.Fatal("owner-key.pem contains non-Ed25519 key")
+ }
+ ownerPrivateKey := ed25519.PrivateKey(block.Bytes)
+ ownerPublicKey = ownerPrivateKey.Public().(ed25519.PublicKey)
+ }
+
+ // TODO(lorenz): This can only bootstrap right now. As soon as @serge's role
+ // management has stabilized we can replace this with a proper
+ // implementation.
+ params := &api.NodeParameters{
+ Cluster: &api.NodeParameters_ClusterBootstrap_{
+ ClusterBootstrap: &api.NodeParameters_ClusterBootstrap{
+ OwnerPublicKey: ownerPublicKey,
+ },
+ },
+ }
+
+ installerImageArgs := core.MakeInstallerImageArgs{
+ TargetPath: args[0],
+ Installer: installerFile,
+ InstallerSize: uint64(installerFileStat.Size()),
+ NodeParams: params,
+ }
+
+ if bundleFile != nil {
+ installerImageArgs.Bundle = bundleFile
+ installerImageArgs.BundleSize = uint64(bundleFileStat.Size())
+ }
+
+ if err := core.MakeInstallerImage(installerImageArgs); err != nil {
+ log.Fatalf("Failed to create installer: %v", err)
+ }
+}
+
+func init() {
+ rootCmd.AddCommand(installCmd)
+ installCmd.AddCommand(genusbCmd)
+
+ bundle = installCmd.PersistentFlags().StringP("bundle", "b", "", "Metropolis bundle file to use")
+ installer = installCmd.PersistentFlags().StringP("installer", "i", "", "Metropolis installer file to use")
+}