blob: 07faad4630cc846ec3c05d4f37390c22ab0665eb [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
Lorenz Brune6573102021-11-02 14:15:37 +010015 "github.com/spf13/cobra"
16
17 "source.monogon.dev/metropolis/cli/metroctl/core"
Mateusz Zalegad5f2f7a2022-07-05 18:48:56 +020018 clicontext "source.monogon.dev/metropolis/cli/pkg/context"
Serge Bazanski97783222021-12-14 16:04:26 +010019 "source.monogon.dev/metropolis/cli/pkg/datafile"
Lorenz Brune6573102021-11-02 14:15:37 +010020 "source.monogon.dev/metropolis/proto/api"
21)
22
23var installCmd = &cobra.Command{
Lorenz Brun7a510192022-07-04 15:31:38 +000024 Short: "Contains subcommands to install Metropolis via different media.",
Lorenz Brune6573102021-11-02 14:15:37 +010025 Use: "install",
26}
27
Lorenz Brun7a510192022-07-04 15:31:38 +000028var bundlePath = installCmd.PersistentFlags().StringP("bundle", "b", "", "Path to the Metropolis bundle to be installed")
29
Lorenz Brune6573102021-11-02 14:15:37 +010030var genusbCmd = &cobra.Command{
Serge Bazanski97783222021-12-14 16:04:26 +010031 Use: "genusb target",
Lorenz Brune6573102021-11-02 14:15:37 +010032 Short: "Generates a Metropolis installer disk or image.",
Lorenz Brun7a510192022-07-04 15:31:38 +000033 Example: "metroctl install --bundle=metropolis-v0.1.zip genusb /dev/sdx",
Lorenz Brune6573102021-11-02 14:15:37 +010034 Args: cobra.ExactArgs(1), // One positional argument: the target
35 Run: doGenUSB,
36}
37
Mateusz Zalegad5f2f7a2022-07-05 18:48:56 +020038// bootstrap is a flag controlling node parameters included in the installer
39// image. If set, the installed node will bootstrap a new cluster. Otherwise,
40// it will try to connect to the cluster which endpoints were provided with
41// the --endpoints flag.
42var bootstrap bool
43
Lorenz Brune6573102021-11-02 14:15:37 +010044// A PEM block type for a Metropolis initial owner private key
45const ownerKeyType = "METROPOLIS INITIAL OWNER PRIVATE KEY"
46
Lorenz Brun7a510192022-07-04 15:31:38 +000047//go:embed metropolis/installer/kernel.efi
48var installer []byte
49
Lorenz Brune6573102021-11-02 14:15:37 +010050func doGenUSB(cmd *cobra.Command, args []string) {
Lorenz Brun7a510192022-07-04 15:31:38 +000051 var bundleReader io.Reader
52 var bundleSize uint64
53 if bundlePath == nil || *bundlePath == "" {
54 // Attempt Bazel runfile bundle if not explicitly set
55 bundle, err := datafile.Get("metropolis/node/bundle.zip")
56 if err != nil {
57 log.Fatalf("No bundle specified and fallback to runfiles failed: %v", err)
58 }
59 bundleReader = bytes.NewReader(bundle)
60 bundleSize = uint64(len(bundle))
61 } else {
62 // Load bundle from specified path
63 bundle, err := os.Open(*bundlePath)
64 if err != nil {
65 log.Fatalf("Failed to open specified bundle: %v", err)
66 }
67 bundleStat, err := bundle.Stat()
68 if err != nil {
69 log.Fatalf("Failed to stat specified bundle: %v", err)
70 }
71 bundleReader = bundle
72 bundleSize = uint64(bundleStat.Size())
73 }
Lorenz Brune6573102021-11-02 14:15:37 +010074
Mateusz Zalegad5f2f7a2022-07-05 18:48:56 +020075 ctx := clicontext.WithInterrupt(context.Background())
76
Lorenz Brune6573102021-11-02 14:15:37 +010077 // TODO(lorenz): Have a key management story for this
Mateusz Zalega8234c162022-07-08 17:05:50 +020078 if err := os.MkdirAll(flags.configPath, 0700); err != nil && !os.IsExist(err) {
Lorenz Brune6573102021-11-02 14:15:37 +010079 log.Fatalf("Failed to create config directory: %v", err)
80 }
Lorenz Brune6573102021-11-02 14:15:37 +010081
Mateusz Zalegad5f2f7a2022-07-05 18:48:56 +020082 var params *api.NodeParameters
83 if bootstrap {
84 var ownerPublicKey ed25519.PublicKey
Mateusz Zalega8234c162022-07-08 17:05:50 +020085 ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(flags.configPath, "owner-key.pem"))
Mateusz Zalegad5f2f7a2022-07-05 18:48:56 +020086 if os.IsNotExist(err) {
87 pub, priv, err := ed25519.GenerateKey(rand.Reader)
88 if err != nil {
89 log.Fatalf("Failed to generate owner private key: %v", err)
90 }
91 pemPriv := pem.EncodeToMemory(&pem.Block{Type: ownerKeyType, Bytes: priv})
Mateusz Zalega8234c162022-07-08 17:05:50 +020092 if err := os.WriteFile(filepath.Join(flags.configPath, "owner-key.pem"), pemPriv, 0600); err != nil {
Mateusz Zalegad5f2f7a2022-07-05 18:48:56 +020093 log.Fatalf("Failed to store owner private key: %v", err)
94 }
95 ownerPublicKey = pub
96 } else if err != nil {
97 log.Fatalf("Failed to load owner private key: %v", err)
98 } else {
99 block, _ := pem.Decode(ownerPrivateKeyPEM)
100 if block == nil {
101 log.Fatalf("owner-key.pem contains invalid PEM")
102 }
103 if block.Type != ownerKeyType {
104 log.Fatalf("owner-key.pem contains a PEM block that's not a %v", ownerKeyType)
105 }
106 if len(block.Bytes) != ed25519.PrivateKeySize {
107 log.Fatal("owner-key.pem contains non-Ed25519 key")
108 }
109 ownerPrivateKey := ed25519.PrivateKey(block.Bytes)
110 ownerPublicKey = ownerPrivateKey.Public().(ed25519.PublicKey)
111 }
112
113 params = &api.NodeParameters{
114 Cluster: &api.NodeParameters_ClusterBootstrap_{
115 ClusterBootstrap: &api.NodeParameters_ClusterBootstrap{
116 OwnerPublicKey: ownerPublicKey,
117 },
Lorenz Brune6573102021-11-02 14:15:37 +0100118 },
Mateusz Zalegad5f2f7a2022-07-05 18:48:56 +0200119 }
120 } else {
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200121 cc := dialAuthenticated(ctx)
Mateusz Zalegad5f2f7a2022-07-05 18:48:56 +0200122 mgmt := api.NewManagementClient(cc)
123 resT, err := mgmt.GetRegisterTicket(ctx, &api.GetRegisterTicketRequest{})
124 if err != nil {
125 log.Fatalf("While receiving register ticket: %v", err)
126 }
127 resI, err := mgmt.GetClusterInfo(ctx, &api.GetClusterInfoRequest{})
128 if err != nil {
129 log.Fatalf("While receiving cluster directory: %v", err)
130 }
131
132 params = &api.NodeParameters{
133 Cluster: &api.NodeParameters_ClusterRegister_{
134 ClusterRegister: &api.NodeParameters_ClusterRegister{
135 RegisterTicket: resT.Ticket,
136 ClusterDirectory: resI.ClusterDirectory,
137 CaCertificate: resI.CaCertificate,
138 },
139 },
140 }
Lorenz Brune6573102021-11-02 14:15:37 +0100141 }
142
143 installerImageArgs := core.MakeInstallerImageArgs{
144 TargetPath: args[0],
Lorenz Brun7a510192022-07-04 15:31:38 +0000145 Installer: bytes.NewReader(installer),
Serge Bazanski97783222021-12-14 16:04:26 +0100146 InstallerSize: uint64(len(installer)),
Lorenz Brune6573102021-11-02 14:15:37 +0100147 NodeParams: params,
Lorenz Brun7a510192022-07-04 15:31:38 +0000148 Bundle: bundleReader,
149 BundleSize: bundleSize,
Lorenz Brune6573102021-11-02 14:15:37 +0100150 }
151
Serge Bazanski97783222021-12-14 16:04:26 +0100152 log.Printf("Generating installer image (this can take a while, see issues/92).")
Lorenz Brune6573102021-11-02 14:15:37 +0100153 if err := core.MakeInstallerImage(installerImageArgs); err != nil {
154 log.Fatalf("Failed to create installer: %v", err)
155 }
156}
157
158func init() {
159 rootCmd.AddCommand(installCmd)
Mateusz Zalegad5f2f7a2022-07-05 18:48:56 +0200160
161 genusbCmd.Flags().BoolVar(&bootstrap, "bootstrap", false, "Create a bootstrap installer image.")
Lorenz Brune6573102021-11-02 14:15:37 +0100162 installCmd.AddCommand(genusbCmd)
Lorenz Brune6573102021-11-02 14:15:37 +0100163}