blob: a2946bf1c6a3732c0220693fc20149400d68df86 [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
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100149var nodeDeleteCmd = &cobra.Command{
150 Short: "Deletes a node from the cluster.",
151 Use: "delete [NodeID] [--bypass-has-roles] [--bypass-not-decommissioned]",
152 Example: "metroctl node delete metropolis-25fa5f5e9349381d4a5e9e59de0215e3",
153 RunE: func(cmd *cobra.Command, args []string) error {
154 bypassHasRoles, err := cmd.Flags().GetBool("bypass-has-roles")
155 if err != nil {
156 return err
157 }
158
159 bypassNotDecommissioned, err := cmd.Flags().GetBool("bypass-not-decommissioned")
160 if err != nil {
161 return err
162 }
163
164 ctx := clicontext.WithInterrupt(context.Background())
165 mgmt := apb.NewManagementClient(dialAuthenticated(ctx))
166
167 nodes, err := core.GetNodes(ctx, mgmt, fmt.Sprintf("node.id==%q", args[0]))
168 if err != nil {
169 return fmt.Errorf("while calling Management.GetNodes: %v", err)
170 }
171
172 if len(nodes) == 0 {
173 return fmt.Errorf("could not find node with id: %s", args[0])
174 }
175
176 if len(nodes) != 1 {
177 return fmt.Errorf("expected one node, got %d", len(nodes))
178 }
179
180 n := nodes[0]
181 log.Printf("deleting node: %s (%s)", n.Id, n.Status.ExternalAddress)
182
183 req := &apb.DeleteNodeRequest{
184 Node: &apb.DeleteNodeRequest_Id{
185 Id: n.Id,
186 },
187 }
188
189 if bypassHasRoles {
190 req.SafetyBypassHasRoles = &apb.DeleteNodeRequest_SafetyBypassHasRoles{}
191 }
192
193 if bypassNotDecommissioned {
194 req.SafetyBypassNotDecommissioned = &apb.DeleteNodeRequest_SafetyBypassNotDecommissioned{}
195 }
196
197 _, err = mgmt.DeleteNode(ctx, req)
198 return err
199 },
200 Args: cobra.ExactArgs(1),
201}
202
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100203func init() {
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200204 nodeUpdateCmd.Flags().String("bundle-url", "", "The URL to the new version")
205 nodeUpdateCmd.Flags().String("activation-mode", "reboot", "How the update should be activated (kexec, reboot, none)")
206
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100207 nodeDeleteCmd.Flags().Bool("bypass-has-roles", false, "Allows to bypass the HasRoles check")
208 nodeDeleteCmd.Flags().Bool("bypass-not-decommissioned", false, "Allows to bypass the NotDecommissioned check")
209
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100210 nodeCmd.AddCommand(nodeDescribeCmd)
211 nodeCmd.AddCommand(nodeListCmd)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200212 nodeCmd.AddCommand(nodeUpdateCmd)
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100213 nodeCmd.AddCommand(nodeDeleteCmd)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100214 rootCmd.AddCommand(nodeCmd)
215}
216
217func printNodes(nodes []*apb.Node, args []string, onlyColumns map[string]bool) {
218 o := io.WriteCloser(os.Stdout)
219 if flags.output != "" {
220 of, err := os.Create(flags.output)
221 if err != nil {
222 log.Fatalf("Couldn't create the output file at %s: %v", flags.output, err)
223 }
224 o = of
225 }
226
227 // Narrow down the output set to supplied node IDs, if any.
228 qids := make(map[string]bool)
229 if len(args) != 0 && args[0] != "all" {
230 for _, a := range args {
231 qids[a] = true
232 }
233 }
234
Serge Bazanskie0c06172023-09-19 12:28:16 +0000235 var t clitable.Table
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100236 for _, n := range nodes {
237 // Filter the information we want client-side.
238 if len(qids) != 0 {
239 nid := identity.NodeID(n.Pubkey)
240 if _, e := qids[nid]; !e {
241 continue
242 }
243 }
Serge Bazanskie0c06172023-09-19 12:28:16 +0000244 t.Add(nodeEntry(n))
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100245 }
246
Serge Bazanskie0c06172023-09-19 12:28:16 +0000247 t.Print(o, onlyColumns)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100248}