| 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/shepherd/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) | 
 | 		} | 
 | 	} | 
 | } |