blob: 7196d42d1437bb7a263e85e312d06022af978b9a [file] [log] [blame]
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(args[0]), dc: strings.ToLower(args[1])}
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(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 {
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)
}
}
}