blob: a582f023bc81e6beff50ed840aaaf130094fb036 [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 Windelschmidt3b25cf72023-07-17 16:58:10 +02009 "strings"
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010010
11 "github.com/spf13/cobra"
12
Serge Bazanskie0c06172023-09-19 12:28:16 +000013 "source.monogon.dev/go/clitable"
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010014 "source.monogon.dev/metropolis/cli/metroctl/core"
15 clicontext "source.monogon.dev/metropolis/cli/pkg/context"
16 "source.monogon.dev/metropolis/node/core/identity"
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +020017
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +010018 apb "source.monogon.dev/metropolis/proto/api"
19)
20
21var nodeCmd = &cobra.Command{
22 Short: "Updates and queries node information.",
23 Use: "node",
24}
25
26var nodeDescribeCmd = &cobra.Command{
27 Short: "Describes cluster nodes.",
28 Use: "describe [node-id] [--filter] [--output] [--format]",
29 Example: "metroctl node describe metropolis-c556e31c3fa2bf0a36e9ccb9fd5d6056",
30 Run: func(cmd *cobra.Command, args []string) {
31 ctx := clicontext.WithInterrupt(context.Background())
32 cc := dialAuthenticated(ctx)
33 mgmt := apb.NewManagementClient(cc)
34
35 nodes, err := core.GetNodes(ctx, mgmt, flags.filter)
36 if err != nil {
37 log.Fatalf("While calling Management.GetNodes: %v", err)
38 }
39
40 printNodes(nodes, args, nil)
41 },
42 Args: cobra.ArbitraryArgs,
43}
44
45var nodeListCmd = &cobra.Command{
46 Short: "Lists cluster nodes.",
47 Use: "list [node-id] [--filter] [--output] [--format]",
48 Example: "metroctl node list --filter node.status.external_address==\"10.8.0.2\"",
49 Run: func(cmd *cobra.Command, args []string) {
50 ctx := clicontext.WithInterrupt(context.Background())
51 cc := dialAuthenticated(ctx)
52 mgmt := apb.NewManagementClient(cc)
53
54 nodes, err := core.GetNodes(ctx, mgmt, flags.filter)
55 if err != nil {
56 log.Fatalf("While calling Management.GetNodes: %v", err)
57 }
58
59 printNodes(nodes, args, map[string]bool{"node id": true})
60 },
61 Args: cobra.ArbitraryArgs,
62}
63
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +020064var nodeUpdateCmd = &cobra.Command{
65 Short: "Updates the operating system of a cluster node.",
66 Use: "update [NodeID] --bundle-url bundleURL [--activation-mode <none|reboot|kexec>]",
67 Example: "metroctl node update --bundle-url https://example.com/bundle.zip --activation-mode reboot metropolis-25fa5f5e9349381d4a5e9e59de0215e3",
68 RunE: func(cmd *cobra.Command, args []string) error {
69 bundleUrl, err := cmd.Flags().GetString("bundle-url")
70 if err != nil {
71 return err
72 }
73
74 if len(bundleUrl) == 0 {
75 return fmt.Errorf("flag bundle-url is required")
76 }
77
78 activationMode, err := cmd.Flags().GetString("activation-mode")
79 if err != nil {
80 return err
81 }
82
83 var am apb.ActivationMode
84 switch strings.ToLower(activationMode) {
85 case "none":
86 am = apb.ActivationMode_ACTIVATION_NONE
87 case "reboot":
88 am = apb.ActivationMode_ACTIVATION_REBOOT
89 case "kexec":
90 am = apb.ActivationMode_ACTIVATION_KEXEC
91 default:
92 return fmt.Errorf("invalid value for flag activation-mode")
93 }
94
95 ctx := clicontext.WithInterrupt(context.Background())
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +020096
Serge Bazanskic51d47d2024-02-13 18:40:26 +010097 cacert, err := core.GetClusterCAWithTOFU(ctx, connectOptions())
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +020098 if err != nil {
Serge Bazanskic51d47d2024-02-13 18:40:26 +010099 return fmt.Errorf("could not get CA certificate: %w", err)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200100 }
Serge Bazanskic51d47d2024-02-13 18:40:26 +0100101
102 mgmt := apb.NewManagementClient(dialAuthenticated(ctx))
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200103
104 nodes, err := core.GetNodes(ctx, mgmt, "")
105 if err != nil {
106 return fmt.Errorf("while calling Management.GetNodes: %v", err)
107 }
108 // Narrow down the output set to supplied node IDs, if any.
109 qids := make(map[string]bool)
110 if len(args) != 0 && args[0] != "all" {
111 for _, a := range args {
112 qids[a] = true
113 }
114 }
115
116 updateReq := &apb.UpdateNodeRequest{
117 BundleUrl: bundleUrl,
118 ActivationMode: am,
119 }
120
121 for _, n := range nodes {
122 // Filter the information we want client-side.
123 if len(qids) != 0 {
124 nid := identity.NodeID(n.Pubkey)
125 if _, e := qids[nid]; !e {
126 continue
127 }
128 }
129
130 cc := dialAuthenticatedNode(ctx, n.Id, n.Status.ExternalAddress, cacert)
131 nodeMgmt := apb.NewNodeManagementClient(cc)
132 log.Printf("sending update request to: %s (%s)", n.Id, n.Status.ExternalAddress)
133 _, err := nodeMgmt.UpdateNode(ctx, updateReq)
134 if err != nil {
135 return err
136 }
137 }
138
139 return nil
140 },
141 Args: cobra.ExactArgs(1),
142}
143
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100144var nodeDeleteCmd = &cobra.Command{
145 Short: "Deletes a node from the cluster.",
146 Use: "delete [NodeID] [--bypass-has-roles] [--bypass-not-decommissioned]",
147 Example: "metroctl node delete metropolis-25fa5f5e9349381d4a5e9e59de0215e3",
148 RunE: func(cmd *cobra.Command, args []string) error {
149 bypassHasRoles, err := cmd.Flags().GetBool("bypass-has-roles")
150 if err != nil {
151 return err
152 }
153
154 bypassNotDecommissioned, err := cmd.Flags().GetBool("bypass-not-decommissioned")
155 if err != nil {
156 return err
157 }
158
159 ctx := clicontext.WithInterrupt(context.Background())
160 mgmt := apb.NewManagementClient(dialAuthenticated(ctx))
161
162 nodes, err := core.GetNodes(ctx, mgmt, fmt.Sprintf("node.id==%q", args[0]))
163 if err != nil {
164 return fmt.Errorf("while calling Management.GetNodes: %v", err)
165 }
166
167 if len(nodes) == 0 {
168 return fmt.Errorf("could not find node with id: %s", args[0])
169 }
170
171 if len(nodes) != 1 {
172 return fmt.Errorf("expected one node, got %d", len(nodes))
173 }
174
175 n := nodes[0]
176 log.Printf("deleting node: %s (%s)", n.Id, n.Status.ExternalAddress)
177
178 req := &apb.DeleteNodeRequest{
179 Node: &apb.DeleteNodeRequest_Id{
180 Id: n.Id,
181 },
182 }
183
184 if bypassHasRoles {
185 req.SafetyBypassHasRoles = &apb.DeleteNodeRequest_SafetyBypassHasRoles{}
186 }
187
188 if bypassNotDecommissioned {
189 req.SafetyBypassNotDecommissioned = &apb.DeleteNodeRequest_SafetyBypassNotDecommissioned{}
190 }
191
192 _, err = mgmt.DeleteNode(ctx, req)
193 return err
194 },
195 Args: cobra.ExactArgs(1),
196}
197
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100198func init() {
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200199 nodeUpdateCmd.Flags().String("bundle-url", "", "The URL to the new version")
200 nodeUpdateCmd.Flags().String("activation-mode", "reboot", "How the update should be activated (kexec, reboot, none)")
201
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100202 nodeDeleteCmd.Flags().Bool("bypass-has-roles", false, "Allows to bypass the HasRoles check")
203 nodeDeleteCmd.Flags().Bool("bypass-not-decommissioned", false, "Allows to bypass the NotDecommissioned check")
204
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100205 nodeCmd.AddCommand(nodeDescribeCmd)
206 nodeCmd.AddCommand(nodeListCmd)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200207 nodeCmd.AddCommand(nodeUpdateCmd)
Tim Windelschmidt7dbf18c2023-10-31 22:39:42 +0100208 nodeCmd.AddCommand(nodeDeleteCmd)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100209 rootCmd.AddCommand(nodeCmd)
210}
211
212func printNodes(nodes []*apb.Node, args []string, onlyColumns map[string]bool) {
213 o := io.WriteCloser(os.Stdout)
214 if flags.output != "" {
215 of, err := os.Create(flags.output)
216 if err != nil {
217 log.Fatalf("Couldn't create the output file at %s: %v", flags.output, err)
218 }
219 o = of
220 }
221
222 // Narrow down the output set to supplied node IDs, if any.
223 qids := make(map[string]bool)
224 if len(args) != 0 && args[0] != "all" {
225 for _, a := range args {
226 qids[a] = true
227 }
228 }
229
Serge Bazanskie0c06172023-09-19 12:28:16 +0000230 var t clitable.Table
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100231 for _, n := range nodes {
232 // Filter the information we want client-side.
233 if len(qids) != 0 {
234 nid := identity.NodeID(n.Pubkey)
235 if _, e := qids[nid]; !e {
236 continue
237 }
238 }
Serge Bazanskie0c06172023-09-19 12:28:16 +0000239 t.Add(nodeEntry(n))
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100240 }
241
Serge Bazanskie0c06172023-09-19 12:28:16 +0000242 t.Print(o, onlyColumns)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100243}