Skip to content

Commit

Permalink
Use metal-go for the device data source
Browse files Browse the repository at this point in the history
This updates the device data source to use `metal-go` instead of
`packngo`.  As part of this migration, the device data source
gains the new `sos_hostname` attribute which enables customers
to reduce their reliance on the deprecated `facility` attribute,
which was previously necessary in order to compute the SOS
hostname for a device.
  • Loading branch information
ctreatma committed Sep 7, 2023
1 parent 5cf6d50 commit 120ef2e
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 31 deletions.
71 changes: 40 additions & 31 deletions equinix/data_source_metal_device.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
"sort"
"strings"

metalv1 "github.com/equinix-labs/metal-go/metal/v1"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure"
"github.com/packethost/packngo"
)

func dataSourceMetalDevice() *schema.Resource {
Expand Down Expand Up @@ -198,12 +198,17 @@ func dataSourceMetalDevice() *schema.Resource {
},
},
},
"sos_hostname": {
Type: schema.TypeString,
Description: "The hostname to use for [Serial over SSH](https://deploy.equinix.com/developers/docs/metal/resilience-recovery/serial-over-ssh/) access to the device",
Computed: true,
},
},
}
}

func dataSourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) error {
client := meta.(*Config).metal
client := meta.(*Config).metalgo

hostnameRaw, hostnameOK := d.GetOk("hostname")
projectIdRaw, projectIdOK := d.GetOk("project_id")
Expand All @@ -212,17 +217,16 @@ func dataSourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta
if !deviceIdOK && !hostnameOK {
return fmt.Errorf("You must supply device_id or hostname")
}
var device *packngo.Device
var device *metalv1.Device

if hostnameOK {
if !projectIdOK {
return fmt.Errorf("If you lookup via hostname, you must supply project_id")
}
hostname := hostnameRaw.(string)
projectId := projectIdRaw.(string)

ds, _, err := client.Devices.List(
projectId,
&packngo.ListOptions{Search: hostname, Includes: deviceCommonIncludes})
ds, _, err := client.DevicesApi.FindProjectDevices(ctx, projectId).Hostname(hostname).Include(deviceCommonIncludes).Execute()
if err != nil {
return err
}
Expand All @@ -234,26 +238,28 @@ func dataSourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta
} else {
deviceId := deviceIdRaw.(string)
var err error
device, _, err = client.Devices.Get(deviceId, deviceReadOptions)
device, _, err = client.DevicesApi.FindDeviceById(ctx, deviceId).Include(deviceCommonIncludes).Execute()
if err != nil {
return err
}
}

d.Set("hostname", device.Hostname)
d.Set("project_id", device.Project.ID)
d.Set("device_id", device.ID)
d.Set("hostname", device.GetHostname())
d.Set("project_id", device.Project.GetId())
d.Set("device_id", device.GetId())
d.Set("plan", device.Plan.Slug)
d.Set("facility", device.Facility.Code)
if device.Metro != nil {
d.Set("metro", strings.ToLower(device.Metro.Code))
d.Set("metro", strings.ToLower(device.Metro.GetCode()))
}
d.Set("operating_system", device.OS.Slug)
d.Set("state", device.State)
d.Set("billing_cycle", device.BillingCycle)
d.Set("ipxe_script_url", device.IPXEScriptURL)
d.Set("always_pxe", device.AlwaysPXE)
d.Set("root_password", device.RootPassword)
d.Set("operating_system", device.OperatingSystem.GetSlug())
d.Set("state", device.GetState())
d.Set("billing_cycle", device.GetBillingCycle())
d.Set("ipxe_script_url", device.GetIpxeScriptUrl())
d.Set("always_pxe", device.GetAlwaysPxe())
d.Set("root_password", device.GetRootPassword())
d.Set("sos_hostname", device.GetSos())

if device.Storage != nil {
rawStorageBytes, err := json.Marshal(device.Storage)
if err != nil {
Expand All @@ -268,45 +274,48 @@ func dataSourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta
}

if device.HardwareReservation != nil {
d.Set("hardware_reservation_id", device.HardwareReservation.ID)
d.Set("hardware_reservation_id", device.HardwareReservation.GetId())
}
networkType, err := getMetalGoNetworkType(device)
if err != nil {
return fmt.Errorf("[ERR] Error computing network type for device (%s): %s", d.Id(), err)
}
networkType := device.GetNetworkType()

d.Set("network_type", networkType)

d.Set("tags", device.Tags)

keyIDs := []string{}
for _, k := range device.SSHKeys {
keyIDs = append(keyIDs, path.Base(k.URL))
for _, k := range device.SshKeys {
keyIDs = append(keyIDs, path.Base(k.Href))
}
d.Set("ssh_key_ids", keyIDs)
networkInfo := getNetworkInfo(device.Network)
networkInfo := getMetalGoNetworkInfo(device.IpAddresses)

sort.SliceStable(networkInfo.Networks, func(i, j int) bool {
famI := networkInfo.Networks[i]["family"].(int)
famJ := networkInfo.Networks[j]["family"].(int)
famI := networkInfo.Networks[i]["family"].(int32)
famJ := networkInfo.Networks[j]["family"].(int32)
pubI := networkInfo.Networks[i]["public"].(bool)
pubJ := networkInfo.Networks[j]["public"].(bool)
return getNetworkRank(famI, pubI) < getNetworkRank(famJ, pubJ)
return getNetworkRank(int(famI), pubI) < getNetworkRank(int(famJ), pubJ)
})

d.Set("network", networkInfo.Networks)
d.Set("access_public_ipv4", networkInfo.PublicIPv4)
d.Set("access_private_ipv4", networkInfo.PrivateIPv4)
d.Set("access_public_ipv6", networkInfo.PublicIPv6)

ports := getPorts(device.NetworkPorts)
ports := getMetalGoPorts(device.NetworkPorts)
d.Set("ports", ports)

d.SetId(device.ID)
d.SetId(device.GetId())
return nil
}

func findDeviceByHostname(devices []packngo.Device, hostname string) (*packngo.Device, error) {
results := make([]packngo.Device, 0)
for _, d := range devices {
if d.Hostname == hostname {
func findDeviceByHostname(devices *metalv1.DeviceList, hostname string) (*metalv1.Device, error) {
results := make([]metalv1.Device, 0)
for _, d := range devices.GetDevices() {
if *d.Hostname == hostname {
results = append(results, d)
}
}
Expand Down
59 changes: 59 additions & 0 deletions equinix/helpers_device.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package equinix

import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"strings"
"sync"
"time"

metalv1 "github.com/equinix-labs/metal-go/metal/v1"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
Expand Down Expand Up @@ -95,6 +97,48 @@ func getNetworkInfo(ips []*packngo.IPAddressAssignment) NetworkInfo {
return ni
}

func getMetalGoNetworkType(device *metalv1.Device) (*string, error) {

pgDevice := packngo.Device{}
res, err := device.MarshalJSON()
if err != nil {
json.Unmarshal(res, pgDevice)
networkType := pgDevice.GetNetworkType()
return &networkType, nil
}
return nil, err
}

func getMetalGoNetworkInfo(ips []metalv1.IPAssignment) NetworkInfo {
ni := NetworkInfo{Networks: make([]map[string]interface{}, 0, 1)}
for _, ip := range ips {
network := map[string]interface{}{
"address": *ip.Address,
"gateway": *ip.Gateway,
"family": *ip.AddressFamily,
"cidr": *ip.Cidr,
"public": *ip.Public,
}
ni.Networks = append(ni.Networks, network)

// Initial device IPs are fixed and marked as "Management"
if !ip.HasManagement() || *ip.Management {
if *ip.AddressFamily == int32(4) {
if !ip.HasPublic() || *ip.Public {
ni.Host = *ip.Address
ni.IPv4SubnetSize = int(*ip.Cidr)
ni.PublicIPv4 = *ip.Address
} else {
ni.PrivateIPv4 = *ip.Address
}
} else {
ni.PublicIPv6 = *ip.Address
}
}
}
return ni
}

func getNetworkRank(family int, public bool) int {
switch {
case family == 4 && public:
Expand Down Expand Up @@ -122,6 +166,21 @@ func getPorts(ps []packngo.Port) []map[string]interface{} {
return ret
}

func getMetalGoPorts(ps []metalv1.Port) []map[string]interface{} {
ret := make([]map[string]interface{}, 0, 1)
for _, p := range ps {
port := map[string]interface{}{
"name": p.GetName(),
"id": p.GetId(),
"type": p.GetType(),
"mac": p.Data.GetMac(),
"bonded": p.Data.GetBonded(),
}
ret = append(ret, port)
}
return ret
}

func hwReservationStateRefreshFunc(client *packngo.Client, reservationId, instanceId string) retry.StateRefreshFunc {
return func() (interface{}, string, error) {
r, _, err := client.HardwareReservations.Get(reservationId, &packngo.GetOptions{Includes: []string{"device"}})
Expand Down

0 comments on commit 120ef2e

Please sign in to comment.