0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-06-29 18:21:10 +00:00
terraform-provider-proxmox/proxmoxtf/resource/vm/vm.go
Pavel Boldyrev be6f220779
fix(vm): improve cpu.architecture handling (#1683)
* chore(tests): add option to select auth type (root user / token) for tests
* fix(vm): throw an error when `cpu.architecture` can't be set

---------

Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
2024-12-17 22:16:45 -05:00

5784 lines
158 KiB
Go

/*
* 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 resource
import (
"context"
"encoding/base64"
"errors"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-log/tflog"
"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/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/bpg/terraform-provider-proxmox/proxmox"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster"
"github.com/bpg/terraform-provider-proxmox/proxmox/helpers/ptr"
"github.com/bpg/terraform-provider-proxmox/proxmox/nodes/vms"
"github.com/bpg/terraform-provider-proxmox/proxmox/pools"
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
"github.com/bpg/terraform-provider-proxmox/proxmoxtf"
"github.com/bpg/terraform-provider-proxmox/proxmoxtf/resource/validators"
"github.com/bpg/terraform-provider-proxmox/proxmoxtf/resource/vm/disk"
"github.com/bpg/terraform-provider-proxmox/proxmoxtf/resource/vm/network"
"github.com/bpg/terraform-provider-proxmox/proxmoxtf/structure"
"github.com/bpg/terraform-provider-proxmox/utils"
)
const (
dvRebootAfterCreation = false
dvOnBoot = true
dvACPI = true
dvAgentEnabled = false
dvAgentTimeout = "15m"
dvAgentTrim = false
dvAgentType = "virtio"
dvAudioDeviceDevice = "intel-hda"
dvAudioDeviceDriver = "spice"
dvAudioDeviceEnabled = true
dvBIOS = "seabios"
dvCDROMEnabled = false
dvCDROMFileID = ""
dvCDROMInterface = "ide3"
dvCloneDatastoreID = ""
dvCloneNodeName = ""
dvCloneFull = true
dvCloneRetries = 1
dvCPUArchitecture = ""
dvCPUCores = 1
dvCPUHotplugged = 0
dvCPULimit = 0
dvCPUNUMA = false
dvCPUSockets = 1
dvCPUType = "qemu64"
dvCPUUnits = 1024
dvCPUAffinity = ""
dvDescription = ""
dvEFIDiskDatastoreID = "local-lvm"
dvEFIDiskFileFormat = "raw"
dvEFIDiskType = "2m"
dvEFIDiskPreEnrolledKeys = false
dvTPMStateDatastoreID = "local-lvm"
dvTPMStateVersion = "v2.0"
dvInitializationDatastoreID = "local-lvm"
dvInitializationInterface = ""
dvInitializationDNSDomain = ""
dvInitializationDNSServer = ""
dvInitializationIPConfigIPv4Address = ""
dvInitializationIPConfigIPv4Gateway = ""
dvInitializationIPConfigIPv6Address = ""
dvInitializationIPConfigIPv6Gateway = ""
dvInitializationUserAccountPassword = ""
dvInitializationUserDataFileID = ""
dvInitializationVendorDataFileID = ""
dvInitializationNetworkDataFileID = ""
dvInitializationMetaDataFileID = ""
dvInitializationType = ""
dvKeyboardLayout = "en-us"
dvKVMArguments = ""
dvMachineType = ""
dvMemoryDedicated = 512
dvMemoryFloating = 0
dvMemoryShared = 0
dvMemoryHugepages = ""
dvMemoryKeepHugepages = false
dvMigrate = false
dvName = ""
dvOperatingSystemType = "other"
dvPoolID = ""
dvProtection = false
dvSerialDeviceDevice = "socket"
dvSMBIOSFamily = ""
dvSMBIOSManufacturer = ""
dvSMBIOSProduct = ""
dvSMBIOSSKU = ""
dvSMBIOSSerial = ""
dvSMBIOSVersion = ""
dvStarted = true
dvStartupOrder = -1
dvStartupUpDelay = -1
dvStartupDownDelay = -1
dvTabletDevice = true
dvTemplate = false
dvTimeoutClone = 1800
dvTimeoutCreate = 1800
dvTimeoutMigrate = 1800
dvTimeoutReboot = 1800
dvTimeoutShutdownVM = 1800
dvTimeoutStartVM = 1800
dvTimeoutStopVM = 300
dvVGAClipboard = ""
dvVGAMemory = 16
dvVGAType = "std"
dvSCSIHardware = "virtio-scsi-pci"
dvStopOnDestroy = false
dvHookScript = ""
dvWatchdogModel = "i6300esb"
dvWatchdogAction = "none"
maxResourceVirtualEnvironmentVMAudioDevices = 1
maxResourceVirtualEnvironmentVMSerialDevices = 4
// see /usr/share/perl5/PVE/QemuServer/PCI.pm.
maxResourceVirtualEnvironmentVMHostPCIDevices = 16
maxResourceVirtualEnvironmentVMHostUSBDevices = 4
// hardcoded /usr/share/perl5/PVE/QemuServer/Memory.pm: "our $MAX_NUMA = 8".
maxResourceVirtualEnvironmentVMNUMADevices = 8
mkRebootAfterCreation = "reboot"
mkOnBoot = "on_boot"
mkBootOrder = "boot_order"
mkACPI = "acpi"
mkAgent = "agent"
mkAgentEnabled = "enabled"
mkAgentTimeout = "timeout"
mkAgentTrim = "trim"
mkAgentType = "type"
mkAudioDevice = "audio_device"
mkAudioDeviceDevice = "device"
mkAudioDeviceDriver = "driver"
mkAudioDeviceEnabled = "enabled"
mkBIOS = "bios"
mkCDROM = "cdrom"
mkCDROMEnabled = "enabled"
mkCDROMFileID = "file_id"
mkCDROMInterface = "interface"
mkClone = "clone"
mkCloneRetries = "retries"
mkCloneDatastoreID = "datastore_id"
mkCloneNodeName = "node_name"
mkCloneVMID = "vm_id"
mkCloneFull = "full"
mkCPU = "cpu"
mkCPUArchitecture = "architecture"
mkCPUCores = "cores"
mkCPUFlags = "flags"
mkCPUHotplugged = "hotplugged"
mkCPULimit = "limit"
mkCPUNUMA = "numa"
mkCPUSockets = "sockets"
mkCPUType = "type"
mkCPUUnits = "units"
mkCPUAffinity = "affinity"
mkDescription = "description"
mkNUMA = "numa"
mkNUMADevice = "device"
mkNUMACPUIDs = "cpus"
mkNUMAHostNodeNames = "hostnodes"
mkNUMAMemory = "memory"
mkNUMAPolicy = "policy"
mkEFIDisk = "efi_disk"
mkEFIDiskDatastoreID = "datastore_id"
mkEFIDiskFileFormat = "file_format"
mkEFIDiskType = "type"
mkEFIDiskPreEnrolledKeys = "pre_enrolled_keys"
mkTPMState = "tpm_state"
mkTPMStateDatastoreID = "datastore_id"
mkTPMStateVersion = "version"
mkHostPCI = "hostpci"
mkHostPCIDevice = "device"
mkHostPCIDeviceID = "id"
mkHostPCIDeviceMapping = "mapping"
mkHostPCIDeviceMDev = "mdev"
mkHostPCIDevicePCIE = "pcie"
mkHostPCIDeviceROMBAR = "rombar"
mkHostPCIDeviceROMFile = "rom_file"
mkHostPCIDeviceXVGA = "xvga"
mkInitialization = "initialization"
mkInitializationDatastoreID = "datastore_id"
mkInitializationInterface = "interface"
mkInitializationDNS = "dns"
mkInitializationDNSDomain = "domain"
mkInitializationDNSServer = "server"
mkInitializationDNSServers = "servers"
mkInitializationIPConfig = "ip_config"
mkInitializationIPConfigIPv4 = "ipv4"
mkInitializationIPConfigIPv4Address = "address"
mkInitializationIPConfigIPv4Gateway = "gateway"
mkInitializationIPConfigIPv6 = "ipv6"
mkInitializationIPConfigIPv6Address = "address"
mkInitializationIPConfigIPv6Gateway = "gateway"
mkInitializationType = "type"
mkInitializationUserAccount = "user_account"
mkInitializationUserAccountKeys = "keys"
mkInitializationUserAccountPassword = "password"
mkInitializationUserAccountUsername = "username"
mkInitializationUserDataFileID = "user_data_file_id"
mkInitializationVendorDataFileID = "vendor_data_file_id"
mkInitializationNetworkDataFileID = "network_data_file_id"
mkInitializationMetaDataFileID = "meta_data_file_id"
mkInitializationUpgrade = "upgrade"
mkKeyboardLayout = "keyboard_layout"
mkKVMArguments = "kvm_arguments"
mkMachine = "machine"
mkMemory = "memory"
mkMemoryDedicated = "dedicated"
mkMemoryFloating = "floating"
mkMemoryShared = "shared"
mkMemoryHugepages = "hugepages"
mkMemoryKeepHugepages = "keep_hugepages"
mkMigrate = "migrate"
mkName = "name"
mkNodeName = "node_name"
mkOperatingSystem = "operating_system"
mkOperatingSystemType = "type"
mkPoolID = "pool_id"
mkProtection = "protection"
mkSerialDevice = "serial_device"
mkSerialDeviceDevice = "device"
mkSMBIOS = "smbios"
mkSMBIOSFamily = "family"
mkSMBIOSManufacturer = "manufacturer"
mkSMBIOSProduct = "product"
mkSMBIOSSKU = "sku"
mkSMBIOSSerial = "serial"
mkSMBIOSUUID = "uuid"
mkSMBIOSVersion = "version"
mkStarted = "started"
mkStartup = "startup"
mkStartupOrder = "order"
mkStartupUpDelay = "up_delay"
mkStartupDownDelay = "down_delay"
mkTabletDevice = "tablet_device"
mkTags = "tags"
mkTemplate = "template"
mkTimeoutClone = "timeout_clone"
mkTimeoutCreate = "timeout_create"
mkTimeoutMigrate = "timeout_migrate" // this is essentially a "timeout_update", needs to be refactored
mkTimeoutReboot = "timeout_reboot"
mkTimeoutShutdownVM = "timeout_shutdown_vm"
mkTimeoutStartVM = "timeout_start_vm"
mkTimeoutStopVM = "timeout_stop_vm"
mkHostUSB = "usb"
mkHostUSBDevice = "host"
mkHostUSBDeviceMapping = "mapping"
mkHostUSBDeviceUSB3 = "usb3"
mkVGA = "vga"
mkVGAClipboard = "clipboard"
mkVGAMemory = "memory"
mkVGAType = "type"
mkVMID = "vm_id"
mkSCSIHardware = "scsi_hardware"
mkHookScriptFileID = "hook_script_file_id"
mkStopOnDestroy = "stop_on_destroy"
mkWatchdog = "watchdog"
// a workaround for the lack of proper support of default and undefined values in SDK.
mkWatchdogEnabled = "enabled"
mkWatchdogModel = "model"
mkWatchdogAction = "action"
)
// VM returns a resource that manages VMs.
func VM() *schema.Resource {
s := map[string]*schema.Schema{
mkRebootAfterCreation: {
Type: schema.TypeBool,
Description: "Whether to reboot vm after creation",
Optional: true,
Default: dvRebootAfterCreation,
},
mkOnBoot: {
Type: schema.TypeBool,
Description: "Start VM on Node boot",
Optional: true,
Default: dvOnBoot,
},
mkBootOrder: {
Type: schema.TypeList,
Description: "The guest will attempt to boot from devices in the order they appear here",
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
DefaultFunc: func() (interface{}, error) {
return []interface{}{}, nil
},
},
mkACPI: {
Type: schema.TypeBool,
Description: "Whether to enable ACPI",
Optional: true,
Default: dvACPI,
},
mkAgent: {
Type: schema.TypeList,
Description: "The QEMU agent configuration",
Optional: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{
map[string]interface{}{
mkAgentEnabled: dvAgentEnabled,
mkAgentTimeout: dvAgentTimeout,
mkAgentTrim: dvAgentTrim,
mkAgentType: dvAgentType,
},
}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkAgentEnabled: {
Type: schema.TypeBool,
Description: "Whether to enable the QEMU agent",
Optional: true,
Default: dvAgentEnabled,
},
mkAgentTimeout: {
Type: schema.TypeString,
Description: "The maximum amount of time to wait for data from the QEMU agent to become available",
Optional: true,
Default: dvAgentTimeout,
ValidateDiagFunc: TimeoutValidator(),
},
mkAgentTrim: {
Type: schema.TypeBool,
Description: "Whether to enable the FSTRIM feature in the QEMU agent",
Optional: true,
Default: dvAgentTrim,
},
mkAgentType: {
Type: schema.TypeString,
Description: "The QEMU agent interface type",
Optional: true,
Default: dvAgentType,
ValidateDiagFunc: QEMUAgentTypeValidator(),
},
},
},
MaxItems: 1,
MinItems: 0,
},
mkKVMArguments: {
Type: schema.TypeString,
Description: "The args implementation",
Optional: true,
Default: dvKVMArguments,
},
mkAudioDevice: {
Type: schema.TypeList,
Description: "The audio devices",
Optional: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkAudioDeviceDevice: {
Type: schema.TypeString,
Description: "The device",
Optional: true,
Default: dvAudioDeviceDevice,
ValidateDiagFunc: AudioDeviceValidator(),
},
mkAudioDeviceDriver: {
Type: schema.TypeString,
Description: "The driver",
Optional: true,
Default: dvAudioDeviceDriver,
ValidateDiagFunc: AudioDriverValidator(),
},
mkAudioDeviceEnabled: {
Type: schema.TypeBool,
Description: "Whether to enable the audio device",
Optional: true,
Default: dvAudioDeviceEnabled,
},
},
},
MaxItems: maxResourceVirtualEnvironmentVMAudioDevices,
MinItems: 0,
},
mkBIOS: {
Type: schema.TypeString,
Description: "The BIOS implementation",
Optional: true,
Default: dvBIOS,
ValidateDiagFunc: BIOSValidator(),
},
mkCDROM: {
Type: schema.TypeList,
Description: "The CDROM drive",
Optional: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{
map[string]interface{}{
mkCDROMEnabled: dvCDROMEnabled,
mkCDROMFileID: dvCDROMFileID,
mkCDROMInterface: dvCDROMInterface,
},
}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkCDROMEnabled: {
Type: schema.TypeBool,
Description: "Whether to enable the CDROM drive",
Optional: true,
Default: dvCDROMEnabled,
},
mkCDROMFileID: {
Type: schema.TypeString,
Description: "The file id",
Optional: true,
Default: dvCDROMFileID,
ValidateDiagFunc: validation.AnyDiag(
validation.ToDiagFunc(validation.StringInSlice([]string{"none", "cdrom"}, false)),
validators.FileID(),
),
},
mkCDROMInterface: {
Type: schema.TypeString,
Description: "The CDROM interface",
Optional: true,
Default: dvCDROMInterface,
ValidateDiagFunc: IDEInterfaceValidator(),
},
},
},
MaxItems: 1,
MinItems: 0,
},
mkClone: {
Type: schema.TypeList,
Description: "The cloning configuration",
Optional: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkCloneRetries: {
Type: schema.TypeInt,
Description: "The number of Retries to create a clone",
Optional: true,
ForceNew: true,
Default: dvCloneRetries,
},
mkCloneDatastoreID: {
Type: schema.TypeString,
Description: "The ID of the target datastore",
Optional: true,
ForceNew: true,
Default: dvCloneDatastoreID,
},
mkCloneNodeName: {
Type: schema.TypeString,
Description: "The name of the source node",
Optional: true,
ForceNew: true,
Default: dvCloneNodeName,
},
mkCloneVMID: {
Type: schema.TypeInt,
Description: "The ID of the source VM",
Required: true,
ForceNew: true,
ValidateDiagFunc: VMIDValidator(),
},
mkCloneFull: {
Type: schema.TypeBool,
Description: "The Clone Type, create a Full Clone (true) or a linked Clone (false)",
Optional: true,
ForceNew: true,
Default: dvCloneFull,
},
},
},
MaxItems: 1,
MinItems: 0,
},
mkCPU: {
Type: schema.TypeList,
Description: "The CPU allocation",
Optional: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{
map[string]interface{}{
mkCPUArchitecture: dvCPUArchitecture,
mkCPUCores: dvCPUCores,
mkCPUFlags: []interface{}{},
mkCPUHotplugged: dvCPUHotplugged,
mkCPULimit: dvCPULimit,
mkCPUNUMA: dvCPUNUMA,
mkCPUSockets: dvCPUSockets,
mkCPUType: dvCPUType,
mkCPUUnits: dvCPUUnits,
mkCPUAffinity: dvCPUAffinity,
},
}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkCPUArchitecture: {
Type: schema.TypeString,
Description: "The CPU architecture",
Optional: true,
Default: dvCPUArchitecture,
ValidateDiagFunc: CPUArchitectureValidator(),
},
mkCPUCores: {
Type: schema.TypeInt,
Description: "The number of CPU cores",
Optional: true,
Default: dvCPUCores,
ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(1, 2304)),
},
mkCPUFlags: {
Type: schema.TypeList,
Description: "The CPU flags",
Optional: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{}, nil
},
Elem: &schema.Schema{Type: schema.TypeString},
},
mkCPUHotplugged: {
Type: schema.TypeInt,
Description: "The number of hotplugged vCPUs",
Optional: true,
Default: dvCPUHotplugged,
ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(0, 2304)),
},
mkCPULimit: {
Type: schema.TypeInt,
Description: "Limit of CPU usage",
Optional: true,
Default: dvCPULimit,
ValidateDiagFunc: validation.ToDiagFunc(
validation.IntBetween(0, 128),
),
},
mkCPUNUMA: {
Type: schema.TypeBool,
Description: "Enable/disable NUMA.",
Optional: true,
Default: dvCPUNUMA,
},
mkCPUSockets: {
Type: schema.TypeInt,
Description: "The number of CPU sockets",
Optional: true,
Default: dvCPUSockets,
ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(1, 16)),
},
mkCPUType: {
Type: schema.TypeString,
Description: "The emulated CPU type",
Optional: true,
Default: dvCPUType,
ValidateDiagFunc: CPUTypeValidator(),
},
mkCPUUnits: {
Type: schema.TypeInt,
Description: "The CPU units",
Optional: true,
Default: dvCPUUnits,
ValidateDiagFunc: validation.ToDiagFunc(
validation.IntBetween(2, 262144),
),
},
mkCPUAffinity: {
Type: schema.TypeString,
Description: "The CPU affinity",
Optional: true,
Default: dvCPUAffinity,
ValidateDiagFunc: CPUAffinityValidator(),
},
},
},
MaxItems: 1,
MinItems: 0,
},
mkDescription: {
Type: schema.TypeString,
Description: "The description",
Optional: true,
Default: dvDescription,
StateFunc: func(i interface{}) string {
// PVE always adds a newline to the description, so we have to do the same,
// also taking in account the CLRF case (Windows)
// Unlike container, VM description does not have trailing "\n"
if i.(string) != "" {
return strings.ReplaceAll(strings.TrimSpace(i.(string)), "\r\n", "\n")
}
return ""
},
},
mkEFIDisk: {
Type: schema.TypeList,
Description: "The efidisk device",
Optional: true,
ForceNew: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkEFIDiskDatastoreID: {
Type: schema.TypeString,
Description: "The datastore id",
Optional: true,
Default: dvEFIDiskDatastoreID,
},
mkEFIDiskFileFormat: {
Type: schema.TypeString,
Description: "The file format",
Optional: true,
ForceNew: true,
Computed: true,
ValidateDiagFunc: validators.FileFormat(),
},
mkEFIDiskType: {
Type: schema.TypeString,
Description: "Size and type of the OVMF EFI disk",
Optional: true,
ForceNew: true,
Default: dvEFIDiskType,
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{
"2m",
"4m",
}, true)),
},
mkEFIDiskPreEnrolledKeys: {
Type: schema.TypeBool,
Description: "Use an EFI vars template with distribution-specific and Microsoft Standard " +
"keys enrolled, if used with efi type=`4m`.",
Optional: true,
ForceNew: true,
Default: dvEFIDiskPreEnrolledKeys,
},
},
},
MaxItems: 1,
MinItems: 0,
},
mkTPMState: {
Type: schema.TypeList,
Description: "The tpmstate device",
Optional: true,
ForceNew: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkTPMStateDatastoreID: {
Type: schema.TypeString,
Description: "Datastore ID",
Optional: true,
Default: dvTPMStateDatastoreID,
},
mkTPMStateVersion: {
Type: schema.TypeString,
Description: "TPM version",
Optional: true,
ForceNew: true,
Default: dvTPMStateVersion,
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{
"v1.2",
"v2.0",
}, true)),
},
},
},
MaxItems: 1,
MinItems: 0,
},
mkInitialization: {
Type: schema.TypeList,
Description: "The cloud-init configuration",
Optional: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkInitializationDatastoreID: {
Type: schema.TypeString,
Description: "The datastore id",
Optional: true,
Default: dvInitializationDatastoreID,
},
mkInitializationInterface: {
Type: schema.TypeString,
Description: "The IDE interface on which the CloudInit drive will be added",
Optional: true,
Default: dvInitializationInterface,
ValidateDiagFunc: CloudInitInterfaceValidator(),
DiffSuppressFunc: func(_, _, newValue string, _ *schema.ResourceData) bool {
return newValue == ""
},
},
mkInitializationDNS: {
Type: schema.TypeList,
Description: "The DNS configuration",
Optional: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkInitializationDNSDomain: {
Type: schema.TypeString,
Description: "The DNS search domain",
Optional: true,
Default: dvInitializationDNSDomain,
},
mkInitializationDNSServer: {
Type: schema.TypeString,
Description: "The DNS server",
Deprecated: "The `server` attribute is deprecated and will be removed in a future release. " +
"Please use the `servers` attribute instead.",
Optional: true,
Default: dvInitializationDNSServer,
},
mkInitializationDNSServers: {
Type: schema.TypeList,
Description: "The list of DNS servers",
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString, ValidateFunc: validation.IsIPAddress},
MinItems: 0,
},
},
},
MaxItems: 1,
MinItems: 0,
},
mkInitializationIPConfig: {
Type: schema.TypeList,
Description: "The IP configuration",
Optional: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkInitializationIPConfigIPv4: {
Type: schema.TypeList,
Description: "The IPv4 configuration",
Optional: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkInitializationIPConfigIPv4Address: {
Type: schema.TypeString,
Description: "The IPv4 address",
Optional: true,
Default: dvInitializationIPConfigIPv4Address,
},
mkInitializationIPConfigIPv4Gateway: {
Type: schema.TypeString,
Description: "The IPv4 gateway",
Optional: true,
Default: dvInitializationIPConfigIPv4Gateway,
},
},
},
MaxItems: 1,
MinItems: 0,
},
mkInitializationIPConfigIPv6: {
Type: schema.TypeList,
Description: "The IPv6 configuration",
Optional: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkInitializationIPConfigIPv6Address: {
Type: schema.TypeString,
Description: "The IPv6 address",
Optional: true,
Default: dvInitializationIPConfigIPv6Address,
},
mkInitializationIPConfigIPv6Gateway: {
Type: schema.TypeString,
Description: "The IPv6 gateway",
Optional: true,
Default: dvInitializationIPConfigIPv6Gateway,
},
},
},
MaxItems: 1,
MinItems: 0,
},
},
},
MaxItems: 8,
MinItems: 0,
},
mkInitializationUserAccount: {
Type: schema.TypeList,
Description: "The user account configuration",
Optional: true,
ForceNew: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkInitializationUserAccountKeys: {
Type: schema.TypeList,
Description: "The SSH keys",
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
mkInitializationUserAccountPassword: {
Type: schema.TypeString,
Description: "The SSH password",
Optional: true,
ForceNew: true,
Sensitive: true,
Default: dvInitializationUserAccountPassword,
DiffSuppressFunc: func(_, oldVal, _ string, _ *schema.ResourceData) bool {
return len(oldVal) > 0 &&
strings.ReplaceAll(oldVal, "*", "") == ""
},
},
mkInitializationUserAccountUsername: {
Type: schema.TypeString,
Description: "The SSH username",
Optional: true,
ForceNew: true,
},
},
},
MaxItems: 1,
MinItems: 0,
},
mkInitializationUserDataFileID: {
Type: schema.TypeString,
Description: "The ID of a file containing custom user data",
Optional: true,
ForceNew: true,
Default: dvInitializationUserDataFileID,
ValidateDiagFunc: validators.FileID(),
},
mkInitializationVendorDataFileID: {
Type: schema.TypeString,
Description: "The ID of a file containing vendor data",
Optional: true,
ForceNew: true,
Default: dvInitializationVendorDataFileID,
ValidateDiagFunc: validators.FileID(),
},
mkInitializationNetworkDataFileID: {
Type: schema.TypeString,
Description: "The ID of a file containing network config",
Optional: true,
ForceNew: true,
Default: dvInitializationNetworkDataFileID,
ValidateDiagFunc: validators.FileID(),
},
mkInitializationMetaDataFileID: {
Type: schema.TypeString,
Description: "The ID of a file containing meta data config",
Optional: true,
ForceNew: true,
Default: dvInitializationMetaDataFileID,
ValidateDiagFunc: validators.FileID(),
},
mkInitializationType: {
Type: schema.TypeString,
Description: "The cloud-init configuration format",
Optional: true,
ForceNew: true,
Default: dvInitializationType,
ValidateDiagFunc: CloudInitTypeValidator(),
},
mkInitializationUpgrade: {
Type: schema.TypeBool,
Description: "Whether to do an automatic package upgrade after the first boot",
Optional: true,
Computed: true,
Deprecated: "The `upgrade` attribute is deprecated and will be removed in a future release.",
},
},
},
MaxItems: 1,
MinItems: 0,
},
mkHostPCI: {
Type: schema.TypeList,
Description: "The Host PCI devices mapped to the VM",
Optional: true,
ForceNew: false,
DefaultFunc: func() (interface{}, error) {
return []interface{}{}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkHostPCIDevice: {
Type: schema.TypeString,
Description: "The PCI device name for Proxmox, in form of 'hostpciX' where X is a sequential number from 0 to 3",
Required: true,
},
mkHostPCIDeviceID: {
Type: schema.TypeString,
Description: "The PCI ID of the device, for example 0000:00:1f.0 (or 0000:00:1f.0;0000:00:1f.1 for multiple " +
"device functions, or 0000:00:1f for all functions). Use either this or mapping.",
Optional: true,
},
mkHostPCIDeviceMapping: {
Type: schema.TypeString,
Description: "The resource mapping name of the device, for example gpu. Use either this or id.",
Optional: true,
},
mkHostPCIDeviceMDev: {
Type: schema.TypeString,
Description: "The the mediated device to use",
Optional: true,
},
mkHostPCIDevicePCIE: {
Type: schema.TypeBool,
Description: "Tells Proxmox VE to use a PCIe or PCI port. Some guests/device combination require PCIe rather " +
"than PCI. PCIe is only available for q35 machine types.",
Optional: true,
},
mkHostPCIDeviceROMBAR: {
Type: schema.TypeBool,
Description: "Makes the firmware ROM visible for the guest. Default is true",
Optional: true,
},
mkHostPCIDeviceROMFile: {
Type: schema.TypeString,
Description: "A path to a ROM file for the device to use. This is a relative path under /usr/share/kvm/",
Optional: true,
},
mkHostPCIDeviceXVGA: {
Type: schema.TypeBool,
Description: "Marks the PCI(e) device as the primary GPU of the VM. With this enabled, " +
"the vga configuration argument will be ignored.",
Optional: true,
},
},
},
MaxItems: maxResourceVirtualEnvironmentVMHostPCIDevices,
},
mkHostUSB: {
Type: schema.TypeList,
Description: "The Host USB devices mapped to the VM",
Optional: true,
ForceNew: false,
DefaultFunc: func() (interface{}, error) {
return []interface{}{}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkHostUSBDevice: {
Type: schema.TypeString,
Description: "The USB device ID for Proxmox, in form of '<MANUFACTURER>:<ID>'",
Optional: true,
},
mkHostUSBDeviceMapping: {
Type: schema.TypeString,
Description: "The resource mapping name of the device, for example usbdisk. Use either this or id.",
Optional: true,
},
mkHostUSBDeviceUSB3: {
Type: schema.TypeBool,
Description: "Makes the USB device a USB3 device for the machine. Default is false",
Optional: true,
},
},
},
},
mkKeyboardLayout: {
Type: schema.TypeString,
Description: "The keyboard layout",
Optional: true,
Default: dvKeyboardLayout,
ValidateDiagFunc: KeyboardLayoutValidator(),
},
mkMachine: {
Type: schema.TypeString,
Description: "The VM machine type, either default `pc` or `q35`",
Optional: true,
Default: dvMachineType,
ValidateDiagFunc: MachineTypeValidator(),
},
mkMemory: {
Type: schema.TypeList,
Description: "The memory allocation",
Optional: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{
map[string]interface{}{
mkMemoryDedicated: dvMemoryDedicated,
mkMemoryFloating: dvMemoryFloating,
mkMemoryShared: dvMemoryShared,
mkMemoryHugepages: dvMemoryHugepages,
mkMemoryKeepHugepages: dvMemoryKeepHugepages,
},
}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkMemoryDedicated: {
Type: schema.TypeInt,
Description: "The dedicated memory in megabytes",
Optional: true,
Default: dvMemoryDedicated,
ValidateDiagFunc: validation.ToDiagFunc(
validation.IntBetween(64, 268435456),
),
},
mkMemoryFloating: {
Type: schema.TypeInt,
Description: "The floating memory in megabytes (balloon)",
Optional: true,
Default: dvMemoryFloating,
ValidateDiagFunc: validation.ToDiagFunc(
validation.IntBetween(0, 268435456),
),
},
mkMemoryShared: {
Type: schema.TypeInt,
Description: "The shared memory in megabytes",
Optional: true,
Default: dvMemoryShared,
ValidateDiagFunc: validation.ToDiagFunc(
validation.IntBetween(0, 268435456),
),
},
mkMemoryHugepages: {
Type: schema.TypeString,
Description: "Enable/disable hugepages memory",
Optional: true,
Default: dvMemoryHugepages,
RequiredWith: []string{"cpu.0.numa"},
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{
"1024",
"2",
"any",
}, true)),
},
mkMemoryKeepHugepages: {
Type: schema.TypeBool,
Description: "Hugepages will not be deleted after VM shutdown and can be used for subsequent starts",
Optional: true,
Default: dvMemoryKeepHugepages,
RequiredWith: []string{"cpu.0.numa"},
},
},
},
MaxItems: 1,
MinItems: 0,
},
mkName: {
Type: schema.TypeString,
Description: "The name",
Optional: true,
Default: dvName,
},
mkNodeName: {
Type: schema.TypeString,
Description: "The node name",
Required: true,
ValidateFunc: validation.StringIsNotEmpty,
},
mkNUMA: {
Type: schema.TypeList,
Description: "The NUMA topology",
Optional: true,
ForceNew: false,
DefaultFunc: func() (interface{}, error) {
return []interface{}{}, nil
},
DiffSuppressFunc: structure.SuppressIfListsOfMapsAreEqualIgnoringOrderByKey(mkNUMADevice),
DiffSuppressOnRefresh: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkNUMADevice: {
Type: schema.TypeString,
Description: "Numa node device ID",
Optional: false,
Required: true,
RequiredWith: []string{"cpu.0.numa"},
ValidateDiagFunc: validation.ToDiagFunc(validation.StringMatch(
regexp.MustCompile(`^numa\d+$`),
"numa node device ID must be in the format 'numaX' where X is a number",
)),
},
mkNUMACPUIDs: {
Type: schema.TypeString,
Description: "CPUs accessing this NUMA node",
Optional: false,
Required: true,
RequiredWith: []string{"cpu.0.numa"},
ValidateDiagFunc: RangeSemicolonValidator(),
},
mkNUMAMemory: {
Type: schema.TypeInt,
Description: "Amount of memory this NUMA node provides",
Optional: false,
Required: true,
RequiredWith: []string{"cpu.0.numa"},
ValidateDiagFunc: validation.ToDiagFunc(
validation.IntBetween(64, 268435456),
),
},
mkNUMAHostNodeNames: {
Type: schema.TypeString,
Description: "Host NUMA nodes to use",
Optional: true,
RequiredWith: []string{"cpu.0.numa"},
ValidateDiagFunc: RangeSemicolonValidator(),
},
mkNUMAPolicy: {
Type: schema.TypeString,
Description: "NUMA policy",
Optional: true,
Default: "preferred",
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{
"bind",
"interleave",
"preferred",
}, true)),
},
},
},
},
mkMigrate: {
Type: schema.TypeBool,
Description: "Whether to migrate the VM on node change instead of re-creating it",
Optional: true,
Default: dvMigrate,
},
mkOperatingSystem: {
Type: schema.TypeList,
Description: "The operating system configuration",
Optional: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{
map[string]interface{}{
mkOperatingSystemType: dvOperatingSystemType,
},
}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkOperatingSystemType: {
Type: schema.TypeString,
Description: "The type",
Optional: true,
Default: dvOperatingSystemType,
ValidateDiagFunc: OperatingSystemTypeValidator(),
},
},
},
MaxItems: 1,
MinItems: 0,
},
mkPoolID: {
Type: schema.TypeString,
Description: "The ID of the pool to assign the virtual machine to",
Optional: true,
Default: dvPoolID,
},
mkProtection: {
Type: schema.TypeBool,
Description: "Sets the protection flag of the VM. This will disable the remove VM and remove disk operations",
Optional: true,
Default: dvProtection,
},
mkSerialDevice: {
Type: schema.TypeList,
Description: "The serial devices",
Optional: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{
map[string]interface{}{
mkSerialDeviceDevice: dvSerialDeviceDevice,
},
}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkSerialDeviceDevice: {
Type: schema.TypeString,
Description: "The device",
Optional: true,
Default: dvSerialDeviceDevice,
ValidateDiagFunc: SerialDeviceValidator(),
},
},
},
MaxItems: maxResourceVirtualEnvironmentVMSerialDevices,
MinItems: 0,
},
mkSMBIOS: {
Type: schema.TypeList,
Description: "Specifies SMBIOS (type1) settings for the VM",
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkSMBIOSFamily: {
Type: schema.TypeString,
Description: "Sets SMBIOS family string",
Optional: true,
Default: dvSMBIOSFamily,
},
mkSMBIOSManufacturer: {
Type: schema.TypeString,
Description: "Sets SMBIOS manufacturer",
Optional: true,
Default: dvSMBIOSManufacturer,
},
mkSMBIOSProduct: {
Type: schema.TypeString,
Description: "Sets SMBIOS product ID",
Optional: true,
Default: dvSMBIOSProduct,
},
mkSMBIOSSerial: {
Type: schema.TypeString,
Description: "Sets SMBIOS serial number",
Optional: true,
Default: dvSMBIOSSerial,
},
mkSMBIOSSKU: {
Type: schema.TypeString,
Description: "Sets SMBIOS SKU",
Optional: true,
Default: dvSMBIOSSKU,
},
mkSMBIOSUUID: {
Type: schema.TypeString,
Description: "Sets SMBIOS UUID",
Optional: true,
Computed: true,
},
mkSMBIOSVersion: {
Type: schema.TypeString,
Description: "Sets SMBIOS version",
Optional: true,
Default: dvSMBIOSVersion,
},
},
},
MaxItems: 1,
MinItems: 0,
},
mkStarted: {
Type: schema.TypeBool,
Description: "Whether to start the virtual machine",
Optional: true,
Default: dvStarted,
DiffSuppressFunc: func(_, _, _ string, d *schema.ResourceData) bool {
return d.Get(mkTemplate).(bool)
},
},
mkStartup: {
Type: schema.TypeList,
Description: "Defines startup and shutdown behavior of the VM",
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkStartupOrder: {
Type: schema.TypeInt,
Description: "A non-negative number defining the general startup order",
Optional: true,
Default: dvStartupOrder,
},
mkStartupUpDelay: {
Type: schema.TypeInt,
Description: "A non-negative number defining the delay in seconds before the next VM is started",
Optional: true,
Default: dvStartupUpDelay,
},
mkStartupDownDelay: {
Type: schema.TypeInt,
Description: "A non-negative number defining the delay in seconds before the next VM is shut down",
Optional: true,
Default: dvStartupDownDelay,
},
},
},
MaxItems: 1,
MinItems: 0,
},
mkTabletDevice: {
Type: schema.TypeBool,
Description: "Whether to enable the USB tablet device",
Optional: true,
Default: dvTabletDevice,
},
mkTags: {
Type: schema.TypeList,
Description: "Tags of the virtual machine. This is only meta information.",
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validation.StringIsNotEmpty,
},
DiffSuppressFunc: structure.SuppressIfListsAreEqualIgnoringOrder,
DiffSuppressOnRefresh: true,
},
mkTemplate: {
Type: schema.TypeBool,
Description: "Whether to create a template",
Optional: true,
ForceNew: true,
Default: dvTemplate,
},
mkTimeoutClone: {
Type: schema.TypeInt,
Description: "Clone VM timeout",
Optional: true,
Default: dvTimeoutClone,
},
mkTimeoutCreate: {
Type: schema.TypeInt,
Description: "Create VM timeout",
Optional: true,
Default: dvTimeoutCreate,
},
"timeout_move_disk": {
Type: schema.TypeInt,
Description: "MoveDisk timeout",
Optional: true,
Default: 1800,
Deprecated: "This field is deprecated and will be removed in a future release. " +
"An overall operation timeout (timeout_create / timeout_clone / timeout_migrate) is used instead.",
},
mkTimeoutMigrate: {
Type: schema.TypeInt,
Description: "Migrate VM timeout",
Optional: true,
Default: dvTimeoutMigrate,
},
mkTimeoutReboot: {
Type: schema.TypeInt,
Description: "Reboot timeout",
Optional: true,
Default: dvTimeoutReboot,
},
mkTimeoutShutdownVM: {
Type: schema.TypeInt,
Description: "Shutdown timeout",
Optional: true,
Default: dvTimeoutShutdownVM,
},
mkTimeoutStartVM: {
Type: schema.TypeInt,
Description: "Start VM timeout",
Optional: true,
Default: dvTimeoutStartVM,
},
mkTimeoutStopVM: {
Type: schema.TypeInt,
Description: "Stop VM timeout",
Optional: true,
Default: dvTimeoutStopVM,
},
mkVGA: {
Type: schema.TypeList,
Description: "The VGA configuration",
Optional: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{
map[string]interface{}{
mkVGAClipboard: dvVGAClipboard,
mkVGAMemory: dvVGAMemory,
mkVGAType: dvVGAType,
},
}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkVGAClipboard: {
Type: schema.TypeString,
Description: "Enable clipboard support. Set to `vnc` to enable clipboard support for VNC.",
Optional: true,
Default: dvVGAClipboard,
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{
"",
"vnc",
}, true)),
},
mkVGAMemory: {
Type: schema.TypeInt,
Description: "The VGA memory in megabytes (4-512 MB)",
Optional: true,
Default: dvVGAMemory,
ValidateDiagFunc: VGAMemoryValidator(),
},
mkVGAType: {
Type: schema.TypeString,
Description: "The VGA type",
Optional: true,
Default: dvVGAType,
ValidateDiagFunc: VGATypeValidator(),
},
},
},
MaxItems: 1,
MinItems: 0,
},
mkVMID: {
Type: schema.TypeInt,
Description: "The VM identifier",
Optional: true,
Computed: true,
// "ForceNew: true" handled in CustomizeDiff, making sure VMs with legacy configs with vm_id = -1
// do not require re-creation.
ValidateDiagFunc: VMIDValidator(),
},
mkSCSIHardware: {
Type: schema.TypeString,
Description: "The SCSI hardware type",
Optional: true,
Default: dvSCSIHardware,
ValidateDiagFunc: SCSIHardwareValidator(),
},
mkHookScriptFileID: {
Type: schema.TypeString,
Description: "A hook script",
Optional: true,
Default: dvHookScript,
},
mkStopOnDestroy: {
Type: schema.TypeBool,
Description: "Whether to stop rather than shutdown on VM destroy",
Optional: true,
Default: dvStopOnDestroy,
},
mkWatchdog: {
Type: schema.TypeList,
Description: "The watchdog configuration",
Optional: true,
DefaultFunc: func() (interface{}, error) {
return []interface{}{
map[string]interface{}{
mkWatchdogAction: dvWatchdogAction,
mkWatchdogEnabled: false,
mkWatchdogModel: dvWatchdogModel,
},
}, nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkWatchdogAction: {
Type: schema.TypeString,
Description: "The watchdog action",
Optional: true,
Default: dvWatchdogAction,
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{
"debug",
"none",
"pause",
"poweroff",
"reset",
"shutdown",
}, true)),
},
mkWatchdogEnabled: {
Type: schema.TypeBool,
Description: "Whether the watchdog is enabled",
Optional: true,
Default: false,
},
mkWatchdogModel: {
Type: schema.TypeString,
Description: "The watchdog model",
Optional: true,
Default: dvWatchdogModel,
ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{
"i6300esb",
"ib700",
}, true)),
},
},
},
MaxItems: 1,
MinItems: 0,
},
}
structure.MergeSchema(s, disk.Schema())
structure.MergeSchema(s, network.Schema())
return &schema.Resource{
Schema: s,
CreateContext: vmCreate,
ReadContext: vmRead,
UpdateContext: vmUpdate,
DeleteContext: vmDelete,
CustomizeDiff: customdiff.All(
customdiff.All(network.CustomizeDiff()...),
customdiff.ForceNewIf(
mkVMID,
func(_ context.Context, d *schema.ResourceDiff, _ interface{}) bool {
if !d.HasChange(mkVMID) {
return false
}
newValue := d.Get(mkVMID)
// 'vm_id' is ForceNew, except when changing 'vm_id' to existing correct id
// (automatic fix from -1 to actual vm_id must not re-create VM)
return strconv.Itoa(newValue.(int)) != d.Id()
},
),
customdiff.ForceNewIf(
mkNodeName,
func(_ context.Context, d *schema.ResourceDiff, _ interface{}) bool {
if !d.HasChange(mkNodeName) {
return false
}
return !d.Get(mkMigrate).(bool)
},
),
),
Importer: &schema.ResourceImporter{
StateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) {
node, id, err := parseImportIDWithNodeName(d.Id())
if err != nil {
return nil, err
}
d.SetId(id)
err = d.Set(mkNodeName, node)
if err != nil {
return nil, fmt.Errorf("failed setting state during import: %w", err)
}
return []*schema.ResourceData{d}, nil
},
},
}
}
func vmCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
clone := d.Get(mkClone).([]interface{})
// reset the default timeout for the create operation
ctx = context.WithoutCancel(ctx)
if len(clone) > 0 {
return vmCreateClone(ctx, d, m)
}
return vmCreateCustom(ctx, d, m)
}
// Check for an existing CloudInit IDE drive. If no such drive is found, return the specified `defaultValue`.
func findExistingCloudInitDrive(vmConfig *vms.GetResponseData, vmID int, defaultValue string) string {
devs := vmConfig.StorageDevices.Filter(func(device *vms.CustomStorageDevice) bool {
return device.IsCloudInitDrive(vmID)
})
for iface := range devs {
return iface
}
return defaultValue
}
// Return a pointer to the storage device configuration based on a name. The device name is assumed to be a
// valid ide, sata, or scsi interface name.
func getStorageDevice(vmConfig *vms.GetResponseData, deviceName string) *vms.CustomStorageDevice {
if dev, ok := vmConfig.StorageDevices[deviceName]; ok {
return dev
}
return nil
}
// Delete IDE interfaces that can then be used for CloudInit. The first interface will always
// be deleted. The second will be deleted only if it isn't empty and isn't the same as the
// first.
func deleteIdeDrives(ctx context.Context, vmAPI *vms.Client, itf1 string, itf2 string) diag.Diagnostics {
ddUpdateBody := &vms.UpdateRequestBody{}
ddUpdateBody.Delete = append(ddUpdateBody.Delete, itf1)
tflog.Debug(ctx, fmt.Sprintf("Deleting IDE interface '%s'", itf1))
if itf2 != "" && itf2 != itf1 {
ddUpdateBody.Delete = append(ddUpdateBody.Delete, itf2)
tflog.Debug(ctx, fmt.Sprintf("Deleting IDE interface '%s'", itf2))
}
e := vmAPI.UpdateVM(ctx, ddUpdateBody)
if e != nil {
return diag.FromErr(e)
}
return nil
}
// Start the VM, then wait for it to actually start; it may not be started immediately if running in HA mode.
func vmStart(ctx context.Context, vmAPI *vms.Client, d *schema.ResourceData) diag.Diagnostics {
tflog.Debug(ctx, "Starting VM")
startTimeoutSec := d.Get(mkTimeoutStartVM).(int)
ctx, cancel := context.WithTimeout(ctx, time.Duration(startTimeoutSec)*time.Second)
defer cancel()
var diags diag.Diagnostics
log, e := vmAPI.StartVM(ctx, startTimeoutSec)
if e != nil {
return diag.FromErr(e)
}
if len(log) > 0 {
lines := "\n\t| " + strings.Join(log, "\n\t| ")
diags = append(diags, diag.Diagnostic{
Severity: diag.Warning,
Summary: fmt.Sprintf("the VM startup task finished with a warning, task log:\n%s", lines),
})
}
return append(diags, diag.FromErr(vmAPI.WaitForVMStatus(ctx, "running"))...)
}
// Shutdown the VM, then wait for it to actually shut down (it may not be shut down immediately if
// running in HA mode).
func vmShutdown(ctx context.Context, vmAPI *vms.Client, d *schema.ResourceData) diag.Diagnostics {
tflog.Debug(ctx, "Shutting down VM")
forceStop := types.CustomBool(true)
shutdownTimeoutSec := d.Get(mkTimeoutShutdownVM).(int)
ctx, cancel := context.WithTimeout(ctx, time.Duration(shutdownTimeoutSec)*time.Second)
defer cancel()
e := vmAPI.ShutdownVM(ctx, &vms.ShutdownRequestBody{
ForceStop: &forceStop,
Timeout: &shutdownTimeoutSec,
})
if e != nil {
return diag.FromErr(e)
}
return diag.FromErr(vmAPI.WaitForVMStatus(ctx, "stopped"))
}
// Forcefully stop the VM, then wait for it to actually stop.
func vmStop(ctx context.Context, vmAPI *vms.Client, d *schema.ResourceData) diag.Diagnostics {
tflog.Debug(ctx, "Stopping VM")
stopTimeout := d.Get(mkTimeoutStopVM).(int)
ctx, cancel := context.WithTimeout(ctx, time.Duration(stopTimeout)*time.Second)
defer cancel()
e := vmAPI.StopVM(ctx)
if e != nil {
return diag.FromErr(e)
}
return diag.FromErr(vmAPI.WaitForVMStatus(ctx, "stopped"))
}
func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
cloneTimeoutSec := d.Get(mkTimeoutClone).(int)
ctx, cancel := context.WithTimeout(ctx, time.Duration(cloneTimeoutSec)*time.Second)
defer cancel()
config := m.(proxmoxtf.ProviderConfiguration)
client, e := config.GetClient()
if e != nil {
return diag.FromErr(e)
}
clone := d.Get(mkClone).([]interface{})
cloneBlock := clone[0].(map[string]interface{})
cloneRetries := cloneBlock[mkCloneRetries].(int)
cloneDatastoreID := cloneBlock[mkCloneDatastoreID].(string)
cloneNodeName := cloneBlock[mkCloneNodeName].(string)
cloneVMID := cloneBlock[mkCloneVMID].(int)
cloneFull := cloneBlock[mkCloneFull].(bool)
description := d.Get(mkDescription).(string)
name := d.Get(mkName).(string)
tags := d.Get(mkTags).([]interface{})
nodeName := d.Get(mkNodeName).(string)
poolID := d.Get(mkPoolID).(string)
vmIDUntyped, hasVMID := d.GetOk(mkVMID)
vmID := vmIDUntyped.(int)
if !hasVMID {
vmIDNew, err := config.GetIDGenerator().NextID(ctx)
if err != nil {
return diag.FromErr(err)
}
vmID = vmIDNew
err = d.Set(mkVMID, vmID)
if err != nil {
return diag.FromErr(err)
}
}
fullCopy := types.CustomBool(cloneFull)
cloneBody := &vms.CloneRequestBody{
FullCopy: &fullCopy,
VMIDNew: vmID,
}
if cloneDatastoreID != "" {
cloneBody.TargetStorage = &cloneDatastoreID
}
if description != "" {
cloneBody.Description = &description
}
if name != "" {
cloneBody.Name = &name
}
if poolID != "" {
cloneBody.PoolID = &poolID
}
if cloneNodeName != "" && cloneNodeName != nodeName {
// Check if any used datastores of the source VM are not shared
vmConfig, err := client.Node(cloneNodeName).VM(cloneVMID).GetVM(ctx)
if err != nil {
return diag.FromErr(err)
}
datastores := getDiskDatastores(vmConfig, d)
onlySharedDatastores := true
for _, datastore := range datastores {
datastoreStatus, err2 := client.Node(cloneNodeName).Storage(datastore).GetDatastoreStatus(ctx)
if err2 != nil {
return diag.FromErr(err2)
}
if datastoreStatus.Shared != nil && !*datastoreStatus.Shared {
onlySharedDatastores = false
break
}
}
if onlySharedDatastores {
// If the source and the target node are not the same, only clone directly to the target node if
// all used datastores in the source VM are shared. Directly cloning to non-shared storage
// on a different node is currently not supported by proxmox.
cloneBody.TargetNodeName = &nodeName
err = client.Node(cloneNodeName).VM(cloneVMID).CloneVM(ctx, cloneRetries, cloneBody)
if err != nil {
return diag.FromErr(err)
}
} else { //nolint:wsl
// If the source and the target node are not the same and any used datastore in the source VM is
// not shared, clone to the source node and then migrate to the target node. This is a workaround
// for missing functionality in the proxmox api as recommended per
// https://forum.proxmox.com/threads/500-cant-clone-to-non-shared-storage-local.49078/#post-229727
// Temporarily clone to local node
err = client.Node(cloneNodeName).VM(cloneVMID).CloneVM(ctx, cloneRetries, cloneBody)
if err != nil {
return diag.FromErr(err)
}
// Wait for the virtual machine to be created and its configuration lock to be released before migrating.
err = client.Node(cloneNodeName).VM(vmID).WaitForVMConfigUnlock(ctx, true)
if err != nil {
return diag.FromErr(err)
}
// Migrate to target node
withLocalDisks := types.CustomBool(true)
migrateBody := &vms.MigrateRequestBody{
TargetNode: nodeName,
WithLocalDisks: &withLocalDisks,
}
if cloneDatastoreID != "" {
migrateBody.TargetStorage = &cloneDatastoreID
}
err = client.Node(cloneNodeName).VM(vmID).MigrateVM(ctx, migrateBody)
if err != nil {
return diag.FromErr(err)
}
}
} else {
e = client.Node(nodeName).VM(cloneVMID).CloneVM(ctx, cloneRetries, cloneBody)
}
if e != nil {
return diag.FromErr(e)
}
d.SetId(strconv.Itoa(vmID))
vmAPI := client.Node(nodeName).VM(vmID)
// Wait for the virtual machine to be created and its configuration lock to be released.
e = vmAPI.WaitForVMConfigUnlock(ctx, true)
if e != nil {
return diag.FromErr(e)
}
// Now that the virtual machine has been cloned, we need to perform some modifications.
acpi := types.CustomBool(d.Get(mkACPI).(bool))
agent := d.Get(mkAgent).([]interface{})
audioDevices := vmGetAudioDeviceList(d)
bios := d.Get(mkBIOS).(string)
kvmArguments := d.Get(mkKVMArguments).(string)
scsiHardware := d.Get(mkSCSIHardware).(string)
cdrom := d.Get(mkCDROM).([]interface{})
cpu := d.Get(mkCPU).([]interface{})
initialization := d.Get(mkInitialization).([]interface{})
hostPCI := d.Get(mkHostPCI).([]interface{})
hostUSB := d.Get(mkHostUSB).([]interface{})
keyboardLayout := d.Get(mkKeyboardLayout).(string)
memory := d.Get(mkMemory).([]interface{})
numa := d.Get(mkNUMA).([]interface{})
operatingSystem := d.Get(mkOperatingSystem).([]interface{})
serialDevice := d.Get(mkSerialDevice).([]interface{})
onBoot := types.CustomBool(d.Get(mkOnBoot).(bool))
tabletDevice := types.CustomBool(d.Get(mkTabletDevice).(bool))
protection := types.CustomBool(d.Get(mkProtection).(bool))
template := types.CustomBool(d.Get(mkTemplate).(bool))
vga := d.Get(mkVGA).([]interface{})
watchdog := d.Get(mkWatchdog).([]interface{})
updateBody := &vms.UpdateRequestBody{
AudioDevices: audioDevices,
}
ideDevices := vms.CustomStorageDevices{}
var del []string
//nolint:gosimple
if acpi != dvACPI {
updateBody.ACPI = &acpi
}
if len(agent) > 0 && agent[0] != nil {
agentBlock := agent[0].(map[string]interface{})
agentEnabled := types.CustomBool(
agentBlock[mkAgentEnabled].(bool),
)
agentTrim := types.CustomBool(agentBlock[mkAgentTrim].(bool))
agentType := agentBlock[mkAgentType].(string)
updateBody.Agent = &vms.CustomAgent{
Enabled: &agentEnabled,
TrimClonedDisks: &agentTrim,
Type: &agentType,
}
}
if kvmArguments != dvKVMArguments {
updateBody.KVMArguments = &kvmArguments
}
if bios != dvBIOS {
updateBody.BIOS = &bios
}
if scsiHardware != dvSCSIHardware {
updateBody.SCSIHardware = &scsiHardware
}
if len(cdrom) > 0 && cdrom[0] != nil {
cdromBlock := cdrom[0].(map[string]interface{})
cdromEnabled := cdromBlock[mkCDROMEnabled].(bool)
cdromFileID := cdromBlock[mkCDROMFileID].(string)
cdromInterface := cdromBlock[mkCDROMInterface].(string)
if cdromFileID == "" {
cdromFileID = "cdrom"
}
cdromMedia := "cdrom"
ideDevices[cdromInterface] = &vms.CustomStorageDevice{
Enabled: cdromEnabled,
FileVolume: cdromFileID,
Media: &cdromMedia,
}
}
if len(cpu) > 0 && cpu[0] != nil {
cpuBlock := cpu[0].(map[string]interface{})
cpuArchitecture := cpuBlock[mkCPUArchitecture].(string)
cpuCores := cpuBlock[mkCPUCores].(int)
cpuFlags := cpuBlock[mkCPUFlags].([]interface{})
cpuHotplugged := cpuBlock[mkCPUHotplugged].(int)
cpuLimit := cpuBlock[mkCPULimit].(int)
cpuNUMA := types.CustomBool(cpuBlock[mkCPUNUMA].(bool))
cpuSockets := cpuBlock[mkCPUSockets].(int)
cpuType := cpuBlock[mkCPUType].(string)
cpuUnits := cpuBlock[mkCPUUnits].(int)
cpuAffinity := cpuBlock[mkCPUAffinity].(string)
cpuFlagsConverted := make([]string, len(cpuFlags))
for fi, flag := range cpuFlags {
cpuFlagsConverted[fi] = flag.(string)
}
if err := setCPUArchitecture(ctx, cpuArchitecture, client, updateBody); err != nil {
return diag.FromErr(err)
}
updateBody.CPUCores = ptr.Ptr(int64(cpuCores))
updateBody.CPUEmulation = &vms.CustomCPUEmulation{
Flags: &cpuFlagsConverted,
Type: cpuType,
}
updateBody.NUMAEnabled = &cpuNUMA
updateBody.CPUSockets = ptr.Ptr(int64(cpuSockets))
updateBody.CPUUnits = ptr.Ptr(int64(cpuUnits))
if cpuAffinity != "" {
updateBody.CPUAffinity = &cpuAffinity
}
if cpuHotplugged > 0 {
updateBody.VirtualCPUCount = ptr.Ptr(int64(cpuHotplugged))
}
if cpuLimit > 0 {
updateBody.CPULimit = ptr.Ptr(int64(cpuLimit))
}
}
vmConfig, err := vmAPI.GetVM(ctx)
if err != nil {
return diag.FromErr(err)
}
if len(initialization) > 0 && initialization[0] != nil {
tflog.Trace(ctx, "Preparing the CloudInit configuration")
initializationBlock := initialization[0].(map[string]interface{})
initializationDatastoreID := initializationBlock[mkInitializationDatastoreID].(string)
initializationInterface := initializationBlock[mkInitializationInterface].(string)
existingInterface := findExistingCloudInitDrive(vmConfig, vmID, "ide2")
if initializationInterface == "" {
initializationInterface = existingInterface
}
tflog.Trace(ctx, fmt.Sprintf("CloudInit IDE interface is '%s'", initializationInterface))
const cdromCloudInitEnabled = true
cdromCloudInitFileID := fmt.Sprintf("%s:cloudinit", initializationDatastoreID)
cdromCloudInitMedia := "cdrom"
ideDevices[initializationInterface] = &vms.CustomStorageDevice{
Enabled: cdromCloudInitEnabled,
FileVolume: cdromCloudInitFileID,
Media: &cdromCloudInitMedia,
}
if err := deleteIdeDrives(ctx, vmAPI, initializationInterface, existingInterface); err != nil {
return err
}
updateBody.CloudInitConfig = vmGetCloudInitConfig(d)
}
if len(hostPCI) > 0 {
updateBody.PCIDevices = vmGetHostPCIDeviceObjects(d)
}
if len(numa) > 0 {
updateBody.NUMADevices = vmGetNumaDeviceObjects(d)
}
if len(hostUSB) > 0 {
updateBody.USBDevices = vmGetHostUSBDeviceObjects(d)
}
if len(cdrom) > 0 || len(initialization) > 0 {
for iface, dev := range ideDevices {
updateBody.AddCustomStorageDevice(iface, *dev)
}
}
if keyboardLayout != dvKeyboardLayout {
updateBody.KeyboardLayout = &keyboardLayout
}
if len(memory) > 0 && memory[0] != nil {
memoryBlock := memory[0].(map[string]interface{})
memoryDedicated := memoryBlock[mkMemoryDedicated].(int)
memoryFloating := memoryBlock[mkMemoryFloating].(int)
memoryShared := memoryBlock[mkMemoryShared].(int)
hugepages := memoryBlock[mkMemoryHugepages].(string)
keepHugepages := types.CustomBool(memoryBlock[mkMemoryKeepHugepages].(bool))
updateBody.DedicatedMemory = &memoryDedicated
updateBody.FloatingMemory = &memoryFloating
if memoryShared > 0 {
memorySharedName := fmt.Sprintf("vm-%d-ivshmem", vmID)
updateBody.SharedMemory = &vms.CustomSharedMemory{
Name: &memorySharedName,
Size: memoryShared,
}
}
if hugepages != "" {
updateBody.Hugepages = &hugepages
}
if keepHugepages {
updateBody.KeepHugepages = &keepHugepages
}
}
networkDevice := d.Get(network.MkNetworkDevice).([]interface{})
if len(networkDevice) > 0 {
updateBody.NetworkDevices, err = network.GetNetworkDeviceObjects(d)
if err != nil {
return diag.FromErr(err)
}
for i, ni := range updateBody.NetworkDevices {
if !ni.Enabled {
del = append(del, fmt.Sprintf("net%d", i))
}
}
for i := len(updateBody.NetworkDevices); i < network.MaxNetworkDevices; i++ {
del = append(del, fmt.Sprintf("net%d", i))
}
}
if len(operatingSystem) > 0 && operatingSystem[0] != nil {
operatingSystemBlock := operatingSystem[0].(map[string]interface{})
operatingSystemType := operatingSystemBlock[mkOperatingSystemType].(string)
updateBody.OSType = &operatingSystemType
}
if len(serialDevice) > 0 {
updateBody.SerialDevices = vmGetSerialDeviceList(d)
for i := len(updateBody.SerialDevices); i < maxResourceVirtualEnvironmentVMSerialDevices; i++ {
del = append(del, fmt.Sprintf("serial%d", i))
}
}
updateBody.StartOnBoot = &onBoot
updateBody.SMBIOS = vmGetSMBIOS(d)
updateBody.StartupOrder = vmGetStartupOrder(d)
//nolint:gosimple
if tabletDevice != dvTabletDevice {
updateBody.TabletDeviceEnabled = &tabletDevice
}
//nolint:gosimple
if protection != dvProtection {
updateBody.DeletionProtection = &protection
}
if len(tags) > 0 {
tagString := vmGetTagsString(d)
updateBody.Tags = &tagString
}
//nolint:gosimple
if template != dvTemplate {
updateBody.Template = &template
}
if len(vga) > 0 {
vgaDevice, err := vmGetVGADeviceObject(d)
if err != nil {
return diag.FromErr(err)
}
updateBody.VGADevice = vgaDevice
}
hookScript := d.Get(mkHookScriptFileID).(string)
currentHookScript := vmConfig.HookScript
if len(hookScript) > 0 {
updateBody.HookScript = &hookScript
} else if currentHookScript != nil {
del = append(del, "hookscript")
}
if len(watchdog) > 0 && watchdog[0] != nil {
watchdogBlock := watchdog[0].(map[string]interface{})
watchdogEnabled := types.CustomBool(
watchdogBlock[mkWatchdogEnabled].(bool),
)
if watchdogEnabled {
watchdogAction := watchdogBlock[mkWatchdogAction].(string)
watchdogModel := watchdogBlock[mkWatchdogModel].(string)
updateBody.WatchdogDevice = &vms.CustomWatchdogDevice{
Action: &watchdogAction,
Model: &watchdogModel,
}
}
}
updateBody.Delete = del
e = vmAPI.UpdateVM(ctx, updateBody)
if e != nil {
return diag.FromErr(e)
}
vmConfig, e = vmAPI.GetVM(ctx)
if e != nil {
if errors.Is(e, api.ErrResourceDoesNotExist) {
d.SetId("")
return nil
}
return diag.FromErr(e)
}
allDiskInfo := disk.GetInfo(vmConfig, d) // from the cloned VM
planDisks, e := disk.GetDiskDeviceObjects(d, VM(), nil) // from the resource config
if e != nil {
return diag.FromErr(e)
}
e = disk.CreateClone(ctx, d, planDisks, allDiskInfo, vmAPI)
if e != nil {
return diag.FromErr(e)
}
efiDisk := d.Get(mkEFIDisk).([]interface{})
efiDiskInfo := vmGetEfiDisk(d, nil) // from the resource config
for i := range efiDisk {
diskBlock := efiDisk[i].(map[string]interface{})
diskInterface := "efidisk0"
dataStoreID := diskBlock[mkEFIDiskDatastoreID].(string)
efiType := diskBlock[mkEFIDiskType].(string)
currentDiskInfo := vmConfig.EFIDisk
configuredDiskInfo := efiDiskInfo
if currentDiskInfo == nil {
diskUpdateBody := &vms.UpdateRequestBody{}
diskUpdateBody.EFIDisk = configuredDiskInfo
e = vmAPI.UpdateVM(ctx, diskUpdateBody)
if e != nil {
return diag.FromErr(e)
}
continue
}
if efiType != *currentDiskInfo.Type {
return diag.Errorf(
"resizing of efidisks is not supported.",
)
}
deleteOriginalDisk := types.CustomBool(true)
diskMoveBody := &vms.MoveDiskRequestBody{
DeleteOriginalDisk: &deleteOriginalDisk,
Disk: diskInterface,
TargetStorage: dataStoreID,
}
moveDisk := false
if dataStoreID != "" {
moveDisk = true
if allDiskInfo[diskInterface] != nil {
fileIDParts := strings.Split(allDiskInfo[diskInterface].FileVolume, ":")
moveDisk = dataStoreID != fileIDParts[0]
}
}
if moveDisk {
e = vmAPI.MoveVMDisk(ctx, diskMoveBody)
if e != nil {
return diag.FromErr(e)
}
}
}
tpmState := d.Get(mkTPMState).([]interface{})
tpmStateInfo := vmGetTPMState(d, nil) // from the resource config
for i := range tpmState {
diskBlock := tpmState[i].(map[string]interface{})
diskInterface := "tpmstate0"
dataStoreID := diskBlock[mkTPMStateDatastoreID].(string)
currentTPMState := vmConfig.TPMState
configuredTPMStateInfo := tpmStateInfo
if currentTPMState == nil {
diskUpdateBody := &vms.UpdateRequestBody{}
diskUpdateBody.TPMState = configuredTPMStateInfo
e = vmAPI.UpdateVM(ctx, diskUpdateBody)
if e != nil {
return diag.FromErr(e)
}
continue
}
deleteOriginalDisk := types.CustomBool(true)
diskMoveBody := &vms.MoveDiskRequestBody{
DeleteOriginalDisk: &deleteOriginalDisk,
Disk: diskInterface,
TargetStorage: dataStoreID,
}
moveDisk := false
if dataStoreID != "" {
moveDisk = true
if allDiskInfo[diskInterface] != nil {
fileIDParts := strings.Split(allDiskInfo[diskInterface].FileVolume, ":")
moveDisk = dataStoreID != fileIDParts[0]
}
}
if moveDisk {
e = vmAPI.MoveVMDisk(ctx, diskMoveBody)
if e != nil {
return diag.FromErr(e)
}
}
}
return vmCreateStart(ctx, d, m)
}
func setCPUArchitecture(
ctx context.Context,
cpuArchitecture string,
client proxmox.Client,
updateBody *vms.UpdateRequestBody,
) error {
// Only the root account is allowed to change the CPU architecture.
if cpuArchitecture != "" {
if client.API().IsRootTicket(ctx) {
updateBody.CPUArchitecture = &cpuArchitecture
} else {
return errors.New("the `cpu.architecture` can only be set by the root account. " +
"Please switch to the root account or remove the `cpu.architecture` from the VM configuration")
}
}
return nil
}
func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
createTimeoutSec := d.Get(mkTimeoutCreate).(int)
ctx, cancel := context.WithTimeout(ctx, time.Duration(createTimeoutSec)*time.Second)
defer cancel()
config := m.(proxmoxtf.ProviderConfiguration)
client, err := config.GetClient()
if err != nil {
return diag.FromErr(err)
}
resource := VM()
acpi := types.CustomBool(d.Get(mkACPI).(bool))
agentBlock, err := structure.GetSchemaBlock(
resource,
d,
[]string{mkAgent},
0,
true,
)
if err != nil {
return diag.FromErr(err)
}
agentEnabled := types.CustomBool(
agentBlock[mkAgentEnabled].(bool),
)
agentTrim := types.CustomBool(agentBlock[mkAgentTrim].(bool))
agentType := agentBlock[mkAgentType].(string)
kvmArguments := d.Get(mkKVMArguments).(string)
audioDevices := vmGetAudioDeviceList(d)
bios := d.Get(mkBIOS).(string)
cdromBlock, err := structure.GetSchemaBlock(
resource,
d,
[]string{mkCDROM},
0,
true,
)
if err != nil {
return diag.FromErr(err)
}
cdromEnabled := cdromBlock[mkCDROMEnabled].(bool)
cdromFileID := cdromBlock[mkCDROMFileID].(string)
cdromInterface := cdromBlock[mkCDROMInterface].(string)
cdromCloudInitEnabled := false
cdromCloudInitFileID := ""
cdromCloudInitInterface := ""
if cdromFileID == "" {
cdromFileID = "cdrom"
}
cpuBlock, err := structure.GetSchemaBlock(
resource,
d,
[]string{mkCPU},
0,
true,
)
if err != nil {
return diag.FromErr(err)
}
cpuArchitecture := cpuBlock[mkCPUArchitecture].(string)
cpuCores := cpuBlock[mkCPUCores].(int)
cpuFlags := cpuBlock[mkCPUFlags].([]interface{})
cpuHotplugged := cpuBlock[mkCPUHotplugged].(int)
cpuLimit := cpuBlock[mkCPULimit].(int)
cpuSockets := cpuBlock[mkCPUSockets].(int)
cpuNUMA := types.CustomBool(cpuBlock[mkCPUNUMA].(bool))
cpuType := cpuBlock[mkCPUType].(string)
cpuUnits := cpuBlock[mkCPUUnits].(int)
cpuAffinity := cpuBlock[mkCPUAffinity].(string)
description := d.Get(mkDescription).(string)
var efiDisk *vms.CustomEFIDisk
efiDiskBlock := d.Get(mkEFIDisk).([]interface{})
if len(efiDiskBlock) > 0 && efiDiskBlock[0] != nil {
block := efiDiskBlock[0].(map[string]interface{})
datastoreID, _ := block[mkEFIDiskDatastoreID].(string)
fileFormat, _ := block[mkEFIDiskFileFormat].(string)
efiType, _ := block[mkEFIDiskType].(string)
preEnrolledKeys := types.CustomBool(block[mkEFIDiskPreEnrolledKeys].(bool))
if fileFormat == "" {
fileFormat = dvEFIDiskFileFormat
}
efiDisk = &vms.CustomEFIDisk{
Type: &efiType,
FileVolume: fmt.Sprintf("%s:1", datastoreID),
Format: &fileFormat,
PreEnrolledKeys: &preEnrolledKeys,
}
}
var tpmState *vms.CustomTPMState
tpmStateBlock := d.Get(mkTPMState).([]interface{})
if len(tpmStateBlock) > 0 && tpmStateBlock[0] != nil {
block := tpmStateBlock[0].(map[string]interface{})
datastoreID, _ := block[mkTPMStateDatastoreID].(string)
version, _ := block[mkTPMStateVersion].(string)
if version == "" {
version = dvTPMStateVersion
}
tpmState = &vms.CustomTPMState{
FileVolume: fmt.Sprintf("%s:1", datastoreID),
Version: &version,
}
}
initializationConfig := vmGetCloudInitConfig(d)
initializationAttr := d.Get(mkInitialization)
if initializationConfig != nil && initializationAttr != nil {
initialization := initializationAttr.([]interface{})
initializationBlock := initialization[0].(map[string]interface{})
initializationDatastoreID := initializationBlock[mkInitializationDatastoreID].(string)
cdromCloudInitEnabled = true
cdromCloudInitFileID = fmt.Sprintf("%s:cloudinit", initializationDatastoreID)
cdromCloudInitInterface = initializationBlock[mkInitializationInterface].(string)
if cdromCloudInitInterface == "" {
cdromCloudInitInterface = "ide2"
}
}
pciDeviceObjects := vmGetHostPCIDeviceObjects(d)
numaDeviceObjects := vmGetNumaDeviceObjects(d)
usbDeviceObjects := vmGetHostUSBDeviceObjects(d)
keyboardLayout := d.Get(mkKeyboardLayout).(string)
memoryBlock, err := structure.GetSchemaBlock(
resource,
d,
[]string{mkMemory},
0,
true,
)
if err != nil {
return diag.FromErr(err)
}
memoryDedicated := memoryBlock[mkMemoryDedicated].(int)
memoryFloating := memoryBlock[mkMemoryFloating].(int)
memoryShared := memoryBlock[mkMemoryShared].(int)
memoryHugepages := memoryBlock[mkMemoryHugepages].(string)
memoryKeepHugepages := types.CustomBool(memoryBlock[mkMemoryKeepHugepages].(bool))
machine := d.Get(mkMachine).(string)
name := d.Get(mkName).(string)
tags := d.Get(mkTags).([]interface{})
networkDeviceObjects, err := network.GetNetworkDeviceObjects(d)
if err != nil {
return diag.FromErr(err)
}
nodeName := d.Get(mkNodeName).(string)
operatingSystem, err := structure.GetSchemaBlock(
resource,
d,
[]string{mkOperatingSystem},
0,
true,
)
if err != nil {
return diag.FromErr(err)
}
operatingSystemType := operatingSystem[mkOperatingSystemType].(string)
poolID := d.Get(mkPoolID).(string)
protection := types.CustomBool(d.Get(mkProtection).(bool))
serialDevices := vmGetSerialDeviceList(d)
smbios := vmGetSMBIOS(d)
startupOrder := vmGetStartupOrder(d)
onBoot := types.CustomBool(d.Get(mkOnBoot).(bool))
tabletDevice := types.CustomBool(d.Get(mkTabletDevice).(bool))
template := types.CustomBool(d.Get(mkTemplate).(bool))
vgaDevice, err := vmGetVGADeviceObject(d)
if err != nil {
return diag.FromErr(err)
}
vmIDUntyped, hasVMID := d.GetOk(mkVMID)
vmID := vmIDUntyped.(int)
if !hasVMID {
vmIDNew, e := config.GetIDGenerator().NextID(ctx)
if e != nil {
return diag.FromErr(e)
}
vmID = vmIDNew
e = d.Set(mkVMID, vmID)
if e != nil {
return diag.FromErr(e)
}
}
diskDeviceObjects, err := disk.GetDiskDeviceObjects(d, resource, nil)
if err != nil {
return diag.FromErr(err)
}
var bootOrderConverted []string
if cdromEnabled {
bootOrderConverted = []string{cdromInterface}
}
bootOrder := d.Get(mkBootOrder).([]interface{})
if len(bootOrder) == 0 {
if _, ok := diskDeviceObjects["ide0"]; ok {
bootOrderConverted = append(bootOrderConverted, "ide0")
}
if _, ok := diskDeviceObjects["sata0"]; ok {
bootOrderConverted = append(bootOrderConverted, "sata0")
}
if _, ok := diskDeviceObjects["scsi0"]; ok {
bootOrderConverted = append(bootOrderConverted, "scsi0")
}
if _, ok := diskDeviceObjects["virtio0"]; ok {
bootOrderConverted = append(bootOrderConverted, "virtio0")
}
if networkDeviceObjects != nil {
bootOrderConverted = append(bootOrderConverted, "net0")
}
} else {
bootOrderConverted = make([]string, len(bootOrder))
for i, device := range bootOrder {
bootOrderConverted[i] = device.(string)
}
}
cpuFlagsConverted := make([]string, len(cpuFlags))
for fi, flag := range cpuFlags {
cpuFlagsConverted[fi] = flag.(string)
}
ideDevice2Media := "cdrom"
if cdromCloudInitInterface != "" {
diskDeviceObjects[cdromCloudInitInterface] = &vms.CustomStorageDevice{
Enabled: cdromCloudInitEnabled,
FileVolume: cdromCloudInitFileID,
Media: &ideDevice2Media,
}
}
if cdromInterface != "" {
diskDeviceObjects[cdromInterface] = &vms.CustomStorageDevice{
Enabled: cdromEnabled,
FileVolume: cdromFileID,
Media: &ideDevice2Media,
}
}
var memorySharedObject *vms.CustomSharedMemory
if memoryShared > 0 {
memorySharedName := fmt.Sprintf("vm-%d-ivshmem", vmID)
memorySharedObject = &vms.CustomSharedMemory{
Name: &memorySharedName,
Size: memoryShared,
}
}
scsiHardware := d.Get(mkSCSIHardware).(string)
var watchdogObject *vms.CustomWatchdogDevice
watchdogBlock, err := structure.GetSchemaBlock(
resource,
d,
[]string{mkWatchdog},
0,
true,
)
if err != nil {
return diag.FromErr(err)
}
watchdogEnabled := types.CustomBool(
watchdogBlock[mkWatchdogEnabled].(bool),
)
if watchdogEnabled {
watchdogAction := watchdogBlock[mkWatchdogAction].(string)
watchdogModel := watchdogBlock[mkWatchdogModel].(string)
watchdogObject = &vms.CustomWatchdogDevice{
Action: &watchdogAction,
Model: &watchdogModel,
}
}
createBody := &vms.CreateRequestBody{
ACPI: &acpi,
Agent: &vms.CustomAgent{
Enabled: &agentEnabled,
TrimClonedDisks: &agentTrim,
Type: &agentType,
},
AudioDevices: audioDevices,
BIOS: &bios,
Boot: &vms.CustomBoot{
Order: &bootOrderConverted,
},
CloudInitConfig: initializationConfig,
CPUCores: ptr.Ptr(int64(cpuCores)),
CPUEmulation: &vms.CustomCPUEmulation{
Flags: &cpuFlagsConverted,
Type: cpuType,
},
CPUSockets: ptr.Ptr(int64(cpuSockets)),
CPUUnits: ptr.Ptr(int64(cpuUnits)),
DedicatedMemory: &memoryDedicated,
DeletionProtection: &protection,
EFIDisk: efiDisk,
TPMState: tpmState,
FloatingMemory: &memoryFloating,
KeyboardLayout: &keyboardLayout,
NetworkDevices: networkDeviceObjects,
NUMAEnabled: &cpuNUMA,
NUMADevices: numaDeviceObjects,
OSType: &operatingSystemType,
PCIDevices: pciDeviceObjects,
SCSIHardware: &scsiHardware,
SerialDevices: serialDevices,
SharedMemory: memorySharedObject,
StartOnBoot: &onBoot,
SMBIOS: smbios,
StartupOrder: startupOrder,
TabletDeviceEnabled: &tabletDevice,
Template: &template,
USBDevices: usbDeviceObjects,
VGADevice: vgaDevice,
VMID: vmID,
WatchdogDevice: watchdogObject,
CustomStorageDevices: diskDeviceObjects,
}
if err = setCPUArchitecture(ctx, cpuArchitecture, client, createBody); err != nil {
return diag.FromErr(err)
}
if cpuHotplugged > 0 {
createBody.VirtualCPUCount = ptr.Ptr(int64(cpuHotplugged))
}
if cpuLimit > 0 {
createBody.CPULimit = ptr.Ptr(int64(cpuLimit))
}
if cpuAffinity != "" {
createBody.CPUAffinity = &cpuAffinity
}
if description != "" {
createBody.Description = &description
}
if len(tags) > 0 {
tagsString := vmGetTagsString(d)
createBody.Tags = &tagsString
}
if kvmArguments != "" {
createBody.KVMArguments = &kvmArguments
}
if machine != "" {
createBody.Machine = &machine
}
if memoryHugepages != "" {
createBody.Hugepages = &memoryHugepages
}
if memoryKeepHugepages {
createBody.KeepHugepages = &memoryKeepHugepages
}
if name != "" {
createBody.Name = &name
}
if poolID != "" {
createBody.PoolID = &poolID
}
hookScript := d.Get(mkHookScriptFileID).(string)
if len(hookScript) > 0 {
createBody.HookScript = &hookScript
}
err = client.Node(nodeName).VM(0).CreateVM(ctx, createBody)
if err != nil {
return diag.FromErr(err)
}
d.SetId(strconv.Itoa(vmID))
diags := disk.CreateCustomDisks(ctx, client, nodeName, vmID, diskDeviceObjects)
if diags.HasError() {
return diags
}
return vmCreateStart(ctx, d, m)
}
func vmCreateStart(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
started := d.Get(mkStarted).(bool)
template := d.Get(mkTemplate).(bool)
reboot := d.Get(mkRebootAfterCreation).(bool)
if !started || template {
return vmRead(ctx, d, m)
}
config := m.(proxmoxtf.ProviderConfiguration)
client, err := config.GetClient()
if err != nil {
return diag.FromErr(err)
}
nodeName := d.Get(mkNodeName).(string)
vmID, err := strconv.Atoi(d.Id())
if err != nil {
return diag.FromErr(err)
}
vmAPI := client.Node(nodeName).VM(vmID)
// Start the virtual machine and wait for it to reach a running state before continuing.
if diags := vmStart(ctx, vmAPI, d); diags != nil {
return diags
}
if reboot {
rebootTimeoutSec := d.Get(mkTimeoutReboot).(int)
err := vmAPI.RebootVM(
ctx,
&vms.RebootRequestBody{
Timeout: &rebootTimeoutSec,
},
)
if err != nil {
return diag.FromErr(err)
}
}
return vmRead(ctx, d, m)
}
func vmGetAudioDeviceList(d *schema.ResourceData) vms.CustomAudioDevices {
devices := d.Get(mkAudioDevice).([]interface{})
list := make(vms.CustomAudioDevices, len(devices))
for i, v := range devices {
block := v.(map[string]interface{})
device, _ := block[mkAudioDeviceDevice].(string)
driver, _ := block[mkAudioDeviceDriver].(string)
enabled, _ := block[mkAudioDeviceEnabled].(bool)
list[i].Device = device
list[i].Driver = &driver
list[i].Enabled = enabled
}
return list
}
func vmGetCloudInitConfig(d *schema.ResourceData) *vms.CustomCloudInitConfig {
initialization := d.Get(mkInitialization).([]interface{})
if len(initialization) == 0 || initialization[0] == nil {
return nil
}
var initializationConfig *vms.CustomCloudInitConfig
initializationBlock := initialization[0].(map[string]interface{})
initializationConfig = &vms.CustomCloudInitConfig{}
initializationDNS := initializationBlock[mkInitializationDNS].([]interface{})
if len(initializationDNS) > 0 && initializationDNS[0] != nil {
initializationDNSBlock := initializationDNS[0].(map[string]interface{})
domain := initializationDNSBlock[mkInitializationDNSDomain].(string)
if domain != "" {
initializationConfig.SearchDomain = &domain
}
servers := initializationDNSBlock[mkInitializationDNSServers].([]interface{})
deprecatedServer := initializationDNSBlock[mkInitializationDNSServer].(string)
if len(servers) > 0 {
nameserver := strings.Join(utils.ConvertToStringSlice(servers), " ")
initializationConfig.Nameserver = &nameserver
} else if deprecatedServer != "" {
initializationConfig.Nameserver = &deprecatedServer
}
}
initializationIPConfig := initializationBlock[mkInitializationIPConfig].([]interface{})
initializationConfig.IPConfig = make([]vms.CustomCloudInitIPConfig, len(initializationIPConfig))
for i, c := range initializationIPConfig {
configBlock := c.(map[string]interface{})
ipv4 := configBlock[mkInitializationIPConfigIPv4].([]interface{})
if len(ipv4) > 0 && ipv4[0] != nil {
ipv4Block := ipv4[0].(map[string]interface{})
ipv4Address := ipv4Block[mkInitializationIPConfigIPv4Address].(string)
if ipv4Address != "" {
initializationConfig.IPConfig[i].IPv4 = &ipv4Address
}
ipv4Gateway := ipv4Block[mkInitializationIPConfigIPv4Gateway].(string)
if ipv4Gateway != "" {
initializationConfig.IPConfig[i].GatewayIPv4 = &ipv4Gateway
}
}
ipv6 := configBlock[mkInitializationIPConfigIPv6].([]interface{})
if len(ipv6) > 0 && ipv6[0] != nil {
ipv6Block := ipv6[0].(map[string]interface{})
ipv6Address := ipv6Block[mkInitializationIPConfigIPv6Address].(string)
if ipv6Address != "" {
initializationConfig.IPConfig[i].IPv6 = &ipv6Address
}
ipv6Gateway := ipv6Block[mkInitializationIPConfigIPv6Gateway].(string)
if ipv6Gateway != "" {
initializationConfig.IPConfig[i].GatewayIPv6 = &ipv6Gateway
}
}
}
initializationUserAccount := initializationBlock[mkInitializationUserAccount].([]interface{})
if len(initializationUserAccount) > 0 && initializationUserAccount[0] != nil {
initializationUserAccountBlock := initializationUserAccount[0].(map[string]interface{})
keys := initializationUserAccountBlock[mkInitializationUserAccountKeys].([]interface{})
if len(keys) > 0 {
sshKeys := make(vms.CustomCloudInitSSHKeys, len(keys))
for i, k := range keys {
if k != nil {
sshKeys[i] = k.(string)
}
}
initializationConfig.SSHKeys = &sshKeys
}
password := initializationUserAccountBlock[mkInitializationUserAccountPassword].(string)
if password != "" {
initializationConfig.Password = &password
}
username := initializationUserAccountBlock[mkInitializationUserAccountUsername].(string)
initializationConfig.Username = &username
}
initializationUserDataFileID := initializationBlock[mkInitializationUserDataFileID].(string)
if initializationUserDataFileID != "" {
initializationConfig.Files = &vms.CustomCloudInitFiles{
UserVolume: &initializationUserDataFileID,
}
}
initializationVendorDataFileID := initializationBlock[mkInitializationVendorDataFileID].(string)
if initializationVendorDataFileID != "" {
if initializationConfig.Files == nil {
initializationConfig.Files = &vms.CustomCloudInitFiles{}
}
initializationConfig.Files.VendorVolume = &initializationVendorDataFileID
}
initializationNetworkDataFileID := initializationBlock[mkInitializationNetworkDataFileID].(string)
if initializationNetworkDataFileID != "" {
if initializationConfig.Files == nil {
initializationConfig.Files = &vms.CustomCloudInitFiles{}
}
initializationConfig.Files.NetworkVolume = &initializationNetworkDataFileID
}
initializationMetaDataFileID := initializationBlock[mkInitializationMetaDataFileID].(string)
if initializationMetaDataFileID != "" {
if initializationConfig.Files == nil {
initializationConfig.Files = &vms.CustomCloudInitFiles{}
}
initializationConfig.Files.MetaVolume = &initializationMetaDataFileID
}
initializationType := initializationBlock[mkInitializationType].(string)
if initializationType != "" {
initializationConfig.Type = &initializationType
}
return initializationConfig
}
func vmGetEfiDisk(d *schema.ResourceData, disk []interface{}) *vms.CustomEFIDisk {
var efiDisk []interface{}
if disk != nil {
efiDisk = disk
} else {
efiDisk = d.Get(mkEFIDisk).([]interface{})
}
var efiDiskConfig *vms.CustomEFIDisk
if len(efiDisk) > 0 && efiDisk[0] != nil {
efiDiskConfig = &vms.CustomEFIDisk{}
block := efiDisk[0].(map[string]interface{})
datastoreID, _ := block[mkEFIDiskDatastoreID].(string)
fileFormat, _ := block[mkEFIDiskFileFormat].(string)
efiType, _ := block[mkEFIDiskType].(string)
preEnrolledKeys := types.CustomBool(block[mkEFIDiskPreEnrolledKeys].(bool))
// use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume.
// NB SIZE_IN_GiB is ignored, see docs for more info.
efiDiskConfig.FileVolume = fmt.Sprintf("%s:1", datastoreID)
efiDiskConfig.Format = &fileFormat
efiDiskConfig.Type = &efiType
efiDiskConfig.PreEnrolledKeys = &preEnrolledKeys
}
return efiDiskConfig
}
func vmGetEfiDiskAsStorageDevice(d *schema.ResourceData, disk []interface{}) (*vms.CustomStorageDevice, error) {
efiDisk := vmGetEfiDisk(d, disk)
var storageDevice *vms.CustomStorageDevice
if efiDisk != nil {
id := "0"
storageDevice = &vms.CustomStorageDevice{
Enabled: true,
FileVolume: efiDisk.FileVolume,
Format: efiDisk.Format,
DatastoreID: &id,
}
if efiDisk.Type != nil {
ds, err := types.ParseDiskSize(*efiDisk.Type)
if err != nil {
return nil, fmt.Errorf("invalid efi disk type: %s", err.Error())
}
storageDevice.Size = &ds
}
}
return storageDevice, nil
}
func vmGetTPMState(d *schema.ResourceData, disk []interface{}) *vms.CustomTPMState {
var tpmState []interface{}
if disk != nil {
tpmState = disk
} else {
tpmState = d.Get(mkTPMState).([]interface{})
}
var tpmStateConfig *vms.CustomTPMState
if len(tpmState) > 0 && tpmState[0] != nil {
tpmStateConfig = &vms.CustomTPMState{}
block := tpmState[0].(map[string]interface{})
datastoreID, _ := block[mkTPMStateDatastoreID].(string)
version, _ := block[mkTPMStateVersion].(string)
// use the special syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume.
// NB SIZE_IN_GiB is ignored, see docs for more info.
tpmStateConfig.FileVolume = fmt.Sprintf("%s:1", datastoreID)
tpmStateConfig.Version = &version
}
return tpmStateConfig
}
func vmGetTPMStateAsStorageDevice(d *schema.ResourceData, disk []interface{}) *vms.CustomStorageDevice {
tpmState := vmGetTPMState(d, disk)
var storageDevice *vms.CustomStorageDevice
if tpmState != nil {
id := "0"
storageDevice = &vms.CustomStorageDevice{
Enabled: true,
FileVolume: tpmState.FileVolume,
DatastoreID: &id,
}
}
return storageDevice
}
func vmGetHostPCIDeviceObjects(d *schema.ResourceData) vms.CustomPCIDevices {
pciDevice := d.Get(mkHostPCI).([]interface{})
pciDeviceObjects := make(vms.CustomPCIDevices, len(pciDevice))
for _, pciDeviceEntry := range pciDevice {
block := pciDeviceEntry.(map[string]interface{})
deviceName := block[mkHostPCIDevice].(string)
ids, _ := block[mkHostPCIDeviceID].(string)
mdev, _ := block[mkHostPCIDeviceMDev].(string)
pcie := types.CustomBool(block[mkHostPCIDevicePCIE].(bool))
rombar := types.CustomBool(
block[mkHostPCIDeviceROMBAR].(bool),
)
romfile, _ := block[mkHostPCIDeviceROMFile].(string)
xvga := types.CustomBool(block[mkHostPCIDeviceXVGA].(bool))
mapping, _ := block[mkHostPCIDeviceMapping].(string)
device := vms.CustomPCIDevice{
PCIExpress: &pcie,
ROMBAR: &rombar,
XVGA: &xvga,
}
if ids != "" {
dIDs := strings.Split(ids, ";")
device.DeviceIDs = &dIDs
}
if mdev != "" {
device.MDev = &mdev
}
if romfile != "" {
device.ROMFile = &romfile
}
if mapping != "" {
device.Mapping = &mapping
}
pciDeviceObjects[deviceName] = &device
}
return pciDeviceObjects
}
func vmGetNumaDeviceObjects(d *schema.ResourceData) vms.CustomNUMADevices {
numaNode := d.Get(mkNUMA).([]interface{})
numaNodeObjects := make(vms.CustomNUMADevices, len(numaNode))
for i, numaNodeEntry := range numaNode {
block := numaNodeEntry.(map[string]interface{})
deviceName := block[mkNUMADevice].(string)
ids := block[mkNUMACPUIDs].(string)
hostNodes, _ := block[mkNUMAHostNodeNames].(string)
memory, _ := block[mkNUMAMemory].(int)
policy, _ := block[mkNUMAPolicy].(string)
device := vms.CustomNUMADevice{
Memory: &memory,
Policy: &policy,
}
if ids != "" {
dIDs := strings.Split(ids, ";")
device.CPUIDs = dIDs
}
if hostNodes != "" {
dHostNodes := strings.Split(hostNodes, ";")
device.HostNodeNames = &dHostNodes
}
if strings.HasPrefix(deviceName, "numa") {
deviceID, err := strconv.Atoi(deviceName[4:])
if err == nil {
numaNodeObjects[deviceID] = device
continue
}
}
numaNodeObjects[i] = device
}
return numaNodeObjects
}
func vmGetHostUSBDeviceObjects(d *schema.ResourceData) vms.CustomUSBDevices {
usbDevice := d.Get(mkHostUSB).([]interface{})
usbDeviceObjects := make(vms.CustomUSBDevices, len(usbDevice))
for i, usbDeviceEntry := range usbDevice {
block := usbDeviceEntry.(map[string]interface{})
host, _ := block[mkHostUSBDevice].(string)
usb3 := types.CustomBool(block[mkHostUSBDeviceUSB3].(bool))
mapping, _ := block[mkHostUSBDeviceMapping].(string)
device := vms.CustomUSBDevice{
USB3: &usb3,
}
if host != "" {
device.HostDevice = &host
}
if mapping != "" {
device.Mapping = &mapping
}
usbDeviceObjects[i] = device
}
return usbDeviceObjects
}
func vmGetSerialDeviceList(d *schema.ResourceData) vms.CustomSerialDevices {
device := d.Get(mkSerialDevice).([]interface{})
list := make(vms.CustomSerialDevices, len(device))
for i, v := range device {
block := v.(map[string]interface{})
device, _ := block[mkSerialDeviceDevice].(string)
list[i] = device
}
return list
}
func vmGetSMBIOS(d *schema.ResourceData) *vms.CustomSMBIOS {
smbiosSections := d.Get(mkSMBIOS).([]interface{})
if len(smbiosSections) > 0 && smbiosSections[0] != nil {
smbiosBlock := smbiosSections[0].(map[string]interface{})
b64 := types.CustomBool(true)
family, _ := smbiosBlock[mkSMBIOSFamily].(string)
manufacturer, _ := smbiosBlock[mkSMBIOSManufacturer].(string)
product, _ := smbiosBlock[mkSMBIOSProduct].(string)
serial, _ := smbiosBlock[mkSMBIOSSerial].(string)
sku, _ := smbiosBlock[mkSMBIOS].(string)
version, _ := smbiosBlock[mkSMBIOSVersion].(string)
uid, _ := smbiosBlock[mkSMBIOSUUID].(string)
smbios := vms.CustomSMBIOS{
Base64: &b64,
}
if family != "" {
v := base64.StdEncoding.EncodeToString([]byte(family))
smbios.Family = &v
}
if manufacturer != "" {
v := base64.StdEncoding.EncodeToString([]byte(manufacturer))
smbios.Manufacturer = &v
}
if product != "" {
v := base64.StdEncoding.EncodeToString([]byte(product))
smbios.Product = &v
}
if serial != "" {
v := base64.StdEncoding.EncodeToString([]byte(serial))
smbios.Serial = &v
}
if sku != "" {
v := base64.StdEncoding.EncodeToString([]byte(sku))
smbios.SKU = &v
}
if version != "" {
v := base64.StdEncoding.EncodeToString([]byte(version))
smbios.Version = &v
}
if uid != "" {
smbios.UUID = &uid
}
if smbios.UUID == nil || *smbios.UUID == "" {
smbios.UUID = ptr.Ptr(uuid.New().String())
}
return &smbios
}
return nil
}
func vmGetStartupOrder(d *schema.ResourceData) *vms.CustomStartupOrder {
startup := d.Get(mkStartup).([]interface{})
if len(startup) > 0 && startup[0] != nil {
startupBlock := startup[0].(map[string]interface{})
startupOrder := startupBlock[mkStartupOrder].(int)
startupUpDelay := startupBlock[mkStartupUpDelay].(int)
startupDownDelay := startupBlock[mkStartupDownDelay].(int)
order := vms.CustomStartupOrder{}
if startupUpDelay >= 0 {
order.Up = &startupUpDelay
}
if startupDownDelay >= 0 {
order.Down = &startupDownDelay
}
if startupOrder >= 0 {
order.Order = &startupOrder
}
return &order
}
return nil
}
func vmGetTagsString(d *schema.ResourceData) string {
var sanitizedTags []string
tags := d.Get(mkTags).([]interface{})
for _, tag := range tags {
sanitizedTag := strings.TrimSpace(tag.(string))
if len(sanitizedTag) > 0 {
sanitizedTags = append(sanitizedTags, sanitizedTag)
}
}
sort.Strings(sanitizedTags)
return strings.Join(sanitizedTags, ";")
}
func vmGetVGADeviceObject(d *schema.ResourceData) (*vms.CustomVGADevice, error) {
resource := VM()
vgaBlock, err := structure.GetSchemaBlock(
resource,
d,
[]string{mkVGA},
0,
true,
)
if err != nil {
return nil, fmt.Errorf("error getting VGA block: %w", err)
}
vgaClipboard := vgaBlock[mkVGAClipboard].(string)
vgaMemory := vgaBlock[mkVGAMemory].(int)
vgaType := vgaBlock[mkVGAType].(string)
vgaDevice := &vms.CustomVGADevice{}
if vgaClipboard != "" {
vgaDevice.Clipboard = &vgaClipboard
}
if vgaMemory > 0 {
vgaDevice.Memory = ptr.Ptr(int64(vgaMemory))
}
if vgaType != "" {
vgaDevice.Type = &vgaType
}
return vgaDevice, nil
}
func vmRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
config := m.(proxmoxtf.ProviderConfiguration)
client, err := config.GetClient()
if err != nil {
return diag.FromErr(err)
}
vmID, err := strconv.Atoi(d.Id())
if err != nil {
return diag.FromErr(err)
}
vmNodeName, err := client.Cluster().GetVMNodeName(ctx, vmID)
if err != nil {
if errors.Is(err, cluster.ErrVMDoesNotExist) {
d.SetId("")
return nil
}
return diag.FromErr(err)
}
if vmNodeName != d.Get(mkNodeName) {
err = d.Set(mkNodeName, vmNodeName)
if err != nil {
return diag.FromErr(err)
}
}
nodeName := d.Get(mkNodeName).(string)
vmAPI := client.Node(nodeName).VM(vmID)
// Retrieve the entire configuration in order to compare it to the state.
vmConfig, err := vmAPI.GetVM(ctx)
if err != nil {
if errors.Is(err, api.ErrResourceDoesNotExist) {
d.SetId("")
return nil
}
return diag.FromErr(err)
}
vmStatus, err := vmAPI.GetVMStatus(ctx)
if err != nil {
return diag.FromErr(err)
}
return vmReadCustom(ctx, d, m, vmID, vmConfig, vmStatus)
}
func vmReadCustom(
ctx context.Context,
d *schema.ResourceData,
m interface{},
vmID int,
vmConfig *vms.GetResponseData,
vmStatus *vms.GetStatusResponseData,
) diag.Diagnostics {
config := m.(proxmoxtf.ProviderConfiguration)
client, e := config.GetClient()
if e != nil {
return diag.FromErr(e)
}
diags := vmReadPrimitiveValues(d, vmConfig, vmStatus)
if diags.HasError() {
return diags
}
// Fix terraform.tfstate, by replacing '-1' (the old default value) with actual vm_id value
if storedVMID := d.Get(mkVMID).(int); storedVMID == -1 {
diags = append(diags, diag.Diagnostic{
Severity: diag.Warning,
Summary: fmt.Sprintf("VM %s has stored legacy vm_id %d, setting vm_id to its correct value %d.",
d.Id(), storedVMID, vmID),
})
err := d.Set(mkVMID, vmID)
diags = append(diags, diag.FromErr(err)...)
}
nodeName := d.Get(mkNodeName).(string)
clone := d.Get(mkClone).([]interface{})
// Compare the agent configuration to the one stored in the state.
currentAgent := d.Get(mkAgent).([]interface{})
//nolint:gocritic
if len(clone) == 0 || len(currentAgent) > 0 {
if vmConfig.Agent != nil {
agent := map[string]interface{}{}
if vmConfig.Agent.Enabled != nil {
agent[mkAgentEnabled] = bool(*vmConfig.Agent.Enabled)
} else {
agent[mkAgentEnabled] = false
}
if vmConfig.Agent.TrimClonedDisks != nil {
agent[mkAgentTrim] = bool(
*vmConfig.Agent.TrimClonedDisks,
)
} else {
agent[mkAgentTrim] = false
}
if len(currentAgent) > 0 && currentAgent[0] != nil {
currentAgentBlock := currentAgent[0].(map[string]interface{})
currentAgentTimeout := currentAgentBlock[mkAgentTimeout].(string)
if currentAgentTimeout != "" {
agent[mkAgentTimeout] = currentAgentTimeout
} else {
agent[mkAgentTimeout] = dvAgentTimeout
}
} else {
agent[mkAgentTimeout] = dvAgentTimeout
}
if vmConfig.Agent.Type != nil {
agent[mkAgentType] = *vmConfig.Agent.Type
} else {
agent[mkAgentType] = ""
}
if len(clone) > 0 {
if len(currentAgent) > 0 {
err := d.Set(mkAgent, []interface{}{agent})
diags = append(diags, diag.FromErr(err)...)
}
} else if len(currentAgent) > 0 ||
agent[mkAgentEnabled] != dvAgentEnabled ||
agent[mkAgentTimeout] != dvAgentTimeout ||
agent[mkAgentTrim] != dvAgentTrim ||
agent[mkAgentType] != dvAgentType {
err := d.Set(mkAgent, []interface{}{agent})
diags = append(diags, diag.FromErr(err)...)
}
} else if len(clone) > 0 {
if len(currentAgent) > 0 {
err := d.Set(mkAgent, []interface{}{})
diags = append(diags, diag.FromErr(err)...)
}
} else {
err := d.Set(mkAgent, []interface{}{})
diags = append(diags, diag.FromErr(err)...)
}
}
// Compare the audio devices to those stored in the state.
currentAudioDevice := d.Get(mkAudioDevice).([]interface{})
audioDevices := make([]interface{}, 1)
audioDevicesArray := []*vms.CustomAudioDevice{
vmConfig.AudioDevice,
}
audioDevicesCount := 0
for adi, ad := range audioDevicesArray {
m := map[string]interface{}{}
if ad != nil {
m[mkAudioDeviceDevice] = ad.Device
if ad.Driver != nil {
m[mkAudioDeviceDriver] = *ad.Driver
} else {
m[mkAudioDeviceDriver] = ""
}
m[mkAudioDeviceEnabled] = true
audioDevicesCount = adi + 1
} else {
m[mkAudioDeviceDevice] = ""
m[mkAudioDeviceDriver] = ""
m[mkAudioDeviceEnabled] = false
}
audioDevices[adi] = m
}
if len(clone) == 0 || len(currentAudioDevice) > 0 {
err := d.Set(mkAudioDevice, audioDevices[:audioDevicesCount])
diags = append(diags, diag.FromErr(err)...)
}
// Compare the IDE devices to the CD-ROM configurations stored in the state.
currentInterface := dvCDROMInterface
currentCDROM := d.Get(mkCDROM).([]interface{})
if len(currentCDROM) > 0 && currentCDROM[0] != nil {
currentBlock := currentCDROM[0].(map[string]interface{})
currentInterface = currentBlock[mkCDROMInterface].(string)
}
cdromIDEDevice := getStorageDevice(vmConfig, currentInterface)
if cdromIDEDevice != nil {
cdrom := make([]interface{}, 1)
cdromBlock := map[string]interface{}{}
if len(clone) == 0 || len(currentCDROM) > 0 {
cdromBlock[mkCDROMEnabled] = cdromIDEDevice.Enabled
cdromBlock[mkCDROMFileID] = cdromIDEDevice.FileVolume
cdromBlock[mkCDROMInterface] = currentInterface
if len(currentCDROM) > 0 && currentCDROM[0] != nil {
currentBlock := currentCDROM[0].(map[string]interface{})
if currentBlock[mkCDROMFileID] == "" {
cdromBlock[mkCDROMFileID] = ""
}
if currentBlock[mkCDROMEnabled] == false {
cdromBlock[mkCDROMEnabled] = false
}
}
cdrom[0] = cdromBlock
err := d.Set(mkCDROM, cdrom)
diags = append(diags, diag.FromErr(err)...)
}
} else {
err := d.Set(mkCDROM, []interface{}{})
diags = append(diags, diag.FromErr(err)...)
}
// Compare the CPU configuration to the one stored in the state.
cpu := map[string]interface{}{}
if vmConfig.CPUArchitecture != nil {
cpu[mkCPUArchitecture] = *vmConfig.CPUArchitecture
} else {
// Default value of "arch" is "" according to the API documentation.
cpu[mkCPUArchitecture] = ""
}
if vmConfig.CPUCores != nil {
cpu[mkCPUCores] = int(*vmConfig.CPUCores)
} else {
// Default value of "cores" is "1" according to the API documentation.
cpu[mkCPUCores] = 1
}
if vmConfig.VirtualCPUCount != nil {
cpu[mkCPUHotplugged] = int(*vmConfig.VirtualCPUCount)
} else {
// Default value of "vcpus" is "1" according to the API documentation.
cpu[mkCPUHotplugged] = 0
}
if vmConfig.CPULimit != nil {
cpu[mkCPULimit] = int(*vmConfig.CPULimit)
} else {
// Default value of "cpulimit" is "0" according to the API documentation.
cpu[mkCPULimit] = 0
}
if vmConfig.NUMAEnabled != nil {
cpu[mkCPUNUMA] = *vmConfig.NUMAEnabled
} else {
// Default value of "numa" is "false" according to the API documentation.
cpu[mkCPUNUMA] = false
}
currentNUMAList := d.Get(mkNUMA).([]interface{})
numaMap := map[string]interface{}{}
numaDevices := getNUMAInfo(vmConfig, d)
for ni, np := range numaDevices {
if np == nil || np.CPUIDs == nil || np.HostNodeNames == nil {
continue
}
numaNode := map[string]interface{}{}
numaNode[mkNUMADevice] = ni
if len(np.CPUIDs) > 0 {
numaNode[mkNUMACPUIDs] = strings.Join(np.CPUIDs, ";")
}
numaNode[mkNUMAHostNodeNames] = strings.Join(*np.HostNodeNames, ";")
numaNode[mkNUMAMemory] = np.Memory
numaNode[mkNUMAPolicy] = np.Policy
numaMap[ni] = numaNode
}
if len(clone) == 0 || len(currentNUMAList) > 0 {
var numaList []interface{}
if len(currentNUMAList) > 0 {
devices := utils.ListResourcesAttributeValue(currentNUMAList, mkNUMADevice)
numaList = utils.OrderedListFromMapByKeyValues(numaMap, devices)
} else {
numaList = utils.OrderedListFromMap(numaMap)
}
err := d.Set(mkNUMA, numaList)
diags = append(diags, diag.FromErr(err)...)
}
if vmConfig.CPUSockets != nil {
cpu[mkCPUSockets] = int(*vmConfig.CPUSockets)
} else {
// Default value of "sockets" is "1" according to the API documentation.
cpu[mkCPUSockets] = 1
}
if vmConfig.CPUEmulation != nil {
if vmConfig.CPUEmulation.Flags != nil {
convertedFlags := make([]interface{}, len(*vmConfig.CPUEmulation.Flags))
for fi, fv := range *vmConfig.CPUEmulation.Flags {
convertedFlags[fi] = fv
}
cpu[mkCPUFlags] = convertedFlags
} else {
cpu[mkCPUFlags] = []interface{}{}
}
cpu[mkCPUType] = vmConfig.CPUEmulation.Type
} else {
cpu[mkCPUFlags] = []interface{}{}
// Default value of "cputype" is "qemu64" according to the QEMU documentation.
cpu[mkCPUType] = "qemu64"
}
if vmConfig.CPUUnits != nil {
cpu[mkCPUUnits] = int(*vmConfig.CPUUnits)
} else {
// Default value of "cpuunits" is "1024" according to the API documentation.
cpu[mkCPUUnits] = 1024
}
if vmConfig.CPUAffinity != nil {
cpu[mkCPUAffinity] = *vmConfig.CPUAffinity
} else {
cpu[mkCPUAffinity] = ""
}
currentCPU := d.Get(mkCPU).([]interface{})
if len(clone) > 0 {
if len(currentCPU) > 0 {
err := d.Set(mkCPU, []interface{}{cpu})
diags = append(diags, diag.FromErr(err)...)
}
} else if len(currentCPU) > 0 ||
cpu[mkCPUArchitecture] != dvCPUArchitecture ||
cpu[mkCPUCores] != dvCPUCores ||
len(cpu[mkCPUFlags].([]interface{})) > 0 ||
cpu[mkCPUHotplugged] != dvCPUHotplugged ||
cpu[mkCPULimit] != dvCPULimit ||
cpu[mkCPUSockets] != dvCPUSockets ||
cpu[mkCPUType] != dvCPUType ||
cpu[mkCPUUnits] != dvCPUUnits {
err := d.Set(mkCPU, []interface{}{cpu})
diags = append(diags, diag.FromErr(err)...)
}
allDiskInfo := disk.GetInfo(vmConfig, d)
diags = append(diags, disk.Read(ctx, d, allDiskInfo, vmID, client, nodeName, len(clone) > 0)...)
if vmConfig.EFIDisk != nil {
efiDisk := map[string]interface{}{}
fileIDParts := strings.Split(vmConfig.EFIDisk.FileVolume, ":")
efiDisk[mkEFIDiskDatastoreID] = fileIDParts[0]
if vmConfig.EFIDisk.Format != nil {
efiDisk[mkEFIDiskFileFormat] = *vmConfig.EFIDisk.Format
} else {
// disk format may not be returned by config API if it is default for the storage, and that may be different
// from the default qcow2, so we need to read it from the storage API to make sure we have the correct value
volume, err := client.Node(nodeName).Storage(fileIDParts[0]).GetDatastoreFile(ctx, vmConfig.EFIDisk.FileVolume)
if err != nil {
diags = append(diags, diag.FromErr(e)...)
} else {
efiDisk[mkEFIDiskFileFormat] = volume.FileFormat
}
}
if vmConfig.EFIDisk.Type != nil {
efiDisk[mkEFIDiskType] = *vmConfig.EFIDisk.Type
} else {
efiDisk[mkEFIDiskType] = dvEFIDiskType
}
if vmConfig.EFIDisk.PreEnrolledKeys != nil {
efiDisk[mkEFIDiskPreEnrolledKeys] = *vmConfig.EFIDisk.PreEnrolledKeys
} else {
efiDisk[mkEFIDiskPreEnrolledKeys] = false
}
currentEfiDisk := d.Get(mkEFIDisk).([]interface{})
if len(clone) > 0 {
if len(currentEfiDisk) > 0 {
err := d.Set(mkEFIDisk, []interface{}{efiDisk})
diags = append(diags, diag.FromErr(err)...)
}
} else if len(currentEfiDisk) > 0 ||
efiDisk[mkEFIDiskDatastoreID] != dvEFIDiskDatastoreID ||
efiDisk[mkEFIDiskType] != dvEFIDiskType ||
efiDisk[mkEFIDiskPreEnrolledKeys] != dvEFIDiskPreEnrolledKeys ||
efiDisk[mkEFIDiskFileFormat] != dvEFIDiskFileFormat {
err := d.Set(mkEFIDisk, []interface{}{efiDisk})
diags = append(diags, diag.FromErr(err)...)
}
}
if vmConfig.TPMState != nil {
tpmState := map[string]interface{}{}
fileIDParts := strings.Split(vmConfig.TPMState.FileVolume, ":")
tpmState[mkTPMStateDatastoreID] = fileIDParts[0]
tpmState[mkTPMStateVersion] = dvTPMStateVersion
currentTPMState := d.Get(mkTPMState).([]interface{})
if len(clone) > 0 {
if len(currentTPMState) > 0 {
err := d.Set(mkTPMState, []interface{}{tpmState})
diags = append(diags, diag.FromErr(err)...)
}
} else if len(currentTPMState) > 0 ||
tpmState[mkTPMStateDatastoreID] != dvTPMStateDatastoreID ||
tpmState[mkTPMStateVersion] != dvTPMStateVersion {
err := d.Set(mkTPMState, []interface{}{tpmState})
diags = append(diags, diag.FromErr(err)...)
}
}
currentPCIList := d.Get(mkHostPCI).([]interface{})
pciMap := map[string]interface{}{}
for pi, pp := range vmConfig.PCIDevices {
if (pp == nil) || (pp.DeviceIDs == nil && pp.Mapping == nil) {
continue
}
pci := map[string]interface{}{}
pci[mkHostPCIDevice] = pi
if pp.DeviceIDs != nil {
pci[mkHostPCIDeviceID] = strings.Join(*pp.DeviceIDs, ";")
} else {
pci[mkHostPCIDeviceID] = ""
}
if pp.MDev != nil {
pci[mkHostPCIDeviceMDev] = *pp.MDev
} else {
pci[mkHostPCIDeviceMDev] = ""
}
if pp.PCIExpress != nil {
pci[mkHostPCIDevicePCIE] = *pp.PCIExpress
} else {
pci[mkHostPCIDevicePCIE] = false
}
if pp.ROMBAR != nil {
pci[mkHostPCIDeviceROMBAR] = *pp.ROMBAR
} else {
pci[mkHostPCIDeviceROMBAR] = true
}
if pp.ROMFile != nil {
pci[mkHostPCIDeviceROMFile] = *pp.ROMFile
} else {
pci[mkHostPCIDeviceROMFile] = ""
}
if pp.XVGA != nil {
pci[mkHostPCIDeviceXVGA] = *pp.XVGA
} else {
pci[mkHostPCIDeviceXVGA] = false
}
if pp.Mapping != nil {
pci[mkHostPCIDeviceMapping] = *pp.Mapping
} else {
pci[mkHostPCIDeviceMapping] = ""
}
pciMap[pi] = pci
}
if len(clone) == 0 || len(currentPCIList) > 0 {
orderedPCIList := utils.OrderedListFromMap(pciMap)
err := d.Set(mkHostPCI, orderedPCIList)
diags = append(diags, diag.FromErr(err)...)
}
currentUSBList := d.Get(mkHostUSB).([]interface{})
usbMap := map[string]interface{}{}
usbDevices := getUSBInfo(vmConfig, d)
for pi, pp := range usbDevices {
if (pp == nil) || (pp.HostDevice == nil && pp.Mapping == nil) {
continue
}
usb := map[string]interface{}{}
if pp.HostDevice != nil {
usb[mkHostUSBDevice] = *pp.HostDevice
} else {
usb[mkHostUSBDevice] = ""
}
if pp.USB3 != nil {
usb[mkHostUSBDeviceUSB3] = *pp.USB3
} else {
usb[mkHostUSBDeviceUSB3] = false
}
if pp.Mapping != nil {
usb[mkHostUSBDeviceMapping] = *pp.Mapping
} else {
usb[mkHostUSBDeviceMapping] = ""
}
usbMap[pi] = usb
}
if len(clone) == 0 || len(currentUSBList) > 0 {
// NOTE: reordering of devices by PVE may cause an issue here
orderedUSBList := utils.OrderedListFromMap(usbMap)
err := d.Set(mkHostUSB, orderedUSBList)
diags = append(diags, diag.FromErr(err)...)
}
// Compare the initialization configuration to the one stored in the state.
initialization := map[string]interface{}{}
initializationInterface := findExistingCloudInitDrive(vmConfig, vmID, "")
if initializationInterface != "" {
initializationDevice := getStorageDevice(vmConfig, initializationInterface)
fileVolumeParts := strings.Split(initializationDevice.FileVolume, ":")
initialization[mkInitializationInterface] = initializationInterface
initialization[mkInitializationDatastoreID] = fileVolumeParts[0]
}
if vmConfig.CloudInitDNSDomain != nil || vmConfig.CloudInitDNSServer != nil {
initializationDNS := map[string]interface{}{}
if vmConfig.CloudInitDNSDomain != nil {
initializationDNS[mkInitializationDNSDomain] = *vmConfig.CloudInitDNSDomain
} else {
initializationDNS[mkInitializationDNSDomain] = ""
}
// check what we have in the plan
currentInitializationDNSBlock := map[string]interface{}{}
currentInitialization := d.Get(mkInitialization).([]interface{})
if len(currentInitialization) > 0 && currentInitialization[0] != nil {
currentInitializationBlock := currentInitialization[0].(map[string]interface{})
currentInitializationDNS := currentInitializationBlock[mkInitializationDNS].([]interface{})
if len(currentInitializationDNS) > 0 && currentInitializationDNS[0] != nil {
currentInitializationDNSBlock = currentInitializationDNS[0].(map[string]interface{})
}
}
currentInitializationDNSServer, ok := currentInitializationDNSBlock[mkInitializationDNSServer]
if vmConfig.CloudInitDNSServer != nil {
if ok && currentInitializationDNSServer != "" {
// the template is using deprecated attribute mkInitializationDNSServer
initializationDNS[mkInitializationDNSServer] = *vmConfig.CloudInitDNSServer
} else {
dnsServer := strings.Split(*vmConfig.CloudInitDNSServer, " ")
initializationDNS[mkInitializationDNSServers] = dnsServer
}
} else {
initializationDNS[mkInitializationDNSServer] = ""
initializationDNS[mkInitializationDNSServers] = []string{}
}
initialization[mkInitializationDNS] = []interface{}{
initializationDNS,
}
}
ipConfigLast := -1
ipConfigObjects := []*vms.CustomCloudInitIPConfig{
vmConfig.IPConfig0,
vmConfig.IPConfig1,
vmConfig.IPConfig2,
vmConfig.IPConfig3,
vmConfig.IPConfig4,
vmConfig.IPConfig5,
vmConfig.IPConfig6,
vmConfig.IPConfig7,
vmConfig.IPConfig7,
vmConfig.IPConfig8,
vmConfig.IPConfig9,
vmConfig.IPConfig10,
vmConfig.IPConfig11,
vmConfig.IPConfig12,
vmConfig.IPConfig13,
vmConfig.IPConfig14,
vmConfig.IPConfig15,
vmConfig.IPConfig16,
vmConfig.IPConfig17,
vmConfig.IPConfig18,
vmConfig.IPConfig19,
vmConfig.IPConfig20,
vmConfig.IPConfig21,
vmConfig.IPConfig22,
vmConfig.IPConfig23,
vmConfig.IPConfig24,
vmConfig.IPConfig25,
vmConfig.IPConfig26,
vmConfig.IPConfig27,
vmConfig.IPConfig28,
vmConfig.IPConfig29,
vmConfig.IPConfig30,
vmConfig.IPConfig31,
}
ipConfigList := make([]interface{}, len(ipConfigObjects))
for ipConfigIndex, ipConfig := range ipConfigObjects {
ipConfigItem := map[string]interface{}{}
if ipConfig != nil {
ipConfigLast = ipConfigIndex
if ipConfig.GatewayIPv4 != nil || ipConfig.IPv4 != nil {
ipv4 := map[string]interface{}{}
if ipConfig.IPv4 != nil {
ipv4[mkInitializationIPConfigIPv4Address] = *ipConfig.IPv4
} else {
ipv4[mkInitializationIPConfigIPv4Address] = ""
}
if ipConfig.GatewayIPv4 != nil {
ipv4[mkInitializationIPConfigIPv4Gateway] = *ipConfig.GatewayIPv4
} else {
ipv4[mkInitializationIPConfigIPv4Gateway] = ""
}
ipConfigItem[mkInitializationIPConfigIPv4] = []interface{}{
ipv4,
}
} else {
ipConfigItem[mkInitializationIPConfigIPv4] = []interface{}{}
}
if ipConfig.GatewayIPv6 != nil || ipConfig.IPv6 != nil {
ipv6 := map[string]interface{}{}
if ipConfig.IPv6 != nil {
ipv6[mkInitializationIPConfigIPv6Address] = *ipConfig.IPv6
} else {
ipv6[mkInitializationIPConfigIPv6Address] = ""
}
if ipConfig.GatewayIPv6 != nil {
ipv6[mkInitializationIPConfigIPv6Gateway] = *ipConfig.GatewayIPv6
} else {
ipv6[mkInitializationIPConfigIPv6Gateway] = ""
}
ipConfigItem[mkInitializationIPConfigIPv6] = []interface{}{
ipv6,
}
} else {
ipConfigItem[mkInitializationIPConfigIPv6] = []interface{}{}
}
} else {
ipConfigItem[mkInitializationIPConfigIPv4] = []interface{}{}
ipConfigItem[mkInitializationIPConfigIPv6] = []interface{}{}
}
ipConfigList[ipConfigIndex] = ipConfigItem
}
if ipConfigLast >= 0 {
initialization[mkInitializationIPConfig] = ipConfigList[:ipConfigLast+1]
}
if vmConfig.CloudInitPassword != nil || vmConfig.CloudInitSSHKeys != nil ||
vmConfig.CloudInitUsername != nil {
initializationUserAccount := map[string]interface{}{}
if vmConfig.CloudInitSSHKeys != nil {
initializationUserAccount[mkInitializationUserAccountKeys] = []string(
*vmConfig.CloudInitSSHKeys,
)
} else {
initializationUserAccount[mkInitializationUserAccountKeys] = []string{}
}
if vmConfig.CloudInitPassword != nil {
initializationUserAccount[mkInitializationUserAccountPassword] = *vmConfig.CloudInitPassword
} else {
initializationUserAccount[mkInitializationUserAccountPassword] = ""
}
if vmConfig.CloudInitUsername != nil {
initializationUserAccount[mkInitializationUserAccountUsername] = *vmConfig.CloudInitUsername
} else {
initializationUserAccount[mkInitializationUserAccountUsername] = ""
}
initialization[mkInitializationUserAccount] = []interface{}{
initializationUserAccount,
}
}
if vmConfig.CloudInitFiles != nil {
if vmConfig.CloudInitFiles.UserVolume != nil {
initialization[mkInitializationUserDataFileID] = *vmConfig.CloudInitFiles.UserVolume
} else {
initialization[mkInitializationUserDataFileID] = ""
}
if vmConfig.CloudInitFiles.VendorVolume != nil {
initialization[mkInitializationVendorDataFileID] = *vmConfig.CloudInitFiles.VendorVolume
} else {
initialization[mkInitializationVendorDataFileID] = ""
}
if vmConfig.CloudInitFiles.NetworkVolume != nil {
initialization[mkInitializationNetworkDataFileID] = *vmConfig.CloudInitFiles.NetworkVolume
} else {
initialization[mkInitializationNetworkDataFileID] = ""
}
if vmConfig.CloudInitFiles.MetaVolume != nil {
initialization[mkInitializationMetaDataFileID] = *vmConfig.CloudInitFiles.MetaVolume
} else {
initialization[mkInitializationMetaDataFileID] = ""
}
} else if len(initialization) > 0 {
initialization[mkInitializationUserDataFileID] = ""
initialization[mkInitializationVendorDataFileID] = ""
initialization[mkInitializationNetworkDataFileID] = ""
initialization[mkInitializationMetaDataFileID] = ""
}
if vmConfig.CloudInitType != nil {
initialization[mkInitializationType] = *vmConfig.CloudInitType
} else if len(initialization) > 0 {
initialization[mkInitializationType] = ""
}
currentInitialization := d.Get(mkInitialization).([]interface{})
//nolint:gocritic
if len(clone) > 0 {
if len(currentInitialization) > 0 {
if len(initialization) > 0 {
err := d.Set(mkInitialization, []interface{}{initialization})
diags = append(diags, diag.FromErr(err)...)
} else {
err := d.Set(mkInitialization, []interface{}{})
diags = append(diags, diag.FromErr(err)...)
}
}
} else if len(initialization) > 0 {
err := d.Set(mkInitialization, []interface{}{initialization})
diags = append(diags, diag.FromErr(err)...)
} else {
err := d.Set(mkInitialization, []interface{}{})
diags = append(diags, diag.FromErr(err)...)
}
// Compare the operating system configuration to the one stored in the state.
kvmArguments := map[string]interface{}{}
if vmConfig.KVMArguments != nil {
kvmArguments[mkKVMArguments] = *vmConfig.KVMArguments
} else {
kvmArguments[mkKVMArguments] = ""
}
// Compare the memory configuration to the one stored in the state.
memory := map[string]interface{}{}
if vmConfig.DedicatedMemory != nil {
memory[mkMemoryDedicated] = int(*vmConfig.DedicatedMemory)
} else {
memory[mkMemoryDedicated] = 0
}
if vmConfig.FloatingMemory != nil {
memory[mkMemoryFloating] = int(*vmConfig.FloatingMemory)
} else {
memory[mkMemoryFloating] = 0
}
if vmConfig.SharedMemory != nil {
memory[mkMemoryShared] = vmConfig.SharedMemory.Size
} else {
memory[mkMemoryShared] = 0
}
if vmConfig.Hugepages != nil {
memory[mkMemoryHugepages] = *vmConfig.Hugepages
} else {
memory[mkMemoryHugepages] = ""
}
if vmConfig.KeepHugepages != nil {
memory[mkMemoryKeepHugepages] = *vmConfig.KeepHugepages
} else {
memory[mkMemoryKeepHugepages] = false
}
currentMemory := d.Get(mkMemory).([]interface{})
if len(clone) > 0 {
if len(currentMemory) > 0 {
err := d.Set(mkMemory, []interface{}{memory})
diags = append(diags, diag.FromErr(err)...)
}
} else if len(currentMemory) > 0 ||
memory[mkMemoryDedicated] != dvMemoryDedicated ||
memory[mkMemoryFloating] != dvMemoryFloating ||
memory[mkMemoryShared] != dvMemoryShared ||
memory[mkMemoryHugepages] != dvMemoryHugepages ||
memory[mkMemoryKeepHugepages] != dvMemoryKeepHugepages {
err := d.Set(mkMemory, []interface{}{memory})
diags = append(diags, diag.FromErr(err)...)
}
diags = append(diags, network.ReadNetworkDeviceObjects(d, vmConfig)...)
// Compare the operating system configuration to the one stored in the state.
operatingSystem := map[string]interface{}{}
if vmConfig.OSType != nil {
operatingSystem[mkOperatingSystemType] = *vmConfig.OSType
} else {
operatingSystem[mkOperatingSystemType] = ""
}
currentOperatingSystem := d.Get(mkOperatingSystem).([]interface{})
switch {
case len(clone) > 0:
if len(currentOperatingSystem) > 0 {
err := d.Set(
mkOperatingSystem,
[]interface{}{operatingSystem},
)
diags = append(diags, diag.FromErr(err)...)
}
case len(currentOperatingSystem) > 0 || operatingSystem[mkOperatingSystemType] != dvOperatingSystemType:
err := d.Set(mkOperatingSystem, []interface{}{operatingSystem})
diags = append(diags, diag.FromErr(err)...)
default:
err := d.Set(mkOperatingSystem, []interface{}{})
diags = append(diags, diag.FromErr(err)...)
}
// Compare the pool ID to the value stored in the state.
currentPoolID := d.Get(mkPoolID).(string)
if len(clone) == 0 || currentPoolID != dvPoolID {
if vmConfig.PoolID != nil {
err := d.Set(mkPoolID, *vmConfig.PoolID)
diags = append(diags, diag.FromErr(err)...)
}
}
// Compare the serial devices to those stored in the state.
serialDevices := make([]interface{}, 4)
serialDevicesArray := []*string{
vmConfig.SerialDevice0,
vmConfig.SerialDevice1,
vmConfig.SerialDevice2,
vmConfig.SerialDevice3,
}
serialDevicesCount := 0
for sdi, sd := range serialDevicesArray {
m := map[string]interface{}{}
if sd != nil {
m[mkSerialDeviceDevice] = *sd
serialDevicesCount = sdi + 1
} else {
m[mkSerialDeviceDevice] = ""
}
serialDevices[sdi] = m
}
currentSerialDevice := d.Get(mkSerialDevice).([]interface{})
if len(clone) == 0 || len(currentSerialDevice) > 0 {
err := d.Set(mkSerialDevice, serialDevices[:serialDevicesCount])
diags = append(diags, diag.FromErr(err)...)
}
// Compare the SMBIOS to the one stored in the state.
var smbios map[string]interface{}
if vmConfig.SMBIOS != nil {
smbios = map[string]interface{}{}
if vmConfig.SMBIOS.Family != nil {
b, err := base64.StdEncoding.DecodeString(*vmConfig.SMBIOS.Family)
diags = append(diags, diag.FromErr(err)...)
smbios[mkSMBIOSFamily] = string(b)
} else {
smbios[mkSMBIOSFamily] = dvSMBIOSFamily
}
if vmConfig.SMBIOS.Manufacturer != nil {
b, err := base64.StdEncoding.DecodeString(*vmConfig.SMBIOS.Manufacturer)
diags = append(diags, diag.FromErr(err)...)
smbios[mkSMBIOSManufacturer] = string(b)
} else {
smbios[mkSMBIOSManufacturer] = dvSMBIOSManufacturer
}
if vmConfig.SMBIOS.Product != nil {
b, err := base64.StdEncoding.DecodeString(*vmConfig.SMBIOS.Product)
diags = append(diags, diag.FromErr(err)...)
smbios[mkSMBIOSProduct] = string(b)
} else {
smbios[mkSMBIOSProduct] = dvSMBIOSProduct
}
if vmConfig.SMBIOS.Serial != nil {
b, err := base64.StdEncoding.DecodeString(*vmConfig.SMBIOS.Serial)
diags = append(diags, diag.FromErr(err)...)
smbios[mkSMBIOSSerial] = string(b)
} else {
smbios[mkSMBIOSSerial] = dvSMBIOSSerial
}
if vmConfig.SMBIOS.SKU != nil {
b, err := base64.StdEncoding.DecodeString(*vmConfig.SMBIOS.SKU)
diags = append(diags, diag.FromErr(err)...)
smbios[mkSMBIOSSKU] = string(b)
} else {
smbios[mkSMBIOSSKU] = dvSMBIOSSKU
}
if vmConfig.SMBIOS.Version != nil {
b, err := base64.StdEncoding.DecodeString(*vmConfig.SMBIOS.Version)
diags = append(diags, diag.FromErr(err)...)
smbios[mkSMBIOSVersion] = string(b)
} else {
smbios[mkSMBIOSVersion] = dvSMBIOSVersion
}
if vmConfig.SMBIOS.UUID != nil {
smbios[mkSMBIOSUUID] = *vmConfig.SMBIOS.UUID
} else {
smbios[mkSMBIOSUUID] = nil
}
}
currentSMBIOS := d.Get(mkSMBIOS).([]interface{})
switch {
case len(clone) > 0:
if len(currentSMBIOS) > 0 {
err := d.Set(mkSMBIOS, currentSMBIOS)
diags = append(diags, diag.FromErr(err)...)
}
case len(smbios) == 0:
err := d.Set(mkSMBIOS, []interface{}{})
diags = append(diags, diag.FromErr(err)...)
default:
if len(currentSMBIOS) > 0 ||
smbios[mkSMBIOSFamily] != dvSMBIOSFamily ||
smbios[mkSMBIOSManufacturer] != dvSMBIOSManufacturer ||
smbios[mkSMBIOSProduct] != dvSMBIOSProduct ||
smbios[mkSMBIOSSerial] != dvSMBIOSSerial ||
smbios[mkSMBIOSSKU] != dvSMBIOSSKU ||
smbios[mkSMBIOSVersion] != dvSMBIOSVersion {
err := d.Set(mkSMBIOS, []interface{}{smbios})
diags = append(diags, diag.FromErr(err)...)
}
}
// Compare the startup order to the one stored in the state.
var startup map[string]interface{}
if vmConfig.StartupOrder != nil {
startup = map[string]interface{}{}
if vmConfig.StartupOrder.Order != nil {
startup[mkStartupOrder] = *vmConfig.StartupOrder.Order
} else {
startup[mkStartupOrder] = dvStartupOrder
}
if vmConfig.StartupOrder.Up != nil {
startup[mkStartupUpDelay] = *vmConfig.StartupOrder.Up
} else {
startup[mkStartupUpDelay] = dvStartupUpDelay
}
if vmConfig.StartupOrder.Down != nil {
startup[mkStartupDownDelay] = *vmConfig.StartupOrder.Down
} else {
startup[mkStartupDownDelay] = dvStartupDownDelay
}
}
currentStartup := d.Get(mkStartup).([]interface{})
switch {
case len(clone) > 0:
if len(currentStartup) > 0 {
err := d.Set(mkStartup, []interface{}{startup})
diags = append(diags, diag.FromErr(err)...)
}
case len(startup) == 0:
err := d.Set(mkStartup, []interface{}{})
diags = append(diags, diag.FromErr(err)...)
default:
if len(currentStartup) > 0 ||
startup[mkStartupOrder] != mkStartupOrder ||
startup[mkStartupUpDelay] != dvStartupUpDelay ||
startup[mkStartupDownDelay] != dvStartupDownDelay {
err := d.Set(mkStartup, []interface{}{startup})
diags = append(diags, diag.FromErr(err)...)
}
}
// Compare the VGA configuration to the one stored in the state.
vga := map[string]interface{}{}
if vmConfig.VGADevice != nil {
if vmConfig.VGADevice.Clipboard != nil {
vga[mkVGAClipboard] = *vmConfig.VGADevice.Clipboard
} else {
vga[mkVGAClipboard] = dvVGAClipboard
}
if vmConfig.VGADevice.Memory != nil {
vga[mkVGAMemory] = int(*vmConfig.VGADevice.Memory)
} else {
vga[mkVGAMemory] = dvVGAMemory
}
if vmConfig.VGADevice.Type != nil {
vga[mkVGAType] = *vmConfig.VGADevice.Type
}
} else {
vga[mkVGAClipboard] = ""
vga[mkVGAMemory] = 0
vga[mkVGAType] = ""
}
currentVGA := d.Get(mkVGA).([]interface{})
switch {
case len(clone) > 0 && len(currentVGA) > 0:
err := d.Set(mkVGA, []interface{}{vga})
diags = append(diags, diag.FromErr(err)...)
case len(currentVGA) > 0 ||
vga[mkVGAClipboard] != dvVGAClipboard ||
vga[mkVGAMemory] != dvVGAMemory ||
vga[mkVGAType] != dvVGAType:
err := d.Set(mkVGA, []interface{}{vga})
diags = append(diags, diag.FromErr(err)...)
default:
err := d.Set(mkVGA, []interface{}{})
diags = append(diags, diag.FromErr(err)...)
}
// Compare SCSI hardware type
scsiHardware := d.Get(mkSCSIHardware).(string)
if len(clone) == 0 || scsiHardware != dvSCSIHardware {
if vmConfig.SCSIHardware != nil {
err := d.Set(mkSCSIHardware, *vmConfig.SCSIHardware)
diags = append(diags, diag.FromErr(err)...)
}
}
watchdog := map[string]interface{}{}
if vmConfig.WatchdogDevice != nil {
watchdog[mkWatchdogEnabled] = true
if vmConfig.WatchdogDevice.Action != nil {
watchdog[mkWatchdogAction] = *vmConfig.WatchdogDevice.Action
} else {
watchdog[mkWatchdogAction] = dvWatchdogAction
}
if vmConfig.WatchdogDevice.Model != nil {
watchdog[mkWatchdogModel] = *vmConfig.WatchdogDevice.Model
} else {
watchdog[mkWatchdogModel] = dvWatchdogModel
}
} else {
watchdog[mkWatchdogEnabled] = false
watchdog[mkWatchdogAction] = dvWatchdogAction
watchdog[mkWatchdogModel] = dvWatchdogModel
}
currentWatchdog := d.Get(mkWatchdog).([]interface{})
currentWatchdogEnabled := len(currentWatchdog) > 0 &&
currentWatchdog[0] != nil && currentWatchdog[0].(map[string]interface{})[mkWatchdogEnabled].(bool)
currentWatchdogDisabled := len(currentWatchdog) > 0 &&
currentWatchdog[0] != nil && !currentWatchdog[0].(map[string]interface{})[mkWatchdogEnabled].(bool)
switch {
case len(clone) > 0 && len(currentWatchdog) > 0:
err := d.Set(mkWatchdog, []interface{}{watchdog})
diags = append(diags, diag.FromErr(err)...)
case currentWatchdogEnabled ||
watchdog[mkWatchdogEnabled] != false ||
watchdog[mkWatchdogAction] != dvWatchdogAction ||
watchdog[mkWatchdogModel] != dvWatchdogModel:
err := d.Set(mkWatchdog, []interface{}{watchdog})
diags = append(diags, diag.FromErr(err)...)
case currentWatchdogDisabled && vmConfig.WatchdogDevice == nil:
// do nothing
default:
err := d.Set(mkWatchdog, []interface{}{})
diags = append(diags, diag.FromErr(err)...)
}
vmAPI := client.Node(nodeName).VM(vmID)
started := d.Get(mkStarted).(bool)
agentTimeout, e := getAgentTimeout(d)
if e != nil {
return diag.FromErr(e)
}
diags = append(
diags,
network.ReadNetworkValues(ctx, d, vmAPI, started, vmConfig, agentTimeout)...)
// during import these core attributes might not be set, so set them explicitly here
d.SetId(strconv.Itoa(vmID))
e = d.Set(mkVMID, vmID)
diags = append(diags, diag.FromErr(e)...)
e = d.Set(mkNodeName, nodeName)
diags = append(diags, diag.FromErr(e)...)
return diags
}
func vmReadPrimitiveValues(
d *schema.ResourceData,
vmConfig *vms.GetResponseData,
vmStatus *vms.GetStatusResponseData,
) diag.Diagnostics {
var diags diag.Diagnostics
var err error
clone := d.Get(mkClone).([]interface{})
currentACPI := d.Get(mkACPI).(bool)
//nolint:gosimple
if len(clone) == 0 || currentACPI != dvACPI {
if vmConfig.ACPI != nil {
err = d.Set(mkACPI, bool(*vmConfig.ACPI))
} else {
// Default value of "acpi" is "1" according to the API documentation.
err = d.Set(mkACPI, true)
}
diags = append(diags, diag.FromErr(err)...)
}
currentKVMArguments := d.Get(mkKVMArguments).(string)
if len(clone) == 0 || currentKVMArguments != dvKVMArguments {
// PVE API returns "args" as " " if it is set to empty.
if vmConfig.KVMArguments != nil && len(strings.TrimSpace(*vmConfig.KVMArguments)) > 0 {
err = d.Set(mkKVMArguments, *vmConfig.KVMArguments)
} else {
// Default value of "args" is "" according to the API documentation.
err = d.Set(mkKVMArguments, "")
}
diags = append(diags, diag.FromErr(err)...)
}
currentBIOS := d.Get(mkBIOS).(string)
if len(clone) == 0 || currentBIOS != dvBIOS {
if vmConfig.BIOS != nil {
err = d.Set(mkBIOS, *vmConfig.BIOS)
} else {
// Default value of "bios" is "seabios" according to the API documentation.
err = d.Set(mkBIOS, "seabios")
}
diags = append(diags, diag.FromErr(err)...)
}
currentDescription := d.Get(mkDescription).(string)
if len(clone) == 0 || currentDescription != dvDescription {
if vmConfig.Description != nil {
err = d.Set(mkDescription, *vmConfig.Description)
} else {
// Default value of "description" is "" according to the API documentation.
err = d.Set(mkDescription, "")
}
diags = append(diags, diag.FromErr(err)...)
}
currentTags := d.Get(mkTags).([]interface{})
if len(clone) == 0 || len(currentTags) > 0 {
var tags []string
if vmConfig.Tags != nil {
for _, tag := range strings.Split(*vmConfig.Tags, ";") {
t := strings.TrimSpace(tag)
if len(t) > 0 {
tags = append(tags, t)
}
}
sort.Strings(tags)
}
err = d.Set(mkTags, tags)
diags = append(diags, diag.FromErr(err)...)
}
currentKeyboardLayout := d.Get(mkKeyboardLayout).(string)
if len(clone) == 0 || currentKeyboardLayout != dvKeyboardLayout {
if vmConfig.KeyboardLayout != nil {
err = d.Set(mkKeyboardLayout, *vmConfig.KeyboardLayout)
} else {
// Default value of "keyboard" is "" according to the API documentation.
err = d.Set(mkKeyboardLayout, "")
}
diags = append(diags, diag.FromErr(err)...)
}
currentMachine := d.Get(mkMachine).(string)
if len(clone) == 0 || currentMachine != dvMachineType {
if vmConfig.Machine != nil {
err = d.Set(mkMachine, *vmConfig.Machine)
} else {
err = d.Set(mkMachine, "")
}
diags = append(diags, diag.FromErr(err)...)
}
currentName := d.Get(mkName).(string)
if len(clone) == 0 || currentName != dvName {
if vmConfig.Name != nil {
err = d.Set(mkName, *vmConfig.Name)
} else {
// Default value of "name" is "" according to the API documentation.
err = d.Set(mkName, "")
}
diags = append(diags, diag.FromErr(err)...)
}
currentProtection := d.Get(mkProtection).(bool)
//nolint:gosimple
if len(clone) == 0 || currentProtection != dvProtection {
if vmConfig.DeletionProtection != nil {
err = d.Set(
mkProtection,
bool(*vmConfig.DeletionProtection),
)
} else {
// Default value of "protection" is "0" according to the API documentation.
err = d.Set(mkProtection, false)
}
diags = append(diags, diag.FromErr(err)...)
}
if !d.Get(mkTemplate).(bool) {
err = d.Set(mkStarted, vmStatus.Status == "running")
diags = append(diags, diag.FromErr(err)...)
}
currentTabletDevice := d.Get(mkTabletDevice).(bool)
//nolint:gosimple
if len(clone) == 0 || currentTabletDevice != dvTabletDevice {
if vmConfig.TabletDeviceEnabled != nil {
err = d.Set(
mkTabletDevice,
bool(*vmConfig.TabletDeviceEnabled),
)
} else {
// Default value of "tablet" is "1" according to the API documentation.
err = d.Set(mkTabletDevice, true)
}
diags = append(diags, diag.FromErr(err)...)
}
currentTemplate := d.Get(mkTemplate).(bool)
//nolint:gosimple
if len(clone) == 0 || currentTemplate != dvTemplate {
if vmConfig.Template != nil {
err = d.Set(mkTemplate, bool(*vmConfig.Template))
} else {
// Default value of "template" is "0" according to the API documentation.
err = d.Set(mkTemplate, false)
}
diags = append(diags, diag.FromErr(err)...)
}
return diags
}
// vmUpdatePool moves the VM to the pool it is supposed to be in if the pool ID changed.
func vmUpdatePool(
ctx context.Context,
d *schema.ResourceData,
api *pools.Client,
vmID int,
) error {
oldPoolValue, newPoolValue := d.GetChange(mkPoolID)
if cmp.Equal(newPoolValue, oldPoolValue) {
return nil
}
oldPool := oldPoolValue.(string)
newPool := newPoolValue.(string)
vmList := (types.CustomCommaSeparatedList)([]string{strconv.Itoa(vmID)})
tflog.Debug(ctx, fmt.Sprintf("Moving VM %d from pool '%s' to pool '%s'", vmID, oldPool, newPool))
if oldPool != "" {
trueValue := types.CustomBool(true)
poolUpdate := &pools.PoolUpdateRequestBody{
VMs: &vmList,
Delete: &trueValue,
}
err := api.UpdatePool(ctx, oldPool, poolUpdate)
if err != nil {
return fmt.Errorf("while removing VM %d from pool %s: %w", vmID, oldPool, err)
}
}
if newPool != "" {
poolUpdate := &pools.PoolUpdateRequestBody{VMs: &vmList}
err := api.UpdatePool(ctx, newPool, poolUpdate)
if err != nil {
return fmt.Errorf("while adding VM %d to pool %s: %w", vmID, newPool, err)
}
}
return nil
}
func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
config := m.(proxmoxtf.ProviderConfiguration)
client, e := config.GetClient()
if e != nil {
return diag.FromErr(e)
}
nodeName := d.Get(mkNodeName).(string)
rebootRequired := false
vmID, e := strconv.Atoi(d.Id())
if e != nil {
return diag.FromErr(e)
}
e = vmUpdatePool(ctx, d, client.Pool(), vmID)
if e != nil {
return diag.FromErr(e)
}
// If the node name has changed we need to migrate the VM to the new node before we do anything else.
if d.HasChange(mkNodeName) {
migrateTimeoutSec := d.Get(mkTimeoutMigrate).(int)
ctx, cancel := context.WithTimeout(ctx, time.Duration(migrateTimeoutSec)*time.Second)
defer cancel()
oldNodeNameValue, _ := d.GetChange(mkNodeName)
oldNodeName := oldNodeNameValue.(string)
vmAPI := client.Node(oldNodeName).VM(vmID)
trueValue := types.CustomBool(true)
migrateBody := &vms.MigrateRequestBody{
TargetNode: nodeName,
WithLocalDisks: &trueValue,
OnlineMigration: &trueValue,
}
err := vmAPI.MigrateVM(ctx, migrateBody)
if err != nil {
return diag.FromErr(err)
}
}
vmAPI := client.Node(nodeName).VM(vmID)
updateBody := &vms.UpdateRequestBody{}
var del []string
resource := VM()
// Retrieve the entire configuration as we need to process certain values.
vmConfig, e := vmAPI.GetVM(ctx)
if e != nil {
return diag.FromErr(e)
}
// Prepare the new primitive configuration values.
if d.HasChange(mkACPI) {
acpi := types.CustomBool(d.Get(mkACPI).(bool))
updateBody.ACPI = &acpi
rebootRequired = true
}
if d.HasChange(mkKVMArguments) {
kvmArguments := d.Get(mkKVMArguments).(string)
updateBody.KVMArguments = &kvmArguments
rebootRequired = true
}
if d.HasChange(mkBIOS) {
bios := d.Get(mkBIOS).(string)
updateBody.BIOS = &bios
rebootRequired = true
}
if d.HasChange(mkDescription) {
description := d.Get(mkDescription).(string)
updateBody.Description = &description
}
if d.HasChange(mkOnBoot) {
startOnBoot := types.CustomBool(d.Get(mkOnBoot).(bool))
updateBody.StartOnBoot = &startOnBoot
}
if d.HasChange(mkTags) {
tagString := vmGetTagsString(d)
updateBody.Tags = &tagString
}
if d.HasChange(mkKeyboardLayout) {
keyboardLayout := d.Get(mkKeyboardLayout).(string)
updateBody.KeyboardLayout = &keyboardLayout
rebootRequired = true
}
if d.HasChange(mkMachine) {
machine := d.Get(mkMachine).(string)
updateBody.Machine = &machine
rebootRequired = true
}
name := d.Get(mkName).(string)
if name == "" {
del = append(del, "name")
} else {
updateBody.Name = &name
}
if d.HasChange(mkProtection) {
protection := types.CustomBool(d.Get(mkProtection).(bool))
updateBody.DeletionProtection = &protection
}
if d.HasChange(mkTabletDevice) {
tabletDevice := types.CustomBool(d.Get(mkTabletDevice).(bool))
updateBody.TabletDeviceEnabled = &tabletDevice
rebootRequired = true
}
template := types.CustomBool(d.Get(mkTemplate).(bool))
if d.HasChange(mkTemplate) {
updateBody.Template = &template
rebootRequired = true
}
// Prepare the new agent configuration.
if d.HasChange(mkAgent) {
agentBlock, err := structure.GetSchemaBlock(
resource,
d,
[]string{mkAgent},
0,
true,
)
if err != nil {
return diag.FromErr(err)
}
agentEnabled := types.CustomBool(
agentBlock[mkAgentEnabled].(bool),
)
agentTrim := types.CustomBool(agentBlock[mkAgentTrim].(bool))
agentType := agentBlock[mkAgentType].(string)
updateBody.Agent = &vms.CustomAgent{
Enabled: &agentEnabled,
TrimClonedDisks: &agentTrim,
Type: &agentType,
}
rebootRequired = true
}
// Prepare the new audio devices.
if d.HasChange(mkAudioDevice) {
updateBody.AudioDevices = vmGetAudioDeviceList(d)
for i, ad := range updateBody.AudioDevices {
if !ad.Enabled {
del = append(del, fmt.Sprintf("audio%d", i))
}
}
for i := len(updateBody.AudioDevices); i < maxResourceVirtualEnvironmentVMAudioDevices; i++ {
del = append(del, fmt.Sprintf("audio%d", i))
}
rebootRequired = true
}
// Prepare the new boot configuration.
if d.HasChange(mkBootOrder) {
bootOrder := d.Get(mkBootOrder).([]interface{})
bootOrderConverted := make([]string, len(bootOrder))
for i, device := range bootOrder {
bootOrderConverted[i] = device.(string)
}
updateBody.Boot = &vms.CustomBoot{
Order: &bootOrderConverted,
}
rebootRequired = true
}
// Prepare the new CD-ROM configuration.
if d.HasChange(mkCDROM) {
cdromBlock, err := structure.GetSchemaBlock(
resource,
d,
[]string{mkCDROM},
0,
true,
)
if err != nil {
return diag.FromErr(err)
}
cdromEnabled := cdromBlock[mkCDROMEnabled].(bool)
cdromFileID := cdromBlock[mkCDROMFileID].(string)
cdromInterface := cdromBlock[mkCDROMInterface].(string)
old, _ := d.GetChange(mkCDROM)
if len(old.([]interface{})) > 0 && old.([]interface{})[0] != nil {
oldList := old.([]interface{})[0]
oldBlock := oldList.(map[string]interface{})
// If the interface is not set, use the default, for backward compatibility.
oldInterface, ok := oldBlock[mkCDROMInterface].(string)
if !ok || oldInterface == "" {
oldInterface = dvCDROMInterface
}
if oldInterface != cdromInterface {
del = append(del, oldInterface)
}
}
if !cdromEnabled && cdromFileID == "" {
del = append(del, cdromInterface)
}
if cdromFileID == "" {
cdromFileID = "cdrom"
}
cdromMedia := "cdrom"
updateBody.AddCustomStorageDevice(cdromInterface, vms.CustomStorageDevice{
Enabled: cdromEnabled,
FileVolume: cdromFileID,
Media: &cdromMedia,
})
}
// Prepare the new CPU configuration.
if d.HasChange(mkCPU) {
cpuBlock, err := structure.GetSchemaBlock(
resource,
d,
[]string{mkCPU},
0,
true,
)
if err != nil {
return diag.FromErr(err)
}
cpuArchitecture := cpuBlock[mkCPUArchitecture].(string)
cpuCores := cpuBlock[mkCPUCores].(int)
cpuFlags := cpuBlock[mkCPUFlags].([]interface{})
cpuHotplugged := cpuBlock[mkCPUHotplugged].(int)
cpuLimit := cpuBlock[mkCPULimit].(int)
cpuNUMA := types.CustomBool(cpuBlock[mkCPUNUMA].(bool))
cpuSockets := cpuBlock[mkCPUSockets].(int)
cpuType := cpuBlock[mkCPUType].(string)
cpuUnits := cpuBlock[mkCPUUnits].(int)
cpuAffinity := cpuBlock[mkCPUAffinity].(string)
if err = setCPUArchitecture(ctx, cpuArchitecture, client, updateBody); err != nil {
return diag.FromErr(err)
}
updateBody.CPUCores = ptr.Ptr(int64(cpuCores))
updateBody.CPUSockets = ptr.Ptr(int64(cpuSockets))
updateBody.CPUUnits = ptr.Ptr(int64(cpuUnits))
updateBody.NUMAEnabled = &cpuNUMA
// CPU affinity is a special case, only root can change it.
// we can't even have it in the delete list, as PVE will return an error for non-root.
// Hence, checking explicitly if it has changed.
if d.HasChange(mkCPU + ".0." + mkCPUAffinity) {
if cpuAffinity != "" {
updateBody.CPUAffinity = &cpuAffinity
} else {
del = append(del, "affinity")
}
}
if cpuHotplugged > 0 {
updateBody.VirtualCPUCount = ptr.Ptr(int64(cpuHotplugged))
} else {
del = append(del, "vcpus")
}
if cpuLimit > 0 {
updateBody.CPULimit = ptr.Ptr(int64(cpuLimit))
} else {
del = append(del, "cpulimit")
}
cpuFlagsConverted := make([]string, len(cpuFlags))
for fi, flag := range cpuFlags {
cpuFlagsConverted[fi] = flag.(string)
}
updateBody.CPUEmulation = &vms.CustomCPUEmulation{
Flags: &cpuFlagsConverted,
Type: cpuType,
}
rebootRequired = true
}
// Prepare the new disk device configuration.
allDiskInfo := disk.GetInfo(vmConfig, d)
planDisks, err := disk.GetDiskDeviceObjects(d, resource, nil)
if err != nil {
return diag.FromErr(err)
}
rr, err := disk.Update(ctx, client, nodeName, vmID, d, planDisks, allDiskInfo, updateBody)
if err != nil {
return diag.FromErr(err)
}
rebootRequired = rebootRequired || rr
// Prepare the new efi disk configuration.
if d.HasChange(mkEFIDisk) {
efiDisk := vmGetEfiDisk(d, nil)
updateBody.EFIDisk = efiDisk
rebootRequired = true
}
// Prepare the new tpm state configuration.
if d.HasChange(mkTPMState) {
tpmState := vmGetTPMState(d, nil)
updateBody.TPMState = tpmState
rebootRequired = true
}
// Prepare the new cloud-init configuration.
stoppedBeforeUpdate := false
if d.HasChange(mkInitialization) {
initializationConfig := vmGetCloudInitConfig(d)
updateBody.CloudInitConfig = initializationConfig
initialization := d.Get(mkInitialization).([]interface{})
if updateBody.CloudInitConfig != nil && len(initialization) > 0 && initialization[0] != nil {
var fileVolume string
initializationBlock := initialization[0].(map[string]interface{})
initializationDatastoreID := initializationBlock[mkInitializationDatastoreID].(string)
initializationInterface := initializationBlock[mkInitializationInterface].(string)
cdromMedia := "cdrom"
existingInterface := findExistingCloudInitDrive(vmConfig, vmID, "")
if initializationInterface == "" && existingInterface == "" {
initializationInterface = "ide2"
} else if initializationInterface == "" {
initializationInterface = existingInterface
}
mustMove := existingInterface != "" && initializationInterface != existingInterface
if mustMove {
tflog.Debug(ctx, fmt.Sprintf("CloudInit must be moved from %s to %s", existingInterface, initializationInterface))
}
oldInit, _ := d.GetChange(mkInitialization)
oldInitBlock := oldInit.([]interface{})[0].(map[string]interface{})
prevDatastoreID := oldInitBlock[mkInitializationDatastoreID].(string)
mustChangeDatastore := prevDatastoreID != initializationDatastoreID
if mustChangeDatastore {
tflog.Debug(ctx, fmt.Sprintf("CloudInit must be moved from datastore %s to datastore %s",
prevDatastoreID, initializationDatastoreID))
}
if mustMove || mustChangeDatastore || existingInterface == "" {
// CloudInit must be moved, either from a device to another or from a datastore
// to another (or both). This requires the VM to be stopped.
if err := vmShutdown(ctx, vmAPI, d); err != nil {
return err
}
if err := deleteIdeDrives(ctx, vmAPI, initializationInterface, existingInterface); err != nil {
return err
}
stoppedBeforeUpdate = true
fileVolume = fmt.Sprintf("%s:cloudinit", initializationDatastoreID)
} else {
ideDevice := getStorageDevice(vmConfig, existingInterface)
fileVolume = ideDevice.FileVolume
}
updateBody.AddCustomStorageDevice(initializationInterface, vms.CustomStorageDevice{
Enabled: true,
FileVolume: fileVolume,
Media: &cdromMedia,
})
}
rebootRequired = true
}
// Prepare the new hostpci devices configuration.
if d.HasChange(mkHostPCI) {
updateBody.PCIDevices = vmGetHostPCIDeviceObjects(d)
for i := len(updateBody.PCIDevices); i < maxResourceVirtualEnvironmentVMHostPCIDevices; i++ {
del = append(del, fmt.Sprintf("hostpci%d", i))
}
rebootRequired = true
}
// Prepare the new numa devices configuration.
if d.HasChange(mkNUMA) {
updateBody.NUMADevices = vmGetNumaDeviceObjects(d)
for i := len(updateBody.NUMADevices); i < maxResourceVirtualEnvironmentVMNUMADevices; i++ {
del = append(del, fmt.Sprintf("numa%d", i))
}
rebootRequired = true
}
// Prepare the new usb devices configuration.
if d.HasChange(mkHostUSB) {
updateBody.USBDevices = vmGetHostUSBDeviceObjects(d)
for i := len(updateBody.USBDevices); i < maxResourceVirtualEnvironmentVMHostUSBDevices; i++ {
del = append(del, fmt.Sprintf("usb%d", i))
}
rebootRequired = true
}
// Prepare the new memory configuration.
if d.HasChange(mkMemory) {
memoryBlock, er := structure.GetSchemaBlock(
resource,
d,
[]string{mkMemory},
0,
true,
)
if er != nil {
return diag.FromErr(er)
}
memoryDedicated := memoryBlock[mkMemoryDedicated].(int)
memoryFloating := memoryBlock[mkMemoryFloating].(int)
memoryShared := memoryBlock[mkMemoryShared].(int)
memoryHugepages := memoryBlock[mkMemoryHugepages].(string)
memoryKeepHugepages := types.CustomBool(memoryBlock[mkMemoryKeepHugepages].(bool))
updateBody.DedicatedMemory = &memoryDedicated
updateBody.FloatingMemory = &memoryFloating
if memoryShared > 0 {
memorySharedName := fmt.Sprintf("vm-%d-ivshmem", vmID)
updateBody.SharedMemory = &vms.CustomSharedMemory{
Name: &memorySharedName,
Size: memoryShared,
}
}
if d.HasChange(mkMemory + ".0." + mkMemoryHugepages) {
if memoryHugepages != "" {
updateBody.Hugepages = &memoryHugepages
} else {
del = append(del, "hugepages")
}
}
if d.HasChange(mkMemory + ".0." + mkMemoryKeepHugepages) {
if memoryHugepages != "" {
updateBody.KeepHugepages = &memoryKeepHugepages
} else {
del = append(del, "keephugepages")
}
}
rebootRequired = true
}
// Prepare the new network device configuration.
if d.HasChange(network.MkNetworkDevice) {
updateBody.NetworkDevices, err = network.GetNetworkDeviceObjects(d)
if err != nil {
return diag.FromErr(err)
}
for i, nd := range updateBody.NetworkDevices {
if !nd.Enabled {
del = append(del, fmt.Sprintf("net%d", i))
}
}
for i := len(updateBody.NetworkDevices); i < network.MaxNetworkDevices; i++ {
del = append(del, fmt.Sprintf("net%d", i))
}
rebootRequired = true
}
// Prepare the new operating system configuration.
if d.HasChange(mkOperatingSystem) {
operatingSystem, err := structure.GetSchemaBlock(
resource,
d,
[]string{mkOperatingSystem},
0,
true,
)
if err != nil {
return diag.FromErr(err)
}
operatingSystemType := operatingSystem[mkOperatingSystemType].(string)
updateBody.OSType = &operatingSystemType
rebootRequired = true
}
// Prepare the new serial devices.
if d.HasChange(mkSerialDevice) {
updateBody.SerialDevices = vmGetSerialDeviceList(d)
for i := len(updateBody.SerialDevices); i < maxResourceVirtualEnvironmentVMSerialDevices; i++ {
del = append(del, fmt.Sprintf("serial%d", i))
}
rebootRequired = true
}
if d.HasChange(mkSMBIOS) {
updateBody.SMBIOS = vmGetSMBIOS(d)
if updateBody.SMBIOS == nil {
del = append(del, "smbios1")
}
}
if d.HasChange(mkStartup) {
updateBody.StartupOrder = vmGetStartupOrder(d)
if updateBody.StartupOrder == nil {
del = append(del, "startup")
}
}
// Prepare the new VGA configuration.
if d.HasChange(mkVGA) {
updateBody.VGADevice, e = vmGetVGADeviceObject(d)
if e != nil {
return diag.FromErr(e)
}
rebootRequired = true
}
// Prepare the new SCSI hardware type
if d.HasChange(mkSCSIHardware) {
scsiHardware := d.Get(mkSCSIHardware).(string)
updateBody.SCSIHardware = &scsiHardware
rebootRequired = true
}
if d.HasChanges(mkHookScriptFileID) {
hookScript := d.Get(mkHookScriptFileID).(string)
if len(hookScript) > 0 {
updateBody.HookScript = &hookScript
} else {
del = append(del, "hookscript")
}
}
// Prepare the new watchdog configuration.
if d.HasChange(mkWatchdog) {
watchdogBlock, err := structure.GetSchemaBlock(
resource,
d,
[]string{mkWatchdog},
0,
true,
)
if err != nil {
return diag.FromErr(err)
}
watchdogEnabled := types.CustomBool(
watchdogBlock[mkWatchdogEnabled].(bool),
)
if watchdogEnabled {
watchdogAction := watchdogBlock[mkWatchdogAction].(string)
watchdogModel := watchdogBlock[mkWatchdogModel].(string)
updateBody.WatchdogDevice = &vms.CustomWatchdogDevice{
Action: &watchdogAction,
Model: &watchdogModel,
}
} else {
del = append(del, "watchdog")
}
rebootRequired = true
}
// Update the configuration now that everything has been prepared.
updateBody.Delete = del
e = vmAPI.UpdateVM(ctx, updateBody)
if e != nil {
return diag.FromErr(e)
}
// Determine if the state of the virtual machine state needs to be changed.
//nolint: nestif
if (d.HasChange(mkStarted) || stoppedBeforeUpdate) && !bool(template) {
started := d.Get(mkStarted).(bool)
if started {
if diags := vmStart(ctx, vmAPI, d); diags != nil {
return diags
}
} else {
if e := vmShutdown(ctx, vmAPI, d); e != nil {
return e
}
rebootRequired = false
}
}
// Change the disk locations and/or sizes, if necessary.
return vmUpdateDiskLocationAndSize(
ctx,
d,
m,
!bool(template) && rebootRequired,
)
}
func vmUpdateDiskLocationAndSize(
ctx context.Context,
d *schema.ResourceData,
m interface{},
reboot bool,
) diag.Diagnostics {
config := m.(proxmoxtf.ProviderConfiguration)
client, err := config.GetClient()
if err != nil {
return diag.FromErr(err)
}
nodeName := d.Get(mkNodeName).(string)
started := d.Get(mkStarted).(bool)
template := d.Get(mkTemplate).(bool)
vmID, err := strconv.Atoi(d.Id())
if err != nil {
return diag.FromErr(err)
}
vmAPI := client.Node(nodeName).VM(vmID)
// Determine if any of the disks are changing location and/or size, and initiate the necessary actions.
//nolint: nestif
if d.HasChange(disk.MkDisk) {
diskOld, diskNew := d.GetChange(disk.MkDisk)
resource := VM()
diskOldEntries, err := disk.GetDiskDeviceObjects(
d,
resource,
diskOld.([]interface{}),
)
if err != nil {
return diag.FromErr(err)
}
diskNewEntries, err := disk.GetDiskDeviceObjects(
d,
resource,
diskNew.([]interface{}),
)
if err != nil {
return diag.FromErr(err)
}
// Add efidisk if it has changes
if d.HasChange(mkEFIDisk) {
diskOld, diskNew := d.GetChange(mkEFIDisk)
oldEfiDisk, e := vmGetEfiDiskAsStorageDevice(d, diskOld.([]interface{}))
if e != nil {
return diag.FromErr(e)
}
newEfiDisk, e := vmGetEfiDiskAsStorageDevice(d, diskNew.([]interface{}))
if e != nil {
return diag.FromErr(e)
}
if oldEfiDisk != nil {
diskOldEntries["efidisk0"] = oldEfiDisk
}
if newEfiDisk != nil {
diskNewEntries["efidisk0"] = newEfiDisk
}
if oldEfiDisk != nil && newEfiDisk != nil && oldEfiDisk.Size != newEfiDisk.Size {
return diag.Errorf(
"resizing of efidisk is not supported.",
)
}
}
// Add tpm state if it has changes
if d.HasChange(mkTPMState) {
diskOld, diskNew := d.GetChange(mkTPMState)
oldTPMState := vmGetTPMStateAsStorageDevice(d, diskOld.([]interface{}))
newTPMState := vmGetTPMStateAsStorageDevice(d, diskNew.([]interface{}))
if oldTPMState != nil {
diskOldEntries["tpmstate0"] = oldTPMState
}
if newTPMState != nil {
diskNewEntries["tpmstate0"] = newTPMState
}
if oldTPMState != nil && newTPMState != nil && oldTPMState.Size != newTPMState.Size {
return diag.Errorf(
"resizing of tpm state is not supported.",
)
}
}
var diskMoveBodies []*vms.MoveDiskRequestBody
var diskResizeBodies []*vms.ResizeDiskRequestBody
shutdownForDisksRequired := false
for oldIface, oldDisk := range diskOldEntries {
if _, present := diskNewEntries[oldIface]; !present {
return diag.Errorf(
"deletion of disks not supported. Please delete disk by hand. Old interface was %q",
oldIface,
)
}
if *oldDisk.DatastoreID != *diskNewEntries[oldIface].DatastoreID {
if oldDisk.IsOwnedBy(vmID) {
deleteOriginalDisk := types.CustomBool(true)
diskMoveBodies = append(
diskMoveBodies,
&vms.MoveDiskRequestBody{
DeleteOriginalDisk: &deleteOriginalDisk,
Disk: oldIface,
TargetStorage: *diskNewEntries[oldIface].DatastoreID,
},
)
// Cannot be done while VM is running.
shutdownForDisksRequired = true
} else {
return diag.Errorf(
"Cannot move %s:%s to datastore %s in VM %d configuration, it is not owned by this VM!",
*oldDisk.DatastoreID,
*oldDisk.PathInDatastore(),
*diskNewEntries[oldIface].DatastoreID,
vmID,
)
}
}
if *oldDisk.Size != *diskNewEntries[oldIface].Size {
if *oldDisk.Size < *diskNewEntries[oldIface].Size {
if oldDisk.IsOwnedBy(vmID) {
diskResizeBodies = append(
diskResizeBodies,
&vms.ResizeDiskRequestBody{
Disk: oldIface,
Size: *diskNewEntries[oldIface].Size,
},
)
} else {
return diag.Errorf(
"Cannot resize %s:%s in VM %d, it is not owned by this VM!",
*oldDisk.DatastoreID,
*oldDisk.PathInDatastore(),
vmID,
)
}
} else {
return diag.Errorf(
"Cannot shrink %s:%s in VM %d, it is not supported!",
*oldDisk.DatastoreID,
*oldDisk.PathInDatastore(),
vmID,
)
}
}
}
if shutdownForDisksRequired && !template {
if e := vmShutdown(ctx, vmAPI, d); e != nil {
return e
}
}
for _, reqBody := range diskMoveBodies {
err = vmAPI.MoveVMDisk(ctx, reqBody)
if err != nil {
return diag.FromErr(err)
}
}
for _, reqBody := range diskResizeBodies {
err = vmAPI.ResizeVMDisk(ctx, reqBody)
if err != nil {
return diag.FromErr(err)
}
}
if shutdownForDisksRequired && started && !template {
if diags := vmStart(ctx, vmAPI, d); diags != nil {
return diags
}
// This concludes an equivalent of a reboot, avoid doing another.
reboot = false
}
}
// Perform a regular reboot in case it's necessary and haven't already been done.
if reboot {
vmStatus, err := vmAPI.GetVMStatus(ctx)
if err != nil {
return diag.FromErr(err)
}
if vmStatus.Status != "stopped" {
rebootTimeoutSec := d.Get(mkTimeoutReboot).(int)
err := vmAPI.RebootVM(
ctx,
&vms.RebootRequestBody{
Timeout: &rebootTimeoutSec,
},
)
if err != nil {
return diag.FromErr(err)
}
}
}
return vmRead(ctx, d, m)
}
func vmDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
timeout := d.Get(mkTimeoutStopVM).(int)
shutdownTimeout := d.Get(mkTimeoutShutdownVM).(int)
if shutdownTimeout > timeout {
timeout = shutdownTimeout
}
ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
defer cancel()
config := m.(proxmoxtf.ProviderConfiguration)
client, err := config.GetClient()
if err != nil {
return diag.FromErr(err)
}
nodeName := d.Get(mkNodeName).(string)
vmID, err := strconv.Atoi(d.Id())
if err != nil {
return diag.FromErr(err)
}
vmAPI := client.Node(nodeName).VM(vmID)
// Stop or shut down the virtual machine before deleting it.
status, err := vmAPI.GetVMStatus(ctx)
if err != nil {
return diag.FromErr(err)
}
stop := d.Get(mkStopOnDestroy).(bool)
//nolint: nestif
if status.Status != "stopped" {
if stop {
if e := vmStop(ctx, vmAPI, d); e != nil {
return e
}
} else {
if e := vmShutdown(ctx, vmAPI, d); e != nil {
return e
}
}
}
err = vmAPI.DeleteVM(ctx)
if err != nil {
if errors.Is(err, api.ErrResourceDoesNotExist) {
d.SetId("")
return nil
}
return diag.FromErr(err)
}
// Wait for the state to become unavailable as that clearly indicates the destruction of the VM.
err = vmAPI.WaitForVMStatus(ctx, "")
if err == nil {
return diag.Errorf("failed to delete VM \"%d\"", vmID)
}
d.SetId("")
return nil
}
// getDiskDatastores returns a list of the used datastores in a VM.
func getDiskDatastores(vm *vms.GetResponseData, d *schema.ResourceData) []string {
storageDevices := disk.GetInfo(vm, d)
datastoresSet := map[string]int{}
for _, diskInfo := range storageDevices {
// Ignore empty storage devices and storage devices (like ide) which may not have any media mounted
if diskInfo == nil || diskInfo.FileVolume == "none" {
continue
}
fileIDParts := strings.Split(diskInfo.FileVolume, ":")
datastoresSet[fileIDParts[0]] = 1
}
if vm.EFIDisk != nil {
fileIDParts := strings.Split(vm.EFIDisk.FileVolume, ":")
datastoresSet[fileIDParts[0]] = 1
}
if vm.TPMState != nil {
fileIDParts := strings.Split(vm.TPMState.FileVolume, ":")
datastoresSet[fileIDParts[0]] = 1
}
var datastores []string //nolint: prealloc
for datastore := range datastoresSet {
datastores = append(datastores, datastore)
}
return datastores
}
func getNUMAInfo(resp *vms.GetResponseData, _ *schema.ResourceData) map[string]*vms.CustomNUMADevice {
numaDevices := map[string]*vms.CustomNUMADevice{}
numaDevices["numa0"] = resp.NUMADevices0
numaDevices["numa1"] = resp.NUMADevices1
numaDevices["numa2"] = resp.NUMADevices2
numaDevices["numa3"] = resp.NUMADevices3
numaDevices["numa4"] = resp.NUMADevices4
numaDevices["numa5"] = resp.NUMADevices5
numaDevices["numa6"] = resp.NUMADevices6
numaDevices["numa7"] = resp.NUMADevices7
return numaDevices
}
func getUSBInfo(resp *vms.GetResponseData, _ *schema.ResourceData) map[string]*vms.CustomUSBDevice {
usbDevices := map[string]*vms.CustomUSBDevice{}
usbDevices["usb0"] = resp.USBDevice0
usbDevices["usb1"] = resp.USBDevice1
usbDevices["usb2"] = resp.USBDevice2
usbDevices["usb3"] = resp.USBDevice3
return usbDevices
}
func parseImportIDWithNodeName(id string) (string, string, error) {
nodeName, id, found := strings.Cut(id, "/")
if !found {
return "", "", fmt.Errorf("unexpected format of ID (%s), expected node/id", id)
}
return nodeName, id, nil
}
func getAgentTimeout(d *schema.ResourceData) (time.Duration, error) {
resource := VM()
agentBlock, err := structure.GetSchemaBlock(
resource,
d,
[]string{mkAgent},
0,
true,
)
if err != nil {
return 0, fmt.Errorf("failed to get agent block: %w", err)
}
agentTimeout, err := time.ParseDuration(
agentBlock[mkAgentTimeout].(string),
)
if err != nil {
return 0, fmt.Errorf("failed to parse agent timeout: %w", err)
}
return agentTimeout, nil
}