blob: 7ea36e369667272d7866ea16a5a8bcca137aecb3 [file] [log] [blame]
Lorenz Brune6573102021-11-02 14:15:37 +01001package main
2
3import (
Serge Bazanski97783222021-12-14 16:04:26 +01004 "bytes"
Lorenz Brune6573102021-11-02 14:15:37 +01005 "crypto/ed25519"
6 "crypto/rand"
Lorenz Brun7a510192022-07-04 15:31:38 +00007 _ "embed"
Lorenz Brune6573102021-11-02 14:15:37 +01008 "encoding/pem"
Lorenz Brun7a510192022-07-04 15:31:38 +00009 "io"
Lorenz Brune6573102021-11-02 14:15:37 +010010 "log"
11 "os"
12 "path/filepath"
13
14 "github.com/adrg/xdg"
15 "github.com/spf13/cobra"
16
17 "source.monogon.dev/metropolis/cli/metroctl/core"
Serge Bazanski97783222021-12-14 16:04:26 +010018 "source.monogon.dev/metropolis/cli/pkg/datafile"
Lorenz Brune6573102021-11-02 14:15:37 +010019 "source.monogon.dev/metropolis/proto/api"
20)
21
22var installCmd = &cobra.Command{
Lorenz Brun7a510192022-07-04 15:31:38 +000023 Short: "Contains subcommands to install Metropolis via different media.",
Lorenz Brune6573102021-11-02 14:15:37 +010024 Use: "install",
25}
26
Lorenz Brun7a510192022-07-04 15:31:38 +000027var bundlePath = installCmd.PersistentFlags().StringP("bundle", "b", "", "Path to the Metropolis bundle to be installed")
28
Lorenz Brune6573102021-11-02 14:15:37 +010029var genusbCmd = &cobra.Command{
Serge Bazanski97783222021-12-14 16:04:26 +010030 Use: "genusb target",
Lorenz Brune6573102021-11-02 14:15:37 +010031 Short: "Generates a Metropolis installer disk or image.",
Lorenz Brun7a510192022-07-04 15:31:38 +000032 Example: "metroctl install --bundle=metropolis-v0.1.zip genusb /dev/sdx",
Lorenz Brune6573102021-11-02 14:15:37 +010033 Args: cobra.ExactArgs(1), // One positional argument: the target
34 Run: doGenUSB,
35}
36
37// A PEM block type for a Metropolis initial owner private key
38const ownerKeyType = "METROPOLIS INITIAL OWNER PRIVATE KEY"
39
Lorenz Brun7a510192022-07-04 15:31:38 +000040//go:embed metropolis/installer/kernel.efi
41var installer []byte
42
Lorenz Brune6573102021-11-02 14:15:37 +010043func doGenUSB(cmd *cobra.Command, args []string) {
Lorenz Brun7a510192022-07-04 15:31:38 +000044 var bundleReader io.Reader
45 var bundleSize uint64
46 if bundlePath == nil || *bundlePath == "" {
47 // Attempt Bazel runfile bundle if not explicitly set
48 bundle, err := datafile.Get("metropolis/node/bundle.zip")
49 if err != nil {
50 log.Fatalf("No bundle specified and fallback to runfiles failed: %v", err)
51 }
52 bundleReader = bytes.NewReader(bundle)
53 bundleSize = uint64(len(bundle))
54 } else {
55 // Load bundle from specified path
56 bundle, err := os.Open(*bundlePath)
57 if err != nil {
58 log.Fatalf("Failed to open specified bundle: %v", err)
59 }
60 bundleStat, err := bundle.Stat()
61 if err != nil {
62 log.Fatalf("Failed to stat specified bundle: %v", err)
63 }
64 bundleReader = bundle
65 bundleSize = uint64(bundleStat.Size())
66 }
Lorenz Brune6573102021-11-02 14:15:37 +010067
68 // TODO(lorenz): Have a key management story for this
69 if err := os.MkdirAll(filepath.Join(xdg.ConfigHome, "metroctl"), 0700); err != nil {
70 log.Fatalf("Failed to create config directory: %v", err)
71 }
72 var ownerPublicKey ed25519.PublicKey
Lorenz Brun764a2de2021-11-22 16:26:36 +010073 ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "metroctl/owner-key.pem"))
Lorenz Brune6573102021-11-02 14:15:37 +010074 if os.IsNotExist(err) {
75 pub, priv, err := ed25519.GenerateKey(rand.Reader)
76 if err != nil {
77 log.Fatalf("Failed to generate owner private key: %v", err)
78 }
79 pemPriv := pem.EncodeToMemory(&pem.Block{Type: ownerKeyType, Bytes: priv})
Lorenz Brun764a2de2021-11-22 16:26:36 +010080 if err := os.WriteFile(filepath.Join(xdg.ConfigHome, "metroctl/owner-key.pem"), pemPriv, 0600); err != nil {
Lorenz Brune6573102021-11-02 14:15:37 +010081 log.Fatalf("Failed to store owner private key: %v", err)
82 }
83 ownerPublicKey = pub
84 } else if err != nil {
85 log.Fatalf("Failed to load owner private key: %v", err)
86 } else {
87 block, _ := pem.Decode(ownerPrivateKeyPEM)
88 if block == nil {
89 log.Fatalf("owner-key.pem contains invalid PEM")
90 }
91 if block.Type != ownerKeyType {
92 log.Fatalf("owner-key.pem contains a PEM block that's not a %v", ownerKeyType)
93 }
94 if len(block.Bytes) != ed25519.PrivateKeySize {
95 log.Fatal("owner-key.pem contains non-Ed25519 key")
96 }
97 ownerPrivateKey := ed25519.PrivateKey(block.Bytes)
98 ownerPublicKey = ownerPrivateKey.Public().(ed25519.PublicKey)
99 }
100
101 // TODO(lorenz): This can only bootstrap right now. As soon as @serge's role
102 // management has stabilized we can replace this with a proper
103 // implementation.
104 params := &api.NodeParameters{
105 Cluster: &api.NodeParameters_ClusterBootstrap_{
106 ClusterBootstrap: &api.NodeParameters_ClusterBootstrap{
107 OwnerPublicKey: ownerPublicKey,
108 },
109 },
110 }
111
112 installerImageArgs := core.MakeInstallerImageArgs{
113 TargetPath: args[0],
Lorenz Brun7a510192022-07-04 15:31:38 +0000114 Installer: bytes.NewReader(installer),
Serge Bazanski97783222021-12-14 16:04:26 +0100115 InstallerSize: uint64(len(installer)),
Lorenz Brune6573102021-11-02 14:15:37 +0100116 NodeParams: params,
Lorenz Brun7a510192022-07-04 15:31:38 +0000117 Bundle: bundleReader,
118 BundleSize: bundleSize,
Lorenz Brune6573102021-11-02 14:15:37 +0100119 }
120
Serge Bazanski97783222021-12-14 16:04:26 +0100121 log.Printf("Generating installer image (this can take a while, see issues/92).")
Lorenz Brune6573102021-11-02 14:15:37 +0100122 if err := core.MakeInstallerImage(installerImageArgs); err != nil {
123 log.Fatalf("Failed to create installer: %v", err)
124 }
125}
126
127func init() {
128 rootCmd.AddCommand(installCmd)
129 installCmd.AddCommand(genusbCmd)
Lorenz Brune6573102021-11-02 14:15:37 +0100130}