diff --git a/equinix/config.go b/equinix/config.go index 56e55c55f..226237739 100644 --- a/equinix/config.go +++ b/equinix/config.go @@ -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" @@ -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 @@ -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 } @@ -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 @@ -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 @@ -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) diff --git a/equinix/data_source_metal_device.go b/equinix/data_source_metal_device.go index d0a9bc84f..1537f2e34 100644 --- a/equinix/data_source_metal_device.go +++ b/equinix/data_source_metal_device.go @@ -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 { @@ -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") @@ -211,7 +217,8 @@ 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") @@ -219,9 +226,7 @@ func dataSourceMetalDeviceRead(d *schema.ResourceData, meta interface{}) error { 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 } @@ -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 { @@ -267,27 +274,30 @@ 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) @@ -295,17 +305,17 @@ func dataSourceMetalDeviceRead(d *schema.ResourceData, meta interface{}) error { 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) } } diff --git a/equinix/data_source_metal_device_bgp_neighbors.go b/equinix/data_source_metal_device_bgp_neighbors.go index 3d3b7f1ec..c8d0fa0fd 100644 --- a/equinix/data_source_metal_device_bgp_neighbors.go +++ b/equinix/data_source_metal_device_bgp_neighbors.go @@ -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 { @@ -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 } @@ -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{}{ @@ -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, diff --git a/equinix/data_source_metal_device_bgp_neighbors_acc_test.go b/equinix/data_source_metal_device_bgp_neighbors_acc_test.go new file mode 100644 index 000000000..5714ffee4 --- /dev/null +++ b/equinix/data_source_metal_device_bgp_neighbors_acc_test.go @@ -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)) +} diff --git a/equinix/helpers_device.go b/equinix/helpers_device.go index 427464c2f..3097dfcf7 100644 --- a/equinix/helpers_device.go +++ b/equinix/helpers_device.go @@ -1,14 +1,15 @@ package equinix import ( + "encoding/json" "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" "github.com/packethost/packngo" @@ -94,6 +95,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: @@ -121,6 +164,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"}}) diff --git a/go.mod b/go.mod index 26b2967d5..f1fcefdef 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.20 require ( github.com/antihax/optional v1.0.0 github.com/equinix-labs/fabric-go v0.4.0 + github.com/equinix-labs/metal-go v0.8.0 github.com/equinix/ecx-go/v2 v2.3.0 github.com/equinix/ne-go v1.10.0 github.com/equinix/oauth2-go v1.0.0 diff --git a/go.sum b/go.sum index 322625739..f9e154297 100644 --- a/go.sum +++ b/go.sum @@ -260,6 +260,8 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go. github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/equinix-labs/fabric-go v0.4.0 h1:YM6jkdPlYJrgUEfCqt1WpXe2gACM5EavRL26ocIMM9c= github.com/equinix-labs/fabric-go v0.4.0/go.mod h1:/0uePNYbhu/1qWrxhD011AjU6yjf7r0sZgTCn8TyitI= +github.com/equinix-labs/metal-go v0.8.0 h1:s5mDkQREvsor4KGe9+6Wzu/Rx0N2KZWhWVRfI+R2S74= +github.com/equinix-labs/metal-go v0.8.0/go.mod h1:SmxCklxW+KjmBLVMdEXgtFO5gD5/b4N0VxcNgUYbOH4= github.com/equinix/ecx-go/v2 v2.3.0 h1:SOABrI2TP073Mx3gVoWa4qGlot1Z2hECAOY8W4nYDPU= github.com/equinix/ecx-go/v2 v2.3.0/go.mod h1:FvCdZ3jXU8Z4CPKig2DT+4J2HdwgRK17pIcznM7RXyk= github.com/equinix/ne-go v1.10.0 h1:hy1umXQFPi1b3z/maZ8kqLRFlD1PXD4qp0cV8rnyL8k=