Skip to content

Commit

Permalink
Use metal-go for the device and BGP neighbors data sources
Browse files Browse the repository at this point in the history
This updates the device data source and the device BGP neighbors
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.

There were no tests for the BGP neighbors data source, so those are
added here as well.
  • Loading branch information
ctreatma committed Jun 27, 2023
1 parent 6dbd3d3 commit c521c51
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 47 deletions.
53 changes: 45 additions & 8 deletions equinix/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"time"

v4 "github.com/equinix-labs/fabric-go/fabric/v4"
metalv1 "github.com/equinix-labs/metal-go/metal/v1"
"github.com/equinix/ecx-go/v2"
"github.com/equinix/ne-go"
"github.com/equinix/oauth2-go"
Expand Down Expand Up @@ -80,13 +81,15 @@ type Config struct {
PageSize int
Token string

ecx ecx.Client
ne ne.Client
metal *packngo.Client
ecx ecx.Client
ne ne.Client
metal *packngo.Client
metalgo *metalv1.APIClient

ecxUserAgent string
neUserAgent string
metalUserAgent string
ecxUserAgent string
neUserAgent string
metalUserAgent string
metalGoUserAgent string

terraformVersion string
fabricClient *v4.APIClient
Expand Down Expand Up @@ -158,6 +161,7 @@ func (c *Config) Load(ctx context.Context) error {
c.ecx = ecxClient
c.ne = neClient
c.metal = c.NewMetalClient()
c.metalgo = c.NewMetalGoClient()
c.fabricClient = c.NewFabricClient()
return nil
}
Expand All @@ -183,11 +187,11 @@ func (c *Config) NewFabricClient() *v4.APIClient {
return client
}

// NewMetalClient returns a new client for accessing Equinix Metal's API.
// NewMetalClient returns a new packngo client for accessing Equinix Metal's API.
func (c *Config) NewMetalClient() *packngo.Client {
transport := http.DefaultTransport
// transport = &DumpTransport{http.DefaultTransport} // Debug only
transport = logging.NewTransport("Equinix Metal", transport)
transport = logging.NewTransport("Equinix Metal (packngo)", transport)
retryClient := retryablehttp.NewClient()
retryClient.HTTPClient.Transport = transport
retryClient.RetryMax = c.MaxRetries
Expand All @@ -203,6 +207,35 @@ func (c *Config) NewMetalClient() *packngo.Client {
return client
}

// NewMetalGoClient returns a new metal-go client for accessing Equinix Metal's API.
func (c *Config) NewMetalGoClient() *metalv1.APIClient {
transport := http.DefaultTransport
transport = logging.NewSubsystemLoggingHTTPTransport("Equinix Metal (metal-go)", transport)
retryClient := retryablehttp.NewClient()
retryClient.HTTPClient.Transport = transport
retryClient.RetryMax = c.MaxRetries
retryClient.RetryWaitMin = time.Second
retryClient.RetryWaitMax = c.MaxRetryWait
retryClient.CheckRetry = MetalRetryPolicy
standardClient := retryClient.StandardClient()

baseURL, _ := url.Parse(c.BaseURL)
baseURL.Path = path.Join(baseURL.Path, metalBasePath) + "/"

configuration := metalv1.NewConfiguration()
configuration.Servers = metalv1.ServerConfigurations{
metalv1.ServerConfiguration{
URL: baseURL.String(),
},
}
configuration.HTTPClient = standardClient
configuration.AddDefaultHeader("X-Auth-Token", os.Getenv("METAL_AUTH_TOKEN"))
configuration.UserAgent = c.fullUserAgent(configuration.UserAgent)
client := metalv1.NewAPIClient(configuration)
c.metalGoUserAgent = client.GetConfig().UserAgent
return client
}

func (c *Config) requestTimeout() time.Duration {
if c.RequestTimeout == 0 {
return 5 * time.Second
Expand Down Expand Up @@ -271,6 +304,10 @@ func (c *Config) addModuleToMetalUserAgent(d *schema.ResourceData) {
c.metal.UserAgent = generateModuleUserAgentString(d, c.metalUserAgent)
}

func (c *Config) addModuleToMetalGoUserAgent(d *schema.ResourceData) {
c.metalgo.GetConfig().UserAgent = generateModuleUserAgentString(d, c.metalUserAgent)
}

func generateModuleUserAgentString(d *schema.ResourceData, baseUserAgent string) string {
var m providerMeta
err := d.GetProviderMeta(&m)
Expand Down
72 changes: 41 additions & 31 deletions equinix/data_source_metal_device.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
package equinix

import (
"context"
"encoding/json"
"fmt"
"path"
"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 @@ -197,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(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 @@ -211,17 +217,16 @@ func dataSourceMetalDeviceRead(d *schema.ResourceData, meta interface{}) error {
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(context.Background(), projectId).Hostname(hostname).Include(deviceCommonIncludes).Execute()
if err != nil {
return err
}
Expand All @@ -233,26 +238,28 @@ func dataSourceMetalDeviceRead(d *schema.ResourceData, meta interface{}) error {
} else {
deviceId := deviceIdRaw.(string)
var err error
device, _, err = client.Devices.Get(deviceId, deviceReadOptions)
device, _, err = client.DevicesApi.FindDeviceById(context.Background(), 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 @@ -267,45 +274,48 @@ func dataSourceMetalDeviceRead(d *schema.ResourceData, meta interface{}) error {
}

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
14 changes: 7 additions & 7 deletions equinix/data_source_metal_device_bgp_neighbors.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package equinix

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

func bgpNeighborSchema() *schema.Resource {
Expand Down Expand Up @@ -103,10 +103,10 @@ func dataSourceMetalDeviceBGPNeighbors() *schema.Resource {
}

func dataSourceMetalDeviceBGPNeighborsRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Config).metal
client := meta.(*Config).metalgo
deviceID := d.Get("device_id").(string)

bgpNeighborsRaw, _, err := client.Devices.ListBGPNeighbors(deviceID, nil)
bgpNeighborsRaw, _, err := client.DevicesApi.GetBgpNeighborData(nil, deviceID).Execute()
if err != nil {
return err
}
Expand All @@ -116,7 +116,7 @@ func dataSourceMetalDeviceBGPNeighborsRead(d *schema.ResourceData, meta interfac
return nil
}

func getRoutesSlice(routes []packngo.BGPRoute) []map[string]interface{} {
func getRoutesSlice(routes []metalv1.BgpRoute) []map[string]interface{} {
ret := []map[string]interface{}{}
for _, r := range routes {
ret = append(ret, map[string]interface{}{
Expand All @@ -126,13 +126,13 @@ func getRoutesSlice(routes []packngo.BGPRoute) []map[string]interface{} {
return ret
}

func getBgpNeighbors(ns []packngo.BGPNeighbor) []map[string]interface{} {
func getBgpNeighbors(ns *metalv1.BgpSessionNeighbors) []map[string]interface{} {
ret := make([]map[string]interface{}, 0, 1)
for _, n := range ns {
for _, n := range ns.BgpNeighbors {
neighbor := map[string]interface{}{
"address_family": n.AddressFamily,
"customer_as": n.CustomerAs,
"customer_ip": n.CustomerIP,
"customer_ip": n.CustomerIp,
"md5_enabled": n.Md5Enabled,
"md5_password": n.Md5Password,
"multihop": n.Multihop,
Expand Down
42 changes: 42 additions & 0 deletions equinix/data_source_metal_device_bgp_neighbors_acc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package equinix

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestAccDataSourceMetalDeviceBgpNeighbors(t *testing.T) {
projectName := fmt.Sprintf("ds-device-%s", acctest.RandString(10))

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ExternalProviders: testExternalProviders,
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccDataSourceMetalDeviceBgpNeighborsConfig(projectName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(
"data.equinix_metal_device_bgp_neighbors.test", "bgp_neighbors.#"),
),
},
},
})
}

func testAccDataSourceMetalDeviceBgpNeighborsConfig(projectName string) string {
return fmt.Sprintf(`
%s
data "equinix_metal_device_bgp_neighbors" "test" {
device_id = equinix_metal_device.test.id
}
output "bgp_neighbors_listing" {
value = data.equinix_metal_device_bgp_neighbors.test.bgp_neighbors
}
`, testDataSourceMetalDeviceConfig_basic(projectName))
}
Loading

0 comments on commit c521c51

Please sign in to comment.