diff --git a/docs/data-sources/equinix_metal_device.md b/docs/data-sources/equinix_metal_device.md index 0b1150988..1acc0c656 100644 --- a/docs/data-sources/equinix_metal_device.md +++ b/docs/data-sources/equinix_metal_device.md @@ -70,6 +70,7 @@ In addition to all arguments above, the following attributes are exported: * `ports` - List of ports assigned to the device. See [Ports Attribute](#ports-attribute) below for more details. * `root_password` - Root password to the server (if still available). +* `sos_hostname` - The hostname to use for [Serial over SSH](https://deploy.equinix.com/developers/docs/metal/resilience-recovery/serial-over-ssh/) access to the device * `ssh_key_ids` - List of IDs of SSH keys deployed in the device, can be both user or project SSH keys. * `state` - The state of the device. * `tags` - Tags attached to the device. diff --git a/docs/resources/equinix_metal_device.md b/docs/resources/equinix_metal_device.md index d540aa5f5..f03faf68b 100644 --- a/docs/resources/equinix_metal_device.md +++ b/docs/resources/equinix_metal_device.md @@ -285,6 +285,7 @@ See [network_types guide](../guides/network_types.md) for more info. more details. * `project_id` - The ID of the project the device belongs to. * `root_password` - Root password to the server (disabled after 24 hours). +* `sos_hostname` - The hostname to use for [Serial over SSH](https://deploy.equinix.com/developers/docs/metal/resilience-recovery/serial-over-ssh/) access to the device * `ssh_key_ids` - List of IDs of SSH keys deployed in the device, can be both user and project SSH keys. * `state` - The status of the device. * `tags` - Tags attached to the device. diff --git a/equinix/data_source_metal_device.go b/equinix/data_source_metal_device.go index f656068a1..22b619d46 100644 --- a/equinix/data_source_metal_device.go +++ b/equinix/data_source_metal_device.go @@ -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 { @@ -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") @@ -212,7 +217,8 @@ 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") @@ -220,9 +226,7 @@ func dataSourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta 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 } @@ -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 { @@ -268,27 +274,30 @@ 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 := getNetworkType(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 := getNetworkInfo(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) @@ -299,14 +308,14 @@ func dataSourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta ports := getPorts(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/errors.go b/equinix/errors.go index 61c03c5a4..c3c546762 100644 --- a/equinix/errors.go +++ b/equinix/errors.go @@ -22,25 +22,35 @@ func friendlyError(err error) error { if 0 == len(errors) { errors = Errors{e.SingleError} } - er := &ErrorResponse{ - StatusCode: resp.StatusCode, - Errors: errors, - } - respHead := resp.Header - - // this checks if the error comes from API (and not from cache/LB) - if len(errors) > 0 { - ct := respHead.Get("Content-Type") - xrid := respHead.Get("X-Request-Id") - if strings.Contains(ct, "application/json") && len(xrid) > 0 { - er.IsAPIError = true - } - } - return er + + return convertToFriendlyError(errors, resp) } return err } +func friendlyErrorForMetalGo(err error, resp *http.Response) error { + errors := Errors([]string{err.Error()}) + return convertToFriendlyError(errors, resp) +} + +func convertToFriendlyError(errors Errors, resp *http.Response) error { + er := &ErrorResponse{ + StatusCode: resp.StatusCode, + Errors: errors, + } + respHead := resp.Header + + // this checks if the error comes from API (and not from cache/LB) + if len(errors) > 0 { + ct := respHead.Get("Content-Type") + xrid := respHead.Get("X-Request-Id") + if strings.Contains(ct, "application/json") && len(xrid) > 0 { + er.IsAPIError = true + } + } + return er +} + func isForbidden(err error) bool { r, ok := err.(*packngo.ErrorResponse) if ok && r.Response != nil { diff --git a/equinix/helpers_device.go b/equinix/helpers_device.go index d7a1903f5..fcf26c1f8 100644 --- a/equinix/helpers_device.go +++ b/equinix/helpers_device.go @@ -2,6 +2,7 @@ package equinix import ( "context" + "encoding/json" "errors" "fmt" "log" @@ -68,64 +69,46 @@ type NetworkInfo struct { PrivateIPv4 string } -func getNetworkInfoMetalGo(ips []metalv1.IPAssignment) NetworkInfo { +func getNetworkInfo(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, + "address": ip.GetAddress(), + "gateway": ip.GetGateway(), + "family": ip.GetAddressFamily(), + "cidr": ip.GetCidr(), + "public": ip.GetPublic(), } ni.Networks = append(ni.Networks, network) // Initial device IPs are fixed and marked as "Management" - if *ip.Management { - if *ip.AddressFamily == 4 { - if *ip.Public { - ni.Host = *ip.Address - ni.IPv4SubnetSize = int(*ip.Cidr) - ni.PublicIPv4 = *ip.Address + if ip.GetManagement() { + if ip.GetAddressFamily() == 4 { + if ip.GetPublic() { + ni.Host = ip.GetAddress() + ni.IPv4SubnetSize = int(ip.GetCidr()) + ni.PublicIPv4 = ip.GetAddress() } else { - ni.PrivateIPv4 = *ip.Address + ni.PrivateIPv4 = ip.GetAddress() } } else { - ni.PublicIPv6 = *ip.Address + ni.PublicIPv6 = ip.GetAddress() } } } return ni } -func getNetworkInfo(ips []*packngo.IPAddressAssignment) 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.Management { - if ip.AddressFamily == 4 { - if ip.Public { - ni.Host = ip.Address - ni.IPv4SubnetSize = ip.CIDR - ni.PublicIPv4 = ip.Address - } else { - ni.PrivateIPv4 = ip.Address - } - } else { - ni.PublicIPv6 = ip.Address - } +func getNetworkType(device *metalv1.Device) (*string, error) { + pgDevice := packngo.Device{} + res, err := device.MarshalJSON() + if err == nil { + if err = json.Unmarshal(res, &pgDevice); err == nil { + networkType := pgDevice.GetNetworkType() + return &networkType, nil } } - return ni + return nil, err } func getNetworkRank(family int, public bool) int { @@ -140,30 +123,15 @@ func getNetworkRank(family int, public bool) int { return 3 } -func getPortsMetalGo(ps []metalv1.Port) []map[string]interface{} { - ret := make([]map[string]interface{}, 0, 1) - for _, p := range ps { - port := map[string]interface{}{ - "name": p.Name, - "id": p.Id, - "type": p.Type, - "mac": p.Data.Mac, - "bonded": p.Data.Bonded, - } - ret = append(ret, port) - } - return ret -} - -func getPorts(ps []packngo.Port) []map[string]interface{} { +func getPorts(ps []metalv1.Port) []map[string]interface{} { ret := make([]map[string]interface{}, 0, 1) for _, p := range ps { port := map[string]interface{}{ - "name": p.Name, - "id": p.ID, - "type": p.Type, - "mac": p.Data.MAC, - "bonded": p.Data.Bonded, + "name": p.GetName(), + "id": p.GetId(), + "type": p.GetType(), + "mac": p.Data.GetMac(), + "bonded": p.Data.GetBonded(), } ret = append(ret, port) } @@ -271,19 +239,19 @@ func ipAddressSchema() *schema.Resource { } func getDeviceMap(device metalv1.Device) map[string]interface{} { - networkInfo := getNetworkInfoMetalGo(device.IpAddresses) + networkInfo := getNetworkInfo(device.IpAddresses) sort.SliceStable(networkInfo.Networks, func(i, j int) bool { - famI := int(*networkInfo.Networks[i]["family"].(*int32)) - famJ := int(*networkInfo.Networks[j]["family"].(*int32)) - pubI := *networkInfo.Networks[i]["public"].(*bool) - pubJ := *networkInfo.Networks[j]["public"].(*bool) + famI := int(networkInfo.Networks[i]["family"].(int32)) + famJ := int(networkInfo.Networks[j]["family"].(int32)) + pubI := networkInfo.Networks[i]["public"].(bool) + pubJ := networkInfo.Networks[j]["public"].(bool) return getNetworkRank(famI, pubI) < getNetworkRank(famJ, pubJ) }) keyIDs := []string{} for _, k := range device.SshKeys { keyIDs = append(keyIDs, path.Base(k.GetHref())) } - ports := getPortsMetalGo(device.NetworkPorts) + ports := getPorts(device.NetworkPorts) return map[string]interface{}{ "hostname": device.GetHostname(), @@ -306,5 +274,6 @@ func getDeviceMap(device metalv1.Device) map[string]interface{} { "network": networkInfo.Networks, "ssh_key_ids": keyIDs, "ports": ports, + "sos_hostname": device.GetSos(), } } diff --git a/equinix/resource_metal_device.go b/equinix/resource_metal_device.go index 1596ff188..756dadc26 100644 --- a/equinix/resource_metal_device.go +++ b/equinix/resource_metal_device.go @@ -27,7 +27,6 @@ var ( var ( deviceCommonIncludes = []string{"project", "metro", "facility", "hardware_reservation"} - deviceReadOptions = &packngo.GetOptions{Includes: deviceCommonIncludes} ) func resourceMetalDevice() *schema.Resource { @@ -410,6 +409,11 @@ func resourceMetalDevice() *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, + }, }, CustomizeDiff: customdiff.Sequence( customdiff.ForceNewIf("custom_data", reinstallDisabledAndNoChangesAllowed("custom_data")), @@ -606,12 +610,12 @@ func resourceMetalDeviceCreate(ctx context.Context, d *schema.ResourceData, meta } func resourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) error { - meta.(*Config).addModuleToMetalUserAgent(d) - client := meta.(*Config).metal + meta.(*Config).addModuleToMetalGoUserAgent(d) + client := meta.(*Config).metalgo - device, _, err := client.Devices.Get(d.Id(), deviceReadOptions) + device, resp, err := client.DevicesApi.FindDeviceById(context.Background(), d.Id()).Include(deviceCommonIncludes).Execute() if err != nil { - err = friendlyError(err) + err = friendlyErrorForMetalGo(err, resp) // If the device somehow already destroyed, mark as successfully gone. // Checking d.IsNewResource prevents the creation of a resource from failing @@ -625,23 +629,24 @@ func resourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta i return err } - d.Set("hostname", device.Hostname) - d.Set("plan", device.Plan.Slug) - d.Set("deployed_facility", device.Facility.Code) - d.Set("facilities", []string{device.Facility.Code}) + d.Set("hostname", device.GetHostname()) + d.Set("plan", device.Plan.GetSlug()) + d.Set("deployed_facility", device.Facility.GetCode()) + d.Set("facilities", []string{device.Facility.GetCode()}) if device.Metro != nil { - d.Set("metro", device.Metro.Code) - } - d.Set("operating_system", device.OS.Slug) - d.Set("state", device.State) - d.Set("billing_cycle", device.BillingCycle) - d.Set("locked", device.Locked) - d.Set("created", device.Created) - d.Set("updated", device.Updated) - d.Set("ipxe_script_url", device.IPXEScriptURL) - d.Set("always_pxe", device.AlwaysPXE) - d.Set("root_password", device.RootPassword) - d.Set("project_id", device.Project.ID) + d.Set("metro", device.Metro.GetCode()) + } + d.Set("operating_system", device.OperatingSystem.GetSlug()) + d.Set("state", device.GetState()) + d.Set("billing_cycle", device.GetBillingCycle()) + d.Set("locked", device.GetLocked()) + d.Set("created", device.GetCreatedAt().Format(time.RFC3339)) + d.Set("updated", device.GetUpdatedAt().Format(time.RFC3339)) + d.Set("ipxe_script_url", device.GetIpxeScriptUrl()) + d.Set("always_pxe", device.GetAlwaysPxe()) + d.Set("root_password", device.GetRootPassword()) + d.Set("project_id", device.Project.GetId()) + d.Set("sos_hostname", device.GetSos()) if device.Storage != nil { rawStorageBytes, err := json.Marshal(device.Storage) if err != nil { @@ -655,10 +660,13 @@ func resourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta i d.Set("storage", storageString) } if device.HardwareReservation != nil { - d.Set("deployed_hardware_reservation_id", device.HardwareReservation.ID) + d.Set("deployed_hardware_reservation_id", device.HardwareReservation.GetId()) } - networkType := device.GetNetworkType() + networkType, err := getNetworkType(device) + if err != nil { + return fmt.Errorf("[ERR] Error computing network type for device (%s): %s", d.Id(), err) + } d.Set("network_type", networkType) wfrd := "wait_for_reservation_deprovision" @@ -676,18 +684,18 @@ func resourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta i 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 := getNetworkInfo(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)