|  | package main | 
|  |  | 
|  | import ( | 
|  | "bufio" | 
|  | "context" | 
|  | "os" | 
|  | "sort" | 
|  | "strconv" | 
|  | "strings" | 
|  |  | 
|  | "github.com/packethost/packngo" | 
|  | "github.com/spf13/cobra" | 
|  | "k8s.io/klog/v2" | 
|  |  | 
|  | "source.monogon.dev/cloud/equinix/wrapngo" | 
|  | clicontext "source.monogon.dev/metropolis/cli/pkg/context" | 
|  | ) | 
|  |  | 
|  | var yoinkCmd = &cobra.Command{ | 
|  | Use: "yoink", | 
|  | Long: `This moves a specified amount of servers that match the given spec to a different metro. | 
|  | While spec is a easy to find argument that matches the equinix system spec e.g. w3amd.75xx24c.512.8160.x86, | 
|  | metro does not represent the public facing name. Instead it is the acutal datacenter name e.g. fr2"`, | 
|  | Short: "Move a server base on the spec from one to another project", | 
|  | Args:  cobra.NoArgs, | 
|  | Run:   doYoink, | 
|  | } | 
|  |  | 
|  | func init() { | 
|  | yoinkCmd.Flags().Int("count", 1, "how many machines should be moved") | 
|  | yoinkCmd.Flags().String("equinix_source_project", "", "from which project should the machine be yoinked") | 
|  | yoinkCmd.Flags().String("equinix_target_project", "", "to which project should the machine be moved") | 
|  | yoinkCmd.Flags().String("spec", "", "which device spec should be moved") | 
|  | yoinkCmd.Flags().String("metro", "", "to which metro should be moved") | 
|  | rootCmd.AddCommand(yoinkCmd) | 
|  | } | 
|  |  | 
|  | func doYoink(cmd *cobra.Command, args []string) { | 
|  | srcProject, err := cmd.Flags().GetString("equinix_source_project") | 
|  | if err != nil { | 
|  | klog.Exitf("flag: %v", err) | 
|  | } | 
|  |  | 
|  | dstProject, err := cmd.Flags().GetString("equinix_target_project") | 
|  | if err != nil { | 
|  | klog.Exitf("flag: %v", err) | 
|  | } | 
|  |  | 
|  | if srcProject == "" || dstProject == "" { | 
|  | klog.Exitf("missing project flags") | 
|  | } | 
|  |  | 
|  | count, err := cmd.Flags().GetInt("count") | 
|  | if err != nil { | 
|  | klog.Exitf("flag: %v", err) | 
|  | } | 
|  |  | 
|  | spec, err := cmd.Flags().GetString("spec") | 
|  | if err != nil { | 
|  | klog.Exitf("flag: %v", err) | 
|  | } | 
|  |  | 
|  | if spec == "" { | 
|  | klog.Exitf("missing spec flag") | 
|  | } | 
|  |  | 
|  | metro, err := cmd.Flags().GetString("metro") | 
|  | if err != nil { | 
|  | klog.Exitf("flag: %v", err) | 
|  | } | 
|  |  | 
|  | if metro == "" { | 
|  | klog.Exitf("missing metro flag") | 
|  | } | 
|  |  | 
|  | ctx := clicontext.WithInterrupt(context.Background()) | 
|  | api := wrapngo.New(&c) | 
|  |  | 
|  | klog.Infof("Listing reservations for %q", srcProject) | 
|  | reservations, err := api.ListReservations(ctx, srcProject) | 
|  | if err != nil { | 
|  | klog.Exitf("Failed to list reservations: %v", err) | 
|  | } | 
|  |  | 
|  | type configDC struct { | 
|  | config string | 
|  | dc     string | 
|  | } | 
|  | mtypes := make(map[configDC]int) | 
|  |  | 
|  | var matchingReservations []packngo.HardwareReservation | 
|  | reqType := configDC{config: strings.ToLower(spec), dc: strings.ToLower(metro)} | 
|  |  | 
|  | klog.Infof("Got %d reservations", len(reservations)) | 
|  | for _, r := range reservations { | 
|  | curType := configDC{config: strings.ToLower(r.Plan.Name), dc: strings.ToLower(r.Facility.Metro.Code)} | 
|  |  | 
|  | mtypes[curType]++ | 
|  | if curType == reqType { | 
|  | matchingReservations = append(matchingReservations, r) | 
|  | } | 
|  | } | 
|  |  | 
|  | klog.Infof("Found the following configurations:") | 
|  | for dc, c := range mtypes { | 
|  | klog.Infof("%s | %s | %d", dc.dc, dc.config, c) | 
|  | } | 
|  |  | 
|  | if len(matchingReservations) == 0 { | 
|  | klog.Exitf("Configuration not found: %s - %s", reqType.dc, reqType.config) | 
|  | } | 
|  |  | 
|  | if len(matchingReservations)-count < 0 { | 
|  | klog.Exitf("Not enough machines with matching configuration found ") | 
|  | } | 
|  |  | 
|  | // prefer hosts that are not deployed | 
|  | sort.Slice(matchingReservations, func(i, j int) bool { | 
|  | return matchingReservations[i].Device == nil && matchingReservations[j].Device != nil | 
|  | }) | 
|  |  | 
|  | toMove := matchingReservations[:count] | 
|  | var toDelete []string | 
|  | for _, r := range toMove { | 
|  | if r.Device != nil { | 
|  | toDelete = append(toDelete, r.Device.Hostname) | 
|  | } | 
|  | } | 
|  |  | 
|  | stdInReader := bufio.NewReader(os.Stdin) | 
|  | klog.Infof("Will move %d machines with spec %s in %s from %s to %s.", count, spec, metro, srcProject, dstProject) | 
|  | if len(toDelete) > 0 { | 
|  | klog.Warningf("Not enough free machines found. This will delete %d provisioned hosts! Hosts scheduled for deletion: ", len(toDelete)) | 
|  | klog.Warningf("%s", strings.Join(toDelete, ", ")) | 
|  | klog.Warningf("Please confirm by inputting in the number of machines that will be moved.") | 
|  |  | 
|  | read, err := stdInReader.ReadString('\n') | 
|  | if err != nil { | 
|  | klog.Exitf("failed reading input: %v", err) | 
|  | } | 
|  |  | 
|  | atoi, err := strconv.Atoi(strings.TrimSpace(read)) | 
|  | if err != nil { | 
|  | klog.Exitf("failed parsing number: %v", err) | 
|  | } | 
|  |  | 
|  | if atoi != len(toDelete) { | 
|  | klog.Exitf("Confirmation failed! Wanted \"%q\" got \"%d\"", len(toDelete), atoi) | 
|  | } else { | 
|  | klog.Infof("Thanks for the confirmation! continuing...") | 
|  | } | 
|  | } | 
|  |  | 
|  | 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") | 
|  | for _, r := range matchingReservations[:count] { | 
|  | if r.Device != nil { | 
|  | klog.Warningf("Deleting server %s (%s) on %s", r.Device.ID, r.Device.Hostname, r.ID) | 
|  |  | 
|  | if err := api.DeleteDevice(ctx, r.Device.ID); err != nil { | 
|  | klog.Errorf("failed deleting device %s (%s): %v", r.Device.ID, r.Device.Hostname, err) | 
|  | continue | 
|  | } | 
|  | } | 
|  |  | 
|  | _, err := api.MoveReservation(ctx, r.ID, dstProject) | 
|  | if err != nil { | 
|  | klog.Errorf("failed moving device %s: %v", r.ID, err) | 
|  | } | 
|  | } | 
|  | } |