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
+}