blob: b9692b6feced9a0edf953ba166fb0de62b62382d [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
14 "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())
96 mgmt := apb.NewManagementClient(dialAuthenticated(ctx))
97
98 // TODO(q3k): save CA certificate on takeover
99 info, err := mgmt.GetClusterInfo(ctx, &apb.GetClusterInfoRequest{})
100 if err != nil {
101 return fmt.Errorf("couldn't get cluster info: %w", err)
102 }
103 cacert, err := x509.ParseCertificate(info.CaCertificate)
104 if err != nil {
105 return fmt.Errorf("remote CA certificate invalid: %w", err)
106 }
107
108 nodes, err := core.GetNodes(ctx, mgmt, "")
109 if err != nil {
110 return fmt.Errorf("while calling Management.GetNodes: %v", err)
111 }
112 // Narrow down the output set to supplied node IDs, if any.
113 qids := make(map[string]bool)
114 if len(args) != 0 && args[0] != "all" {
115 for _, a := range args {
116 qids[a] = true
117 }
118 }
119
120 updateReq := &apb.UpdateNodeRequest{
121 BundleUrl: bundleUrl,
122 ActivationMode: am,
123 }
124
125 for _, n := range nodes {
126 // Filter the information we want client-side.
127 if len(qids) != 0 {
128 nid := identity.NodeID(n.Pubkey)
129 if _, e := qids[nid]; !e {
130 continue
131 }
132 }
133
134 cc := dialAuthenticatedNode(ctx, n.Id, n.Status.ExternalAddress, cacert)
135 nodeMgmt := apb.NewNodeManagementClient(cc)
136 log.Printf("sending update request to: %s (%s)", n.Id, n.Status.ExternalAddress)
137 _, err := nodeMgmt.UpdateNode(ctx, updateReq)
138 if err != nil {
139 return err
140 }
141 }
142
143 return nil
144 },
145 Args: cobra.ExactArgs(1),
146}
147
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100148func init() {
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200149 nodeUpdateCmd.Flags().String("bundle-url", "", "The URL to the new version")
150 nodeUpdateCmd.Flags().String("activation-mode", "reboot", "How the update should be activated (kexec, reboot, none)")
151
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100152 nodeCmd.AddCommand(nodeDescribeCmd)
153 nodeCmd.AddCommand(nodeListCmd)
Tim Windelschmidt3b25cf72023-07-17 16:58:10 +0200154 nodeCmd.AddCommand(nodeUpdateCmd)
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100155 rootCmd.AddCommand(nodeCmd)
156}
157
158func printNodes(nodes []*apb.Node, args []string, onlyColumns map[string]bool) {
159 o := io.WriteCloser(os.Stdout)
160 if flags.output != "" {
161 of, err := os.Create(flags.output)
162 if err != nil {
163 log.Fatalf("Couldn't create the output file at %s: %v", flags.output, err)
164 }
165 o = of
166 }
167
168 // Narrow down the output set to supplied node IDs, if any.
169 qids := make(map[string]bool)
170 if len(args) != 0 && args[0] != "all" {
171 for _, a := range args {
172 qids[a] = true
173 }
174 }
175
176 var t table
177 for _, n := range nodes {
178 // Filter the information we want client-side.
179 if len(qids) != 0 {
180 nid := identity.NodeID(n.Pubkey)
181 if _, e := qids[nid]; !e {
182 continue
183 }
184 }
185 t.add(nodeEntry(n))
186 }
187
188 t.print(o, onlyColumns)
189}