blob: 4e16213faeea8d5d2974cd581a18b6e44ae158ea [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"
Jan Schär62cecde2025-04-16 15:24:04 +000022 "source.monogon.dev/osbase/oci"
23 "source.monogon.dev/osbase/oci/registry"
Lorenz Brun9ce40712024-02-13 21:54:46 +010024 "source.monogon.dev/version"
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +020025
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010026 apb "source.monogon.dev/metropolis/proto/api"
27)
28
29var nodeCmd = &cobra.Command{
30 Short: "Updates and queries node information.",
31 Use: "node",
32}
33
34var nodeDescribeCmd = &cobra.Command{
35 Short: "Describes cluster nodes.",
Serge Bazanski98840342024-05-22 13:03:55 +020036 Use: "describe [node-id] [--filter] [--output] [--format] [--columns]",
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010037 Example: "metroctl node describe metropolis-c556e31c3fa2bf0a36e9ccb9fd5d6056",
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020038 RunE: func(cmd *cobra.Command, args []string) error {
Tim Windelschmidtb765f242024-05-08 01:40:02 +020039 ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
Tim Windelschmidt9bd9bd42025-02-14 17:08:52 +010040 cc, err := newAuthenticatedClient(ctx)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020041 if err != nil {
Tim Windelschmidt9bd9bd42025-02-14 17:08:52 +010042 return fmt.Errorf("while creating client: %w", err)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020043 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010044 mgmt := apb.NewManagementClient(cc)
45
46 nodes, err := core.GetNodes(ctx, mgmt, flags.filter)
47 if err != nil {
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020048 return fmt.Errorf("while calling Management.GetNodes: %w", err)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010049 }
50
Serge Bazanski98840342024-05-22 13:03:55 +020051 var columns map[string]bool
52 if flags.columns != "" {
53 columns = make(map[string]bool)
54 for _, p := range strings.Split(flags.columns, ",") {
55 p = strings.ToLower(p)
56 p = strings.TrimSpace(p)
57 columns[p] = true
58 }
59 }
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020060
61 return printNodes(nodes, args, columns)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010062 },
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +020063 Args: PrintUsageOnWrongArgs(cobra.ArbitraryArgs),
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010064}
65
66var nodeListCmd = &cobra.Command{
67 Short: "Lists cluster nodes.",
68 Use: "list [node-id] [--filter] [--output] [--format]",
69 Example: "metroctl node list --filter node.status.external_address==\"10.8.0.2\"",
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020070 RunE: func(cmd *cobra.Command, args []string) error {
Tim Windelschmidtb765f242024-05-08 01:40:02 +020071 ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
Tim Windelschmidt9bd9bd42025-02-14 17:08:52 +010072 cc, err := newAuthenticatedClient(ctx)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020073 if err != nil {
Tim Windelschmidt9bd9bd42025-02-14 17:08:52 +010074 return fmt.Errorf("while creating client: %w", err)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020075 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010076 mgmt := apb.NewManagementClient(cc)
77
78 nodes, err := core.GetNodes(ctx, mgmt, flags.filter)
79 if err != nil {
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020080 return fmt.Errorf("while calling Management.GetNodes: %w", err)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010081 }
82
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +020083 return printNodes(nodes, args, map[string]bool{"node id": true})
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010084 },
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +020085 Args: PrintUsageOnWrongArgs(cobra.ArbitraryArgs),
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010086}
87
Jan Schär62cecde2025-04-16 15:24:04 +000088// parseImageRef parses a reference to an OCI image stored in a registry.
89//
90// The format is [http[s]://]host[:port]/repository[:tag]@digest, where []
91// indicates optional components. This format is for convenience and is similar
92// to what other tools use.
93func parseImageRef(imageRef string) (*apb.OSImageRef, error) {
94 scheme := "https"
95 var ok bool
96 if imageRef, ok = strings.CutPrefix(imageRef, "https://"); ok {
97 scheme = "https"
98 } else if imageRef, ok = strings.CutPrefix(imageRef, "http://"); ok {
99 scheme = "http"
100 }
101 host, rest, ok := strings.Cut(imageRef, "/")
102 if !ok || host == "" {
103 return nil, fmt.Errorf("missing host")
104 }
105 rest, digest, ok := strings.Cut(rest, "@")
106 if !ok || digest == "" {
107 return nil, fmt.Errorf("missing digest")
108 }
109 repository, tag, _ := strings.Cut(rest, ":")
110
111 if !registry.RepositoryRegexp.MatchString(repository) {
112 return nil, fmt.Errorf("invalid repository %q", repository)
113 }
114 if tag != "" && !registry.TagRegexp.MatchString(tag) {
115 return nil, fmt.Errorf("invalid tag %q", tag)
116 }
117 if _, _, err := oci.ParseDigest(digest); err != nil {
118 return nil, err
119 }
120
121 return &apb.OSImageRef{
122 Scheme: scheme,
123 Host: host,
124 Repository: repository,
125 Tag: tag,
126 Digest: digest,
127 }, nil
128}
129
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200130var nodeUpdateCmd = &cobra.Command{
131 Short: "Updates the operating system of a cluster node.",
Lorenz Brun9ce40712024-02-13 21:54:46 +0100132 Use: "update [NodeIDs]",
Jan Schär62cecde2025-04-16 15:24:04 +0000133 Example: "metroctl node update --image-ref registry.example/monogon-os/node:0.1-amd64@sha256:345db5d8fc468218c5232bf54a1358b6825c28d658fa12c9a1edcc7539690686 --activation-mode reboot metropolis-25fa5f5e9349381d4a5e9e59de0215e3",
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200134 RunE: func(cmd *cobra.Command, args []string) error {
Jan Schär62cecde2025-04-16 15:24:04 +0000135 imageRef, err := cmd.Flags().GetString("image-ref")
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200136 if err != nil {
137 return err
138 }
139
Jan Schär62cecde2025-04-16 15:24:04 +0000140 if len(imageRef) == 0 {
141 return fmt.Errorf("flag image-ref is required")
142 }
143 osImage, err := parseImageRef(imageRef)
144 if err != nil {
145 return fmt.Errorf("invalid image-ref: %w", err)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200146 }
147
148 activationMode, err := cmd.Flags().GetString("activation-mode")
149 if err != nil {
150 return err
151 }
152
153 var am apb.ActivationMode
154 switch strings.ToLower(activationMode) {
155 case "none":
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100156 am = apb.ActivationMode_ACTIVATION_MODE_NONE
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200157 case "reboot":
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100158 am = apb.ActivationMode_ACTIVATION_MODE_REBOOT
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200159 case "kexec":
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100160 am = apb.ActivationMode_ACTIVATION_MODE_KEXEC
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200161 default:
162 return fmt.Errorf("invalid value for flag activation-mode")
163 }
164
Lorenz Brun9ce40712024-02-13 21:54:46 +0100165 maxUnavailable, err := cmd.Flags().GetUint64("max-unavailable")
166 if err != nil {
167 return err
168 }
169 if maxUnavailable == 0 {
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200170 return fmt.Errorf("unable to update notes with max-unavailable set to zero")
Lorenz Brun9ce40712024-02-13 21:54:46 +0100171 }
172 unavailableSemaphore := semaphore.NewWeighted(int64(maxUnavailable))
173
Tim Windelschmidtb765f242024-05-08 01:40:02 +0200174 ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200175
Serge Bazanskic51d47d2024-02-13 18:40:26 +0100176 cacert, err := core.GetClusterCAWithTOFU(ctx, connectOptions())
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200177 if err != nil {
Serge Bazanskic51d47d2024-02-13 18:40:26 +0100178 return fmt.Errorf("could not get CA certificate: %w", err)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200179 }
Serge Bazanskic51d47d2024-02-13 18:40:26 +0100180
Tim Windelschmidt9bd9bd42025-02-14 17:08:52 +0100181 conn, err := newAuthenticatedClient(ctx)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200182 if err != nil {
183 return err
184 }
185 mgmt := apb.NewManagementClient(conn)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200186
187 nodes, err := core.GetNodes(ctx, mgmt, "")
188 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +0200189 return fmt.Errorf("while calling Management.GetNodes: %w", err)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200190 }
191 // Narrow down the output set to supplied node IDs, if any.
192 qids := make(map[string]bool)
193 if len(args) != 0 && args[0] != "all" {
194 for _, a := range args {
195 qids[a] = true
196 }
197 }
198
Lorenz Bruncceb6a32024-04-16 13:33:15 +0000199 excludedNodesSlice, err := cmd.Flags().GetStringArray("exclude")
200 if err != nil {
201 return err
202 }
203 excludedNodes := make(map[string]bool)
204 for _, n := range excludedNodesSlice {
205 excludedNodes[n] = true
206 }
207
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200208 updateReq := &apb.UpdateNodeRequest{
Jan Schär62cecde2025-04-16 15:24:04 +0000209 OsImage: osImage,
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200210 ActivationMode: am,
211 }
212
Lorenz Brun9ce40712024-02-13 21:54:46 +0100213 var wg sync.WaitGroup
214
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200215 for _, n := range nodes {
216 // Filter the information we want client-side.
217 if len(qids) != 0 {
Jan Schär39d9c242024-09-24 13:49:55 +0200218 if _, e := qids[n.Id]; !e {
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200219 continue
220 }
221 }
Jan Schär39d9c242024-09-24 13:49:55 +0200222 if excludedNodes[n.Id] {
Lorenz Bruncceb6a32024-04-16 13:33:15 +0000223 continue
224 }
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200225
Lorenz Brun9ce40712024-02-13 21:54:46 +0100226 if err := unavailableSemaphore.Acquire(ctx, 1); err != nil {
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200227 return err
228 }
Lorenz Brun9ce40712024-02-13 21:54:46 +0100229 wg.Add(1)
230
Tim Windelschmidtb41b5482024-04-18 23:24:01 +0200231 go func(n *apb.Node) {
Lorenz Brun9ce40712024-02-13 21:54:46 +0100232 defer wg.Done()
Tim Windelschmidt9bd9bd42025-02-14 17:08:52 +0100233 cc, err := newAuthenticatedNodeClient(ctx, n.Id, n.Status.ExternalAddress, cacert)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200234 if err != nil {
Tim Windelschmidt9bd9bd42025-02-14 17:08:52 +0100235 log.Fatalf("failed to create node client: %v", err)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200236 }
Lorenz Brun9ce40712024-02-13 21:54:46 +0100237 nodeMgmt := apb.NewNodeManagementClient(cc)
238 log.Printf("sending update request to: %s (%s)", n.Id, n.Status.ExternalAddress)
239 start := time.Now()
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200240 _, err = nodeMgmt.UpdateNode(ctx, updateReq)
Lorenz Brun9ce40712024-02-13 21:54:46 +0100241 if err != nil {
242 log.Printf("update request to node %s failed: %v", n.Id, err)
243 // A failed UpdateNode does not mean that the node is now unavailable as it
244 // hasn't started activating yet.
245 unavailableSemaphore.Release(1)
246 }
247 // Wait for the internal activation sleep plus the heartbeat
248 // to make sure the node has missed one heartbeat (or is
249 // back up already).
250 time.Sleep((5 + 10) * time.Second)
251 for {
252 select {
253 case <-time.After(10 * time.Second):
254 nodes, err := core.GetNodes(ctx, mgmt, fmt.Sprintf("node.id == %q", n.Id))
255 if err != nil {
256 log.Printf("while getting node status for %s: %v", n.Id, err)
Lorenz Brun76612022024-03-05 19:20:36 +0100257 continue
Lorenz Brun9ce40712024-02-13 21:54:46 +0100258 }
259 if len(nodes) == 0 {
260 log.Printf("node status for %s returned no node", n.Id)
Lorenz Brun76612022024-03-05 19:20:36 +0100261 continue
Lorenz Brun9ce40712024-02-13 21:54:46 +0100262 }
263 if len(nodes) > 1 {
264 log.Printf("node status for %s returned too many nodes (%d)", n.Id, len(nodes))
Lorenz Brun76612022024-03-05 19:20:36 +0100265 continue
Lorenz Brun9ce40712024-02-13 21:54:46 +0100266 }
267 s := nodes[0]
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100268 if s.Health == apb.Node_HEALTH_HEALTHY {
Lorenz Brun9ce40712024-02-13 21:54:46 +0100269 if s.Status != nil && s.Status.Version != nil {
270 log.Printf("node %s updated in %v to version %s", s.Id, time.Since(start), version.Semver(s.Status.Version))
271 } else {
272 log.Printf("node %s updated in %v to unknown version", s.Id, time.Since(start))
273 }
274 unavailableSemaphore.Release(1)
275 return
276 }
277 case <-ctx.Done():
278 log.Printf("update to node %s incomplete", n.Id)
279 return
280 }
281 }
282 }(n)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200283 }
284
Lorenz Brun9ce40712024-02-13 21:54:46 +0100285 // Wait for all update processes to finish
286 wg.Wait()
287
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200288 return nil
289 },
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +0200290 Args: PrintUsageOnWrongArgs(cobra.MinimumNArgs(1)),
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200291}
292
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100293var nodeDeleteCmd = &cobra.Command{
294 Short: "Deletes a node from the cluster.",
295 Use: "delete [NodeID] [--bypass-has-roles] [--bypass-not-decommissioned]",
296 Example: "metroctl node delete metropolis-25fa5f5e9349381d4a5e9e59de0215e3",
297 RunE: func(cmd *cobra.Command, args []string) error {
298 bypassHasRoles, err := cmd.Flags().GetBool("bypass-has-roles")
299 if err != nil {
300 return err
301 }
302
303 bypassNotDecommissioned, err := cmd.Flags().GetBool("bypass-not-decommissioned")
304 if err != nil {
305 return err
306 }
307
Tim Windelschmidtb765f242024-05-08 01:40:02 +0200308 ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
Tim Windelschmidt9bd9bd42025-02-14 17:08:52 +0100309 conn, err := newAuthenticatedClient(ctx)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200310 if err != nil {
311 return err
312 }
313 mgmt := apb.NewManagementClient(conn)
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100314
315 nodes, err := core.GetNodes(ctx, mgmt, fmt.Sprintf("node.id==%q", args[0]))
316 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +0200317 return fmt.Errorf("while calling Management.GetNodes: %w", err)
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100318 }
319
320 if len(nodes) == 0 {
321 return fmt.Errorf("could not find node with id: %s", args[0])
322 }
323
324 if len(nodes) != 1 {
325 return fmt.Errorf("expected one node, got %d", len(nodes))
326 }
327
328 n := nodes[0]
Lorenz Brun2542ef82024-08-20 13:33:02 +0200329 if n.Status != nil && n.Status.ExternalAddress != "" {
330 log.Printf("deleting node: %s (%s)", n.Id, n.Status.ExternalAddress)
331 } else {
332 log.Printf("deleting node: %s", n.Id)
333 }
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100334
335 req := &apb.DeleteNodeRequest{
336 Node: &apb.DeleteNodeRequest_Id{
337 Id: n.Id,
338 },
339 }
340
341 if bypassHasRoles {
342 req.SafetyBypassHasRoles = &apb.DeleteNodeRequest_SafetyBypassHasRoles{}
343 }
344
345 if bypassNotDecommissioned {
346 req.SafetyBypassNotDecommissioned = &apb.DeleteNodeRequest_SafetyBypassNotDecommissioned{}
347 }
348
349 _, err = mgmt.DeleteNode(ctx, req)
350 return err
351 },
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +0200352 Args: PrintUsageOnWrongArgs(cobra.ExactArgs(1)),
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100353}
354
Tim Windelschmidt9bd9bd42025-02-14 17:08:52 +0100355func newNodeClient(ctx context.Context, node string) (apb.NodeManagementClient, error) {
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000356 // First connect to the main management service and figure out the node's IP
357 // address.
Tim Windelschmidt9bd9bd42025-02-14 17:08:52 +0100358 cc, err := newAuthenticatedClient(ctx)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200359 if err != nil {
Tim Windelschmidt9bd9bd42025-02-14 17:08:52 +0100360 return nil, fmt.Errorf("while creating client: %w", err)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200361 }
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000362 mgmt := apb.NewManagementClient(cc)
363 nodes, err := core.GetNodes(ctx, mgmt, fmt.Sprintf("node.id == %q", node))
364 if err != nil {
365 return nil, fmt.Errorf("when getting node info: %w", err)
366 }
367
368 if len(nodes) == 0 {
369 return nil, fmt.Errorf("no such node")
370 }
371 if len(nodes) > 1 {
372 return nil, fmt.Errorf("expression matched more than one node")
373 }
374 n := nodes[0]
375 if n.Status == nil || n.Status.ExternalAddress == "" {
376 return nil, fmt.Errorf("node has no external address")
377 }
378
379 cacert, err := core.GetClusterCAWithTOFU(ctx, connectOptions())
380 if err != nil {
381 return nil, fmt.Errorf("could not get CA certificate: %w", err)
382 }
383
Tim Windelschmidt9bd9bd42025-02-14 17:08:52 +0100384 // Create a gprc client with the actual node and its management port.
385 cl, err := newAuthenticatedNodeClient(ctx, n.Id, n.Status.ExternalAddress, cacert)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200386 if err != nil {
Tim Windelschmidt9bd9bd42025-02-14 17:08:52 +0100387 return nil, fmt.Errorf("while creating client: %w", err)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200388 }
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000389 nmgmt := apb.NewNodeManagementClient(cl)
390 return nmgmt, nil
391}
392
393var nodeRebootCmd = &cobra.Command{
394 Short: "Reboot a node",
395 Long: `Reboot a node.
396
397This command can be used quite flexibly. Without any options it performs a
398normal, firmware-assisted reboot. It can roll back the last update by also
399passing the --rollback option. To reboot quicker the --kexec option can be used
400to skip firmware during reboot and boot straigt into the kernel.
401
402It can also be used to reboot into the firmware (BIOS) setup UI by passing the
403--firmware flag. This flag cannot be combined with any others.
404 `,
405 Use: "reboot [node-id]",
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +0200406 Args: PrintUsageOnWrongArgs(cobra.ExactArgs(1)),
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000407 SilenceUsage: true,
408 RunE: func(cmd *cobra.Command, args []string) error {
409 ctx := cmd.Context()
410
411 kexecFlag, err := cmd.Flags().GetBool("kexec")
412 if err != nil {
413 return err
414 }
415 rollbackFlag, err := cmd.Flags().GetBool("rollback")
416 if err != nil {
417 return err
418 }
419 firmwareFlag, err := cmd.Flags().GetBool("firmware")
420 if err != nil {
421 return err
422 }
423
424 if kexecFlag && firmwareFlag {
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200425 return fmt.Errorf("--kexec cannot be used with --firmware as firmware is not involved when using kexec")
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000426 }
427 if firmwareFlag && rollbackFlag {
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200428 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 +0000429 }
430 var req apb.RebootRequest
431 if kexecFlag {
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100432 req.Type = apb.RebootRequest_TYPE_KEXEC
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000433 } else {
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100434 req.Type = apb.RebootRequest_TYPE_FIRMWARE
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000435 }
436 if firmwareFlag {
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100437 req.NextBoot = apb.RebootRequest_NEXT_BOOT_START_FIRMWARE_UI
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000438 }
439 if rollbackFlag {
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100440 req.NextBoot = apb.RebootRequest_NEXT_BOOT_START_ROLLBACK
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000441 }
442
Tim Windelschmidt9bd9bd42025-02-14 17:08:52 +0100443 nmgmt, err := newNodeClient(ctx, args[0])
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000444 if err != nil {
Tim Windelschmidt9bd9bd42025-02-14 17:08:52 +0100445 return fmt.Errorf("failed to create node client: %w", err)
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000446 }
447
448 if _, err := nmgmt.Reboot(ctx, &req); err != nil {
449 return fmt.Errorf("reboot RPC failed: %w", err)
450 }
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200451 log.Printf("Node %v is being rebooted", args[0])
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000452
453 return nil
454 },
455}
456
457var nodePoweroffCmd = &cobra.Command{
458 Short: "Power off a node",
459 Use: "poweroff [node-id]",
Tim Windelschmidtfc6e1cf2024-09-18 17:34:07 +0200460 Args: PrintUsageOnWrongArgs(cobra.ExactArgs(1)),
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000461 SilenceUsage: true,
462 RunE: func(cmd *cobra.Command, args []string) error {
463 ctx := cmd.Context()
464
Tim Windelschmidt9bd9bd42025-02-14 17:08:52 +0100465 nmgmt, err := newNodeClient(ctx, args[0])
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000466 if err != nil {
467 return err
468 }
469
470 if _, err := nmgmt.Reboot(ctx, &apb.RebootRequest{
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100471 Type: apb.RebootRequest_TYPE_POWER_OFF,
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000472 }); err != nil {
473 return fmt.Errorf("reboot RPC failed: %w", err)
474 }
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200475 log.Printf("Node %v is being powered off", args[0])
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000476
477 return nil
478 },
479}
480
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100481func init() {
Jan Schär62cecde2025-04-16 15:24:04 +0000482 nodeUpdateCmd.Flags().String("image-ref", "", "Reference to the new version stored in an OCI registry, in the format [http[s]://]host[:port]/repository[:tag]@digest")
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200483 nodeUpdateCmd.Flags().String("activation-mode", "reboot", "How the update should be activated (kexec, reboot, none)")
Lorenz Brun9ce40712024-02-13 21:54:46 +0100484 nodeUpdateCmd.Flags().Uint64("max-unavailable", 1, "Maximum nodes which can be unavailable during the update process")
Lorenz Bruncceb6a32024-04-16 13:33:15 +0000485 nodeUpdateCmd.Flags().StringArray("exclude", nil, "List of nodes to exclude (useful with the \"all\" argument)")
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200486
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100487 nodeDeleteCmd.Flags().Bool("bypass-has-roles", false, "Allows to bypass the HasRoles check")
488 nodeDeleteCmd.Flags().Bool("bypass-not-decommissioned", false, "Allows to bypass the NotDecommissioned check")
489
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000490 nodeRebootCmd.Flags().Bool("rollback", false, "Reboot into the last OS version in the other slot")
491 nodeRebootCmd.Flags().Bool("firmware", false, "Reboot into the firmware (BIOS) setup UI")
492 nodeRebootCmd.Flags().Bool("kexec", false, "Use kexec to reboot much quicker without going through firmware")
493
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100494 nodeCmd.AddCommand(nodeDescribeCmd)
495 nodeCmd.AddCommand(nodeListCmd)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200496 nodeCmd.AddCommand(nodeUpdateCmd)
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100497 nodeCmd.AddCommand(nodeDeleteCmd)
Lorenz Bruncc32cc42024-09-09 20:14:05 +0000498 nodeCmd.AddCommand(nodeRebootCmd)
499 nodeCmd.AddCommand(nodePoweroffCmd)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100500 rootCmd.AddCommand(nodeCmd)
501}
502
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200503func printNodes(nodes []*apb.Node, args []string, onlyColumns map[string]bool) error {
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100504 o := io.WriteCloser(os.Stdout)
505 if flags.output != "" {
506 of, err := os.Create(flags.output)
507 if err != nil {
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200508 return fmt.Errorf("couldn't create the output file at %s: %w", flags.output, err)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100509 }
510 o = of
511 }
512
513 // Narrow down the output set to supplied node IDs, if any.
514 qids := make(map[string]bool)
515 if len(args) != 0 && args[0] != "all" {
516 for _, a := range args {
517 qids[a] = true
518 }
519 }
520
Serge Bazanskie0c06172023-09-19 12:28:16 +0000521 var t clitable.Table
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100522 for _, n := range nodes {
523 // Filter the information we want client-side.
524 if len(qids) != 0 {
Jan Schär39d9c242024-09-24 13:49:55 +0200525 if _, e := qids[n.Id]; !e {
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100526 continue
527 }
528 }
Serge Bazanskie0c06172023-09-19 12:28:16 +0000529 t.Add(nodeEntry(n))
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100530 }
531
Serge Bazanskie0c06172023-09-19 12:28:16 +0000532 t.Print(o, onlyColumns)
Tim Windelschmidt0b4fb8c2024-09-18 17:34:23 +0200533 return nil
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100534}