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