blob: a875725505d4cad528c94edde4d9033703c9829e [file] [log] [blame]
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +01001package main
2
3import (
4 "context"
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +02005 "fmt"
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +01006 "io"
7 "log"
8 "os"
Tim Windelschmidtb765f242024-05-08 01:40:02 +02009 "os/signal"
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +020010 "strings"
Lorenz Brun9ce40712024-02-13 21:54:46 +010011 "sync"
12 "time"
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010013
14 "github.com/spf13/cobra"
Lorenz Brun9ce40712024-02-13 21:54:46 +010015 "golang.org/x/sync/semaphore"
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010016
Serge Bazanskie0c06172023-09-19 12:28:16 +000017 "source.monogon.dev/go/clitable"
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010018 "source.monogon.dev/metropolis/cli/metroctl/core"
Lorenz Brun9ce40712024-02-13 21:54:46 +010019 "source.monogon.dev/version"
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +020020
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010021 apb "source.monogon.dev/metropolis/proto/api"
22)
23
24var nodeCmd = &cobra.Command{
25 Short: "Updates and queries node information.",
26 Use: "node",
27}
28
29var nodeDescribeCmd = &cobra.Command{
30 Short: "Describes cluster nodes.",
Serge Bazanski98840342024-05-22 13:03:55 +020031 Use: "describe [node-id] [--filter] [--output] [--format] [--columns]",
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010032 Example: "metroctl node describe metropolis-c556e31c3fa2bf0a36e9ccb9fd5d6056",
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020033 RunE: func(cmd *cobra.Command, args []string) error {
Tim Windelschmidtb765f242024-05-08 01:40:02 +020034 ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020035 cc, err := dialAuthenticated(ctx)
36 if err != nil {
37 return fmt.Errorf("while dialing node: %w", err)
38 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010039 mgmt := apb.NewManagementClient(cc)
40
41 nodes, err := core.GetNodes(ctx, mgmt, flags.filter)
42 if err != nil {
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020043 return fmt.Errorf("while calling Management.GetNodes: %w", err)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010044 }
45
Serge Bazanski98840342024-05-22 13:03:55 +020046 var columns map[string]bool
47 if flags.columns != "" {
48 columns = make(map[string]bool)
49 for _, p := range strings.Split(flags.columns, ",") {
50 p = strings.ToLower(p)
51 p = strings.TrimSpace(p)
52 columns[p] = true
53 }
54 }
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020055
56 return printNodes(nodes, args, columns)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010057 },
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +020058 Args: PrintUsageOnWrongArgs(cobra.ArbitraryArgs),
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010059}
60
61var nodeListCmd = &cobra.Command{
62 Short: "Lists cluster nodes.",
63 Use: "list [node-id] [--filter] [--output] [--format]",
64 Example: "metroctl node list --filter node.status.external_address==\"10.8.0.2\"",
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020065 RunE: func(cmd *cobra.Command, args []string) error {
Tim Windelschmidtb765f242024-05-08 01:40:02 +020066 ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020067 cc, err := dialAuthenticated(ctx)
68 if err != nil {
69 return fmt.Errorf("while dialing node: %w", err)
70 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010071 mgmt := apb.NewManagementClient(cc)
72
73 nodes, err := core.GetNodes(ctx, mgmt, flags.filter)
74 if err != nil {
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020075 return fmt.Errorf("while calling Management.GetNodes: %w", err)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010076 }
77
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020078 return printNodes(nodes, args, map[string]bool{"node id": true})
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010079 },
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +020080 Args: PrintUsageOnWrongArgs(cobra.ArbitraryArgs),
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010081}
82
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +020083var nodeUpdateCmd = &cobra.Command{
84 Short: "Updates the operating system of a cluster node.",
Lorenz Brun9ce40712024-02-13 21:54:46 +010085 Use: "update [NodeIDs]",
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +020086 Example: "metroctl node update --bundle-url https://example.com/bundle.zip --activation-mode reboot metropolis-25fa5f5e9349381d4a5e9e59de0215e3",
87 RunE: func(cmd *cobra.Command, args []string) error {
88 bundleUrl, err := cmd.Flags().GetString("bundle-url")
89 if err != nil {
90 return err
91 }
92
93 if len(bundleUrl) == 0 {
94 return fmt.Errorf("flag bundle-url is required")
95 }
96
97 activationMode, err := cmd.Flags().GetString("activation-mode")
98 if err != nil {
99 return err
100 }
101
102 var am apb.ActivationMode
103 switch strings.ToLower(activationMode) {
104 case "none":
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100105 am = apb.ActivationMode_ACTIVATION_MODE_NONE
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200106 case "reboot":
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100107 am = apb.ActivationMode_ACTIVATION_MODE_REBOOT
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200108 case "kexec":
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100109 am = apb.ActivationMode_ACTIVATION_MODE_KEXEC
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200110 default:
111 return fmt.Errorf("invalid value for flag activation-mode")
112 }
113
Lorenz Brun9ce40712024-02-13 21:54:46 +0100114 maxUnavailable, err := cmd.Flags().GetUint64("max-unavailable")
115 if err != nil {
116 return err
117 }
118 if maxUnavailable == 0 {
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200119 return fmt.Errorf("unable to update notes with max-unavailable set to zero")
Lorenz Brun9ce40712024-02-13 21:54:46 +0100120 }
121 unavailableSemaphore := semaphore.NewWeighted(int64(maxUnavailable))
122
Tim Windelschmidtb765f242024-05-08 01:40:02 +0200123 ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200124
Serge Bazanskic51d47d2024-02-13 18:40:26 +0100125 cacert, err := core.GetClusterCAWithTOFU(ctx, connectOptions())
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200126 if err != nil {
Serge Bazanskic51d47d2024-02-13 18:40:26 +0100127 return fmt.Errorf("could not get CA certificate: %w", err)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200128 }
Serge Bazanskic51d47d2024-02-13 18:40:26 +0100129
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200130 conn, err := dialAuthenticated(ctx)
131 if err != nil {
132 return err
133 }
134 mgmt := apb.NewManagementClient(conn)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200135
136 nodes, err := core.GetNodes(ctx, mgmt, "")
137 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +0200138 return fmt.Errorf("while calling Management.GetNodes: %w", err)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200139 }
140 // Narrow down the output set to supplied node IDs, if any.
141 qids := make(map[string]bool)
142 if len(args) != 0 && args[0] != "all" {
143 for _, a := range args {
144 qids[a] = true
145 }
146 }
147
Lorenz Bruncceb6a32024-04-16 13:33:15 +0000148 excludedNodesSlice, err := cmd.Flags().GetStringArray("exclude")
149 if err != nil {
150 return err
151 }
152 excludedNodes := make(map[string]bool)
153 for _, n := range excludedNodesSlice {
154 excludedNodes[n] = true
155 }
156
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200157 updateReq := &apb.UpdateNodeRequest{
158 BundleUrl: bundleUrl,
159 ActivationMode: am,
160 }
161
Lorenz Brun9ce40712024-02-13 21:54:46 +0100162 var wg sync.WaitGroup
163
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200164 for _, n := range nodes {
165 // Filter the information we want client-side.
166 if len(qids) != 0 {
Jan Schär39d9c242024-09-24 13:49:55 +0200167 if _, e := qids[n.Id]; !e {
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200168 continue
169 }
170 }
Jan Schär39d9c242024-09-24 13:49:55 +0200171 if excludedNodes[n.Id] {
Lorenz Bruncceb6a32024-04-16 13:33:15 +0000172 continue
173 }
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200174
Lorenz Brun9ce40712024-02-13 21:54:46 +0100175 if err := unavailableSemaphore.Acquire(ctx, 1); err != nil {
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200176 return err
177 }
Lorenz Brun9ce40712024-02-13 21:54:46 +0100178 wg.Add(1)
179
Tim Windelschmidtb41b5482024-04-18 23:24:01 +0200180 go func(n *apb.Node) {
Lorenz Brun9ce40712024-02-13 21:54:46 +0100181 defer wg.Done()
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200182 cc, err := dialAuthenticatedNode(ctx, n.Id, n.Status.ExternalAddress, cacert)
183 if err != nil {
184 log.Fatalf("failed to dial node: %v", err)
185 }
Lorenz Brun9ce40712024-02-13 21:54:46 +0100186 nodeMgmt := apb.NewNodeManagementClient(cc)
187 log.Printf("sending update request to: %s (%s)", n.Id, n.Status.ExternalAddress)
188 start := time.Now()
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200189 _, err = nodeMgmt.UpdateNode(ctx, updateReq)
Lorenz Brun9ce40712024-02-13 21:54:46 +0100190 if err != nil {
191 log.Printf("update request to node %s failed: %v", n.Id, err)
192 // A failed UpdateNode does not mean that the node is now unavailable as it
193 // hasn't started activating yet.
194 unavailableSemaphore.Release(1)
195 }
196 // Wait for the internal activation sleep plus the heartbeat
197 // to make sure the node has missed one heartbeat (or is
198 // back up already).
199 time.Sleep((5 + 10) * time.Second)
200 for {
201 select {
202 case <-time.After(10 * time.Second):
203 nodes, err := core.GetNodes(ctx, mgmt, fmt.Sprintf("node.id == %q", n.Id))
204 if err != nil {
205 log.Printf("while getting node status for %s: %v", n.Id, err)
Lorenz Brun76612022024-03-05 19:20:36 +0100206 continue
Lorenz Brun9ce40712024-02-13 21:54:46 +0100207 }
208 if len(nodes) == 0 {
209 log.Printf("node status for %s returned no node", n.Id)
Lorenz Brun76612022024-03-05 19:20:36 +0100210 continue
Lorenz Brun9ce40712024-02-13 21:54:46 +0100211 }
212 if len(nodes) > 1 {
213 log.Printf("node status for %s returned too many nodes (%d)", n.Id, len(nodes))
Lorenz Brun76612022024-03-05 19:20:36 +0100214 continue
Lorenz Brun9ce40712024-02-13 21:54:46 +0100215 }
216 s := nodes[0]
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100217 if s.Health == apb.Node_HEALTH_HEALTHY {
Lorenz Brun9ce40712024-02-13 21:54:46 +0100218 if s.Status != nil && s.Status.Version != nil {
219 log.Printf("node %s updated in %v to version %s", s.Id, time.Since(start), version.Semver(s.Status.Version))
220 } else {
221 log.Printf("node %s updated in %v to unknown version", s.Id, time.Since(start))
222 }
223 unavailableSemaphore.Release(1)
224 return
225 }
226 case <-ctx.Done():
227 log.Printf("update to node %s incomplete", n.Id)
228 return
229 }
230 }
231 }(n)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200232 }
233
Lorenz Brun9ce40712024-02-13 21:54:46 +0100234 // Wait for all update processes to finish
235 wg.Wait()
236
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200237 return nil
238 },
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +0200239 Args: PrintUsageOnWrongArgs(cobra.MinimumNArgs(1)),
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200240}
241
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100242var nodeDeleteCmd = &cobra.Command{
243 Short: "Deletes a node from the cluster.",
244 Use: "delete [NodeID] [--bypass-has-roles] [--bypass-not-decommissioned]",
245 Example: "metroctl node delete metropolis-25fa5f5e9349381d4a5e9e59de0215e3",
246 RunE: func(cmd *cobra.Command, args []string) error {
247 bypassHasRoles, err := cmd.Flags().GetBool("bypass-has-roles")
248 if err != nil {
249 return err
250 }
251
252 bypassNotDecommissioned, err := cmd.Flags().GetBool("bypass-not-decommissioned")
253 if err != nil {
254 return err
255 }
256
Tim Windelschmidtb765f242024-05-08 01:40:02 +0200257 ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200258 conn, err := dialAuthenticated(ctx)
259 if err != nil {
260 return err
261 }
262 mgmt := apb.NewManagementClient(conn)
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100263
264 nodes, err := core.GetNodes(ctx, mgmt, fmt.Sprintf("node.id==%q", args[0]))
265 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +0200266 return fmt.Errorf("while calling Management.GetNodes: %w", err)
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100267 }
268
269 if len(nodes) == 0 {
270 return fmt.Errorf("could not find node with id: %s", args[0])
271 }
272
273 if len(nodes) != 1 {
274 return fmt.Errorf("expected one node, got %d", len(nodes))
275 }
276
277 n := nodes[0]
Lorenz Brun2542ef82024-08-20 13:33:02 +0200278 if n.Status != nil && n.Status.ExternalAddress != "" {
279 log.Printf("deleting node: %s (%s)", n.Id, n.Status.ExternalAddress)
280 } else {
281 log.Printf("deleting node: %s", n.Id)
282 }
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100283
284 req := &apb.DeleteNodeRequest{
285 Node: &apb.DeleteNodeRequest_Id{
286 Id: n.Id,
287 },
288 }
289
290 if bypassHasRoles {
291 req.SafetyBypassHasRoles = &apb.DeleteNodeRequest_SafetyBypassHasRoles{}
292 }
293
294 if bypassNotDecommissioned {
295 req.SafetyBypassNotDecommissioned = &apb.DeleteNodeRequest_SafetyBypassNotDecommissioned{}
296 }
297
298 _, err = mgmt.DeleteNode(ctx, req)
299 return err
300 },
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +0200301 Args: PrintUsageOnWrongArgs(cobra.ExactArgs(1)),
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100302}
303
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000304func dialNode(ctx context.Context, node string) (apb.NodeManagementClient, error) {
305 // First connect to the main management service and figure out the node's IP
306 // address.
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200307 cc, err := dialAuthenticated(ctx)
308 if err != nil {
309 return nil, fmt.Errorf("while dialing node: %w", err)
310 }
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000311 mgmt := apb.NewManagementClient(cc)
312 nodes, err := core.GetNodes(ctx, mgmt, fmt.Sprintf("node.id == %q", node))
313 if err != nil {
314 return nil, fmt.Errorf("when getting node info: %w", err)
315 }
316
317 if len(nodes) == 0 {
318 return nil, fmt.Errorf("no such node")
319 }
320 if len(nodes) > 1 {
321 return nil, fmt.Errorf("expression matched more than one node")
322 }
323 n := nodes[0]
324 if n.Status == nil || n.Status.ExternalAddress == "" {
325 return nil, fmt.Errorf("node has no external address")
326 }
327
328 cacert, err := core.GetClusterCAWithTOFU(ctx, connectOptions())
329 if err != nil {
330 return nil, fmt.Errorf("could not get CA certificate: %w", err)
331 }
332
333 // Dial the actual node at its management port.
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200334 cl, err := dialAuthenticatedNode(ctx, n.Id, n.Status.ExternalAddress, cacert)
335 if err != nil {
336 return nil, fmt.Errorf("while dialing node: %w", err)
337 }
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000338 nmgmt := apb.NewNodeManagementClient(cl)
339 return nmgmt, nil
340}
341
342var nodeRebootCmd = &cobra.Command{
343 Short: "Reboot a node",
344 Long: `Reboot a node.
345
346This command can be used quite flexibly. Without any options it performs a
347normal, firmware-assisted reboot. It can roll back the last update by also
348passing the --rollback option. To reboot quicker the --kexec option can be used
349to skip firmware during reboot and boot straigt into the kernel.
350
351It can also be used to reboot into the firmware (BIOS) setup UI by passing the
352--firmware flag. This flag cannot be combined with any others.
353 `,
354 Use: "reboot [node-id]",
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +0200355 Args: PrintUsageOnWrongArgs(cobra.ExactArgs(1)),
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000356 SilenceUsage: true,
357 RunE: func(cmd *cobra.Command, args []string) error {
358 ctx := cmd.Context()
359
360 kexecFlag, err := cmd.Flags().GetBool("kexec")
361 if err != nil {
362 return err
363 }
364 rollbackFlag, err := cmd.Flags().GetBool("rollback")
365 if err != nil {
366 return err
367 }
368 firmwareFlag, err := cmd.Flags().GetBool("firmware")
369 if err != nil {
370 return err
371 }
372
373 if kexecFlag && firmwareFlag {
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200374 return fmt.Errorf("--kexec cannot be used with --firmware as firmware is not involved when using kexec")
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000375 }
376 if firmwareFlag && rollbackFlag {
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200377 return fmt.Errorf("--firmware cannot be used with --rollback as the next boot won't be into the OS")
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000378 }
379 var req apb.RebootRequest
380 if kexecFlag {
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100381 req.Type = apb.RebootRequest_TYPE_KEXEC
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000382 } else {
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100383 req.Type = apb.RebootRequest_TYPE_FIRMWARE
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000384 }
385 if firmwareFlag {
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100386 req.NextBoot = apb.RebootRequest_NEXT_BOOT_START_FIRMWARE_UI
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000387 }
388 if rollbackFlag {
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100389 req.NextBoot = apb.RebootRequest_NEXT_BOOT_START_ROLLBACK
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000390 }
391
392 nmgmt, err := dialNode(ctx, args[0])
393 if err != nil {
394 return fmt.Errorf("failed to dial node: %w", err)
395 }
396
397 if _, err := nmgmt.Reboot(ctx, &req); err != nil {
398 return fmt.Errorf("reboot RPC failed: %w", err)
399 }
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200400 log.Printf("Node %v is being rebooted", args[0])
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000401
402 return nil
403 },
404}
405
406var nodePoweroffCmd = &cobra.Command{
407 Short: "Power off a node",
408 Use: "poweroff [node-id]",
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +0200409 Args: PrintUsageOnWrongArgs(cobra.ExactArgs(1)),
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000410 SilenceUsage: true,
411 RunE: func(cmd *cobra.Command, args []string) error {
412 ctx := cmd.Context()
413
414 nmgmt, err := dialNode(ctx, args[0])
415 if err != nil {
416 return err
417 }
418
419 if _, err := nmgmt.Reboot(ctx, &apb.RebootRequest{
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100420 Type: apb.RebootRequest_TYPE_POWER_OFF,
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000421 }); err != nil {
422 return fmt.Errorf("reboot RPC failed: %w", err)
423 }
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200424 log.Printf("Node %v is being powered off", args[0])
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000425
426 return nil
427 },
428}
429
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100430func init() {
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200431 nodeUpdateCmd.Flags().String("bundle-url", "", "The URL to the new version")
432 nodeUpdateCmd.Flags().String("activation-mode", "reboot", "How the update should be activated (kexec, reboot, none)")
Lorenz Brun9ce40712024-02-13 21:54:46 +0100433 nodeUpdateCmd.Flags().Uint64("max-unavailable", 1, "Maximum nodes which can be unavailable during the update process")
Lorenz Bruncceb6a32024-04-16 13:33:15 +0000434 nodeUpdateCmd.Flags().StringArray("exclude", nil, "List of nodes to exclude (useful with the \"all\" argument)")
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200435
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100436 nodeDeleteCmd.Flags().Bool("bypass-has-roles", false, "Allows to bypass the HasRoles check")
437 nodeDeleteCmd.Flags().Bool("bypass-not-decommissioned", false, "Allows to bypass the NotDecommissioned check")
438
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000439 nodeRebootCmd.Flags().Bool("rollback", false, "Reboot into the last OS version in the other slot")
440 nodeRebootCmd.Flags().Bool("firmware", false, "Reboot into the firmware (BIOS) setup UI")
441 nodeRebootCmd.Flags().Bool("kexec", false, "Use kexec to reboot much quicker without going through firmware")
442
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100443 nodeCmd.AddCommand(nodeDescribeCmd)
444 nodeCmd.AddCommand(nodeListCmd)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200445 nodeCmd.AddCommand(nodeUpdateCmd)
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100446 nodeCmd.AddCommand(nodeDeleteCmd)
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000447 nodeCmd.AddCommand(nodeRebootCmd)
448 nodeCmd.AddCommand(nodePoweroffCmd)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100449 rootCmd.AddCommand(nodeCmd)
450}
451
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200452func printNodes(nodes []*apb.Node, args []string, onlyColumns map[string]bool) error {
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100453 o := io.WriteCloser(os.Stdout)
454 if flags.output != "" {
455 of, err := os.Create(flags.output)
456 if err != nil {
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200457 return fmt.Errorf("couldn't create the output file at %s: %w", flags.output, err)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100458 }
459 o = of
460 }
461
462 // Narrow down the output set to supplied node IDs, if any.
463 qids := make(map[string]bool)
464 if len(args) != 0 && args[0] != "all" {
465 for _, a := range args {
466 qids[a] = true
467 }
468 }
469
Serge Bazanskie0c06172023-09-19 12:28:16 +0000470 var t clitable.Table
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100471 for _, n := range nodes {
472 // Filter the information we want client-side.
473 if len(qids) != 0 {
Jan Schär39d9c242024-09-24 13:49:55 +0200474 if _, e := qids[n.Id]; !e {
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100475 continue
476 }
477 }
Serge Bazanskie0c06172023-09-19 12:28:16 +0000478 t.Add(nodeEntry(n))
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100479 }
480
Serge Bazanskie0c06172023-09-19 12:28:16 +0000481 t.Print(o, onlyColumns)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200482 return nil
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100483}