diff --git a/docs/data-sources/equinix_metal_device.md b/docs/data-sources/equinix_metal_device.md index c216fc257..0b1150988 100644 --- a/docs/data-sources/equinix_metal_device.md +++ b/docs/data-sources/equinix_metal_device.md @@ -4,7 +4,9 @@ subcategory: "Metal" # equinix_metal_device (Data Source) -Provides an Equinix Metal device datasource. +The datasource can be used to fetch a single device. + +If you need to fetch a list of devices which meet filter criteria, you can use the [equinix_metal_devices](equinix_metal_devices.md) datasource. ~> **Note:** All arguments including the `root_password` and `user_data` will be stored in the raw state as plain-text. diff --git a/docs/data-sources/equinix_metal_devices.md b/docs/data-sources/equinix_metal_devices.md new file mode 100644 index 000000000..b6570c394 --- /dev/null +++ b/docs/data-sources/equinix_metal_devices.md @@ -0,0 +1,69 @@ +--- +subcategory: "Metal" +--- + +# equinix_metal_devices + +The datasource can be used to find a list of devices which meet filter criteria. + +If you need to fetch a single device by ID or by project ID and hostname, use the [equinix_metal_device](equinix_metal_device.md) datasource. + +## Example Usage + +```hcl +# Following example will select c3.small.x86 devices which are deplyed in metro 'da' (Dallas) +# OR 'sv' (Sillicon Valley). +data "equinix_metal_devices" "example" { + project_id = local.project_id + filter { + attribute = "plan" + values = ["c3.small.x86"] + } + filter { + attribute = "metro" + values = ["da", "sv"] + } +} + +output "devices" { + organization_id = local.org_id + value = data.equinix_metal_devices.example.devices +} +``` + +```hcl +# Following example takes advantage of the `search` field in the API request, and will select devices with +# string "database" in one of the searched attributes. See `search` in argument reference. +data "equinix_metal_devices" "example" { + search = "database" +} + +output "devices" { + value = data.equinix_metal_devices.example.devices +} +``` + +## search vs filter + +The difference between `search` and `filter` is that `search` is an API parameter, interpreted by the Equinix Metal service. The "filter" arguments will reduce the API list (or search) results by applying client-side filtering, within this provider. + +## Argument Reference + +The following arguments are supported: + +* `project_id` - (Optional) ID of project containing the devices. Exactly one of `project_id` and `organization_id` must be set. +* `organization_id` - (Optional) ID of organization containing the devices. +* `search` - (Optional) - Search string to filter devices by hostname, description, short_id, reservation short_id, tags, plan name, plan slug, facility code, facility name, operating system name, operating system slug, IP addresses. +* `filter` - (Optional) One or more attribute/values pairs to filter. List of atributes to filter can be found in the [attribute reference](equinix_metal_device.md#attributes-reference) of the `equinix_metal_device` datasource. + - `attribute` - (Required) The attribute used to filter. Filter attributes are case-sensitive + - `values` - (Required) The filter values. Filter values are case-sensitive. If you specify multiple values for a filter, the values are joined with an OR by default, and the request returns all results that match any of the specified values + - `match_by` - (Optional) The type of comparison to apply. One of: `in` , `re`, `substring`, `less_than`, `less_than_or_equal`, `greater_than`, `greater_than_or_equal`. Default is `in`. + - `all` - (Optional) If is set to true, the values are joined with an AND, and the requests returns only the results that match all specified values. Default is `false`. + +All fields in the `devices` block defined below can be used as attribute for both `sort` and `filter` blocks. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `devices` - list of resources with attributes like in the [equninix_metal_device datasources](equinix_metal_device.md). diff --git a/equinix/data_source_metal_devices.go b/equinix/data_source_metal_devices.go new file mode 100644 index 000000000..3904af6ec --- /dev/null +++ b/equinix/data_source_metal_devices.go @@ -0,0 +1,97 @@ +package equinix + +import ( + "context" + "fmt" + + metalv1 "github.com/equinix-labs/metal-go/metal/v1" + "github.com/equinix/terraform-provider-equinix/equinix/internal/datalist" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceMetalDevices() *schema.Resource { + dsmd := dataSourceMetalDevice() + sch := dsmd.Schema + for _, v := range sch { + if v.Optional { + v.Optional = false + } + if v.ConflictsWith != nil { + v.ConflictsWith = nil + } + } + dataListConfig := &datalist.ResourceConfig{ + RecordSchema: sch, + ResultAttributeName: "devices", + ResultAttributeDescription: "List of devices that match specified filters", + FlattenRecord: flattenDevice, + GetRecords: getDevices, + ExtraQuerySchema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeString, + Description: "The id of the project to query for devices", + Optional: true, + ConflictsWith: []string{"organization_id"}, + }, + "organization_id": { + Type: schema.TypeString, + Description: "The id of the organization to query for devices", + Optional: true, + ConflictsWith: []string{"project_id"}, + }, + "search": { + Type: schema.TypeString, + Description: "Search string to filter devices by hostname, description, short_id, reservation short_id, tags, plan name, plan slug, facility code, facility name, operating system name, operating system slug, IP addresses.", + Optional: true, + }, + }, + } + return datalist.NewResource(dataListConfig) +} + +func getDevices(meta interface{}, extra map[string]interface{}) ([]interface{}, error) { + client := meta.(*Config).metalgo + projectID := extra["project_id"].(string) + orgID := extra["organization_id"].(string) + + if (len(projectID) == 0) && (len(orgID) == 0) { + return nil, fmt.Errorf("one of project_id or organization_id must be specified") + } + + search := extra["search"].(string) + + var devices *metalv1.DeviceList + devicesIf := []interface{}{} + var err error + + if len(projectID) > 0 { + query := client.DevicesApi.FindProjectDevices( + context.Background(), projectID).Include(deviceCommonIncludes) + if len(search) > 0 { + query = query.Search(search) + } + devices, _, err = query.Execute() + } + + if len(orgID) > 0 { + query := client.DevicesApi.FindOrganizationDevices( + context.Background(), orgID).Include(deviceCommonIncludes) + if len(search) > 0 { + query = query.Search(search) + } + devices, _, err = query.Execute() + } + + for _, d := range devices.Devices { + devicesIf = append(devicesIf, d) + } + return devicesIf, err +} + +func flattenDevice(rawDevice interface{}, meta interface{}, extra map[string]interface{}) (map[string]interface{}, error) { + device, ok := rawDevice.(metalv1.Device) + if !ok { + return nil, fmt.Errorf("expected device to be of type *metalv1.Device, got %T", rawDevice) + } + return getDeviceMap(device), nil +} diff --git a/equinix/data_source_metal_devices_acc_test.go b/equinix/data_source_metal_devices_acc_test.go new file mode 100644 index 000000000..e783b5566 --- /dev/null +++ b/equinix/data_source_metal_devices_acc_test.go @@ -0,0 +1,82 @@ +package equinix + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceMetalDevices(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, + CheckDestroy: testAccMetalDeviceCheckDestroyed, + Steps: []resource.TestStep{ + { + Config: testDataSourceMetalDevicesConfig_basic(projectName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "data.equinix_metal_devices.test_filter_tags", "devices.#", "1"), + resource.TestCheckResourceAttr( + "data.equinix_metal_devices.test_search", "devices.#", "1"), + resource.TestCheckResourceAttrPair( + "equinix_metal_device.dev_tags", "id", + "data.equinix_metal_devices.test_filter_tags", "devices.0.device_id"), + resource.TestCheckResourceAttrPair( + "equinix_metal_device.dev_search", "id", + "data.equinix_metal_devices.test_search", "devices.0.device_id"), + ), + }, + }, + }) +} + +func testDataSourceMetalDevicesConfig_basic(projSuffix string) string { + return fmt.Sprintf(` +%[1]s + +resource "equinix_metal_project" "test" { + name = "tfacc-project-%[2]s" +} + +resource "equinix_metal_device" "dev_tags" { + hostname = "tfacc-test-device1" + plan = local.plan + metro = local.metro + operating_system = local.os + billing_cycle = "hourly" + project_id = "${equinix_metal_project.test.id}" + termination_time = "%[3]s" + tags = ["tag1", "tag2"] +} + +resource "equinix_metal_device" "dev_search" { + hostname = "tfacc-test-device2-unlikelystring" + plan = local.plan + metro = local.metro + operating_system = local.os + billing_cycle = "hourly" + project_id = "${equinix_metal_project.test.id}" + termination_time = "%[3]s" +} + +data "equinix_metal_devices" "test_filter_tags" { + project_id = equinix_metal_project.test.id + filter { + attribute = "tags" + values = ["tag1"] + } + depends_on = [equinix_metal_device.dev_tags] +} + +data "equinix_metal_devices" "test_search" { + project_id = equinix_metal_project.test.id + search = "unlikelystring" + depends_on = [equinix_metal_device.dev_search] +}`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), projSuffix, testDeviceTerminationTime()) +} diff --git a/equinix/helpers_device.go b/equinix/helpers_device.go index 6deef124b..d7a1903f5 100644 --- a/equinix/helpers_device.go +++ b/equinix/helpers_device.go @@ -5,10 +5,13 @@ import ( "errors" "fmt" "log" + "path" + "sort" "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" @@ -65,6 +68,36 @@ type NetworkInfo struct { PrivateIPv4 string } +func getNetworkInfoMetalGo(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.Management { + if *ip.AddressFamily == 4 { + if *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 getNetworkInfo(ips []*packngo.IPAddressAssignment) NetworkInfo { ni := NetworkInfo{Networks: make([]map[string]interface{}, 0, 1)} for _, ip := range ips { @@ -107,6 +140,21 @@ 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{} { ret := make([]map[string]interface{}, 0, 1) for _, p := range ps { @@ -221,3 +269,42 @@ func ipAddressSchema() *schema.Resource { }, } } + +func getDeviceMap(device metalv1.Device) map[string]interface{} { + networkInfo := getNetworkInfoMetalGo(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) + 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) + + return map[string]interface{}{ + "hostname": device.GetHostname(), + "project_id": device.Project.GetId(), + "description": device.GetDescription(), + "device_id": device.GetId(), + "facility": device.Facility.GetCode(), + "metro": device.Metro.GetCode(), + "plan": device.Plan.GetSlug(), + "operating_system": device.OperatingSystem.GetSlug(), + "state": device.GetState(), + "billing_cycle": device.GetBillingCycle(), + "ipxe_script_url": device.GetIpxeScriptUrl(), + "always_pxe": device.GetAlwaysPxe(), + "root_password": device.GetRootPassword(), + "tags": stringArrToIfArr(device.GetTags()), + "access_public_ipv6": networkInfo.PublicIPv6, + "access_public_ipv4": networkInfo.PublicIPv4, + "access_private_ipv4": networkInfo.PrivateIPv4, + "network": networkInfo.Networks, + "ssh_key_ids": keyIDs, + "ports": ports, + } +} diff --git a/equinix/provider.go b/equinix/provider.go index 63db867d3..d8eda438f 100644 --- a/equinix/provider.go +++ b/equinix/provider.go @@ -129,6 +129,7 @@ func Provider() *schema.Provider { "equinix_metal_organization": dataSourceMetalOrganization(), "equinix_metal_spot_market_price": dataSourceSpotMarketPrice(), "equinix_metal_device": dataSourceMetalDevice(), + "equinix_metal_devices": dataSourceMetalDevices(), "equinix_metal_device_bgp_neighbors": dataSourceMetalDeviceBGPNeighbors(), "equinix_metal_plans": dataSourceMetalPlans(), "equinix_metal_port": dataSourceMetalPort(),