blob: cbbb9b69bca972f931687b1d381597eeb25dfdd3 [file] [log] [blame]
Lorenz Brune6573102021-11-02 14:15:37 +01001package main
2
3import (
Serge Bazanski97783222021-12-14 16:04:26 +01004 "bytes"
Mateusz Zalegad5f2f7a2022-07-05 18:48:56 +02005 "context"
Lorenz Brune6573102021-11-02 14:15:37 +01006 "crypto/ed25519"
7 "crypto/rand"
Lorenz Brun7a510192022-07-04 15:31:38 +00008 _ "embed"
Lorenz Brune6573102021-11-02 14:15:37 +01009 "encoding/pem"
Lorenz Brun7a510192022-07-04 15:31:38 +000010 "io"
Lorenz Brune6573102021-11-02 14:15:37 +010011 "log"
12 "os"
13 "path/filepath"
14
15 "github.com/adrg/xdg"
16 "github.com/spf13/cobra"
17
18 "source.monogon.dev/metropolis/cli/metroctl/core"
Mateusz Zalegad5f2f7a2022-07-05 18:48:56 +020019 clicontext "source.monogon.dev/metropolis/cli/pkg/context"
Serge Bazanski97783222021-12-14 16:04:26 +010020 "source.monogon.dev/metropolis/cli/pkg/datafile"
Lorenz Brune6573102021-11-02 14:15:37 +010021 "source.monogon.dev/metropolis/proto/api"
22)
23
24var installCmd = &cobra.Command{
Lorenz Brun7a510192022-07-04 15:31:38 +000025 Short: "Contains subcommands to install Metropolis via different media.",
Lorenz Brune6573102021-11-02 14:15:37 +010026 Use: "install",
27}
28
Lorenz Brun7a510192022-07-04 15:31:38 +000029var bundlePath = installCmd.PersistentFlags().StringP("bundle", "b", "", "Path to the Metropolis bundle to be installed")
30
Lorenz Brune6573102021-11-02 14:15:37 +010031var genusbCmd = &cobra.Command{
Serge Bazanski97783222021-12-14 16:04:26 +010032 Use: "genusb target",
Lorenz Brune6573102021-11-02 14:15:37 +010033 Short: "Generates a Metropolis installer disk or image.",
Lorenz Brun7a510192022-07-04 15:31:38 +000034 Example: "metroctl install --bundle=metropolis-v0.1.zip genusb /dev/sdx",
Lorenz Brune6573102021-11-02 14:15:37 +010035 Args: cobra.ExactArgs(1), // One positional argument: the target
36 Run: doGenUSB,
37}
38
Mateusz Zalegad5f2f7a2022-07-05 18:48:56 +020039// bootstrap is a flag controlling node parameters included in the installer
40// image. If set, the installed node will bootstrap a new cluster. Otherwise,
41// it will try to connect to the cluster which endpoints were provided with
42// the --endpoints flag.
43var bootstrap bool
44
Lorenz Brune6573102021-11-02 14:15:37 +010045// A PEM block type for a Metropolis initial owner private key
46const ownerKeyType = "METROPOLIS INITIAL OWNER PRIVATE KEY"
47
Lorenz Brun7a510192022-07-04 15:31:38 +000048//go:embed metropolis/installer/kernel.efi
49var installer []byte
50
Lorenz Brune6573102021-11-02 14:15:37 +010051func doGenUSB(cmd *cobra.Command, args []string) {
Lorenz Brun7a510192022-07-04 15:31:38 +000052 var bundleReader io.Reader
53 var bundleSize uint64
54 if bundlePath == nil || *bundlePath == "" {
55 // Attempt Bazel runfile bundle if not explicitly set
56 bundle, err := datafile.Get("metropolis/node/bundle.zip")
57 if err != nil {
58 log.Fatalf("No bundle specified and fallback to runfiles failed: %v", err)
59 }
60 bundleReader = bytes.NewReader(bundle)
61 bundleSize = uint64(len(bundle))
62 } else {
63 // Load bundle from specified path
64 bundle, err := os.Open(*bundlePath)
65 if err != nil {
66 log.Fatalf("Failed to open specified bundle: %v", err)
67 }
68 bundleStat, err := bundle.Stat()
69 if err != nil {
70 log.Fatalf("Failed to stat specified bundle: %v", err)
71 }
72 bundleReader = bundle
73 bundleSize = uint64(bundleStat.Size())
74 }
Lorenz Brune6573102021-11-02 14:15:37 +010075
Mateusz Zalegad5f2f7a2022-07-05 18:48:56 +020076 ctx := clicontext.WithInterrupt(context.Background())
77
Lorenz Brune6573102021-11-02 14:15:37 +010078 // TODO(lorenz): Have a key management story for this
79 if err := os.MkdirAll(filepath.Join(xdg.ConfigHome, "metroctl"), 0700); err != nil {
80 log.Fatalf("Failed to create config directory: %v", err)
81 }
Lorenz Brune6573102021-11-02 14:15:37 +010082
Mateusz Zalegad5f2f7a2022-07-05 18:48:56 +020083 var params *api.NodeParameters
84 if bootstrap {
85 var ownerPublicKey ed25519.PublicKey
86 ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "metroctl/owner-key.pem"))
87 if os.IsNotExist(err) {
88 pub, priv, err := ed25519.GenerateKey(rand.Reader)
89 if err != nil {
90 log.Fatalf("Failed to generate owner private key: %v", err)
91 }
92 pemPriv := pem.EncodeToMemory(&pem.Block{Type: ownerKeyType, Bytes: priv})
93 if err := os.WriteFile(filepath.Join(xdg.ConfigHome, "metroctl/owner-key.pem"), pemPriv, 0600); err != nil {
94 log.Fatalf("Failed to store owner private key: %v", err)
95 }
96 ownerPublicKey = pub
97 } else if err != nil {
98 log.Fatalf("Failed to load owner private key: %v", err)
99 } else {
100 block, _ := pem.Decode(ownerPrivateKeyPEM)
101 if block == nil {
102 log.Fatalf("owner-key.pem contains invalid PEM")
103 }
104 if block.Type != ownerKeyType {
105 log.Fatalf("owner-key.pem contains a PEM block that's not a %v", ownerKeyType)
106 }
107 if len(block.Bytes) != ed25519.PrivateKeySize {
108 log.Fatal("owner-key.pem contains non-Ed25519 key")
109 }
110 ownerPrivateKey := ed25519.PrivateKey(block.Bytes)
111 ownerPublicKey = ownerPrivateKey.Public().(ed25519.PublicKey)
112 }
113
114 params = &api.NodeParameters{
115 Cluster: &api.NodeParameters_ClusterBootstrap_{
116 ClusterBootstrap: &api.NodeParameters_ClusterBootstrap{
117 OwnerPublicKey: ownerPublicKey,
118 },
Lorenz Brune6573102021-11-02 14:15:37 +0100119 },
Mateusz Zalegad5f2f7a2022-07-05 18:48:56 +0200120 }
121 } else {
122 ocert, opkey, err := getCredentials()
123 if err == noCredentialsError {
124 log.Fatalf("In order to create a non-bootstrap node installer, you have to take ownership of the cluster first: %v", err)
125 }
126 if err != nil {
127 log.Fatalf("While retrieving owner credentials: %v", err)
128 }
129 if len(flags.clusterEndpoints) == 0 {
130 log.Fatal("At least one cluster endpoint is required while generating non-bootstrap installer images.")
131 }
Mateusz Zalegaf7774962022-07-08 12:26:55 +0200132 cc, err := dialCluster(ctx, opkey, ocert, flags.proxyAddr, flags.clusterEndpoints)
Mateusz Zalegad5f2f7a2022-07-05 18:48:56 +0200133 if err != nil {
134 log.Fatalf("While dialing the cluster: %v", err)
135 }
136 mgmt := api.NewManagementClient(cc)
137 resT, err := mgmt.GetRegisterTicket(ctx, &api.GetRegisterTicketRequest{})
138 if err != nil {
139 log.Fatalf("While receiving register ticket: %v", err)
140 }
141 resI, err := mgmt.GetClusterInfo(ctx, &api.GetClusterInfoRequest{})
142 if err != nil {
143 log.Fatalf("While receiving cluster directory: %v", err)
144 }
145
146 params = &api.NodeParameters{
147 Cluster: &api.NodeParameters_ClusterRegister_{
148 ClusterRegister: &api.NodeParameters_ClusterRegister{
149 RegisterTicket: resT.Ticket,
150 ClusterDirectory: resI.ClusterDirectory,
151 CaCertificate: resI.CaCertificate,
152 },
153 },
154 }
Lorenz Brune6573102021-11-02 14:15:37 +0100155 }
156
157 installerImageArgs := core.MakeInstallerImageArgs{
158 TargetPath: args[0],
Lorenz Brun7a510192022-07-04 15:31:38 +0000159 Installer: bytes.NewReader(installer),
Serge Bazanski97783222021-12-14 16:04:26 +0100160 InstallerSize: uint64(len(installer)),
Lorenz Brune6573102021-11-02 14:15:37 +0100161 NodeParams: params,
Lorenz Brun7a510192022-07-04 15:31:38 +0000162 Bundle: bundleReader,
163 BundleSize: bundleSize,
Lorenz Brune6573102021-11-02 14:15:37 +0100164 }
165
Serge Bazanski97783222021-12-14 16:04:26 +0100166 log.Printf("Generating installer image (this can take a while, see issues/92).")
Lorenz Brune6573102021-11-02 14:15:37 +0100167 if err := core.MakeInstallerImage(installerImageArgs); err != nil {
168 log.Fatalf("Failed to create installer: %v", err)
169 }
170}
171
172func init() {
173 rootCmd.AddCommand(installCmd)
Mateusz Zalegad5f2f7a2022-07-05 18:48:56 +0200174
175 genusbCmd.Flags().BoolVar(&bootstrap, "bootstrap", false, "Create a bootstrap installer image.")
Lorenz Brune6573102021-11-02 14:15:37 +0100176 installCmd.AddCommand(genusbCmd)
Lorenz Brune6573102021-11-02 14:15:37 +0100177}