diff --git a/anxcloud/common_resource_tagging_test.go b/anxcloud/common_resource_tagging_test.go index 55d57d75..cc0de8c8 100644 --- a/anxcloud/common_resource_tagging_test.go +++ b/anxcloud/common_resource_tagging_test.go @@ -105,7 +105,3 @@ func generateTagsString(tags ...string) string { ret.WriteString("]\n") return ret.String() } - -func withoutTags(tpl string) string { - return fmt.Sprintf(tpl, "") -} diff --git a/anxcloud/helper_test.go b/anxcloud/helper_test.go deleted file mode 100644 index 52b31dcb..00000000 --- a/anxcloud/helper_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package anxcloud - -import ( - "testing" - - "go.anx.io/go-anxcloud/pkg/client" -) - -func integrationTestClientFromEnv(t *testing.T) client.Client { - c, err := client.New(client.AuthFromEnv(false)) - if err != nil { - t.Errorf("failed to initialize integration test client from env: %s", err) - } - return c -} diff --git a/anxcloud/provider.go b/anxcloud/provider.go index 84488157..700f5707 100644 --- a/anxcloud/provider.go +++ b/anxcloud/provider.go @@ -30,7 +30,6 @@ func Provider(version string) *schema.Provider { }, }, ResourcesMap: map[string]*schema.Resource{ - "anxcloud_virtual_server": resourceVirtualServer(), "anxcloud_vlan": resourceVLAN(), "anxcloud_network_prefix": resourceNetworkPrefix(), "anxcloud_ip_address": resourceIPAddress(), diff --git a/anxcloud/resource_virtual_server.go b/anxcloud/resource_virtual_server.go deleted file mode 100644 index dcdeda3b..00000000 --- a/anxcloud/resource_virtual_server.go +++ /dev/null @@ -1,552 +0,0 @@ -package anxcloud - -import ( - "context" - "fmt" - "log" - "strconv" - "strings" - "time" - - "go.anx.io/go-anxcloud/pkg/vsphere/provisioning/progress" - "go.anx.io/go-anxcloud/pkg/vsphere/provisioning/templates" - - "github.com/hashicorp/go-cty/cty" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "go.anx.io/go-anxcloud/pkg/ipam/address" - "go.anx.io/go-anxcloud/pkg/vsphere" - "go.anx.io/go-anxcloud/pkg/vsphere/provisioning/nictype" - "go.anx.io/go-anxcloud/pkg/vsphere/provisioning/vm" -) - -const maxDNSEntries = 4 - -func resourceVirtualServer() *schema.Resource { - return &schema.Resource{ - Description: ` -The virtual_server resource allows you to configure and run virtual machines. - -### Known limitations -- removal of disks not supported -- removal of networks not supported -`, - CreateContext: tagsMiddlewareCreate(resourceVirtualServerCreate), - ReadContext: tagsMiddlewareRead(resourceVirtualServerRead), - UpdateContext: tagsMiddlewareUpdate(resourceVirtualServerUpdate), - DeleteContext: resourceVirtualServerDelete, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(60 * time.Minute), - Read: schema.DefaultTimeout(1 * time.Minute), - Update: schema.DefaultTimeout(60 * time.Minute), - Delete: schema.DefaultTimeout(15 * time.Minute), // ENGSUP-6288 - }, - Schema: withTagsAttribute(schemaVirtualServer()), - CustomizeDiff: customdiff.All( - customdiff.ForceNewIf("template_id", func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) bool { - // prevent ForceNew when vm-template is controlled by (named) "template" parameter - _, exist := d.GetOkExists("template") - return !exist - }), - customdiff.ForceNewIf("network", func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) bool { - old, newNetworks := d.GetChange("network") - oldNets := expandVirtualServerNetworks(old.([]interface{})) - newNets := expandVirtualServerNetworks(newNetworks.([]interface{})) - - if len(oldNets) > len(newNets) { - // some network has been deleted - return true - } - - // Get the IPs which are associated with the VM from info.network key - vmInfoState := d.Get("info").([]interface{}) - infoObject := expandVirtualServerInfo(vmInfoState) - vmIPMap := make(map[string]struct{}) - for _, vmNet := range infoObject.Network { - for _, ip := range append(vmNet.IPv4, vmNet.IPv6...) { - vmIPMap[ip] = struct{}{} - } - } - - for i, newNet := range newNets { - if i+1 > len(oldNets) { - // new networks were added - break - } - - if newNet.VLAN != oldNets[i].VLAN { - key := fmt.Sprintf("network.%d.vlan_id", i) - if err := d.ForceNew(key); err != nil { - log.Fatalf("[ERROR] unable to force new '%s': %v", key, err) - } - } - - if newNet.NICType != oldNets[i].NICType { - key := fmt.Sprintf("network.%d.nic_type", i) - if err := d.ForceNew(key); err != nil { - log.Fatalf("[ERROR] unable to force new '%s': %v", key, err) - } - } - - if len(newNet.IPs) < len(oldNets[i].IPs) { - // IPs are missing - key := fmt.Sprintf("network.%d.ips", i) - if err := d.ForceNew(key); err != nil { - log.Fatalf("[ERROR] unable to force new '%s': %v", key, err) - } - } else { - for j, ip := range newNet.IPs { - if j >= len(oldNets[i].IPs) || ip != oldNets[i].IPs[j] { - if _, ipExpected := vmIPMap[ip]; ipExpected { - continue - } - - key := fmt.Sprintf("network.%d.ips", i) - if err := d.ForceNew(key); err != nil { - log.Fatalf("[ERROR] unable to force new '%s': %v", key, err) - } - } - } - } - } - - return false - }), - customdiff.ForceNewIf("disk", func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) bool { - old, new := d.GetChange("disk") - oldDisks := expandVirtualServerDisks(old.([]interface{})) - newDisks := expandVirtualServerDisks(new.([]interface{})) - - if len(oldDisks) > len(newDisks) { - return true - } - - for i, disk := range newDisks { - if i+1 > len(oldDisks) { - // new disks were added - break - } - - if disk.SizeGBs < oldDisks[i].SizeGBs { - key := fmt.Sprintf("disk.%d.disk_gb", i) - if err := d.ForceNew(key); err != nil { - log.Fatalf("[ERROR] unable to force new '%s': %v", key, err) - } - } - } - return false - }), - ), - } -} - -func resourceVirtualServerCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - var ( - diags diag.Diagnostics - networks []vm.Network - disks []Disk - ) - - provContext := m.(providerContext) - vsphereAPI := vsphere.NewAPI(provContext.legacyClient) - addressAPI := address.NewAPI(provContext.legacyClient) - locationID := d.Get("location_id").(string) - - networks = expandVirtualServerNetworks(d.Get("network").([]interface{})) - for i, n := range networks { - if len(n.IPs) > 0 { - continue - } - - res, err := addressAPI.ReserveRandom(ctx, address.ReserveRandom{ - LocationID: locationID, - VlanID: n.VLAN, - Count: 1, - }) - if err != nil { - diags = append(diags, diag.FromErr(err)...) - } else if len(res.Data) > 0 { - networks[i].IPs = append(networks[i].IPs, res.Data[0].Address) - } else { - diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Free IP not found", - Detail: fmt.Sprintf("Free IP not found for VLAN: '%s'", n.VLAN), - AttributePath: cty.Path{cty.GetAttrStep{Name: "ips"}}, - }) - } - } - - dns := expandVirtualServerDNS(d.Get("dns").([]interface{})) - if len(dns) != maxDNSEntries { - diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "DNS entries exceed limit", - Detail: "Number of DNS entries cannot exceed limit 4", - AttributePath: cty.Path{cty.GetAttrStep{Name: "dns"}}, - }) - } - - disks = expandVirtualServerDisks(d.Get("disk").([]interface{})) - - // We require at least one disk to be specified either via Disk or via Disks array - if len(disks) < 1 { - diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "No disk specified", - Detail: "Minimum of one disk has to be specified", - AttributePath: cty.Path{cty.GetAttrStep{Name: "size_gb"}}, - }) - } - - templateID, _diags := templateIDFromResourceData(ctx, vsphereAPI, d) - diags = append(diags, _diags...) - - templateType := "templates" - if _, isNamedTemplate := d.GetOk("template"); !isNamedTemplate { - templateType = d.Get("template_type").(string) - } - - if len(diags) > 0 { - return diags - } - - def := vm.Definition{ - Location: locationID, - TemplateType: templateType, - TemplateID: templateID, - Hostname: d.Get("hostname").(string), - Memory: d.Get("memory").(int), - CPUs: d.Get("cpus").(int), - Disk: disks[0].SizeGBs, - DiskType: disks[0].Type, - AdditionalDisks: mapToAdditionalDisks(disks[1:]), - CPUPerformanceType: d.Get("cpu_performance_type").(string), - Sockets: d.Get("sockets").(int), - Network: networks, - DNS1: dns[0], - DNS2: dns[1], - DNS3: dns[2], - DNS4: dns[3], - Password: d.Get("password").(string), - SSH: d.Get("ssh_key").(string), - Script: d.Get("script").(string), - BootDelay: d.Get("boot_delay").(int), - EnterBIOSSetup: d.Get("enter_bios_setup").(bool), - } - - base64Encoding := true - provisioning, err := vsphereAPI.Provisioning().VM().Provision(ctx, def, base64Encoding) - if err != nil { - return diag.FromErr(err) - } - - vmIdentifier, err := vsphereAPI.Provisioning().Progress().AwaitCompletion(ctx, provisioning.Identifier) - if err != nil { - return diag.Errorf("failed to await completion: %s", err) - } - - d.SetId(vmIdentifier) - - return resourceVirtualServerRead(ctx, d, m) -} - -func resourceVirtualServerRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - var diags diag.Diagnostics - - c := m.(providerContext).legacyClient - vsphereAPI := vsphere.NewAPI(c) - nicAPI := nictype.NewAPI(c) - - info, err := vsphereAPI.Info().Get(ctx, d.Id()) - if err != nil { - if err := handleNotFoundError(err); err != nil { - return diag.FromErr(err) - } - d.SetId("") - return nil - } - - // `template_id` field isn't set for VMs with "from_scratch" templates - // that's why we keep the configured `template_id` if the `template_type` is "from_scratch" - if templateType, ok := d.Get("template_type").(string); !ok || templateType != "from_scratch" { - if err = d.Set("template_id", info.TemplateID); err != nil { - diags = append(diags, diag.FromErr(err)...) - } - } - - if err = d.Set("cpu_performance_type", info.CPUPerformanceType); err != nil { - diags = append(diags, diag.FromErr(err)...) - } - if err = d.Set("location_id", info.LocationID); err != nil { - diags = append(diags, diag.FromErr(err)...) - } - //if err = d.Set("template_type", info.TemplateType); err != nil { - // diags = append(diags, diag.FromErr(err)...) - //} - if err = d.Set("cpus", info.CPU); err != nil { - diags = append(diags, diag.FromErr(err)...) - } - - // engine ensures that the number of CPUs is divisible by the number of sockets - // -> floor division is fine - if err = d.Set("sockets", info.CPU/info.Cores); err != nil { - diags = append(diags, diag.FromErr(err)...) - } - - if err = d.Set("memory", info.RAM); err != nil { - diags = append(diags, diag.FromErr(err)...) - } - - disks := make([]Disk, len(info.DiskInfo)) - for i, diskInfo := range info.DiskInfo { - diskGB := roundDiskSize(diskInfo.DiskGB) - disks[i] = Disk{ - Disk: &vm.Disk{ - ID: diskInfo.DiskID, - Type: diskInfo.DiskType, - SizeGBs: diskGB, - }, - ExactDiskSize: diskInfo.DiskGB, - } - } - - flattenedDisks := flattenVirtualServerDisks(disks) - if err = d.Set("disk", flattenedDisks); err != nil { - diags = append(diags, diag.FromErr(err)...) - } - - nicTypes, err := nicAPI.List(ctx) - if err != nil { - return diag.FromErr(err) - } - - specNetworks := expandVirtualServerNetworks(d.Get("network").([]interface{})) - networks := make([]vm.Network, 0, len(info.Network)) - for i, net := range info.Network { - if len(nicTypes) < net.NIC { - diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Requested invalid nic type", - Detail: fmt.Sprintf("NIC type index out of range, available %d, wanted %d", len(nicTypes), net.NIC), - }) - continue - } - - if len(specNetworks) > i { - expectedIPMap := make(map[string]struct{}, len(specNetworks[i].IPs)) - for _, ip := range specNetworks[i].IPs { - expectedIPMap[ip] = struct{}{} - } - - network := vm.Network{ - NICType: nicTypes[net.NIC-1], - VLAN: net.VLAN, - } - - for _, ipv4 := range net.IPv4 { - if _, ok := expectedIPMap[ipv4]; ok { - network.IPs = append(network.IPs, ipv4) - delete(expectedIPMap, ipv4) - } - } - - for _, ipv6 := range net.IPv6 { - if _, ok := expectedIPMap[ipv6]; ok { - network.IPs = append(network.IPs, ipv6) - delete(expectedIPMap, ipv6) - } - } - - for ip := range expectedIPMap { - network.IPs = append(network.IPs, ip) - } - - networks = append(networks, network) - } - } - - flattenedNetworks := flattenVirtualServerNetwork(networks) - if err = d.Set("network", flattenedNetworks); err != nil { - diags = append(diags, diag.FromErr(err)...) - } - - flattenedInfo := flattenVirtualServerInfo(&info) - if err = d.Set("info", flattenedInfo); err != nil { - diags = append(diags, diag.FromErr(err)...) - } - - return diags -} - -func resourceVirtualServerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - provContext := m.(providerContext) - vsphereAPI := vsphere.NewAPI(provContext.legacyClient) - ch := vm.Change{ - Reboot: d.Get("force_restart_if_needed").(bool), - EnableDangerous: d.Get("critical_operation_confirmed").(bool), - } - - if d.HasChanges("sockets", "memory", "cpus") { - ch.CPUs = d.Get("cpus").(int) - ch.CPUSockets = d.Get("sockets").(int) - ch.MemoryMBs = d.Get("memory").(int) - } - - // cpu_performance_type might not be set because info endpoint didn't expose it previously - // therefore only change it when the argument changes - if d.HasChange("cpu_performance_type") { - ch.CPUPerformanceType = d.Get("cpu_performance_type").(string) - } - - if d.HasChange("network") { - old, new := d.GetChange("network") - oldNets := expandVirtualServerNetworks(old.([]interface{})) - newNets := expandVirtualServerNetworks(new.([]interface{})) - - if len(oldNets) < len(newNets) { - ch.AddNICs = newNets[len(oldNets):] - } else { - return diag.Errorf( - "unsupported update operation, cannot remove network or update its parameters", - ) - } - } - - if d.HasChange("disk") { - old, new := d.GetChange("disk") - oldDisks := expandVirtualServerDisks(old.([]interface{})) - newDisks := expandVirtualServerDisks(new.([]interface{})) - - if len(newDisks) < len(oldDisks) { - return diag.Errorf("removing disks is not supported yet, expected at least %d, got %d", len(oldDisks), len(newDisks)) - } - - changeDisks := make([]vm.Disk, 0, len(oldDisks)) - addDisks := make([]vm.Disk, 0, len(newDisks)) - for i := range newDisks { - if i >= len(oldDisks) { - addDisks = append(addDisks, *newDisks[i].Disk) - continue - } - - actualDisk := oldDisks[i] - expectedDisk := newDisks[i] - - // Compare the floating point disk size with the changed disk size from the configuration. - // This ensures that scaling operations are not reliant on rounding the disk size to integers. - if actualDisk.Type != expectedDisk.Type || actualDisk.ExactDiskSize < float64(expectedDisk.SizeGBs) { - changeDisks = append(changeDisks, *expectedDisk.Disk) - } - } - ch.ChangeDisks = changeDisks - ch.AddDisks = addDisks - } - - provisioning, err := vsphereAPI.Provisioning().VM().Update(ctx, d.Id(), ch) - if err != nil { - return diag.FromErr(err) - } - - if _, err = vsphereAPI.Provisioning().Progress().AwaitCompletion(ctx, provisioning.Identifier); err != nil { - return diag.FromErr(err) - } - - // wait for API to be updated - time.Sleep(time.Minute) - - return resourceVirtualServerRead(ctx, d, m) -} - -func resourceVirtualServerDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - c := m.(providerContext).legacyClient - vsphereAPI := vsphere.NewAPI(c) - progressAPI := progress.NewAPI(c) - - delayedDeprovision := false - response, err := vsphereAPI.Provisioning().VM().Deprovision(ctx, d.Id(), delayedDeprovision) - if err != nil { - if err := handleNotFoundError(err); err != nil { - return diag.FromErr(err) - } - d.SetId("") - return nil - } - - err = retry.RetryContext(ctx, d.Timeout(schema.TimeoutDelete), func() *retry.RetryError { - response, err := progressAPI.Get(ctx, response.Identifier) - if err != nil { - return retry.NonRetryableError(fmt.Errorf("failed to fetch deprovison progress: %w", err)) - } - - if len(response.Errors) > 0 { - joinedErrors := strings.Join(response.Errors, ",") - return retry.NonRetryableError(fmt.Errorf("errors during deprovision: [%s]", joinedErrors)) - } - - if response.Progress == 100 { - d.SetId("") - return nil - } - - return retry.RetryableError(fmt.Errorf("waiting for vm with id '%s' to be deleted", d.Id())) - }) - if err != nil { - return diag.FromErr(err) - } - - return nil -} - -func templateIDFromResourceData(ctx context.Context, a vsphere.API, d *schema.ResourceData) (string, diag.Diagnostics) { - if templateID, ok := d.GetOk("template_id"); ok { - return templateID.(string), nil - } - - // TODO: templates pagination is currently broken (see comments in ENGSUP-4364) - // template count is far from 1K but this needs proper pagination as soon as ADC API 2.0 is available - templates, err := a.Provisioning().Templates().List(ctx, d.Get("location_id").(string), "templates", 1, 1000) - if err != nil { - return "", diag.FromErr(err) - } - - return findNamedTemplate(d.Get("template").(string), d.Get("template_build").(string), templates) -} - -func findNamedTemplate(name, build string, tpls []templates.Template) (string, diag.Diagnostics) { - var ( - match = -1 - buildNo = -1 - latest = build == "" || build == "latest" - ) - - for i, template := range tpls { - if template.Name != name { - continue - } - - if latest { - currentTemplateBuildNo, _ := strconv.Atoi(template.Build[1:]) - - if latest && (match < 0 || currentTemplateBuildNo > buildNo) { - match = i - buildNo = currentTemplateBuildNo - } - } else if template.Build == build { - match = i - break - } - - } - - if match < 0 { - return "", diag.Errorf("named template %q with %q build wasn't found at the specified location", name, build) - } - - return tpls[match].ID, nil -} diff --git a/anxcloud/resource_virtual_server_test.go b/anxcloud/resource_virtual_server_test.go deleted file mode 100644 index cb83e18b..00000000 --- a/anxcloud/resource_virtual_server_test.go +++ /dev/null @@ -1,657 +0,0 @@ -package anxcloud - -import ( - "context" - "fmt" - "log" - "os" - "regexp" - "sort" - "strconv" - "strings" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/anexia-it/terraform-provider-anxcloud/anxcloud/testutils/environment" - "github.com/anexia-it/terraform-provider-anxcloud/anxcloud/testutils/recorder" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" - "go.anx.io/go-anxcloud/pkg/client" - "go.anx.io/go-anxcloud/pkg/vsphere" - "go.anx.io/go-anxcloud/pkg/vsphere/provisioning/templates" - "go.anx.io/go-anxcloud/pkg/vsphere/provisioning/vm" -) - -// This versioning scheme that currently seems to be in place for template build numbers. -var buildNumberRegex = regexp.MustCompile(`[bB]?(\d+)`) - -const ( - templateName = "Flatcar Linux Stable" - vmPoweredOn = "poweredOn" -) - -func getVMRecorder(t *testing.T) recorder.VMRecoder { - vmRecorder := recorder.VMRecoder{} - t.Cleanup(func() { - vmRecorder.Cleanup(context.TODO()) - }) - return vmRecorder -} - -func TestAccAnxCloudVirtualServer(t *testing.T) { - environment.SkipIfNoEnvironment(t) - resourceName := "acc_test_vm_test" - resourcePath := "anxcloud_virtual_server." + resourceName - - vmRecorder := getVMRecorder(t) - envInfo := environment.GetEnvInfo(t) - - templateID, diag := templateIDFromResourceData( - context.TODO(), - vsphere.NewAPI(integrationTestClientFromEnv(t)), - schema.TestResourceDataRaw(t, schemaVirtualServer(), map[string]interface{}{ - "template": templateName, - "location_id": envInfo.Location, - }), - ) - if diag.HasError() { - t.Fatalf("failed to retrieve template: %#v\n", diag) - } - - vmDef := vm.Definition{ - Location: envInfo.Location, - Hostname: fmt.Sprintf("terraform-test-%s-create-virtual-server", envInfo.TestRunName), - TemplateID: templateID, - TemplateType: "templates", - Memory: 2048, - CPUs: 2, - Sockets: 2, - CPUPerformanceType: "performance", - Disk: 50, - DiskType: "ENT6", - Network: []vm.Network{createNewNetworkInterface(envInfo)}, - DNS1: "8.8.8.8", - Password: "flatcar#1234$%%", - } - vmRecorder.RecordVMByName(fmt.Sprintf("%%-%s", vmDef.Hostname)) - - // upscale resources - vmDefUpscale := vmDef - vmDefUpscale.CPUs = 4 - vmDefUpscale.Memory = 4096 - - // down scale resources which does not require recreation of the VM - vmDefDownscale := vmDefUpscale - vmDefUpscale.CPUs = 2 - vmDefDownscale.Memory = 3072 - - testSteps := []resource.TestStep{ - // create VM - { - Config: withoutTags(testAccConfigAnxCloudVirtualServer(resourceName, templateName, &vmDef)), - Check: resource.ComposeTestCheckFunc( - testAccCheckAnxCloudVirtualServerExists(resourcePath, &vmDef), - resource.TestCheckResourceAttr(resourcePath, "location_id", vmDef.Location), - resource.TestCheckResourceAttr(resourcePath, "template_id", vmDef.TemplateID), - resource.TestCheckResourceAttr(resourcePath, "cpus", strconv.Itoa(vmDef.CPUs)), - resource.TestCheckResourceAttr(resourcePath, "memory", strconv.Itoa(vmDef.Memory)), - ), - }, - } - - testSteps = append( - testSteps, - // tagging operations - testAccAnxCloudCommonResourceTagTestSteps( - testAccConfigAnxCloudVirtualServer(resourceName, templateName, &vmDef), - resourcePath, - )..., - ) - - testSteps = append(testSteps, []resource.TestStep{ - // scale cpu & memory up - { - Config: withoutTags(testAccConfigAnxCloudVirtualServer(resourceName, templateName, &vmDefUpscale)), - Check: resource.ComposeTestCheckFunc( - testAccCheckAnxCloudVirtualServerExists(resourcePath, &vmDefUpscale), - resource.TestCheckResourceAttr(resourcePath, "location_id", vmDefUpscale.Location), - resource.TestCheckResourceAttr(resourcePath, "template_id", vmDefUpscale.TemplateID), - resource.TestCheckResourceAttr(resourcePath, "cpus", strconv.Itoa(vmDefUpscale.CPUs)), - resource.TestCheckResourceAttr(resourcePath, "memory", strconv.Itoa(vmDefUpscale.Memory)), - ), - }, - // scale cpu & memory down - { - Config: withoutTags(testAccConfigAnxCloudVirtualServer(resourceName, templateName, &vmDefDownscale)), - Check: resource.ComposeTestCheckFunc( - testAccCheckAnxCloudVirtualServerExists(resourcePath, &vmDefDownscale), - resource.TestCheckResourceAttr(resourcePath, "location_id", vmDefDownscale.Location), - resource.TestCheckResourceAttr(resourcePath, "template_id", vmDefDownscale.TemplateID), - resource.TestCheckResourceAttr(resourcePath, "cpus", strconv.Itoa(vmDefDownscale.CPUs)), - resource.TestCheckResourceAttr(resourcePath, "memory", strconv.Itoa(vmDefDownscale.Memory)), - ), - }, - // check importability - { - ResourceName: resourcePath, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"critical_operation_confirmed", "enter_bios_setup", "force_restart_if_needed", "hostname", "password", "template", "template_type", "network"}, - }, - }...) - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviderFactories, - CheckDestroy: testAccCheckAnxCloudVirtualServerDestroy, - Steps: testSteps, - }) -} - -func TestAccAnxCloudVirtualServerFromScratch(t *testing.T) { - environment.SkipIfNoEnvironment(t) - envInfo := environment.GetEnvInfo(t) - vmRecorder := getVMRecorder(t) - - vmDef := vm.Definition{ - Location: envInfo.Location, - Hostname: fmt.Sprintf("terraform-test-%s-create-virtual-server-from-scratch", envInfo.TestRunName), - TemplateID: "114", // Debian GNU\/Linux 10, 64 Bit - TemplateType: "from_scratch", - Memory: 2048, - CPUs: 2, - Sockets: 2, - CPUPerformanceType: "performance", - Disk: 50, - DiskType: "ENT6", - Network: []vm.Network{createNewNetworkInterface(envInfo)}, - DNS1: "8.8.8.8", - Password: "flatcar#1234$%%", - } - - vmRecorder.RecordVMByName(fmt.Sprintf("%%-%s", vmDef.Hostname)) - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviderFactories, - CheckDestroy: testAccCheckAnxCloudVirtualServerDestroy, - Steps: []resource.TestStep{ - { - Config: withoutTags(testAccConfigAnxCloudVirtualServer("foo", "", &vmDef)), - Check: testAccCheckAnxCloudVirtualServerExists("anxcloud_virtual_server.foo", &vmDef), - }, - }, - }) -} - -func TestAccAnxCloudVirtualServerMultiDiskScaling(t *testing.T) { - environment.SkipIfNoEnvironment(t) - resourceName := "acc_test_vm_test_multi_disk" - resourcePath := "anxcloud_virtual_server." + resourceName - - vmRecorder := getVMRecorder(t) - envInfo := environment.GetEnvInfo(t) - templateID := vsphereAccTestTemplateByLocationAndPrefix(envInfo.Location, templateName) - vmDef := vm.Definition{ - Location: envInfo.Location, - TemplateType: "templates", - TemplateID: templateID, - Hostname: fmt.Sprintf("terraform-test-%s-multi-disk-scaling", envInfo.TestRunName), - Memory: 2048, - CPUs: 2, - Sockets: 2, - CPUPerformanceType: "performance", - Network: []vm.Network{createNewNetworkInterface(envInfo)}, - DNS1: "8.8.8.8", - Password: "flatcar#1234$%%", - } - vmRecorder.RecordVMByName(fmt.Sprintf("%%-%s", vmDef.Hostname)) - - disks := []vm.Disk{ - { - Type: "ENT1", - SizeGBs: 40, - }, - } - - t.Run("AddDisk", func(t *testing.T) { - addDiskDef := vmDef - addDiskDef.Hostname = fmt.Sprintf("terraform-test-%s-add-disk", envInfo.TestRunName) - addDiskDef.Network = []vm.Network{createNewNetworkInterface(envInfo)} - - disksAdd := append(disks, vm.Disk{ - - Type: "ENT6", - SizeGBs: 50, - }) - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviderFactories, - CheckDestroy: testAccCheckAnxCloudVirtualServerDestroy, - Steps: []resource.TestStep{ - { - Config: testAccConfigAnxCloudVirtualServerMultiDiskSupport(resourceName, &addDiskDef, disks), - Check: resource.ComposeTestCheckFunc( - testAccCheckAnxCloudVirtualServerDisks(resourcePath, disks), - ), - }, - { - Config: testAccConfigAnxCloudVirtualServerMultiDiskSupport(resourceName, &addDiskDef, disksAdd), - Check: resource.ComposeTestCheckFunc( - testAccCheckAnxCloudVirtualServerDisks(resourcePath, disksAdd), - ), - }, - }, - }) - }) - - t.Run("ChangeAddDisk", func(t *testing.T) { - changeDiskDef := vmDef - changeDiskDef.Hostname = fmt.Sprintf("terraform-test-%s-change-add-disk", envInfo.TestRunName) - changeDiskDef.Network = []vm.Network{createNewNetworkInterface(envInfo)} - vmRecorder.RecordVMByName(fmt.Sprintf("%%-%s", changeDiskDef.Hostname)) - disksChange := append(disks, vm.Disk{ - SizeGBs: 50, - Type: "ENT6", - }) - - disksChange[0].SizeGBs = 70 - disksChange[0].Type = "ENT1" - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviderFactories, - CheckDestroy: testAccCheckAnxCloudVirtualServerDestroy, - Steps: []resource.TestStep{ - { - Config: testAccConfigAnxCloudVirtualServerMultiDiskSupport(resourceName, &changeDiskDef, disks), - Check: resource.ComposeTestCheckFunc( - testAccCheckAnxCloudVirtualServerDisks(resourcePath, disks), - ), - }, - { - Config: testAccConfigAnxCloudVirtualServerMultiDiskSupport(resourceName, &changeDiskDef, disksChange), - Check: resource.ComposeTestCheckFunc( - testAccCheckAnxCloudVirtualServerDisks(resourcePath, disksChange), - ), - }, - }, - }) - }) - - t.Run("MultiDiskTemplateChange", func(t *testing.T) { - changeDiskDef := vmDef - changeDiskDef.Hostname = fmt.Sprintf("terraform-test-%s-multi-disk-template-change", envInfo.TestRunName) - changeDiskDef.Network = []vm.Network{createNewNetworkInterface(envInfo)} - vmRecorder.RecordVMByName(fmt.Sprintf("%%-%s", changeDiskDef.Hostname)) - changeDiskDef.TemplateID = vsphereAccTestTemplateByLocationAndPrefix(envInfo.Location, "Debian 11") - templateDisks := []vm.Disk{ - { - Type: "ENT6", - SizeGBs: 50, - }, - { - Type: "ENT6", - SizeGBs: 50, - }, - } - - templateDisksChanged := append(templateDisks, vm.Disk{ - SizeGBs: 70, - Type: "ENT1", - }) - templateDisksChanged[1].SizeGBs = 60 - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviderFactories, - CheckDestroy: testAccCheckAnxCloudVirtualServerDestroy, - Steps: []resource.TestStep{ - { - Config: testAccConfigAnxCloudVirtualServerMultiDiskSupport(resourceName, &changeDiskDef, templateDisks), - Check: resource.ComposeTestCheckFunc( - testAccCheckAnxCloudVirtualServerDisks(resourcePath, templateDisks), - ), - }, - { - Config: testAccConfigAnxCloudVirtualServerMultiDiskSupport(resourceName, &changeDiskDef, templateDisksChanged), - Check: resource.ComposeTestCheckFunc( - testAccCheckAnxCloudVirtualServerDisks(resourcePath, templateDisksChanged), - ), - }, - }, - }) - }) -} - -func testAccCheckAnxCloudVirtualServerDestroy(s *terraform.State) error { - c := testAccProvider.Meta().(providerContext).legacyClient - v := vsphere.NewAPI(c) - ctx := context.Background() - - for _, rs := range s.RootModule().Resources { - if rs.Type != "anxcloud_virtual_server" { - continue - } - - if rs.Primary.ID == "" { - return nil - } - - info, err := v.Info().Get(ctx, rs.Primary.ID) - if err != nil { - if err := handleNotFoundError(err); err != nil { - return err - } - return nil - } - if info.Identifier != "" { - return fmt.Errorf("virtual machine '%s' exists", info.Identifier) - } - } - - return nil -} - -//nolint:unparam -func testAccConfigAnxCloudVirtualServer(resourceName string, templateName string, def *vm.Definition) string { - templateConfig := fmt.Sprintf(`template = "%s"`, templateName) - if def.TemplateID != "" && def.TemplateType != "" { - templateConfig = fmt.Sprintf(` - template_id = "%s" - template_type = "%s" - `, def.TemplateID, def.TemplateType) - } - - return fmt.Sprintf(` - resource "anxcloud_virtual_server" "%s" { - location_id = "%s" - - // template config - %s - - hostname = "%s" - cpus = %d - sockets = %d - cpu_performance_type = "%s" - memory = %d - password = "%s" - - // generated network string - %s - - // generated disk string - %s - - // generated tags - %%s - - force_restart_if_needed = true - critical_operation_confirmed = true - } - `, resourceName, def.Location, templateConfig, def.Hostname, def.CPUs, def.Sockets, def.CPUPerformanceType, def.Memory, - def.Password, generateNetworkSubResourceString(def.Network), generateDisksSubResourceString([]vm.Disk{ - { - SizeGBs: def.Disk, - Type: def.DiskType, - }, - })) -} - -func testAccConfigAnxCloudVirtualServerMultiDiskSupport(resourceName string, def *vm.Definition, disks []vm.Disk) string { - return fmt.Sprintf(` - resource "anxcloud_virtual_server" "%s" { - location_id = "%s" - template_id = "%s" - template_type = "%s" - hostname = "%s" - cpus = %d - memory = %d - password = "%s" - - // generated network string - %s - - // generated disks string - %s - - force_restart_if_needed = true - critical_operation_confirmed = true - } - `, resourceName, def.Location, def.TemplateID, def.TemplateType, def.Hostname, def.CPUs, def.Memory, - def.Password, generateNetworkSubResourceString(def.Network), generateDisksSubResourceString(disks)) -} - -func testAccCheckAnxCloudVirtualServerExists(n string, def *vm.Definition) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[n] - c := testAccProvider.Meta().(providerContext).legacyClient - v := vsphere.NewAPI(c) - ctx := context.Background() - - if !ok { - return fmt.Errorf("virtual server not found: %s", n) - } - - if rs.Primary.ID == "" { - return fmt.Errorf("virtual server id not set") - } - - info, err := v.Info().Get(ctx, rs.Primary.ID) - if err != nil { - return err - } - - if info.Status != vmPoweredOn { - return fmt.Errorf("virtual machine found but it is not in the expected state '%s': '%s'", vmPoweredOn, info.Status) - } - - if info.CPU != def.CPUs { - return fmt.Errorf("virtual machine cpu does not match, got %d - expected %d", info.CPU, def.CPUs) - } - if info.CPUPerformanceType != def.CPUPerformanceType { - return fmt.Errorf("virtual machine cpu_performance_type does not match, got %s - expected %s", info.CPUPerformanceType, def.CPUPerformanceType) - } - if info.RAM != def.Memory { - return fmt.Errorf("virtual machine memory does not match, got %d - expected %d", info.RAM, def.Memory) - } - - if len(info.DiskInfo) != 1 { - return fmt.Errorf("unspported number of attached disks, got %d - expected 1", len(info.DiskInfo)) - } - infoDiskGB := roundDiskSize(info.DiskInfo[0].DiskGB) - if infoDiskGB != def.Disk { - return fmt.Errorf("virtual machine disk size does not match, got %d - expected %d", infoDiskGB, def.Disk) - } - - if len(info.Network) != len(def.Network) { - return fmt.Errorf("virtual machine networks number do not match, got %d - expected %d", len(info.Network), len(info.Network)) - } - for i, n := range def.Network { - if n.VLAN != info.Network[i].VLAN { - return fmt.Errorf("virtual machine network[%d].vlan do not match, got %s - expected %s", i, info.Network[i].VLAN, n.VLAN) - } - } - - return nil - } -} - -func testAccCheckAnxCloudVirtualServerDisks(n string, expectedDisks []vm.Disk) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[n] - c := testAccProvider.Meta().(providerContext).legacyClient - v := vsphere.NewAPI(c) - ctx := context.Background() - - if !ok { - return fmt.Errorf("virtual server not found: %s", n) - } - - if rs.Primary.ID == "" { - return fmt.Errorf("virtual server id not set") - } - - info, err := v.Info().Get(ctx, rs.Primary.ID) - if err != nil { - return err - } - - if len(info.DiskInfo) != len(expectedDisks) { - return fmt.Errorf("virtual machine disk count do not match, got %d - expected %d", len(info.DiskInfo), len(expectedDisks)) - } - - for i, disk := range info.DiskInfo { - if disk.DiskType != expectedDisks[i].Type { - return fmt.Errorf("virtual machine disk with ID %d has incorrect type, got %s - expected %s", disk.DiskID, disk.DiskType, expectedDisks[i].Type) - } else if roundDiskSize(disk.DiskGB) != expectedDisks[i].SizeGBs { - return fmt.Errorf("virtual machine disk with ID %d has incorrect size, got %f - expected %d", disk.DiskID, disk.DiskGB, expectedDisks[i].SizeGBs) - } - } - - return nil - } -} - -func generateNetworkSubResourceString(networks []vm.Network) string { - var output string - template := "\nnetwork {\n\tvlan_id = \"%s\"\n\tnic_type = \"%s\"\n\tips = [\"%s\"]\n}\n" - - for _, n := range networks { - output += fmt.Sprintf(template, n.VLAN, n.NICType, n.IPs[0]) - } - - return output -} - -func generateDisksSubResourceString(disks []vm.Disk) string { - var output string - template := "\ndisk {\n\tdisk_gb = %d\n\tdisk_type = \"%s\"\n}\n" - - for _, d := range disks { - output += fmt.Sprintf(template, d.SizeGBs, d.Type) - } - - return output -} - -func vsphereAccTestTemplateByLocationAndPrefix(locationID string, templateNamePrefix string) string { - if _, ok := os.LookupEnv(client.TokenEnvName); !ok { - // we are running in unit test environment so do nothing - return "" - } - cli, err := client.New(client.AuthFromEnv(false)) - if err != nil { - log.Fatalf("Error creating client for retrieving template ID: %v\n", err) - } - - tplAPI := templates.NewAPI(cli) - tpls, err := tplAPI.List(context.TODO(), locationID, templates.TemplateTypeTemplates, 1, 500) - - if err != nil { - log.Fatalf("Error retrieving templates: %v\n", err) - } - - selected := make([]templates.Template, 0, 1) - for _, tpl := range tpls { - if strings.HasPrefix(tpl.Name, templateNamePrefix) { - selected = append(selected, tpl) - } - } - - if len(selected) < 1 { - log.Fatalf("Template with prefix '%s' not found at location with ID '%s'", templateNamePrefix, locationID) - } - - sort.Slice(selected, func(i, j int) bool { - return extractBuildNumber(selected[i].Build) > extractBuildNumber(selected[j].Build) - }) - - log.Printf("VSphere: selected template %v (build %v, ID %v)\n", selected[0].Name, selected[0].Build, selected[0].ID) - - return selected[0].ID -} - -func extractBuildNumber(version string) int { - match := buildNumberRegex.FindStringSubmatch(version) - if len(match) != 2 { - panic("the version doesn't match the given regex") - } - number, err := strconv.ParseInt(match[1], 10, 0) - if err != nil { - panic(fmt.Sprintf("could not extract version for %s", version)) - } - return int(number) -} - -func TestVersionParsing(t *testing.T) { - require.Equal(t, 5555, extractBuildNumber("b5555")) - require.Equal(t, 6666, extractBuildNumber("6666")) -} - -func createNewNetworkInterface(info environment.Info) vm.Network { - return vm.Network{ - VLAN: info.VlanID, - NICType: "vmxnet3", - IPs: []string{info.Prefix.GetNextIP()}, - } -} - -func mockedTemplateList() []templates.Template { - return []templates.Template{ - {ID: "e9325be9-25b9-468e-851e-56b5c0367e5a", Name: "Ubuntu 21.04", Build: "b72"}, - {ID: "b21b8b77-30e3-478a-9b6d-1f61d29e9f9a", Name: "Flatcar Linux Stable", Build: "b73"}, - {ID: "ec547552-d453-42e6-987d-51abe703c439", Name: "Debian 11", Build: "b18"}, - {ID: "26a47eee-dc9a-4eea-b67a-8fb1baa2fcc0", Name: "Flatcar Linux Stable", Build: "b74"}, - {ID: "cb16dc94-ec55-4e9a-a1a3-b76a91bbe274", Name: "Windows 2022", Build: "b06"}, - {ID: "fc3a63c6-6f4e-4193-b368-ebe9e08b4302", Name: "Debian 10", Build: "b80"}, - {ID: "844ac596-5f62-4ed2-936e-b99ffe0d4f88", Name: "Flatcar Linux Stable", Build: "b72"}, - {ID: "c3d4f0a6-978a-49fb-a952-7361bf531e4f", Name: "Debian 9", Build: "b92"}, - {ID: "086c5f99-1be6-46ec-8374-cdc23cedd6a4", Name: "Windows 2022", Build: "b12"}, - {ID: "9d863fd9-d0d3-4959-b226-e73192f3e43d", Name: "Debian 11", Build: "possibly-valid-build-id"}, - } -} - -func TestFindNamedTemplate(t *testing.T) { - type testCase struct { - expectedID string - expectExisting bool - namedTemplate string - namedTemplateBuild string - } - - testCases := []testCase{ - // valid test cases - {"844ac596-5f62-4ed2-936e-b99ffe0d4f88", true, "Flatcar Linux Stable", "b72"}, - {"26a47eee-dc9a-4eea-b67a-8fb1baa2fcc0", true, "Flatcar Linux Stable", "latest"}, - {"26a47eee-dc9a-4eea-b67a-8fb1baa2fcc0", true, "Flatcar Linux Stable", ""}, - {"26a47eee-dc9a-4eea-b67a-8fb1baa2fcc0", true, "Flatcar Linux Stable", "b74"}, - {"b21b8b77-30e3-478a-9b6d-1f61d29e9f9a", true, "Flatcar Linux Stable", "b73"}, - {"086c5f99-1be6-46ec-8374-cdc23cedd6a4", true, "Windows 2022", "latest"}, - {"086c5f99-1be6-46ec-8374-cdc23cedd6a4", true, "Windows 2022", "b12"}, - {"cb16dc94-ec55-4e9a-a1a3-b76a91bbe274", true, "Windows 2022", "b06"}, - {"9d863fd9-d0d3-4959-b226-e73192f3e43d", true, "Debian 11", "possibly-valid-build-id"}, - - // non-existing template name - {"", false, "FooOS 22.05", "b01"}, - {"", false, "FooOS 22.05", "b06"}, - {"", false, "Bar OS 95", "latest"}, - - // non-existing build id - {"", false, "Windows 2022", "foo"}, - {"", false, "Windows 2022", "b00"}, - } - - for _, testCase := range testCases { - if id, diag := findNamedTemplate(testCase.namedTemplate, testCase.namedTemplateBuild, mockedTemplateList()); testCase.expectExisting == (diag != nil) { - t.Errorf("unexpected error: %v", diag) - } else if id != testCase.expectedID { - t.Errorf("identifier %q expected, got %q", testCase.expectedID, id) - } - } - -} diff --git a/anxcloud/schema_virtual_server.go b/anxcloud/schema_virtual_server.go deleted file mode 100644 index 22c1892c..00000000 --- a/anxcloud/schema_virtual_server.go +++ /dev/null @@ -1,363 +0,0 @@ -package anxcloud - -import ( - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func schemaVirtualServer() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "hostname": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "Virtual server hostname.", - }, - "location_id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "Location identifier.", - }, - "template": { - Type: schema.TypeString, - ForceNew: true, - Optional: true, - ExactlyOneOf: []string{"template_id", "template"}, - Description: "Named template. Can be used instead of the template_id to select a template. " + - "Example: (`Debian 11`, `Windows 2022`).", - }, - "template_build": { - Type: schema.TypeString, - ForceNew: true, - Optional: true, - Description: "Template build identifier optionally used with `template`. Will default to latest build. Example: `b42`", - }, - "template_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ExactlyOneOf: []string{"template_id", "template"}, - Description: "Template identifier.", - }, - "template_type": { - Type: schema.TypeString, - ForceNew: true, - Description: "OS template type.", - Optional: true, - RequiredWith: []string{"template_id"}, - }, - "cpus": { - Type: schema.TypeInt, - Required: true, - Description: "Amount of CPUs.", - }, - "cpu_performance_type": { - Type: schema.TypeString, - Optional: true, - Default: "standard", - Description: "CPU type. Example: (`best-effort`, `standard`, `enterprise`, `performance`), defaults to `standard`.", - }, - "sockets": { - Type: schema.TypeInt, - Optional: true, - Computed: true, - Description: "Amount of CPU sockets Number of cores have to be a multiple of sockets, as they will be spread evenly across all sockets. " + - "Defaults to number of cores, i.e. one socket per CPU core.", - }, - "memory": { - Type: schema.TypeInt, - Required: true, - Description: "Memory in MB.", - }, - "disk": { - Type: schema.TypeList, - Required: true, - Description: "Virtual Server Disks", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "disk_id": { - Type: schema.TypeInt, - Computed: true, - Description: "Device identifier of the disk.", - }, - "disk_gb": { - Type: schema.TypeInt, - Required: true, - Description: "Disk capacity in GB.", - }, - "disk_type": { - Type: schema.TypeString, - Optional: true, - Description: "Disk category (limits disk performance, e.g. IOPS). Default value depends on location.", - }, - "disk_exact": { - Type: schema.TypeFloat, - Computed: true, - Description: "Exact floating point disk size. Not configurable; just for comparison.", - }, - }, - }, - }, - "network": { - Type: schema.TypeList, - Optional: true, - Description: "Network interface", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "vlan_id": { - Type: schema.TypeString, - Required: true, - Description: "VLAN identifier.", - }, - "nic_type": { - Type: schema.TypeString, - Required: true, - Description: "Network interface card type.", - }, - "ips": { - Type: schema.TypeSet, - Optional: true, - ForceNew: true, - Description: "Requested set of IPs and IPs identifiers. IPs are ignored when using template_type 'from_scratch'. " + - "Defaults to free IPs from IP pool attached to VLAN.", - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - }, - }, - }, - "dns": { - Type: schema.TypeList, - Optional: true, - MaxItems: 4, - ForceNew: true, - Description: "DNS configuration. Maximum items 4. Defaults to template settings.", - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "password": { - Type: schema.TypeString, - Optional: true, - Sensitive: true, - ForceNew: true, - Description: "Plaintext password. Example: ('!anx123mySuperStrongPassword123anx!', 'go3ju0la1ro3', …). For systems that support it, we strongly recommend using a SSH key instead.", - }, - "ssh_key": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Description: "Public key (instead of password, only for Linux systems). Recommended over providing a plaintext password.", - }, - "script": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Description: "Script to be executed after provisioning. " + - "Consider the corresponding shebang at the beginning of your script. " + - "If you want to use PowerShell, the first line should be: #ps1_sysnative.", - }, - "boot_delay": { - Type: schema.TypeInt, - Optional: true, - Description: "Boot delay in seconds. Example: (0, 1, …).", - }, - "enter_bios_setup": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Start the VM into BIOS setup on next boot.", - }, - "force_restart_if_needed": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Certain operations may only be performed in powered off state. " + - "Such as: shrinking memory, shrinking/adding CPU, removing disk and scaling a disk beyond 2 GB. " + - "Passing this value as true will always execute a power off and reboot request after completing all other operations. " + - "Without this flag set to true scaling operations requiring a reboot will fail.", - }, - "critical_operation_confirmed": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Confirms a critical operation (if needed). " + - "Potentially dangerous operations (e.g. resulting in data loss) require an additional confirmation. " + - "The parameter is used for VM UPDATE requests.", - }, - "info": { - Type: schema.TypeList, - Computed: true, - Description: "Virtual server info", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "identifier": { - Type: schema.TypeString, - Computed: true, - Description: identifierDescription, - }, - "status": { - Type: schema.TypeString, - Computed: true, - Description: "Virtual server status.", - }, - "name": { - Type: schema.TypeString, - Computed: true, - Description: "Virtual server name.", - }, - "custom_name": { - Type: schema.TypeString, - Computed: true, - Description: "Virtual server custom name.", - }, - "location_code": { - Type: schema.TypeString, - Computed: true, - Description: "Location code.", - }, - "location_country": { - Type: schema.TypeString, - Computed: true, - Description: "Location country.", - }, - "location_name": { - Type: schema.TypeString, - Computed: true, - Description: "Location name.", - }, - "cpu": { - Type: schema.TypeInt, - Computed: true, - Description: "Number of CPUs.", - }, - "cores": { - Type: schema.TypeInt, - Computed: true, - Description: "Number of CPU cores.", - }, - "ram": { - Type: schema.TypeInt, - Computed: true, - Description: "Memory in MB.", - }, - "disks_number": { - Type: schema.TypeInt, - Computed: true, - Description: "Number of the attached disks.", - }, - "disks_info": { - Type: schema.TypeList, - Computed: true, - Description: "Disks info.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "disk_id": { - Type: schema.TypeInt, - Computed: true, - Description: "Disk identifier.", - }, - "disk_gb": { - Type: schema.TypeInt, - Computed: true, - Description: "Size of the disk in GB.", - }, - "disk_type": { - Type: schema.TypeString, - Computed: true, - Description: "Disk type.", - }, - "iops": { - Type: schema.TypeInt, - Computed: true, - Description: "Disk input/output operations per second.", - }, - "latency": { - Type: schema.TypeInt, - Computed: true, - Description: "Disk latency.", - }, - "storage_type": { - Type: schema.TypeString, - Computed: true, - Description: "Disk storage type.", - }, - "bus_type": { - Type: schema.TypeString, - Computed: true, - Description: "Bus type.", - }, - "bus_type_label": { - Type: schema.TypeString, - Computed: true, - Description: "Bus type label.", - }, - }, - }, - }, - "network": { - Type: schema.TypeList, - Computed: true, - Description: "Network interfaces.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "id": { - Type: schema.TypeInt, - Computed: true, - Description: "Network interface card identifier.", - }, - "ip_v4": { - Type: schema.TypeList, - Computed: true, - Description: "List of IPv4 addresses attached to the interface.", - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "ip_v6": { - Type: schema.TypeList, - Computed: true, - Description: "List of IPv6 addresses attached to the interface.", - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "nic": { - Type: schema.TypeInt, - Computed: true, - Description: "NIC type number.", - }, - "vlan": { - Type: schema.TypeString, - Computed: true, - Description: "VLAN identifier.", - }, - "mac_address": { - Type: schema.TypeString, - Computed: true, - Description: "MAC address of the NIC.", - }, - }, - }, - }, - "guest_os": { - Type: schema.TypeString, - Computed: true, - Description: "Guest operating system.", - }, - "version_tools": { - Type: schema.TypeString, - Computed: true, - Description: "Version tools.", - }, - "guest_tools_status": { - Type: schema.TypeString, - Computed: true, - Description: "Guest tools status.", - }, - }, - }, - }, - } -} diff --git a/anxcloud/setup_test.go b/anxcloud/setup_test.go index ae5cbd70..5d6d4532 100644 --- a/anxcloud/setup_test.go +++ b/anxcloud/setup_test.go @@ -4,30 +4,19 @@ import ( "log" "os" "testing" - "time" "github.com/anexia-it/terraform-provider-anxcloud/anxcloud/testutils/environment" - testutil "go.anx.io/go-anxcloud/pkg/utils/test" ) func TestMain(m *testing.M) { - testutil.Seed(time.Now().UnixNano()) - - // setup test environment - var env *environment.Info - var err error - - env, err = environment.InitEnvironment() - if err != nil { - log.Fatalf("could not setup environment: %s", err.Error()) - } + env := environment.InitEnvironment() // run tests exitCode := m.Run() // cleanup - err = env.CleanUp() - if err != nil { + + if err := env.CleanUp(); err != nil { log.Fatalf("could not clean up environment: %s", err.Error()) } os.Exit(exitCode) diff --git a/anxcloud/struct_virtual_server.go b/anxcloud/struct_virtual_server.go deleted file mode 100644 index 19c98c0f..00000000 --- a/anxcloud/struct_virtual_server.go +++ /dev/null @@ -1,308 +0,0 @@ -package anxcloud - -import ( - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "go.anx.io/go-anxcloud/pkg/vsphere/info" - "go.anx.io/go-anxcloud/pkg/vsphere/provisioning/vm" -) - -// expanders - -func expandVirtualServerNetworks(p []interface{}) []vm.Network { - var networks []vm.Network - if len(p) < 1 { - return networks - } - - for _, elem := range p { - in := elem.(map[string]interface{}) - network := vm.Network{} - - if v, ok := in["vlan_id"]; ok { - network.VLAN = v.(string) - } - if v, ok := in["nic_type"]; ok { - network.NICType = v.(string) - } - if v, ok := in["ips"]; ok { - ips := v.(*schema.Set) - for _, ip := range ips.List() { - network.IPs = append(network.IPs, ip.(string)) - } - } - - networks = append(networks, network) - } - - return networks -} - -func expandVirtualServerDisks(p []interface{}) []Disk { - disks := make([]Disk, len(p)) - - for i, elem := range p { - in := elem.(map[string]interface{}) - disk := Disk{Disk: &vm.Disk{}} - - if v, ok := in["disk_type"]; ok { - disk.Type = v.(string) - } - if v, ok := in["disk_gb"]; ok { - disk.SizeGBs = v.(int) - } - if v, ok := in["disk_id"]; ok { - disk.ID = v.(int) - } - if v, ok := in["disk_exact"]; ok { - disk.ExactDiskSize = v.(float64) - } - - disks[i] = disk - } - - return disks -} - -func mapToAdditionalDisks(disks []Disk) []vm.AdditionalDisk { - out := make([]vm.AdditionalDisk, 0, len(disks)) - for _, disk := range disks { - out = append(out, vm.AdditionalDisk{ - SizeGBs: disk.SizeGBs, - Type: disk.Type, - }) - } - return out -} - -func expandVirtualServerDNS(p []interface{}) (dns [maxDNSEntries]string) { - if len(p) < 1 { - return dns - } - - for i, elem := range p { - if i > len(dns) { - return dns - } - dns[i] = elem.(string) - } - - return dns -} - -func expandVirtualServerInfo(p []interface{}) info.Info { - var i info.Info - if len(p) < 1 { - return i - } - - att := p[0].(map[string]interface{}) - if v, ok := att["identifier"]; ok { - i.Identifier = v.(string) - } - if v, ok := att["status"]; ok { - i.Status = v.(string) - } - if v, ok := att["name"]; ok { - i.Name = v.(string) - } - if v, ok := att["custom_name"]; ok { - i.CustomName = v.(string) - } - if v, ok := att["location_code"]; ok { - i.LocationCode = v.(string) - } - if v, ok := att["location_country"]; ok { - i.LocationCountry = v.(string) - } - if v, ok := att["location_name"]; ok { - i.LocationName = v.(string) - } - if v, ok := att["cpu"]; ok { - i.CPU = v.(int) - } - if v, ok := att["cores"]; ok { - i.Cores = v.(int) - } - if v, ok := att["ram"]; ok { - i.RAM = v.(int) - } - if v, ok := att["disks_number"]; ok { - i.Disks = v.(int) - } - if v, ok := att["guest_os"]; ok { - i.GuestOS = v.(string) - } - if v, ok := att["version_tools"]; ok { - i.VersionTools = v.(string) - } - if v, ok := att["guest_tools_status"]; ok { - i.GuestToolsStatus = v.(string) - } - - if v, ok := att["disks_info"]; ok { - disks := v.([]interface{}) - - for _, elem := range disks { - disk := info.DiskInfo{} - d := elem.(map[string]interface{}) - - if v, ok := d["disk_id"]; ok { - disk.DiskID = v.(int) - } - if v, ok := d["disk_gb"]; ok { - switch t := v.(type) { - case int: - disk.DiskGB = float64(t) - case float64: - disk.DiskGB = t - } - } - if v, ok := d["disk_type"]; ok { - disk.DiskType = v.(string) - } - if v, ok := d["iops"]; ok { - disk.IOPS = v.(int) - } - if v, ok := d["latency"]; ok { - disk.Latency = v.(int) - } - if v, ok := d["storage_type"]; ok { - disk.StorageType = v.(string) - } - if v, ok := d["bus_type"]; ok { - disk.BusType = v.(string) - } - if v, ok := d["bus_type_label"]; ok { - disk.BusTypeLabel = v.(string) - } - - i.DiskInfo = append(i.DiskInfo, disk) - } - } - - if v, ok := att["network"]; ok { - networks := v.([]interface{}) - - for _, elem := range networks { - network := info.Network{} - n := elem.(map[string]interface{}) - - if v, ok := n["id"]; ok { - network.ID = v.(int) - } - if v, ok := n["nic"]; ok { - network.NIC = v.(int) - } - if v, ok := n["vlan"]; ok { - network.VLAN = v.(string) - } - if v, ok := n["mac_address"]; ok { - network.MACAddress = v.(string) - } - if v, ok := n["ip_v4"]; ok { - for _, ip := range v.([]interface{}) { - network.IPv4 = append(network.IPv4, ip.(string)) - } - } - if v, ok := n["ip_v6"]; ok { - for _, ip := range v.([]interface{}) { - network.IPv6 = append(network.IPv6, ip.(string)) - } - } - - i.Network = append(i.Network, network) - } - } - - return i -} - -// flatteners - -func flattenVirtualServerNetwork(in []vm.Network) []interface{} { - att := []interface{}{} - if len(in) < 1 { - return att - } - - for _, n := range in { - net := map[string]interface{}{} - net["vlan_id"] = n.VLAN - net["nic_type"] = n.NICType - net["ips"] = n.IPs - att = append(att, net) - } - - return att -} - -func flattenVirtualServerInfo(in *info.Info) []interface{} { - if in == nil { - return []interface{}{} - } - - att := map[string]interface{}{} - att["identifier"] = in.Identifier - att["status"] = in.Status - att["name"] = in.Name - att["custom_name"] = in.CustomName - att["location_code"] = in.LocationCode - att["location_country"] = in.LocationCountry - att["location_name"] = in.LocationName - att["cpu"] = in.CPU - att["cores"] = in.Cores - att["ram"] = in.RAM - att["disks_number"] = in.Disks - att["guest_os"] = in.GuestOS - att["version_tools"] = in.VersionTools - att["guest_tools_status"] = in.GuestToolsStatus - - disksInfo := []interface{}{} - for _, d := range in.DiskInfo { - di := map[string]interface{}{} - di["disk_id"] = d.DiskID - di["disk_gb"] = d.DiskGB - di["disk_type"] = d.DiskType - di["iops"] = d.IOPS - di["latency"] = d.Latency - di["storage_type"] = d.StorageType - di["bus_type"] = d.BusType - di["bus_type_label"] = d.BusTypeLabel - disksInfo = append(disksInfo, di) - } - att["disks_info"] = disksInfo - - networkInfo := []interface{}{} - for _, n := range in.Network { - ni := map[string]interface{}{} - ni["id"] = n.ID - ni["nic"] = n.NIC - ni["vlan"] = n.VLAN - ni["mac_address"] = n.MACAddress - ni["ip_v4"] = n.IPv4 - ni["ip_v6"] = n.IPv6 - networkInfo = append(networkInfo, ni) - } - att["network"] = networkInfo - - return []interface{}{att} -} - -func flattenVirtualServerDisks(in []Disk) []interface{} { - att := make([]interface{}, len(in)) - - for i, d := range in { - disk := map[string]interface{}{} - disk["disk_type"] = d.Type - disk["disk_gb"] = d.SizeGBs - disk["disk_id"] = d.ID - disk["disk_exact"] = d.ExactDiskSize - att[i] = disk - } - - return att -} - -func roundDiskSize(size float64) int { - return int(size + 0.5) -} diff --git a/anxcloud/struct_virtual_server_test.go b/anxcloud/struct_virtual_server_test.go deleted file mode 100644 index b1f813c0..00000000 --- a/anxcloud/struct_virtual_server_test.go +++ /dev/null @@ -1,474 +0,0 @@ -package anxcloud - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "go.anx.io/go-anxcloud/pkg/vsphere/info" - "go.anx.io/go-anxcloud/pkg/vsphere/provisioning/vm" -) - -// expanders tests - -func TestExpanderVirtualServerNetworks(t *testing.T) { - cases := []struct { - Input []interface{} - ExpectedOutput []vm.Network - }{ - { - []interface{}{ - map[string]interface{}{ - "vlan_id": "38f8561acfe34qc49c336d2af31a5cc3", - "nic_type": "vmxnet3", - "ips": schema.NewSet(schema.HashSchema(&schema.Schema{Type: schema.TypeString}), []interface{}{ - "identifier1", - "identifier2", - "10.11.12.13", - "1.0.0.1", - }), - }, - }, - []vm.Network{ - { - VLAN: "38f8561acfe34qc49c336d2af31a5cc3", - NICType: "vmxnet3", - IPs: []string{ - "10.11.12.13", - "1.0.0.1", - "identifier1", - "identifier2", - }, - }, - }, - }, - } - - for _, tc := range cases { - output := expandVirtualServerNetworks(tc.Input) - if diff := cmp.Diff(tc.ExpectedOutput, output); diff != "" { - t.Fatalf("Unexpected output from expander: mismatch (-want +got):\n%s", diff) - } - } -} - -func TestExpanderVirtualServerDNS(t *testing.T) { - cases := []struct { - Input []interface{} - ExpectedOutput [maxDNSEntries]string - }{ - { - []interface{}{ - "1.1.1.1", - "2.2.2.2", - "3.3.3.3", - "4.4.4.4", - }, - [maxDNSEntries]string{ - "1.1.1.1", - "2.2.2.2", - "3.3.3.3", - "4.4.4.4", - }, - }, - { - []interface{}{ - "1.1.1.1", - "2.2.2.2", - }, - [maxDNSEntries]string{ - "1.1.1.1", - "2.2.2.2", - "", - "", - }, - }, - { - []interface{}{}, - [maxDNSEntries]string{ - "", - "", - "", - "", - }, - }, - } - - for _, tc := range cases { - output := expandVirtualServerDNS(tc.Input) - if diff := cmp.Diff(tc.ExpectedOutput, output); diff != "" { - t.Fatalf("Unexpected output from expander: mismatch (-want +got):\n%s", diff) - } - } -} - -func TestExpanderVirtualServerDisks(t *testing.T) { - cases := []struct { - Input []interface{} - ExpectedOutput []Disk - }{ - { - []interface{}{ - map[string]interface{}{ - "disk_gb": 10, - "disk_id": 2000, - "disk_type": "STD1", - "disk_exact": 10.10, - }, - }, - []Disk{ - {Disk: &vm.Disk{ID: 2000, Type: "STD1", SizeGBs: 10}, ExactDiskSize: 10.10}, - }, - }, - } - - for _, tc := range cases { - output := expandVirtualServerDisks(tc.Input) - if diff := cmp.Diff(tc.ExpectedOutput, output); diff != "" { - t.Fatalf("Unexpected output from expander: mismatch (-want +got):\n%s", diff) - } - } -} - -func TestExpanderVirtualServerInfo(t *testing.T) { - cases := []struct { - Input []interface{} - ExpectedOutput info.Info - }{ - { - []interface{}{ - map[string]interface{}{ - "name": "12345-test", - "custom_name": "test-vm", - "identifier": "1111111111111111111111", - "guest_os": "Ubuntu Linux (64-bit)", - "location_code": "ANX04", - "location_country": "AT", - "location_name": "ANX04 - AT, Vienna, Datasix", - "status": "poweredOn", - "network": []interface{}{ - map[string]interface{}{ - "nic": 3, - "id": 4000, - "vlan": "111111111111111111111", - "mac_address": "00:50:56:bb:c0:81", - "ip_v4": []interface{}{"1.1.1.1"}, - "ip_v6": []interface{}{"2001:db8::8a2e:370:7334"}, - }, - }, - "ram": 4096, - "cpu": 4, - "cores": 4, - "disks_number": 1, - "disks_info": []interface{}{ - map[string]interface{}{ - "disk_type": "HPC5", - "storage_type": "SSD", - "bus_type": "SCSI", - "bus_type_label": "SCSI(0:0) Hard disk 1", - "disk_gb": 90.00, - "disk_id": 2000, - "iops": 150000, - "latency": 7, - }, - }, - "version_tools": "guestToolsUnmanaged", - "guest_tools_status": "Active", - }, - }, - info.Info{ - Name: "12345-test", - CustomName: "test-vm", - Identifier: "1111111111111111111111", - GuestOS: "Ubuntu Linux (64-bit)", - LocationCode: "ANX04", - LocationCountry: "AT", - LocationName: "ANX04 - AT, Vienna, Datasix", - Status: "poweredOn", - RAM: 4096, - CPU: 4, - Cores: 4, - Network: []info.Network{ - { - NIC: 3, - ID: 4000, - VLAN: "111111111111111111111", - MACAddress: "00:50:56:bb:c0:81", - IPv4: []string{"1.1.1.1"}, - IPv6: []string{"2001:db8::8a2e:370:7334"}, - }, - }, - Disks: 1, - DiskInfo: []info.DiskInfo{ - { - DiskType: "HPC5", - StorageType: "SSD", - BusType: "SCSI", - BusTypeLabel: "SCSI(0:0) Hard disk 1", - DiskGB: 90.00, - DiskID: 2000, - IOPS: 150000, - Latency: 7, - }, - }, - VersionTools: "guestToolsUnmanaged", - GuestToolsStatus: "Active", - }, - }, - { - []interface{}{ - map[string]interface{}{ - "disks_number": 1, - "disks_info": []interface{}{ - map[string]interface{}{ - "disk_gb": 90.00, - }, - }, - "version_tools": "guestToolsUnmanaged", - "guest_tools_status": "Active", - }, - }, - info.Info{ - Disks: 1, - DiskInfo: []info.DiskInfo{ - { - DiskGB: 90.00, - }, - }, - VersionTools: "guestToolsUnmanaged", - GuestToolsStatus: "Active", - }, - }, - { - []interface{}{ - map[string]interface{}{ - "disks_number": 1, - "disks_info": []interface{}{ - map[string]interface{}{ - "disk_gb": 90, - }, - }, - "version_tools": "guestToolsUnmanaged", - "guest_tools_status": "Active", - }, - }, - info.Info{ - Disks: 1, - DiskInfo: []info.DiskInfo{ - { - DiskGB: 90.00, - }, - }, - VersionTools: "guestToolsUnmanaged", - GuestToolsStatus: "Active", - }, - }, - } - - for _, tc := range cases { - output := expandVirtualServerInfo(tc.Input) - if diff := cmp.Diff(tc.ExpectedOutput, output); diff != "" { - t.Fatalf("Unexpected output from expander: mismatch (-want +got):\n%s", diff) - } - } -} - -// flatteners tests - -func TestFlattenVirtualServerNetwork(t *testing.T) { - cases := []struct { - Input []vm.Network - ExpectedOutput []interface{} - }{ - { - []vm.Network{ - { - VLAN: "38f8561acfe34qc49c336d2af31a5cc3", - NICType: "vmxnet3", - IPs: []string{ - "identifier1", - "identifier2", - "10.11.12.13", - "1.0.0.1", - }, - }, - }, - []interface{}{ - map[string]interface{}{ - "vlan_id": "38f8561acfe34qc49c336d2af31a5cc3", - "nic_type": "vmxnet3", - "ips": []string{ - "identifier1", - "identifier2", - "10.11.12.13", - "1.0.0.1", - }, - }, - }, - }, - } - - for _, tc := range cases { - output := flattenVirtualServerNetwork(tc.Input) - if diff := cmp.Diff(tc.ExpectedOutput, output); diff != "" { - t.Fatalf("Unexpected output from expander: mismatch (-want +got):\n%s", diff) - } - } -} - -func TestFlattenVirtualServerInfo(t *testing.T) { - cases := []struct { - Input info.Info - ExpectedOutput []interface{} - }{ - { - info.Info{ - Name: "12345-test", - CustomName: "test-vm", - Identifier: "1111111111111111111111", - GuestOS: "Ubuntu Linux (64-bit)", - LocationCode: "ANX04", - LocationCountry: "AT", - LocationName: "ANX04 - AT, Vienna, Datasix", - Status: "poweredOn", - RAM: 4096, - CPU: 4, - Cores: 4, - Network: []info.Network{ - { - NIC: 3, - ID: 4000, - VLAN: "111111111111111111111", - MACAddress: "00:50:56:bb:c0:81", - IPv4: []string{"1.1.1.1"}, - IPv6: []string{"2001:db8::8a2e:370:7334"}, - }, - }, - Disks: 1, - DiskInfo: []info.DiskInfo{ - { - DiskType: "HPC5", - StorageType: "SSD", - BusType: "SCSI", - BusTypeLabel: "SCSI(0:0) Hard disk 1", - DiskGB: 90, - DiskID: 2000, - IOPS: 150000, - Latency: 7, - }, - }, - VersionTools: "guestToolsUnmanaged", - GuestToolsStatus: "Active", - }, - []interface{}{ - map[string]interface{}{ - "name": "12345-test", - "custom_name": "test-vm", - "identifier": "1111111111111111111111", - "guest_os": "Ubuntu Linux (64-bit)", - "location_code": "ANX04", - "location_country": "AT", - "location_name": "ANX04 - AT, Vienna, Datasix", - "status": "poweredOn", - "network": []interface{}{ - map[string]interface{}{ - "nic": 3, - "id": 4000, - "vlan": "111111111111111111111", - "mac_address": "00:50:56:bb:c0:81", - "ip_v4": []string{"1.1.1.1"}, - "ip_v6": []string{"2001:db8::8a2e:370:7334"}, - }, - }, - "ram": 4096, - "cpu": 4, - "cores": 4, - "disks_number": 1, - "disks_info": []interface{}{ - map[string]interface{}{ - "disk_type": "HPC5", - "storage_type": "SSD", - "bus_type": "SCSI", - "bus_type_label": "SCSI(0:0) Hard disk 1", - "disk_gb": 90.00, - "disk_id": 2000, - "iops": 150000, - "latency": 7, - }, - }, - "version_tools": "guestToolsUnmanaged", - "guest_tools_status": "Active", - }, - }, - }, - } - - for _, tc := range cases { - output := flattenVirtualServerInfo(&tc.Input) - if diff := cmp.Diff(tc.ExpectedOutput, output); diff != "" { - t.Fatalf("Unexpected output from expander: mismatch (-want +got):\n%s", diff) - } - } -} - -func TestFlattenVirtualServerDisks(t *testing.T) { - cases := []struct { - Input []Disk - ExpectedOutput []interface{} - }{ - { - []Disk{ - { - Disk: &vm.Disk{ - ID: 2000, - Type: "STD1", - SizeGBs: 10, - }, - ExactDiskSize: 10.10, - }, - }, - []interface{}{ - map[string]interface{}{ - "disk_id": 2000, - "disk_type": "STD1", - "disk_gb": 10, - "disk_exact": 10.10, - }, - }, - }, - } - - for _, tc := range cases { - output := flattenVirtualServerDisks(tc.Input) - if diff := cmp.Diff(tc.ExpectedOutput, output); diff != "" { - t.Fatalf("Unexpected output from expander: mismatch (-want +got):\n%s", diff) - } - } -} - -func TestRoundDiskSize(t *testing.T) { - cases := []struct { - Input float64 - ExpectedOutput int - }{ - { - 0.9, - 1, - }, - { - 5.4, - 5, - }, - { - 5.5, - 6, - }, - } - - for _, tc := range cases { - output := roundDiskSize(tc.Input) - if diff := cmp.Diff(tc.ExpectedOutput, output); diff != "" { - t.Fatalf("Unexpected output from rounding: mismatch (-want +got):\n%s", diff) - } - } -} diff --git a/anxcloud/testutils/environment/environment.go b/anxcloud/testutils/environment/environment.go index 44a39e2b..21719b09 100644 --- a/anxcloud/testutils/environment/environment.go +++ b/anxcloud/testutils/environment/environment.go @@ -2,12 +2,14 @@ package environment import ( "context" - "errors" - "github.com/goombaio/namegenerator" "log" "os" + "sync" "testing" "time" + + "github.com/goombaio/namegenerator" + testutil "go.anx.io/go-anxcloud/pkg/utils/test" ) type Info struct { @@ -55,29 +57,39 @@ func shouldRunWithTestEnvironment() bool { return anexiaTokenPresent && runAcceptanceTest } -func InitEnvironment() (*Info, error) { +var initEnvironmentOnce sync.Once + +func InitEnvironment() *Info { if !shouldRunWithTestEnvironment() { - return nil, nil + return nil } var locationID, vlanID string var isSet bool if locationID, isSet = os.LookupEnv("ANEXIA_LOCATION_ID"); !isSet { - return nil, errors.New("'ANEXIA_LOCATION_ID' is not set") + log.Fatal("'ANEXIA_LOCATION_ID' is not set") } if vlanID, isSet = os.LookupEnv("ANEXIA_VLAN_ID"); !isSet { - return nil, errors.New("'ANEXIA_VLAN_ID' is not set") + log.Fatal("'ANEXIA_VLAN_ID' is not set") } log.Println("Setting up new test environment") - // we create a new environment - envInfo = &Info{ - TestRunName: namegenerator.NewNameGenerator(time.Now().UnixNano()).Generate(), - VlanID: vlanID, - Location: locationID, - } - return envInfo, envInfo.setup() + initEnvironmentOnce.Do(func() { + testutil.Seed(time.Now().UnixNano()) + // we create a new environment + envInfo = &Info{ + TestRunName: namegenerator.NewNameGenerator(time.Now().UnixNano()).Generate(), + VlanID: vlanID, + Location: locationID, + } + + if err := envInfo.setup(); err != nil { + log.Fatal(err) + } + }) + + return envInfo } func SkipIfNoEnvironment(t *testing.T) { diff --git a/docs/data-sources/virtual_server_template.md b/docs/data-sources/virtual_server_template.md new file mode 100644 index 00000000..ba022478 --- /dev/null +++ b/docs/data-sources/virtual_server_template.md @@ -0,0 +1,38 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "anxcloud_virtual_server_template Data Source - terraform-provider-anxcloud" +subcategory: "" +description: |- + Retrieves a virtual server template. Can be used to resolve a template ID by name, which is needed for creating anxcloudvirtualserver resources. This datasource does not support 'from_scratch' templates! +--- + +# anxcloud_virtual_server_template (Data Source) + +Retrieves a virtual server template. Can be used to resolve a template ID by name, which is needed for creating anxcloud_virtual_server resources. This datasource does not support 'from_scratch' templates! + +## Example Usage + +```terraform +data "anxcloud_virtual_server_template" "debian11" { + name = "Debian 11" + location = data.anxcloud_core_location.anx04.id +} +``` + + +## Schema + +### Required + +- `location` (String) Datacenter location identifier. + +### Optional + +- `build` (String) Template build. +- `name` (String) Template name. + +### Read-Only + +- `id` (String) The ID of this resource. + + diff --git a/docs/index.md b/docs/index.md index 0ba98eff..7e1173b2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -71,10 +71,15 @@ resource "anxcloud_ip_address" "v6" { network_prefix_id = anxcloud_network_prefix.v6.id } +data "anxcloud_virtual_server_template" "debian11" { + name = "Debian 11" + location = data.anxcloud_core_location.anx04.id +} + resource "anxcloud_virtual_server" "webserver" { hostname = "example-terraform" location_id = data.anxcloud_core_location.anx04.id - template = "Debian 11" + template_id = data.anxcloud_virtual_server_template.debian11.id cpus = 4 memory = 4096 @@ -91,7 +96,7 @@ resource "anxcloud_virtual_server" "webserver" { # Set network interface network { vlan_id = anxcloud_vlan.example.id - ips = [anxcloud_ip_address.v4.id, anxcloud_ip_address.v6.id] + ips = [anxcloud_ip_address.v4.address, anxcloud_ip_address.v6.address] nic_type = "vmxnet3" } diff --git a/docs/resources/virtual_server.md b/docs/resources/virtual_server.md index 60054ee7..ed304749 100644 --- a/docs/resources/virtual_server.md +++ b/docs/resources/virtual_server.md @@ -53,10 +53,15 @@ resource "anxcloud_ip_address" "v6" { network_prefix_id = anxcloud_network_prefix.v6.id } +data "anxcloud_virtual_server_template" "debian11" { + name = "Debian 11" + location = data.anxcloud_core_location.anx04.id +} + resource "anxcloud_virtual_server" "example" { hostname = "example-terraform" location_id = data.anxcloud_core_location.anx04.id - template = "Debian 11" + template_id = data.anxcloud_virtual_server_template.debian11.id cpus = 4 memory = 4096 @@ -75,7 +80,7 @@ resource "anxcloud_virtual_server" "example" { # Set network interface network { vlan_id = anxcloud_vlan.example.id - ips = [anxcloud_ip_address.v4.id, anxcloud_ip_address.v6.id] + ips = [anxcloud_ip_address.v4.address, anxcloud_ip_address.v6.address] nic_type = "vmxnet3" } @@ -100,36 +105,32 @@ resource "anxcloud_virtual_server" "example" { ### Required -- `cpus` (Number) Amount of CPUs. -- `disk` (Block List, Min: 1) Virtual Server Disks (see [below for nested schema](#nestedblock--disk)) -- `hostname` (String) Virtual server hostname. -- `location_id` (String) Location identifier. +- `cpus` (Number) Number of CPUs +- `hostname` (String) Virtual server hostname +- `location_id` (String) Location identifier - `memory` (Number) Memory in MB. +- `template_id` (String) Template identifier ### Optional - `boot_delay` (Number) Boot delay in seconds. Example: (0, 1, …). - `cpu_performance_type` (String) CPU type. Example: (`best-effort`, `standard`, `enterprise`, `performance`), defaults to `standard`. - `critical_operation_confirmed` (Boolean) Confirms a critical operation (if needed). Potentially dangerous operations (e.g. resulting in data loss) require an additional confirmation. The parameter is used for VM UPDATE requests. +- `disk` (Block List) Virtual Server Disk. (see [below for nested schema](#nestedblock--disk)) - `dns` (List of String) DNS configuration. Maximum items 4. Defaults to template settings. - `enter_bios_setup` (Boolean) Start the VM into BIOS setup on next boot. - `force_restart_if_needed` (Boolean) Certain operations may only be performed in powered off state. Such as: shrinking memory, shrinking/adding CPU, removing disk and scaling a disk beyond 2 GB. Passing this value as true will always execute a power off and reboot request after completing all other operations. Without this flag set to true scaling operations requiring a reboot will fail. -- `network` (Block List) Network interface (see [below for nested schema](#nestedblock--network)) +- `network` (Block List) Network interface. (see [below for nested schema](#nestedblock--network)) - `password` (String, Sensitive) Plaintext password. Example: ('!anx123mySuperStrongPassword123anx!', 'go3ju0la1ro3', …). For systems that support it, we strongly recommend using a SSH key instead. - `script` (String) Script to be executed after provisioning. Consider the corresponding shebang at the beginning of your script. If you want to use PowerShell, the first line should be: #ps1_sysnative. - `sockets` (Number) Amount of CPU sockets Number of cores have to be a multiple of sockets, as they will be spread evenly across all sockets. Defaults to number of cores, i.e. one socket per CPU core. -- `ssh_key` (String) Public key (instead of password, only for Linux systems). Recommended over providing a plaintext password. -- `tags` (Set of String) Set of tags attached to the resource. -- `template` (String) Named template. Can be used instead of the template_id to select a template. Example: (`Debian 11`, `Windows 2022`). -- `template_build` (String) Template build identifier optionally used with `template`. Will default to latest build. Example: `b42` -- `template_id` (String) Template identifier. -- `template_type` (String) OS template type. -- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) +- `ssh_key` (String, Sensitive) Public key (instead of password, only for Linux systems). Recommended over providing a plaintext password. +- `tags` (Set of String) Set of tags attached to the resource +- `template_type` (String) OS template type ### Read-Only -- `id` (String) The ID of this resource. -- `info` (List of Object) Virtual server info (see [below for nested schema](#nestedatt--info)) +- `id` (String) Virtual server identifier ### Nested Schema for `disk` @@ -137,14 +138,10 @@ resource "anxcloud_virtual_server" "example" { Required: - `disk_gb` (Number) Disk capacity in GB. - -Optional: - -- `disk_type` (String) Disk category (limits disk performance, e.g. IOPS). Default value depends on location. +- `disk_type` (String) Disk category (limits disk performance, e.g. IOPS). Read-Only: -- `disk_exact` (Number) Exact floating point disk size. Not configurable; just for comparison. - `disk_id` (Number) Device identifier of the disk. @@ -158,67 +155,6 @@ Required: Optional: -- `ips` (List of String) Requested list of IPs and IPs identifiers. IPs are ignored when using template_type 'from_scratch'. Defaults to free IPs from IP pool attached to VLAN. - - - -### Nested Schema for `timeouts` - -Optional: - -- `create` (String) -- `delete` (String) -- `read` (String) -- `update` (String) - - - -### Nested Schema for `info` - -Read-Only: - -- `cores` (Number) Number of CPU cores. -- `cpu` (Number) Number of CPUs. -- `custom_name` (String) Virtual server custom name. -- `disks_info` (List of Object) Disks info. (see [below for nested schema](#nestedobjatt--info--disks_info)) -- `disks_number` (Number) Number of the attached disks. -- `guest_os` (String) Guest operating system. -- `guest_tools_status` (String) Guest tools status. -- `identifier` (String) Identifier of the API resource. -- `location_code` (String) Location code. -- `location_country` (String) Location country. -- `location_name` (String) Location name. -- `name` (String) Virtual server name. -- `network` (List of Object) Network interfaces. (see [below for nested schema](#nestedobjatt--info--network)) -- `ram` (Number) Memory in MB. -- `status` (String) Virtual server status. -- `version_tools` (String) Version tools. - - -### Nested Schema for `info.disks_info` - -Read-Only: - -- `bus_type` (String) Bus type. -- `bus_type_label` (String) Bus type label. -- `disk_gb` (Number) Size of the disk in GB. -- `disk_id` (Number) Disk identifier. -- `disk_type` (String) Disk type. -- `iops` (Number) Disk input/output operations per second. -- `latency` (Number) Disk latency. -- `storage_type` (String) Disk storage type. - - - -### Nested Schema for `info.network` - -Read-Only: - -- `id` (Number) Network interface card identifier. -- `ip_v4` (List of String) List of IPv4 addresses attached to the interface. -- `ip_v6` (List of String) List of IPv6 addresses attached to the interface. -- `mac_address` (String) MAC address of the NIC. -- `nic` (Number) NIC type number. -- `vlan` (String) VLAN identifier. +- `ips` (List of String) List of IP addresses and identifiers to be assigned and configured. diff --git a/examples/data-sources/anxcloud_virtual_server_template/data-source.tf b/examples/data-sources/anxcloud_virtual_server_template/data-source.tf new file mode 100644 index 00000000..6dc5642e --- /dev/null +++ b/examples/data-sources/anxcloud_virtual_server_template/data-source.tf @@ -0,0 +1,4 @@ +data "anxcloud_virtual_server_template" "debian11" { + name = "Debian 11" + location = data.anxcloud_core_location.anx04.id +} diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf index ae4ede0b..6cd23191 100644 --- a/examples/provider/provider.tf +++ b/examples/provider/provider.tf @@ -52,10 +52,15 @@ resource "anxcloud_ip_address" "v6" { network_prefix_id = anxcloud_network_prefix.v6.id } +data "anxcloud_virtual_server_template" "debian11" { + name = "Debian 11" + location = data.anxcloud_core_location.anx04.id +} + resource "anxcloud_virtual_server" "webserver" { hostname = "example-terraform" location_id = data.anxcloud_core_location.anx04.id - template = "Debian 11" + template_id = data.anxcloud_virtual_server_template.debian11.id cpus = 4 memory = 4096 @@ -72,7 +77,7 @@ resource "anxcloud_virtual_server" "webserver" { # Set network interface network { vlan_id = anxcloud_vlan.example.id - ips = [anxcloud_ip_address.v4.id, anxcloud_ip_address.v6.id] + ips = [anxcloud_ip_address.v4.address, anxcloud_ip_address.v6.address] nic_type = "vmxnet3" } diff --git a/examples/resources/anxcloud_virtual_server/resource.tf b/examples/resources/anxcloud_virtual_server/resource.tf index 8b52ddf8..dc4edf1d 100644 --- a/examples/resources/anxcloud_virtual_server/resource.tf +++ b/examples/resources/anxcloud_virtual_server/resource.tf @@ -34,10 +34,15 @@ resource "anxcloud_ip_address" "v6" { network_prefix_id = anxcloud_network_prefix.v6.id } +data "anxcloud_virtual_server_template" "debian11" { + name = "Debian 11" + location = data.anxcloud_core_location.anx04.id +} + resource "anxcloud_virtual_server" "example" { hostname = "example-terraform" location_id = data.anxcloud_core_location.anx04.id - template = "Debian 11" + template_id = data.anxcloud_virtual_server_template.debian11.id cpus = 4 memory = 4096 @@ -56,7 +61,7 @@ resource "anxcloud_virtual_server" "example" { # Set network interface network { vlan_id = anxcloud_vlan.example.id - ips = [anxcloud_ip_address.v4.id, anxcloud_ip_address.v6.id] + ips = [anxcloud_ip_address.v4.address, anxcloud_ip_address.v6.address] nic_type = "vmxnet3" } diff --git a/go.mod b/go.mod index e42440ca..0ee9dae1 100644 --- a/go.mod +++ b/go.mod @@ -9,11 +9,12 @@ require ( github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.6.0 github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e - github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 github.com/hashicorp/terraform-plugin-framework v1.5.0 + github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 github.com/hashicorp/terraform-plugin-go v0.20.0 github.com/hashicorp/terraform-plugin-mux v0.13.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.31.0 + github.com/hashicorp/terraform-plugin-testing v1.6.0 github.com/mitchellh/go-testing-interface v1.14.1 github.com/onsi/ginkgo/v2 v2.13.2 github.com/onsi/gomega v1.30.0 @@ -46,6 +47,7 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 // indirect github.com/hashicorp/go-hclog v1.6.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.6.0 // indirect @@ -54,14 +56,15 @@ require ( github.com/hashicorp/hc-install v0.6.2 // indirect github.com/hashicorp/hcl/v2 v2.19.1 // indirect github.com/hashicorp/logutils v1.0.0 // indirect - github.com/hashicorp/terraform-exec v0.19.0 // indirect - github.com/hashicorp/terraform-json v0.18.0 // indirect + github.com/hashicorp/terraform-exec v0.20.0 // indirect + github.com/hashicorp/terraform-json v0.21.0 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -72,12 +75,14 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/oklog/run v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zclconf/go-cty v1.14.1 // indirect golang.org/x/crypto v0.18.0 // indirect + golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/term v0.16.0 // indirect diff --git a/go.sum b/go.sum index 6f1b7884..89d4c0d5 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,7 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -105,12 +106,14 @@ github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5R github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.19.0 h1:FpqZ6n50Tk95mItTSS9BjeOVUb4eg81SpgVtZNNtFSM= -github.com/hashicorp/terraform-exec v0.19.0/go.mod h1:tbxUpe3JKruE9Cuf65mycSIT8KiNPZ0FkuTE3H4urQg= -github.com/hashicorp/terraform-json v0.18.0 h1:pCjgJEqqDESv4y0Tzdqfxr/edOIGkjs8keY42xfNBwU= -github.com/hashicorp/terraform-json v0.18.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk= +github.com/hashicorp/terraform-exec v0.20.0 h1:DIZnPsqzPGuUnq6cH8jWcPunBfY+C+M8JyYF3vpnuEo= +github.com/hashicorp/terraform-exec v0.20.0/go.mod h1:ckKGkJWbsNqFKV1itgMnE0hY9IYf1HoiekpuN0eWoDw= +github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRyRNd+zTI05U= +github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk= github.com/hashicorp/terraform-plugin-framework v1.5.0 h1:8kcvqJs/x6QyOFSdeAyEgsenVOUeC/IyKpi2ul4fjTg= github.com/hashicorp/terraform-plugin-framework v1.5.0/go.mod h1:6waavirukIlFpVpthbGd2PUNYaFedB0RwW3MDzJ/rtc= +github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 h1:HOjBuMbOEzl7snOdOoUfE2Jgeto6JOjLVQ39Ls2nksc= +github.com/hashicorp/terraform-plugin-framework-validators v0.12.0/go.mod h1:jfHGE/gzjxYz6XoUwi/aYiiKrJDeutQNUtGQXkaHklg= github.com/hashicorp/terraform-plugin-go v0.20.0 h1:oqvoUlL+2EUbKNsJbIt3zqqZ7wi6lzn4ufkn/UA51xQ= github.com/hashicorp/terraform-plugin-go v0.20.0/go.mod h1:Rr8LBdMlY53a3Z/HpP+ZU3/xCDqtKNCkeI9qOyT10QE= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= @@ -119,6 +122,8 @@ github.com/hashicorp/terraform-plugin-mux v0.13.0 h1:79U401/3nd8CWwDGtTHc8F3miSC github.com/hashicorp/terraform-plugin-mux v0.13.0/go.mod h1:Ndv0FtwDG2ogzH59y64f2NYimFJ6I0smRgFUKfm6dyQ= github.com/hashicorp/terraform-plugin-sdk/v2 v2.31.0 h1:Bl3e2ei2j/Z3Hc2HIS15Gal2KMKyLAZ2om1HCEvK6es= github.com/hashicorp/terraform-plugin-sdk/v2 v2.31.0/go.mod h1:i2C41tszDjiWfziPQDL5R/f3Zp0gahXe5No/MIO9rCE= +github.com/hashicorp/terraform-plugin-testing v1.6.0 h1:Wsnfh+7XSVRfwcr2jZYHsnLOnZl7UeaOBvsx6dl/608= +github.com/hashicorp/terraform-plugin-testing v1.6.0/go.mod h1:cJGG0/8j9XhHaJZRC+0sXFI4uzqQZ9Az4vh6C4GJpFE= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= @@ -144,8 +149,9 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -186,8 +192,8 @@ github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= @@ -227,6 +233,8 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 h1:EDuYyU/MkFXllv9QF9819VlI9a4tzGuCbhG0ExK9o1U= +golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= diff --git a/internal/provider/customtypes/cpu_performance_type_string.go b/internal/provider/customtypes/cpu_performance_type_string.go new file mode 100644 index 00000000..a1c121e6 --- /dev/null +++ b/internal/provider/customtypes/cpu_performance_type_string.go @@ -0,0 +1,86 @@ +package customtypes + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ basetypes.StringValuable = CPUPerformanceTypeStringValue{} +var _ basetypes.StringValuableWithSemanticEquals = CPUPerformanceTypeStringValue{} + +type CPUPerformanceTypeStringValue struct { + basetypes.StringValue +} + +func (v CPUPerformanceTypeStringValue) StringSemanticEquals(ctx context.Context, newValuable basetypes.StringValuable) (bool, diag.Diagnostics) { + var diags diag.Diagnostics + + newValue, ok := newValuable.(CPUPerformanceTypeStringValue) + if !ok { + diags.AddError( + "Semantic Equality Check Error", + "Received unexpected value type", + ) + + return false, diags + } + + return strings.HasPrefix(v.ValueString(), newValue.ValueString()), diags +} + +func CPUPerformanceTypeValue(value string) CPUPerformanceTypeStringValue { + return CPUPerformanceTypeStringValue{ + StringValue: types.StringValue(value), + } +} + +var _ basetypes.StringTypable = CPUPerformanceTypeStringType{} + +type CPUPerformanceTypeStringType struct { + basetypes.StringType +} + +func (t CPUPerformanceTypeStringType) String() string { + return "CPUPerformanceTypeStringType" +} + +func (t CPUPerformanceTypeStringType) ValueFromString(ctx context.Context, in basetypes.StringValue) (basetypes.StringValuable, diag.Diagnostics) { + value := CPUPerformanceTypeStringValue{ + StringValue: in, + } + + return value, nil +} + +func (t CPUPerformanceTypeStringType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + attrValue, err := t.StringType.ValueFromTerraform(ctx, in) + + if err != nil { + return nil, err + } + + stringValue, ok := attrValue.(basetypes.StringValue) + + if !ok { + return nil, fmt.Errorf("unexpected value type of %T", attrValue) + } + + stringValuable, diags := t.ValueFromString(ctx, stringValue) + + if diags.HasError() { + return nil, fmt.Errorf("unexpected error converting StringValue to StringValuable: %v", diags) + } + + return stringValuable, nil +} + +func (t CPUPerformanceTypeStringType) ValueType(ctx context.Context) attr.Value { + return CPUPerformanceTypeStringValue{} +} diff --git a/internal/provider/customtypes/hostname_string.go b/internal/provider/customtypes/hostname_string.go new file mode 100644 index 00000000..8ead9718 --- /dev/null +++ b/internal/provider/customtypes/hostname_string.go @@ -0,0 +1,86 @@ +package customtypes + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +var _ basetypes.StringValuable = HostnameStringValue{} +var _ basetypes.StringValuableWithSemanticEquals = HostnameStringValue{} + +type HostnameStringValue struct { + basetypes.StringValue +} + +func (v HostnameStringValue) StringSemanticEquals(ctx context.Context, newValuable basetypes.StringValuable) (bool, diag.Diagnostics) { + var diags diag.Diagnostics + + newValue, ok := newValuable.(HostnameStringValue) + if !ok { + diags.AddError( + "Semantic Equality Check Error", + "Received unexpected value type", + ) + + return false, diags + } + + return strings.HasSuffix(v.ValueString(), newValue.ValueString()), diags +} + +func HostnameValue(value string) HostnameStringValue { + return HostnameStringValue{ + StringValue: types.StringValue(value), + } +} + +var _ basetypes.StringTypable = HostnameStringType{} + +type HostnameStringType struct { + basetypes.StringType +} + +func (t HostnameStringType) String() string { + return "HostnameStringType" +} + +func (t HostnameStringType) ValueFromString(ctx context.Context, in basetypes.StringValue) (basetypes.StringValuable, diag.Diagnostics) { + value := HostnameStringValue{ + StringValue: in, + } + + return value, nil +} + +func (t HostnameStringType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) { + attrValue, err := t.StringType.ValueFromTerraform(ctx, in) + + if err != nil { + return nil, err + } + + stringValue, ok := attrValue.(basetypes.StringValue) + + if !ok { + return nil, fmt.Errorf("unexpected value type of %T", attrValue) + } + + stringValuable, diags := t.ValueFromString(ctx, stringValue) + + if diags.HasError() { + return nil, fmt.Errorf("unexpected error converting StringValue to StringValuable: %v", diags) + } + + return stringValuable, nil +} + +func (t HostnameStringType) ValueType(ctx context.Context) attr.Value { + return HostnameStringValue{} +} diff --git a/internal/provider/planmodifiers/keep_ip_address_order.go b/internal/provider/planmodifiers/keep_ip_address_order.go new file mode 100644 index 00000000..1088a8cc --- /dev/null +++ b/internal/provider/planmodifiers/keep_ip_address_order.go @@ -0,0 +1,37 @@ +package planmodifiers + +import ( + "context" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +func KeepIPAddressOrderPlanModifier() planmodifier.List { + return &keepIPAddressOrderPlanModifier{} +} + +type keepIPAddressOrderPlanModifier struct{} + +func (*keepIPAddressOrderPlanModifier) Description(context.Context) string { + return "Ensures that if the addresses in state are equal to the ones from plan, the order from state will be preserved" +} + +func (m *keepIPAddressOrderPlanModifier) MarkdownDescription(ctx context.Context) string { + return m.Description(ctx) +} + +func (m *keepIPAddressOrderPlanModifier) PlanModifyList(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + if req.StateValue.IsNull() { + return + } + + var stateValues, planValues []string + resp.Diagnostics.Append(req.StateValue.ElementsAs(ctx, &stateValues, true)...) + resp.Diagnostics.Append(req.PlanValue.ElementsAs(ctx, &planValues, true)...) + + if cmp.Diff(stateValues, planValues, cmpopts.SortSlices(func(a, b string) bool { return a < b })) == "" { + resp.PlanValue = req.StateValue + } +} diff --git a/internal/provider/planmodifiers/string_modifiers.go b/internal/provider/planmodifiers/string_modifiers.go new file mode 100644 index 00000000..a4cc0f57 --- /dev/null +++ b/internal/provider/planmodifiers/string_modifiers.go @@ -0,0 +1,56 @@ +package planmodifiers + +import ( + "context" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +type keepStringPrefixModifier struct{} + +func (m keepStringPrefixModifier) Description(_ context.Context) string { + return "Ensures that if the the plan value is the suffix of the state value, the value from state will be preserved" +} + +func (m keepStringPrefixModifier) MarkdownDescription(ctx context.Context) string { + return m.Description(ctx) +} + +func (m keepStringPrefixModifier) PlanModifyString(_ context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + if req.StateValue.IsNull() { + return + } + + if strings.HasSuffix(req.StateValue.ValueString(), req.PlanValue.ValueString()) { + resp.PlanValue = req.StateValue + } +} + +func KeepStringPrefix() planmodifier.String { + return keepStringPrefixModifier{} +} + +type keepStringSuffixModifier struct{} + +func (m keepStringSuffixModifier) Description(_ context.Context) string { + return "Ensures that if the the plan value is the prefix of the state value, the value from state will be preserved" +} + +func (m keepStringSuffixModifier) MarkdownDescription(ctx context.Context) string { + return m.Description(ctx) +} + +func (m keepStringSuffixModifier) PlanModifyString(_ context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + if req.StateValue.IsNull() { + return + } + + if strings.HasPrefix(req.StateValue.ValueString(), req.PlanValue.ValueString()) { + resp.PlanValue = req.StateValue + } +} + +func KeepStringSuffix() planmodifier.String { + return keepStringSuffixModifier{} +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 121e7fc8..a1f84baf 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" + "go.anx.io/go-anxcloud/pkg/api" "go.anx.io/go-anxcloud/pkg/client" ) @@ -68,16 +69,38 @@ func (p *AnexiaProvider) Configure(ctx context.Context, req provider.ConfigureRe client.UserAgent(fmt.Sprintf("%s/%s (%s)", "terraform-provider-anxcloud", p.version, runtime.GOOS)), } - resp.ResourceData = opts - resp.DataSourceData = opts + engine, err := api.NewAPI(api.WithClientOptions(opts...)) + if err != nil { + resp.Diagnostics.AddError("Unable to create generic API client", err.Error()) + } + + legacyClient, err := client.New(opts...) + if err != nil { + resp.Diagnostics.AddError("Unable to create legacy API client", err.Error()) + return + } + + providerConfig := providerConfiguration{engine, legacyClient} + + resp.ResourceData = providerConfig + resp.DataSourceData = providerConfig +} + +type providerConfiguration struct { + engine api.API + legacyClient client.Client } func (p *AnexiaProvider) Resources(ctx context.Context) []func() resource.Resource { - return nil + return []func() resource.Resource{ + NewVirtuaServerResource, + } } func (p *AnexiaProvider) DataSources(ctx context.Context) []func() datasource.DataSource { - return nil + return []func() datasource.DataSource{ + NewVirtuaServerTemplateDataSource, + } } func New(version string) func() provider.Provider { diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go new file mode 100644 index 00000000..11fd65f6 --- /dev/null +++ b/internal/provider/provider_test.go @@ -0,0 +1,88 @@ +package provider + +import ( + "context" + "fmt" + "log" + "runtime" + "testing" + + "github.com/anexia-it/terraform-provider-anxcloud/anxcloud" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-mux/tf5to6server" + "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" + "go.anx.io/go-anxcloud/pkg/client" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ + "anxcloud": func() (tfprotov6.ProviderServer, error) { + ctx := context.TODO() + upgradedSdkServer, err := tf5to6server.UpgradeServer( + ctx, + anxcloud.Provider("test").GRPCProvider, + ) + if err != nil { + return nil, err + } + + providers := []func() tfprotov6.ProviderServer{ + providerserver.NewProtocol6(New("test")()), + func() tfprotov6.ProviderServer { + return upgradedSdkServer + }, + } + + muxServer, err := tf6muxserver.NewMuxServer(ctx, providers...) + + if err != nil { + return nil, err + } + + return muxServer.ProviderServer(), nil + }, +} + +//nolint:unused +var testAccProtoV6MockProviderFactories = func(endpoint string) map[string]func() (tfprotov6.ProviderServer, error) { + return map[string]func() (tfprotov6.ProviderServer, error){ + "anxcloud": func() (tfprotov6.ProviderServer, error) { + return providerserver.NewProtocol6WithError(NewAnexiaMockProvider(endpoint))() + }, + } +} + +type anexiaMockProvider struct { + AnexiaProvider + endpoint string +} + +func NewAnexiaMockProvider(endpoint string) provider.Provider { + return &anexiaMockProvider{ + endpoint: endpoint, + } +} + +func (p *anexiaMockProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + logger := anxcloud.NewTerraformr(log.Default().Writer()) + opts := []client.Option{ + client.BaseURL(p.endpoint), + client.IgnoreMissingToken(), + client.Logger(logger.WithName("client")), + client.UserAgent(fmt.Sprintf("%s/%s (%s)", "terraform-provider-anxcloud", p.version, runtime.GOOS)), + } + + resp.ResourceData = opts + resp.DataSourceData = opts +} + +func testAccPreCheck(t *testing.T) {} + +func TestFrameworkSuite(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "framework suite") +} diff --git a/internal/provider/setup_test.go b/internal/provider/setup_test.go new file mode 100644 index 00000000..36ade7ee --- /dev/null +++ b/internal/provider/setup_test.go @@ -0,0 +1,23 @@ +package provider + +import ( + "log" + "os" + "testing" + + "github.com/anexia-it/terraform-provider-anxcloud/anxcloud/testutils/environment" +) + +func TestMain(m *testing.M) { + env := environment.InitEnvironment() + + // run tests + exitCode := m.Run() + + // cleanup + + if err := env.CleanUp(); err != nil { + log.Fatalf("could not clean up environment: %s", err.Error()) + } + os.Exit(exitCode) +} diff --git a/internal/provider/tag_utils.go b/internal/provider/tag_utils.go new file mode 100644 index 00000000..2c2ac412 --- /dev/null +++ b/internal/provider/tag_utils.go @@ -0,0 +1,72 @@ +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "go.anx.io/go-anxcloud/pkg/api" + corev1 "go.anx.io/go-anxcloud/pkg/apis/core/v1" +) + +func ensureTags(ctx context.Context, engine api.API, id string, plan tfsdk.Plan) (diags diag.Diagnostics) { + var tagSet types.Set + diags.Append(plan.GetAttribute(ctx, path.Root("tags"), &tagSet)...) + if diags.HasError() { + return + } + + var tags []string + diags.Append(tagSet.ElementsAs(ctx, &tags, true)...) + + resource := corev1.Resource{Identifier: id} + + remote, err := corev1.ListTags(ctx, engine, &resource) + if err != nil { + diags.AddError("Unable to list tags", err.Error()) + return + } + + toRemove := sliceSubstract(remote, tags) + if err := corev1.Untag(ctx, engine, &resource, toRemove...); err != nil { + diags.AddError("Failed to untag resource", err.Error()) + } + + toAdd := sliceSubstract(tags, remote) + if err := corev1.Tag(ctx, engine, &resource, toAdd...); err != nil { + diags.AddError("Failed to tag resource", err.Error()) + } + + return +} + +func sliceSubstract[T comparable](a, b []T) []T { + out := make([]T, 0, len(a)) +outer: + for i := range a { + for j := range b { + if a[i] == b[j] { + continue outer + } + } + out = append(out, a[i]) + } + return out +} + +func readTags(ctx context.Context, engine api.API, id string, tagSet *types.Set) (diags diag.Diagnostics) { + tags, err := corev1.ListTags(ctx, engine, &corev1.Resource{Identifier: id}) + if err != nil { + diags.AddError("Unable to list tags", err.Error()) + return + } + + newTagSet, tagSetDiags := types.SetValueFrom(ctx, types.StringType, &tags) + diags.Append(tagSetDiags...) + + *tagSet = newTagSet + + return +} diff --git a/internal/provider/validators/ip_address.go b/internal/provider/validators/ip_address.go new file mode 100644 index 00000000..1ab108c4 --- /dev/null +++ b/internal/provider/validators/ip_address.go @@ -0,0 +1,41 @@ +package validators + +import ( + "context" + "net/netip" + + "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +var _ validator.String = ipAddressValidator{} + +type ipAddressValidator struct{} + +func (v ipAddressValidator) Description(ctx context.Context) string { + return "value must be a valid ip address; identifiers are no longer supported" +} + +func (v ipAddressValidator) MarkdownDescription(ctx context.Context) string { + return v.Description(ctx) +} + +func (v ipAddressValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + val := req.ConfigValue.ValueString() + + if _, err := netip.ParseAddr(val); err != nil { + resp.Diagnostics.Append(validatordiag.InvalidAttributeValueMatchDiagnostic( + req.Path, + v.Description(ctx), + val, + )) + } +} + +func ValidIPAddress() validator.String { + return ipAddressValidator{} +} diff --git a/internal/provider/virtual_server_resource.go b/internal/provider/virtual_server_resource.go new file mode 100644 index 00000000..c06e98e0 --- /dev/null +++ b/internal/provider/virtual_server_resource.go @@ -0,0 +1,376 @@ +package provider + +import ( + "context" + "time" + + "github.com/anexia-it/terraform-provider-anxcloud/internal/provider/customtypes" + "github.com/anexia-it/terraform-provider-anxcloud/internal/utils" + + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + + "go.anx.io/go-anxcloud/pkg/api" + "go.anx.io/go-anxcloud/pkg/ipam" + "go.anx.io/go-anxcloud/pkg/ipam/address" + "go.anx.io/go-anxcloud/pkg/vsphere" + "go.anx.io/go-anxcloud/pkg/vsphere/provisioning/nictype" + "go.anx.io/go-anxcloud/pkg/vsphere/provisioning/vm" +) + +var _ resource.Resource = &VirtualServerResource{} +var _ resource.ResourceWithImportState = &VirtualServerResource{} +var _ resource.ResourceWithConfigValidators = &VirtualServerResource{} + +func NewVirtuaServerResource() resource.Resource { + return &VirtualServerResource{} +} + +type VirtualServerResource struct { + engine api.API + vsphereAPI vsphere.API + ipamAPI ipam.API + nicTypeAPI nictype.API +} + +type VirtualServerResourceModel struct { + ID types.String `tfsdk:"id"` + Hostname customtypes.HostnameStringValue `tfsdk:"hostname"` + Location types.String `tfsdk:"location_id"` + Template types.String `tfsdk:"template_id"` + TemplateType types.String `tfsdk:"template_type"` + CPUs types.Int64 `tfsdk:"cpus"` + CPUPerformanceType customtypes.CPUPerformanceTypeStringValue `tfsdk:"cpu_performance_type"` + CPUSockets types.Int64 `tfsdk:"sockets"` + Memory types.Int64 `tfsdk:"memory"` + Disks types.List `tfsdk:"disk"` + Networks types.List `tfsdk:"network"` + DNS types.List `tfsdk:"dns"` + Password types.String `tfsdk:"password"` + SSH types.String `tfsdk:"ssh_key"` + Script types.String `tfsdk:"script"` + BootDelay types.Int64 `tfsdk:"boot_delay"` + EnterBIOSSetup types.Bool `tfsdk:"enter_bios_setup"` + ForceRestartIfNeeded types.Bool `tfsdk:"force_restart_if_needed"` + CriticalOperationConfirmed types.Bool `tfsdk:"critical_operation_confirmed"` + + Tags types.Set `tfsdk:"tags"` +} + +type VirtualServerDiskModel struct { + ID types.Int64 `tfsdk:"disk_id"` + SizeGB types.Int64 `tfsdk:"disk_gb"` + Type types.String `tfsdk:"disk_type"` +} + +type VirtualServerNetworkModel struct { + VLAN types.String `tfsdk:"vlan_id"` + NICType types.String `tfsdk:"nic_type"` + IPs types.List `tfsdk:"ips"` +} + +func (r *VirtualServerResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_virtual_server" +} + +func (r *VirtualServerResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + providerConfig := req.ProviderData.(providerConfiguration) + r.vsphereAPI = vsphere.NewAPI(providerConfig.legacyClient) + r.ipamAPI = ipam.NewAPI(providerConfig.legacyClient) + r.nicTypeAPI = nictype.NewAPI(providerConfig.legacyClient) + r.engine = providerConfig.engine +} + +func (*VirtualServerResource) ConfigValidators(context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + resourcevalidator.ExactlyOneOf( + path.MatchRoot("password"), + path.MatchRoot("ssh_key"), + ), + } +} + +func (r *VirtualServerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data VirtualServerResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + var dns [4]string + var dnsFromPlan []string + resp.Diagnostics.Append(data.DNS.ElementsAs(ctx, &dnsFromPlan, false)...) + for i := 0; i < len(dnsFromPlan) && i < len(dns); i++ { + dns[i] = dnsFromPlan[i] + } + + var planDisks []VirtualServerDiskModel + resp.Diagnostics.Append(data.Disks.ElementsAs(ctx, &planDisks, false)...) + disks := make([]vm.AdditionalDisk, 0, len(planDisks)) + for _, disk := range planDisks { + disks = append(disks, vm.AdditionalDisk{ + SizeGBs: int(disk.SizeGB.ValueInt64()), + Type: disk.Type.ValueString(), + }) + } + + var planNetworks []VirtualServerNetworkModel + resp.Diagnostics.Append(data.Networks.ElementsAs(ctx, &planNetworks, false)...) + networks := make([]vm.Network, 0, len(planNetworks)) + for _, network := range planNetworks { + var ips []string + resp.Diagnostics.Append(network.IPs.ElementsAs(ctx, &ips, true)...) + + if len(ips) == 0 { + reserveSummary, err := r.ipamAPI.Address().ReserveRandom(ctx, address.ReserveRandom{ + LocationID: data.Location.ValueString(), + VlanID: network.VLAN.ValueString(), + Count: 1, + }) + if err != nil { + resp.Diagnostics.AddError("Unable to reserve random address", err.Error()) + return + } + + ips = append(ips, reserveSummary.Data[0].Address) + } + + networks = append(networks, vm.Network{ + VLAN: network.VLAN.ValueString(), + NICType: network.NICType.ValueString(), + IPs: ips, + }) + } + + if resp.Diagnostics.HasError() { + return + } + + create := vm.Definition{ + Hostname: data.Hostname.ValueString(), + Location: data.Location.ValueString(), + TemplateID: data.Template.ValueString(), + TemplateType: data.TemplateType.ValueString(), + Memory: int(data.Memory.ValueInt64()), + CPUs: int(data.CPUs.ValueInt64()), + CPUPerformanceType: data.CPUPerformanceType.ValueString(), + Sockets: int(data.CPUSockets.ValueInt64()), + Disk: disks[0].SizeGBs, + DiskType: disks[0].Type, + AdditionalDisks: disks[1:], + Network: networks, + DNS1: dns[0], + DNS2: dns[1], + DNS3: dns[2], + DNS4: dns[3], + Password: data.Password.ValueString(), + SSH: data.SSH.ValueString(), + Script: data.Script.ValueString(), + BootDelay: int(data.BootDelay.ValueInt64()), + EnterBIOSSetup: data.EnterBIOSSetup.ValueBool(), + } + + provisioning, err := r.vsphereAPI.Provisioning().VM().Provision(ctx, create, true) + if err != nil { + resp.Diagnostics.AddError("failed provisioning vm", err.Error()) + return + } + + vmIdentifier, err := r.vsphereAPI.Provisioning().Progress().AwaitCompletion(ctx, provisioning.Identifier) + if err != nil { + resp.Diagnostics.AddError("failed awaiting vm provisioning", err.Error()) + return + } + + data.ID = types.StringValue(vmIdentifier) + + resp.Diagnostics.Append(ensureTags(ctx, r.engine, vmIdentifier, req.Plan)...) + + time.Sleep(2 * time.Minute) // need to wait for guest tools to report data + + if diags, notFound := r.setFromInfo(ctx, &data); notFound { + resp.State.RemoveResource(ctx) + return + } else { + resp.Diagnostics.Append(diags...) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *VirtualServerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state VirtualServerResourceModel + + resp.Diagnostics.Append(resp.State.Get(ctx, &state)...) + + if diags, notFound := r.setFromInfo(ctx, &state); notFound { + resp.State.RemoveResource(ctx) + return + } else { + resp.Diagnostics.Append(diags...) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *VirtualServerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state VirtualServerResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + change := vm.Change{ + Reboot: plan.ForceRestartIfNeeded.ValueBool(), + EnableDangerous: plan.CriticalOperationConfirmed.ValueBool(), + } + + if !plan.Tags.Equal(state.Tags) { + resp.Diagnostics.Append(ensureTags(ctx, r.engine, state.ID.ValueString(), req.Plan)...) + } + + needsUpdate := false + if !plan.Memory.Equal(state.Memory) { + needsUpdate = true + change.MemoryMBs = int(plan.Memory.ValueInt64()) + } + if !plan.CPUs.Equal(state.CPUs) { + needsUpdate = true + change.CPUs = int(plan.CPUs.ValueInt64()) + } + if !plan.CPUSockets.Equal(state.CPUSockets) { + needsUpdate = true + change.CPUSockets = int(plan.CPUSockets.ValueInt64()) + } + if !plan.CPUPerformanceType.Equal(state.CPUPerformanceType) { + needsUpdate = true + change.CPUPerformanceType = plan.CPUPerformanceType.ValueString() + } + if !plan.BootDelay.Equal(state.BootDelay) { + needsUpdate = true + change.BootDelaySecs = int(plan.BootDelay.ValueInt64()) + } + if !plan.EnterBIOSSetup.Equal(state.EnterBIOSSetup) { + needsUpdate = true + change.EnterBIOSSetup = plan.EnterBIOSSetup.ValueBool() + } + if !plan.Disks.Equal(state.Disks) { + needsUpdate = true + var disksFromPlan, disksFromState []VirtualServerDiskModel + resp.Diagnostics.Append(plan.Disks.ElementsAs(ctx, &disksFromPlan, false)...) + resp.Diagnostics.Append(state.Disks.ElementsAs(ctx, &disksFromState, false)...) + + for _, diskFromState := range disksFromState { + diskInPlan := false + for _, diskFromPlan := range disksFromPlan { + if diskFromPlan.ID.Equal(diskFromState.ID) { + diskInPlan = true + + if !diskFromPlan.Type.Equal(diskFromState.Type) || + !diskFromPlan.SizeGB.Equal(diskFromState.SizeGB) { + change.ChangeDisks = append(change.ChangeDisks, vm.Disk{ + ID: int(diskFromPlan.ID.ValueInt64()), + Type: diskFromPlan.Type.ValueString(), + SizeGBs: int(diskFromPlan.SizeGB.ValueInt64()), + }) + } + } + } + + if !diskInPlan { + change.DeleteDiskIDs = append(change.DeleteDiskIDs, int(diskFromState.ID.ValueInt64())) + } + } + + for _, diskFromPlan := range disksFromPlan { + if diskFromPlan.ID.IsUnknown() { + change.AddDisks = append(change.AddDisks, vm.Disk{ + Type: diskFromPlan.Type.ValueString(), + SizeGBs: int(diskFromPlan.SizeGB.ValueInt64()), + }) + } + } + } + + if needsUpdate { + provisioning, err := r.vsphereAPI.Provisioning().VM().Update(ctx, state.ID.ValueString(), change) + if err != nil { + resp.Diagnostics.AddError("error updating vm", err.Error()) + return + } + + if _, err = r.vsphereAPI.Provisioning().Progress().AwaitCompletion(ctx, provisioning.Identifier); err != nil { + resp.Diagnostics.AddError("error waiting for vm update to complete", err.Error()) + return + } + + time.Sleep(time.Minute) // need to wait for guest tools to report data + } + + if diags, notFound := r.setFromInfo(ctx, &plan); notFound { + resp.State.RemoveResource(ctx) + return + } else { + resp.Diagnostics.Append(diags...) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *VirtualServerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state VirtualServerResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + deprovisioning, err := r.vsphereAPI.Provisioning().VM().Deprovision(ctx, state.ID.ValueString(), false) + if utils.IsLegacyClientNotFound(err) { + resp.State.RemoveResource(ctx) + return + } else if err != nil { + resp.Diagnostics.AddError("error deleting vm", err.Error()) + return + } + + _, err = r.vsphereAPI.Provisioning().Progress().AwaitCompletion(ctx, deprovisioning.Identifier) + if err != nil { + resp.Diagnostics.AddError("error awaiting vm deletion", err.Error()) + } +} + +func (r *VirtualServerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + info, err := r.vsphereAPI.Info().Get(ctx, req.ID) + if err != nil { + resp.Diagnostics.AddError("Unable to fetch virtual server", err.Error()) + return + } + + if info.TemplateID == "" { + resp.Diagnostics.AddError( + "Cannot import virtual server with `from_scratch` template", + "Importing virtual servers which have been provisioned with a `from_scratch` template "+ + "is not supported.", + ) + return + } + + resp.Diagnostics.AddWarning( + "Resource Import Considerations", + "Virtual server import does not include 'password' and 'ssh_key' attributes. "+ + "To prevent the virtual server from getting replaced in the next apply, make sure to add "+ + "either 'password' or 'ssh_key' (depending on which attribute is configured) to the 'ignore_changes' attribute "+ + "in the lifecycle block.", + ) + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/internal/provider/virtual_server_resource_schema.go b/internal/provider/virtual_server_resource_schema.go new file mode 100644 index 00000000..6314de36 --- /dev/null +++ b/internal/provider/virtual_server_resource_schema.go @@ -0,0 +1,263 @@ +package provider + +import ( + "context" + + "github.com/anexia-it/terraform-provider-anxcloud/internal/provider/customtypes" + "github.com/anexia-it/terraform-provider-anxcloud/internal/provider/planmodifiers" + "github.com/anexia-it/terraform-provider-anxcloud/internal/provider/validators" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func (r *VirtualServerResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: ` +The virtual_server resource allows you to configure and run virtual machines. + +### Known limitations +- removal of disks not supported +- removal of networks not supported +`, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Virtual server identifier", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "hostname": schema.StringAttribute{ + Required: true, + MarkdownDescription: "Virtual server hostname", + CustomType: customtypes.HostnameStringType{}, + PlanModifiers: []planmodifier.String{ + planmodifiers.KeepStringPrefix(), + stringplanmodifier.RequiresReplace(), + }, + }, + "location_id": schema.StringAttribute{ + Required: true, + MarkdownDescription: "Location identifier", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "template_id": schema.StringAttribute{ + Required: true, + MarkdownDescription: "Template identifier", + }, + "template_type": schema.StringAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: "OS template type", + Default: stringdefault.StaticString("templates"), + Validators: []validator.String{ + stringvalidator.OneOf("templates", "from_scratch"), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + }, + }, + "cpus": schema.Int64Attribute{ + Required: true, + MarkdownDescription: "Number of CPUs", + }, + "cpu_performance_type": schema.StringAttribute{ + CustomType: customtypes.CPUPerformanceTypeStringType{}, + Optional: true, + Computed: true, + MarkdownDescription: "CPU type. Example: (`best-effort`, `standard`, `enterprise`, `performance`), defaults to `standard`.", + Default: stringdefault.StaticString("standard"), + PlanModifiers: []planmodifier.String{ + planmodifiers.KeepStringSuffix(), + }, + }, + "sockets": schema.Int64Attribute{ + Optional: true, + Computed: true, + MarkdownDescription: "Amount of CPU sockets Number of cores have to be a multiple of sockets, as they will be spread evenly across all sockets. " + + "Defaults to number of cores, i.e. one socket per CPU core.", + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + }, + "memory": schema.Int64Attribute{ + Required: true, + MarkdownDescription: "Memory in MB.", + }, + "dns": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "DNS configuration. Maximum items 4. Defaults to template settings.", + Validators: []validator.List{ + listvalidator.SizeAtMost(4), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplaceIfConfigured(), + }, + }, + "password": schema.StringAttribute{ + Optional: true, + Sensitive: true, + MarkdownDescription: "Plaintext password. Example: ('!anx123mySuperStrongPassword123anx!', 'go3ju0la1ro3', …). For systems that support it, we strongly recommend using a SSH key instead.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + }, + }, + "ssh_key": schema.StringAttribute{ + Optional: true, + Sensitive: true, + MarkdownDescription: "Public key (instead of password, only for Linux systems). Recommended over providing a plaintext password.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + }, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.Expressions{ + path.MatchRoot("password"), + }...), + }, + }, + "script": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Script to be executed after provisioning. " + + "Consider the corresponding shebang at the beginning of your script. " + + "If you want to use PowerShell, the first line should be: #ps1_sysnative.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + }, + }, + "boot_delay": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "Boot delay in seconds. Example: (0, 1, …).", + }, + "enter_bios_setup": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Start the VM into BIOS setup on next boot.", + }, + "force_restart_if_needed": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Certain operations may only be performed in powered off state. " + + "Such as: shrinking memory, shrinking/adding CPU, removing disk and scaling a disk beyond 2 GB. " + + "Passing this value as true will always execute a power off and reboot request after completing all other operations. " + + "Without this flag set to true scaling operations requiring a reboot will fail.", + }, + "critical_operation_confirmed": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Confirms a critical operation (if needed). " + + "Potentially dangerous operations (e.g. resulting in data loss) require an additional confirmation. " + + "The parameter is used for VM UPDATE requests.", + }, + + "tags": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Computed: true, + MarkdownDescription: "Set of tags attached to the resource", + }, + }, + Blocks: map[string]schema.Block{ + "disk": r.disksSchema(), + "network": r.networksSchema(), + }, + } +} + +func (r *VirtualServerResource) disksSchema() schema.Block { + return schema.ListNestedBlock{ + MarkdownDescription: "Virtual Server Disk.", + Validators: []validator.List{ + listvalidator.IsRequired(), + listvalidator.SizeAtLeast(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "disk_id": schema.Int64Attribute{ + Computed: true, + MarkdownDescription: "Device identifier of the disk.", + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + }, + "disk_gb": schema.Int64Attribute{ + Required: true, + MarkdownDescription: "Disk capacity in GB.", + }, + "disk_type": schema.StringAttribute{ + Required: true, + MarkdownDescription: "Disk category (limits disk performance, e.g. IOPS).", + }, + }, + }, + } +} + +func (r *VirtualServerResource) networksSchema() schema.Block { + return schema.ListNestedBlock{ + MarkdownDescription: "Network interface.", + Validators: []validator.List{ + listvalidator.IsRequired(), + listvalidator.SizeAtLeast(1), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplaceIf(func(ctx context.Context, req planmodifier.ListRequest, resp *listplanmodifier.RequiresReplaceIfFuncResponse) { + if req.State.Raw.IsNull() { + return + } + + var plan, state []VirtualServerNetworkModel + resp.Diagnostics.Append(req.PlanValue.ElementsAs(ctx, &plan, false)...) + resp.Diagnostics.Append(req.StateValue.ElementsAs(ctx, &state, false)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.RequiresReplace = len(state) != len(plan) + }, "", ""), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "vlan_id": schema.StringAttribute{ + Required: true, + MarkdownDescription: "VLAN identifier.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "nic_type": schema.StringAttribute{ + Required: true, + Description: "Network interface card type.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "ips": schema.ListAttribute{ + Optional: true, + Computed: true, + ElementType: types.StringType, + Validators: []validator.List{ + listvalidator.ValueStringsAre(validators.ValidIPAddress()), + listvalidator.UniqueValues(), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + planmodifiers.KeepIPAddressOrderPlanModifier(), + listplanmodifier.RequiresReplaceIfConfigured(), + }, + MarkdownDescription: "List of IP addresses and identifiers to be assigned and configured.", + }, + }, + }, + } +} diff --git a/internal/provider/virtual_server_resource_test.go b/internal/provider/virtual_server_resource_test.go new file mode 100644 index 00000000..12631236 --- /dev/null +++ b/internal/provider/virtual_server_resource_test.go @@ -0,0 +1,364 @@ +//nolint:unparam +package provider + +import ( + "context" + "fmt" + "regexp" + "strings" + "testing" + + "text/template" + + "github.com/anexia-it/terraform-provider-anxcloud/anxcloud/testutils/environment" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "go.anx.io/go-anxcloud/pkg/client" + "go.anx.io/go-anxcloud/pkg/vsphere" +) + +type virtualServerResourceData struct { + Hostname string + + Template string + TemplateID string + TemplateType string + + Location string + + CPUs int + CPUPerformanceType string + Sockets int + Memory int + DNS *[]string + Script string + + Disks []virtualServerResourceDataDisk + Networks []virtualServerResourceDataNetwork + + ForceRestartIfNeeded bool + CriticalOperationConfirmed bool +} + +type virtualServerResourceDataDisk struct { + SizeGB int + Type string +} + +type virtualServerResourceDataNetwork struct { + VLAN string + IPs []string + NICType string +} + +func (d virtualServerResourceData) toTerraform(location, templateName string) string { + var out strings.Builder + + tmpl := template.Must(template.New("virtual server").Parse(` + data "anxcloud_core_location" "foo" { + code = "{{ .Location }}" + } + + {{ if .Template }} + data "anxcloud_virtual_server_template" "foo" { + name = "{{ .Template }}" + location = data.anxcloud_core_location.foo.id + } + {{ end }} + + resource "anxcloud_virtual_server" "foo" { + hostname = "{{ .Hostname }}" + cpus = {{ .CPUs }} + cpu_performance_type = "{{ .CPUPerformanceType }}" + memory = {{ .Memory }} + + {{ if gt .Sockets 0 }} + sockets = {{ .Sockets }} + {{ end }} + + {{ if .TemplateID }} + template_id = "{{ .TemplateID }}" + {{ else }} + template_id = data.anxcloud_virtual_server_template.foo.id + {{ end }} + template_type = "{{ .TemplateType }}" + + + location_id = data.anxcloud_core_location.foo.id + + {{ range .Disks }} + disk { + disk_gb = {{ .SizeGB }} + disk_type = "{{ .Type }}" + } + {{ end }} + + {{ range .Networks }} + network { + vlan_id = "{{ .VLAN }}" + ips = [ + {{ range .IPs }} + "{{ . }}", + {{ end}} + ] + nic_type = "{{ .NICType }}" + } + {{ end }} + + password = "flatcar#1234$%%" + + {{ if .ForceRestartIfNeeded }} + force_restart_if_needed = true + {{ end }} + + {{ if .CriticalOperationConfirmed }} + critical_operation_confirmed = true + {{ end }} + } + `)) + + if err := tmpl.Execute(&out, d); err != nil { + panic(err) + } + + return out.String() +} + +func TestAccVirtualServerResource(t *testing.T) { + environment.SkipIfNoEnvironment(t) + envInfo := environment.GetEnvInfo(t) + + config := virtualServerResourceData{ + Hostname: fmt.Sprintf("terraform-test-%s", envInfo.TestRunName), + Location: "ANX04", + CPUs: 4, + CPUPerformanceType: "standard", + Memory: 2048, + Template: "Debian 11", + TemplateType: "templates", + Disks: []virtualServerResourceDataDisk{ + {SizeGB: 20, Type: "STD4"}, + {SizeGB: 15, Type: "STD2"}, + }, + Networks: []virtualServerResourceDataNetwork{ + { + VLAN: envInfo.VlanID, + NICType: "vmxnet3", + IPs: []string{ + envInfo.Prefix.GetNextIP(), + envInfo.Prefix.GetNextIP(), + }, + }, + }, + } + + changedConfig := config + changedConfig.CPUs = 2 + changedConfig.Sockets = 2 + changedConfig.Memory = 4096 + changedConfig.Disks = append(config.Disks, virtualServerResourceDataDisk{ + SizeGB: 30, + Type: "ENT2", + }) + + changedConfigAllowCriticalAndRestart := config + changedConfigAllowCriticalAndRestart.ForceRestartIfNeeded = true + changedConfigAllowCriticalAndRestart.CriticalOperationConfirmed = true + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: config.toTerraform("ANX04", "Debian 11"), + Check: testAccCheckVirtualServerResourceExists(t, "anxcloud_virtual_server.foo", config), + }, + { + Config: changedConfig.toTerraform("ANX04", "Debian 11"), + Check: testAccCheckVirtualServerResourceExists(t, "anxcloud_virtual_server.foo", changedConfig), + ExpectError: regexp.MustCompile("VM has to be powered off"), + }, + { + Config: changedConfigAllowCriticalAndRestart.toTerraform("ANX04", "Debian 11"), + Check: testAccCheckVirtualServerResourceExists(t, "anxcloud_virtual_server.foo", changedConfigAllowCriticalAndRestart), + }, + { + ResourceName: "anxcloud_virtual_server.foo", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "hostname", // implements semantic equality (not covered by import state verification) + "cpu_performance_type", // implements semantic equality (not covered by import state verification) + "password", // field is not returned by API + "critical_operation_confirmed", // field is only used for resource updates + "force_restart_if_needed", // field is only used for resource updates + }, + }, + }, + }) +} + +func TestAccVirtualServerResourceFromScratch(t *testing.T) { + environment.SkipIfNoEnvironment(t) + envInfo := environment.GetEnvInfo(t) + + config := virtualServerResourceData{ + Hostname: fmt.Sprintf("terraform-test-%s-from-scratch", envInfo.TestRunName), + Location: "ANX04", + CPUs: 4, + CPUPerformanceType: "standard", + Memory: 2048, + TemplateID: "114", + TemplateType: "from_scratch", + Disks: []virtualServerResourceDataDisk{ + {SizeGB: 20, Type: "STD4"}, + {SizeGB: 15, Type: "STD2"}, + }, + Networks: []virtualServerResourceDataNetwork{ + { + VLAN: envInfo.VlanID, + NICType: "vmxnet3", + IPs: []string{ + envInfo.Prefix.GetNextIP(), + envInfo.Prefix.GetNextIP(), + }, + }, + }, + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: config.toTerraform("ANX04", "Debian 11"), + Check: testAccCheckVirtualServerResourceExists(t, "anxcloud_virtual_server.foo", config), + }, + { + ResourceName: "anxcloud_virtual_server.foo", + ImportState: true, + ExpectError: regexp.MustCompile("Cannot import virtual server with `from_scratch` template"), + }, + }, + }) +} + +func testClient(t *testing.T) client.Client { + t.Helper() + client, err := client.New(client.AuthFromEnv(false)) + if err != nil { + t.Error(err) + } + + return client +} + +func testAccCheckVirtualServerResourceExists(t *testing.T, n string, config virtualServerResourceData) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("virtual server not found: %s", n) + } + if rs.Primary.ID == "" { + return fmt.Errorf("virtual server id not set") + } + + vsphereAPI := vsphere.NewAPI(testClient(t)) + info, err := vsphereAPI.Info().Get(context.TODO(), rs.Primary.ID) + if err != nil { + return err + } + + if !strings.HasSuffix(info.Name, config.Hostname) { + return fmt.Errorf("configured virtual machine hostname is not a suffix of actual hostname, got %s - expected %s", info.Name, config.Hostname) + } + if info.CPU != config.CPUs { + return fmt.Errorf("virtual machine cpu does not match, got %d - expected %d", info.CPU, config.CPUs) + } + if info.RAM != config.Memory { + return fmt.Errorf("virtual machine cpu does not match, got %d - expected %d", info.CPU, config.CPUs) + } + if !strings.HasPrefix(info.CPUPerformanceType, config.CPUPerformanceType) { + return fmt.Errorf("cpu_performance_type does not match") + } + + if len(info.DiskInfo) != len(config.Disks) { + return fmt.Errorf("unexpected number of disks, got %d - expected %d", len(info.DiskInfo), len(config.Disks)) + } + for i := range info.DiskInfo { + if int(info.DiskInfo[i].DiskGB) != config.Disks[i].SizeGB { + return fmt.Errorf("unexpected disk size for disk with index %d, got %d - expected %d", i, int(info.DiskInfo[i].DiskGB), config.Disks[i].SizeGB) + } + if info.DiskInfo[i].DiskType != config.Disks[i].Type { + return fmt.Errorf("unexpected disk type for disk with index %d, got %q - expected %q", i, info.DiskInfo[i].DiskType, config.Disks[i].Type) + } + } + + if len(info.Network) != len(config.Networks) { + return fmt.Errorf("unexpected number of networks, got %d - expected %d", len(info.Network), len(config.Networks)) + } + for i := range info.Network { + if info.Network[i].VLAN != config.Networks[i].VLAN { + return fmt.Errorf("unexpected disk size for disk with index %d, got %d - expected %d", i, int(info.DiskInfo[i].DiskGB), config.Disks[i].SizeGB) + } + // todo: check ips and nictype + } + + return nil + } +} + +// WIP (provisioning.Create does not send content-type header which makes ghttp unhappy) +// var _ = Describe("Virtual Server resource", func() { +// var server *ghttp.Server + +// BeforeEach(func() { +// server = ghttp.NewServer() +// }) + +// AfterEach(func() { +// server.Close() +// }) + +// It("foo", func() { +// resource.Test(GinkgoT(), resource.TestCase{ +// IsUnitTest: true, +// ProtoV6ProviderFactories: testAccProtoV6MockProviderFactories(server.URL()), +// Steps: []resource.TestStep{ +// { +// PreConfig: func() { +// server.AppendHandlers(ghttp.CombineHandlers( +// ghttp.VerifyRequest("POST", "/api/vsphere/v1/provisioning/vm.json/test-location-id/templates/test-template-id"), +// ghttp.VerifyJSONRepresenting(map[string]any{ +// "hostname": "test-hostname", +// "cpus": 2, +// "cpu_performance_type": "standard", +// "memory_mb": 1024, +// "disk_gb": 5, +// "disk_type": "STD4", +// }), +// ghttp.VerifyMimeType(""), +// ghttp.RespondWithJSONEncoded(200, map[string]any{ +// "identifier": "fake-task-id", +// }), +// )) +// }, +// Config: ` +// resource "anxcloud_vm" "foo" { +// hostname = "test-hostname" +// location_id = "test-location-id" +// template_id = "test-template-id" +// cpus = 2 +// memory = 1024 + +// disk { +// disk_gb = 5 +// disk_type = "STD4" +// } +// } +// `, +// }, +// }, +// }) +// }) +// }) diff --git a/internal/provider/virtual_server_template_data_source.go b/internal/provider/virtual_server_template_data_source.go new file mode 100644 index 00000000..62242cce --- /dev/null +++ b/internal/provider/virtual_server_template_data_source.go @@ -0,0 +1,130 @@ +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + "go.anx.io/go-anxcloud/pkg/api" + corev1 "go.anx.io/go-anxcloud/pkg/apis/core/v1" + vspherev1 "go.anx.io/go-anxcloud/pkg/apis/vsphere/v1" +) + +var _ datasource.DataSource = &VirtualServerTemplateDataSource{} +var _ datasource.DataSourceWithConfigure = &VirtualServerTemplateDataSource{} +var _ datasource.DataSourceWithConfigValidators = &VirtualServerTemplateDataSource{} + +func NewVirtuaServerTemplateDataSource() datasource.DataSource { + return &VirtualServerTemplateDataSource{} +} + +type VirtualServerTemplateDataSource struct { + engine api.API +} + +func (*VirtualServerTemplateDataSource) ConfigValidators(context.Context) []datasource.ConfigValidator { + return []datasource.ConfigValidator{ + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("id"), + path.MatchRoot("name"), + ), + datasourcevalidator.Conflicting( + path.MatchRoot("id"), + path.MatchRoot("build"), + ), + } +} + +type VirtualServerTemplateDataSourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Build types.String `tfsdk:"build"` + Location types.String `tfsdk:"location"` +} + +func (ds *VirtualServerTemplateDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + ds.engine = req.ProviderData.(providerConfiguration).engine +} + +func (ds *VirtualServerTemplateDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_virtual_server_template" +} + +func (ds *VirtualServerTemplateDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Retrieves a virtual server template. Can be used to resolve a template ID by name, which is needed for creating anxcloud_virtual_server resources. " + + "This datasource does not support 'from_scratch' templates!", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + "location": schema.StringAttribute{ + MarkdownDescription: "Datacenter location identifier.", + Required: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Template name.", + Optional: true, + Computed: true, + }, + "build": schema.StringAttribute{ + MarkdownDescription: "Template build.", + Optional: true, + Computed: true, + }, + }, + } +} + +func (ds *VirtualServerTemplateDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data VirtualServerTemplateDataSourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + template := vspherev1.Template{ + Identifier: data.ID.ValueString(), + Type: vspherev1.TypeTemplate, + Location: corev1.Location{Identifier: data.Location.ValueString()}, + } + + if !data.ID.IsNull() { + if err := ds.engine.Get(ctx, &template); err != nil { + resp.Diagnostics.AddError("Could not find named template", err.Error()) + return + } + } else { + tpl, err := vspherev1.FindNamedTemplate( + ctx, + ds.engine, + data.Name.ValueString(), + data.Build.ValueString(), + corev1.Location{Identifier: data.Location.ValueString()}, + ) + if err != nil { + resp.Diagnostics.AddError("Could not find named template", err.Error()) + return + } + + template = *tpl + } + + data = VirtualServerTemplateDataSourceModel{ + ID: types.StringValue(template.Identifier), + Name: types.StringValue(template.Name), + Build: types.StringValue(template.Build), + Location: data.Location, + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/provider/virtual_server_utils.go b/internal/provider/virtual_server_utils.go new file mode 100644 index 00000000..34225da7 --- /dev/null +++ b/internal/provider/virtual_server_utils.go @@ -0,0 +1,143 @@ +package provider + +import ( + "context" + "errors" + "fmt" + "math" + + "github.com/anexia-it/terraform-provider-anxcloud/internal/provider/customtypes" + "github.com/anexia-it/terraform-provider-anxcloud/internal/utils" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "go.anx.io/go-anxcloud/pkg/vsphere/info" + "go.anx.io/go-anxcloud/pkg/vsphere/provisioning/nictype" +) + +func (r *VirtualServerResource) setFromInfo(ctx context.Context, data *VirtualServerResourceModel) (diags diag.Diagnostics, notFound bool) { + info, err := r.vsphereAPI.Info().Get(ctx, data.ID.ValueString()) + if utils.IsLegacyClientNotFound(err) { + return nil, true + } else if err != nil { + diags.AddError("failed reading vm info", err.Error()) + return diags, false + } + + var ( + templateType types.String + + template = data.Template + networks = data.Networks + ) + + // if template type == "templates" -> use data from info endpoint + // else -> use local data + if info.TemplateID != "" { + template = types.StringValue(info.TemplateID) + templateType = types.StringValue("templates") + diags.Append(r.toNetworkList(ctx, info.Network, &networks)...) + } else { + templateType = types.StringValue("from_scratch") + } + + *data = VirtualServerResourceModel{ + ID: types.StringValue(info.Identifier), + Hostname: customtypes.HostnameValue(info.Name), + Template: template, + TemplateType: templateType, + Location: types.StringValue(info.LocationID), + CPUs: types.Int64Value(int64(info.CPU)), + CPUPerformanceType: customtypes.CPUPerformanceTypeValue(info.CPUPerformanceType), + CPUSockets: types.Int64Value(int64(info.CPU / info.Cores)), + Memory: types.Int64Value(int64(info.RAM)), + Networks: networks, + + // not returned by API -> take over from state + DNS: data.DNS, + Password: data.Password, + SSH: data.SSH, + Script: data.Script, + BootDelay: data.BootDelay, + EnterBIOSSetup: data.EnterBIOSSetup, + CriticalOperationConfirmed: data.CriticalOperationConfirmed, + ForceRestartIfNeeded: data.ForceRestartIfNeeded, + } + + diags.Append(readTags(ctx, r.engine, info.Identifier, &data.Tags)...) + + diags.Append(r.toDiskList(ctx, info.DiskInfo, &data.Disks)...) + + return diags, false +} + +func (r *VirtualServerResource) toDiskList(ctx context.Context, infoDisks []info.DiskInfo, list *types.List) (diags diag.Diagnostics) { + var listDisks []VirtualServerDiskModel + + for _, disk := range infoDisks { + listDisks = append(listDisks, VirtualServerDiskModel{ + ID: types.Int64Value(int64(disk.DiskID)), + SizeGB: types.Int64Value(int64(math.Round(disk.DiskGB))), + Type: types.StringValue(disk.DiskType), + }) + } + + *list, diags = types.ListValueFrom(ctx, r.disksSchema().GetNestedObject().Type(), listDisks) + return diags +} + +func (r *VirtualServerResource) toNetworkList(ctx context.Context, infoNetworks []info.Network, list *types.List) (diags diag.Diagnostics) { + var prevNetworkList []VirtualServerNetworkModel + diags.Append(list.ElementsAs(ctx, &prevNetworkList, false)...) + + var networkList []VirtualServerNetworkModel + + for i, network := range infoNetworks { + nicType, err := nicTypeFromID(ctx, r.nicTypeAPI, network.NIC) + if err != nil { + diags.AddError("unknown nic type", err.Error()) + } + + ips := append(network.IPv4, network.IPv6...) + // order is not stable -> if previous state contains same elements, use that to prevent inconsitency errors + if len(prevNetworkList) > i { + var prevIPs []string + diags.Append(prevNetworkList[i].IPs.ElementsAs(ctx, &prevIPs, true)...) + if cmp.Diff(ips, prevIPs, cmpopts.SortSlices(func(a, b string) bool { return a < b })) == "" { + ips = prevIPs + } + } + + ipList, ipListDiags := types.ListValueFrom(ctx, types.StringType, ips) + diags.Append(ipListDiags...) + + networkList = append(networkList, VirtualServerNetworkModel{ + VLAN: types.StringValue(network.VLAN), + NICType: types.StringValue(nicType), + IPs: ipList, + }) + } + + if diags.HasError() { + return diags + } + + *list, diags = types.ListValueFrom(ctx, r.networksSchema().GetNestedObject().Type(), networkList) + return diags +} + +func nicTypeFromID(ctx context.Context, nicTypeAPI nictype.API, nicTypeID int) (string, error) { + nicTypeIndex := nicTypeID - 1 + + types, err := nicTypeAPI.List(ctx) + if err != nil { + return "", fmt.Errorf("fetch available nic types: %w", err) + } + + if nicTypeIndex < 0 || nicTypeIndex >= len(types) { + return "", errors.New("nic type not found") + } + + return types[nicTypeIndex], nil +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 00000000..7a90a917 --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,16 @@ +package utils + +import ( + "errors" + "net/http" + + "go.anx.io/go-anxcloud/pkg/client" +) + +func IsLegacyClientNotFound(err error) bool { + var respErr *client.ResponseError + if errors.As(err, &respErr) && respErr.ErrorData.Code == http.StatusNotFound { + return true + } + return false +}