blob: 1de5803f1b95a3bf04af11f8b1d82f7c5843c9d0 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Tim Windelschmidt886c3862023-05-23 16:47:41 +02004package main
5
6import (
7 "bufio"
8 "context"
9 "os"
Tim Windelschmidtb765f242024-05-08 01:40:02 +020010 "os/signal"
Tim Windelschmidt886c3862023-05-23 16:47:41 +020011 "sort"
12 "strconv"
13 "strings"
14
15 "github.com/packethost/packngo"
16 "github.com/spf13/cobra"
17 "k8s.io/klog/v2"
18
Tim Windelschmidtb6308cd2023-10-10 21:19:03 +020019 "source.monogon.dev/cloud/equinix/wrapngo"
Tim Windelschmidt886c3862023-05-23 16:47:41 +020020)
21
22var yoinkCmd = &cobra.Command{
23 Use: "yoink",
24 Long: `This moves a specified amount of servers that match the given spec to a different metro.
25While spec is a easy to find argument that matches the equinix system spec e.g. w3amd.75xx24c.512.8160.x86,
26metro does not represent the public facing name. Instead it is the acutal datacenter name e.g. fr2"`,
27 Short: "Move a server base on the spec from one to another project",
28 Args: cobra.NoArgs,
29 Run: doYoink,
30}
31
32func init() {
33 yoinkCmd.Flags().Int("count", 1, "how many machines should be moved")
34 yoinkCmd.Flags().String("equinix_source_project", "", "from which project should the machine be yoinked")
35 yoinkCmd.Flags().String("equinix_target_project", "", "to which project should the machine be moved")
36 yoinkCmd.Flags().String("spec", "", "which device spec should be moved")
37 yoinkCmd.Flags().String("metro", "", "to which metro should be moved")
38 rootCmd.AddCommand(yoinkCmd)
39}
40
41func doYoink(cmd *cobra.Command, args []string) {
42 srcProject, err := cmd.Flags().GetString("equinix_source_project")
43 if err != nil {
44 klog.Exitf("flag: %v", err)
45 }
46
47 dstProject, err := cmd.Flags().GetString("equinix_target_project")
48 if err != nil {
49 klog.Exitf("flag: %v", err)
50 }
51
52 if srcProject == "" || dstProject == "" {
53 klog.Exitf("missing project flags")
54 }
55
56 count, err := cmd.Flags().GetInt("count")
57 if err != nil {
58 klog.Exitf("flag: %v", err)
59 }
60
61 spec, err := cmd.Flags().GetString("spec")
62 if err != nil {
63 klog.Exitf("flag: %v", err)
64 }
65
66 if spec == "" {
67 klog.Exitf("missing spec flag")
68 }
69
70 metro, err := cmd.Flags().GetString("metro")
71 if err != nil {
72 klog.Exitf("flag: %v", err)
73 }
74
75 if metro == "" {
76 klog.Exitf("missing metro flag")
77 }
78
Tim Windelschmidtb765f242024-05-08 01:40:02 +020079 ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
Tim Windelschmidt886c3862023-05-23 16:47:41 +020080 api := wrapngo.New(&c)
81
82 klog.Infof("Listing reservations for %q", srcProject)
83 reservations, err := api.ListReservations(ctx, srcProject)
84 if err != nil {
85 klog.Exitf("Failed to list reservations: %v", err)
86 }
87
88 type configDC struct {
89 config string
90 dc string
91 }
92 mtypes := make(map[configDC]int)
93
94 var matchingReservations []packngo.HardwareReservation
Tim Windelschmidt12c81402023-07-03 02:16:08 +020095 reqType := configDC{config: strings.ToLower(spec), dc: strings.ToLower(metro)}
Tim Windelschmidt886c3862023-05-23 16:47:41 +020096
97 klog.Infof("Got %d reservations", len(reservations))
98 for _, r := range reservations {
99 curType := configDC{config: strings.ToLower(r.Plan.Name), dc: strings.ToLower(r.Facility.Metro.Code)}
100
101 mtypes[curType]++
102 if curType == reqType {
103 matchingReservations = append(matchingReservations, r)
104 }
105 }
106
107 klog.Infof("Found the following configurations:")
108 for dc, c := range mtypes {
109 klog.Infof("%s | %s | %d", dc.dc, dc.config, c)
110 }
111
112 if len(matchingReservations) == 0 {
113 klog.Exitf("Configuration not found: %s - %s", reqType.dc, reqType.config)
114 }
115
116 if len(matchingReservations)-count < 0 {
117 klog.Exitf("Not enough machines with matching configuration found ")
118 }
119
120 // prefer hosts that are not deployed
121 sort.Slice(matchingReservations, func(i, j int) bool {
122 return matchingReservations[i].Device == nil && matchingReservations[j].Device != nil
123 })
124
125 toMove := matchingReservations[:count]
126 var toDelete []string
127 for _, r := range toMove {
128 if r.Device != nil {
129 toDelete = append(toDelete, r.Device.Hostname)
130 }
131 }
132
133 stdInReader := bufio.NewReader(os.Stdin)
134 klog.Infof("Will move %d machines with spec %s in %s from %s to %s.", count, spec, metro, srcProject, dstProject)
135 if len(toDelete) > 0 {
136 klog.Warningf("Not enough free machines found. This will delete %d provisioned hosts! Hosts scheduled for deletion: ", len(toDelete))
137 klog.Warningf("%s", strings.Join(toDelete, ", "))
138 klog.Warningf("Please confirm by inputting in the number of machines that will be moved.")
139
140 read, err := stdInReader.ReadString('\n')
141 if err != nil {
142 klog.Exitf("failed reading input: %v", err)
143 }
144
Tim Windelschmidt12c81402023-07-03 02:16:08 +0200145 atoi, err := strconv.Atoi(strings.TrimSpace(read))
Tim Windelschmidt886c3862023-05-23 16:47:41 +0200146 if err != nil {
147 klog.Exitf("failed parsing number: %v", err)
148 }
149
150 if atoi != len(toDelete) {
151 klog.Exitf("Confirmation failed! Wanted \"%q\" got \"%d\"", len(toDelete), atoi)
152 } else {
153 klog.Infof("Thanks for the confirmation! continuing...")
154 }
155 }
156
157 klog.Infof("Note: It can be normal for a device move to fail for project validation issues. This is a known issue and can be ignored")
Tim Windelschmidt12c81402023-07-03 02:16:08 +0200158 for _, r := range matchingReservations[:count] {
Tim Windelschmidt886c3862023-05-23 16:47:41 +0200159 if r.Device != nil {
160 klog.Warningf("Deleting server %s (%s) on %s", r.Device.ID, r.Device.Hostname, r.ID)
161
162 if err := api.DeleteDevice(ctx, r.Device.ID); err != nil {
163 klog.Errorf("failed deleting device %s (%s): %v", r.Device.ID, r.Device.Hostname, err)
164 continue
165 }
166 }
167
168 _, err := api.MoveReservation(ctx, r.ID, dstProject)
169 if err != nil {
170 klog.Errorf("failed moving device %s: %v", r.ID, err)
171 }
172 }
173}