m/c/metroctl: add takeownership command
This add a simple command to take ownership of a cluster previously
installed using metroctl install. It calls the newly-formed cluster and
retrieves a signed owner certificate for the owner key and stores that
to disk for further use by metroctl.
Change-Id: Ibd2771c571bda41270c3bbb110105f4f8f5b118d
Reviewed-on: https://review.monogon.dev/c/monogon/+/463
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/metropolis/cli/metroctl/BUILD.bazel b/metropolis/cli/metroctl/BUILD.bazel
index eb09e96..6a311c8 100644
--- a/metropolis/cli/metroctl/BUILD.bazel
+++ b/metropolis/cli/metroctl/BUILD.bazel
@@ -5,6 +5,7 @@
srcs = [
"install.go",
"main.go",
+ "takeownership.go",
],
data = [
"//metropolis/node",
@@ -14,6 +15,9 @@
visibility = ["//visibility:private"],
deps = [
"//metropolis/cli/metroctl/core:go_default_library",
+ "//metropolis/cli/pkg/context:go_default_library",
+ "//metropolis/node:go_default_library",
+ "//metropolis/node/core/rpc:go_default_library",
"//metropolis/proto/api:go_default_library",
"@com_github_adrg_xdg//:go_default_library",
"@com_github_spf13_cobra//:go_default_library",
diff --git a/metropolis/cli/metroctl/takeownership.go b/metropolis/cli/metroctl/takeownership.go
new file mode 100644
index 0000000..a264644
--- /dev/null
+++ b/metropolis/cli/metroctl/takeownership.go
@@ -0,0 +1,76 @@
+package main
+
+import (
+ "context"
+ "crypto/ed25519"
+ "encoding/pem"
+ "log"
+ "net"
+ "os"
+ "path/filepath"
+
+ "github.com/adrg/xdg"
+ "github.com/spf13/cobra"
+
+ apb "source.monogon.dev/metropolis/proto/api"
+
+ clicontext "source.monogon.dev/metropolis/cli/pkg/context"
+ "source.monogon.dev/metropolis/node"
+ "source.monogon.dev/metropolis/node/core/rpc"
+)
+
+var takeownershipCommand = &cobra.Command{
+ Use: "takeownership <node-addr>",
+ Short: "Takes ownership of a new Metropolis cluster",
+ Long: `This takes ownership of a new Metropolis cluster by asking the new
+cluster to issue an owner certificate to for the owner key generated by a
+previous invocation of metroctl install on this machine.`,
+ Example: "takeownership 192.0.2.1",
+ Args: cobra.ExactArgs(1), // One positional argument: the node address
+ Run: doTakeOwnership,
+}
+
+func doTakeOwnership(cmd *cobra.Command, args []string) {
+ ctx := clicontext.WithInterrupt(context.Background())
+ ownerPrivateKeyPEM, err := os.ReadFile(filepath.Join(xdg.ConfigHome, "metroctl/owner-key.pem"))
+ if os.IsNotExist(err) {
+ log.Fatalf("Owner key does not exist. takeownership needs to be executed on the same system that has previously installed the cluster using metroctl install.")
+ } else if err != nil {
+ log.Fatalf("Failed to load owner private key: %v", err)
+ }
+ 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)
+
+ client, err := rpc.NewEphemeralClient(net.JoinHostPort(args[0], node.CuratorServicePort.PortString()), ownerPrivateKey, nil)
+ if err != nil {
+ log.Fatalf("Failed to create client to given node address: %v", err)
+ }
+ defer client.Close()
+ aaa := apb.NewAAAClient(client)
+ ownerCert, err := rpc.RetrieveOwnerCertificate(ctx, aaa, ownerPrivateKey)
+ if err != nil {
+ log.Fatalf("Failed to retrive owner certificate from cluster: %v", err)
+ }
+ ownerCertPEM := pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: ownerCert.Certificate[0],
+ }
+ if err := os.WriteFile(filepath.Join(xdg.ConfigHome, "metroctl/owner.pem"), pem.EncodeToMemory(&ownerCertPEM), 0644); err != nil {
+ log.Printf("Failed to store retrieved owner certificate: %v", err)
+ log.Fatalln("Sorry, the cluster has been lost as taking ownership cannot be repeated. Fix the reason the file couldn't be written and reinstall the node.")
+ }
+ log.Print("Successfully retrieved owner credentials! You now own this cluster.")
+}
+
+func init() {
+ rootCmd.AddCommand(takeownershipCommand)
+}