| Tim Windelschmidt | 6d33a43 | 2025-02-04 14:34:25 +0100 | [diff] [blame^] | 1 | // Copyright The Monogon Project Authors. |
| 2 | // SPDX-License-Identifier: Apache-2.0 |
| 3 | |
| Tim Windelschmidt | 886c386 | 2023-05-23 16:47:41 +0200 | [diff] [blame] | 4 | package main |
| 5 | |
| 6 | import ( |
| 7 | "bufio" |
| 8 | "context" |
| 9 | "os" |
| Tim Windelschmidt | b765f24 | 2024-05-08 01:40:02 +0200 | [diff] [blame] | 10 | "os/signal" |
| Tim Windelschmidt | 886c386 | 2023-05-23 16:47:41 +0200 | [diff] [blame] | 11 | "sort" |
| 12 | "strconv" |
| 13 | "strings" |
| 14 | |
| 15 | "github.com/packethost/packngo" |
| 16 | "github.com/spf13/cobra" |
| 17 | "k8s.io/klog/v2" |
| 18 | |
| Tim Windelschmidt | b6308cd | 2023-10-10 21:19:03 +0200 | [diff] [blame] | 19 | "source.monogon.dev/cloud/equinix/wrapngo" |
| Tim Windelschmidt | 886c386 | 2023-05-23 16:47:41 +0200 | [diff] [blame] | 20 | ) |
| 21 | |
| 22 | var yoinkCmd = &cobra.Command{ |
| 23 | Use: "yoink", |
| 24 | Long: `This moves a specified amount of servers that match the given spec to a different metro. |
| 25 | While spec is a easy to find argument that matches the equinix system spec e.g. w3amd.75xx24c.512.8160.x86, |
| 26 | metro 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 | |
| 32 | func 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 | |
| 41 | func 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 Windelschmidt | b765f24 | 2024-05-08 01:40:02 +0200 | [diff] [blame] | 79 | ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt) |
| Tim Windelschmidt | 886c386 | 2023-05-23 16:47:41 +0200 | [diff] [blame] | 80 | 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 Windelschmidt | 12c8140 | 2023-07-03 02:16:08 +0200 | [diff] [blame] | 95 | reqType := configDC{config: strings.ToLower(spec), dc: strings.ToLower(metro)} |
| Tim Windelschmidt | 886c386 | 2023-05-23 16:47:41 +0200 | [diff] [blame] | 96 | |
| 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 Windelschmidt | 12c8140 | 2023-07-03 02:16:08 +0200 | [diff] [blame] | 145 | atoi, err := strconv.Atoi(strings.TrimSpace(read)) |
| Tim Windelschmidt | 886c386 | 2023-05-23 16:47:41 +0200 | [diff] [blame] | 146 | 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 Windelschmidt | 12c8140 | 2023-07-03 02:16:08 +0200 | [diff] [blame] | 158 | for _, r := range matchingReservations[:count] { |
| Tim Windelschmidt | 886c386 | 2023-05-23 16:47:41 +0200 | [diff] [blame] | 159 | 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 | } |