| package main | 
 |  | 
 | import ( | 
 | 	"context" | 
 | 	"fmt" | 
 | 	"net/netip" | 
 | 	"slices" | 
 | 	"strings" | 
 | 	"time" | 
 |  | 
 | 	"github.com/packethost/packngo" | 
 | 	"k8s.io/klog/v2" | 
 |  | 
 | 	"source.monogon.dev/cloud/bmaas/bmdb" | 
 | 	"source.monogon.dev/cloud/bmaas/bmdb/model" | 
 | 	"source.monogon.dev/cloud/equinix/wrapngo" | 
 | 	"source.monogon.dev/cloud/lib/sinbin" | 
 | 	"source.monogon.dev/cloud/shepherd" | 
 | 	"source.monogon.dev/cloud/shepherd/manager" | 
 | ) | 
 |  | 
 | type equinixProvider struct { | 
 | 	config *providerConfig | 
 | 	api    wrapngo.Client | 
 | 	sshKey *manager.SSHKey | 
 |  | 
 | 	// badReservations is a holiday resort for Equinix hardware reservations which | 
 | 	// failed to be provisioned for some reason or another. We keep a list of them in | 
 | 	// memory just so that we don't repeatedly try to provision the same known bad | 
 | 	// machines. | 
 | 	badReservations sinbin.Sinbin[string] | 
 |  | 
 | 	reservationDeadline time.Time | 
 | 	reservationCache    []packngo.HardwareReservation | 
 | } | 
 |  | 
 | func (ep *equinixProvider) RebootMachine(ctx context.Context, id shepherd.ProviderID) error { | 
 | 	if err := ep.api.RebootDevice(ctx, string(id)); err != nil { | 
 | 		return fmt.Errorf("failed to reboot device: %w", err) | 
 | 	} | 
 |  | 
 | 	// TODO(issue/215): replace this | 
 | 	// This is required as Equinix doesn't reboot the machines synchronously | 
 | 	// during the API call. | 
 | 	select { | 
 | 	case <-time.After(time.Duration(ep.config.RebootWaitSeconds) * time.Second): | 
 | 	case <-ctx.Done(): | 
 | 		return fmt.Errorf("while waiting for reboot: %w", ctx.Err()) | 
 | 	} | 
 | 	return nil | 
 | } | 
 |  | 
 | func (ep *equinixProvider) ReinstallMachine(ctx context.Context, id shepherd.ProviderID) error { | 
 | 	return shepherd.ErrNotImplemented | 
 | } | 
 |  | 
 | func (ep *equinixProvider) GetMachine(ctx context.Context, id shepherd.ProviderID) (shepherd.Machine, error) { | 
 | 	machines, err := ep.ListMachines(ctx) | 
 | 	if err != nil { | 
 | 		return nil, err | 
 | 	} | 
 |  | 
 | 	for _, machine := range machines { | 
 | 		if machine.ID() == id { | 
 | 			return machine, nil | 
 | 		} | 
 | 	} | 
 |  | 
 | 	return nil, shepherd.ErrMachineNotFound | 
 | } | 
 |  | 
 | func (ep *equinixProvider) ListMachines(ctx context.Context) ([]shepherd.Machine, error) { | 
 | 	if ep.reservationDeadline.Before(time.Now()) { | 
 | 		reservations, err := ep.listReservations(ctx) | 
 | 		if err != nil { | 
 | 			return nil, err | 
 | 		} | 
 | 		ep.reservationCache = reservations | 
 | 		ep.reservationDeadline = time.Now().Add(ep.config.ReservationCacheTimeout) | 
 | 	} | 
 |  | 
 | 	devices, err := ep.managedDevices(ctx) | 
 | 	if err != nil { | 
 | 		return nil, err | 
 | 	} | 
 |  | 
 | 	machines := make([]shepherd.Machine, 0, len(ep.reservationCache)+len(devices)) | 
 | 	for _, device := range devices { | 
 | 		machines = append(machines, &machine{device}) | 
 | 	} | 
 |  | 
 | 	for _, res := range ep.reservationCache { | 
 | 		machines = append(machines, reservation{res}) | 
 | 	} | 
 |  | 
 | 	return machines, nil | 
 | } | 
 |  | 
 | func (ep *equinixProvider) CreateMachine(ctx context.Context, session *bmdb.Session, request shepherd.CreateMachineRequest) (shepherd.Machine, error) { | 
 | 	if request.UnusedMachine == nil { | 
 | 		return nil, fmt.Errorf("parameter UnusedMachine is missing") | 
 | 	} | 
 |  | 
 | 	//TODO: Do we just trust the implementation to be correct? | 
 | 	res, ok := request.UnusedMachine.(reservation) | 
 | 	if !ok { | 
 | 		return nil, fmt.Errorf("invalid type for parameter UnusedMachine") | 
 | 	} | 
 |  | 
 | 	d, err := ep.provision(ctx, session, res.HardwareReservation) | 
 | 	if err != nil { | 
 | 		klog.Errorf("Failed to provision reservation %s: %v", res.HardwareReservation.ID, err) | 
 | 		until := time.Now().Add(time.Hour) | 
 | 		klog.Errorf("Adding hardware reservation %s to sinbin until %s", res.HardwareReservation.ID, until) | 
 | 		ep.badReservations.Add(res.HardwareReservation.ID, until) | 
 | 		return nil, err | 
 | 	} | 
 |  | 
 | 	return &machine{*d}, nil | 
 | } | 
 |  | 
 | func (ep *equinixProvider) Type() model.Provider { | 
 | 	return model.ProviderEquinix | 
 | } | 
 |  | 
 | type reservation struct { | 
 | 	packngo.HardwareReservation | 
 | } | 
 |  | 
 | func (e reservation) ID() shepherd.ProviderID { | 
 | 	return shepherd.InvalidProviderID | 
 | } | 
 |  | 
 | func (e reservation) Addr() netip.Addr { | 
 | 	return netip.Addr{} | 
 | } | 
 |  | 
 | func (e reservation) State() shepherd.State { | 
 | 	return shepherd.StateKnownUnused | 
 | } | 
 |  | 
 | type machine struct { | 
 | 	packngo.Device | 
 | } | 
 |  | 
 | func (e *machine) ID() shepherd.ProviderID { | 
 | 	return shepherd.ProviderID(e.Device.ID) | 
 | } | 
 |  | 
 | func (e *machine) Addr() netip.Addr { | 
 | 	ni := e.GetNetworkInfo() | 
 |  | 
 | 	var addr string | 
 | 	if ni.PublicIPv4 != "" { | 
 | 		addr = ni.PublicIPv4 | 
 | 	} else if ni.PublicIPv6 != "" { | 
 | 		addr = ni.PublicIPv6 | 
 | 	} else { | 
 | 		klog.Errorf("missing address for machine: %v", e.ID()) | 
 | 		return netip.Addr{} | 
 | 	} | 
 |  | 
 | 	a, err := netip.ParseAddr(addr) | 
 | 	if err != nil { | 
 | 		klog.Errorf("failed parsing address %q: %v", addr, err) | 
 | 		return netip.Addr{} | 
 | 	} | 
 |  | 
 | 	return a | 
 | } | 
 |  | 
 | func (e *machine) State() shepherd.State { | 
 | 	return shepherd.StateKnownUsed | 
 | } | 
 |  | 
 | // listReservations doesn't lock the mutex and expects the caller to lock. | 
 | func (ep *equinixProvider) listReservations(ctx context.Context) ([]packngo.HardwareReservation, error) { | 
 | 	klog.Infof("Retrieving hardware reservations, this will take a while...") | 
 | 	reservations, err := ep.api.ListReservations(ctx, ep.config.ProjectId) | 
 | 	if err != nil { | 
 | 		return nil, fmt.Errorf("failed to list reservations: %w", err) | 
 | 	} | 
 |  | 
 | 	var available []packngo.HardwareReservation | 
 | 	var inUse, notProvisionable, penalized int | 
 | 	for _, reservation := range reservations { | 
 | 		if reservation.Device != nil { | 
 | 			inUse++ | 
 | 			continue | 
 | 		} | 
 | 		if !reservation.Provisionable { | 
 | 			notProvisionable++ | 
 | 			continue | 
 | 		} | 
 | 		if ep.badReservations.Penalized(reservation.ID) { | 
 | 			penalized++ | 
 | 			continue | 
 | 		} | 
 | 		available = append(available, reservation) | 
 | 	} | 
 | 	klog.Infof("Retrieved hardware reservations: %d (total), %d (available), %d (in use), %d (not provisionable), %d (penalized)", len(reservations), len(available), inUse, notProvisionable, penalized) | 
 |  | 
 | 	return available, nil | 
 | } | 
 |  | 
 | // provision attempts to create a device within Equinix using given Hardware | 
 | // Reservation rsv. The resulting device is registered with BMDB, and tagged as | 
 | // "provided" in the process. | 
 | func (ep *equinixProvider) provision(ctx context.Context, sess *bmdb.Session, rsv packngo.HardwareReservation) (*packngo.Device, error) { | 
 | 	klog.Infof("Creating a new device using reservation ID %s.", rsv.ID) | 
 | 	hostname := ep.config.DevicePrefix + rsv.ID[:18] | 
 | 	kid, err := ep.sshEquinixId(ctx) | 
 | 	if err != nil { | 
 | 		return nil, err | 
 | 	} | 
 | 	req := &packngo.DeviceCreateRequest{ | 
 | 		Hostname:              hostname, | 
 | 		OS:                    ep.config.OS, | 
 | 		Plan:                  rsv.Plan.Slug, | 
 | 		ProjectID:             ep.config.ProjectId, | 
 | 		HardwareReservationID: rsv.ID, | 
 | 		ProjectSSHKeys:        []string{kid}, | 
 | 	} | 
 | 	if ep.config.UseProjectKeys { | 
 | 		klog.Warningf("INSECURE: Machines will be created with ALL PROJECT SSH KEYS!") | 
 | 		req.ProjectSSHKeys = nil | 
 | 	} | 
 |  | 
 | 	nd, err := ep.api.CreateDevice(ctx, req) | 
 | 	if err != nil { | 
 | 		return nil, fmt.Errorf("while creating new device within Equinix: %w", err) | 
 | 	} | 
 | 	klog.Infof("Created a new device within Equinix (RID: %s, PID: %s, HOST: %s)", rsv.ID, nd.ID, hostname) | 
 |  | 
 | 	slices.DeleteFunc(ep.reservationCache, func(v packngo.HardwareReservation) bool { | 
 | 		return rsv.ID == v.ID | 
 | 	}) | 
 |  | 
 | 	err = ep.assimilate(ctx, sess, nd.ID) | 
 | 	if err != nil { | 
 | 		// TODO(serge@monogon.tech) at this point the device at Equinix isn't | 
 | 		// matched by a BMDB record. Schedule device deletion or make sure this | 
 | 		// case is being handled elsewhere. | 
 | 		return nil, err | 
 | 	} | 
 | 	return nd, nil | 
 | } | 
 |  | 
 | // assimilate brings in an already existing machine from Equinix into the BMDB. | 
 | // This is only used in manual testing. | 
 | func (ep *equinixProvider) assimilate(ctx context.Context, sess *bmdb.Session, deviceID string) error { | 
 | 	return sess.Transact(ctx, func(q *model.Queries) error { | 
 | 		// Create a new machine record within BMDB. | 
 | 		m, err := q.NewMachine(ctx) | 
 | 		if err != nil { | 
 | 			return fmt.Errorf("while creating a new machine record in BMDB: %w", err) | 
 | 		} | 
 |  | 
 | 		// Link the new machine with the Equinix device, and tag it "provided". | 
 | 		p := model.MachineAddProvidedParams{ | 
 | 			MachineID:  m.MachineID, | 
 | 			ProviderID: deviceID, | 
 | 			Provider:   model.ProviderEquinix, | 
 | 		} | 
 | 		klog.Infof("Setting \"provided\" tag (ID: %s, PID: %s, Provider: %s).", p.MachineID, p.ProviderID, p.Provider) | 
 | 		if err := q.MachineAddProvided(ctx, p); err != nil { | 
 | 			return fmt.Errorf("while tagging machine active: %w", err) | 
 | 		} | 
 | 		return nil | 
 | 	}) | 
 | } | 
 |  | 
 | // sshEquinixGet looks up the Equinix key matching providerConfig.KeyLabel, | 
 | // returning its packngo.SSHKey instance. | 
 | func (ep *equinixProvider) sshEquinix(ctx context.Context) (*packngo.SSHKey, error) { | 
 | 	ks, err := ep.api.ListSSHKeys(ctx) | 
 | 	if err != nil { | 
 | 		return nil, fmt.Errorf("while listing SSH keys: %w", err) | 
 | 	} | 
 |  | 
 | 	for _, k := range ks { | 
 | 		if k.Label == ep.config.KeyLabel { | 
 | 			return &k, nil | 
 | 		} | 
 | 	} | 
 | 	return nil, NoSuchKey | 
 | } | 
 |  | 
 | // sshEquinixId looks up the Equinix key identified by providerConfig.KeyLabel, | 
 | // returning its Equinix-assigned UUID. | 
 | func (ep *equinixProvider) sshEquinixId(ctx context.Context) (string, error) { | 
 | 	k, err := ep.sshEquinix(ctx) | 
 | 	if err != nil { | 
 | 		return "", err | 
 | 	} | 
 | 	return k.ID, nil | 
 | } | 
 |  | 
 | // sshEquinixUpdate makes sure the existing SSH key registered with Equinix | 
 | // matches the one from sshPub. | 
 | func (ep *equinixProvider) sshEquinixUpdate(ctx context.Context, kid string) error { | 
 | 	pub, err := ep.sshKey.PublicKey() | 
 | 	if err != nil { | 
 | 		return err | 
 | 	} | 
 | 	_, err = ep.api.UpdateSSHKey(ctx, kid, &packngo.SSHKeyUpdateRequest{ | 
 | 		Key: &pub, | 
 | 	}) | 
 | 	if err != nil { | 
 | 		return fmt.Errorf("while updating the SSH key: %w", err) | 
 | 	} | 
 | 	return nil | 
 | } | 
 |  | 
 | // sshEquinixUpload registers a new SSH key from sshPub. | 
 | func (ep *equinixProvider) sshEquinixUpload(ctx context.Context) error { | 
 | 	pub, err := ep.sshKey.PublicKey() | 
 | 	if err != nil { | 
 | 		return fmt.Errorf("while generating public key: %w", err) | 
 | 	} | 
 | 	_, err = ep.api.CreateSSHKey(ctx, &packngo.SSHKeyCreateRequest{ | 
 | 		Label:     ep.config.KeyLabel, | 
 | 		Key:       pub, | 
 | 		ProjectID: ep.config.ProjectId, | 
 | 	}) | 
 | 	if err != nil { | 
 | 		return fmt.Errorf("while creating an SSH key: %w", err) | 
 | 	} | 
 | 	return nil | 
 | } | 
 |  | 
 | // SSHEquinixEnsure initializes the locally managed SSH key (from a persistence | 
 | // path or explicitly set key) and updates or uploads it to Equinix. The key is | 
 | // generated as needed The key is generated as needed | 
 | func (ep *equinixProvider) SSHEquinixEnsure(ctx context.Context) error { | 
 | 	k, err := ep.sshEquinix(ctx) | 
 | 	switch err { | 
 | 	case NoSuchKey: | 
 | 		if err := ep.sshEquinixUpload(ctx); err != nil { | 
 | 			return fmt.Errorf("while uploading key: %w", err) | 
 | 		} | 
 | 		return nil | 
 | 	case nil: | 
 | 		if err := ep.sshEquinixUpdate(ctx, k.ID); err != nil { | 
 | 			return fmt.Errorf("while updating key: %w", err) | 
 | 		} | 
 | 		return nil | 
 | 	default: | 
 | 		return err | 
 | 	} | 
 | } | 
 |  | 
 | // managedDevices provides a map of device provider IDs to matching | 
 | // packngo.Device instances. It calls Equinix API's ListDevices. The returned | 
 | // devices are filtered according to DevicePrefix provided through Opts. The | 
 | // returned error value, if not nil, will originate in wrapngo. | 
 | func (ep *equinixProvider) managedDevices(ctx context.Context) (map[string]packngo.Device, error) { | 
 | 	ds, err := ep.api.ListDevices(ctx, ep.config.ProjectId) | 
 | 	if err != nil { | 
 | 		return nil, err | 
 | 	} | 
 | 	dm := map[string]packngo.Device{} | 
 | 	for _, d := range ds { | 
 | 		if strings.HasPrefix(d.Hostname, ep.config.DevicePrefix) { | 
 | 			dm[d.ID] = d | 
 | 		} | 
 | 	} | 
 | 	return dm, nil | 
 | } |