From 3fcac8901b5036ec2336d9370fa91443b15e733a Mon Sep 17 00:00:00 2001 From: Adam Barreiro Date: Fri, 21 Jun 2024 10:46:10 +0200 Subject: [PATCH] Add vcd_org_vdc_template resource and data source (#1276) Signed-off-by: abarreiro --- .changes/v3.13.0/1276-features.md | 2 + .changes/v3.13.0/1276-improvements.md | 1 + go.mod | 3 +- go.sum | 6 +- vcd/datasource_vcd_org_vdc_template.go | 247 ++++++ vcd/datasource_vcd_resource_list.go | 48 ++ vcd/datasource_vcd_resource_list_test.go | 5 + vcd/provider.go | 2 + vcd/resource_vcd_nsxv_firewall_rule.go | 6 + vcd/resource_vcd_org_oidc.go | 2 +- vcd/resource_vcd_org_vdc_template.go | 769 ++++++++++++++++++ vcd/resource_vcd_org_vdc_template_test.go | 600 ++++++++++++++ vcd/testcheck_funcs_test.go | 5 +- website/docs/d/org_vdc_template.html.markdown | 37 + website/docs/d/resource_list.html.markdown | 1 + website/docs/r/org_vdc_template.html.markdown | 156 ++++ website/vcd.erb | 6 + 17 files changed, 1891 insertions(+), 5 deletions(-) create mode 100644 .changes/v3.13.0/1276-features.md create mode 100644 .changes/v3.13.0/1276-improvements.md create mode 100644 vcd/datasource_vcd_org_vdc_template.go create mode 100644 vcd/resource_vcd_org_vdc_template.go create mode 100644 vcd/resource_vcd_org_vdc_template_test.go create mode 100644 website/docs/d/org_vdc_template.html.markdown create mode 100644 website/docs/r/org_vdc_template.html.markdown diff --git a/.changes/v3.13.0/1276-features.md b/.changes/v3.13.0/1276-features.md new file mode 100644 index 000000000..34e99d60c --- /dev/null +++ b/.changes/v3.13.0/1276-features.md @@ -0,0 +1,2 @@ +* **New Resource:** `vcd_org_vdc_template` to manage VDC Templates [GH-1276] +* **New Data Source:** `vcd_org_vdc_template` to read VDC Templates [GH-1276] diff --git a/.changes/v3.13.0/1276-improvements.md b/.changes/v3.13.0/1276-improvements.md new file mode 100644 index 000000000..8949382c0 --- /dev/null +++ b/.changes/v3.13.0/1276-improvements.md @@ -0,0 +1 @@ +* Data source `vcd_resource_list` can list VDC Templates [GH-1276] diff --git a/go.mod b/go.mod index 8eb987237..d017ff71d 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,11 @@ go 1.22.3 require ( github.com/davecgh/go-spew v1.1.1 + github.com/google/uuid v1.6.0 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 github.com/kr/pretty v0.3.1 - github.com/vmware/go-vcloud-director/v2 v2.25.0-alpha.13 + github.com/vmware/go-vcloud-director/v2 v2.25.0-alpha.14 ) require ( diff --git a/go.sum b/go.sum index 4fdb60d5f..14ca94cdb 100644 --- a/go.sum +++ b/go.sum @@ -46,6 +46,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= @@ -148,8 +150,8 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/vmware/go-vcloud-director/v2 v2.25.0-alpha.13 h1:KPiMTxbyHMNKUIeaH43Qh2eHYetH9crpwGUu2SNYiUI= -github.com/vmware/go-vcloud-director/v2 v2.25.0-alpha.13/go.mod h1:vbuNYzuADDBFhi9i2dIKWeNxMK1VFF0dACq01amYBIM= +github.com/vmware/go-vcloud-director/v2 v2.25.0-alpha.14 h1:yHY0U8bweiVqhCR3OP0ItNdMSLPXeE7uudpXeDZbH/8= +github.com/vmware/go-vcloud-director/v2 v2.25.0-alpha.14/go.mod h1:vbuNYzuADDBFhi9i2dIKWeNxMK1VFF0dACq01amYBIM= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/vcd/datasource_vcd_org_vdc_template.go b/vcd/datasource_vcd_org_vdc_template.go new file mode 100644 index 000000000..93d0cb8e8 --- /dev/null +++ b/vcd/datasource_vcd_org_vdc_template.go @@ -0,0 +1,247 @@ +package vcd + +import ( + "context" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func datasourceVcdOrgVdcTemplate() *schema.Resource { + return &schema.Resource{ + ReadContext: datasourceVcdVdcTemplateRead, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the VDC Template as seen by the System administrator", + }, + "description": { + Type: schema.TypeString, + Computed: true, + Description: "Description of the VDC Template as seen by the System administrator", + }, + "tenant_name": { + Type: schema.TypeString, + Computed: true, + Description: "Name of the VDC Template as seen by the tenants (organizations)", + }, + "tenant_description": { + Type: schema.TypeString, + Computed: true, + Description: "Description of the VDC Template as seen by the tenants (organizations)", + }, + "provider_vdc": { + Type: schema.TypeSet, + Computed: true, + Description: "A Provider VDC that the VDCs instantiated from this template use", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "ID of Provider VDC", + }, + "external_network_id": { + Type: schema.TypeString, + Computed: true, + Description: "ID of the External network that the VDCs instantiated from this template use", + }, + "gateway_edge_cluster_id": { + Type: schema.TypeString, + Computed: true, + Description: "ID of the Edge Cluster that the VDCs instantiated from this template use with the NSX-T Gateway", + }, + "services_edge_cluster_id": { + Type: schema.TypeString, + Computed: true, + Description: "ID of the Edge Cluster that the VDCs instantiated from this template use for services", + }, + }, + }, + }, + "allocation_model": { + Type: schema.TypeString, + Computed: true, + Description: "Allocation model that the VDCs instantiated from this template use", + }, + "compute_configuration": { + Type: schema.TypeList, + Computed: true, + Description: "The compute configuration for the VDCs instantiated from this template", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cpu_allocated": { + Type: schema.TypeInt, + Computed: true, + Description: "The maximum amount of CPU, in MHz, available to the VMs running within the VDC that is instantiated from this template", + }, + "cpu_limit": { + Type: schema.TypeInt, + Computed: true, + Description: "The limit amount of CPU, in MHz, of the VDC that is instantiated from this template. 0 means unlimited", + }, + "cpu_guaranteed": { + Type: schema.TypeInt, + Computed: true, + Description: "The percentage of the CPU guaranteed to be available to VMs running within the VDC instantiated from this template", + }, + "cpu_speed": { + Type: schema.TypeInt, + Computed: true, + Description: "Specifies the clock frequency, in MHz, for any virtual CPU that is allocated to a VM", + }, + "memory_allocated": { + Type: schema.TypeInt, + Computed: true, + Description: "The maximum amount of Memory, in MB, available to the VMs running within the VDC that is instantiated from this template", + }, + "memory_limit": { + Type: schema.TypeInt, + Computed: true, + Description: "The limit amount of Memory, in MB, of the VDC that is instantiated from this template. 0 means unlimited", + }, + "memory_guaranteed": { + Type: schema.TypeInt, + Computed: true, + Description: "The percentage of the Memory guaranteed to be available to VMs running within the VDC instantiated from this template", + }, + "elasticity": { + Type: schema.TypeBool, + Computed: true, + Description: "True if compute capacity can grow or shrink based on demand", + }, + "include_vm_memory_overhead": { + Type: schema.TypeBool, + Computed: true, + Description: "True if the instantiated VDC includes memory overhead into its accounting for admission control", + }, + }, + }, + }, + "storage_profile": { + Type: schema.TypeSet, + Computed: true, + Description: "Storage profiles that the VDCs instantiated from this template use", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + Description: "Name of Provider VDC storage profile to use for the VDCs instantiated from this template", + }, + "default": { + Type: schema.TypeBool, + Computed: true, + Description: "True if this is default storage profile for the VDCs instantiated from this template", + }, + "limit": { + Type: schema.TypeInt, + Computed: true, + Description: "Storage limit of the VDCs instantiated from this template, in Megabytes. 0 means unlimited", + }, + }, + }, + }, + "enable_fast_provisioning": { + Type: schema.TypeBool, + Computed: true, + Description: "If 'true', the VDCs instantiated from this template have Fast provisioning enabled", + }, + "enable_thin_provisioning": { + Type: schema.TypeBool, + Computed: true, + Description: "If 'true', the VDCs instantiated from this template have Thin provisioning enabled", + }, + "edge_gateway": { + Type: schema.TypeList, + Computed: true, + Description: "VDCs instantiated from this template create a new Edge Gateway with the provided setup", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + Description: "Name of the Edge Gateway", + }, + "description": { + Type: schema.TypeString, + Computed: true, + Description: "Description of the Edge Gateway", + }, + "ip_allocation_count": { + Type: schema.TypeInt, + Computed: true, + Description: "Storage used in MB", + }, + "routed_network_name": { + Type: schema.TypeString, + Computed: true, + Description: "Name of the routed network to create with the Edge Gateway", + }, + "routed_network_description": { + Type: schema.TypeString, + Computed: true, + Description: "Description of the routed network to create with the Edge Gateway", + }, + "routed_network_gateway_cidr": { + Type: schema.TypeString, + Computed: true, + Description: "CIDR of the Edge Gateway for the routed network created with the Edge Gateway", + }, + "static_ip_pool": { + Type: schema.TypeSet, + Computed: true, + Description: "IP ranges used for the network created with the Edge Gateway. Only required if the 'edge_gateway' block is used", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "start_address": { + Type: schema.TypeString, + Computed: true, + Description: "Start address of the IP range", + }, + "end_address": { + Type: schema.TypeString, + Computed: true, + Description: "End address of the IP range", + }, + }, + }, + }, + }, + }, + }, + "network_pool_id": { + Type: schema.TypeString, + Computed: true, + Description: "The Network pool of the instantiated VDCs. If empty, means that it is automatically chosen", + }, + "nic_quota": { + Type: schema.TypeInt, + Computed: true, + Description: "Quota of the NICs of the instantiated VDCs. 0 means unlimited", + }, + "vm_quota": { + Type: schema.TypeInt, + Computed: true, + Description: "Quota for the VMs of the instantiated VDCs. 0 means unlimited", + }, + "provisioned_network_quota": { + Type: schema.TypeInt, + Computed: true, + Description: "Quota for the provisioned networks of the instantiated VDCs. 0 means unlimited", + }, + "readable_by_org_ids": { + Type: schema.TypeSet, + Computed: true, + Description: "IDs of the Organizations that will be able to view and instantiate this VDC template. This attribute is not available for tenants", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func datasourceVcdVdcTemplateRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return genericVcdVdcTemplateRead(ctx, d, meta, "datasource") +} diff --git a/vcd/datasource_vcd_resource_list.go b/vcd/datasource_vcd_resource_list.go index bdbc7c5fc..310d2aefa 100644 --- a/vcd/datasource_vcd_resource_list.go +++ b/vcd/datasource_vcd_resource_list.go @@ -1018,6 +1018,52 @@ func vappNetworkList(d *schema.ResourceData, vnt vappNetworkType, meta interface return genericResourceList(d, "vcd_vapp_network", []string{org.Org.Name, vdc.Vdc.Name, vappName}, items) } +func vdcTemplateList(d *schema.ResourceData, meta interface{}) (list []string, err error) { + client := meta.(*VCDClient) + + parentOrg := d.Get("parent").(string) + isSystemOrg := parentOrg == "" || strings.ToLower(parentOrg) == "system" + if isSystemOrg && !client.Client.IsSysAdmin { + return nil, fmt.Errorf("'parent' is not set or is 'System': Only an administrator can list all VDC Templates from System") + } + + var items []resourceRef + var ancestors []string + if isSystemOrg { + adminVdcTemplates, err := client.QueryAdminVdcTemplates() + if err != nil { + return nil, err + } + for _, adminVdcTemplate := range adminVdcTemplates { + items = append(items, resourceRef{ + name: adminVdcTemplate.Name, + id: extractUuid(adminVdcTemplate.HREF), + href: adminVdcTemplate.HREF, + parent: "System", + }) + } + } else { + org, err := client.GetOrg(parentOrg) + if err != nil { + return nil, err + } + vdcTemplates, err := org.QueryVdcTemplates() + if err != nil { + return nil, err + } + for _, vdcTemplate := range vdcTemplates { + items = append(items, resourceRef{ + name: vdcTemplate.Name, + id: extractUuid(vdcTemplate.HREF), + href: vdcTemplate.HREF, + parent: org.Org.Name, + }) + } + ancestors = []string{org.Org.Name} + } + return genericResourceList(d, "vcd_org_vdc_template", ancestors, items) +} + func genericResourceList(d *schema.ResourceData, resType string, ancestors []string, refs []resourceRef) (list []string, err error) { listMode := d.Get("list_mode").(string) nameIdSeparator := d.Get("name_id_separator").(string) @@ -1427,6 +1473,8 @@ func datasourceVcdResourceListRead(_ context.Context, d *schema.ResourceData, me list, err = globalRolesList(d, meta) case "vcd_library_certificate": list, err = libraryCertificateList(d, meta) + case "vcd_org_vdc_template": + list, err = vdcTemplateList(d, meta) //// place holder to remind of what needs to be implemented // case "edgegateway_vpn", diff --git a/vcd/datasource_vcd_resource_list_test.go b/vcd/datasource_vcd_resource_list_test.go index 0c4d57f73..0df5c1558 100644 --- a/vcd/datasource_vcd_resource_list_test.go +++ b/vcd/datasource_vcd_resource_list_test.go @@ -40,6 +40,10 @@ func TestAccVcdDatasourceResourceList(t *testing.T) { {name: "user", resourceType: "vcd_org_user"}, } + if usingSysAdmin() { + lists = append(lists, listDef{name: "admin-vdc-template", resourceType: "vcd_org_vdc_template"}) + } + knownNetworkPool1 := testConfig.VCD.ProviderVdc.NetworkPool if knownNetworkPool1 != "" && usingSysAdmin() { lists = append(lists, listDef{name: "network_pool", resourceType: "vcd_network_pool", knownItem: knownNetworkPool1}) @@ -82,6 +86,7 @@ func TestAccVcdDatasourceResourceList(t *testing.T) { } else { fmt.Print("`Nsxt.Vdc` value isn't configured, datasource test using this will be skipped\n") } + lists = append(lists, listDef{name: "vdc-template", resourceType: "vcd_org_vdc_template", parent: testConfig.VCD.Org}) } else { fmt.Print("`VCD.Org` value isn't configured, datasource test will be skipped\n") } diff --git a/vcd/provider.go b/vcd/provider.go index 2a18913c9..6051e1c9d 100644 --- a/vcd/provider.go +++ b/vcd/provider.go @@ -158,6 +158,7 @@ var globalDataSourceMap = map[string]*schema.Resource{ "vcd_solution_landing_zone": datasourceVcdSolutionLandingZone(), // 3.13 "vcd_solution_add_on": datasourceVcdSolutionAddon(), // 3.13 "vcd_org_oidc": datasourceVcdOrgOidc(), // 3.13 + "vcd_org_vdc_template": datasourceVcdOrgVdcTemplate(), // 3.13 } var globalResourceMap = map[string]*schema.Resource{ @@ -271,6 +272,7 @@ var globalResourceMap = map[string]*schema.Resource{ "vcd_solution_landing_zone": resourceVcdSolutionLandingZone(), // 3.13 "vcd_solution_add_on": resourceVcdSolutionAddon(), // 3.13 "vcd_org_oidc": resourceVcdOrgOidc(), // 3.13 + "vcd_org_vdc_template": resourceVcdOrgVdcTemplate(), // 3.13 } // Provider returns a terraform.ResourceProvider. diff --git a/vcd/resource_vcd_nsxv_firewall_rule.go b/vcd/resource_vcd_nsxv_firewall_rule.go index a445a7e30..8378878e2 100644 --- a/vcd/resource_vcd_nsxv_firewall_rule.go +++ b/vcd/resource_vcd_nsxv_firewall_rule.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "log" + "slices" "strconv" "strings" @@ -880,6 +881,11 @@ func stringInSlice(str string, list []string) bool { return false } +// stringInSlicePartially checks whether the input string contains any of the strings of the slice +func stringInSlicePartially(str string, list []string) bool { + return slices.ContainsFunc(list, func(s string) bool { return strings.Contains(str, s) }) +} + // resourceVcdNsxvFirewallRuleServiceHash generates a hash for service TypeSet. Its main purpose is to // avoid hash changes when port or source_port ar left empty or set as 'any'. Having empty port and // source_port is the same as having "any". diff --git a/vcd/resource_vcd_org_oidc.go b/vcd/resource_vcd_org_oidc.go index 72d4b1ca4..2cfc258d9 100644 --- a/vcd/resource_vcd_org_oidc.go +++ b/vcd/resource_vcd_org_oidc.go @@ -376,7 +376,7 @@ func genericVcdOrgOidcRead(_ context.Context, d *schema.ResourceData, meta inter orgId := d.Get("org_id").(string) adminOrg, err := vcdClient.GetAdminOrgByNameOrId(orgId) - if govcd.IsNotFound(err) && origin == "resource" { + if govcd.ContainsNotFound(err) && origin == "resource" { log.Printf("[INFO] unable to find Organization '%s' Open ID Connect settings: %s. Removing from state", orgId, err) d.SetId("") return nil diff --git a/vcd/resource_vcd_org_vdc_template.go b/vcd/resource_vcd_org_vdc_template.go new file mode 100644 index 000000000..a71ce4973 --- /dev/null +++ b/vcd/resource_vcd_org_vdc_template.go @@ -0,0 +1,769 @@ +package vcd + +import ( + "context" + "fmt" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/vmware/go-vcloud-director/v2/govcd" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "log" + "net" + "strings" +) + +func resourceVcdOrgVdcTemplate() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceVcdVdcTemplateCreate, + UpdateContext: resourceVcdVdcTemplateUpdate, + ReadContext: resourceVcdVdcTemplateRead, + DeleteContext: resourceVcdVdcTemplateDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceVcdVdcTemplateImport, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the VDC Template as seen by the System administrator", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "Description of the VDC Template as seen by the System administrator", + }, + "tenant_name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the VDC Template as seen by the tenants (organizations)", + }, + "tenant_description": { + Type: schema.TypeString, + Optional: true, + Description: "Description of the VDC Template as seen by the tenants (organizations)", + }, + "provider_vdc": { + Type: schema.TypeSet, + Required: true, + Description: "A Provider VDC that the VDCs instantiated from this template will use", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Required: true, + Description: "ID of Provider VDC", + }, + "external_network_id": { + Type: schema.TypeString, + Required: true, + Description: "ID of the External network that the VDCs instantiated from this template will use", + }, + "gateway_edge_cluster_id": { + Type: schema.TypeString, + Optional: true, + Description: "ID of the Edge Cluster that the VDCs instantiated from this template will use with the Edge Gateway", + }, + "services_edge_cluster_id": { + Type: schema.TypeString, + Optional: true, + Description: "ID of the Edge Cluster that the VDCs instantiated from this template will use for services", + }, + }, + }, + }, + "allocation_model": { + Type: schema.TypeString, + Required: true, + Description: "Allocation model that the VDCs instantiated from this template will use. Must be one of: 'AllocationVApp', 'AllocationPool', 'ReservationPool' or 'Flex'", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"AllocationVApp", "AllocationPool", "ReservationPool", "Flex"}, false)), + }, + "compute_configuration": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Description: "The compute configuration for the VDCs instantiated from this template", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cpu_allocated": { + Type: schema.TypeInt, + Optional: true, + Description: "AllocationPool, ReservationPool, Flex: The maximum amount of CPU, in MHz, available to the VMs running within the VDC that is instantiated from this template. Minimum is 256MHz", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), + }, + "cpu_limit": { + Type: schema.TypeInt, + Optional: true, + Computed: true, // Is set implicitly when allocation model is ReservationPool + Description: "AllocationVApp, ReservationPool, Flex: The limit amount of CPU, in MHz, of the VDC that is instantiated from this template. Minimum is 256MHz. 0 means unlimited", + ValidateDiagFunc: validation.ToDiagFunc(validation.Any(validation.IntBetween(0, 0), validation.IntAtLeast(256))), + }, + "cpu_guaranteed": { + Type: schema.TypeInt, + Optional: true, + Description: "AllocationVApp, AllocationPool, Flex: The percentage of the CPU guaranteed to be available to VMs running within the VDC instantiated from this template", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(0, 100)), + }, + "cpu_speed": { + Type: schema.TypeInt, + Optional: true, + Description: "AllocationVApp, AllocationPool, Flex: Specifies the clock frequency, in MHz, for any virtual CPU that is allocated to a VM. Minimum is 256MHz", + ValidateDiagFunc: validation.ToDiagFunc(validation.Any(validation.IntBetween(0, 0), validation.IntAtLeast(256))), + }, + "memory_allocated": { + Type: schema.TypeInt, + Optional: true, + Description: "AllocationPool, ReservationPool, Flex: The maximum amount of Memory, in MB, available to the VMs running within the VDC that is instantiated from this template", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), + }, + "memory_limit": { + Type: schema.TypeInt, + Optional: true, + Description: "AllocationVApp, ReservationPool, Flex: The limit amount of Memory, in MB, of the VDC that is instantiated from this template. Minimum is 1024MB. 0 means unlimited", + ValidateDiagFunc: validation.ToDiagFunc(validation.Any(validation.IntBetween(0, 0), validation.IntAtLeast(1024))), + }, + "memory_guaranteed": { + Type: schema.TypeInt, + Optional: true, + Description: "AllocationVApp, AllocationPool, Flex: The percentage of the Memory guaranteed to be available to VMs running within the VDC instantiated from this template", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(0, 100)), + }, + "elasticity": { + Type: schema.TypeBool, + Optional: true, + Computed: true, // Is set implicitly when allocation model is AllocationVApp + Description: "Flex only: True if compute capacity can grow or shrink based on demand", + }, + "include_vm_memory_overhead": { + Type: schema.TypeBool, + Optional: true, + Computed: true, // Is set implicitly when allocation model is AllocationPool + Description: "Flex only: True if the instantiated VDC includes memory overhead into its accounting for admission control", + }, + }, + }, + }, + "storage_profile": { + Type: schema.TypeSet, + Required: true, + Description: "Storage profiles that the VDCs instantiated from this template will use", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of Provider VDC storage profile to use for the VDCs instantiated from this template", + }, + "default": { + Type: schema.TypeBool, + Required: true, + Description: "True if this is default storage profile for the VDCs instantiated from this template", + }, + "limit": { + Type: schema.TypeInt, + Required: true, + Description: "Storage limit for the VDCs instantiated from this template, in Megabytes. 0 means unlimited", + }, + }, + }, + }, + "enable_fast_provisioning": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "If 'true', the VDCs instantiated from this template will have Fast provisioning enabled", + }, + "enable_thin_provisioning": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "If 'true', the VDCs instantiated from this template will have Thin provisioning enabled", + }, + "edge_gateway": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "VDCs instantiated from this template will create a new Edge Gateway with the provided setup", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the Edge Gateway", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "Description of the Edge Gateway", + }, + "ip_allocation_count": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + Description: "Allocated IPs for the Edge Gateway", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(0, 100)), + }, + "routed_network_name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the routed network to create with the Edge Gateway", + }, + "routed_network_description": { + Type: schema.TypeString, + Optional: true, + Description: "Description of the routed network to create with the Edge Gateway", + }, + "routed_network_gateway_cidr": { + Type: schema.TypeString, + Required: true, + Description: "CIDR of the Edge Gateway for the created routed network", + ValidateDiagFunc: validation.ToDiagFunc(validation.IsCIDR), + }, + "static_ip_pool": { + Type: schema.TypeSet, + MinItems: 1, + MaxItems: 1, // Due to a bug in VCD + Optional: true, + Description: "IP ranges used for the network created with the Edge Gateway. Only required if the 'edge_gateway' block is used", + Elem: networkV2IpRange, + }, + }, + }, + }, + "network_pool_id": { + Type: schema.TypeString, + Optional: true, + Description: "If set, specifies the Network pool for the instantiated VDCs. Otherwise, it is automatically chosen", + }, + "nic_quota": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + Description: "Quota for the NICs of the instantiated VDCs. 0 means unlimited", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), + }, + "vm_quota": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + Description: "Quota for the VMs of the instantiated VDCs. 0 means unlimited", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), + }, + "provisioned_network_quota": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + Description: "Quota for the provisioned networks of the instantiated VDCs. 0 means unlimited", + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), + }, + "readable_by_org_ids": { + Type: schema.TypeSet, + Optional: true, + Description: "IDs of the Organizations that will be able to view and instantiate this VDC template", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func resourceVcdVdcTemplateCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return genericVcdVdcTemplateCreateOrUpdate(ctx, d, meta, "create") +} + +func resourceVcdVdcTemplateRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return genericVcdVdcTemplateRead(ctx, d, meta, "resource") +} + +func resourceVcdVdcTemplateUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return genericVcdVdcTemplateCreateOrUpdate(ctx, d, meta, "update") +} + +func resourceVcdVdcTemplateDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vdcTemplate, err := getVdcTemplate(d, meta.(*VCDClient)) + if err != nil { + if govcd.ContainsNotFound(err) { + return nil + } + return diag.FromErr(err) + } + err = vdcTemplate.Delete() + if err != nil { + return diag.FromErr(err) + } + return nil +} + +func resourceVcdVdcTemplateImport(_ context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + vdcTemplate, err := meta.(*VCDClient).GetVdcTemplateByName(d.Id()) + if err != nil { + return nil, fmt.Errorf("could not import VDC Template with name %s: %s", d.Id(), err) + } + + dSet(d, "name", vdcTemplate.VdcTemplate.Name) + d.SetId(vdcTemplate.VdcTemplate.ID) + return []*schema.ResourceData{d}, nil +} + +func genericVcdVdcTemplateCreateOrUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}, operation string) diag.Diagnostics { + vcdClient := meta.(*VCDClient) + + providerVdcBlocks := d.Get("provider_vdc").(*schema.Set).List() + pvdcs := make([]*types.VMWVdcTemplateProviderVdcSpecification, len(providerVdcBlocks)) + + // Generate unique UUIDs as bindings. Each binding "describes a section" in the Provider VDC binding section. + // For example, "urn:vcloud:binding:foo" would describe "External network" for each Provider VDC, whereas + // "urn:vcloud:binding:bar" would describe the "Gateway Edge cluster" for each PVDC. + bindingNs := "urn:vcloud:binding:" + externalNetworkBindingId := fmt.Sprintf("%s%s", bindingNs, uuid.NewString()) + edgeGatewayBindingId := fmt.Sprintf("%s%s", bindingNs, uuid.NewString()) + servicesBindingId := fmt.Sprintf("%s%s", bindingNs, uuid.NewString()) + gatewayEdgeClusterSet, servicesEdgeClusterSet := false, false + for i, p := range providerVdcBlocks { + pvdcBlock := p.(map[string]interface{}) + var bindings []*types.VMWVdcTemplateBinding + + if pvdcBlock["external_network_id"] != "" { + bindings = append(bindings, &types.VMWVdcTemplateBinding{ + Name: externalNetworkBindingId, + Value: &types.Reference{ID: pvdcBlock["external_network_id"].(string)}, + }) + } + if pvdcBlock["gateway_edge_cluster_id"] != "" { + gatewayEdgeClusterSet = true + bindings = append(bindings, &types.VMWVdcTemplateBinding{ + Name: edgeGatewayBindingId, + Value: &types.Reference{ID: fmt.Sprintf("urn:vcloud:backingEdgeCluster:%s", pvdcBlock["gateway_edge_cluster_id"].(string))}, + }) + } + if pvdcBlock["services_edge_cluster_id"] != "" { + servicesEdgeClusterSet = true + bindings = append(bindings, &types.VMWVdcTemplateBinding{ + Name: servicesBindingId, + Value: &types.Reference{ID: fmt.Sprintf("urn:vcloud:backingEdgeCluster:%s", pvdcBlock["services_edge_cluster_id"].(string))}, + }) + } + pvdcs[i] = &types.VMWVdcTemplateProviderVdcSpecification{ + ID: pvdcBlock["id"].(string), + Binding: bindings, + } + } + + // If user sets "gateway_edge_cluster_id" inside a "provider_vdc" block, but "edge_gateway" attribute is empty, + // we should throw an error, as subsequent plans won't be able to recover the Gateway Edge cluster field and will + // cause unwanted updates-in-place until solved. + if _, ok := d.GetOk("edge_gateway"); !ok && gatewayEdgeClusterSet { + return diag.Errorf("You have set a 'gateway_edge_cluster_id' attribute inside a 'provider_vdc' block, but no 'edge_gateway' block was found. " + + "Either remove the 'gateway_edge_cluster_id' or add the 'edge_gateway' block to the configuration") + } + + // Gateway configuration + var gateway *types.VdcTemplateSpecificationGatewayConfiguration + if g, ok := d.GetOk("edge_gateway"); ok && len(g.([]interface{})) > 0 { + gatewayBlock := g.([]interface{})[0].(map[string]interface{}) + + // Static pools. Schema guarantees there's at least 1 + staticPoolBlocks := gatewayBlock["static_ip_pool"].(*schema.Set).List() + staticPools := make([]*types.IPRange, len(staticPoolBlocks)) + for i, b := range staticPoolBlocks { + block := b.(map[string]interface{}) + staticPools[i] = &types.IPRange{ + StartAddress: block["start_address"].(string), + EndAddress: block["end_address"].(string), + } + } + + ip, cidr, err := net.ParseCIDR(gatewayBlock["routed_network_gateway_cidr"].(string)) + if err != nil { + return diag.Errorf("could not %s VDC Template: error parsing 'routed_network_gateway_cidr': %s", operation, err) + } + prefixLength, _ := cidr.Mask.Size() + + gateway = &types.VdcTemplateSpecificationGatewayConfiguration{ + Gateway: &types.EdgeGateway{ + Name: gatewayBlock["name"].(string), + Description: gatewayBlock["description"].(string), + Configuration: &types.GatewayConfiguration{ + GatewayInterfaces: &types.GatewayInterfaces{GatewayInterface: []*types.GatewayInterface{ + { + Name: edgeGatewayBindingId, + DisplayName: edgeGatewayBindingId, + Connected: true, + Network: &types.Reference{ + HREF: edgeGatewayBindingId, + }, + QuickAddAllocatedIpCount: gatewayBlock["ip_allocation_count"].(int), + }, + }}, + }, + }, + Network: &types.OrgVDCNetwork{ + Name: gatewayBlock["routed_network_name"].(string), + Description: gatewayBlock["routed_network_description"].(string), + Configuration: &types.NetworkConfiguration{ + IPScopes: &types.IPScopes{IPScope: []*types.IPScope{ + { + Gateway: ip.String(), + Netmask: fmt.Sprintf("%d.%d.%d.%d", cidr.Mask[0], cidr.Mask[1], cidr.Mask[2], cidr.Mask[3]), + SubnetPrefixLengthInt: &prefixLength, + IPRanges: &types.IPRanges{IPRange: staticPools}, + }, + }}, + FenceMode: "natRouted", + }, + }, + } + if gatewayEdgeClusterSet { + gateway.Gateway.Configuration.EdgeClusterConfiguration = &types.EdgeClusterConfiguration{PrimaryEdgeCluster: &types.Reference{HREF: edgeGatewayBindingId}} + } + } + + // Storage profiles + spBlocks := d.Get("storage_profile").(*schema.Set).List() + storageProfiles := make([]*types.VdcStorageProfile, len(spBlocks)) + for i, sp := range spBlocks { + spBlock := sp.(map[string]interface{}) + storageProfile := &types.VdcStorageProfile{ + Name: spBlock["name"].(string), + Enabled: addrOf(true), + Units: "MB", + Limit: int64(spBlock["limit"].(int)), + Default: spBlock["default"].(bool), + } + storageProfiles[i] = storageProfile + } + + allocationModel := getVdcTemplateType(d.Get("allocation_model").(string)) + settings := types.VMWVdcTemplate{ + NetworkBackingType: "NSX_T", // The only supported network provider + ProviderVdcReference: pvdcs, + Name: d.Get("name").(string), + Description: d.Get("description").(string), + TenantName: d.Get("tenant_name").(string), + TenantDescription: d.Get("tenant_description").(string), + VdcTemplateSpecification: &types.VMWVdcTemplateSpecification{ + Type: allocationModel, + NicQuota: d.Get("nic_quota").(int), + VmQuota: d.Get("vm_quota").(int), + ProvisionedNetworkQuota: d.Get("provisioned_network_quota").(int), + GatewayConfiguration: gateway, + StorageProfile: storageProfiles, + ThinProvision: d.Get("enable_thin_provisioning").(bool), + FastProvisioningEnabled: d.Get("enable_fast_provisioning").(bool), + }, + } + + // Get the compute configuration. There are too many combinations to perform "smart validations" here, + // and given that VCD behaves well when input is incorrect, we skip them. + // Schema guarantees that there's exactly 1 item, so we can retrieve every field directly. + if c, ok := d.GetOk("compute_configuration.0.elasticity"); ok { + if allocationModel != types.VdcTemplatePayAsYouGoType && allocationModel != types.VdcTemplateFlexType { + return diag.Errorf("could not %s the VDC Template, 'elasticity' can only be set when 'allocation_model' is AllocationVApp or Flex', but it is %s", operation, d.Get("allocation_model")) + } + settings.VdcTemplateSpecification.IsElastic = addrOf(c.(bool)) + } + if c, ok := d.GetOk("compute_configuration.0.include_vm_memory_overhead"); ok { + if allocationModel != types.VdcTemplateAllocationPoolType && allocationModel != types.VdcTemplateReservationPoolType && allocationModel != types.VdcTemplateFlexType { + return diag.Errorf("could not %s the VDC Template, 'include_vm_memory_overhead' can only be set when 'allocation_model' is AllocationPool, ReservationPool or Flex', but it is %s", operation, d.Get("allocation_model")) + } + settings.VdcTemplateSpecification.IncludeMemoryOverhead = addrOf(c.(bool)) + } + + if c, ok := d.GetOk("compute_configuration.0.cpu_allocated"); ok { + settings.VdcTemplateSpecification.CpuAllocationMhz = c.(int) + } + if c, ok := d.GetOk("compute_configuration.0.cpu_limit"); ok { + settings.VdcTemplateSpecification.CpuLimitMhz = c.(int) + } + if c, ok := d.GetOk("compute_configuration.0.cpu_guaranteed"); ok { + settings.VdcTemplateSpecification.CpuGuaranteedPercentage = c.(int) + } + if c, ok := d.GetOk("compute_configuration.0.cpu_speed"); ok { + if allocationModel != types.VdcTemplateAllocationPoolType { + settings.VdcTemplateSpecification.CpuLimitMhzPerVcpu = c.(int) + } else { + settings.VdcTemplateSpecification.VCpuInMhz = c.(int) + } + } + if c, ok := d.GetOk("compute_configuration.0.memory_allocated"); ok { + settings.VdcTemplateSpecification.MemoryAllocationMB = c.(int) + } + if c, ok := d.GetOk("compute_configuration.0.memory_limit"); ok { + settings.VdcTemplateSpecification.MemoryLimitMb = c.(int) + } + if c, ok := d.GetOk("compute_configuration.0.memory_guaranteed"); ok { + settings.VdcTemplateSpecification.MemoryGuaranteedPercentage = c.(int) + } + + // Network pool. If not specified, we need to populate the AutomaticNetworkPoolReference field with an empty object + // (it's not a bool for some reason). + if networkPoolId, ok := d.GetOk("network_pool_id"); ok { + // We need to convert ID to HREF to avoid problems in the backend. + settings.VdcTemplateSpecification.NetworkPoolReference = &types.Reference{ + ID: networkPoolId.(string), + HREF: fmt.Sprintf("%s/cloudapi/%s%s%s", vcdClient.Client.VCDHREF.String(), types.OpenApiPathVersion1_0_0, types.OpenApiEndpointNetworkPools, networkPoolId), + } + settings.VdcTemplateSpecification.AutomaticNetworkPoolReference = nil + } else { + settings.VdcTemplateSpecification.NetworkPoolReference = nil + settings.VdcTemplateSpecification.AutomaticNetworkPoolReference = &types.AutomaticNetworkPoolReference{} + } + + if servicesEdgeClusterSet { + settings.VdcTemplateSpecification.NetworkProfileConfiguration = &types.VdcTemplateNetworkProfile{ + ServicesEdgeCluster: &types.Reference{HREF: servicesBindingId}, + } + } + + // The create and update operations are almost identical + var err error + var vdcTemplate *govcd.VdcTemplate + switch operation { + case "create": + vdcTemplate, err = vcdClient.CreateVdcTemplate(settings) + case "update": + vdcTemplate, err = vcdClient.GetVdcTemplateById(d.Id()) + if err != nil { + return diag.Errorf("could not retrieve the VDC Template to update it: %s", err) + } + vdcTemplate, err = vdcTemplate.Update(settings) + default: + return diag.Errorf("the operation '%s' is not supported for VDC templates", operation) + } + if err != nil { + return diag.Errorf("could not %s the VDC Template: %s", operation, err) + } + + if vdcTemplate != nil { + orgs := d.Get("readable_by_org_ids").(*schema.Set) + if len(orgs.List()) > 0 { + err = vdcTemplate.SetAccessControl(convertSchemaSetToSliceOfStrings(orgs)) + if err != nil { + return diag.Errorf("could not %s VDC Template, setting access list failed: %s", operation, err) + } + } + } + + return resourceVcdVdcTemplateRead(ctx, d, meta) +} + +func genericVcdVdcTemplateRead(_ context.Context, d *schema.ResourceData, meta interface{}, origin string) diag.Diagnostics { + vdcTemplate, err := getVdcTemplate(d, meta.(*VCDClient)) + if govcd.ContainsNotFound(err) && origin == "resource" { + log.Printf("[INFO] unable to find VDC Template: %s. Removing from state", err) + d.SetId("") + return nil + } + if err != nil { + return diag.FromErr(err) + } + if vdcTemplate.VdcTemplate.VdcTemplateSpecification == nil { + return diag.Errorf("could not read VDC Template with ID '%s', its specification is nil", vdcTemplate.VdcTemplate.ID) + } + + // Retrieve the Binding name for the Services Edge cluster, if present. This will be used later, when reading Provider + // VDC information, to put the correct data into the Terraform state. + servicesEdgeClusterBindingName := "" + if vdcTemplate.VdcTemplate.VdcTemplateSpecification.NetworkProfileConfiguration != nil && + vdcTemplate.VdcTemplate.VdcTemplateSpecification.NetworkProfileConfiguration.ServicesEdgeCluster != nil { + servicesEdgeClusterBindingName = vdcTemplate.VdcTemplate.VdcTemplateSpecification.NetworkProfileConfiguration.ServicesEdgeCluster.HREF + } + + dSet(d, "name", vdcTemplate.VdcTemplate.Name) + dSet(d, "description", vdcTemplate.VdcTemplate.Description) + dSet(d, "tenant_name", vdcTemplate.VdcTemplate.TenantName) + dSet(d, "tenant_description", vdcTemplate.VdcTemplate.TenantDescription) + dSet(d, "allocation_model", getVdcTemplateType(vdcTemplate.VdcTemplate.VdcTemplateSpecification.Type)) + dSet(d, "enable_fast_provisioning", vdcTemplate.VdcTemplate.VdcTemplateSpecification.FastProvisioningEnabled) + dSet(d, "enable_thin_provisioning", vdcTemplate.VdcTemplate.VdcTemplateSpecification.ThinProvision) + dSet(d, "nic_quota", vdcTemplate.VdcTemplate.VdcTemplateSpecification.NicQuota) + dSet(d, "vm_quota", vdcTemplate.VdcTemplate.VdcTemplateSpecification.VmQuota) + dSet(d, "provisioned_network_quota", vdcTemplate.VdcTemplate.VdcTemplateSpecification.ProvisionedNetworkQuota) + + // Compute block + compute := map[string]interface{}{ + "cpu_allocated": vdcTemplate.VdcTemplate.VdcTemplateSpecification.CpuAllocationMhz, + "cpu_limit": vdcTemplate.VdcTemplate.VdcTemplateSpecification.CpuLimitMhz, + "cpu_guaranteed": vdcTemplate.VdcTemplate.VdcTemplateSpecification.CpuGuaranteedPercentage, + "memory_allocated": vdcTemplate.VdcTemplate.VdcTemplateSpecification.MemoryAllocationMB, + "memory_limit": vdcTemplate.VdcTemplate.VdcTemplateSpecification.MemoryLimitMb, + "memory_guaranteed": vdcTemplate.VdcTemplate.VdcTemplateSpecification.MemoryGuaranteedPercentage, + "elasticity": vdcTemplate.VdcTemplate.VdcTemplateSpecification.IsElastic != nil && *vdcTemplate.VdcTemplate.VdcTemplateSpecification.IsElastic, + "include_vm_memory_overhead": vdcTemplate.VdcTemplate.VdcTemplateSpecification.IncludeMemoryOverhead != nil && *vdcTemplate.VdcTemplate.VdcTemplateSpecification.IncludeMemoryOverhead, + } + if vdcTemplate.VdcTemplate.VdcTemplateSpecification.Type != types.VdcTemplateAllocationPoolType { + compute["cpu_speed"] = vdcTemplate.VdcTemplate.VdcTemplateSpecification.CpuLimitMhzPerVcpu + } else { + compute["cpu_speed"] = vdcTemplate.VdcTemplate.VdcTemplateSpecification.VCpuInMhz + } + err = d.Set("compute_configuration", []interface{}{compute}) + if err != nil { + return diag.FromErr(err) + } + + // Network pool, optional + if vdcTemplate.VdcTemplate.VdcTemplateSpecification.NetworkPoolReference != nil { + dSet(d, "network_pool_id", vdcTemplate.VdcTemplate.VdcTemplateSpecification.NetworkPoolReference.ID) + } + + // Storage profiles + storageProfiles := make([]interface{}, len(vdcTemplate.VdcTemplate.VdcTemplateSpecification.StorageProfile)) + for i, storageProfile := range vdcTemplate.VdcTemplate.VdcTemplateSpecification.StorageProfile { + sp := map[string]interface{}{} + sp["name"] = storageProfile.Name + sp["default"] = storageProfile.Default + sp["limit"] = storageProfile.Limit + storageProfiles[i] = sp + } + err = d.Set("storage_profile", storageProfiles) + if err != nil { + return diag.FromErr(err) + } + + // Edge gateway configuration, optional. + gatewayConfiguration := vdcTemplate.VdcTemplate.VdcTemplateSpecification.GatewayConfiguration + var edgeGatewayConfig []interface{} + edgeGatewayEdgeClusterBindingName := "" + if gatewayConfiguration != nil && gatewayConfiguration.Gateway != nil && gatewayConfiguration.Network != nil && + gatewayConfiguration.Gateway.Configuration != nil && gatewayConfiguration.Gateway.Configuration.GatewayInterfaces != nil && + len(gatewayConfiguration.Gateway.Configuration.GatewayInterfaces.GatewayInterface) > 0 && + gatewayConfiguration.Network.Configuration != nil && gatewayConfiguration.Network.Configuration.IPScopes != nil && + len(gatewayConfiguration.Network.Configuration.IPScopes.IPScope) > 0 { + + ec := map[string]interface{}{} + ec["name"] = gatewayConfiguration.Gateway.Name + ec["description"] = gatewayConfiguration.Gateway.Description + ec["ip_allocation_count"] = gatewayConfiguration.Gateway.Configuration.GatewayInterfaces.GatewayInterface[0].QuickAddAllocatedIpCount + ec["routed_network_name"] = gatewayConfiguration.Network.Name + ec["routed_network_description"] = gatewayConfiguration.Network.Description + if gatewayConfiguration.Network.Configuration.IPScopes.IPScope[0].SubnetPrefixLengthInt != nil { + ec["routed_network_gateway_cidr"] = fmt.Sprintf("%s/%d", gatewayConfiguration.Network.Configuration.IPScopes.IPScope[0].Gateway, *vdcTemplate.VdcTemplate.VdcTemplateSpecification.GatewayConfiguration.Network.Configuration.IPScopes.IPScope[0].SubnetPrefixLengthInt) + } + if gatewayConfiguration.Network.Configuration.IPScopes.IPScope[0].IPRanges != nil { + ipRanges := make([]interface{}, len(gatewayConfiguration.Network.Configuration.IPScopes.IPScope[0].IPRanges.IPRange)) + for i, ir := range gatewayConfiguration.Network.Configuration.IPScopes.IPScope[0].IPRanges.IPRange { + ipRange := map[string]interface{}{} + ipRange["start_address"] = ir.StartAddress + ipRange["end_address"] = ir.EndAddress + ipRanges[i] = ipRange + } + ec["static_ip_pool"] = ipRanges + } + + // Retrieve the Binding name for the Gateway Edge cluster, if present. This will be used later, when reading Provider + // VDC information, to put the correct data into the Terraform state. + if gatewayConfiguration.Gateway.Configuration.EdgeClusterConfiguration != nil && + gatewayConfiguration.Gateway.Configuration.EdgeClusterConfiguration.PrimaryEdgeCluster != nil { + edgeGatewayEdgeClusterBindingName = gatewayConfiguration.Gateway.Configuration.EdgeClusterConfiguration.PrimaryEdgeCluster.HREF + } + + edgeGatewayConfig = append(edgeGatewayConfig, ec) + } + err = d.Set("edge_gateway", edgeGatewayConfig) + if err != nil { + return diag.FromErr(err) + } + + // Provider VDC information must be handled at the end, because we need the Binding names for the + // Gateway and Services edge clusters, which are obtained above. + pvdcBlock := make([]interface{}, len(vdcTemplate.VdcTemplate.ProviderVdcReference)) + for i, providerVdcRef := range vdcTemplate.VdcTemplate.ProviderVdcReference { + p := map[string]interface{}{} + pvdcId, err := govcd.GetUuidFromHref(providerVdcRef.HREF, true) + if err != nil { + return diag.Errorf("error reading VDC Template: %s", err) + } + p["id"] = fmt.Sprintf("urn:vcloud:providervdc:%s", pvdcId) + for _, binding := range providerVdcRef.Binding { + if binding.Value == nil { + continue + } + switch binding.Name { + // We do the ReplaceAll as the UUIDs from vcd_nsxt_edge_cluster (or other API calls) come without any namespace, but the VDC Template uses + // "urn:vcloud:backingEdgeCluster:". If we don't remove them, Terraform would ask for a replacement of the whole block in subsequent plans. + case edgeGatewayEdgeClusterBindingName: + p["gateway_edge_cluster_id"] = strings.ReplaceAll(binding.Value.ID, "urn:vcloud:backingEdgeCluster:", "") + case servicesEdgeClusterBindingName: + p["services_edge_cluster_id"] = strings.ReplaceAll(binding.Value.ID, "urn:vcloud:backingEdgeCluster:", "") + default: + if strings.HasPrefix(binding.Value.ID, "urn:vcloud:network:") { + p["external_network_id"] = binding.Value.ID + } + } + } + pvdcBlock[i] = p + } + err = d.Set("provider_vdc", pvdcBlock) + if err != nil { + return diag.FromErr(err) + } + + // Access settings is only available for System administrators + if meta.(*VCDClient).Client.IsSysAdmin { + access, err := vdcTemplate.GetAccessControl() + if err != nil { + return diag.Errorf("could not read VDC Template, retrieving its setting access list failed: %s", err) + } + if access != nil && access.AccessSettings != nil { + orgIds := make([]string, len(access.AccessSettings.AccessSetting)) + for i, setting := range access.AccessSettings.AccessSetting { + if setting.Subject != nil { + orgIds[i] = fmt.Sprintf("urn:vcloud:org:%s", extractUuid(setting.Subject.HREF)) + } + } + err = d.Set("readable_by_org_ids", orgIds) + if err != nil { + return diag.FromErr(err) + } + } + } + + d.SetId(vdcTemplate.VdcTemplate.ID) + + return nil +} + +// getVdcTemplate retrieves a VDC Template with the available information in the configuration. +func getVdcTemplate(d *schema.ResourceData, vcdClient *VCDClient) (*govcd.VdcTemplate, error) { + var vdcTemplate *govcd.VdcTemplate + var err error + if d.Id() == "" { + name := d.Get("name").(string) + vdcTemplate, err = vcdClient.GetVdcTemplateByName(name) + if err != nil { + return nil, fmt.Errorf("could not read VDC Template with name %s: %s", name, err) + } + } else { + vdcTemplate, err = vcdClient.GetVdcTemplateById(d.Id()) + if err != nil { + return nil, fmt.Errorf("could not read VDC Template with ID %s: %s", d.Id(), err) + } + } + return vdcTemplate, nil +} + +// getVdcTemplateType transforms the allocation models used by VDC Templates to the allocation models used by regular +// VDCs and viceversa. +func getVdcTemplateType(input string) string { + switch input { + case types.VdcTemplatePayAsYouGoType: + return "AllocationVApp" + case types.VdcTemplateAllocationPoolType: + return "AllocationPool" + case types.VdcTemplateReservationPoolType: + return "ReservationPool" + case types.VdcTemplateFlexType: + return "Flex" + case "AllocationVApp": + return types.VdcTemplatePayAsYouGoType + case "AllocationPool": + return types.VdcTemplateAllocationPoolType + case "ReservationPool": + return types.VdcTemplateReservationPoolType + case "Flex": + return types.VdcTemplateFlexType + } + return "" +} diff --git a/vcd/resource_vcd_org_vdc_template_test.go b/vcd/resource_vcd_org_vdc_template_test.go new file mode 100644 index 000000000..94c0326ff --- /dev/null +++ b/vcd/resource_vcd_org_vdc_template_test.go @@ -0,0 +1,600 @@ +//go:build vdc || ALL || functional + +package vcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/govcd" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +// TestAccVcdVdcTemplate tests the creation and update of 4 VDC templates with vcd_org_vdc_template, each one using a different Allocation model. +// Also tests the data source and import features. +func TestAccVcdVdcTemplate(t *testing.T) { + preTestChecks(t) + skipIfNotSysAdmin(t) + + var params = StringMap{ + "ProviderVcdSystem": providerVcdSystem, + "ProviderVcdTenant": providerVcdOrg1, + "OrgToPublish": testConfig.VCD.Org, + "ProviderVdc": testConfig.VCD.NsxtProviderVdc.Name, + "Vdc": testConfig.Nsxt.Vdc, + "EdgeCluster": testConfig.Nsxt.NsxtEdgeCluster, + "ExternalNetwork": testConfig.Nsxt.ExternalNetwork, + "NetworkPool": testConfig.VCD.NsxtProviderVdc.NetworkPool, + "StorageProfile": testConfig.VCD.NsxtProviderVdc.StorageProfile, + "StorageProfile2": testConfig.VCD.NsxtProviderVdc.StorageProfile2, + "Name1": t.Name() + "1", + "Name2": t.Name() + "2", + "Name3": t.Name() + "3", + "Name4": t.Name() + "4", + "FuncName": t.Name() + "Step1", + "StorageProfileLimit": 1024, + "CpuLimit": 0, + "CpuGuaranteed": 20, + "CpuSpeed": 1000, + "CpuAllocated": 256, + "MemoryLimit": 0, + "MemoryGuaranteed": 50, + "MemoryAllocated": 1024, + } + testParamsNotEmpty(t, params) + + step1 := templateFill(testAccVdcTemplateResource, params) + debugPrintf("#[DEBUG] CONFIGURATION - Step 1: %s", step1) + params["FuncName"] = t.Name() + "Step2" + params["StorageProfileLimit"] = 2048 + params["CpuLimit"] = 512 + params["CpuGuaranteed"] = 100 + params["CpuSpeed"] = 1500 + params["CpuAllocated"] = 512 + params["MemoryLimit"] = 2048 + params["MemoryGuaranteed"] = 60 + params["MemoryAllocated"] = 1536 + step2 := templateFill(testAccVdcTemplateResource, params) + debugPrintf("#[DEBUG] CONFIGURATION - Step 2: %s", step2) + params["FuncName"] = t.Name() + "Step3" + step3 := templateFill(testAccVdcTemplateResourceAndDatasource, params) + debugPrintf("#[DEBUG] CONFIGURATION - Step 3: %s", step3) + + if vcdShortTest { + t.Skip(acceptanceTestsSkipped) + return + } + + template := "vcd_org_vdc_template.template" + dsTemplate := "data.vcd_org_vdc_template.ds_template" + + resource.Test(t, resource.TestCase{ + ProviderFactories: buildMultipleProviders(), + CheckDestroy: resource.ComposeAggregateTestCheckFunc( + testAccCheckVdcTemplateDestroyed(params["Name1"].(string)), + testAccCheckVdcTemplateDestroyed(params["Name2"].(string)), + testAccCheckVdcTemplateDestroyed(params["Name3"].(string)), + testAccCheckVdcTemplateDestroyed(params["Name4"].(string)), + ), + Steps: []resource.TestStep{ + { + Config: step1, + Check: resource.ComposeTestCheckFunc( + // First VDC template + resource.TestCheckResourceAttr(template+"1", "name", params["Name1"].(string)), + resource.TestCheckResourceAttr(template+"1", "description", params["Name1"].(string)+"_description"), + resource.TestCheckResourceAttr(template+"1", "tenant_name", params["Name1"].(string)+"_tenant"), + resource.TestCheckResourceAttr(template+"1", "tenant_description", params["Name1"].(string)+"_tenant_description"), + resource.TestMatchTypeSetElemNestedAttrs(template+"1", "provider_vdc.*", map[string]*regexp.Regexp{ + "id": regexp.MustCompile(`^urn:vcloud:providervdc:.+$`), + "external_network_id": regexp.MustCompile(`^urn:vcloud:network:.+$`), + }), + resource.TestCheckResourceAttr(template+"1", "allocation_model", "AllocationVApp"), + resource.TestCheckTypeSetElemNestedAttrs(template+"1", "compute_configuration.*", map[string]string{ + "cpu_allocated": "0", // Not used + "cpu_limit": "0", + "cpu_guaranteed": "20", + "cpu_speed": "1000", + "memory_allocated": "0", // Not used + "memory_guaranteed": "50", + "memory_limit": "0", + "elasticity": "true", // Computed + "include_vm_memory_overhead": "false", // Not used + }), + resource.TestCheckTypeSetElemNestedAttrs(template+"1", "storage_profile.*", map[string]string{ + "name": params["StorageProfile"].(string), + "default": "true", + "limit": "1024", + }), + resource.TestCheckResourceAttr(template+"1", "enable_fast_provisioning", "false"), + resource.TestCheckResourceAttr(template+"1", "enable_thin_provisioning", "false"), + resource.TestCheckTypeSetElemNestedAttrs(template+"1", "storage_profile.*", map[string]string{ + "name": params["StorageProfile"].(string), + "default": "true", + "limit": "1024", + }), + resource.TestCheckNoResourceAttr(template+"1", "edge_gateway.0"), + resource.TestMatchResourceAttr(template+"1", "network_pool_id", regexp.MustCompile(`^urn:vcloud:networkpool:.+$`)), + resource.TestCheckResourceAttr(template+"1", "nic_quota", "0"), + resource.TestCheckResourceAttr(template+"1", "vm_quota", "0"), + resource.TestCheckResourceAttr(template+"1", "provisioned_network_quota", "0"), + resource.TestCheckResourceAttr(template+"1", "readable_by_org_ids.#", "1"), + + // Second VDC template + resource.TestCheckResourceAttr(template+"2", "name", params["Name2"].(string)), + resource.TestCheckResourceAttr(template+"2", "description", params["Name2"].(string)+"_description"), + resource.TestCheckResourceAttr(template+"2", "tenant_name", params["Name2"].(string)+"_tenant"), + resource.TestCheckResourceAttr(template+"2", "tenant_description", params["Name2"].(string)+"_tenant_description"), + resource.TestMatchTypeSetElemNestedAttrs(template+"2", "provider_vdc.*", map[string]*regexp.Regexp{ + "id": regexp.MustCompile(`^urn:vcloud:providervdc:.+$`), + "external_network_id": regexp.MustCompile(`^urn:vcloud:network:.+$`), + }), + resource.TestCheckResourceAttr(template+"2", "allocation_model", "AllocationPool"), + resource.TestCheckTypeSetElemNestedAttrs(template+"2", "compute_configuration.*", map[string]string{ + "cpu_allocated": "256", + "cpu_limit": "0", // Not used + "cpu_guaranteed": "20", + "cpu_speed": "1000", + "memory_allocated": "1024", + "memory_guaranteed": "50", + "memory_limit": "0", // Not used + "elasticity": "false", // Not used + "include_vm_memory_overhead": "true", // Computed + }), + resource.TestCheckTypeSetElemNestedAttrs(template+"2", "storage_profile.*", map[string]string{ + "name": params["StorageProfile"].(string), + "default": "true", + "limit": "1024", + }), + resource.TestCheckTypeSetElemNestedAttrs(template+"2", "storage_profile.*", map[string]string{ + "name": params["StorageProfile2"].(string), + "default": "false", + "limit": "128", + }), + resource.TestCheckResourceAttr(template+"2", "enable_fast_provisioning", "true"), + resource.TestCheckResourceAttr(template+"2", "enable_thin_provisioning", "true"), + resource.TestCheckResourceAttr(template+"2", "edge_gateway.0.name", "edgy2"), + resource.TestCheckResourceAttr(template+"2", "edge_gateway.0.ip_allocation_count", "10"), + resource.TestCheckResourceAttr(template+"2", "edge_gateway.0.routed_network_name", "net2"), + resource.TestCheckResourceAttr(template+"2", "edge_gateway.0.routed_network_gateway_cidr", "1.2.3.4/24"), + resource.TestCheckTypeSetElemNestedAttrs(template+"2", "edge_gateway.0.static_ip_pool.*", map[string]string{ + "start_address": "1.2.3.4", + "end_address": "1.2.3.4", + }), + resource.TestCheckNoResourceAttr(template+"2", "network_pool_id"), + resource.TestCheckResourceAttr(template+"2", "nic_quota", "0"), + resource.TestCheckResourceAttr(template+"2", "vm_quota", "0"), + resource.TestCheckResourceAttr(template+"2", "provisioned_network_quota", "0"), + resource.TestCheckResourceAttr(template+"2", "readable_by_org_ids.#", "0"), + + // Third VDC template + resource.TestCheckResourceAttr(template+"3", "name", params["Name3"].(string)), + resource.TestCheckResourceAttr(template+"3", "description", params["Name3"].(string)+"_description"), + resource.TestCheckResourceAttr(template+"3", "tenant_name", params["Name3"].(string)+"_tenant"), + resource.TestCheckResourceAttr(template+"3", "tenant_description", params["Name3"].(string)+"_tenant_description"), + resource.TestMatchTypeSetElemNestedAttrs(template+"3", "provider_vdc.*", map[string]*regexp.Regexp{ + "id": regexp.MustCompile(`^urn:vcloud:providervdc:.+$`), + "external_network_id": regexp.MustCompile(`^urn:vcloud:network:.+$`), + "gateway_edge_cluster_id": regexp.MustCompile(`^.+$`), + }), + resource.TestCheckResourceAttr(template+"3", "allocation_model", "ReservationPool"), + resource.TestCheckTypeSetElemNestedAttrs(template+"3", "compute_configuration.*", map[string]string{ + "cpu_allocated": "256", + "cpu_limit": "0", // Unlimited + "cpu_guaranteed": "0", // Not used + "cpu_speed": "0", // Not used + "memory_allocated": "1024", + "memory_guaranteed": "0", // Not used + "memory_limit": "0", // Not used + "elasticity": "false", + "include_vm_memory_overhead": "true", // Computed + }), + resource.TestCheckTypeSetElemNestedAttrs(template+"3", "storage_profile.*", map[string]string{ + "name": params["StorageProfile"].(string), + "default": "true", + "limit": "1024", + }), + resource.TestCheckResourceAttr(template+"3", "enable_fast_provisioning", "false"), + resource.TestCheckResourceAttr(template+"3", "enable_thin_provisioning", "false"), + resource.TestCheckResourceAttr(template+"3", "edge_gateway.0.name", "edgy3"), + resource.TestCheckResourceAttr(template+"3", "edge_gateway.0.ip_allocation_count", "15"), + resource.TestCheckResourceAttr(template+"3", "edge_gateway.0.routed_network_name", "net3"), + resource.TestCheckResourceAttr(template+"3", "edge_gateway.0.routed_network_gateway_cidr", "1.1.1.1/2"), + resource.TestCheckTypeSetElemNestedAttrs(template+"3", "edge_gateway.0.static_ip_pool.*", map[string]string{ + "start_address": "1.1.1.1", + "end_address": "1.1.1.1", + }), + resource.TestMatchResourceAttr(template+"3", "network_pool_id", regexp.MustCompile(`^urn:vcloud:networkpool:.+$`)), + resource.TestCheckResourceAttr(template+"3", "nic_quota", "100"), + resource.TestCheckResourceAttr(template+"3", "vm_quota", "100"), + resource.TestCheckResourceAttr(template+"3", "provisioned_network_quota", "20"), + resource.TestCheckResourceAttr(template+"1", "readable_by_org_ids.#", "1"), + + // Fourth VDC template + resource.TestCheckResourceAttr(template+"4", "name", params["Name4"].(string)), + resource.TestCheckResourceAttr(template+"4", "description", params["Name4"].(string)+"_description"), + resource.TestCheckResourceAttr(template+"4", "tenant_name", params["Name4"].(string)+"_tenant"), + resource.TestCheckResourceAttr(template+"4", "tenant_description", params["Name4"].(string)+"_tenant_description"), + resource.TestMatchTypeSetElemNestedAttrs(template+"4", "provider_vdc.*", map[string]*regexp.Regexp{ + "id": regexp.MustCompile(`^urn:vcloud:providervdc:.+$`), + "external_network_id": regexp.MustCompile(`^urn:vcloud:network:.+$`), + }), + resource.TestCheckResourceAttr(template+"4", "allocation_model", "Flex"), + resource.TestCheckTypeSetElemNestedAttrs(template+"4", "compute_configuration.*", map[string]string{ + "cpu_allocated": "256", + "cpu_limit": "0", + "cpu_guaranteed": "20", + "cpu_speed": "1000", + "memory_allocated": "1024", + "memory_guaranteed": "50", + "memory_limit": "0", + "elasticity": "true", + "include_vm_memory_overhead": "true", + }), + resource.TestCheckTypeSetElemNestedAttrs(template+"4", "storage_profile.*", map[string]string{ + "name": params["StorageProfile"].(string), + "default": "true", + "limit": "1024", + }), + resource.TestCheckResourceAttr(template+"4", "enable_fast_provisioning", "false"), + resource.TestCheckResourceAttr(template+"4", "enable_thin_provisioning", "false"), + resource.TestCheckNoResourceAttr(template+"4", "edge_gateway.0"), + resource.TestMatchResourceAttr(template+"4", "network_pool_id", regexp.MustCompile(`^urn:vcloud:networkpool:.+$`)), + resource.TestCheckResourceAttr(template+"4", "nic_quota", "0"), + resource.TestCheckResourceAttr(template+"4", "vm_quota", "0"), + resource.TestCheckResourceAttr(template+"4", "provisioned_network_quota", "0"), + resource.TestCheckResourceAttr(template+"4", "readable_by_org_ids.#", "1"), + ), + }, + { + Config: step2, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(template+"1", "name", params["Name1"].(string)), + resource.TestCheckTypeSetElemNestedAttrs(template+"1", "compute_configuration.*", map[string]string{ + "cpu_allocated": "0", // Not used + "cpu_limit": "512", + "cpu_guaranteed": "100", + "cpu_speed": "1500", + "memory_allocated": "0", // Not used + "memory_guaranteed": "60", + "memory_limit": "2048", + "elasticity": "true", // Computed + "include_vm_memory_overhead": "false", // Not used + }), + + resource.TestCheckResourceAttr(template+"2", "name", params["Name2"].(string)), + resource.TestCheckTypeSetElemNestedAttrs(template+"2", "compute_configuration.*", map[string]string{ + "cpu_allocated": "512", + "cpu_limit": "0", // Not used + "cpu_guaranteed": "100", + "cpu_speed": "1500", + "memory_allocated": "1536", + "memory_guaranteed": "60", + "memory_limit": "0", // Not used + "elasticity": "false", // Not used + "include_vm_memory_overhead": "true", // Computed + }), + resource.TestCheckResourceAttr(template+"3", "name", params["Name3"].(string)), + resource.TestCheckTypeSetElemNestedAttrs(template+"3", "compute_configuration.*", map[string]string{ + "cpu_allocated": "512", + "cpu_limit": "512", + "cpu_guaranteed": "0", // Not used + "cpu_speed": "0", // Not used + "memory_allocated": "1536", + "memory_guaranteed": "0", // Not used + "memory_limit": "0", // Not used + "elasticity": "false", + "include_vm_memory_overhead": "true", // Computed + }), + resource.TestCheckResourceAttr(template+"4", "name", params["Name4"].(string)), + resource.TestCheckTypeSetElemNestedAttrs(template+"4", "compute_configuration.*", map[string]string{ + "cpu_allocated": "512", + "cpu_limit": "512", + "cpu_guaranteed": "100", + "cpu_speed": "1500", + "memory_allocated": "1536", + "memory_guaranteed": "60", + "memory_limit": "2048", + "elasticity": "true", + "include_vm_memory_overhead": "true", + }), + ), + }, + { + Config: step3, + Check: resource.ComposeTestCheckFunc( + resourceFieldsEqual(template+"1", dsTemplate+"1", nil), + resourceFieldsEqual(template+"2", dsTemplate+"2", nil), + resourceFieldsEqual(template+"3", dsTemplate+"3", nil), + resourceFieldsEqual(template+"4", dsTemplate+"4", nil), + + // This one is read by a tenant, so we ignore 'readable_by_org_ids' completely + resourceFieldsEqualCustom(template+"4", dsTemplate+"5", []string{"readable_by_org_ids"}, stringInSlicePartially), + ), + }, + { + ResourceName: template + "1", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: importStateIdTopHierarchy(params["Name1"].(string)), + }, + }, + }) + postTestChecks(t) +} + +// Checks that a VDC Template is correctly destroyed +func testAccCheckVdcTemplateDestroyed(vdcTemplateName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*VCDClient) + var err error + var id string + for _, rs := range s.RootModule().Resources { + if rs.Type == "vcd_org_vdc_template" && rs.Primary.Attributes["name"] == vdcTemplateName { + id = rs.Primary.ID + } + } + + if id == "" { + return fmt.Errorf("vcd_vdc_template with name %s was not found in tfstate", vdcTemplateName) + } + + _, err = conn.GetVdcTemplateById(id) + if err != nil && !govcd.ContainsNotFound(err) { + return fmt.Errorf("VDC Template '%s' still exists, but got an error when retrieving: %s", id, err) + } + if err == nil { + return fmt.Errorf("VDC Template '%s' still exists", id) + } + + return nil + } +} + +const testAccVdcTemplateResource = ` +data "vcd_org" "org" { + provider = {{.ProviderVcdSystem}} + + name = "{{.OrgToPublish}}" +} + +data "vcd_org_vdc" "vdc" { + provider = {{.ProviderVcdSystem}} + + org = data.vcd_org.org.name + name = "{{.Vdc}}" +} + +data "vcd_nsxt_edge_cluster" "ec" { + provider = {{.ProviderVcdSystem}} + + org = data.vcd_org.org.name + vdc_id = data.vcd_org_vdc.vdc.id + name = "{{.EdgeCluster}}" +} + +data "vcd_provider_vdc" "pvdc" { + provider = {{.ProviderVcdSystem}} + + name = "{{.ProviderVdc}}" +} + +data "vcd_external_network_v2" "ext_net" { + provider = {{.ProviderVcdSystem}} + + name = "{{.ExternalNetwork}}" +} + +data "vcd_network_pool" "np" { + provider = {{.ProviderVcdSystem}} + + name = "{{.NetworkPool}}" +} + +resource "vcd_org_vdc_template" "template1" { + provider = {{.ProviderVcdSystem}} + + name = "{{.Name1}}" + tenant_name = "{{.Name1}}_tenant" + description = "{{.Name1}}_description" + tenant_description = "{{.Name1}}_tenant_description" + allocation_model = "AllocationVApp" + + compute_configuration { + cpu_limit = {{.CpuLimit}} + cpu_guaranteed = {{.CpuGuaranteed}} + cpu_speed = {{.CpuSpeed}} + memory_limit = {{.MemoryLimit}} + memory_guaranteed = {{.MemoryGuaranteed}} + } + + provider_vdc { + id = data.vcd_provider_vdc.pvdc.id + external_network_id = data.vcd_external_network_v2.ext_net.id + } + + storage_profile { + name = "{{.StorageProfile}}" + default = true + limit = {{.StorageProfileLimit}} + } + + network_pool_id = data.vcd_network_pool.np.id + + readable_by_org_ids = [ + data.vcd_org.org.id + ] +} + +resource "vcd_org_vdc_template" "template2" { + provider = {{.ProviderVcdSystem}} + + name = "{{.Name2}}" + tenant_name = "{{.Name2}}_tenant" + description = "{{.Name2}}_description" + tenant_description = "{{.Name2}}_tenant_description" + allocation_model = "AllocationPool" + + compute_configuration { + cpu_allocated = {{.CpuAllocated}} + cpu_guaranteed = {{.CpuGuaranteed}} + cpu_speed = {{.CpuSpeed}} + memory_allocated = {{.MemoryAllocated}} + memory_guaranteed = {{.MemoryGuaranteed}} + } + + edge_gateway { + name = "edgy2" + ip_allocation_count = 10 + routed_network_name = "net2" + routed_network_gateway_cidr = "1.2.3.4/24" + static_ip_pool { + start_address = "1.2.3.4" + end_address = "1.2.3.4" + } + } + + provider_vdc { + id = data.vcd_provider_vdc.pvdc.id + external_network_id = data.vcd_external_network_v2.ext_net.id + } + + storage_profile { + name = "{{.StorageProfile}}" + default = true + limit = {{.StorageProfileLimit}} + } + storage_profile { + name = "{{.StorageProfile2}}" + default = false + limit = 128 + } + + enable_fast_provisioning = true + enable_thin_provisioning = true +} + +resource "vcd_org_vdc_template" "template3" { + provider = {{.ProviderVcdSystem}} + + name = "{{.Name3}}" + tenant_name = "{{.Name3}}_tenant" + description = "{{.Name3}}_description" + tenant_description = "{{.Name3}}_tenant_description" + allocation_model = "ReservationPool" + + compute_configuration { + cpu_allocated = {{.CpuAllocated}} + cpu_limit = {{.CpuLimit}} + memory_allocated = {{.MemoryAllocated}} + } + + edge_gateway { + name = "edgy3" + ip_allocation_count = 15 + routed_network_name = "net3" + routed_network_gateway_cidr = "1.1.1.1/2" + static_ip_pool { + start_address = "1.1.1.1" + end_address = "1.1.1.1" + } + } + + provider_vdc { + id = data.vcd_provider_vdc.pvdc.id + external_network_id = data.vcd_external_network_v2.ext_net.id + gateway_edge_cluster_id = data.vcd_nsxt_edge_cluster.ec.id + } + + storage_profile { + name = "{{.StorageProfile}}" + default = true + limit = {{.StorageProfileLimit}} + } + + network_pool_id = data.vcd_network_pool.np.id + + nic_quota = 100 + vm_quota = 100 + provisioned_network_quota = 20 + + readable_by_org_ids = [ + data.vcd_org.org.id + ] +} + +resource "vcd_org_vdc_template" "template4" { + provider = {{.ProviderVcdSystem}} + + name = "{{.Name4}}" + tenant_name = "{{.Name4}}_tenant" + description = "{{.Name4}}_description" + tenant_description = "{{.Name4}}_tenant_description" + allocation_model = "Flex" + + compute_configuration { + cpu_allocated = {{.CpuAllocated}} + cpu_limit = {{.CpuLimit}} + cpu_guaranteed = {{.CpuGuaranteed}} + cpu_speed = {{.CpuSpeed}} + memory_allocated = {{.MemoryAllocated}} + memory_limit = {{.MemoryLimit}} + memory_guaranteed = {{.MemoryGuaranteed}} + + elasticity = true + include_vm_memory_overhead = true + } + + provider_vdc { + id = data.vcd_provider_vdc.pvdc.id + external_network_id = data.vcd_external_network_v2.ext_net.id + } + + storage_profile { + name = "{{.StorageProfile}}" + default = true + limit = {{.StorageProfileLimit}} + } + + network_pool_id = data.vcd_network_pool.np.id + + readable_by_org_ids = [ + data.vcd_org.org.id + ] +} +` + +const testAccVdcTemplateResourceAndDatasource = testAccVdcTemplateResource + ` +data "vcd_org_vdc_template" "ds_template1" { + provider = {{.ProviderVcdSystem}} + + name = vcd_org_vdc_template.template1.name +} + +data "vcd_org_vdc_template" "ds_template2" { + provider = {{.ProviderVcdSystem}} + + name = vcd_org_vdc_template.template2.name +} + +data "vcd_org_vdc_template" "ds_template3" { + provider = {{.ProviderVcdSystem}} + + name = vcd_org_vdc_template.template3.name +} + +data "vcd_org_vdc_template" "ds_template4" { + provider = {{.ProviderVcdSystem}} + + name = vcd_org_vdc_template.template4.name +} + +data "vcd_org_vdc_template" "ds_template5" { + provider = {{.ProviderVcdTenant}} + + # Careful, uses tenant_name as we use tenant configuration now + name = vcd_org_vdc_template.template4.tenant_name +} +` diff --git a/vcd/testcheck_funcs_test.go b/vcd/testcheck_funcs_test.go index 345d7d2da..76a23a63c 100644 --- a/vcd/testcheck_funcs_test.go +++ b/vcd/testcheck_funcs_test.go @@ -116,6 +116,9 @@ func testCheckOutputNonEmpty(name string) resource.TestCheckFunc { // firstObject except `[]excludeFields`. This is very useful to check if data sources have all // the same values as resources func resourceFieldsEqual(firstObject, secondObject string, excludeFields []string) resource.TestCheckFunc { + return resourceFieldsEqualCustom(firstObject, secondObject, excludeFields, stringInSlice) +} +func resourceFieldsEqualCustom(firstObject, secondObject string, excludeFields []string, exclusionChecker func(str string, list []string) bool) resource.TestCheckFunc { return func(s *terraform.State) error { resource1, ok := s.RootModule().Resources[firstObject] if !ok { @@ -129,7 +132,7 @@ func resourceFieldsEqual(firstObject, secondObject string, excludeFields []strin for fieldName := range resource1.Primary.Attributes { // Do not validate the fields marked for exclusion - if excludeFields != nil && stringInSlice(fieldName, excludeFields) { + if excludeFields != nil && exclusionChecker(fieldName, excludeFields) { continue } diff --git a/website/docs/d/org_vdc_template.html.markdown b/website/docs/d/org_vdc_template.html.markdown new file mode 100644 index 000000000..96eb803ae --- /dev/null +++ b/website/docs/d/org_vdc_template.html.markdown @@ -0,0 +1,37 @@ +--- +layout: "vcd" +page_title: "VMware Cloud Director: vcd_org_vdc_template" +sidebar_current: "docs-vcd-data-source-org-vdc-template" +description: |- + Provides a data source to read Organization VDC Templates from VMware Cloud Director. +--- + +# vcd\_org\_vdc\_template + +Provides a data source to read Organization VDC Templates from VMware Cloud Director. +Can be used by System Administrators or tenants, only if the template is published in that tenant. + +Supported in provider *v3.13+* + +-> VDC Templates that do not use NSX-T can be read, but may be missing some details. + +## Example Usage + +```hcl +data "vcd_org_vdc_template" "template" { + name = "myTemplate" +} +``` + +-> Note that when using the data source as a tenant, `name` is the VDC Template name as seen by tenants + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) Name of the existing Organization VDC Template to read + +## Attribute Reference + +All the arguments from [the `vcd_org_vdc_template` resource](/providers/vmware/vcd/latest/docs/resources/org_vdc_template) are available as read-only. +If you use this data source as a tenant, the attribute `readable_by_org_ids` must not be used (it will be always empty). diff --git a/website/docs/d/resource_list.html.markdown b/website/docs/d/resource_list.html.markdown index ba6ffbbdd..bb4aa339f 100644 --- a/website/docs/d/resource_list.html.markdown +++ b/website/docs/d/resource_list.html.markdown @@ -363,6 +363,7 @@ The following arguments are supported: * `vcd_org` * `vcd_external_network` * `vcd_org_vdc` + * `vcd_org_vdc_template` * `vcd_catalog` * `vcd_catalog_item` * `vcd_catalog_vapp_template` diff --git a/website/docs/r/org_vdc_template.html.markdown b/website/docs/r/org_vdc_template.html.markdown new file mode 100644 index 000000000..76a7d3875 --- /dev/null +++ b/website/docs/r/org_vdc_template.html.markdown @@ -0,0 +1,156 @@ +--- +layout: "vcd" +page_title: "VMware Cloud Director: vcd_org_vdc_template" +sidebar_current: "docs-vcd-resource-org-vdc-template" +description: |- + Provides a resource to create Organization VDC Templates in VMware Cloud Director. This can be used to create, delete, and update a Organization VDC Template. +--- + +# vcd\_org\_vdc\_template + +Provides a resource to create Organization VDC Templates in VMware Cloud Director. This can be used to create, delete, and update a Organization VDC Template. +Requires system administrator privileges. + +~> Only supports NSX-T network provider + +Supported in provider *v3.13+* + +## Example Usage + +```hcl +data "vcd_org" "org" { + name = "my_org" +} + +data "vcd_provider_vdc" "pvdc1" { + name = "nsxTPvdc1" +} + +data "vcd_provider_vdc" "pvdc2" { + name = "nsxTPvdc2" +} + +data "vcd_external_network_v2" "ext_net" { + name = "nsxt-extnet" +} + +data "vcd_network_pool" "np1" { + name = "NSX-T Overlay 1" +} + +resource "vcd_org_vdc_template" "tmpl" { + name = "myTemplate" + description = "Requires System privileges" + tenant_name = "myAwesomeTemplate" + tenant_description = "Any tenant can use this" + allocation_model = "AllocationVApp" + + compute_configuration { + cpu_limit = 0 + cpu_guaranteed = 20 + cpu_speed = 256 + memory_limit = 1024 + memory_guaranteed = 30 + } + + provider_vdc { + id = data.vcd_provider_vdc.pvdc1.id + external_network_id = data.vcd_external_network_v2.ext_net.id + } + + provider_vdc { + id = data.vcd_provider_vdc.pvdc2.id + external_network_id = data.vcd_external_network_v2.ext_net.id + } + + storage_profile { + name = "*" + default = true + limit = 1024 + } + + network_pool_id = data.vcd_network_pool.np1.id + + readable_by_org_ids = [ + data.vcd_org.org.id + ] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) Name to give to the Organization VDC Template, as seen by System administrators +* `description` - (Optional) Description of the Organization VDC Template, as seen by System administrators +* `tenant_name` - (Required) Name to give to the Organization VDC Template, as seen by the allowed tenants +* `tenant_description` - (Optional) Description of the Organization VDC Template, as seen by the allowed tenants +* `provider_vdc` - (Required) A block that defines a candidate location for the instantiated VDCs. There must be **at least one**, which has the following properties: + * `id` - (Required) ID of the Provider VDC, can be obtained with + [`vcd_provider_vdc` data source](/providers/vmware/vcd/latest/docs/data-sources/provider_vdc) + * `external_network_id` - (Required) ID of the Provider Gateway to use, can be obtained with + [`vcd_external_network_v2` data source](/providers/vmware/vcd/latest/docs/data-sources/external_network_v2) + * `gateway_edge_cluster_id` - (Optional) ID of the Edge Cluster that the VDCs instantiated from this template will use with the Edge Gateway. + Can be obtained with [`vcd_nsxt_edge_cluster` data source](/providers/vmware/vcd/latest/docs/data-sources/nsxt_edge_cluster). + If set, a `edge_gateway` block **must** be present in the VDC Template configuration (see below). + * `services_edge_cluster_id` - (Optional) ID of the Edge Cluster that the VDCs instantiated from this template will use for services. + Can be obtained with [`vcd_nsxt_edge_cluster` data source](/providers/vmware/vcd/latest/docs/data-sources/nsxt_edge_cluster) +* `allocation_model` - (Required) Allocation model that the VDCs instantiated from this template will use. + Must be one of: `AllocationVApp`, `AllocationPool`, `ReservationPool` or `Flex` +* `compute_configuration`: The compute configuration for the VDCs instantiated from this template: + * `cpu_allocated` - (Required for `AllocationPool`, `ReservationPool` or `Flex`) The maximum amount of CPU, in MHz, available to the VMs running within the VDC that is instantiated from this template. Minimum is 256MHz + * `cpu_limit` - (Required for `AllocationVApp`, `ReservationPool` or `Flex`) The limit amount of CPU, in MHz, of the VDC that is instantiated from this template. Minimum is 256MHz. 0 means unlimited + * `cpu_guaranteed` - (Required for `AllocationVApp`, `AllocationPool` or `Flex`) The percentage of the CPU guaranteed to be available to VMs running within the VDC instantiated from this template + * `cpu_speed` - (Required for `AllocationVApp`, `AllocationPool` or `Flex`) Specifies the clock frequency, in MHz, for any virtual CPU that is allocated to a VM. Minimum is 256MHz + * `memory_allocated` - (Required for `AllocationPool`, `ReservationPool` or `Flex`) The maximum amount of Memory, in MB, available to the VMs running within the VDC that is instantiated from this template + * `memory_limit` - (Required for `AllocationVApp`, `ReservationPool` or `Flex`) The limit amount of Memory, in MB, of the VDC that is instantiated from this template. Minimum is 1024MB. 0 means unlimited + * `memory_guaranteed` - (Required for `AllocationVApp`, `AllocationPool` or `Flex`) The percentage of the Memory guaranteed to be available to VMs running within the VDC instantiated from this template + * `elasticity` - (Required for `Flex`) True if compute capacity can grow or shrink based on demand + * `include_vm_memory_overhead` - (Required for `Flex`) True if the instantiated VDC includes memory overhead into its accounting for admission control +* `storage_profile` - (Required) A block that defines a storage profile that the VDCs instantiated from this template will use. Must be **at least one**, which has the following properties: + * `name` - (Required) Name of Provider VDC storage profile to use for the VDCs instantiated from this template + * `default` - (Required) True if this is default storage profile for the VDCs instantiated from this template. Only **one** block should have this set to `true` + * `limit` - (Required) Storage limit for the VDCs instantiated from this template, in MB. 0 means unlimited +* `enable_fast_provisioning` - (Optional) If `true`, the VDCs instantiated from this template will have Fast provisioning enabled. Defaults to `false` +* `enable_thin_provisioning` - (Optional) If `true`, the VDCs instantiated from this template will have Thin provisioning enabled. Defaults to `false` +* `edge_gateway` - (Optional) VDCs instantiated from this template will create a new Edge Gateway with the provided setup. Required if any `provider_vdc` block + has defined a `gateway_edge_cluster_id`. This **unique** block has the following properties: + * `name` - (Required) Name of the Edge Gateway + * `description` - (Optional) Description of the Edge Gateway + * `ip_allocation_count` - (Optional) Allocated IPs for the Edge Gateway. Defaults to 0 + * `network_name` - (Required) Name of the routed network to create with the Edge Gateway + * `network_description` - (Optional) Description of the routed network to create with the Edge Gateway + * `network_gateway_cidr` - (Required) CIDR of the Edge Gateway for the created routed network + * `static_ip_pool` - (Required) **One block** with a single IP range (this is a constraint due to a bug in VCD 10.5+) that has two properties: `start_address`, the start address of the IP range; + `end_address`, the end address of the IP range +* `network_pool_id` - (Optional) If set, specifies the Network pool for the instantiated VDCs. Otherwise, it is automatically chosen +* `nic_quota` - (Optional) Quota for the NICs of the instantiated VDCs. 0 means unlimited. Defaults to 0 +* `vm_quota` - (Optional) Quota for the VMs of the instantiated VDCs. 0 means unlimited. Defaults to 0 +* `provisioned_network_quota` - (Optional) Quota for the provisioned networks of the instantiated VDCs. 0 means unlimited. Defaults to 0 +* `readable_by_org_ids` - (Optional) A set of Organization IDs that will be able to view and read this VDC template, they can be obtained with + [`vcd_org` data source](/providers/vmware/vcd/latest/docs/data-sources/org) + +## Importing + +~> **Note:** The current implementation of Terraform import can only import resources into the state. It does not generate +configuration. [More information.][docs-import] + +An existing Organization VDC Template can be [imported][docs-import] into this resource via supplying its System name (`name`). +For example, using this structure, representing an existing Organization VDC Template that was **not** created using Terraform: + +```hcl +resource "vcd_org_vdc_template" "an_existing_vdc_template" { + # ... +} +``` + +You can import such Organization VDC Template into Terraform state using one of the following commands + +``` +terraform import vcd_org_vdc_template.an_existing_vdc_template "MyTemplate" +``` + +After that, you must expand the configuration file before you can either update or delete the Organization VDC Template. Running `terraform plan` +at this stage will show the difference between the minimal configuration file and the stored properties. + +[docs-import]:https://www.terraform.io/docs/import/ diff --git a/website/vcd.erb b/website/vcd.erb index 567a6d252..8a4ef94d4 100644 --- a/website/vcd.erb +++ b/website/vcd.erb @@ -178,6 +178,9 @@ > vcd_provider_vdc + > + vcd_org_vdc_template + > vcd_portgroup @@ -714,6 +717,9 @@ > vcd_provider_vdc + > + vcd_org_vdc_template + > vcd_cloned_vapp