mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-07-04 21:14:05 +00:00
Fix for another use case of mismanaged default value. This one was a bit trickier to spot as it triggered only when provider is authenticated using root@pam, as architecture change is allowed only for root. Removing default value altogether, as the PVE API default for this attribute is an empty string. VM2 resource will have no such issue, related: #1310, #1311 Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
5590 lines
153 KiB
Go
5590 lines
153 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/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 = ""
|
|
|
|
maxResourceVirtualEnvironmentVMAudioDevices = 1
|
|
maxResourceVirtualEnvironmentVMSerialDevices = 4
|
|
maxResourceVirtualEnvironmentVMHostPCIDevices = 8
|
|
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 an "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"
|
|
mkVGAEnabled = "enabled"
|
|
mkVGAMemory = "memory"
|
|
mkVGAType = "type"
|
|
mkVMID = "vm_id"
|
|
mkSCSIHardware = "scsi_hardware"
|
|
mkHookScriptFileID = "hook_script_file_id"
|
|
mkStopOnDestroy = "stop_on_destroy"
|
|
)
|
|
|
|
// 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,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
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,
|
|
},
|
|
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)),
|
|
},
|
|
mkVGAEnabled: {
|
|
Type: schema.TypeBool,
|
|
Deprecated: "The `enabled` attribute is deprecated and will be removed in a future release. " +
|
|
"Use type `none` instead.",
|
|
Description: "Whether to enable the VGA device",
|
|
Optional: 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,
|
|
},
|
|
}
|
|
|
|
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 {
|
|
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 {
|
|
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{})
|
|
|
|
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.CustomStorageDevices.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.CustomStorageDevices[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 := client.Cluster().GetVMID(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{})
|
|
|
|
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)
|
|
}
|
|
|
|
// Only the root account is allowed to change the CPU architecture, which makes this check necessary.
|
|
if client.API().IsRootTicket() && cpuArchitecture != "" {
|
|
updateBody.CPUArchitecture = &cpuArchitecture
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
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 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 := client.Cluster().GetVMID(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)
|
|
|
|
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,
|
|
CustomStorageDevices: diskDeviceObjects,
|
|
}
|
|
|
|
// Only the root account is allowed to change the CPU architecture, which makes this check necessary.
|
|
if client.API().IsRootTicket() && cpuArchitecture != "" {
|
|
createBody.CPUArchitecture = &cpuArchitecture
|
|
}
|
|
|
|
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 i, pciDeviceEntry := range pciDevice {
|
|
block := pciDeviceEntry.(map[string]interface{})
|
|
|
|
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[i] = 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.
|
|
// However, assume the provider's default value as a workaround when the root account is not being used.
|
|
if !client.API().IsRootTicket() {
|
|
cpu[mkCPUArchitecture] = dvCPUArchitecture
|
|
} else {
|
|
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{}{}
|
|
|
|
pciDevices := getPCIInfo(vmConfig, d)
|
|
for pi, pp := range 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)...)
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
// Only the root account is allowed to change the CPU architecture, which makes this check necessary.
|
|
if client.API().IsRootTicket() && cpuArchitecture != "" {
|
|
updateBody.CPUArchitecture = &cpuArchitecture
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
// 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.IsOwnedBy(vmID) {
|
|
diskResizeBodies = append(
|
|
diskResizeBodies,
|
|
&vms.ResizeDiskRequestBody{
|
|
Disk: oldIface,
|
|
Size: *diskNewEntries[oldIface].Size,
|
|
},
|
|
)
|
|
} else {
|
|
return diag.Errorf(
|
|
"Cannot resize %s:%s in VM %d configuration, it is not owned by this VM!",
|
|
*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 getPCIInfo(resp *vms.GetResponseData, _ *schema.ResourceData) map[string]*vms.CustomPCIDevice {
|
|
pciDevices := map[string]*vms.CustomPCIDevice{}
|
|
|
|
pciDevices["hostpci0"] = resp.PCIDevice0
|
|
pciDevices["hostpci1"] = resp.PCIDevice1
|
|
pciDevices["hostpci2"] = resp.PCIDevice2
|
|
pciDevices["hostpci3"] = resp.PCIDevice3
|
|
|
|
return pciDevices
|
|
}
|
|
|
|
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
|
|
}
|