|  | package main | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "crypto/ed25519" | 
|  | "crypto/rand" | 
|  | "encoding/pem" | 
|  | "log" | 
|  | "os" | 
|  | "path/filepath" | 
|  |  | 
|  | "github.com/adrg/xdg" | 
|  | "github.com/spf13/cobra" | 
|  |  | 
|  | "source.monogon.dev/metropolis/cli/metroctl/core" | 
|  | "source.monogon.dev/metropolis/cli/pkg/datafile" | 
|  | "source.monogon.dev/metropolis/proto/api" | 
|  | ) | 
|  |  | 
|  | var installCmd = &cobra.Command{ | 
|  | Short: "Contains subcommands to install Metropolis over different mediums.", | 
|  | Use:   "install", | 
|  | } | 
|  |  | 
|  | var genusbCmd = &cobra.Command{ | 
|  | Use:     "genusb target", | 
|  | Short:   "Generates a Metropolis installer disk or image.", | 
|  | Example: "metroctl install genusb /dev/sdx", | 
|  | 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) { | 
|  | installer := datafile.MustGet("metropolis/installer/kernel.efi") | 
|  | bundle := datafile.MustGet("metropolis/node/node.zip") | 
|  |  | 
|  | // 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 := os.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 := os.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:     bytes.NewBuffer(installer), | 
|  | InstallerSize: uint64(len(installer)), | 
|  | NodeParams:    params, | 
|  | Bundle:        bytes.NewBuffer(bundle), | 
|  | BundleSize:    uint64(len(bundle)), | 
|  | } | 
|  |  | 
|  | log.Printf("Generating installer image (this can take a while, see issues/92).") | 
|  | if err := core.MakeInstallerImage(installerImageArgs); err != nil { | 
|  | log.Fatalf("Failed to create installer: %v", err) | 
|  | } | 
|  | } | 
|  |  | 
|  | func init() { | 
|  | rootCmd.AddCommand(installCmd) | 
|  | installCmd.AddCommand(genusbCmd) | 
|  | } |