/* * 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" "errors" "fmt" "regexp" "sort" "strconv" "strings" "time" "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/helpers/ptr" "github.com/bpg/terraform-provider-proxmox/proxmox/nodes/containers" "github.com/bpg/terraform-provider-proxmox/proxmox/types" "github.com/bpg/terraform-provider-proxmox/proxmoxtf" "github.com/bpg/terraform-provider-proxmox/proxmoxtf/resource/validators" resource "github.com/bpg/terraform-provider-proxmox/proxmoxtf/resource/vm" "github.com/bpg/terraform-provider-proxmox/proxmoxtf/structure" "github.com/bpg/terraform-provider-proxmox/utils" ) const ( dvCloneDatastoreID = "" dvCloneNodeName = "" dvConsoleEnabled = true dvConsoleMode = "tty" dvConsoleTTYCount = 2 dvInitializationDNSDomain = "" dvInitializationDNSServer = "" dvInitializationIPConfigIPv4Address = "" dvInitializationIPConfigIPv4Gateway = "" dvInitializationIPConfigIPv6Address = "" dvInitializationIPConfigIPv6Gateway = "" dvInitializationHostname = "" dvInitializationUserAccountPassword = "" dvCPUArchitecture = "amd64" dvCPUCores = 1 dvCPUUnits = 1024 dvDescription = "" dvDevicePassthroughMode = "0660" dvDiskDatastoreID = "local" dvDiskSize = 4 dvFeaturesNesting = false dvFeaturesKeyControl = false dvFeaturesFUSE = false dvHookScript = "" dvMemoryDedicated = 512 dvMemorySwap = 0 dvMountPointACL = false dvMountPointBackup = false dvMountPointPath = "" dvMountPointQuota = false dvMountPointReadOnly = false dvMountPointReplicate = true dvMountPointShared = false dvMountPointSize = "" dvNetworkInterfaceBridge = "vmbr0" dvNetworkInterfaceEnabled = true dvNetworkInterfaceFirewall = false dvNetworkInterfaceMACAddress = "" dvNetworkInterfaceRateLimit = 0 dvNetworkInterfaceVLANID = 0 dvNetworkInterfaceMTU = 0 dvOperatingSystemType = "unmanaged" dvPoolID = "" dvProtection = false dvStarted = true dvStartupOrder = -1 dvStartupUpDelay = -1 dvStartupDownDelay = -1 dvStartOnBoot = true dvTemplate = false dvTimeoutCreate = 1800 dvTimeoutClone = 1800 dvTimeoutUpdate = 1800 dvTimeoutDelete = 60 dvUnprivileged = false maxNetworkInterfaces = 10 maxPassthroughDevices = 8 maxMountPoints = 256 mkClone = "clone" mkCloneDatastoreID = "datastore_id" mkCloneNodeName = "node_name" mkCloneVMID = "vm_id" mkConsole = "console" mkConsoleEnabled = "enabled" mkConsoleMode = "type" mkConsoleTTYCount = "tty_count" mkCPU = "cpu" mkCPUArchitecture = "architecture" mkCPUCores = "cores" mkCPUUnits = "units" mkDescription = "description" mkDisk = "disk" mkDiskDatastoreID = "datastore_id" mkDiskSize = "size" mkFeatures = "features" mkFeaturesNesting = "nesting" mkFeaturesKeyControl = "keyctl" mkFeaturesFUSE = "fuse" mkFeaturesMountTypes = "mount" mkHookScriptFileID = "hook_script_file_id" mkInitialization = "initialization" mkInitializationDNS = "dns" mkInitializationDNSDomain = "domain" mkInitializationDNSServer = "server" mkInitializationDNSServers = "servers" mkInitializationHostname = "hostname" mkInitializationIPConfig = "ip_config" mkInitializationIPConfigIPv4 = "ipv4" mkInitializationIPConfigIPv4Address = "address" mkInitializationIPConfigIPv4Gateway = "gateway" mkInitializationIPConfigIPv6 = "ipv6" mkInitializationIPConfigIPv6Address = "address" mkInitializationIPConfigIPv6Gateway = "gateway" mkInitializationUserAccount = "user_account" mkInitializationUserAccountKeys = "keys" mkInitializationUserAccountPassword = "password" mkMemory = "memory" mkMemoryDedicated = "dedicated" mkMemorySwap = "swap" mkMountPoint = "mount_point" mkMountPointACL = "acl" mkMountPointBackup = "backup" mkMountPointMountOptions = "mount_options" mkMountPointPath = "path" mkMountPointQuota = "quota" mkMountPointReadOnly = "read_only" mkMountPointReplicate = "replicate" mkMountPointShared = "shared" mkMountPointSize = "size" mkMountPointVolume = "volume" mkDevicePassthroughDenyWrite = "deny_write" mkDevicePassthrough = "device_passthrough" // #nosec G101 mkDevicePassthroughPath = "path" mkDevicePassthroughUID = "uid" mkDevicePassthroughGID = "gid" mkDevicePassthroughMode = "mode" mkNetworkInterface = "network_interface" mkNetworkInterfaceBridge = "bridge" mkNetworkInterfaceEnabled = "enabled" mkNetworkInterfaceFirewall = "firewall" mkNetworkInterfaceMACAddress = "mac_address" mkNetworkInterfaceName = "name" mkNetworkInterfaceRateLimit = "rate_limit" mkNetworkInterfaceVLANID = "vlan_id" mkNetworkInterfaceMTU = "mtu" mkNodeName = "node_name" mkOperatingSystem = "operating_system" mkOperatingSystemTemplateFileID = "template_file_id" mkOperatingSystemType = "type" mkPoolID = "pool_id" mkProtection = "protection" mkStarted = "started" mkStartup = "startup" mkStartupOrder = "order" mkStartupUpDelay = "up_delay" mkStartupDownDelay = "down_delay" mkStartOnBoot = "start_on_boot" mkTags = "tags" mkTemplate = "template" mkTimeoutCreate = "timeout_create" mkTimeoutClone = "timeout_clone" mkTimeoutUpdate = "timeout_update" mkTimeoutDelete = "timeout_delete" mkUnprivileged = "unprivileged" mkVMID = "vm_id" ) // Container returns a resource that manages a container. func Container() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ 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{ 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 container", Required: true, ForceNew: true, ValidateDiagFunc: resource.VMIDValidator(), }, }, }, MaxItems: 1, MinItems: 0, }, mkConsole: { Type: schema.TypeList, Description: "The console configuration", Optional: true, DefaultFunc: func() (interface{}, error) { return []interface{}{ map[string]interface{}{ mkConsoleEnabled: dvConsoleEnabled, mkConsoleMode: dvConsoleMode, mkConsoleTTYCount: dvConsoleTTYCount, }, }, nil }, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ mkConsoleEnabled: { Type: schema.TypeBool, Description: "Whether to enable the console device", Optional: true, Default: dvConsoleEnabled, }, mkConsoleMode: { Type: schema.TypeString, Description: "The console mode", Optional: true, Default: dvConsoleMode, ValidateDiagFunc: ConsoleModeValidator(), }, mkConsoleTTYCount: { Type: schema.TypeInt, Description: "The number of available TTY", Optional: true, Default: dvConsoleTTYCount, ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(0, 6)), }, }, }, 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, mkCPUUnits: dvCPUUnits, }, }, 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, 128)), }, mkCPUUnits: { Type: schema.TypeInt, Description: "The CPU units", Optional: true, Default: dvCPUUnits, ValidateDiagFunc: validation.ToDiagFunc( validation.IntBetween(0, 500000), ), }, }, }, 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) if i.(string) != "" { return strings.ReplaceAll(strings.TrimSpace(i.(string)), "\r\n", "\n") + "\n" } return "" }, }, mkDisk: { Type: schema.TypeList, Description: "The disks", Optional: true, ForceNew: true, DefaultFunc: func() (interface{}, error) { return []interface{}{ map[string]interface{}{ mkDiskDatastoreID: dvDiskDatastoreID, mkDiskSize: dvDiskSize, }, }, nil }, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ mkDiskDatastoreID: { Type: schema.TypeString, Description: "The datastore id", Optional: true, ForceNew: true, Default: dvDiskDatastoreID, }, mkDiskSize: { Type: schema.TypeInt, Description: "The rootfs size in gigabytes", Optional: true, ForceNew: true, Default: dvDiskSize, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), }, }, }, MaxItems: 1, MinItems: 0, }, mkFeatures: { Type: schema.TypeList, Description: "Features", Optional: true, DefaultFunc: func() (interface{}, error) { return []interface{}{ map[string]interface{}{ mkFeaturesNesting: dvFeaturesNesting, mkFeaturesKeyControl: dvFeaturesKeyControl, mkFeaturesFUSE: dvFeaturesFUSE, mkFeaturesMountTypes: []interface{}{}, }, }, nil }, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ mkFeaturesNesting: { Type: schema.TypeBool, Description: "Whether the container runs as nested", Optional: true, Default: dvFeaturesNesting, }, mkFeaturesKeyControl: { Type: schema.TypeBool, Description: "Whether the container supports `keyctl()` system call", Optional: true, Default: dvFeaturesKeyControl, }, mkFeaturesFUSE: { Type: schema.TypeBool, Description: "Whether the container supports FUSE mounts", Optional: true, Default: dvFeaturesFUSE, }, mkFeaturesMountTypes: { Type: schema.TypeList, Description: "List of allowed mount types", Optional: true, Elem: &schema.Schema{ Type: schema.TypeString, ValidateDiagFunc: MountTypeValidator(), }, }, }, }, MaxItems: 1, MinItems: 0, }, mkHookScriptFileID: { Type: schema.TypeString, Description: "A hook script", Optional: true, Default: dvHookScript, }, mkInitialization: { Type: schema.TypeList, Description: "The initialization configuration", Optional: true, DefaultFunc: func() (interface{}, error) { return []interface{}{}, nil }, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ 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, }, mkInitializationHostname: { Type: schema.TypeString, Description: "The hostname", Optional: true, Default: dvInitializationHostname, }, 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, DefaultFunc: func() (interface{}, error) { return []interface{}{}, nil }, 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, "*", "") == "" }, }, }, }, MaxItems: 1, MinItems: 0, }, }, }, MaxItems: 1, MinItems: 0, }, mkMemory: { Type: schema.TypeList, Description: "The memory allocation", Optional: true, DefaultFunc: func() (interface{}, error) { return []interface{}{ map[string]interface{}{ mkMemoryDedicated: dvMemoryDedicated, mkMemorySwap: dvMemorySwap, }, }, 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(16, 268435456), ), }, mkMemorySwap: { Type: schema.TypeInt, Description: "The swap size in megabytes", Optional: true, Default: dvMemorySwap, ValidateDiagFunc: validation.ToDiagFunc( validation.IntBetween(0, 268435456), ), }, }, }, MaxItems: 1, MinItems: 0, }, mkMountPoint: { Type: schema.TypeList, Description: "A mount point", Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ mkMountPointACL: { Type: schema.TypeBool, Description: "Explicitly enable or disable ACL support", Optional: true, Default: dvMountPointACL, }, mkMountPointBackup: { Type: schema.TypeBool, Description: "Whether to include the mount point in backups (only used for volume mount points)", Optional: true, Default: dvMountPointBackup, }, mkMountPointMountOptions: { Type: schema.TypeList, Description: "Extra mount options.", Optional: true, Elem: &schema.Schema{ Type: schema.TypeString, }, }, mkMountPointPath: { Type: schema.TypeString, Description: "Path to the mount point as seen from inside the container", Required: true, // StateFunc: func(i interface{}) string { // // PVE strips leading slashes from the path, so we have to do the same // return strings.TrimPrefix(i.(string), "/") // }, DiffSuppressFunc: func(_, oldVal, newVal string, _ *schema.ResourceData) bool { return "/"+oldVal == newVal }, }, mkMountPointQuota: { Type: schema.TypeBool, Description: "Enable user quotas inside the container (not supported with volume mounts)", Optional: true, Default: dvMountPointQuota, }, mkMountPointReadOnly: { Type: schema.TypeBool, Description: "Read-only mount point", Optional: true, Default: dvMountPointReadOnly, }, mkMountPointReplicate: { Type: schema.TypeBool, Description: "Will include this volume to a storage replica job", Optional: true, Default: dvMountPointReplicate, }, mkMountPointShared: { Type: schema.TypeBool, Description: "Mark this non-volume mount point as available on all nodes", Optional: true, Default: dvMountPointShared, }, mkMountPointSize: { Type: schema.TypeString, Description: "Volume size (only used for volume mount points)", Optional: true, Default: dvMountPointSize, ValidateDiagFunc: validators.FileSize(), }, mkMountPointVolume: { Type: schema.TypeString, Description: "Volume, device or directory to mount into the container", Required: true, DiffSuppressFunc: func(_, oldVal, newVal string, _ *schema.ResourceData) bool { // For *new* volume mounts PVE returns an actual volume ID which is saved in the stare, // so on reapply the provider will try override it:" // "local-lvm" -> "local-lvm:vm-101-disk-1" // "local-lvm:8" -> "local-lvm:vm-101-disk-1" // There is also an option to mount an existing volume, so // "local-lvm:vm-101-disk-1" -> "local-lvm:vm-101-disk-1" // which is a valid case. return oldVal == newVal || strings.HasPrefix(oldVal, strings.Split(newVal, ":")[0]+":") }, }, }, }, MaxItems: maxMountPoints, MinItems: 0, }, mkDevicePassthrough: { Type: schema.TypeList, Description: "Device to pass through to the container", Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ mkDevicePassthroughDenyWrite: { Type: schema.TypeBool, Description: "Deny the container to write to the device", Optional: true, Default: false, }, mkDevicePassthroughGID: { Type: schema.TypeInt, Description: "Group ID to be assigned to the device node", Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), }, mkDevicePassthroughMode: { Type: schema.TypeString, Description: "Access mode to be set on the device node (e.g. 0666)", Optional: true, Default: dvDevicePassthroughMode, ValidateDiagFunc: validation.ToDiagFunc(validation.StringMatch( regexp.MustCompile(`0[0-7]{3}`), "Octal access mode", )), }, mkDevicePassthroughPath: { Type: schema.TypeString, Description: "Device to pass through to the container", Required: true, ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), }, mkDevicePassthroughUID: { Type: schema.TypeInt, Description: "Device UID in the container", Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), }, }, }, MaxItems: maxPassthroughDevices, MinItems: 0, }, mkNetworkInterface: { Type: schema.TypeList, Description: "The network interfaces", Optional: true, DefaultFunc: func() (interface{}, error) { return make([]interface{}, 1), nil }, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ mkNetworkInterfaceBridge: { Type: schema.TypeString, Description: "The bridge", Optional: true, Default: dvNetworkInterfaceBridge, }, mkNetworkInterfaceEnabled: { Type: schema.TypeBool, Description: "Whether to enable the network device", Optional: true, Default: dvNetworkInterfaceEnabled, }, mkNetworkInterfaceFirewall: { Type: schema.TypeBool, Description: "Whether this interface's firewall rules should be used.", Optional: true, Default: dvNetworkInterfaceFirewall, }, mkNetworkInterfaceMACAddress: { Type: schema.TypeString, Description: "The MAC address", Optional: true, Default: dvNetworkInterfaceMACAddress, DiffSuppressFunc: func(_, _, newVal string, _ *schema.ResourceData) bool { return newVal == "" }, ValidateDiagFunc: validators.MACAddress(), }, mkNetworkInterfaceName: { Type: schema.TypeString, Description: "The network interface name", Required: true, }, mkNetworkInterfaceRateLimit: { Type: schema.TypeFloat, Description: "The rate limit in megabytes per second", Optional: true, Default: dvNetworkInterfaceRateLimit, }, mkNetworkInterfaceVLANID: { Type: schema.TypeInt, Description: "The VLAN identifier", Optional: true, Default: dvNetworkInterfaceVLANID, }, mkNetworkInterfaceMTU: { Type: schema.TypeInt, Description: "Maximum transmission unit (MTU)", Optional: true, Default: dvNetworkInterfaceMTU, }, }, }, MaxItems: maxNetworkInterfaces, MinItems: 0, }, mkNodeName: { Type: schema.TypeString, Description: "The node name", Required: true, ForceNew: true, }, mkOperatingSystem: { Type: schema.TypeList, Description: "The operating system configuration", Optional: true, ForceNew: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ mkOperatingSystemTemplateFileID: { Type: schema.TypeString, Description: "The ID of an OS template file", Required: true, ForceNew: true, ValidateDiagFunc: validators.FileID(), }, 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 container to", Optional: true, ForceNew: true, Default: dvPoolID, }, mkProtection: { Type: schema.TypeBool, Description: "Whether to set the protection flag of the container. " + "This will prevent the container itself and its disk for remove/update operations.", Optional: true, ForceNew: false, Default: dvProtection, }, mkStarted: { Type: schema.TypeBool, Description: "Whether to start the container", 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 container", 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 container is started", Optional: true, Default: dvStartupUpDelay, }, mkStartupDownDelay: { Type: schema.TypeInt, Description: "A non-negative number defining the delay in seconds before the next container is shut down", Optional: true, Default: dvStartupDownDelay, }, }, }, MaxItems: 1, MinItems: 0, }, mkStartOnBoot: { Type: schema.TypeBool, Description: "Automatically start container when the host system boots.", Optional: true, ForceNew: false, Default: dvStartOnBoot, }, mkTags: { Type: schema.TypeList, Description: "Tags of the container. 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, }, mkTimeoutCreate: { Type: schema.TypeInt, Description: "Create container timeout", Optional: true, Default: dvTimeoutCreate, }, mkTimeoutClone: { Type: schema.TypeInt, Description: "Clone container timeout", Optional: true, Default: dvTimeoutClone, }, mkTimeoutUpdate: { Type: schema.TypeInt, Description: "Update container timeout", Optional: true, Default: dvTimeoutUpdate, }, mkTimeoutDelete: { Type: schema.TypeInt, Description: "Delete container timeout", Optional: true, Default: dvTimeoutDelete, }, "timeout_start": { Type: schema.TypeInt, Description: "Start container timeout", Optional: true, Default: 300, Deprecated: "This field is deprecated and will be removed in a future release. " + "An overall operation timeout (`timeout_create` / `timeout_clone`) is used instead.", }, mkUnprivileged: { Type: schema.TypeBool, Description: "Whether the container runs as unprivileged on the host", Optional: true, ForceNew: true, Default: dvUnprivileged, }, mkVMID: { Type: schema.TypeInt, Description: "The VM identifier", Optional: true, Computed: true, ValidateDiagFunc: resource.VMIDValidator(), }, }, CreateContext: containerCreate, ReadContext: containerRead, UpdateContext: containerUpdate, DeleteContext: containerDelete, CustomizeDiff: customdiff.All( 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() }, ), ), 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 containerCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { clone := d.Get(mkClone).([]interface{}) if len(clone) > 0 { return containerCreateClone(ctx, d, m) } return containerCreateCustom(ctx, d, m) } func containerCreateClone(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, err := config.GetClient() if err != nil { return diag.FromErr(err) } clone := d.Get(mkClone).([]interface{}) cloneBlock := clone[0].(map[string]interface{}) cloneDatastoreID := cloneBlock[mkCloneDatastoreID].(string) cloneNodeName := cloneBlock[mkCloneNodeName].(string) cloneVMID := cloneBlock[mkCloneVMID].(int) description := d.Get(mkDescription).(string) initialization := d.Get(mkInitialization).([]interface{}) initializationHostname := "" if len(initialization) > 0 && initialization[0] != nil { initializationBlock := initialization[0].(map[string]interface{}) initializationHostname = initializationBlock[mkInitializationHostname].(string) } nodeName := d.Get(mkNodeName).(string) poolID := d.Get(mkPoolID).(string) tags := d.Get(mkTags).([]interface{}) vmIDUntyped, hasVMID := d.GetOk(mkVMID) vmID := vmIDUntyped.(int) if !hasVMID { vmIDNew, err := config.GetIDGenerator().NextID(ctx) if err != nil { return diag.FromErr(err) } vmID = vmIDNew err = d.Set(mkVMID, vmID) if err != nil { return diag.FromErr(err) } } fullCopy := types.CustomBool(true) cloneBody := &containers.CloneRequestBody{ FullCopy: &fullCopy, VMIDNew: vmID, } if cloneDatastoreID != "" { cloneBody.TargetStorage = &cloneDatastoreID } if description != "" { cloneBody.Description = &description } if initializationHostname != "" { cloneBody.Hostname = &initializationHostname } if poolID != "" { cloneBody.PoolID = &poolID } if cloneNodeName != "" && cloneNodeName != nodeName { cloneBody.TargetNodeName = &nodeName err = client.Node(cloneNodeName).Container(cloneVMID).CloneContainer(ctx, cloneBody) } else { err = client.Node(nodeName).Container(cloneVMID).CloneContainer(ctx, cloneBody) } if err != nil { return diag.FromErr(err) } d.SetId(strconv.Itoa(vmID)) containerAPI := client.Node(nodeName).Container(vmID) // Wait for the container to be created and its configuration lock to be released. err = containerAPI.WaitForContainerConfigUnlock(ctx, true) if err != nil { return diag.FromErr(err) } // Now that the virtual machine has been cloned, we need to perform some modifications. updateBody := &containers.UpdateRequestBody{} startOnBoot := types.CustomBool(d.Get(mkStartOnBoot).(bool)) updateBody.StartOnBoot = &startOnBoot protection := types.CustomBool(d.Get(mkProtection).(bool)) updateBody.Protection = &protection updateBody.StartupBehavior = containerGetStartupBehavior(d) console := d.Get(mkConsole).([]interface{}) if len(console) > 0 && console[0] != nil { consoleBlock := console[0].(map[string]interface{}) consoleEnabled := types.CustomBool( consoleBlock[mkConsoleEnabled].(bool), ) consoleMode := consoleBlock[mkConsoleMode].(string) consoleTTYCount := consoleBlock[mkConsoleTTYCount].(int) updateBody.ConsoleEnabled = &consoleEnabled updateBody.ConsoleMode = &consoleMode updateBody.TTY = &consoleTTYCount } cpu := d.Get(mkCPU).([]interface{}) if len(cpu) > 0 && cpu[0] != nil { cpuBlock := cpu[0].(map[string]interface{}) cpuArchitecture := cpuBlock[mkCPUArchitecture].(string) cpuCores := cpuBlock[mkCPUCores].(int) cpuUnits := cpuBlock[mkCPUUnits].(int) updateBody.CPUArchitecture = &cpuArchitecture updateBody.CPUCores = &cpuCores updateBody.CPUUnits = &cpuUnits } hookScript := d.Get(mkHookScriptFileID).(string) if hookScript != "" { updateBody.HookScript = &hookScript } var initializationIPConfigIPv4Address []string var initializationIPConfigIPv4Gateway []string var initializationIPConfigIPv6Address []string var initializationIPConfigIPv6Gateway []string if len(initialization) > 0 && initialization[0] != nil { initializationBlock := initialization[0].(map[string]interface{}) initializationDNS := initializationBlock[mkInitializationDNS].([]interface{}) if len(initializationDNS) > 0 && initializationDNS[0] != nil { initializationDNSBlock := initializationDNS[0].(map[string]interface{}) initializationDNSDomain := initializationDNSBlock[mkInitializationDNSDomain].(string) updateBody.DNSDomain = &initializationDNSDomain servers := initializationDNSBlock[mkInitializationDNSServers].([]interface{}) deprecatedServer := initializationDNSBlock[mkInitializationDNSServer].(string) if len(servers) > 0 { nameserver := strings.Join(utils.ConvertToStringSlice(servers), " ") updateBody.DNSServer = &nameserver } else { updateBody.DNSServer = &deprecatedServer } } initializationHostname := initializationBlock[mkInitializationHostname].(string) if initializationHostname != dvInitializationHostname { updateBody.Hostname = &initializationHostname } initializationIPConfig := initializationBlock[mkInitializationIPConfig].([]interface{}) for _, 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{}) initializationIPConfigIPv4Address = append( initializationIPConfigIPv4Address, ipv4Block[mkInitializationIPConfigIPv4Address].(string), ) initializationIPConfigIPv4Gateway = append( initializationIPConfigIPv4Gateway, ipv4Block[mkInitializationIPConfigIPv4Gateway].(string), ) } else { initializationIPConfigIPv4Address = append(initializationIPConfigIPv4Address, "") initializationIPConfigIPv4Gateway = append(initializationIPConfigIPv4Gateway, "") } ipv6 := configBlock[mkInitializationIPConfigIPv6].([]interface{}) if len(ipv6) > 0 && ipv6[0] != nil { ipv6Block := ipv6[0].(map[string]interface{}) initializationIPConfigIPv6Address = append( initializationIPConfigIPv6Address, ipv6Block[mkInitializationIPConfigIPv6Address].(string), ) initializationIPConfigIPv6Gateway = append( initializationIPConfigIPv6Gateway, ipv6Block[mkInitializationIPConfigIPv6Gateway].(string), ) } else { initializationIPConfigIPv6Address = append(initializationIPConfigIPv6Address, "") initializationIPConfigIPv6Gateway = append(initializationIPConfigIPv6Gateway, "") } } 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 { initializationUserAccountKeys := make( containers.CustomSSHKeys, len(keys), ) for ki, kv := range keys { initializationUserAccountKeys[ki] = kv.(string) } updateBody.SSHKeys = &initializationUserAccountKeys } else { updateBody.Delete = append(updateBody.Delete, "ssh-public-keys") } initializationUserAccountPassword := initializationUserAccountBlock[mkInitializationUserAccountPassword].(string) if initializationUserAccountPassword != dvInitializationUserAccountPassword { updateBody.Password = &initializationUserAccountPassword } else { updateBody.Delete = append(updateBody.Delete, "password") } } } memory := d.Get(mkMemory).([]interface{}) if len(memory) > 0 && memory[0] != nil { memoryBlock := memory[0].(map[string]interface{}) memoryDedicated := memoryBlock[mkMemoryDedicated].(int) memorySwap := memoryBlock[mkMemorySwap].(int) updateBody.DedicatedMemory = &memoryDedicated updateBody.Swap = &memorySwap } devicePassthrough := d.Get(mkDevicePassthrough).([]interface{}) passthroughDevices := make( containers.CustomPassthroughDevices, len(devicePassthrough), ) for di, dv := range devicePassthrough { devicePassthroughMap := dv.(map[string]interface{}) devicePassthroughObject := containers.CustomPassthroughDevice{} denyWrite := types.CustomBool( devicePassthroughMap[mkDevicePassthroughDenyWrite].(bool), ) gid := devicePassthroughMap[mkDevicePassthroughGID].(int) mode := devicePassthroughMap[mkDevicePassthroughMode].(string) path := devicePassthroughMap[mkDevicePassthroughPath].(string) uid := devicePassthroughMap[mkDevicePassthroughUID].(int) devicePassthroughObject.DenyWrite = &denyWrite devicePassthroughObject.GID = &gid devicePassthroughObject.Mode = &mode devicePassthroughObject.Path = path devicePassthroughObject.UID = &uid passthroughDevices[fmt.Sprintf("dev%d", di)] = &devicePassthroughObject } updateBody.PassthroughDevices = passthroughDevices networkInterface := d.Get(mkNetworkInterface).([]interface{}) if len(networkInterface) == 0 { networkInterface, err = containerGetExistingNetworkInterface(ctx, containerAPI) if err != nil { return diag.FromErr(err) } } networkInterfaces := make( containers.CustomNetworkInterfaces, len(networkInterface), ) for ni, nv := range networkInterface { networkInterfaceMap := nv.(map[string]interface{}) networkInterfaceObject := containers.CustomNetworkInterface{} bridge := networkInterfaceMap[mkNetworkInterfaceBridge].(string) enabled := networkInterfaceMap[mkNetworkInterfaceEnabled].(bool) firewall := types.CustomBool( networkInterfaceMap[mkNetworkInterfaceFirewall].(bool), ) macAddress := networkInterfaceMap[mkNetworkInterfaceMACAddress].(string) name := networkInterfaceMap[mkNetworkInterfaceName].(string) rateLimit := networkInterfaceMap[mkNetworkInterfaceRateLimit].(float64) vlanID := networkInterfaceMap[mkNetworkInterfaceVLANID].(int) mtu, _ := networkInterfaceMap[mkNetworkInterfaceMTU].(int) if bridge != "" { networkInterfaceObject.Bridge = &bridge } networkInterfaceObject.Enabled = enabled networkInterfaceObject.Firewall = &firewall if len(initializationIPConfigIPv4Address) > ni { if initializationIPConfigIPv4Address[ni] != "" { networkInterfaceObject.IPv4Address = &initializationIPConfigIPv4Address[ni] } if initializationIPConfigIPv4Gateway[ni] != "" { networkInterfaceObject.IPv4Gateway = &initializationIPConfigIPv4Gateway[ni] } if initializationIPConfigIPv6Address[ni] != "" { networkInterfaceObject.IPv6Address = &initializationIPConfigIPv6Address[ni] } if initializationIPConfigIPv6Gateway[ni] != "" { networkInterfaceObject.IPv6Gateway = &initializationIPConfigIPv6Gateway[ni] } } if macAddress != "" { networkInterfaceObject.MACAddress = &macAddress } networkInterfaceObject.Name = name if rateLimit != 0 { networkInterfaceObject.RateLimit = &rateLimit } if vlanID != 0 { networkInterfaceObject.Tag = &vlanID } if mtu != 0 { networkInterfaceObject.MTU = &mtu } networkInterfaces[fmt.Sprintf("net%d", ni)] = &networkInterfaceObject } updateBody.NetworkInterfaces = networkInterfaces for key, ni := range updateBody.NetworkInterfaces { if !ni.Enabled { updateBody.Delete = append(updateBody.Delete, key) } } for i := len(updateBody.NetworkInterfaces); i < maxNetworkInterfaces; i++ { updateBody.Delete = append(updateBody.Delete, fmt.Sprintf("net%d", i)) } operatingSystem := d.Get(mkOperatingSystem).([]interface{}) if len(operatingSystem) > 0 && operatingSystem[0] != nil { operatingSystemBlock := operatingSystem[0].(map[string]interface{}) operatingSystemTemplateFileID := operatingSystemBlock[mkOperatingSystemTemplateFileID].(string) operatingSystemType := operatingSystemBlock[mkOperatingSystemType].(string) updateBody.OSTemplateFileVolume = &operatingSystemTemplateFileID updateBody.OSType = &operatingSystemType } if len(tags) > 0 { tagString := containerGetTagsString(d) updateBody.Tags = &tagString } template := types.CustomBool(d.Get(mkTemplate).(bool)) if template { updateBody.Template = &template } err = containerAPI.UpdateContainer(ctx, updateBody) if err != nil { return diag.FromErr(err) } // Wait for the container's lock to be released. err = containerAPI.WaitForContainerConfigUnlock(ctx, true) if err != nil { return diag.FromErr(err) } return containerCreateStart(ctx, d, m) } func containerCreateCustom(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) } nodeName := d.Get(mkNodeName).(string) container := Container() consoleBlock, err := structure.GetSchemaBlock( container, d, []string{mkConsole}, 0, true, ) if err != nil { return diag.FromErr(err) } consoleEnabled := types.CustomBool( consoleBlock[mkConsoleEnabled].(bool), ) consoleMode := consoleBlock[mkConsoleMode].(string) consoleTTYCount := consoleBlock[mkConsoleTTYCount].(int) cpuBlock, err := structure.GetSchemaBlock( container, d, []string{mkCPU}, 0, true, ) if err != nil { return diag.FromErr(err) } cpuArchitecture := cpuBlock[mkCPUArchitecture].(string) cpuCores := cpuBlock[mkCPUCores].(int) cpuUnits := cpuBlock[mkCPUUnits].(int) description := d.Get(mkDescription).(string) diskBlock, err := structure.GetSchemaBlock( container, d, []string{mkDisk}, 0, true, ) if err != nil { return diag.FromErr(err) } diskDatastoreID := diskBlock[mkDiskDatastoreID].(string) features, err := containerGetFeatures(container, d) if err != nil { return diag.FromErr(err) } hookScript := d.Get(mkHookScriptFileID).(string) initialization := d.Get(mkInitialization).([]interface{}) initializationDNSDomain := dvInitializationDNSDomain initializationDNSServer := dvInitializationDNSServer initializationHostname := dvInitializationHostname var initializationIPConfigIPv4Address []string var initializationIPConfigIPv4Gateway []string var initializationIPConfigIPv6Address []string var initializationIPConfigIPv6Gateway []string initializationUserAccountKeys := containers.CustomSSHKeys{} initializationUserAccountPassword := dvInitializationUserAccountPassword if len(initialization) > 0 && initialization[0] != nil { initializationBlock := initialization[0].(map[string]interface{}) initializationDNS := initializationBlock[mkInitializationDNS].([]interface{}) if len(initializationDNS) > 0 && initializationDNS[0] != nil { initializationDNSBlock := initializationDNS[0].(map[string]interface{}) initializationDNSDomain = initializationDNSBlock[mkInitializationDNSDomain].(string) servers := initializationDNSBlock[mkInitializationDNSServers].([]interface{}) deprecatedServer := initializationDNSBlock[mkInitializationDNSServer].(string) if len(servers) > 0 { nameserver := strings.Join(utils.ConvertToStringSlice(servers), " ") initializationDNSServer = nameserver } else { initializationDNSServer = deprecatedServer } } initializationHostname = initializationBlock[mkInitializationHostname].(string) initializationIPConfig := initializationBlock[mkInitializationIPConfig].([]interface{}) for _, c := range initializationIPConfig { if c == nil { continue } configBlock := c.(map[string]interface{}) ipv4 := configBlock[mkInitializationIPConfigIPv4].([]interface{}) if len(ipv4) > 0 && ipv4[0] != nil { ipv4Block := ipv4[0].(map[string]interface{}) initializationIPConfigIPv4Address = append( initializationIPConfigIPv4Address, ipv4Block[mkInitializationIPConfigIPv4Address].(string), ) initializationIPConfigIPv4Gateway = append( initializationIPConfigIPv4Gateway, ipv4Block[mkInitializationIPConfigIPv4Gateway].(string), ) } else { initializationIPConfigIPv4Address = append(initializationIPConfigIPv4Address, "") initializationIPConfigIPv4Gateway = append(initializationIPConfigIPv4Gateway, "") } ipv6 := configBlock[mkInitializationIPConfigIPv6].([]interface{}) if len(ipv6) > 0 && ipv6[0] != nil { ipv6Block := ipv6[0].(map[string]interface{}) initializationIPConfigIPv6Address = append( initializationIPConfigIPv6Address, ipv6Block[mkInitializationIPConfigIPv6Address].(string), ) initializationIPConfigIPv6Gateway = append( initializationIPConfigIPv6Gateway, ipv6Block[mkInitializationIPConfigIPv6Gateway].(string), ) } else { initializationIPConfigIPv6Address = append(initializationIPConfigIPv6Address, "") initializationIPConfigIPv6Gateway = append(initializationIPConfigIPv6Gateway, "") } } initializationUserAccount := initializationBlock[mkInitializationUserAccount].([]interface{}) if len(initializationUserAccount) > 0 && initializationUserAccount[0] != nil { initializationUserAccountBlock := initializationUserAccount[0].(map[string]interface{}) keys := initializationUserAccountBlock[mkInitializationUserAccountKeys].([]interface{}) initializationUserAccountKeys = make( containers.CustomSSHKeys, len(keys), ) for ki, kv := range keys { initializationUserAccountKeys[ki] = kv.(string) } initializationUserAccountPassword = initializationUserAccountBlock[mkInitializationUserAccountPassword].(string) } } memoryBlock, err := structure.GetSchemaBlock( container, d, []string{mkMemory}, 0, true, ) if err != nil { return diag.FromErr(err) } memoryDedicated := memoryBlock[mkMemoryDedicated].(int) memorySwap := memoryBlock[mkMemorySwap].(int) mountPoint := d.Get(mkMountPoint).([]interface{}) mountPoints := make(containers.CustomMountPoints, len(mountPoint)) // because of default bool values: for mi, mp := range mountPoint { mountPointMap := mp.(map[string]interface{}) mountPointObject := containers.CustomMountPoint{} acl := types.CustomBool(mountPointMap[mkMountPointACL].(bool)) backup := types.CustomBool(mountPointMap[mkMountPointBackup].(bool)) mountOptions := mountPointMap[mkMountPointMountOptions].([]interface{}) path := mountPointMap[mkMountPointPath].(string) quota := types.CustomBool(mountPointMap[mkMountPointQuota].(bool)) readOnly := types.CustomBool(mountPointMap[mkMountPointReadOnly].(bool)) replicate := types.CustomBool(mountPointMap[mkMountPointReplicate].(bool)) shared := types.CustomBool(mountPointMap[mkMountPointShared].(bool)) size := mountPointMap[mkMountPointSize].(string) volume := mountPointMap[mkMountPointVolume].(string) // we have to set only the values that are different from the provider's defaults, if acl { mountPointObject.ACL = &acl } if backup { mountPointObject.Backup = &backup } if path != dvMountPointPath { mountPointObject.MountPoint = path } if quota { mountPointObject.Quota = "a } if readOnly { mountPointObject.ReadOnly = &readOnly } if !replicate { mountPointObject.Replicate = &replicate } if shared { mountPointObject.Shared = &shared } if len(size) > 0 { var ds types.DiskSize ds, err = types.ParseDiskSize(size) if err != nil { return diag.Errorf("invalid disk size: %s", err.Error()) } mountPointObject.Volume = fmt.Sprintf("%s:%d", volume, ds.InGigabytes()) } else { mountPointObject.Volume = volume } if len(mountOptions) > 0 { mountOptionsArray := make([]string, 0, len(mountPoint)) for _, option := range mountOptions { mountOptionsArray = append(mountOptionsArray, option.(string)) } mountPointObject.MountOptions = &mountOptionsArray } mountPoints[fmt.Sprintf("mp%d", mi)] = &mountPointObject } var rootFS *containers.CustomRootFS diskSize := diskBlock[mkDiskSize].(int) if diskDatastoreID != "" && (diskSize != dvDiskSize || len(mountPoints) > 0) { // This is a special case where the rootfs size is set to a non-default value at creation time. // see https://pve.proxmox.com/pve-docs/chapter-pct.html#_storage_backed_mount_points rootFS = &containers.CustomRootFS{ Volume: fmt.Sprintf("%s:%d", diskDatastoreID, diskSize), } } networkInterface := d.Get(mkNetworkInterface).([]interface{}) networkInterfaces := make(containers.CustomNetworkInterfaces, len(networkInterface)) for ni, nv := range networkInterface { networkInterfaceMap := nv.(map[string]interface{}) networkInterfaceObject := containers.CustomNetworkInterface{} bridge := networkInterfaceMap[mkNetworkInterfaceBridge].(string) enabled := networkInterfaceMap[mkNetworkInterfaceEnabled].(bool) macAddress := networkInterfaceMap[mkNetworkInterfaceMACAddress].(string) name := networkInterfaceMap[mkNetworkInterfaceName].(string) rateLimit := networkInterfaceMap[mkNetworkInterfaceRateLimit].(float64) vlanID := networkInterfaceMap[mkNetworkInterfaceVLANID].(int) mtu := networkInterfaceMap[mkNetworkInterfaceMTU].(int) firewall := networkInterfaceMap[mkNetworkInterfaceFirewall].(bool) if bridge != "" { networkInterfaceObject.Bridge = &bridge } networkInterfaceObject.Enabled = enabled networkInterfaceObject.Name = name if len(initializationIPConfigIPv4Address) > ni { if initializationIPConfigIPv4Address[ni] != "" { networkInterfaceObject.IPv4Address = &initializationIPConfigIPv4Address[ni] } if initializationIPConfigIPv4Gateway[ni] != "" { networkInterfaceObject.IPv4Gateway = &initializationIPConfigIPv4Gateway[ni] } if initializationIPConfigIPv6Address[ni] != "" { networkInterfaceObject.IPv6Address = &initializationIPConfigIPv6Address[ni] } if initializationIPConfigIPv6Gateway[ni] != "" { networkInterfaceObject.IPv6Gateway = &initializationIPConfigIPv6Gateway[ni] } } if firewall { networkInterfaceObject.Firewall = types.CustomBool(firewall).Pointer() } if macAddress != "" { networkInterfaceObject.MACAddress = &macAddress } if rateLimit != 0 { networkInterfaceObject.RateLimit = &rateLimit } if vlanID != 0 { networkInterfaceObject.Tag = &vlanID } if mtu != 0 { networkInterfaceObject.MTU = &mtu } networkInterfaces[fmt.Sprintf("net%d", ni)] = &networkInterfaceObject } devicePassthrough := d.Get(mkDevicePassthrough).([]interface{}) passthroughDevices := make( containers.CustomPassthroughDevices, len(devicePassthrough), ) for di, dv := range devicePassthrough { devicePassthroughMap := dv.(map[string]interface{}) devicePassthroughObject := containers.CustomPassthroughDevice{} denyWrite := types.CustomBool( devicePassthroughMap[mkDevicePassthroughDenyWrite].(bool), ) gid := devicePassthroughMap[mkDevicePassthroughGID].(int) mode := devicePassthroughMap[mkDevicePassthroughMode].(string) path := devicePassthroughMap[mkDevicePassthroughPath].(string) uid := devicePassthroughMap[mkDevicePassthroughUID].(int) devicePassthroughObject.DenyWrite = &denyWrite devicePassthroughObject.GID = &gid devicePassthroughObject.Mode = &mode devicePassthroughObject.Path = path devicePassthroughObject.UID = &uid passthroughDevices[fmt.Sprintf("dev%d", di)] = &devicePassthroughObject } operatingSystem := d.Get(mkOperatingSystem).([]interface{}) if len(operatingSystem) == 0 || operatingSystem[0] == nil { return diag.Errorf( "\"%s\": required field is not set", mkOperatingSystem, ) } operatingSystemBlock := operatingSystem[0].(map[string]interface{}) operatingSystemTemplateFileID := operatingSystemBlock[mkOperatingSystemTemplateFileID].(string) operatingSystemType := operatingSystemBlock[mkOperatingSystemType].(string) poolID := d.Get(mkPoolID).(string) protection := types.CustomBool(d.Get(mkProtection).(bool)) started := types.CustomBool(d.Get(mkStarted).(bool)) startOnBoot := types.CustomBool(d.Get(mkStartOnBoot).(bool)) startupBehavior := containerGetStartupBehavior(d) tags := d.Get(mkTags).([]interface{}) template := types.CustomBool(d.Get(mkTemplate).(bool)) unprivileged := types.CustomBool(d.Get(mkUnprivileged).(bool)) vmIDUntyped, hasVMID := d.GetOk(mkVMID) vmID := vmIDUntyped.(int) if !hasVMID { vmIDNew, err := config.GetIDGenerator().NextID(ctx) if err != nil { return diag.FromErr(err) } vmID = vmIDNew err = d.Set(mkVMID, vmID) if err != nil { return diag.FromErr(err) } } // Attempt to create the container using the retrieved values. createBody := containers.CreateRequestBody{ ConsoleEnabled: &consoleEnabled, ConsoleMode: &consoleMode, CPUArchitecture: &cpuArchitecture, CPUCores: &cpuCores, CPUUnits: &cpuUnits, DatastoreID: &diskDatastoreID, DedicatedMemory: &memoryDedicated, PassthroughDevices: passthroughDevices, Features: features, MountPoints: mountPoints, NetworkInterfaces: networkInterfaces, OSTemplateFileVolume: &operatingSystemTemplateFileID, OSType: &operatingSystemType, Protection: &protection, RootFS: rootFS, Start: &started, StartOnBoot: &startOnBoot, StartupBehavior: startupBehavior, Swap: &memorySwap, Template: &template, TTY: &consoleTTYCount, Unprivileged: &unprivileged, VMID: &vmID, } if description != "" { createBody.Description = &description } if hookScript != "" { createBody.HookScript = &hookScript } if initializationDNSDomain != "" { createBody.DNSDomain = &initializationDNSDomain } if initializationDNSServer != "" { createBody.DNSServer = &initializationDNSServer } if initializationHostname != "" { createBody.Hostname = &initializationHostname } if len(initializationUserAccountKeys) > 0 { createBody.SSHKeys = &initializationUserAccountKeys } if initializationUserAccountPassword != "" { createBody.Password = &initializationUserAccountPassword } if poolID != "" { createBody.PoolID = &poolID } if len(tags) > 0 { tagsString := containerGetTagsString(d) createBody.Tags = &tagsString } err = client.Node(nodeName).Container(0).CreateContainer(ctx, &createBody) if err != nil { return diag.FromErr(err) } d.SetId(strconv.Itoa(vmID)) // Wait for the container's lock to be released. err = client.Node(nodeName).Container(vmID).WaitForContainerConfigUnlock(ctx, true) if err != nil { return diag.FromErr(err) } return containerCreateStart(ctx, d, m) } func containerCreateStart(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { started := d.Get(mkStarted).(bool) template := d.Get(mkTemplate).(bool) if !started || template { return containerRead(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) } containerAPI := client.Node(nodeName).Container(vmID) // Start the container and wait for it to reach a running state before continuing. err = containerAPI.StartContainer(ctx) if err != nil { return diag.FromErr(err) } return containerRead(ctx, d, m) } func containerGetExistingNetworkInterface( ctx context.Context, containerAPI *containers.Client, ) ([]interface{}, error) { containerInfo, err := containerAPI.GetContainer(ctx) if err != nil { return []interface{}{}, fmt.Errorf("error getting container information: %w", err) } networkInterfaces := make([]interface{}, 0, len(containerInfo.NetworkInterfaces)) for _, nv := range containerInfo.NetworkInterfaces { networkInterface := map[string]interface{}{} networkInterface[mkNetworkInterfaceEnabled] = true networkInterface[mkNetworkInterfaceName] = nv.Name if nv.Bridge != nil { networkInterface[mkNetworkInterfaceBridge] = *nv.Bridge } else { networkInterface[mkNetworkInterfaceBridge] = "" } if nv.Firewall != nil && *nv.Firewall { networkInterface[mkNetworkInterfaceFirewall] = true } else { networkInterface[mkNetworkInterfaceFirewall] = false } if nv.MACAddress != nil { networkInterface[mkNetworkInterfaceMACAddress] = *nv.MACAddress } else { networkInterface[mkNetworkInterfaceMACAddress] = "" } if nv.RateLimit != nil { networkInterface[mkNetworkInterfaceRateLimit] = *nv.RateLimit } else { networkInterface[mkNetworkInterfaceRateLimit] = float64(0) } if nv.Tag != nil { networkInterface[mkNetworkInterfaceVLANID] = *nv.Tag } else { networkInterface[mkNetworkInterfaceVLANID] = 0 } if nv.MTU != nil { networkInterface[mkNetworkInterfaceMTU] = *nv.MTU } else { networkInterface[mkNetworkInterfaceMTU] = 0 } networkInterfaces = append(networkInterfaces, networkInterface) } return networkInterfaces, nil } func containerGetTagsString(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 containerGetStartupBehavior(d *schema.ResourceData) *containers.CustomStartupBehavior { 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 := containers.CustomStartupBehavior{} if startupUpDelay >= 0 { order.Up = &startupUpDelay } if startupDownDelay >= 0 { order.Down = &startupDownDelay } if startupOrder >= 0 { order.Order = &startupOrder } return &order } return nil } func containerGetFeatures(resource *schema.Resource, d *schema.ResourceData) (*containers.CustomFeatures, error) { featuresBlock, err := structure.GetSchemaBlock( resource, d, []string{mkFeatures}, 0, true, ) if err != nil { return nil, fmt.Errorf("error getting container features from schema: %w", err) } nesting := types.CustomBool(featuresBlock[mkFeaturesNesting].(bool)) keyctl := types.CustomBool(featuresBlock[mkFeaturesKeyControl].(bool)) fuse := types.CustomBool(featuresBlock[mkFeaturesFUSE].(bool)) mountTypes := featuresBlock[mkFeaturesMountTypes].([]interface{}) var mountTypesConverted []string if mountTypes != nil { mountTypesConverted = make([]string, len(mountTypes)) for i, mountType := range mountTypes { mountTypesConverted[i] = mountType.(string) } } else { mountTypesConverted = []string{} } features := containers.CustomFeatures{ MountTypes: &mountTypesConverted, } if nesting { features.Nesting = &nesting } if keyctl { features.KeyControl = &keyctl } if fuse { features.FUSE = &fuse } return &features, nil } func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { var diags diag.Diagnostics config := m.(proxmoxtf.ProviderConfiguration) client, e := config.GetClient() if e != nil { return diag.FromErr(e) } nodeName := d.Get(mkNodeName).(string) vmID, e := strconv.Atoi(d.Id()) if e != nil { return diag.FromErr(e) } containerAPI := client.Node(nodeName).Container(vmID) // Retrieve the entire configuration in order to compare it to the state. containerConfig, e := containerAPI.GetContainer(ctx) if e != nil { if errors.Is(e, api.ErrResourceDoesNotExist) { d.SetId("") return nil } return diag.FromErr(e) } // 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)...) } clone := d.Get(mkClone).([]interface{}) // Compare the primitive values to those stored in the state. currentDescription := d.Get(mkDescription).(string) if len(clone) == 0 || currentDescription != dvDescription { if containerConfig.Description != nil { e = d.Set(mkDescription, *containerConfig.Description) } else { e = d.Set(mkDescription, "") } diags = append(diags, diag.FromErr(e)...) } // Compare the console configuration to the one stored in the state. console := map[string]interface{}{} if containerConfig.ConsoleEnabled != nil { console[mkConsoleEnabled] = *containerConfig.ConsoleEnabled } else { // Default value of "console" is "1" according to the API documentation. console[mkConsoleEnabled] = true } if containerConfig.ConsoleMode != nil { console[mkConsoleMode] = *containerConfig.ConsoleMode } else { // Default value of "cmode" is "tty" according to the API documentation. console[mkConsoleMode] = "tty" } if containerConfig.TTY != nil { console[mkConsoleTTYCount] = *containerConfig.TTY } else { // Default value of "tty" is "2" according to the API documentation. console[mkConsoleTTYCount] = 2 } currentConsole := d.Get(mkConsole).([]interface{}) if len(clone) > 0 { if len(currentConsole) > 0 { err := d.Set(mkConsole, []interface{}{console}) diags = append(diags, diag.FromErr(err)...) } } else if len(currentConsole) > 0 || console[mkConsoleEnabled] != types.CustomBool(dvConsoleEnabled) || console[mkConsoleMode] != dvConsoleMode || console[mkConsoleTTYCount] != dvConsoleTTYCount { err := d.Set(mkConsole, []interface{}{console}) diags = append(diags, diag.FromErr(err)...) } // Compare the CPU configuration to the one stored in the state. cpu := map[string]interface{}{} if containerConfig.CPUArchitecture != nil { cpu[mkCPUArchitecture] = *containerConfig.CPUArchitecture } else { // Default value of "arch" is "amd64" according to the API documentation. cpu[mkCPUArchitecture] = "amd64" } if containerConfig.CPUCores != nil { cpu[mkCPUCores] = *containerConfig.CPUCores } else { // Default value of "cores" is "1" according to the API documentation. cpu[mkCPUCores] = 1 } if containerConfig.CPUUnits != nil { cpu[mkCPUUnits] = *containerConfig.CPUUnits } else { // Default value of "cpuunits" is "1024" according to the API documentation. cpu[mkCPUUnits] = 1024 } 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 || cpu[mkCPUUnits] != dvCPUUnits { err := d.Set(mkCPU, []interface{}{cpu}) diags = append(diags, diag.FromErr(err)...) } // Compare the disk configuration to the one stored in the state. disk := map[string]interface{}{} if containerConfig.RootFS != nil { volumeParts := strings.Split(containerConfig.RootFS.Volume, ":") disk[mkDiskDatastoreID] = volumeParts[0] disk[mkDiskSize] = containerConfig.RootFS.Size.InGigabytes() } else { // Default value of "storage" is "local" according to the API documentation. disk[mkDiskDatastoreID] = "local" disk[mkDiskSize] = dvDiskSize } currentDisk := d.Get(mkDisk).([]interface{}) if len(clone) > 0 { if len(currentDisk) > 0 && currentDisk[0] != nil { // do not override the rootfs size if it was not changed during the clone operation if currentDisk[0].(map[string]interface{})[mkDiskSize] == dvDiskSize { disk[mkDiskSize] = dvDiskSize } err := d.Set(mkDisk, []interface{}{disk}) diags = append(diags, diag.FromErr(err)...) } } else if len(currentDisk) > 0 || disk[mkDiskDatastoreID] != dvDiskDatastoreID || disk[mkDiskSize] != dvDiskSize { err := d.Set(mkDisk, []interface{}{disk}) diags = append(diags, diag.FromErr(err)...) } // Compare the memory configuration to the one stored in the state. memory := map[string]interface{}{} if containerConfig.DedicatedMemory != nil { memory[mkMemoryDedicated] = *containerConfig.DedicatedMemory } else { memory[mkMemoryDedicated] = 0 } if containerConfig.Swap != nil { memory[mkMemorySwap] = *containerConfig.Swap } else { memory[mkMemorySwap] = 0 } 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[mkMemorySwap] != dvMemorySwap { err := d.Set(mkMemory, []interface{}{memory}) diags = append(diags, diag.FromErr(err)...) } // Compare the initialization and network interface configuration to the one stored in the state. initialization := map[string]interface{}{} if containerConfig.DNSDomain != nil || containerConfig.DNSServer != nil { initializationDNS := map[string]interface{}{} if containerConfig.DNSDomain != nil { initializationDNS[mkInitializationDNSDomain] = *containerConfig.DNSDomain } 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 containerConfig.DNSServer != nil { if ok && currentInitializationDNSServer != "" { initializationDNS[mkInitializationDNSServer] = *containerConfig.DNSServer } else { dnsServer := strings.Split(*containerConfig.DNSServer, " ") initializationDNS[mkInitializationDNSServers] = dnsServer } } else { initializationDNS[mkInitializationDNSServer] = "" initializationDNS[mkInitializationDNSServers] = []string{} } initialization[mkInitializationDNS] = []interface{}{ initializationDNS, } } if containerConfig.Hostname != nil { initialization[mkInitializationHostname] = *containerConfig.Hostname } else { initialization[mkInitializationHostname] = "" } passthroughDevicesMap := make(map[string]interface{}, len(containerConfig.PassthroughDevices)) for key, dp := range containerConfig.PassthroughDevices { passthroughDevice := map[string]interface{}{} if dp.DenyWrite != nil { passthroughDevice[mkDevicePassthroughDenyWrite] = *dp.DenyWrite } else { passthroughDevice[mkDevicePassthroughDenyWrite] = false } if dp.GID != nil { passthroughDevice[mkDevicePassthroughGID] = *dp.GID } else { passthroughDevice[mkDevicePassthroughGID] = 0 } if dp.Mode != nil { passthroughDevice[mkDevicePassthroughMode] = *dp.Mode } else { passthroughDevice[mkDevicePassthroughMode] = dvDevicePassthroughMode } passthroughDevice[mkDevicePassthroughPath] = dp.Path if dp.UID != nil { passthroughDevice[mkDevicePassthroughUID] = *dp.UID } else { passthroughDevice[mkDevicePassthroughUID] = 0 } passthroughDevicesMap[key] = passthroughDevice } passthroughDevices := utils.OrderedListFromMap(passthroughDevicesMap) currentPassthroughDevices := d.Get(mkDevicePassthrough).([]interface{}) if len(clone) > 0 { if len(currentPassthroughDevices) > 0 { err := d.Set(mkDevicePassthrough, passthroughDevices) diags = append(diags, diag.FromErr(err)...) } } else if len(passthroughDevicesMap) > 0 { err := d.Set(mkDevicePassthrough, passthroughDevices) diags = append(diags, diag.FromErr(err)...) } mountPointsMap := make(map[string]interface{}, len(containerConfig.MountPoints)) for key, mp := range containerConfig.MountPoints { mountPoint := map[string]interface{}{} if mp.ACL != nil { mountPoint[mkMountPointACL] = *mp.ACL } else { mountPoint[mkMountPointACL] = false } if mp.Backup != nil { mountPoint[mkMountPointBackup] = *mp.Backup } else { mountPoint[mkMountPointBackup] = dvMountPointBackup } if mp.MountOptions != nil { mountPoint[mkMountPointMountOptions] = *mp.MountOptions } else { mountPoint[mkMountPointMountOptions] = []string{} } mountPoint[mkMountPointPath] = mp.MountPoint if mp.Quota != nil { mountPoint[mkMountPointQuota] = *mp.Quota } else { mountPoint[mkMountPointQuota] = false } if mp.ReadOnly != nil { mountPoint[mkMountPointReadOnly] = *mp.ReadOnly } else { mountPoint[mkMountPointReadOnly] = false } if mp.Replicate != nil { mountPoint[mkMountPointReplicate] = *mp.Replicate } else { mountPoint[mkMountPointReplicate] = true } if mp.Shared != nil { mountPoint[mkMountPointShared] = *mp.Shared } else { mountPoint[mkMountPointShared] = false } if mp.DiskSize != nil { mountPoint[mkMountPointSize] = *mp.DiskSize } else { mountPoint[mkMountPointSize] = "" } mountPoint[mkMountPointVolume] = mp.Volume mountPointsMap[key] = mountPoint } mountPoints := utils.OrderedListFromMap(mountPointsMap) currentMountPoints := d.Get(mkMountPoint).([]interface{}) if len(clone) > 0 { if len(currentMountPoints) > 0 { err := d.Set(mkMountPoint, mountPoints) diags = append(diags, diag.FromErr(err)...) } } else if len(mountPointsMap) > 0 { err := d.Set(mkMountPoint, mountPoints) diags = append(diags, diag.FromErr(err)...) } var ipConfigList []interface{} networkInterfacesMap := make(map[string]interface{}, len(containerConfig.NetworkInterfaces)) for key, nv := range containerConfig.NetworkInterfaces { if nv.IPv4Address != nil || nv.IPv4Gateway != nil || nv.IPv6Address != nil || nv.IPv6Gateway != nil { ipConfig := map[string]interface{}{} if nv.IPv4Address != nil || nv.IPv4Gateway != nil { ip := map[string]interface{}{} if nv.IPv4Address != nil { ip[mkInitializationIPConfigIPv4Address] = *nv.IPv4Address } else { ip[mkInitializationIPConfigIPv4Address] = "" } if nv.IPv4Gateway != nil { ip[mkInitializationIPConfigIPv4Gateway] = *nv.IPv4Gateway } else { ip[mkInitializationIPConfigIPv4Gateway] = "" } ipConfig[mkInitializationIPConfigIPv4] = []interface{}{ ip, } } else { ipConfig[mkInitializationIPConfigIPv4] = []interface{}{} } if nv.IPv6Address != nil || nv.IPv6Gateway != nil { ip := map[string]interface{}{} if nv.IPv6Address != nil { ip[mkInitializationIPConfigIPv6Address] = *nv.IPv6Address } else { ip[mkInitializationIPConfigIPv6Address] = "" } if nv.IPv6Gateway != nil { ip[mkInitializationIPConfigIPv6Gateway] = *nv.IPv6Gateway } else { ip[mkInitializationIPConfigIPv6Gateway] = "" } ipConfig[mkInitializationIPConfigIPv6] = []interface{}{ ip, } } else { ipConfig[mkInitializationIPConfigIPv6] = []interface{}{} } ipConfigList = append(ipConfigList, ipConfig) } networkInterface := map[string]interface{}{} networkInterface[mkNetworkInterfaceEnabled] = true networkInterface[mkNetworkInterfaceName] = nv.Name if nv.Bridge != nil { networkInterface[mkNetworkInterfaceBridge] = *nv.Bridge } else { networkInterface[mkNetworkInterfaceBridge] = "" } if nv.Firewall != nil && *nv.Firewall { networkInterface[mkNetworkInterfaceFirewall] = true } else { networkInterface[mkNetworkInterfaceFirewall] = false } if nv.MACAddress != nil { networkInterface[mkNetworkInterfaceMACAddress] = *nv.MACAddress } else { networkInterface[mkNetworkInterfaceMACAddress] = "" } if nv.RateLimit != nil { networkInterface[mkNetworkInterfaceRateLimit] = *nv.RateLimit } else { networkInterface[mkNetworkInterfaceRateLimit] = 0 } if nv.Tag != nil { networkInterface[mkNetworkInterfaceVLANID] = *nv.Tag } else { networkInterface[mkNetworkInterfaceVLANID] = 0 } if nv.MTU != nil { networkInterface[mkNetworkInterfaceMTU] = *nv.MTU } else { networkInterface[mkNetworkInterfaceMTU] = 0 } networkInterfacesMap[key] = networkInterface } networkInterfaces := utils.OrderedListFromMap(networkInterfacesMap) initialization[mkInitializationIPConfig] = ipConfigList currentInitialization := d.Get(mkInitialization).([]interface{}) if len(currentInitialization) > 0 && currentInitialization[0] != nil { currentInitializationMap := currentInitialization[0].(map[string]interface{}) initialization[mkInitializationUserAccount] = currentInitializationMap[mkInitializationUserAccount].([]interface{}) } if len(clone) > 0 { if len(currentInitialization) > 0 && currentInitialization[0] != nil { currentInitializationBlock := currentInitialization[0].(map[string]interface{}) currentInitializationDNS := currentInitializationBlock[mkInitializationDNS].([]interface{}) if len(currentInitializationDNS) == 0 { initialization[mkInitializationDNS] = []interface{}{} } currentInitializationIPConfig := currentInitializationBlock[mkInitializationIPConfig].([]interface{}) if len(currentInitializationIPConfig) == 0 { initialization[mkInitializationIPConfig] = []interface{}{} } currentInitializationUserAccount := currentInitializationBlock[mkInitializationUserAccount].([]interface{}) if len(currentInitializationUserAccount) == 0 { initialization[mkInitializationUserAccount] = []interface{}{} } if len(initialization) > 0 { e = d.Set( mkInitialization, []interface{}{initialization}, ) } else { e = d.Set(mkInitialization, []interface{}{}) } diags = append(diags, diag.FromErr(e)...) } currentNetworkInterface := d.Get(mkNetworkInterface).([]interface{}) if len(currentNetworkInterface) > 0 { err := d.Set( mkNetworkInterface, networkInterfaces, ) diags = append(diags, diag.FromErr(err)...) } } else { if len(initialization) > 0 { e = d.Set(mkInitialization, []interface{}{initialization}) } else { e = d.Set(mkInitialization, []interface{}{}) } diags = append(diags, diag.FromErr(e)...) err := d.Set(mkNetworkInterface, networkInterfaces) diags = append(diags, diag.FromErr(err)...) } // Compare the startup behavior to the one stored in the state. var startup map[string]interface{} if containerConfig.StartupBehavior != nil { startup = map[string]interface{}{} if containerConfig.StartupBehavior.Order != nil { startup[mkStartupOrder] = *containerConfig.StartupBehavior.Order } else { startup[mkStartupOrder] = dvStartupOrder } if containerConfig.StartupBehavior.Up != nil { startup[mkStartupUpDelay] = *containerConfig.StartupBehavior.Up } else { startup[mkStartupUpDelay] = dvStartupUpDelay } if containerConfig.StartupBehavior.Down != nil { startup[mkStartupDownDelay] = *containerConfig.StartupBehavior.Down } else { startup[mkStartupDownDelay] = dvStartupDownDelay } } currentStartup := d.Get(mkStartup).([]interface{}) switch { case len(clone) > 0 && 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)...) case 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 operating system configuration to the one stored in the state. operatingSystem := map[string]interface{}{} if containerConfig.OSType != nil { operatingSystem[mkOperatingSystemType] = *containerConfig.OSType } else { // Default value of "ostype" is "" according to the API documentation. operatingSystem[mkOperatingSystemType] = "" } currentOperatingSystem := d.Get(mkOperatingSystem).([]interface{}) if len(currentOperatingSystem) > 0 && currentOperatingSystem[0] != nil { currentOperatingSystemMap := currentOperatingSystem[0].(map[string]interface{}) operatingSystem[mkOperatingSystemTemplateFileID] = currentOperatingSystemMap[mkOperatingSystemTemplateFileID] } if len(clone) > 0 { if len(currentOperatingSystem) > 0 { err := d.Set( mkOperatingSystem, []interface{}{operatingSystem}, ) diags = append(diags, diag.FromErr(err)...) } } else if len(currentOperatingSystem) > 0 || operatingSystem[mkOperatingSystemType] != dvOperatingSystemType { err := d.Set(mkOperatingSystem, []interface{}{operatingSystem}) diags = append(diags, diag.FromErr(err)...) } currentUnprivileged := types.CustomBool(d.Get(mkUnprivileged).(bool)) if len(clone) == 0 || currentUnprivileged { if containerConfig.Unprivileged != nil { e = d.Set( mkUnprivileged, bool(*containerConfig.Unprivileged), ) } else { e = d.Set(mkUnprivileged, false) } diags = append(diags, diag.FromErr(e)...) } currentProtection := types.CustomBool(d.Get(mkProtection).(bool)) if len(clone) == 0 || currentProtection { if containerConfig.Protection != nil { e = d.Set( mkProtection, bool(*containerConfig.Protection), ) } else { e = d.Set(mkProtection, false) } diags = append(diags, diag.FromErr(e)...) } currentTags := d.Get(mkTags).([]interface{}) if len(clone) == 0 || len(currentTags) > 0 { var tags []string if containerConfig.Tags != nil { for _, tag := range strings.Split(*containerConfig.Tags, ";") { t := strings.TrimSpace(tag) if len(t) > 0 { tags = append(tags, t) } } sort.Strings(tags) } e = d.Set(mkTags, tags) diags = append(diags, diag.FromErr(e)...) } currentTemplate := d.Get(mkTemplate).(bool) if len(clone) == 0 || currentTemplate { if containerConfig.Template != nil { e = d.Set( mkTemplate, bool(*containerConfig.Template), ) } else { e = d.Set(mkTemplate, false) } diags = append(diags, diag.FromErr(e)...) } // Determine the state of the container in order to update the "started" argument. status, e := containerAPI.GetContainerStatus(ctx) if e != nil { return diag.FromErr(e) } e = d.Set(mkStarted, status.Status == "running") diags = append(diags, diag.FromErr(e)...) return diags } func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { updateTimeoutSec := d.Get(mkTimeoutUpdate).(int) ctx, cancel := context.WithTimeout(ctx, time.Duration(updateTimeoutSec)*time.Second) defer cancel() config := m.(proxmoxtf.ProviderConfiguration) client, e := config.GetClient() if e != nil { return diag.FromErr(e) } nodeName := d.Get(mkNodeName).(string) vmID, e := strconv.Atoi(d.Id()) if e != nil { return diag.FromErr(e) } containerAPI := client.Node(nodeName).Container(vmID) // Prepare the new request object. updateBody := containers.UpdateRequestBody{ Delete: []string{}, } rebootRequired := false container := Container() // Retrieve the clone argument as the update logic varies for clones. clone := d.Get(mkClone).([]interface{}) // Prepare the new primitive values. description := d.Get(mkDescription).(string) updateBody.Description = &description template := types.CustomBool(d.Get(mkTemplate).(bool)) if d.HasChange(mkTemplate) { updateBody.Template = &template } // Prepare the new console configuration. if d.HasChange(mkConsole) { consoleBlock, err := structure.GetSchemaBlock( container, d, []string{mkConsole}, 0, true, ) if err != nil { return diag.FromErr(err) } consoleEnabled := types.CustomBool( consoleBlock[mkConsoleEnabled].(bool), ) consoleMode := consoleBlock[mkConsoleMode].(string) consoleTTYCount := consoleBlock[mkConsoleTTYCount].(int) updateBody.ConsoleEnabled = &consoleEnabled updateBody.ConsoleMode = &consoleMode updateBody.TTY = &consoleTTYCount rebootRequired = true } // Prepare the new CPU configuration. if d.HasChange(mkCPU) { cpuBlock, err := structure.GetSchemaBlock( container, d, []string{mkCPU}, 0, true, ) if err != nil { return diag.FromErr(err) } cpuArchitecture := cpuBlock[mkCPUArchitecture].(string) cpuCores := cpuBlock[mkCPUCores].(int) cpuUnits := cpuBlock[mkCPUUnits].(int) updateBody.CPUArchitecture = &cpuArchitecture updateBody.CPUCores = &cpuCores updateBody.CPUUnits = &cpuUnits } if d.HasChange(mkFeatures) { features, err := containerGetFeatures(container, d) if err != nil { return diag.FromErr(err) } updateBody.Features = features } if d.HasChange(mkHookScriptFileID) { hookScript := d.Get(mkHookScriptFileID).(string) if hookScript != "" { updateBody.HookScript = &hookScript } else { updateBody.Delete = append(updateBody.Delete, "hookscript") } } // Prepare the new initialization configuration. initialization := d.Get(mkInitialization).([]interface{}) initializationDNSDomain := dvInitializationDNSDomain initializationDNSServer := dvInitializationDNSServer initializationHostname := dvInitializationHostname var initializationIPConfigIPv4Address []string var initializationIPConfigIPv4Gateway []string var initializationIPConfigIPv6Address []string var initializationIPConfigIPv6Gateway []string if len(initialization) > 0 && initialization[0] != nil { initializationBlock := initialization[0].(map[string]interface{}) initializationDNS := initializationBlock[mkInitializationDNS].([]interface{}) if len(initializationDNS) > 0 { initializationDNSBlock := initializationDNS[0].(map[string]interface{}) initializationDNSDomain = initializationDNSBlock[mkInitializationDNSDomain].(string) servers := initializationDNSBlock[mkInitializationDNSServers].([]interface{}) deprecatedServer := initializationDNSBlock[mkInitializationDNSServer].(string) if len(servers) > 0 { initializationDNSServer = strings.Join(utils.ConvertToStringSlice(servers), " ") } else { initializationDNSServer = deprecatedServer } } initializationHostname = initializationBlock[mkInitializationHostname].(string) initializationIPConfig := initializationBlock[mkInitializationIPConfig].([]interface{}) for _, c := range initializationIPConfig { if c == nil { continue } configBlock := c.(map[string]interface{}) ipv4 := configBlock[mkInitializationIPConfigIPv4].([]interface{}) if len(ipv4) > 0 && ipv4[0] != nil { ipv4Block := ipv4[0].(map[string]interface{}) initializationIPConfigIPv4Address = append( initializationIPConfigIPv4Address, ipv4Block[mkInitializationIPConfigIPv4Address].(string), ) initializationIPConfigIPv4Gateway = append( initializationIPConfigIPv4Gateway, ipv4Block[mkInitializationIPConfigIPv4Gateway].(string), ) } else { initializationIPConfigIPv4Address = append(initializationIPConfigIPv4Address, "") initializationIPConfigIPv4Gateway = append(initializationIPConfigIPv4Gateway, "") } ipv6 := configBlock[mkInitializationIPConfigIPv6].([]interface{}) if len(ipv6) > 0 && ipv6[0] != nil { ipv6Block := ipv6[0].(map[string]interface{}) initializationIPConfigIPv6Address = append( initializationIPConfigIPv6Address, ipv6Block[mkInitializationIPConfigIPv6Address].(string), ) initializationIPConfigIPv6Gateway = append( initializationIPConfigIPv6Gateway, ipv6Block[mkInitializationIPConfigIPv6Gateway].(string), ) } else { initializationIPConfigIPv6Address = append(initializationIPConfigIPv6Address, "") initializationIPConfigIPv6Gateway = append(initializationIPConfigIPv6Gateway, "") } } } if d.HasChange(mkInitialization + "." + mkInitializationDNS) { updateBody.DNSDomain = &initializationDNSDomain updateBody.DNSServer = &initializationDNSServer rebootRequired = true } if d.HasChange(mkInitialization + "." + mkInitializationHostname) { updateBody.Hostname = &initializationHostname rebootRequired = true } // Prepare the new memory configuration. if d.HasChange(mkMemory) { memoryBlock, err := structure.GetSchemaBlock( container, d, []string{mkMemory}, 0, true, ) if err != nil { return diag.FromErr(err) } memoryDedicated := memoryBlock[mkMemoryDedicated].(int) memorySwap := memoryBlock[mkMemorySwap].(int) updateBody.DedicatedMemory = &memoryDedicated updateBody.Swap = &memorySwap rebootRequired = true } // Prepare the new device passthrough configuration. if d.HasChange(mkDevicePassthrough) { _, newDevicePassthrough := d.GetChange(mkDevicePassthrough) devicePassthrough := newDevicePassthrough.([]interface{}) passthroughDevices := make( containers.CustomPassthroughDevices, len(devicePassthrough), ) for i, dp := range devicePassthrough { devicePassthroughMap := dp.(map[string]interface{}) devicePassthroughObject := containers.CustomPassthroughDevice{} denyWrite := types.CustomBool(devicePassthroughMap[mkDevicePassthroughDenyWrite].(bool)) gid := devicePassthroughMap[mkDevicePassthroughGID].(int) mode := devicePassthroughMap[mkDevicePassthroughMode].(string) path := devicePassthroughMap[mkDevicePassthroughPath].(string) uid := devicePassthroughMap[mkDevicePassthroughUID].(int) devicePassthroughObject.DenyWrite = &denyWrite devicePassthroughObject.GID = &gid devicePassthroughObject.Mode = &mode devicePassthroughObject.Path = path devicePassthroughObject.UID = &uid passthroughDevices[fmt.Sprintf("dev%d", i)] = &devicePassthroughObject } updateBody.PassthroughDevices = passthroughDevices rebootRequired = true } // Prepare the new mount point configuration. if d.HasChange(mkMountPoint) { _, newMountPoints := d.GetChange(mkMountPoint) mountPoints := newMountPoints.([]interface{}) mountPointsMap := make( containers.CustomMountPoints, len(mountPoints), ) for i, mp := range mountPoints { mountPointMap := mp.(map[string]interface{}) mountPointObject := containers.CustomMountPoint{} acl := types.CustomBool(mountPointMap[mkMountPointACL].(bool)) backup := types.CustomBool(mountPointMap[mkMountPointBackup].(bool)) mountOptions := mountPointMap[mkMountPointMountOptions].([]interface{}) path := mountPointMap[mkMountPointPath].(string) quota := types.CustomBool(mountPointMap[mkMountPointQuota].(bool)) readOnly := types.CustomBool(mountPointMap[mkMountPointReadOnly].(bool)) replicate := types.CustomBool(mountPointMap[mkMountPointReplicate].(bool)) shared := types.CustomBool(mountPointMap[mkMountPointShared].(bool)) size := mountPointMap[mkMountPointSize].(string) volume := mountPointMap[mkMountPointVolume].(string) mountPointObject.ACL = &acl mountPointObject.Backup = &backup mountPointObject.MountPoint = path mountPointObject.Quota = "a mountPointObject.ReadOnly = &readOnly mountPointObject.Replicate = &replicate mountPointObject.Shared = &shared // this is a totally hackish way to determine if the mount point is new or not during the container update. // an attached storage-backed MP has volume in the format "storage:disk file", i.e. `local-lvm:vm-123-disk-1` // while a new storage-backed MP has just plain volume name, i.e. `local-lvm` // device or directory MPs won't have a colon in the volume name either, and we don't need to do the special // handling for them. createNewMP := !strings.Contains(volume, ":") if len(size) > 0 && createNewMP { var ds types.DiskSize ds, err := types.ParseDiskSize(size) if err != nil { return diag.Errorf("invalid disk size: %s", err.Error()) } mountPointObject.Volume = fmt.Sprintf("%s:%d", volume, ds.InGigabytes()) } else { mountPointObject.Volume = volume } if len(mountOptions) > 0 { mountOptionsArray := make([]string, 0, len(mountPoints)) for _, option := range mountOptions { mountOptionsArray = append(mountOptionsArray, option.(string)) } mountPointObject.MountOptions = &mountOptionsArray } mountPointsMap[fmt.Sprintf("mp%d", i)] = &mountPointObject } updateBody.MountPoints = mountPointsMap rebootRequired = true } // Prepare the new network interface configuration. networkInterface := d.Get(mkNetworkInterface).([]interface{}) if len(networkInterface) == 0 && len(clone) > 0 { networkInterface, e = containerGetExistingNetworkInterface(ctx, containerAPI) if e != nil { return diag.FromErr(e) } } if d.HasChange(mkInitialization) || d.HasChange(mkNetworkInterface) { networkInterfaces := make( containers.CustomNetworkInterfaces, len(networkInterface), ) for ni, nv := range networkInterface { networkInterfaceMap := nv.(map[string]interface{}) networkInterfaceObject := containers.CustomNetworkInterface{} bridge := networkInterfaceMap[mkNetworkInterfaceBridge].(string) enabled := networkInterfaceMap[mkNetworkInterfaceEnabled].(bool) firewall := types.CustomBool( networkInterfaceMap[mkNetworkInterfaceFirewall].(bool), ) macAddress := networkInterfaceMap[mkNetworkInterfaceMACAddress].(string) name := networkInterfaceMap[mkNetworkInterfaceName].(string) rateLimit := networkInterfaceMap[mkNetworkInterfaceRateLimit].(float64) vlanID := networkInterfaceMap[mkNetworkInterfaceVLANID].(int) mtu := networkInterfaceMap[mkNetworkInterfaceMTU].(int) if bridge != "" { networkInterfaceObject.Bridge = &bridge } networkInterfaceObject.Enabled = enabled networkInterfaceObject.Firewall = &firewall if len(initializationIPConfigIPv4Address) > ni { if initializationIPConfigIPv4Address[ni] != "" { networkInterfaceObject.IPv4Address = &initializationIPConfigIPv4Address[ni] } if initializationIPConfigIPv4Gateway[ni] != "" { networkInterfaceObject.IPv4Gateway = &initializationIPConfigIPv4Gateway[ni] } if initializationIPConfigIPv6Address[ni] != "" { networkInterfaceObject.IPv6Address = &initializationIPConfigIPv6Address[ni] } if initializationIPConfigIPv6Gateway[ni] != "" { networkInterfaceObject.IPv6Gateway = &initializationIPConfigIPv6Gateway[ni] } } if macAddress != "" { networkInterfaceObject.MACAddress = &macAddress } networkInterfaceObject.Name = name if rateLimit != 0 { networkInterfaceObject.RateLimit = &rateLimit } if vlanID != 0 { networkInterfaceObject.Tag = &vlanID } if mtu != 0 { networkInterfaceObject.MTU = &mtu } networkInterfaces[fmt.Sprintf("net%d", ni)] = &networkInterfaceObject } updateBody.NetworkInterfaces = networkInterfaces for key, ni := range updateBody.NetworkInterfaces { if !ni.Enabled { updateBody.Delete = append(updateBody.Delete, key) } } for i := len(updateBody.NetworkInterfaces); i < maxNetworkInterfaces; i++ { updateBody.Delete = append(updateBody.Delete, fmt.Sprintf("net%d", i)) } rebootRequired = true } if d.HasChange(mkStartup) { updateBody.StartupBehavior = containerGetStartupBehavior(d) if updateBody.StartupBehavior == nil { updateBody.Delete = append(updateBody.Delete, "startup") } } // Prepare the new operating system configuration. if d.HasChange(mkOperatingSystem) { operatingSystem, err := structure.GetSchemaBlock( container, d, []string{mkOperatingSystem}, 0, true, ) if err != nil { return diag.FromErr(err) } operatingSystemType := operatingSystem[mkOperatingSystemType].(string) updateBody.OSType = &operatingSystemType rebootRequired = true } if d.HasChange(mkProtection) { protection := types.CustomBool(d.Get(mkProtection).(bool)) updateBody.Protection = &protection } if d.HasChange(mkTags) { tagString := containerGetTagsString(d) updateBody.Tags = &tagString } // Update the configuration now that everything has been prepared. e = containerAPI.UpdateContainer(ctx, &updateBody) if e != nil { return diag.FromErr(e) } // Determine if the state of the container needs to be changed. started := d.Get(mkStarted).(bool) if d.HasChange(mkStarted) && !bool(template) { if started { e = containerAPI.StartContainer(ctx) if e != nil { return diag.FromErr(e) } } else { forceStop := types.CustomBool(true) // Using delete timeout here as we're in the similar situation // as in the delete function, where we need to wait for the container // to be stopped before we can proceed with the update. // see `containerDelete` function for more details about the logic here // Needs to be refactored to a common function shutdownTimeoutSec := max(1, d.Get(mkTimeoutDelete).(int)-5) e = containerAPI.ShutdownContainer(ctx, &containers.ShutdownRequestBody{ ForceStop: &forceStop, Timeout: &shutdownTimeoutSec, }) if e != nil { return diag.FromErr(e) } e = containerAPI.WaitForContainerStatus(ctx, "stopped") if e != nil { return diag.FromErr(e) } rebootRequired = false } } // As a final step in the update procedure, we might need to reboot the container. if !bool(template) && started && rebootRequired { rebootTimeout := 300 e = containerAPI.RebootContainer( ctx, &containers.RebootRequestBody{ Timeout: &rebootTimeout, }, ) if e != nil { return diag.FromErr(e) } } return containerRead(ctx, d, m) } func containerDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { deleteTimeoutSec := d.Get(mkTimeoutDelete).(int) ctx, cancel := context.WithTimeout(ctx, time.Duration(deleteTimeoutSec)*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) } containerAPI := client.Node(nodeName).Container(vmID) // Shut down the container before deleting it. status, err := containerAPI.GetContainerStatus(ctx) if err != nil { return diag.FromErr(err) } if status.Status != "stopped" { forceStop := types.CustomBool(true) err = containerAPI.ShutdownContainer( ctx, &containers.ShutdownRequestBody{ ForceStop: &forceStop, // the timeout here must be less that the context timeout set above, // otherwise the context will be cancelled before PVE forcefully stops the container Timeout: ptr.Ptr(max(1, deleteTimeoutSec-5)), }, ) if err != nil { return diag.FromErr(err) } err = containerAPI.WaitForContainerStatus(ctx, "stopped") if err != nil { return diag.FromErr(err) } } err = containerAPI.DeleteContainer(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 container. err = containerAPI.WaitForContainerStatus(ctx, "") if err == nil { return diag.Errorf("failed to delete container \"%d\"", vmID) } d.SetId("") return nil } 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 }