Tim Windelschmidt | 886c386 | 2023-05-23 16:47:41 +0200 | [diff] [blame] | 1 | package main |
| 2 | |
| 3 | import ( |
| 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 | |
| 19 | var yoinkCmd = &cobra.Command{ |
| 20 | Use: "yoink", |
| 21 | Long: `This moves a specified amount of servers that match the given spec to a different metro. |
| 22 | While spec is a easy to find argument that matches the equinix system spec e.g. w3amd.75xx24c.512.8160.x86, |
| 23 | metro 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 | |
| 29 | func 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 | |
| 38 | func 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 | } |