diff --git a/example/provider_configuration.tf b/example/provider.tf similarity index 100% rename from example/provider_configuration.tf rename to example/provider.tf diff --git a/example/resource_virtual_environment_container.tf b/example/resource_virtual_environment_container.tf new file mode 100644 index 00000000..216ea59b --- /dev/null +++ b/example/resource_virtual_environment_container.tf @@ -0,0 +1,40 @@ +resource "proxmox_virtual_environment_container" "example" { + description = "Managed by Terraform" + + initialization { + dns { + server = "1.1.1.1" + } + + hostname = "terraform-provider-proxmox-example-lxc" + + ip_config { + ipv4 { + address = "dhcp" + } + } + + user_account { + keys = ["${trimspace(tls_private_key.example.public_key_openssh)}"] + password = "example" + } + } + + network_interface { + name = "veth0" + } + + node_name = "${data.proxmox_virtual_environment_nodes.example.names[0]}" + + operating_system { + template_file_id = "${proxmox_virtual_environment_file.ubuntu_container_template.id}" + type = "ubuntu" + } + + pool_id = "${proxmox_virtual_environment_pool.example.id}" + vm_id = 2039 +} + +output "resource_proxmox_virtual_environment_container_example_id" { + value = "${proxmox_virtual_environment_container.example.id}" +} diff --git a/example/resource_virtual_environment_file.tf b/example/resource_virtual_environment_file.tf index 9cd0333c..d233f57c 100644 --- a/example/resource_virtual_environment_file.tf +++ b/example/resource_virtual_environment_file.tf @@ -1,12 +1,6 @@ -resource "proxmox_virtual_environment_file" "ubuntu_cloud_image" { - content_type = "iso" - datastore_id = "${element(data.proxmox_virtual_environment_datastores.example.datastore_ids, index(data.proxmox_virtual_environment_datastores.example.datastore_ids, "local"))}" - node_name = "${data.proxmox_virtual_environment_datastores.example.node_name}" - - source_file { - path = "https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img" - } -} +#=============================================================================== +# Cloud Config (cloud-init) +#=============================================================================== resource "proxmox_virtual_environment_file" "cloud_config" { content_type = "snippets" @@ -37,6 +31,20 @@ users: } } +#=============================================================================== +# Ubuntu Cloud Image +#=============================================================================== + +resource "proxmox_virtual_environment_file" "ubuntu_cloud_image" { + content_type = "iso" + datastore_id = "${element(data.proxmox_virtual_environment_datastores.example.datastore_ids, index(data.proxmox_virtual_environment_datastores.example.datastore_ids, "local"))}" + node_name = "${data.proxmox_virtual_environment_datastores.example.node_name}" + + source_file { + path = "https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img" + } +} + output "resource_proxmox_virtual_environment_file_ubuntu_cloud_image_content_type" { value = "${proxmox_virtual_environment_file.ubuntu_cloud_image.content_type}" } @@ -72,3 +80,53 @@ output "resource_proxmox_virtual_environment_file_ubuntu_cloud_image_node_name" output "resource_proxmox_virtual_environment_file_ubuntu_cloud_image_source_file" { value = "${proxmox_virtual_environment_file.ubuntu_cloud_image.source_file}" } + +#=============================================================================== +# Ubuntu Container Template +#=============================================================================== + +resource "proxmox_virtual_environment_file" "ubuntu_container_template" { + content_type = "vztmpl" + datastore_id = "${element(data.proxmox_virtual_environment_datastores.example.datastore_ids, index(data.proxmox_virtual_environment_datastores.example.datastore_ids, "local"))}" + node_name = "${data.proxmox_virtual_environment_datastores.example.node_name}" + + source_file { + path = "http://download.proxmox.com/images/system/ubuntu-18.04-standard_18.04.1-1_amd64.tar.gz" + } +} + +output "resource_proxmox_virtual_environment_file_ubuntu_container_template_content_type" { + value = "${proxmox_virtual_environment_file.ubuntu_container_template.content_type}" +} + +output "resource_proxmox_virtual_environment_file_ubuntu_container_template_datastore_id" { + value = "${proxmox_virtual_environment_file.ubuntu_container_template.datastore_id}" +} + +output "resource_proxmox_virtual_environment_file_ubuntu_container_template_file_modification_date" { + value = "${proxmox_virtual_environment_file.ubuntu_container_template.file_modification_date}" +} + +output "resource_proxmox_virtual_environment_file_ubuntu_container_template_file_name" { + value = "${proxmox_virtual_environment_file.ubuntu_container_template.file_name}" +} + +output "resource_proxmox_virtual_environment_file_ubuntu_container_template_file_size" { + value = "${proxmox_virtual_environment_file.ubuntu_container_template.file_size}" +} + +output "resource_proxmox_virtual_environment_file_ubuntu_container_template_file_tag" { + value = "${proxmox_virtual_environment_file.ubuntu_container_template.file_tag}" +} + +output "resource_proxmox_virtual_environment_file_ubuntu_container_template_id" { + value = "${proxmox_virtual_environment_file.ubuntu_container_template.id}" +} + +output "resource_proxmox_virtual_environment_file_ubuntu_container_template_node_name" { + value = "${proxmox_virtual_environment_file.ubuntu_container_template.node_name}" +} + +output "resource_proxmox_virtual_environment_file_ubuntu_container_template_source_file" { + value = "${proxmox_virtual_environment_file.ubuntu_container_template.source_file}" +} diff --git a/example/resource_virtual_environment_vm.tf b/example/resource_virtual_environment_vm.tf index e79f7402..ab5fd8b8 100644 --- a/example/resource_virtual_environment_vm.tf +++ b/example/resource_virtual_environment_vm.tf @@ -58,21 +58,6 @@ resource "proxmox_virtual_environment_vm" "example" { } } -resource "local_file" "example_ssh_private_key" { - filename = "${path.module}/autogenerated/id_rsa" - sensitive_content = "${tls_private_key.example.private_key_pem}" -} - -resource "local_file" "example_ssh_public_key" { - filename = "${path.module}/autogenerated/id_rsa.pub" - sensitive_content = "${tls_private_key.example.public_key_openssh}" -} - -resource "tls_private_key" "example" { - algorithm = "RSA" - rsa_bits = 2048 -} - output "resource_proxmox_virtual_environment_vm_example_id" { value = "${proxmox_virtual_environment_vm.example.id}" } diff --git a/example/ssh.tf b/example/ssh.tf new file mode 100644 index 00000000..2e284394 --- /dev/null +++ b/example/ssh.tf @@ -0,0 +1,14 @@ +resource "local_file" "example_ssh_private_key" { + filename = "${path.module}/autogenerated/id_rsa" + sensitive_content = "${tls_private_key.example.private_key_pem}" +} + +resource "local_file" "example_ssh_public_key" { + filename = "${path.module}/autogenerated/id_rsa.pub" + sensitive_content = "${tls_private_key.example.public_key_openssh}" +} + +resource "tls_private_key" "example" { + algorithm = "RSA" + rsa_bits = 2048 +} diff --git a/go.mod b/go.mod index 09d10d03..48e1a5a3 100644 --- a/go.mod +++ b/go.mod @@ -8,4 +8,5 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/pkg/sftp v1.10.1 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 + google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 ) diff --git a/proxmox/virtual_environment_container.go b/proxmox/virtual_environment_container.go index c9bfaab4..0d5602f7 100644 --- a/proxmox/virtual_environment_container.go +++ b/proxmox/virtual_environment_container.go @@ -110,3 +110,33 @@ func (c *VirtualEnvironmentClient) WaitForContainerState(nodeName string, vmID i return fmt.Errorf("Timeout while waiting for container \"%d\" to enter the state \"%s\"", vmID, state) } + +// WaitForContainerLock waits for a container lock to be released. +func (c *VirtualEnvironmentClient) WaitForContainerLock(nodeName string, vmID int, timeout int, delay int) error { + timeDelay := int64(delay) + timeMax := float64(timeout) + timeStart := time.Now() + timeElapsed := timeStart.Sub(timeStart) + + for timeElapsed.Seconds() < timeMax { + if int64(timeElapsed.Seconds())%timeDelay == 0 { + data, err := c.GetContainerStatus(nodeName, vmID) + + if err != nil { + return err + } + + if data.Lock == nil || *data.Lock == "" { + return nil + } + + time.Sleep(1 * time.Second) + } + + time.Sleep(200 * time.Millisecond) + + timeElapsed = time.Now().Sub(timeStart) + } + + return fmt.Errorf("Timeout while waiting for container \"%d\" to become unlocked", vmID) +} diff --git a/proxmox/virtual_environment_container_types.go b/proxmox/virtual_environment_container_types.go index 5fe09a4a..6cca097f 100644 --- a/proxmox/virtual_environment_container_types.go +++ b/proxmox/virtual_environment_container_types.go @@ -34,7 +34,7 @@ type VirtualEnvironmentContainerCreateRequestBody struct { Lock *string `json:"lock,omitempty" url:"lock,omitempty,int"` MountPoints VirtualEnvironmentContainerCustomMountPointArray `json:"mp,omitempty" url:"mp,omitempty,numbered"` NetworkInterfaces VirtualEnvironmentContainerCustomNetworkInterfaceArray `json:"net,omitempty" url:"net,omitempty,numbered"` - OSTemplateFileVolume string `json:"ostemplate" url:"ostemplate"` + OSTemplateFileVolume *string `json:"ostemplate,omitempty" url:"ostemplate,omitempty"` OSType *string `json:"ostype,omitempty" url:"ostype,omitempty"` Password *string `json:"password,omitempty" url:"password,omitempty"` PoolID *string `json:"pool,omitempty" url:"pool,omitempty"` @@ -51,7 +51,7 @@ type VirtualEnvironmentContainerCreateRequestBody struct { TTY *int `json:"tty,omitempty" url:"tty,omitempty"` Unique *CustomBool `json:"unique,omitempty" url:"unique,omitempty,int"` Unprivileged *CustomBool `json:"unprivileged,omitempty" url:"unprivileged,omitempty,int"` - VMID int `json:"vmid" url:"vmid"` + VMID *int `json:"vmid,omitempty" url:"vmid,omitempty"` } // VirtualEnvironmentContainerCustomFeatures contains the values for the "features" property. @@ -66,7 +66,7 @@ type VirtualEnvironmentContainerCustomFeatures struct { type VirtualEnvironmentContainerCustomMountPoint struct { ACL *CustomBool `json:"acl,omitempty" url:"acl,omitempty,int"` Backup *CustomBool `json:"backup,omitempty" url:"backup,omitempty,int"` - DiskSize *int `json:"size,omitempty" url:"size,omitempty"` + DiskSize *string `json:"size,omitempty" url:"size,omitempty"` Enabled bool `json:"-" url:"-"` MountOptions *[]string `json:"mountoptions,omitempty" url:"mountoptions,omitempty"` MountPoint string `json:"mp" url:"mp"` @@ -92,7 +92,7 @@ type VirtualEnvironmentContainerCustomNetworkInterface struct { MACAddress *string `json:"hwaddr,omitempty" url:"hwaddr,omitempty"` MTU *int `json:"mtu,omitempty" url:"mtu,omitempty"` Name string `json:"name" url:"name"` - RateLimit *int `json:"rate,omitempty" url:"rate,omitempty"` + RateLimit *float64 `json:"rate,omitempty" url:"rate,omitempty"` Tag *int `json:"tag,omitempty" url:"tag,omitempty"` Trunks *[]int `json:"trunks,omitempty" url:"trunks,omitempty"` Type *string `json:"type,omitempty" url:"type,omitempty"` @@ -104,7 +104,7 @@ type VirtualEnvironmentContainerCustomNetworkInterfaceArray []VirtualEnvironment // VirtualEnvironmentContainerCustomRootFS contains the values for the "rootfs" property. type VirtualEnvironmentContainerCustomRootFS struct { ACL *CustomBool `json:"acl,omitempty" url:"acl,omitempty,int"` - DiskSize *int `json:"size,omitempty" url:"size,omitempty"` + DiskSize *string `json:"size,omitempty" url:"size,omitempty"` MountOptions *[]string `json:"mountoptions,omitempty" url:"mountoptions,omitempty"` Quota *CustomBool `json:"quota,omitempty" url:"quota,omitempty,int"` ReadOnly *CustomBool `json:"ro,omitempty" url:"ro,omitempty,int"` @@ -177,16 +177,16 @@ type VirtualEnvironmentContainerGetStatusResponseBody struct { // VirtualEnvironmentContainerGetStatusResponseData contains the data from a container get status response. type VirtualEnvironmentContainerGetStatusResponseData struct { - CPUCount *float64 `json:"cpus,omitempty"` - Lock *string `json:"lock,omitempty"` - MemoryAllocation *int `json:"maxmem,omitempty"` - Name *string `json:"name,omitempty"` - RootDiskSize *int `json:"maxdisk,omitempty"` - Status string `json:"status,omitempty"` - SwapAllocation *int `json:"maxswap,omitempty"` - Tags *string `json:"tags,omitempty"` - Uptime *int `json:"uptime,omitempty"` - VMID string `json:"vmid,omitempty"` + CPUCount *float64 `json:"cpus,omitempty"` + Lock *string `json:"lock,omitempty"` + MemoryAllocation *int `json:"maxmem,omitempty"` + Name *string `json:"name,omitempty"` + RootDiskSize *interface{} `json:"maxdisk,omitempty"` + Status string `json:"status,omitempty"` + SwapAllocation *int `json:"maxswap,omitempty"` + Tags *string `json:"tags,omitempty"` + Uptime *int `json:"uptime,omitempty"` + VMID string `json:"vmid,omitempty"` } // VirtualEnvironmentContainerRebootRequestBody contains the body for a container reboot request. @@ -265,7 +265,7 @@ func (r VirtualEnvironmentContainerCustomMountPoint) EncodeValues(key string, v } if r.DiskSize != nil { - values = append(values, fmt.Sprintf("size=%d", *r.DiskSize)) + values = append(values, fmt.Sprintf("size=%s", *r.DiskSize)) } if r.MountOptions != nil { @@ -317,6 +317,15 @@ func (r VirtualEnvironmentContainerCustomMountPoint) EncodeValues(key string, v return nil } +// EncodeValues converts a VirtualEnvironmentContainerCustomMountPointArray array to multiple URL values. +func (r VirtualEnvironmentContainerCustomMountPointArray) EncodeValues(key string, v *url.Values) error { + for i, d := range r { + d.EncodeValues(fmt.Sprintf("%s%d", key, i), v) + } + + return nil +} + // EncodeValues converts a VirtualEnvironmentContainerCustomNetworkInterface struct to a URL vlaue. func (r VirtualEnvironmentContainerCustomNetworkInterface) EncodeValues(key string, v *url.Values) error { values := []string{} @@ -360,7 +369,7 @@ func (r VirtualEnvironmentContainerCustomNetworkInterface) EncodeValues(key stri values = append(values, fmt.Sprintf("name=%s", r.Name)) if r.RateLimit != nil { - values = append(values, fmt.Sprintf("rate=%d", *r.RateLimit)) + values = append(values, fmt.Sprintf("rate=%.2f", *r.RateLimit)) } if r.Tag != nil { @@ -388,6 +397,15 @@ func (r VirtualEnvironmentContainerCustomNetworkInterface) EncodeValues(key stri return nil } +// EncodeValues converts a VirtualEnvironmentContainerCustomNetworkInterfaceArray array to multiple URL values. +func (r VirtualEnvironmentContainerCustomNetworkInterfaceArray) EncodeValues(key string, v *url.Values) error { + for i, d := range r { + d.EncodeValues(fmt.Sprintf("%s%d", key, i), v) + } + + return nil +} + // EncodeValues converts a VirtualEnvironmentContainerCustomRootFS struct to a URL vlaue. func (r VirtualEnvironmentContainerCustomRootFS) EncodeValues(key string, v *url.Values) error { values := []string{} @@ -401,7 +419,7 @@ func (r VirtualEnvironmentContainerCustomRootFS) EncodeValues(key string, v *url } if r.DiskSize != nil { - values = append(values, fmt.Sprintf("size=%d", *r.DiskSize)) + values = append(values, fmt.Sprintf("size=%s", *r.DiskSize)) } if r.MountOptions != nil { @@ -453,7 +471,7 @@ func (r VirtualEnvironmentContainerCustomRootFS) EncodeValues(key string, v *url // EncodeValues converts a VirtualEnvironmentContainerCustomSSHKeys array to a URL vlaue. func (r VirtualEnvironmentContainerCustomSSHKeys) EncodeValues(key string, v *url.Values) error { - v.Add(key, strings.ReplaceAll(url.QueryEscape(strings.Join(r, "\n")), "+", "%20")) + v.Add(key, strings.Join(r, "\n")) return nil } @@ -538,7 +556,7 @@ func (r *VirtualEnvironmentContainerCustomMountPoint) UnmarshalJSON(b []byte) er v := strings.Split(strings.TrimSpace(p), "=") if len(v) == 1 { - r.Volume = v[1] + r.Volume = v[0] } else if len(v) == 2 { switch v[0] { case "acl": @@ -570,13 +588,7 @@ func (r *VirtualEnvironmentContainerCustomMountPoint) UnmarshalJSON(b []byte) er bv := CustomBool(v[1] == "1") r.Shared = &bv case "size": - iv, err := strconv.Atoi(v[1]) - - if err != nil { - return err - } - - r.DiskSize = &iv + r.DiskSize = &v[1] } } } @@ -600,7 +612,7 @@ func (r *VirtualEnvironmentContainerCustomNetworkInterface) UnmarshalJSON(b []by v := strings.Split(strings.TrimSpace(p), "=") if len(v) == 1 { - r.Name = v[1] + r.Name = v[0] } else if len(v) == 2 { switch v[0] { case "bridge": @@ -629,13 +641,13 @@ func (r *VirtualEnvironmentContainerCustomNetworkInterface) UnmarshalJSON(b []by case "name": r.Name = v[1] case "rate": - iv, err := strconv.Atoi(v[1]) + fv, err := strconv.ParseFloat(v[1], 64) if err != nil { return err } - r.RateLimit = &iv + r.RateLimit = &fv case "tag": iv, err := strconv.Atoi(v[1]) @@ -687,7 +699,7 @@ func (r *VirtualEnvironmentContainerCustomRootFS) UnmarshalJSON(b []byte) error v := strings.Split(strings.TrimSpace(p), "=") if len(v) == 1 { - r.Volume = v[1] + r.Volume = v[0] } else if len(v) == 2 { switch v[0] { case "acl": @@ -714,13 +726,7 @@ func (r *VirtualEnvironmentContainerCustomRootFS) UnmarshalJSON(b []byte) error bv := CustomBool(v[1] == "1") r.Shared = &bv case "size": - iv, err := strconv.Atoi(v[1]) - - if err != nil { - return err - } - - r.DiskSize = &iv + r.DiskSize = &v[1] } } } diff --git a/proxmoxtf/provider.go b/proxmoxtf/provider.go index 2b07db75..43d2689c 100644 --- a/proxmoxtf/provider.go +++ b/proxmoxtf/provider.go @@ -46,6 +46,7 @@ func Provider() *schema.Provider { }, ResourcesMap: map[string]*schema.Resource{ "proxmox_virtual_environment_certificate": resourceVirtualEnvironmentCertificate(), + "proxmox_virtual_environment_container": resourceVirtualEnvironmentContainer(), "proxmox_virtual_environment_dns": resourceVirtualEnvironmentDNS(), "proxmox_virtual_environment_file": resourceVirtualEnvironmentFile(), "proxmox_virtual_environment_group": resourceVirtualEnvironmentGroup(), diff --git a/proxmoxtf/resource_virtual_environment_container.go b/proxmoxtf/resource_virtual_environment_container.go index 2d25c239..84a79713 100644 --- a/proxmoxtf/resource_virtual_environment_container.go +++ b/proxmoxtf/resource_virtual_environment_container.go @@ -5,6 +5,7 @@ package proxmoxtf import ( + "fmt" "strconv" "strings" @@ -77,7 +78,7 @@ const ( mkResourceVirtualEnvironmentContainerMemory = "memory" mkResourceVirtualEnvironmentContainerMemoryDedicated = "dedicated" mkResourceVirtualEnvironmentContainerMemorySwap = "swap" - mkResourceVirtualEnvironmentContainerNetworkInterface = "network_device" + mkResourceVirtualEnvironmentContainerNetworkInterface = "network_interface" mkResourceVirtualEnvironmentContainerNetworkInterfaceBridge = "bridge" mkResourceVirtualEnvironmentContainerNetworkInterfaceEnabled = "enabled" mkResourceVirtualEnvironmentContainerNetworkInterfaceMACAddress = "mac_address" @@ -324,7 +325,7 @@ func resourceVirtualEnvironmentContainer() *schema.Resource { Sensitive: true, Default: dvResourceVirtualEnvironmentContainerInitializationUserAccountPassword, DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - return strings.ReplaceAll(old, "*", "") == "" + return len(old) > 0 && strings.ReplaceAll(old, "*", "") == "" }, }, }, @@ -459,7 +460,7 @@ func resourceVirtualEnvironmentContainer() *schema.Resource { }, mkResourceVirtualEnvironmentContainerPoolID: { Type: schema.TypeString, - Description: "The ID of the pool to assign the virtual machine to", + Description: "The ID of the pool to assign the container to", Optional: true, ForceNew: true, Default: dvResourceVirtualEnvironmentContainerPoolID, @@ -576,7 +577,7 @@ func resourceVirtualEnvironmentContainerCreate(d *schema.ResourceData, m interfa initializationUserAccountBlock := initializationUserAccount[0].(map[string]interface{}) keys := initializationUserAccountBlock[mkResourceVirtualEnvironmentContainerInitializationUserAccountKeys].([]interface{}) - initializationUserAccountKeys := make(proxmox.VirtualEnvironmentContainerCustomSSHKeys, len(keys)) + initializationUserAccountKeys = make(proxmox.VirtualEnvironmentContainerCustomSSHKeys, len(keys)) for ki, kv := range keys { initializationUserAccountKeys[ki] = kv.(string) @@ -606,7 +607,7 @@ func resourceVirtualEnvironmentContainerCreate(d *schema.ResourceData, m interfa enabled := networkInterfaceMap[mkResourceVirtualEnvironmentContainerNetworkInterfaceEnabled].(bool) macAddress := networkInterfaceMap[mkResourceVirtualEnvironmentContainerNetworkInterfaceMACAddress].(string) name := networkInterfaceMap[mkResourceVirtualEnvironmentContainerNetworkInterfaceName].(string) - rateLimit := networkInterfaceMap[mkResourceVirtualEnvironmentContainerNetworkInterfaceRateLimit].(int) + rateLimit := networkInterfaceMap[mkResourceVirtualEnvironmentContainerNetworkInterfaceRateLimit].(float64) vlanID := networkInterfaceMap[mkResourceVirtualEnvironmentContainerNetworkInterfaceVLANID].(int) if bridge != "" { @@ -637,9 +638,7 @@ func resourceVirtualEnvironmentContainerCreate(d *schema.ResourceData, m interfa networkInterfaceObject.MACAddress = &macAddress } - if name != "" { - networkInterfaceObject.Name = name - } + networkInterfaceObject.Name = name if rateLimit != 0 { networkInterfaceObject.RateLimit = &rateLimit @@ -676,20 +675,23 @@ func resourceVirtualEnvironmentContainerCreate(d *schema.ResourceData, m interfa } // Attempt to create the resource using the retrieved values. + datastoreID := "local-lvm" + body := proxmox.VirtualEnvironmentContainerCreateRequestBody{ ConsoleEnabled: &consoleEnabled, ConsoleMode: &consoleMode, CPUArchitecture: &cpuArchitecture, CPUCores: &cpuCores, CPUUnits: &cpuUnits, + DatastoreID: &datastoreID, DedicatedMemory: &memoryDedicated, NetworkInterfaces: networkInterfaceArray, - OSTemplateFileVolume: operatingSystemTemplateFileID, + OSTemplateFileVolume: &operatingSystemTemplateFileID, OSType: &operatingSystemType, StartOnBoot: &started, Swap: &memorySwap, TTY: &consoleTTYCount, - VMID: vmID, + VMID: &vmID, } if description != "" { @@ -728,6 +730,13 @@ func resourceVirtualEnvironmentContainerCreate(d *schema.ResourceData, m interfa d.SetId(strconv.Itoa(vmID)) + // Wait for the container's lock to be released. + err = veClient.WaitForContainerLock(nodeName, vmID, 300, 5) + + if err != nil { + return err + } + return resourceVirtualEnvironmentContainerCreateStart(d, m) } @@ -815,10 +824,11 @@ func resourceVirtualEnvironmentContainerRead(d *schema.ResourceData, m interface } // Retrieve the entire configuration in order to compare it to the state. - _, err = veClient.GetContainer(nodeName, vmID) + containerConfig, err := veClient.GetContainer(nodeName, vmID) if err != nil { - if strings.Contains(err.Error(), "HTTP 404") { + if strings.Contains(err.Error(), "HTTP 404") || + (strings.Contains(err.Error(), "HTTP 500") && strings.Contains(err.Error(), "does not exist")) { d.SetId("") return nil @@ -827,6 +837,276 @@ func resourceVirtualEnvironmentContainerRead(d *schema.ResourceData, m interface return err } + // Compare the primitive values to those stored in the state. + if containerConfig.Description != nil { + d.Set(mkResourceVirtualEnvironmentContainerDescription, strings.TrimSpace(*containerConfig.Description)) + } else { + d.Set(mkResourceVirtualEnvironmentContainerDescription, "") + } + + // Compare the console configuration to the one stored in the state. + console := map[string]interface{}{} + + if containerConfig.ConsoleEnabled != nil { + console[mkResourceVirtualEnvironmentContainerConsoleEnabled] = *containerConfig.ConsoleEnabled + } else { + // Default value of "console" is "1" according to the API documentation. + console[mkResourceVirtualEnvironmentContainerConsoleEnabled] = true + } + + if containerConfig.ConsoleMode != nil { + console[mkResourceVirtualEnvironmentContainerConsoleMode] = *containerConfig.ConsoleMode + } else { + // Default value of "cmode" is "tty" according to the API documentation. + console[mkResourceVirtualEnvironmentContainerConsoleMode] = "tty" + } + + if containerConfig.TTY != nil { + console[mkResourceVirtualEnvironmentContainerConsoleTTYCount] = *containerConfig.TTY + } else { + // Default value of "tty" is "2" according to the API documentation. + console[mkResourceVirtualEnvironmentContainerConsoleTTYCount] = 2 + } + + currentConsole := d.Get(mkResourceVirtualEnvironmentContainerConsole).([]interface{}) + + if len(currentConsole) > 0 || + console[mkResourceVirtualEnvironmentContainerConsoleEnabled] != proxmox.CustomBool(dvResourceVirtualEnvironmentContainerConsoleEnabled) || + console[mkResourceVirtualEnvironmentContainerConsoleMode] != dvResourceVirtualEnvironmentContainerConsoleMode || + console[mkResourceVirtualEnvironmentContainerConsoleTTYCount] != dvResourceVirtualEnvironmentContainerConsoleTTYCount { + d.Set(mkResourceVirtualEnvironmentContainerConsole, []interface{}{console}) + } + + // Compare the CPU configuration to the one stored in the state. + cpu := map[string]interface{}{} + + if containerConfig.CPUArchitecture != nil { + cpu[mkResourceVirtualEnvironmentContainerCPUArchitecture] = *containerConfig.CPUArchitecture + } else { + // Default value of "arch" is "amd64" according to the API documentation. + cpu[mkResourceVirtualEnvironmentContainerCPUArchitecture] = "amd64" + } + + if containerConfig.CPUCores != nil { + cpu[mkResourceVirtualEnvironmentContainerCPUCores] = *containerConfig.CPUCores + } else { + // Default value of "cores" is "1" according to the API documentation. + cpu[mkResourceVirtualEnvironmentContainerCPUCores] = 1 + } + + if containerConfig.CPUUnits != nil { + cpu[mkResourceVirtualEnvironmentContainerCPUUnits] = *containerConfig.CPUUnits + } else { + // Default value of "cpuunits" is "1024" according to the API documentation. + cpu[mkResourceVirtualEnvironmentContainerCPUUnits] = 1024 + } + + currentCPU := d.Get(mkResourceVirtualEnvironmentContainerCPU).([]interface{}) + + if len(currentCPU) > 0 || + cpu[mkResourceVirtualEnvironmentContainerCPUArchitecture] != dvResourceVirtualEnvironmentContainerCPUArchitecture || + cpu[mkResourceVirtualEnvironmentContainerCPUCores] != dvResourceVirtualEnvironmentContainerCPUCores || + cpu[mkResourceVirtualEnvironmentContainerCPUUnits] != dvResourceVirtualEnvironmentContainerCPUUnits { + d.Set(mkResourceVirtualEnvironmentContainerCPU, []interface{}{cpu}) + } + + // Compare the memory configuration to the one stored in the state. + memory := map[string]interface{}{} + + if containerConfig.DedicatedMemory != nil { + memory[mkResourceVirtualEnvironmentContainerMemoryDedicated] = *containerConfig.DedicatedMemory + } else { + memory[mkResourceVirtualEnvironmentContainerMemoryDedicated] = 0 + } + + if containerConfig.Swap != nil { + memory[mkResourceVirtualEnvironmentContainerMemorySwap] = *containerConfig.Swap + } else { + memory[mkResourceVirtualEnvironmentContainerMemorySwap] = 0 + } + + currentMemory := d.Get(mkResourceVirtualEnvironmentContainerMemory).([]interface{}) + + if len(currentMemory) > 0 || + memory[mkResourceVirtualEnvironmentContainerMemoryDedicated] != dvResourceVirtualEnvironmentContainerMemoryDedicated || + memory[mkResourceVirtualEnvironmentContainerMemorySwap] != dvResourceVirtualEnvironmentContainerMemorySwap { + d.Set(mkResourceVirtualEnvironmentContainerMemory, []interface{}{memory}) + } + + // Compare the initialization and network interface configuration to the one stored in the state. + initialization := map[string]interface{}{} + + if containerConfig.DNSDomain != nil || containerConfig.DNSServer != nil { + initializationDNS := map[string]interface{}{} + + if containerConfig.DNSDomain != nil { + initializationDNS[mkResourceVirtualEnvironmentContainerInitializationDNSDomain] = *containerConfig.DNSDomain + } else { + initializationDNS[mkResourceVirtualEnvironmentContainerInitializationDNSDomain] = "" + } + + if containerConfig.DNSServer != nil { + initializationDNS[mkResourceVirtualEnvironmentContainerInitializationDNSServer] = *containerConfig.DNSServer + } else { + initializationDNS[mkResourceVirtualEnvironmentContainerInitializationDNSServer] = "" + } + + initialization[mkResourceVirtualEnvironmentContainerInitializationDNS] = []interface{}{initializationDNS} + } + + if containerConfig.Hostname != nil { + initialization[mkResourceVirtualEnvironmentContainerInitializationHostname] = *containerConfig.Hostname + } else { + initialization[mkResourceVirtualEnvironmentContainerInitializationHostname] = "" + } + + ipConfigList := []interface{}{} + networkInterfaceArray := []*proxmox.VirtualEnvironmentContainerCustomNetworkInterface{ + containerConfig.NetworkInterface0, + containerConfig.NetworkInterface1, + containerConfig.NetworkInterface2, + containerConfig.NetworkInterface3, + containerConfig.NetworkInterface4, + containerConfig.NetworkInterface5, + containerConfig.NetworkInterface6, + containerConfig.NetworkInterface7, + } + networkInterfaceList := []interface{}{} + + for _, nv := range networkInterfaceArray { + if nv == nil { + continue + } + + if nv.IPv4Address != nil || nv.IPv4Gateway != nil || nv.IPv6Address != nil || nv.IPv6Gateway != nil { + ipConfig := map[string]interface{}{} + + if nv.IPv4Address != nil || nv.IPv4Gateway != nil { + ip := map[string]interface{}{} + + if nv.IPv4Address != nil { + ip[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4Address] = *nv.IPv4Address + } else { + ip[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4Address] = "" + } + + if nv.IPv4Gateway != nil { + ip[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4Gateway] = *nv.IPv4Gateway + } else { + ip[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4Gateway] = "" + } + + ipConfig[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4] = []interface{}{ip} + } else { + ipConfig[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4] = []interface{}{} + } + + if nv.IPv6Address != nil || nv.IPv6Gateway != nil { + ip := map[string]interface{}{} + + if nv.IPv6Address != nil { + ip[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6Address] = *nv.IPv6Address + } else { + ip[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6Address] = "" + } + + if nv.IPv6Gateway != nil { + ip[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6Gateway] = *nv.IPv6Gateway + } else { + ip[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6Gateway] = "" + } + + ipConfig[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6] = []interface{}{ip} + } else { + ipConfig[mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6] = []interface{}{} + } + + ipConfigList = append(ipConfigList, ipConfig) + } + + networkInterface := map[string]interface{}{} + + if nv.Bridge != nil { + networkInterface[mkResourceVirtualEnvironmentContainerNetworkInterfaceBridge] = *nv.Bridge + } else { + networkInterface[mkResourceVirtualEnvironmentContainerNetworkInterfaceBridge] = "" + } + + networkInterface[mkResourceVirtualEnvironmentContainerNetworkInterfaceEnabled] = true + + if nv.MACAddress != nil { + networkInterface[mkResourceVirtualEnvironmentContainerNetworkInterfaceMACAddress] = *nv.MACAddress + } else { + networkInterface[mkResourceVirtualEnvironmentContainerNetworkInterfaceMACAddress] = "" + } + + networkInterface[mkResourceVirtualEnvironmentContainerNetworkInterfaceName] = nv.Name + + if nv.RateLimit != nil { + networkInterface[mkResourceVirtualEnvironmentContainerNetworkInterfaceRateLimit] = *nv.RateLimit + } else { + networkInterface[mkResourceVirtualEnvironmentContainerNetworkInterfaceRateLimit] = 0 + } + + if nv.Tag != nil { + networkInterface[mkResourceVirtualEnvironmentContainerNetworkInterfaceVLANID] = *nv.Tag + } else { + networkInterface[mkResourceVirtualEnvironmentContainerNetworkInterfaceVLANID] = 0 + } + + networkInterfaceList = append(networkInterfaceList, networkInterface) + } + + initialization[mkResourceVirtualEnvironmentContainerInitializationIPConfig] = ipConfigList + + currentInitialization := d.Get(mkResourceVirtualEnvironmentContainerInitialization).([]interface{}) + + if len(currentInitialization) > 0 { + currentInitializationMap := currentInitialization[0].(map[string]interface{}) + + initialization[mkResourceVirtualEnvironmentContainerInitializationUserAccount] = currentInitializationMap[mkResourceVirtualEnvironmentContainerInitializationUserAccount].([]interface{}) + } + + if len(initialization) > 0 { + d.Set(mkResourceVirtualEnvironmentContainerInitialization, []interface{}{initialization}) + } else { + d.Set(mkResourceVirtualEnvironmentContainerInitialization, []interface{}{}) + } + + d.Set(mkResourceVirtualEnvironmentContainerNetworkInterface, networkInterfaceList) + + // Compare the operating system configuration to the one stored in the state. + operatingSystem := map[string]interface{}{} + + if containerConfig.OSType != nil { + operatingSystem[mkResourceVirtualEnvironmentContainerOperatingSystemType] = *containerConfig.OSType + } else { + // Default value of "ostype" is "" according to the API documentation. + operatingSystem[mkResourceVirtualEnvironmentContainerOperatingSystemType] = "" + } + + currentOperatingSystem := d.Get(mkResourceVirtualEnvironmentContainerOperatingSystem).([]interface{}) + + if len(currentOperatingSystem) > 0 { + currentOperatingSystemMap := currentOperatingSystem[0].(map[string]interface{}) + + operatingSystem[mkResourceVirtualEnvironmentContainerOperatingSystemTemplateFileID] = currentOperatingSystemMap[mkResourceVirtualEnvironmentContainerOperatingSystemTemplateFileID] + } + + if len(currentOperatingSystem) > 0 || + operatingSystem[mkResourceVirtualEnvironmentContainerOperatingSystemType] != dvResourceVirtualEnvironmentContainerOperatingSystemType { + d.Set(mkResourceVirtualEnvironmentContainerOperatingSystem, []interface{}{operatingSystem}) + } + + // Determine the state of the container in order to update the "started" argument. + status, err := veClient.GetContainerStatus(nodeName, vmID) + + if err != nil { + return err + } + + d.Set(mkResourceVirtualEnvironmentContainerStarted, status.Status == "running") + return nil } @@ -845,13 +1125,132 @@ func resourceVirtualEnvironmentContainerUpdate(d *schema.ResourceData, m interfa return err } - // Retrieve the entire configuration as we need to process certain values. - _, err = veClient.GetContainer(nodeName, vmID) + // Prepare the new request object. + body := proxmox.VirtualEnvironmentContainerUpdateRequestBody{} + rebootRequired := false + resource := resourceVirtualEnvironmentContainer() + + // Prepare the new primitive values. + if d.HasChange(mkResourceVirtualEnvironmentContainerDescription) { + description := d.Get(mkResourceVirtualEnvironmentContainerDescription).(string) + + body.Description = &description + } + + // Prepare the new console configuration. + if d.HasChange(mkResourceVirtualEnvironmentContainerConsole) { + consoleBlock, err := getSchemaBlock(resource, d, m, []string{mkResourceVirtualEnvironmentContainerConsole}, 0, true) + + if err != nil { + return err + } + + consoleEnabled := proxmox.CustomBool(consoleBlock[mkResourceVirtualEnvironmentContainerConsoleEnabled].(bool)) + consoleMode := consoleBlock[mkResourceVirtualEnvironmentContainerConsoleMode].(string) + consoleTTYCount := consoleBlock[mkResourceVirtualEnvironmentContainerConsoleTTYCount].(int) + + body.ConsoleEnabled = &consoleEnabled + body.ConsoleMode = &consoleMode + body.TTY = &consoleTTYCount + + rebootRequired = true + } + + // Prepare the new CPU configuration. + if d.HasChange(mkResourceVirtualEnvironmentContainerCPU) { + cpuBlock, err := getSchemaBlock(resource, d, m, []string{mkResourceVirtualEnvironmentContainerCPU}, 0, true) + + if err != nil { + return err + } + + cpuArchitecture := cpuBlock[mkResourceVirtualEnvironmentContainerCPUArchitecture].(string) + cpuCores := cpuBlock[mkResourceVirtualEnvironmentContainerCPUCores].(int) + cpuUnits := cpuBlock[mkResourceVirtualEnvironmentContainerCPUUnits].(int) + + body.CPUArchitecture = &cpuArchitecture + body.CPUCores = &cpuCores + body.CPUUnits = &cpuUnits + + rebootRequired = true + } + + // Prepare the new memory configuration. + if d.HasChange(mkResourceVirtualEnvironmentContainerMemory) { + memoryBlock, err := getSchemaBlock(resource, d, m, []string{mkResourceVirtualEnvironmentContainerMemory}, 0, true) + + if err != nil { + return err + } + + memoryDedicated := memoryBlock[mkResourceVirtualEnvironmentContainerMemoryDedicated].(int) + memorySwap := memoryBlock[mkResourceVirtualEnvironmentContainerMemorySwap].(int) + + body.DedicatedMemory = &memoryDedicated + body.Swap = &memorySwap + + rebootRequired = true + } + + // Update the configuration now that everything has been prepared. + err = veClient.UpdateContainer(nodeName, vmID, &body) if err != nil { return err } + // Determine if the state of the container needs to be changed. + if d.HasChange(mkResourceVirtualEnvironmentContainerStarted) { + started := d.Get(mkResourceVirtualEnvironmentContainerStarted).(bool) + + if started { + err = veClient.StartContainer(nodeName, vmID) + + if err != nil { + return err + } + + err = veClient.WaitForContainerState(nodeName, vmID, "running", 120, 5) + + if err != nil { + return err + } + } else { + forceStop := proxmox.CustomBool(true) + shutdownTimeout := 300 + + err = veClient.ShutdownContainer(nodeName, vmID, &proxmox.VirtualEnvironmentContainerShutdownRequestBody{ + ForceStop: &forceStop, + Timeout: &shutdownTimeout, + }) + + if err != nil { + return err + } + + err = veClient.WaitForContainerState(nodeName, vmID, "stopped", 30, 5) + + if err != nil { + return err + } + + rebootRequired = false + } + } + + // As a final step in the update procedure, we might need to reboot the virtual machine. + if rebootRequired { + rebootTimeout := 300 + + err = veClient.RebootContainer(nodeName, vmID, &proxmox.VirtualEnvironmentContainerRebootRequestBody{ + Timeout: &rebootTimeout, + }) + + if err != nil { + return err + } + } + return resourceVirtualEnvironmentContainerRead(d, m) } @@ -870,6 +1269,33 @@ func resourceVirtualEnvironmentContainerDelete(d *schema.ResourceData, m interfa return err } + // Shut down the container before deleting it. + status, err := veClient.GetContainerStatus(nodeName, vmID) + + if err != nil { + return err + } + + if status.Status != "stopped" { + forceStop := proxmox.CustomBool(true) + shutdownTimeout := 300 + + err = veClient.ShutdownContainer(nodeName, vmID, &proxmox.VirtualEnvironmentContainerShutdownRequestBody{ + ForceStop: &forceStop, + Timeout: &shutdownTimeout, + }) + + if err != nil { + return err + } + + err = veClient.WaitForContainerState(nodeName, vmID, "stopped", 30, 5) + + if err != nil { + return err + } + } + err = veClient.DeleteContainer(nodeName, vmID) if err != nil { @@ -882,6 +1308,13 @@ func resourceVirtualEnvironmentContainerDelete(d *schema.ResourceData, m interfa return err } + // Wait for the state to become unavailable as that clearly indicates the destruction of the container. + err = veClient.WaitForContainerState(nodeName, vmID, "", 60, 2) + + if err == nil { + return fmt.Errorf("Failed to delete container \"%d\"", vmID) + } + d.SetId("") return nil diff --git a/proxmoxtf/resource_virtual_environment_container_test.go b/proxmoxtf/resource_virtual_environment_container_test.go new file mode 100644 index 00000000..076d7a41 --- /dev/null +++ b/proxmoxtf/resource_virtual_environment_container_test.go @@ -0,0 +1,237 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +package proxmoxtf + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/schema" +) + +// TestResourceVirtualEnvironmentContainerInstantiation tests whether the ResourceVirtualEnvironmentContainer instance can be instantiated. +func TestResourceVirtualEnvironmentContainerInstantiation(t *testing.T) { + s := resourceVirtualEnvironmentContainer() + + if s == nil { + t.Fatalf("Cannot instantiate resourceVirtualEnvironmentContainer") + } +} + +// TestResourceVirtualEnvironmentContainerSchema tests the resourceVirtualEnvironmentContainer schema. +func TestResourceVirtualEnvironmentContainerSchema(t *testing.T) { + s := resourceVirtualEnvironmentContainer() + + testRequiredArguments(t, s, []string{ + mkResourceVirtualEnvironmentContainerNodeName, + mkResourceVirtualEnvironmentContainerOperatingSystem, + }) + + testOptionalArguments(t, s, []string{ + mkResourceVirtualEnvironmentContainerCPU, + mkResourceVirtualEnvironmentContainerDescription, + mkResourceVirtualEnvironmentContainerInitialization, + mkResourceVirtualEnvironmentContainerMemory, + mkResourceVirtualEnvironmentContainerPoolID, + mkResourceVirtualEnvironmentContainerStarted, + mkResourceVirtualEnvironmentContainerVMID, + }) + + testSchemaValueTypes(t, s, []string{ + mkResourceVirtualEnvironmentContainerCPU, + mkResourceVirtualEnvironmentContainerDescription, + mkResourceVirtualEnvironmentContainerInitialization, + mkResourceVirtualEnvironmentContainerMemory, + mkResourceVirtualEnvironmentContainerOperatingSystem, + mkResourceVirtualEnvironmentContainerPoolID, + mkResourceVirtualEnvironmentContainerStarted, + mkResourceVirtualEnvironmentContainerVMID, + }, []schema.ValueType{ + schema.TypeList, + schema.TypeString, + schema.TypeList, + schema.TypeList, + schema.TypeList, + schema.TypeString, + schema.TypeBool, + schema.TypeInt, + }) + + cpuSchema := testNestedSchemaExistence(t, s, mkResourceVirtualEnvironmentContainerCPU) + + testOptionalArguments(t, cpuSchema, []string{ + mkResourceVirtualEnvironmentContainerCPUArchitecture, + mkResourceVirtualEnvironmentContainerCPUCores, + mkResourceVirtualEnvironmentContainerCPUUnits, + }) + + testSchemaValueTypes(t, cpuSchema, []string{ + mkResourceVirtualEnvironmentContainerCPUArchitecture, + mkResourceVirtualEnvironmentContainerCPUCores, + mkResourceVirtualEnvironmentContainerCPUUnits, + }, []schema.ValueType{ + schema.TypeString, + schema.TypeInt, + schema.TypeInt, + }) + + initializationSchema := testNestedSchemaExistence(t, s, mkResourceVirtualEnvironmentContainerInitialization) + + testOptionalArguments(t, initializationSchema, []string{ + mkResourceVirtualEnvironmentContainerInitializationDNS, + mkResourceVirtualEnvironmentContainerInitializationHostname, + mkResourceVirtualEnvironmentContainerInitializationIPConfig, + mkResourceVirtualEnvironmentContainerInitializationUserAccount, + }) + + testSchemaValueTypes(t, initializationSchema, []string{ + mkResourceVirtualEnvironmentContainerInitializationDNS, + mkResourceVirtualEnvironmentContainerInitializationHostname, + mkResourceVirtualEnvironmentContainerInitializationIPConfig, + mkResourceVirtualEnvironmentContainerInitializationUserAccount, + }, []schema.ValueType{ + schema.TypeList, + schema.TypeString, + schema.TypeList, + schema.TypeList, + }) + + initializationDNSSchema := testNestedSchemaExistence(t, initializationSchema, mkResourceVirtualEnvironmentContainerInitializationDNS) + + testOptionalArguments(t, initializationDNSSchema, []string{ + mkResourceVirtualEnvironmentContainerInitializationDNSDomain, + mkResourceVirtualEnvironmentContainerInitializationDNSServer, + }) + + testSchemaValueTypes(t, initializationDNSSchema, []string{ + mkResourceVirtualEnvironmentContainerInitializationDNSDomain, + mkResourceVirtualEnvironmentContainerInitializationDNSServer, + }, []schema.ValueType{ + schema.TypeString, + schema.TypeString, + }) + + initializationIPConfigSchema := testNestedSchemaExistence(t, initializationSchema, mkResourceVirtualEnvironmentContainerInitializationIPConfig) + + testOptionalArguments(t, initializationIPConfigSchema, []string{ + mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4, + mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6, + }) + + testSchemaValueTypes(t, initializationIPConfigSchema, []string{ + mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4, + mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6, + }, []schema.ValueType{ + schema.TypeList, + schema.TypeList, + }) + + initializationIPConfigIPv4Schema := testNestedSchemaExistence(t, initializationIPConfigSchema, mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4) + + testOptionalArguments(t, initializationIPConfigIPv4Schema, []string{ + mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4Address, + mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4Gateway, + }) + + testSchemaValueTypes(t, initializationIPConfigIPv4Schema, []string{ + mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4Address, + mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv4Gateway, + }, []schema.ValueType{ + schema.TypeString, + schema.TypeString, + }) + + initializationIPConfigIPv6Schema := testNestedSchemaExistence(t, initializationIPConfigSchema, mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6) + + testOptionalArguments(t, initializationIPConfigIPv6Schema, []string{ + mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6Address, + mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6Gateway, + }) + + testSchemaValueTypes(t, initializationIPConfigIPv6Schema, []string{ + mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6Address, + mkResourceVirtualEnvironmentContainerInitializationIPConfigIPv6Gateway, + }, []schema.ValueType{ + schema.TypeString, + schema.TypeString, + }) + + initializationUserAccountSchema := testNestedSchemaExistence(t, initializationSchema, mkResourceVirtualEnvironmentContainerInitializationUserAccount) + + testOptionalArguments(t, initializationUserAccountSchema, []string{ + mkResourceVirtualEnvironmentContainerInitializationUserAccountKeys, + mkResourceVirtualEnvironmentContainerInitializationUserAccountPassword, + }) + + testSchemaValueTypes(t, initializationUserAccountSchema, []string{ + mkResourceVirtualEnvironmentContainerInitializationUserAccountKeys, + mkResourceVirtualEnvironmentContainerInitializationUserAccountPassword, + }, []schema.ValueType{ + schema.TypeList, + schema.TypeString, + }) + + memorySchema := testNestedSchemaExistence(t, s, mkResourceVirtualEnvironmentContainerMemory) + + testOptionalArguments(t, memorySchema, []string{ + mkResourceVirtualEnvironmentContainerMemoryDedicated, + mkResourceVirtualEnvironmentContainerMemorySwap, + }) + + testSchemaValueTypes(t, memorySchema, []string{ + mkResourceVirtualEnvironmentContainerMemoryDedicated, + mkResourceVirtualEnvironmentContainerMemorySwap, + }, []schema.ValueType{ + schema.TypeInt, + schema.TypeInt, + }) + + networkInterfaceSchema := testNestedSchemaExistence(t, s, mkResourceVirtualEnvironmentContainerNetworkInterface) + + testRequiredArguments(t, networkInterfaceSchema, []string{ + mkResourceVirtualEnvironmentContainerNetworkInterfaceName, + }) + + testOptionalArguments(t, networkInterfaceSchema, []string{ + mkResourceVirtualEnvironmentContainerNetworkInterfaceBridge, + mkResourceVirtualEnvironmentContainerNetworkInterfaceEnabled, + mkResourceVirtualEnvironmentContainerNetworkInterfaceMACAddress, + mkResourceVirtualEnvironmentContainerNetworkInterfaceRateLimit, + mkResourceVirtualEnvironmentContainerNetworkInterfaceVLANID, + }) + + testSchemaValueTypes(t, networkInterfaceSchema, []string{ + mkResourceVirtualEnvironmentContainerNetworkInterfaceBridge, + mkResourceVirtualEnvironmentContainerNetworkInterfaceEnabled, + mkResourceVirtualEnvironmentContainerNetworkInterfaceMACAddress, + mkResourceVirtualEnvironmentContainerNetworkInterfaceName, + mkResourceVirtualEnvironmentContainerNetworkInterfaceRateLimit, + mkResourceVirtualEnvironmentContainerNetworkInterfaceVLANID, + }, []schema.ValueType{ + schema.TypeString, + schema.TypeBool, + schema.TypeString, + schema.TypeString, + schema.TypeFloat, + schema.TypeInt, + }) + + operatingSystemSchema := testNestedSchemaExistence(t, s, mkResourceVirtualEnvironmentContainerOperatingSystem) + + testRequiredArguments(t, operatingSystemSchema, []string{ + mkResourceVirtualEnvironmentContainerOperatingSystemTemplateFileID, + }) + + testOptionalArguments(t, operatingSystemSchema, []string{ + mkResourceVirtualEnvironmentContainerOperatingSystemType, + }) + + testSchemaValueTypes(t, operatingSystemSchema, []string{ + mkResourceVirtualEnvironmentContainerOperatingSystemTemplateFileID, + mkResourceVirtualEnvironmentContainerOperatingSystemType, + }, []schema.ValueType{ + schema.TypeString, + schema.TypeString, + }) +} diff --git a/proxmoxtf/resource_virtual_environment_file.go b/proxmoxtf/resource_virtual_environment_file.go index 086381c4..970d24b6 100644 --- a/proxmoxtf/resource_virtual_environment_file.go +++ b/proxmoxtf/resource_virtual_environment_file.go @@ -402,7 +402,8 @@ func resourceVirtualEnvironmentFileGetContentType(d *schema.ResourceData, m inte } if contentType == "" { - if strings.HasSuffix(sourceFilePath, ".tar.xz") { + if strings.HasSuffix(sourceFilePath, ".tar.gz") || + strings.HasSuffix(sourceFilePath, ".tar.xz") { contentType = "vztmpl" } else { ext := strings.TrimLeft(strings.ToLower(filepath.Ext(sourceFilePath)), ".") diff --git a/proxmoxtf/resource_virtual_environment_vm.go b/proxmoxtf/resource_virtual_environment_vm.go index f5813663..439a92a6 100644 --- a/proxmoxtf/resource_virtual_environment_vm.go +++ b/proxmoxtf/resource_virtual_environment_vm.go @@ -547,7 +547,7 @@ func resourceVirtualEnvironmentVM() *schema.Resource { Sensitive: true, Default: dvResourceVirtualEnvironmentVMInitializationUserAccountPassword, DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - return strings.ReplaceAll(old, "*", "") == "" + return len(old) > 0 && strings.ReplaceAll(old, "*", "") == "" }, }, mkResourceVirtualEnvironmentVMInitializationUserAccountUsername: { @@ -1484,7 +1484,8 @@ func resourceVirtualEnvironmentVMRead(d *schema.ResourceData, m interface{}) err vmConfig, err := veClient.GetVM(nodeName, vmID) if err != nil { - if strings.Contains(err.Error(), "HTTP 404") { + if strings.Contains(err.Error(), "HTTP 404") || + (strings.Contains(err.Error(), "HTTP 500") && strings.Contains(err.Error(), "does not exist")) { d.SetId("") return nil @@ -2502,28 +2503,37 @@ func resourceVirtualEnvironmentVMDelete(d *schema.ResourceData, m interface{}) e } // Shut down the virtual machine before deleting it. - forceStop := proxmox.CustomBool(true) - shutdownTimeout := 300 - - err = veClient.ShutdownVM(nodeName, vmID, &proxmox.VirtualEnvironmentVMShutdownRequestBody{ - ForceStop: &forceStop, - Timeout: &shutdownTimeout, - }) + status, err := veClient.GetVMStatus(nodeName, vmID) if err != nil { return err } - err = veClient.WaitForVMState(nodeName, vmID, "stopped", 30, 5) + if status.Status != "stopped" { + forceStop := proxmox.CustomBool(true) + shutdownTimeout := 300 - if err != nil { - return err + err = veClient.ShutdownVM(nodeName, vmID, &proxmox.VirtualEnvironmentVMShutdownRequestBody{ + ForceStop: &forceStop, + Timeout: &shutdownTimeout, + }) + + if err != nil { + return err + } + + err = veClient.WaitForVMState(nodeName, vmID, "stopped", 30, 5) + + if err != nil { + return err + } } err = veClient.DeleteVM(nodeName, vmID) if err != nil { - if strings.Contains(err.Error(), "HTTP 404") { + if strings.Contains(err.Error(), "HTTP 404") || + (strings.Contains(err.Error(), "HTTP 500") && strings.Contains(err.Error(), "does not exist")) { d.SetId("") return nil @@ -2533,7 +2543,7 @@ func resourceVirtualEnvironmentVMDelete(d *schema.ResourceData, m interface{}) e } // Wait for the state to become unavailable as that clearly indicates the destruction of the VM. - err = veClient.WaitForVMState(nodeName, vmID, "", 30, 2) + err = veClient.WaitForVMState(nodeName, vmID, "", 60, 2) if err == nil { return fmt.Errorf("Failed to delete VM \"%d\"", vmID)