blob: 10fc9f590571bc68d21334e8818840a70f1c0f01 [file] [log] [blame]
Lorenz Brune6573102021-11-02 14:15:37 +01001package main
2
3import (
4 "crypto/ed25519"
5 "crypto/rand"
6 "encoding/pem"
Lorenz Brune6573102021-11-02 14:15:37 +01007 "log"
8 "os"
9 "path/filepath"
10
11 "github.com/adrg/xdg"
12 "github.com/spf13/cobra"
13
14 "source.monogon.dev/metropolis/cli/metroctl/core"
15 "source.monogon.dev/metropolis/proto/api"
16)
17
18var installCmd = &cobra.Command{
19 Short: "Contains subcommands to install Metropolis over different mediums.",
20 Use: "install",
21}
22
23// install flags
24var installer *string
25var bundle *string
26
27var genusbCmd = &cobra.Command{
28 Use: "genusb target --installer=inst.efi --bundle=bundle.bin",
29 Short: "Generates a Metropolis installer disk or image.",
30 Example: "metroctl install genusb /dev/sdx --installer=installer_x86_64.efi --bundle=metropolis_dev_x86_64.tar.xz",
31 Args: cobra.ExactArgs(1), // One positional argument: the target
32 Run: doGenUSB,
33}
34
Lorenz Brunf8ede092021-11-08 20:50:57 +010035// If useInTreeArtifacts is true metroctl should use a bundle and installer
36// directly from the build tree. It is automatically set to true if metroctl is
37// running under bazel run. Specifying either one manually still overrides
38// the in-tree artifacts.
39var useInTreeArtifacts = os.Getenv("BUILD_WORKSPACE_DIRECTORY") != ""
40
41var inTreeInstaller = "metropolis/node/installer/kernel.efi"
42var inTreeBundle = "metropolis/node/node.zip"
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
47func doGenUSB(cmd *cobra.Command, args []string) {
Lorenz Brunf8ede092021-11-08 20:50:57 +010048 if useInTreeArtifacts && *installer == "" {
49 installer = &inTreeInstaller
50 }
51 if useInTreeArtifacts && *bundle == "" {
52 bundle = &inTreeBundle
53 }
Lorenz Brune6573102021-11-02 14:15:37 +010054 installerFile, err := os.Open(*installer)
55 if err != nil {
56 log.Fatalf("Failed to open installer: %v", err)
57 }
58 installerFileStat, err := installerFile.Stat()
59 if err != nil {
60 log.Fatalf("Failed to stat installer: %v", err)
61 }
62 var bundleFile *os.File
63 var bundleFileStat os.FileInfo
64 if bundle != nil && *bundle != "" {
65 bundleFile, err = os.Open(*bundle)
66 if err != nil {
67 log.Fatalf("Failed to open bundle: %v", err)
68 }
69 bundleFileStat, err = bundleFile.Stat()
70 if err != nil {
71 log.Fatalf("Failed to stat bundle: %v", err)
72 }
73 }
74
75 // TODO(lorenz): Have a key management story for this
76 if err := os.MkdirAll(filepath.Join(xdg.ConfigHome, "metroctl"), 0700); err != nil {
77 log.Fatalf("Failed to create config directory: %v", err)
78 }
79 var ownerPublicKey ed25519.PublicKey
Lorenz Brun764a2de2021-11-22 16:26:36 +010080 ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "metroctl/owner-key.pem"))
Lorenz Brune6573102021-11-02 14:15:37 +010081 if os.IsNotExist(err) {
82 pub, priv, err := ed25519.GenerateKey(rand.Reader)
83 if err != nil {
84 log.Fatalf("Failed to generate owner private key: %v", err)
85 }
86 pemPriv := pem.EncodeToMemory(&pem.Block{Type: ownerKeyType, Bytes: priv})
Lorenz Brun764a2de2021-11-22 16:26:36 +010087 if err := os.WriteFile(filepath.Join(xdg.ConfigHome, "metroctl/owner-key.pem"), pemPriv, 0600); err != nil {
Lorenz Brune6573102021-11-02 14:15:37 +010088 log.Fatalf("Failed to store owner private key: %v", err)
89 }
90 ownerPublicKey = pub
91 } else if err != nil {
92 log.Fatalf("Failed to load owner private key: %v", err)
93 } else {
94 block, _ := pem.Decode(ownerPrivateKeyPEM)
95 if block == nil {
96 log.Fatalf("owner-key.pem contains invalid PEM")
97 }
98 if block.Type != ownerKeyType {
99 log.Fatalf("owner-key.pem contains a PEM block that's not a %v", ownerKeyType)
100 }
101 if len(block.Bytes) != ed25519.PrivateKeySize {
102 log.Fatal("owner-key.pem contains non-Ed25519 key")
103 }
104 ownerPrivateKey := ed25519.PrivateKey(block.Bytes)
105 ownerPublicKey = ownerPrivateKey.Public().(ed25519.PublicKey)
106 }
107
108 // TODO(lorenz): This can only bootstrap right now. As soon as @serge's role
109 // management has stabilized we can replace this with a proper
110 // implementation.
111 params := &api.NodeParameters{
112 Cluster: &api.NodeParameters_ClusterBootstrap_{
113 ClusterBootstrap: &api.NodeParameters_ClusterBootstrap{
114 OwnerPublicKey: ownerPublicKey,
115 },
116 },
117 }
118
119 installerImageArgs := core.MakeInstallerImageArgs{
120 TargetPath: args[0],
121 Installer: installerFile,
122 InstallerSize: uint64(installerFileStat.Size()),
123 NodeParams: params,
124 }
125
126 if bundleFile != nil {
127 installerImageArgs.Bundle = bundleFile
128 installerImageArgs.BundleSize = uint64(bundleFileStat.Size())
129 }
130
131 if err := core.MakeInstallerImage(installerImageArgs); err != nil {
132 log.Fatalf("Failed to create installer: %v", err)
133 }
134}
135
136func init() {
137 rootCmd.AddCommand(installCmd)
138 installCmd.AddCommand(genusbCmd)
139
140 bundle = installCmd.PersistentFlags().StringP("bundle", "b", "", "Metropolis bundle file to use")
141 installer = installCmd.PersistentFlags().StringP("installer", "i", "", "Metropolis installer file to use")
142}