| package main |
| |
| import ( |
| "bytes" |
| "context" |
| "crypto/ed25519" |
| "crypto/rand" |
| _ "embed" |
| "encoding/pem" |
| "io" |
| "log" |
| "os" |
| "path/filepath" |
| |
| "github.com/adrg/xdg" |
| "github.com/spf13/cobra" |
| |
| "source.monogon.dev/metropolis/cli/metroctl/core" |
| clicontext "source.monogon.dev/metropolis/cli/pkg/context" |
| "source.monogon.dev/metropolis/cli/pkg/datafile" |
| "source.monogon.dev/metropolis/proto/api" |
| ) |
| |
| var installCmd = &cobra.Command{ |
| Short: "Contains subcommands to install Metropolis via different media.", |
| Use: "install", |
| } |
| |
| var bundlePath = installCmd.PersistentFlags().StringP("bundle", "b", "", "Path to the Metropolis bundle to be installed") |
| |
| var genusbCmd = &cobra.Command{ |
| Use: "genusb target", |
| Short: "Generates a Metropolis installer disk or image.", |
| Example: "metroctl install --bundle=metropolis-v0.1.zip genusb /dev/sdx", |
| Args: cobra.ExactArgs(1), // One positional argument: the target |
| Run: doGenUSB, |
| } |
| |
| // bootstrap is a flag controlling node parameters included in the installer |
| // image. If set, the installed node will bootstrap a new cluster. Otherwise, |
| // it will try to connect to the cluster which endpoints were provided with |
| // the --endpoints flag. |
| var bootstrap bool |
| |
| // A PEM block type for a Metropolis initial owner private key |
| const ownerKeyType = "METROPOLIS INITIAL OWNER PRIVATE KEY" |
| |
| //go:embed metropolis/installer/kernel.efi |
| var installer []byte |
| |
| func doGenUSB(cmd *cobra.Command, args []string) { |
| var bundleReader io.Reader |
| var bundleSize uint64 |
| if bundlePath == nil || *bundlePath == "" { |
| // Attempt Bazel runfile bundle if not explicitly set |
| bundle, err := datafile.Get("metropolis/node/bundle.zip") |
| if err != nil { |
| log.Fatalf("No bundle specified and fallback to runfiles failed: %v", err) |
| } |
| bundleReader = bytes.NewReader(bundle) |
| bundleSize = uint64(len(bundle)) |
| } else { |
| // Load bundle from specified path |
| bundle, err := os.Open(*bundlePath) |
| if err != nil { |
| log.Fatalf("Failed to open specified bundle: %v", err) |
| } |
| bundleStat, err := bundle.Stat() |
| if err != nil { |
| log.Fatalf("Failed to stat specified bundle: %v", err) |
| } |
| bundleReader = bundle |
| bundleSize = uint64(bundleStat.Size()) |
| } |
| |
| ctx := clicontext.WithInterrupt(context.Background()) |
| |
| // 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 params *api.NodeParameters |
| if bootstrap { |
| 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) |
| } |
| |
| params = &api.NodeParameters{ |
| Cluster: &api.NodeParameters_ClusterBootstrap_{ |
| ClusterBootstrap: &api.NodeParameters_ClusterBootstrap{ |
| OwnerPublicKey: ownerPublicKey, |
| }, |
| }, |
| } |
| } else { |
| ocert, opkey, err := getCredentials() |
| if err == noCredentialsError { |
| log.Fatalf("In order to create a non-bootstrap node installer, you have to take ownership of the cluster first: %v", err) |
| } |
| if err != nil { |
| log.Fatalf("While retrieving owner credentials: %v", err) |
| } |
| if len(flags.clusterEndpoints) == 0 { |
| log.Fatal("At least one cluster endpoint is required while generating non-bootstrap installer images.") |
| } |
| cc, err := dialCluster(ctx, opkey, ocert, "", flags.clusterEndpoints) |
| if err != nil { |
| log.Fatalf("While dialing the cluster: %v", err) |
| } |
| mgmt := api.NewManagementClient(cc) |
| resT, err := mgmt.GetRegisterTicket(ctx, &api.GetRegisterTicketRequest{}) |
| if err != nil { |
| log.Fatalf("While receiving register ticket: %v", err) |
| } |
| resI, err := mgmt.GetClusterInfo(ctx, &api.GetClusterInfoRequest{}) |
| if err != nil { |
| log.Fatalf("While receiving cluster directory: %v", err) |
| } |
| |
| params = &api.NodeParameters{ |
| Cluster: &api.NodeParameters_ClusterRegister_{ |
| ClusterRegister: &api.NodeParameters_ClusterRegister{ |
| RegisterTicket: resT.Ticket, |
| ClusterDirectory: resI.ClusterDirectory, |
| CaCertificate: resI.CaCertificate, |
| }, |
| }, |
| } |
| } |
| |
| installerImageArgs := core.MakeInstallerImageArgs{ |
| TargetPath: args[0], |
| Installer: bytes.NewReader(installer), |
| InstallerSize: uint64(len(installer)), |
| NodeParams: params, |
| Bundle: bundleReader, |
| BundleSize: bundleSize, |
| } |
| |
| 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) |
| |
| genusbCmd.Flags().BoolVar(&bootstrap, "bootstrap", false, "Create a bootstrap installer image.") |
| installCmd.AddCommand(genusbCmd) |
| } |