blob: 1e4afa593c07d874e33469c89d9b0636a83deda2 [file] [log] [blame]
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +01001package main
2
3import (
4 "context"
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +02005 "crypto/x509"
6 "fmt"
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +01007 "io"
8 "log"
9 "os"
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +020010 "strings"
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010011
12 "github.com/spf13/cobra"
13
Serge Bazanskie0c06172023-09-19 12:28:16 +000014 "source.monogon.dev/go/clitable"
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010015 "source.monogon.dev/metropolis/cli/metroctl/core"
16 clicontext "source.monogon.dev/metropolis/cli/pkg/context"
17 "source.monogon.dev/metropolis/node/core/identity"
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +020018
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010019 apb "source.monogon.dev/metropolis/proto/api"
20)
21
22var nodeCmd = &cobra.Command{
23 Short: "Updates and queries node information.",
24 Use: "node",
25}
26
27var nodeDescribeCmd = &cobra.Command{
28 Short: "Describes cluster nodes.",
29 Use: "describe [node-id] [--filter] [--output] [--format]",
30 Example: "metroctl node describe metropolis-c556e31c3fa2bf0a36e9ccb9fd5d6056",
31 Run: func(cmd *cobra.Command, args []string) {
32 ctx := clicontext.WithInterrupt(context.Background())
33 cc := dialAuthenticated(ctx)
34 mgmt := apb.NewManagementClient(cc)
35
36 nodes, err := core.GetNodes(ctx, mgmt, flags.filter)
37 if err != nil {
38 log.Fatalf("While calling Management.GetNodes: %v", err)
39 }
40
41 printNodes(nodes, args, nil)
42 },
43 Args: cobra.ArbitraryArgs,
44}
45
46var nodeListCmd = &cobra.Command{
47 Short: "Lists cluster nodes.",
48 Use: "list [node-id] [--filter] [--output] [--format]",
49 Example: "metroctl node list --filter node.status.external_address==\"10.8.0.2\"",
50 Run: func(cmd *cobra.Command, args []string) {
51 ctx := clicontext.WithInterrupt(context.Background())
52 cc := dialAuthenticated(ctx)
53 mgmt := apb.NewManagementClient(cc)
54
55 nodes, err := core.GetNodes(ctx, mgmt, flags.filter)
56 if err != nil {
57 log.Fatalf("While calling Management.GetNodes: %v", err)
58 }
59
60 printNodes(nodes, args, map[string]bool{"node id": true})
61 },
62 Args: cobra.ArbitraryArgs,
63}
64
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +020065var nodeUpdateCmd = &cobra.Command{
66 Short: "Updates the operating system of a cluster node.",
67 Use: "update [NodeID] --bundle-url bundleURL [--activation-mode <none|reboot|kexec>]",
68 Example: "metroctl node update --bundle-url https://example.com/bundle.zip --activation-mode reboot metropolis-25fa5f5e9349381d4a5e9e59de0215e3",
69 RunE: func(cmd *cobra.Command, args []string) error {
70 bundleUrl, err := cmd.Flags().GetString("bundle-url")
71 if err != nil {
72 return err
73 }
74
75 if len(bundleUrl) == 0 {
76 return fmt.Errorf("flag bundle-url is required")
77 }
78
79 activationMode, err := cmd.Flags().GetString("activation-mode")
80 if err != nil {
81 return err
82 }
83
84 var am apb.ActivationMode
85 switch strings.ToLower(activationMode) {
86 case "none":
87 am = apb.ActivationMode_ACTIVATION_NONE
88 case "reboot":
89 am = apb.ActivationMode_ACTIVATION_REBOOT
90 case "kexec":
91 am = apb.ActivationMode_ACTIVATION_KEXEC
92 default:
93 return fmt.Errorf("invalid value for flag activation-mode")
94 }
95
96 ctx := clicontext.WithInterrupt(context.Background())
97 mgmt := apb.NewManagementClient(dialAuthenticated(ctx))
98
99 // TODO(q3k): save CA certificate on takeover
100 info, err := mgmt.GetClusterInfo(ctx, &apb.GetClusterInfoRequest{})
101 if err != nil {
102 return fmt.Errorf("couldn't get cluster info: %w", err)
103 }
104 cacert, err := x509.ParseCertificate(info.CaCertificate)
105 if err != nil {
106 return fmt.Errorf("remote CA certificate invalid: %w", err)
107 }
108
109 nodes, err := core.GetNodes(ctx, mgmt, "")
110 if err != nil {
111 return fmt.Errorf("while calling Management.GetNodes: %v", err)
112 }
113 // Narrow down the output set to supplied node IDs, if any.
114 qids := make(map[string]bool)
115 if len(args) != 0 && args[0] != "all" {
116 for _, a := range args {
117 qids[a] = true
118 }
119 }
120
121 updateReq := &apb.UpdateNodeRequest{
122 BundleUrl: bundleUrl,
123 ActivationMode: am,
124 }
125
126 for _, n := range nodes {
127 // Filter the information we want client-side.
128 if len(qids) != 0 {
129 nid := identity.NodeID(n.Pubkey)
130 if _, e := qids[nid]; !e {
131 continue
132 }
133 }
134
135 cc := dialAuthenticatedNode(ctx, n.Id, n.Status.ExternalAddress, cacert)
136 nodeMgmt := apb.NewNodeManagementClient(cc)
137 log.Printf("sending update request to: %s (%s)", n.Id, n.Status.ExternalAddress)
138 _, err := nodeMgmt.UpdateNode(ctx, updateReq)
139 if err != nil {
140 return err
141 }
142 }
143
144 return nil
145 },
146 Args: cobra.ExactArgs(1),
147}
148
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100149func init() {
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200150 nodeUpdateCmd.Flags().String("bundle-url", "", "The URL to the new version")
151 nodeUpdateCmd.Flags().String("activation-mode", "reboot", "How the update should be activated (kexec, reboot, none)")
152
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100153 nodeCmd.AddCommand(nodeDescribeCmd)
154 nodeCmd.AddCommand(nodeListCmd)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200155 nodeCmd.AddCommand(nodeUpdateCmd)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100156 rootCmd.AddCommand(nodeCmd)
157}
158
159func printNodes(nodes []*apb.Node, args []string, onlyColumns map[string]bool) {
160 o := io.WriteCloser(os.Stdout)
161 if flags.output != "" {
162 of, err := os.Create(flags.output)
163 if err != nil {
164 log.Fatalf("Couldn't create the output file at %s: %v", flags.output, err)
165 }
166 o = of
167 }
168
169 // Narrow down the output set to supplied node IDs, if any.
170 qids := make(map[string]bool)
171 if len(args) != 0 && args[0] != "all" {
172 for _, a := range args {
173 qids[a] = true
174 }
175 }
176
Serge Bazanskie0c06172023-09-19 12:28:16 +0000177 var t clitable.Table
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100178 for _, n := range nodes {
179 // Filter the information we want client-side.
180 if len(qids) != 0 {
181 nid := identity.NodeID(n.Pubkey)
182 if _, e := qids[nid]; !e {
183 continue
184 }
185 }
Serge Bazanskie0c06172023-09-19 12:28:16 +0000186 t.Add(nodeEntry(n))
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100187 }
188
Serge Bazanskie0c06172023-09-19 12:28:16 +0000189 t.Print(o, onlyColumns)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100190}