blob: 2e0b537fd42c70507d86ebd00a24356705bcdbaf [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +01004package main
5
6import (
7 "context"
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +02008 "fmt"
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +01009 "io"
10 "log"
11 "os"
Tim Windelschmidtb765f242024-05-08 01:40:02 +020012 "os/signal"
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +020013 "strings"
Lorenz Brun9ce40712024-02-13 21:54:46 +010014 "sync"
15 "time"
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010016
17 "github.com/spf13/cobra"
Lorenz Brun9ce40712024-02-13 21:54:46 +010018 "golang.org/x/sync/semaphore"
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010019
Serge Bazanskie0c06172023-09-19 12:28:16 +000020 "source.monogon.dev/go/clitable"
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010021 "source.monogon.dev/metropolis/cli/metroctl/core"
Lorenz Brun9ce40712024-02-13 21:54:46 +010022 "source.monogon.dev/version"
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +020023
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010024 apb "source.monogon.dev/metropolis/proto/api"
25)
26
27var nodeCmd = &cobra.Command{
28 Short: "Updates and queries node information.",
29 Use: "node",
30}
31
32var nodeDescribeCmd = &cobra.Command{
33 Short: "Describes cluster nodes.",
Serge Bazanski98840342024-05-22 13:03:55 +020034 Use: "describe [node-id] [--filter] [--output] [--format] [--columns]",
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010035 Example: "metroctl node describe metropolis-c556e31c3fa2bf0a36e9ccb9fd5d6056",
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020036 RunE: func(cmd *cobra.Command, args []string) error {
Tim Windelschmidtb765f242024-05-08 01:40:02 +020037 ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020038 cc, err := dialAuthenticated(ctx)
39 if err != nil {
40 return fmt.Errorf("while dialing node: %w", err)
41 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010042 mgmt := apb.NewManagementClient(cc)
43
44 nodes, err := core.GetNodes(ctx, mgmt, flags.filter)
45 if err != nil {
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020046 return fmt.Errorf("while calling Management.GetNodes: %w", err)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010047 }
48
Serge Bazanski98840342024-05-22 13:03:55 +020049 var columns map[string]bool
50 if flags.columns != "" {
51 columns = make(map[string]bool)
52 for _, p := range strings.Split(flags.columns, ",") {
53 p = strings.ToLower(p)
54 p = strings.TrimSpace(p)
55 columns[p] = true
56 }
57 }
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020058
59 return printNodes(nodes, args, columns)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010060 },
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +020061 Args: PrintUsageOnWrongArgs(cobra.ArbitraryArgs),
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010062}
63
64var nodeListCmd = &cobra.Command{
65 Short: "Lists cluster nodes.",
66 Use: "list [node-id] [--filter] [--output] [--format]",
67 Example: "metroctl node list --filter node.status.external_address==\"10.8.0.2\"",
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020068 RunE: func(cmd *cobra.Command, args []string) error {
Tim Windelschmidtb765f242024-05-08 01:40:02 +020069 ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020070 cc, err := dialAuthenticated(ctx)
71 if err != nil {
72 return fmt.Errorf("while dialing node: %w", err)
73 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010074 mgmt := apb.NewManagementClient(cc)
75
76 nodes, err := core.GetNodes(ctx, mgmt, flags.filter)
77 if err != nil {
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020078 return fmt.Errorf("while calling Management.GetNodes: %w", err)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010079 }
80
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020081 return printNodes(nodes, args, map[string]bool{"node id": true})
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010082 },
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +020083 Args: PrintUsageOnWrongArgs(cobra.ArbitraryArgs),
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010084}
85
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +020086var nodeUpdateCmd = &cobra.Command{
87 Short: "Updates the operating system of a cluster node.",
Lorenz Brun9ce40712024-02-13 21:54:46 +010088 Use: "update [NodeIDs]",
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +020089 Example: "metroctl node update --bundle-url https://example.com/bundle.zip --activation-mode reboot metropolis-25fa5f5e9349381d4a5e9e59de0215e3",
90 RunE: func(cmd *cobra.Command, args []string) error {
91 bundleUrl, err := cmd.Flags().GetString("bundle-url")
92 if err != nil {
93 return err
94 }
95
96 if len(bundleUrl) == 0 {
97 return fmt.Errorf("flag bundle-url is required")
98 }
99
100 activationMode, err := cmd.Flags().GetString("activation-mode")
101 if err != nil {
102 return err
103 }
104
105 var am apb.ActivationMode
106 switch strings.ToLower(activationMode) {
107 case "none":
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100108 am = apb.ActivationMode_ACTIVATION_MODE_NONE
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200109 case "reboot":
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100110 am = apb.ActivationMode_ACTIVATION_MODE_REBOOT
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200111 case "kexec":
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100112 am = apb.ActivationMode_ACTIVATION_MODE_KEXEC
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200113 default:
114 return fmt.Errorf("invalid value for flag activation-mode")
115 }
116
Lorenz Brun9ce40712024-02-13 21:54:46 +0100117 maxUnavailable, err := cmd.Flags().GetUint64("max-unavailable")
118 if err != nil {
119 return err
120 }
121 if maxUnavailable == 0 {
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200122 return fmt.Errorf("unable to update notes with max-unavailable set to zero")
Lorenz Brun9ce40712024-02-13 21:54:46 +0100123 }
124 unavailableSemaphore := semaphore.NewWeighted(int64(maxUnavailable))
125
Tim Windelschmidtb765f242024-05-08 01:40:02 +0200126 ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200127
Serge Bazanskic51d47d2024-02-13 18:40:26 +0100128 cacert, err := core.GetClusterCAWithTOFU(ctx, connectOptions())
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200129 if err != nil {
Serge Bazanskic51d47d2024-02-13 18:40:26 +0100130 return fmt.Errorf("could not get CA certificate: %w", err)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200131 }
Serge Bazanskic51d47d2024-02-13 18:40:26 +0100132
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200133 conn, err := dialAuthenticated(ctx)
134 if err != nil {
135 return err
136 }
137 mgmt := apb.NewManagementClient(conn)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200138
139 nodes, err := core.GetNodes(ctx, mgmt, "")
140 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +0200141 return fmt.Errorf("while calling Management.GetNodes: %w", err)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200142 }
143 // Narrow down the output set to supplied node IDs, if any.
144 qids := make(map[string]bool)
145 if len(args) != 0 && args[0] != "all" {
146 for _, a := range args {
147 qids[a] = true
148 }
149 }
150
Lorenz Bruncceb6a32024-04-16 13:33:15 +0000151 excludedNodesSlice, err := cmd.Flags().GetStringArray("exclude")
152 if err != nil {
153 return err
154 }
155 excludedNodes := make(map[string]bool)
156 for _, n := range excludedNodesSlice {
157 excludedNodes[n] = true
158 }
159
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200160 updateReq := &apb.UpdateNodeRequest{
161 BundleUrl: bundleUrl,
162 ActivationMode: am,
163 }
164
Lorenz Brun9ce40712024-02-13 21:54:46 +0100165 var wg sync.WaitGroup
166
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200167 for _, n := range nodes {
168 // Filter the information we want client-side.
169 if len(qids) != 0 {
Jan Schär39d9c242024-09-24 13:49:55 +0200170 if _, e := qids[n.Id]; !e {
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200171 continue
172 }
173 }
Jan Schär39d9c242024-09-24 13:49:55 +0200174 if excludedNodes[n.Id] {
Lorenz Bruncceb6a32024-04-16 13:33:15 +0000175 continue
176 }
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200177
Lorenz Brun9ce40712024-02-13 21:54:46 +0100178 if err := unavailableSemaphore.Acquire(ctx, 1); err != nil {
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200179 return err
180 }
Lorenz Brun9ce40712024-02-13 21:54:46 +0100181 wg.Add(1)
182
Tim Windelschmidtb41b5482024-04-18 23:24:01 +0200183 go func(n *apb.Node) {
Lorenz Brun9ce40712024-02-13 21:54:46 +0100184 defer wg.Done()
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200185 cc, err := dialAuthenticatedNode(ctx, n.Id, n.Status.ExternalAddress, cacert)
186 if err != nil {
187 log.Fatalf("failed to dial node: %v", err)
188 }
Lorenz Brun9ce40712024-02-13 21:54:46 +0100189 nodeMgmt := apb.NewNodeManagementClient(cc)
190 log.Printf("sending update request to: %s (%s)", n.Id, n.Status.ExternalAddress)
191 start := time.Now()
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200192 _, err = nodeMgmt.UpdateNode(ctx, updateReq)
Lorenz Brun9ce40712024-02-13 21:54:46 +0100193 if err != nil {
194 log.Printf("update request to node %s failed: %v", n.Id, err)
195 // A failed UpdateNode does not mean that the node is now unavailable as it
196 // hasn't started activating yet.
197 unavailableSemaphore.Release(1)
198 }
199 // Wait for the internal activation sleep plus the heartbeat
200 // to make sure the node has missed one heartbeat (or is
201 // back up already).
202 time.Sleep((5 + 10) * time.Second)
203 for {
204 select {
205 case <-time.After(10 * time.Second):
206 nodes, err := core.GetNodes(ctx, mgmt, fmt.Sprintf("node.id == %q", n.Id))
207 if err != nil {
208 log.Printf("while getting node status for %s: %v", n.Id, err)
Lorenz Brun76612022024-03-05 19:20:36 +0100209 continue
Lorenz Brun9ce40712024-02-13 21:54:46 +0100210 }
211 if len(nodes) == 0 {
212 log.Printf("node status for %s returned no node", n.Id)
Lorenz Brun76612022024-03-05 19:20:36 +0100213 continue
Lorenz Brun9ce40712024-02-13 21:54:46 +0100214 }
215 if len(nodes) > 1 {
216 log.Printf("node status for %s returned too many nodes (%d)", n.Id, len(nodes))
Lorenz Brun76612022024-03-05 19:20:36 +0100217 continue
Lorenz Brun9ce40712024-02-13 21:54:46 +0100218 }
219 s := nodes[0]
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100220 if s.Health == apb.Node_HEALTH_HEALTHY {
Lorenz Brun9ce40712024-02-13 21:54:46 +0100221 if s.Status != nil && s.Status.Version != nil {
222 log.Printf("node %s updated in %v to version %s", s.Id, time.Since(start), version.Semver(s.Status.Version))
223 } else {
224 log.Printf("node %s updated in %v to unknown version", s.Id, time.Since(start))
225 }
226 unavailableSemaphore.Release(1)
227 return
228 }
229 case <-ctx.Done():
230 log.Printf("update to node %s incomplete", n.Id)
231 return
232 }
233 }
234 }(n)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200235 }
236
Lorenz Brun9ce40712024-02-13 21:54:46 +0100237 // Wait for all update processes to finish
238 wg.Wait()
239
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200240 return nil
241 },
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +0200242 Args: PrintUsageOnWrongArgs(cobra.MinimumNArgs(1)),
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200243}
244
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100245var nodeDeleteCmd = &cobra.Command{
246 Short: "Deletes a node from the cluster.",
247 Use: "delete [NodeID] [--bypass-has-roles] [--bypass-not-decommissioned]",
248 Example: "metroctl node delete metropolis-25fa5f5e9349381d4a5e9e59de0215e3",
249 RunE: func(cmd *cobra.Command, args []string) error {
250 bypassHasRoles, err := cmd.Flags().GetBool("bypass-has-roles")
251 if err != nil {
252 return err
253 }
254
255 bypassNotDecommissioned, err := cmd.Flags().GetBool("bypass-not-decommissioned")
256 if err != nil {
257 return err
258 }
259
Tim Windelschmidtb765f242024-05-08 01:40:02 +0200260 ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200261 conn, err := dialAuthenticated(ctx)
262 if err != nil {
263 return err
264 }
265 mgmt := apb.NewManagementClient(conn)
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100266
267 nodes, err := core.GetNodes(ctx, mgmt, fmt.Sprintf("node.id==%q", args[0]))
268 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +0200269 return fmt.Errorf("while calling Management.GetNodes: %w", err)
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100270 }
271
272 if len(nodes) == 0 {
273 return fmt.Errorf("could not find node with id: %s", args[0])
274 }
275
276 if len(nodes) != 1 {
277 return fmt.Errorf("expected one node, got %d", len(nodes))
278 }
279
280 n := nodes[0]
Lorenz Brun2542ef82024-08-20 13:33:02 +0200281 if n.Status != nil && n.Status.ExternalAddress != "" {
282 log.Printf("deleting node: %s (%s)", n.Id, n.Status.ExternalAddress)
283 } else {
284 log.Printf("deleting node: %s", n.Id)
285 }
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100286
287 req := &apb.DeleteNodeRequest{
288 Node: &apb.DeleteNodeRequest_Id{
289 Id: n.Id,
290 },
291 }
292
293 if bypassHasRoles {
294 req.SafetyBypassHasRoles = &apb.DeleteNodeRequest_SafetyBypassHasRoles{}
295 }
296
297 if bypassNotDecommissioned {
298 req.SafetyBypassNotDecommissioned = &apb.DeleteNodeRequest_SafetyBypassNotDecommissioned{}
299 }
300
301 _, err = mgmt.DeleteNode(ctx, req)
302 return err
303 },
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +0200304 Args: PrintUsageOnWrongArgs(cobra.ExactArgs(1)),
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100305}
306
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000307func dialNode(ctx context.Context, node string) (apb.NodeManagementClient, error) {
308 // First connect to the main management service and figure out the node's IP
309 // address.
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200310 cc, err := dialAuthenticated(ctx)
311 if err != nil {
312 return nil, fmt.Errorf("while dialing node: %w", err)
313 }
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000314 mgmt := apb.NewManagementClient(cc)
315 nodes, err := core.GetNodes(ctx, mgmt, fmt.Sprintf("node.id == %q", node))
316 if err != nil {
317 return nil, fmt.Errorf("when getting node info: %w", err)
318 }
319
320 if len(nodes) == 0 {
321 return nil, fmt.Errorf("no such node")
322 }
323 if len(nodes) > 1 {
324 return nil, fmt.Errorf("expression matched more than one node")
325 }
326 n := nodes[0]
327 if n.Status == nil || n.Status.ExternalAddress == "" {
328 return nil, fmt.Errorf("node has no external address")
329 }
330
331 cacert, err := core.GetClusterCAWithTOFU(ctx, connectOptions())
332 if err != nil {
333 return nil, fmt.Errorf("could not get CA certificate: %w", err)
334 }
335
336 // Dial the actual node at its management port.
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200337 cl, err := dialAuthenticatedNode(ctx, n.Id, n.Status.ExternalAddress, cacert)
338 if err != nil {
339 return nil, fmt.Errorf("while dialing node: %w", err)
340 }
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000341 nmgmt := apb.NewNodeManagementClient(cl)
342 return nmgmt, nil
343}
344
345var nodeRebootCmd = &cobra.Command{
346 Short: "Reboot a node",
347 Long: `Reboot a node.
348
349This command can be used quite flexibly. Without any options it performs a
350normal, firmware-assisted reboot. It can roll back the last update by also
351passing the --rollback option. To reboot quicker the --kexec option can be used
352to skip firmware during reboot and boot straigt into the kernel.
353
354It can also be used to reboot into the firmware (BIOS) setup UI by passing the
355--firmware flag. This flag cannot be combined with any others.
356 `,
357 Use: "reboot [node-id]",
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +0200358 Args: PrintUsageOnWrongArgs(cobra.ExactArgs(1)),
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000359 SilenceUsage: true,
360 RunE: func(cmd *cobra.Command, args []string) error {
361 ctx := cmd.Context()
362
363 kexecFlag, err := cmd.Flags().GetBool("kexec")
364 if err != nil {
365 return err
366 }
367 rollbackFlag, err := cmd.Flags().GetBool("rollback")
368 if err != nil {
369 return err
370 }
371 firmwareFlag, err := cmd.Flags().GetBool("firmware")
372 if err != nil {
373 return err
374 }
375
376 if kexecFlag && firmwareFlag {
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200377 return fmt.Errorf("--kexec cannot be used with --firmware as firmware is not involved when using kexec")
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000378 }
379 if firmwareFlag && rollbackFlag {
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200380 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 +0000381 }
382 var req apb.RebootRequest
383 if kexecFlag {
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100384 req.Type = apb.RebootRequest_TYPE_KEXEC
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000385 } else {
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100386 req.Type = apb.RebootRequest_TYPE_FIRMWARE
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000387 }
388 if firmwareFlag {
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100389 req.NextBoot = apb.RebootRequest_NEXT_BOOT_START_FIRMWARE_UI
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000390 }
391 if rollbackFlag {
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100392 req.NextBoot = apb.RebootRequest_NEXT_BOOT_START_ROLLBACK
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000393 }
394
395 nmgmt, err := dialNode(ctx, args[0])
396 if err != nil {
397 return fmt.Errorf("failed to dial node: %w", err)
398 }
399
400 if _, err := nmgmt.Reboot(ctx, &req); err != nil {
401 return fmt.Errorf("reboot RPC failed: %w", err)
402 }
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200403 log.Printf("Node %v is being rebooted", args[0])
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000404
405 return nil
406 },
407}
408
409var nodePoweroffCmd = &cobra.Command{
410 Short: "Power off a node",
411 Use: "poweroff [node-id]",
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +0200412 Args: PrintUsageOnWrongArgs(cobra.ExactArgs(1)),
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000413 SilenceUsage: true,
414 RunE: func(cmd *cobra.Command, args []string) error {
415 ctx := cmd.Context()
416
417 nmgmt, err := dialNode(ctx, args[0])
418 if err != nil {
419 return err
420 }
421
422 if _, err := nmgmt.Reboot(ctx, &apb.RebootRequest{
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100423 Type: apb.RebootRequest_TYPE_POWER_OFF,
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000424 }); err != nil {
425 return fmt.Errorf("reboot RPC failed: %w", err)
426 }
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200427 log.Printf("Node %v is being powered off", args[0])
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000428
429 return nil
430 },
431}
432
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100433func init() {
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200434 nodeUpdateCmd.Flags().String("bundle-url", "", "The URL to the new version")
435 nodeUpdateCmd.Flags().String("activation-mode", "reboot", "How the update should be activated (kexec, reboot, none)")
Lorenz Brun9ce40712024-02-13 21:54:46 +0100436 nodeUpdateCmd.Flags().Uint64("max-unavailable", 1, "Maximum nodes which can be unavailable during the update process")
Lorenz Bruncceb6a32024-04-16 13:33:15 +0000437 nodeUpdateCmd.Flags().StringArray("exclude", nil, "List of nodes to exclude (useful with the \"all\" argument)")
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200438
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100439 nodeDeleteCmd.Flags().Bool("bypass-has-roles", false, "Allows to bypass the HasRoles check")
440 nodeDeleteCmd.Flags().Bool("bypass-not-decommissioned", false, "Allows to bypass the NotDecommissioned check")
441
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000442 nodeRebootCmd.Flags().Bool("rollback", false, "Reboot into the last OS version in the other slot")
443 nodeRebootCmd.Flags().Bool("firmware", false, "Reboot into the firmware (BIOS) setup UI")
444 nodeRebootCmd.Flags().Bool("kexec", false, "Use kexec to reboot much quicker without going through firmware")
445
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100446 nodeCmd.AddCommand(nodeDescribeCmd)
447 nodeCmd.AddCommand(nodeListCmd)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200448 nodeCmd.AddCommand(nodeUpdateCmd)
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100449 nodeCmd.AddCommand(nodeDeleteCmd)
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000450 nodeCmd.AddCommand(nodeRebootCmd)
451 nodeCmd.AddCommand(nodePoweroffCmd)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100452 rootCmd.AddCommand(nodeCmd)
453}
454
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200455func printNodes(nodes []*apb.Node, args []string, onlyColumns map[string]bool) error {
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100456 o := io.WriteCloser(os.Stdout)
457 if flags.output != "" {
458 of, err := os.Create(flags.output)
459 if err != nil {
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200460 return fmt.Errorf("couldn't create the output file at %s: %w", flags.output, err)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100461 }
462 o = of
463 }
464
465 // Narrow down the output set to supplied node IDs, if any.
466 qids := make(map[string]bool)
467 if len(args) != 0 && args[0] != "all" {
468 for _, a := range args {
469 qids[a] = true
470 }
471 }
472
Serge Bazanskie0c06172023-09-19 12:28:16 +0000473 var t clitable.Table
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100474 for _, n := range nodes {
475 // Filter the information we want client-side.
476 if len(qids) != 0 {
Jan Schär39d9c242024-09-24 13:49:55 +0200477 if _, e := qids[n.Id]; !e {
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100478 continue
479 }
480 }
Serge Bazanskie0c06172023-09-19 12:28:16 +0000481 t.Add(nodeEntry(n))
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100482 }
483
Serge Bazanskie0c06172023-09-19 12:28:16 +0000484 t.Print(o, onlyColumns)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200485 return nil
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100486}