From 96d139fcb470df5810732e453b2144c8790262df Mon Sep 17 00:00:00 2001 From: Dan Petersen Date: Thu, 26 Dec 2019 23:01:43 +0100 Subject: [PATCH] Initial work on VM resource implementation --- CHANGELOG.md | 1 + README.md | 49 ++ example/resource_virtual_environment_vm.tf | 27 + proxmox/virtual_environment_vm.go | 28 + proxmox/virtual_environment_vm_types.go | 53 +- proxmoxtf/provider.go | 1 + proxmoxtf/resource_virtual_environment_vm.go | 686 +++++++++++++++--- .../resource_virtual_environment_vm_test.go | 37 + proxmoxtf/utils.go | 150 ++++ 9 files changed, 905 insertions(+), 127 deletions(-) create mode 100644 example/resource_virtual_environment_vm.tf diff --git a/CHANGELOG.md b/CHANGELOG.md index 02c51bf6..58d2388c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,3 +18,4 @@ FEATURES: * **New Resource:** `proxmox_virtual_environment_pool` * **New Resource:** `proxmox_virtual_environment_role` * **New Resource:** `proxmox_virtual_environment_user` +* **New Resource:** `proxmox_virtual_environment_vm` diff --git a/README.md b/README.md index 26bce375..42a5bc6d 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ A Terraform Provider which adds support for Proxmox solutions. - [Pool](#pool-proxmox_virtual_environment_pool-1) - [Role](#role-proxmox_virtual_environment_role-1) - [User](#user-proxmox_virtual_environment_user-1) + - [VM](#vm-proxmox_virtual_environment_vm) - [Developing the Provider](#developing-the-provider) - [Testing the Provider](#testing-the-provider) @@ -310,6 +311,54 @@ This resource doesn't expose any additional attributes. ###### Attributes This resource doesn't expose any additional attributes. +##### VM (proxmox_virtual_environment_vm) + +###### Arguments +* `cdrom` - (Optional) The CDROM configuration + * `enabled` - (Optional) Whether to enable the CDROM drive (defaults to `false`) + * `file_id` - (Optional) A file ID for an ISO file (defaults to `cdrom` as in the physical drive) +* `cloud_init` - (Optional) The cloud-init configuration (conflicts with `cdrom`) + * `dns` - (Optional) The DNS configuration + * `domain` - (Optional) The DNS search domain + * `server` - (Optional) The DNS server + * `ip_config` - (Optional) The IP configuration (one block per network device) + * `ipv4` - (Optional) The IPv4 configuration + * `address` - (Optional) The IPv4 address (use `dhcp` for autodiscovery) + * `gateway` - (Optional) The IPv4 gateway (must be omitted when `dhcp` is used as the address) + * `ipv6` - (Optional) The IPv4 configuration + * `address` - (Optional) The IPv6 address (use `dhcp` for autodiscovery) + * `gateway` - (Optional) The IPv6 gateway (must be omitted when `dhcp` is used as the address) + * `user_account` - (Optional) The user account configuration + * `keys` - (Optional) The SSH keys + * `username` - (Optional) The SSH username +* `cpu` - (Optional) The CPU configuration + * `cores` - (Optional) The number of CPU cores (defaults to `1`) + * `sockets` - (Optional) The number of CPU sockets (defaults to `1`) +* `disk` - (Optional) The disk configuration (multiple blocks supported) + * `datastore_id` - (Optional) The ID of the datastore to create the disk in (defaults to `local-lvm`) + * `enabled` - (Optional) Whether to enable the disk (defaults to `true`) + * `file_format` - (Optional) The file format (defaults to `qcow2`) + * `file_id` - (Optional) The file ID for a disk image + * `size` - (Optional) The disk size in gigabytes (defaults to `8`) +* `keyboard_layout` - (Optional) The keyboard layout (defaults to `en-us`) +* `memory` - (Optional) The memory configuration + * `dedicated` - (Optional) The dedicated memory in megabytes (defaults to `512`) + * `floating` - (Optional) The floating memory in megabytes (defaults to `0`) + * `shared` - (Optional) The shared memory in megabytes (defaults to `0`) +* `name` - (Optional) The virtual machine name +* `network_device` - (Optional) The network device configuration (multiple blocks supported) + * `bridge` - (Optional) The name of the network bridge (defaults to `vmbr0`) + * `enabled` - (Optional) Whether to enable the network device (defaults to `true`) + * `mac_address` - (Optional) The MAC address + * `model` - (Optional) The network device model (defaults to `virtio`) + * `vlan_id` - (Optional) The VLAN identifier +* `node_name` - (Required) The name of the node to assign the virtual machine to +* `os_type` - (Optional) The OS type (defaults to `other`) +* `vm_id` - (Optional) The virtual machine ID + +###### Attributes +This resource doesn't expose any additional attributes. + ## Developing the Provider If you wish to work on the provider, you'll first need [Go](http://www.golang.org) installed on your machine (version 1.13+ is *required*). You'll also need to correctly setup a [GOPATH](http://golang.org/doc/code.html#GOPATH), as well as adding `$GOPATH/bin` to your `$PATH`. diff --git a/example/resource_virtual_environment_vm.tf b/example/resource_virtual_environment_vm.tf new file mode 100644 index 00000000..f02ceaff --- /dev/null +++ b/example/resource_virtual_environment_vm.tf @@ -0,0 +1,27 @@ +resource "proxmox_virtual_environment_vm" "example" { + cloud_init { + dns { + server = "1.1.1.1" + } + + ip_config { + ipv4 { + address = "dhcp" + } + } + + user_account { + keys = ["${trimspace(tls_private_key.example.public_key_openssh)}"] + username = "ubuntu" + } + } + + network_device {} + + node_name = "${data.proxmox_virtual_environment_nodes.example.names[0]}" +} + +resource "tls_private_key" "example" { + algorithm = "RSA" + rsa_bits = 2048 +} diff --git a/proxmox/virtual_environment_vm.go b/proxmox/virtual_environment_vm.go index 8dc2ee82..e7323ade 100644 --- a/proxmox/virtual_environment_vm.go +++ b/proxmox/virtual_environment_vm.go @@ -36,6 +36,34 @@ func (c *VirtualEnvironmentClient) GetVM(nodeName string, vmID int) (*VirtualEnv return resBody.Data, nil } +// GetVMID retrieves the next available VM identifier. +func (c *VirtualEnvironmentClient) GetVMID() (*int, error) { + nodes, err := c.ListNodes() + + if err != nil { + return nil, err + } + + vmID := 100 + +VMID: + for vmID <= 2147483637 { + for _, n := range nodes { + err := c.DoRequest(hmGET, fmt.Sprintf("nodes/%s/qemu/%d/status/current", url.PathEscape(n.Name), vmID), nil, nil) + + if err == nil { + vmID += 5 + + continue VMID + } + } + + return &vmID, nil + } + + return nil, errors.New("Unable to retrieve the next available VM identifier") +} + // ListVMs retrieves a list of virtual machines. func (c *VirtualEnvironmentClient) ListVMs() ([]*VirtualEnvironmentVMListResponseData, error) { return nil, errors.New("Not implemented") diff --git a/proxmox/virtual_environment_vm_types.go b/proxmox/virtual_environment_vm_types.go index e9d21888..f22ea6ea 100644 --- a/proxmox/virtual_environment_vm_types.go +++ b/proxmox/virtual_environment_vm_types.go @@ -13,9 +13,9 @@ import ( // CustomAgent handles QEMU agent parameters. type CustomAgent struct { - Enabled CustomBool `json:"enabled,omitempty" url:"enabled,int"` - TrimClonedDisks CustomBool `json:"fstrim_cloned_disks" url:"fstrim_cloned_disks,int"` - Type string `json:"type" url:"type"` + Enabled *CustomBool `json:"enabled,omitempty" url:"enabled,int"` + TrimClonedDisks *CustomBool `json:"fstrim_cloned_disks" url:"fstrim_cloned_disks,int"` + Type *string `json:"type" url:"type"` } // CustomAudioDevice handles QEMU audio parameters. @@ -65,6 +65,7 @@ type CustomEFIDisk struct { type CustomNetworkDevice struct { Model string `json:"model" url:"model"` Bridge *string `json:"bridge,omitempty" url:"bridge,omitempty"` + Enabled bool `json:"-" url:"-"` Firewall *CustomBool `json:"firewall,omitempty" url:"firewall,omitempty,int"` LinkDown *CustomBool `json:"link_down,omitempty" url:"link_down,omitempty,int"` MACAddress *string `json:"macaddr,omitempty" url:"macaddr,omitempty"` @@ -141,6 +142,7 @@ type CustomStorageDevice struct { BackupEnabled *CustomBool `json:"backup,omitempty" url:"backup,omitempty,int"` Enabled bool `json:"-" url:"-"` FileVolume string `json:"file" url:"file"` + Media *string `json:"media,omitempty" url:"media,omitempty"` } // CustomStorageDevices handles QEMU SATA device parameters. @@ -244,6 +246,7 @@ type VirtualEnvironmentVMCreateRequestBody struct { VirtualCPUCount *int `json:"vcpus,omitempty" url:"vcpus,omitempty"` VirtualIODevices CustomVirtualIODevices `json:"virtio,omitempty" url:"virtio,omitempty"` VMGenerationID *string `json:"vmgenid,omitempty" url:"vmgenid,omitempty"` + VMID *int `json:"vmid,omitempty" url:"vmid,omitempty"` VMStateDatastoreID *string `json:"vmstatestorage,omitempty" url:"vmstatestorage,omitempty"` WatchdogDevice *CustomWatchdogDevice `json:"watchdog,omitempty" url:"watchdog,omitempty"` } @@ -338,19 +341,31 @@ type VirtualEnvironmentVMUpdateRequestBody VirtualEnvironmentVMCreateRequestBody // EncodeValues converts a CustomAgent struct to a URL vlaue. func (r CustomAgent) EncodeValues(key string, v *url.Values) error { - enabled := 0 + values := []string{} - if r.Enabled { - enabled = 1 + if r.Enabled != nil { + if *r.Enabled { + values = append(values, "enabled=1") + } else { + values = append(values, "enabled=0") + } } - trimClonedDisks := 0 - - if r.TrimClonedDisks { - trimClonedDisks = 1 + if r.TrimClonedDisks != nil { + if *r.TrimClonedDisks { + values = append(values, "fstrim_cloned_disks=1") + } else { + values = append(values, "fstrim_cloned_disks=0") + } } - v.Add(key, fmt.Sprintf("enabled=%d,fstrim_cloned_disks=%d,type=%s", enabled, trimClonedDisks, r.Type)) + if r.Type != nil { + values = append(values, fmt.Sprintf("type=%s", *r.Type)) + } + + if len(values) > 0 { + v.Add(key, strings.Join(values, ",")) + } return nil } @@ -421,13 +436,7 @@ func (r CustomCloudInitConfig) EncodeValues(key string, v *url.Values) error { } if r.SSHKeys != nil { - keys := []string{} - - for _, pk := range *r.SSHKeys { - keys = append(keys, pk) - } - - v.Add("sshkeys", strings.Join(keys, "\n")) + v.Add("sshkeys", strings.ReplaceAll(url.QueryEscape(strings.Join(*r.SSHKeys, "\n")), "+", "%20")) } if r.Type != nil { @@ -520,7 +529,9 @@ func (r CustomNetworkDevice) EncodeValues(key string, v *url.Values) error { // EncodeValues converts a CustomNetworkDevices array to multiple URL values. func (r CustomNetworkDevices) EncodeValues(key string, v *url.Values) error { for i, d := range r { - d.EncodeValues(fmt.Sprintf("%s%d", key, i), v) + if d.Enabled { + d.EncodeValues(fmt.Sprintf("%s%d", key, i), v) + } } return nil @@ -745,6 +756,10 @@ func (r CustomStorageDevice) EncodeValues(key string, v *url.Values) error { } } + if r.Media != nil { + values = append(values, fmt.Sprintf("media=%s", *r.Media)) + } + v.Add(key, strings.Join(values, ",")) return nil diff --git a/proxmoxtf/provider.go b/proxmoxtf/provider.go index 16868b6a..85b657d8 100644 --- a/proxmoxtf/provider.go +++ b/proxmoxtf/provider.go @@ -48,6 +48,7 @@ func Provider() *schema.Provider { "proxmox_virtual_environment_pool": resourceVirtualEnvironmentPool(), "proxmox_virtual_environment_role": resourceVirtualEnvironmentRole(), "proxmox_virtual_environment_user": resourceVirtualEnvironmentUser(), + "proxmox_virtual_environment_vm": resourceVirtualEnvironmentVM(), }, Schema: map[string]*schema.Schema{ mkProviderVirtualEnvironment: &schema.Schema{ diff --git a/proxmoxtf/resource_virtual_environment_vm.go b/proxmoxtf/resource_virtual_environment_vm.go index 68692261..346652b4 100644 --- a/proxmoxtf/resource_virtual_environment_vm.go +++ b/proxmoxtf/resource_virtual_environment_vm.go @@ -6,50 +6,251 @@ package proxmoxtf import ( "fmt" + "strconv" + "strings" + "github.com/danitso/terraform-provider-proxmox/proxmox" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/validation" ) const ( + dvResourceVirtualEnvironmentVMCDROMEnabled = false + dvResourceVirtualEnvironmentVMCDROMFileID = "" + dvResourceVirtualEnvironmentVMCloudInitDNSDomain = "" + dvResourceVirtualEnvironmentVMCloudInitDNSServer = "" dvResourceVirtualEnvironmentVMCPUCores = 1 dvResourceVirtualEnvironmentVMCPUSockets = 1 dvResourceVirtualEnvironmentVMDiskDatastoreID = "local-lvm" + dvResourceVirtualEnvironmentVMDiskEnabled = true + dvResourceVirtualEnvironmentVMDiskFileFormat = "qcow2" dvResourceVirtualEnvironmentVMDiskFileID = "" dvResourceVirtualEnvironmentVMDiskSize = 8 - dvResourceVirtualEnvironmentVMMemoryDedicated = 256 + dvResourceVirtualEnvironmentVMKeyboardLayout = "en-us" + dvResourceVirtualEnvironmentVMMemoryDedicated = 512 dvResourceVirtualEnvironmentVMMemoryFloating = 0 dvResourceVirtualEnvironmentVMMemoryShared = 0 dvResourceVirtualEnvironmentVMName = "" dvResourceVirtualEnvironmentVMNetworkDeviceBridge = "vmbr0" + dvResourceVirtualEnvironmentVMNetworkDeviceEnabled = true dvResourceVirtualEnvironmentVMNetworkDeviceMACAddress = "" dvResourceVirtualEnvironmentVMNetworkDeviceModel = "virtio" dvResourceVirtualEnvironmentVMNetworkDeviceVLANID = -1 + dvResourceVirtualEnvironmentVMOSType = "other" dvResourceVirtualEnvironmentVMVMID = -1 - mkResourceVirtualEnvironmentVMCPU = "cpu" - mkResourceVirtualEnvironmentVMCPUCores = "cores" - mkResourceVirtualEnvironmentVMCPUSockets = "sockets" - mkResourceVirtualEnvironmentVMDisk = "disk" - mkResourceVirtualEnvironmentVMDiskDatastoreID = "datastore_id" - mkResourceVirtualEnvironmentVMDiskFileID = "file_id" - mkResourceVirtualEnvironmentVMDiskSize = "size" - mkResourceVirtualEnvironmentVMMemory = "memory" - mkResourceVirtualEnvironmentVMMemoryDedicated = "dedicated" - mkResourceVirtualEnvironmentVMMemoryFloating = "floating" - mkResourceVirtualEnvironmentVMMemoryShared = "shared" - mkResourceVirtualEnvironmentVMName = "name" - mkResourceVirtualEnvironmentVMNetworkDevice = "network_device" - mkResourceVirtualEnvironmentVMNetworkDeviceBridge = "bridge" - mkResourceVirtualEnvironmentVMNetworkDeviceMACAddress = "mac_address" - mkResourceVirtualEnvironmentVMNetworkDeviceModel = "model" - mkResourceVirtualEnvironmentVMNetworkDeviceVLANID = "vlan_id" - mkResourceVirtualEnvironmentVMVMID = "vm_id" + mkResourceVirtualEnvironmentVMCDROM = "cdrom" + mkResourceVirtualEnvironmentVMCDROMEnabled = "enabled" + mkResourceVirtualEnvironmentVMCDROMFileID = "file_id" + mkResourceVirtualEnvironmentVMCloudInit = "cloud_init" + mkResourceVirtualEnvironmentVMCloudInitDNS = "dns" + mkResourceVirtualEnvironmentVMCloudInitDNSDomain = "domain" + mkResourceVirtualEnvironmentVMCloudInitDNSServer = "server" + mkResourceVirtualEnvironmentVMCloudInitIPConfig = "ip_config" + mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv4 = "ipv4" + mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv4Address = "address" + mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv4Gateway = "gateway" + mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv6 = "ipv6" + mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv6Address = "address" + mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv6Gateway = "gateway" + mkResourceVirtualEnvironmentVMCloudInitUserAccount = "user_account" + mkResourceVirtualEnvironmentVMCloudInitUserAccountKeys = "keys" + mkResourceVirtualEnvironmentVMCloudInitUserAccountUsername = "username" + mkResourceVirtualEnvironmentVMCPU = "cpu" + mkResourceVirtualEnvironmentVMCPUCores = "cores" + mkResourceVirtualEnvironmentVMCPUSockets = "sockets" + mkResourceVirtualEnvironmentVMDisk = "disk" + mkResourceVirtualEnvironmentVMDiskDatastoreID = "datastore_id" + mkResourceVirtualEnvironmentVMDiskEnabled = "enabled" + mkResourceVirtualEnvironmentVMDiskFileFormat = "file_format" + mkResourceVirtualEnvironmentVMDiskFileID = "file_id" + mkResourceVirtualEnvironmentVMDiskSize = "size" + mkResourceVirtualEnvironmentVMKeyboardLayout = "keyboard_layout" + mkResourceVirtualEnvironmentVMMemory = "memory" + mkResourceVirtualEnvironmentVMMemoryDedicated = "dedicated" + mkResourceVirtualEnvironmentVMMemoryFloating = "floating" + mkResourceVirtualEnvironmentVMMemoryShared = "shared" + mkResourceVirtualEnvironmentVMName = "name" + mkResourceVirtualEnvironmentVMNetworkDevice = "network_device" + mkResourceVirtualEnvironmentVMNetworkDeviceBridge = "bridge" + mkResourceVirtualEnvironmentVMNetworkDeviceEnabled = "enabled" + mkResourceVirtualEnvironmentVMNetworkDeviceMACAddress = "mac_address" + mkResourceVirtualEnvironmentVMNetworkDeviceModel = "model" + mkResourceVirtualEnvironmentVMNetworkDeviceVLANID = "vlan_id" + mkResourceVirtualEnvironmentVMNodeName = "node_name" + mkResourceVirtualEnvironmentVMOSType = "os_type" + mkResourceVirtualEnvironmentVMVMID = "vm_id" ) func resourceVirtualEnvironmentVM() *schema.Resource { return &schema.Resource{ Schema: map[string]*schema.Schema{ + mkResourceVirtualEnvironmentVMCDROM: &schema.Schema{ + Type: schema.TypeList, + Description: "The CDROM drive", + Optional: true, + DefaultFunc: func() (interface{}, error) { + defaultList := make([]interface{}, 1) + defaultMap := make(map[string]interface{}) + + defaultMap[mkResourceVirtualEnvironmentVMCDROMEnabled] = dvResourceVirtualEnvironmentVMCDROMEnabled + defaultMap[mkResourceVirtualEnvironmentVMCDROMFileID] = dvResourceVirtualEnvironmentVMCDROMFileID + + defaultList[0] = defaultMap + + return defaultList, nil + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + mkResourceVirtualEnvironmentVMCDROMEnabled: { + Type: schema.TypeBool, + Optional: true, + Description: "Whether to enable the CDROM drive", + Default: dvResourceVirtualEnvironmentVMCDROMEnabled, + }, + mkResourceVirtualEnvironmentVMCDROMFileID: { + Type: schema.TypeString, + Optional: true, + Description: "The file id", + Default: dvResourceVirtualEnvironmentVMCDROMFileID, + ValidateFunc: getFileIDValidator(), + }, + }, + }, + MaxItems: 1, + MinItems: 0, + }, + mkResourceVirtualEnvironmentVMCloudInit: &schema.Schema{ + Type: schema.TypeList, + Description: "The cloud-init configuration", + Optional: true, + DefaultFunc: func() (interface{}, error) { + return make([]interface{}, 0), nil + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + mkResourceVirtualEnvironmentVMCloudInitDNS: { + Type: schema.TypeList, + Description: "The DNS configuration", + Optional: true, + DefaultFunc: func() (interface{}, error) { + return make([]interface{}, 0), nil + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + mkResourceVirtualEnvironmentVMCloudInitDNSDomain: { + Type: schema.TypeString, + Optional: true, + Description: "The DNS search domain", + Default: dvResourceVirtualEnvironmentVMCloudInitDNSDomain, + }, + mkResourceVirtualEnvironmentVMCloudInitDNSServer: { + Type: schema.TypeString, + Optional: true, + Description: "The DNS server", + Default: dvResourceVirtualEnvironmentVMCloudInitDNSServer, + }, + }, + }, + MaxItems: 1, + MinItems: 0, + }, + mkResourceVirtualEnvironmentVMCloudInitIPConfig: { + Type: schema.TypeList, + Description: "The IP configuration", + Optional: true, + DefaultFunc: func() (interface{}, error) { + return make([]interface{}, 0), nil + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv4: { + Type: schema.TypeList, + Description: "The IPv4 configuration", + Optional: true, + DefaultFunc: func() (interface{}, error) { + return make([]interface{}, 0), nil + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv4Address: { + Type: schema.TypeString, + Optional: true, + Description: "The IPv4 address", + Default: "", + }, + mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv4Gateway: { + Type: schema.TypeString, + Optional: true, + Description: "The IPv4 gateway", + Default: "", + }, + }, + }, + MaxItems: 1, + MinItems: 0, + }, + mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv6: { + Type: schema.TypeList, + Description: "The IPv6 configuration", + Optional: true, + DefaultFunc: func() (interface{}, error) { + return make([]interface{}, 0), nil + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv6Address: { + Type: schema.TypeString, + Optional: true, + Description: "The IPv6 address", + Default: "", + }, + mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv6Gateway: { + Type: schema.TypeString, + Optional: true, + Description: "The IPv6 gateway", + Default: "", + }, + }, + }, + MaxItems: 1, + MinItems: 0, + }, + }, + }, + MaxItems: 8, + MinItems: 0, + }, + mkResourceVirtualEnvironmentVMCloudInitUserAccount: { + Type: schema.TypeList, + Description: "The user account configuration", + Required: true, + DefaultFunc: func() (interface{}, error) { + return make([]interface{}, 0), nil + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + mkResourceVirtualEnvironmentVMCloudInitUserAccountKeys: { + Type: schema.TypeList, + Required: true, + Description: "The SSH keys", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + mkResourceVirtualEnvironmentVMCloudInitUserAccountUsername: { + Type: schema.TypeString, + Required: true, + Description: "The SSH username", + }, + }, + }, + MaxItems: 1, + MinItems: 0, + }, + }, + }, + MaxItems: 1, + MinItems: 0, + }, mkResourceVirtualEnvironmentVMCPU: &schema.Schema{ Type: schema.TypeList, Description: "The CPU allocation", @@ -84,7 +285,7 @@ func resourceVirtualEnvironmentVM() *schema.Resource { }, }, MaxItems: 1, - MinItems: 1, + MinItems: 0, }, mkResourceVirtualEnvironmentVMDisk: &schema.Schema{ Type: schema.TypeList, @@ -95,6 +296,7 @@ func resourceVirtualEnvironmentVM() *schema.Resource { defaultMap := make(map[string]interface{}) defaultMap[mkResourceVirtualEnvironmentVMDiskDatastoreID] = dvResourceVirtualEnvironmentVMDiskDatastoreID + defaultMap[mkResourceVirtualEnvironmentVMDiskFileFormat] = dvResourceVirtualEnvironmentVMDiskFileFormat defaultMap[mkResourceVirtualEnvironmentVMDiskFileID] = dvResourceVirtualEnvironmentVMDiskFileID defaultMap[mkResourceVirtualEnvironmentVMDiskSize] = dvResourceVirtualEnvironmentVMDiskSize @@ -110,22 +312,44 @@ func resourceVirtualEnvironmentVM() *schema.Resource { Description: "The datastore id", Default: dvResourceVirtualEnvironmentVMDiskDatastoreID, }, - mkResourceVirtualEnvironmentVMDiskFileID: { - Type: schema.TypeString, + mkResourceVirtualEnvironmentVMDiskEnabled: { + Type: schema.TypeBool, Optional: true, - Description: "The file id for a disk image", - Default: dvResourceVirtualEnvironmentVMDiskFileID, + Description: "Whether to enable the disk", + Default: dvResourceVirtualEnvironmentVMDiskEnabled, + }, + mkResourceVirtualEnvironmentVMDiskFileFormat: { + Type: schema.TypeString, + Optional: true, + Description: "The file format", + Default: dvResourceVirtualEnvironmentVMDiskFileFormat, + ValidateFunc: getFileFormatValidator(), + }, + mkResourceVirtualEnvironmentVMDiskFileID: { + Type: schema.TypeString, + Optional: true, + Description: "The file id for a disk image", + Default: dvResourceVirtualEnvironmentVMDiskFileID, + ValidateFunc: getFileIDValidator(), }, mkResourceVirtualEnvironmentVMDiskSize: { - Type: schema.TypeInt, - Optional: true, - Description: "The disk size in gigabytes", - Default: dvResourceVirtualEnvironmentVMDiskSize, + Type: schema.TypeInt, + Optional: true, + Description: "The disk size in gigabytes", + Default: dvResourceVirtualEnvironmentVMDiskSize, + ValidateFunc: validation.IntBetween(1, 8192), }, }, }, MaxItems: 14, - MinItems: 1, + MinItems: 0, + }, + mkResourceVirtualEnvironmentVMKeyboardLayout: { + Type: schema.TypeString, + Optional: true, + Description: "The keyboard layout", + Default: dvResourceVirtualEnvironmentVMKeyboardLayout, + ValidateFunc: getKeyboardLayoutValidator(), }, mkResourceVirtualEnvironmentVMMemory: &schema.Schema{ Type: schema.TypeList, @@ -169,7 +393,7 @@ func resourceVirtualEnvironmentVM() *schema.Resource { }, }, MaxItems: 1, - MinItems: 1, + MinItems: 0, }, mkResourceVirtualEnvironmentVMName: { Type: schema.TypeString, @@ -182,17 +406,7 @@ func resourceVirtualEnvironmentVM() *schema.Resource { Description: "The network devices", Optional: true, DefaultFunc: func() (interface{}, error) { - defaultList := make([]interface{}, 1) - defaultMap := make(map[string]interface{}) - - defaultMap[mkResourceVirtualEnvironmentVMNetworkDeviceBridge] = dvResourceVirtualEnvironmentVMNetworkDeviceBridge - defaultMap[mkResourceVirtualEnvironmentVMNetworkDeviceMACAddress] = dvResourceVirtualEnvironmentVMNetworkDeviceMACAddress - defaultMap[mkResourceVirtualEnvironmentVMNetworkDeviceModel] = dvResourceVirtualEnvironmentVMNetworkDeviceModel - defaultMap[mkResourceVirtualEnvironmentVMNetworkDeviceVLANID] = dvResourceVirtualEnvironmentVMNetworkDeviceVLANID - - defaultList[0] = defaultMap - - return defaultList, nil + return make([]interface{}, 1), nil }, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -202,73 +416,57 @@ func resourceVirtualEnvironmentVM() *schema.Resource { Description: "The bridge", Default: dvResourceVirtualEnvironmentVMNetworkDeviceBridge, }, - mkResourceVirtualEnvironmentVMNetworkDeviceMACAddress: { - Type: schema.TypeString, + mkResourceVirtualEnvironmentVMNetworkDeviceEnabled: { + Type: schema.TypeBool, Optional: true, - Description: "The MAC address", - Default: dvResourceVirtualEnvironmentVMNetworkDeviceMACAddress, + Description: "Whether to enable the network device", + Default: dvResourceVirtualEnvironmentVMNetworkDeviceEnabled, + }, + mkResourceVirtualEnvironmentVMNetworkDeviceMACAddress: { + Type: schema.TypeString, + Optional: true, + Description: "The MAC address", + Default: dvResourceVirtualEnvironmentVMNetworkDeviceMACAddress, + ValidateFunc: getMACAddressValidator(), }, mkResourceVirtualEnvironmentVMNetworkDeviceModel: { - Type: schema.TypeString, - Optional: true, - Description: "The model", - Default: dvResourceVirtualEnvironmentVMNetworkDeviceModel, + Type: schema.TypeString, + Optional: true, + Description: "The model", + Default: dvResourceVirtualEnvironmentVMNetworkDeviceModel, + ValidateFunc: getNetworkDeviceModelValidator(), }, mkResourceVirtualEnvironmentVMNetworkDeviceVLANID: { - Type: schema.TypeInt, - Optional: true, - Description: "The VLAN identifier", - Default: dvResourceVirtualEnvironmentVMNetworkDeviceVLANID, - ValidateFunc: func(i interface{}, k string) (s []string, es []error) { - min := 1 - - v, ok := i.(int) - - if !ok { - es = append(es, fmt.Errorf("expected type of %s to be int", k)) - return - } - - if v != -1 { - if v < min { - es = append(es, fmt.Errorf("expected %s to be at least %d, got %d", k, min, v)) - return - } - } - - return - }, + Type: schema.TypeInt, + Optional: true, + Description: "The VLAN identifier", + Default: dvResourceVirtualEnvironmentVMNetworkDeviceVLANID, + ValidateFunc: getVLANIDValidator(), }, }, }, MaxItems: 8, MinItems: 0, }, + mkResourceVirtualEnvironmentVMNodeName: &schema.Schema{ + Type: schema.TypeString, + Description: "The node name", + Required: true, + }, + mkResourceVirtualEnvironmentVMOSType: { + Type: schema.TypeString, + Optional: true, + Description: "The OS type", + Default: dvResourceVirtualEnvironmentVMOSType, + ValidateFunc: getOSTypeValidator(), + }, mkResourceVirtualEnvironmentVMVMID: { - Type: schema.TypeInt, - Optional: true, - Description: "The identifier", - Default: dvResourceVirtualEnvironmentVMVMID, - ValidateFunc: func(i interface{}, k string) (s []string, es []error) { - min := 100 - max := 2000000 - - v, ok := i.(int) - - if !ok { - es = append(es, fmt.Errorf("expected type of %s to be int", k)) - return - } - - if v != -1 { - if v < min || v > max { - es = append(es, fmt.Errorf("expected %s to be in the range (%d - %d), got %d", k, min, max, v)) - return - } - } - - return - }, + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The VM identifier", + Default: dvResourceVirtualEnvironmentVMVMID, + ValidateFunc: getVMIDValidator(), }, }, Create: resourceVirtualEnvironmentVMCreate, @@ -279,16 +477,271 @@ func resourceVirtualEnvironmentVM() *schema.Resource { } func resourceVirtualEnvironmentVMCreate(d *schema.ResourceData, m interface{}) error { - /* - config := m.(providerConfiguration) - veClient, err := config.GetVEClient() + config := m.(providerConfiguration) + veClient, err := config.GetVEClient() + + if err != nil { + return err + } + + schema := resourceVirtualEnvironmentVM().Schema + cdrom := d.Get(mkResourceVirtualEnvironmentVMCDROM).([]interface{}) + + if len(cdrom) == 0 { + cdromDefault, err := schema[mkResourceVirtualEnvironmentVMCDROM].DefaultValue() if err != nil { return err } - d.SetId("") - */ + cdrom = cdromDefault.([]interface{}) + } + + cdromBlock := cdrom[0].(map[string]interface{}) + cdromEnabled := cdromBlock[mkResourceVirtualEnvironmentVMCDROMEnabled].(bool) + cdromFileID := cdromBlock[mkResourceVirtualEnvironmentVMCDROMFileID].(string) + + if cdromFileID == "" { + cdromFileID = "cdrom" + } + + var cloudInitConfig *proxmox.CustomCloudInitConfig + + cloudInit := d.Get(mkResourceVirtualEnvironmentVMCloudInit).([]interface{}) + + if len(cloudInit) > 0 { + cdromEnabled = true + cdromFileID = "local-lvm:cloudinit" + + cloudInitBlock := cloudInit[0].(map[string]interface{}) + cloudInitConfig = &proxmox.CustomCloudInitConfig{} + cloudInitDNS := cloudInitBlock[mkResourceVirtualEnvironmentVMCloudInitDNS].([]interface{}) + + if len(cloudInitDNS) > 0 { + cloudInitDNSBlock := cloudInitDNS[0].(map[string]interface{}) + domain := cloudInitDNSBlock[mkResourceVirtualEnvironmentVMCloudInitDNSDomain].(string) + + if domain != "" { + cloudInitConfig.SearchDomain = &domain + } + + server := cloudInitDNSBlock[mkResourceVirtualEnvironmentVMCloudInitDNSServer].(string) + + if server != "" { + cloudInitConfig.Nameserver = &server + } + } + + cloudInitIPConfig := cloudInitBlock[mkResourceVirtualEnvironmentVMCloudInitIPConfig].([]interface{}) + cloudInitConfig.IPConfig = make([]proxmox.CustomCloudInitIPConfig, len(cloudInitIPConfig)) + + for i, c := range cloudInitIPConfig { + configBlock := c.(map[string]interface{}) + ipv4 := configBlock[mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv4].([]interface{}) + + if len(ipv4) > 0 { + ipv4Block := ipv4[0].(map[string]interface{}) + ipv4Address := ipv4Block[mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv4Address].(string) + + if ipv4Address != "" { + cloudInitConfig.IPConfig[i].IPv4 = &ipv4Address + } + + ipv4Gateway := ipv4Block[mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv4Gateway].(string) + + if ipv4Gateway != "" { + cloudInitConfig.IPConfig[i].GatewayIPv4 = &ipv4Gateway + } + } + + ipv6 := configBlock[mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv6].([]interface{}) + + if len(ipv6) > 0 { + ipv6Block := ipv6[0].(map[string]interface{}) + ipv6Address := ipv6Block[mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv6Address].(string) + + if ipv6Address != "" { + cloudInitConfig.IPConfig[i].IPv6 = &ipv6Address + } + + ipv6Gateway := ipv6Block[mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv6Gateway].(string) + + if ipv6Gateway != "" { + cloudInitConfig.IPConfig[i].GatewayIPv6 = &ipv6Gateway + } + } + } + + cloudInitUserAccount := cloudInitBlock[mkResourceVirtualEnvironmentVMCloudInitUserAccount].([]interface{}) + + if len(cloudInitUserAccount) > 0 { + cloudInitUserAccountBlock := cloudInitUserAccount[0].(map[string]interface{}) + keys := cloudInitUserAccountBlock[mkResourceVirtualEnvironmentVMCloudInitUserAccountKeys].([]interface{}) + + if len(keys) > 0 { + sshKeys := make(proxmox.CustomCloudInitSSHKeys, len(keys)) + + for i, k := range keys { + sshKeys[i] = k.(string) + } + + cloudInitConfig.SSHKeys = &sshKeys + } + + username := cloudInitUserAccountBlock[mkResourceVirtualEnvironmentVMCloudInitUserAccountUsername].(string) + + cloudInitConfig.Username = &username + } + } + + cpu := d.Get(mkResourceVirtualEnvironmentVMCPU).([]interface{}) + + if len(cpu) == 0 { + cpuDefault, err := schema[mkResourceVirtualEnvironmentVMCPU].DefaultValue() + + if err != nil { + return err + } + + cpu = cpuDefault.([]interface{}) + } + + cpuBlock := cpu[0].(map[string]interface{}) + cpuCores := cpuBlock[mkResourceVirtualEnvironmentVMCPUCores].(int) + cpuSockets := cpuBlock[mkResourceVirtualEnvironmentVMCPUSockets].(int) + + keyboardLayout := d.Get(mkResourceVirtualEnvironmentVMKeyboardLayout).(string) + memory := d.Get(mkResourceVirtualEnvironmentVMMemory).([]interface{}) + + if len(memory) == 0 { + memoryDefault, err := schema[mkResourceVirtualEnvironmentVMMemory].DefaultValue() + + if err != nil { + return err + } + + memory = memoryDefault.([]interface{}) + } + + memoryBlock := memory[0].(map[string]interface{}) + memoryDedicated := memoryBlock[mkResourceVirtualEnvironmentVMMemoryDedicated].(int) + memoryFloating := memoryBlock[mkResourceVirtualEnvironmentVMMemoryFloating].(int) + memoryShared := memoryBlock[mkResourceVirtualEnvironmentVMMemoryShared].(int) + + name := d.Get(mkResourceVirtualEnvironmentVMName).(string) + + networkDevice := d.Get(mkResourceVirtualEnvironmentVMNetworkDevice).([]interface{}) + networkDeviceObjects := make(proxmox.CustomNetworkDevices, len(networkDevice)) + + for i, d := range networkDevice { + block := d.(map[string]interface{}) + + bridge, _ := block[mkResourceVirtualEnvironmentVMNetworkDeviceBridge].(string) + enabled, _ := block[mkResourceVirtualEnvironmentVMNetworkDeviceEnabled].(bool) + macAddress, _ := block[mkResourceVirtualEnvironmentVMNetworkDeviceMACAddress].(string) + model, _ := block[mkResourceVirtualEnvironmentVMNetworkDeviceModel].(string) + vlanID, _ := block[mkResourceVirtualEnvironmentVMNetworkDeviceVLANID].(int) + + device := proxmox.CustomNetworkDevice{ + Enabled: enabled, + Model: model, + } + + if bridge != "" { + device.Bridge = &bridge + } + + if macAddress != "" { + device.MACAddress = &macAddress + } + + if vlanID != -1 { + device.Trunks = []int{vlanID} + } + + networkDeviceObjects[i] = device + } + + nodeName := d.Get(mkResourceVirtualEnvironmentVMNodeName).(string) + osType := d.Get(mkResourceVirtualEnvironmentVMOSType).(string) + vmID := d.Get(mkResourceVirtualEnvironmentVMVMID).(int) + + if vmID == -1 { + vmIDNew, err := veClient.GetVMID() + + if err != nil { + return err + } + + vmID = *vmIDNew + } + + var memorySharedObject *proxmox.CustomSharedMemory + + agentEnabled := proxmox.CustomBool(true) + bootOrder := "c" + + if cdromEnabled { + bootOrder = "cd" + } + + ideDevice2Media := "cdrom" + ideDevices := proxmox.CustomStorageDevices{ + proxmox.CustomStorageDevice{ + Enabled: false, + }, + proxmox.CustomStorageDevice{ + Enabled: false, + }, + proxmox.CustomStorageDevice{ + Enabled: cdromEnabled, + FileVolume: cdromFileID, + Media: &ideDevice2Media, + }, + } + + if memoryShared > 0 { + memorySharedName := fmt.Sprintf("vm-%d-ivshmem", vmID) + memorySharedObject = &proxmox.CustomSharedMemory{ + Name: &memorySharedName, + Size: memoryShared, + } + } + + scsiHardware := "virtio-scsi-pci" + tabletDeviceEnabled := proxmox.CustomBool(true) + + body := &proxmox.VirtualEnvironmentVMCreateRequestBody{ + Agent: &proxmox.CustomAgent{Enabled: &agentEnabled}, + BootOrder: &bootOrder, + CloudInitConfig: cloudInitConfig, + CPUCores: &cpuCores, + CPUSockets: &cpuSockets, + DedicatedMemory: &memoryDedicated, + FloatingMemory: &memoryFloating, + IDEDevices: ideDevices, + KeyboardLayout: &keyboardLayout, + NetworkDevices: networkDeviceObjects, + OSType: &osType, + SCSIHardware: &scsiHardware, + SerialDevices: []string{"socket"}, + SharedMemory: memorySharedObject, + TabletDeviceEnabled: &tabletDeviceEnabled, + VGADevice: &proxmox.CustomVGADevice{Type: "serial0"}, + VMID: &vmID, + } + + if name != "" { + body.Name = &name + } + + err = veClient.CreateVM(nodeName, body) + + if err != nil { + return err + } + + d.SetId(strconv.Itoa(vmID)) return resourceVirtualEnvironmentVMRead(d, m) } @@ -320,16 +773,33 @@ func resourceVirtualEnvironmentVMUpdate(d *schema.ResourceData, m interface{}) e } func resourceVirtualEnvironmentVMDelete(d *schema.ResourceData, m interface{}) error { - /* - config := m.(providerConfiguration) - veClient, err := config.GetVEClient() + config := m.(providerConfiguration) + veClient, err := config.GetVEClient() - if err != nil { - return err + if err != nil { + return err + } + + nodeName := d.Get(mkResourceVirtualEnvironmentVMNodeName).(string) + vmID, err := strconv.Atoi(d.Id()) + + if err != nil { + return err + } + + err = veClient.DeleteVM(nodeName, vmID) + + if err != nil { + if strings.Contains(err.Error(), "HTTP 404") { + d.SetId("") + + return nil } - d.SetId("") - */ + return err + } + + d.SetId("") return nil } diff --git a/proxmoxtf/resource_virtual_environment_vm_test.go b/proxmoxtf/resource_virtual_environment_vm_test.go index 5b90fced..c39cf38d 100644 --- a/proxmoxtf/resource_virtual_environment_vm_test.go +++ b/proxmoxtf/resource_virtual_environment_vm_test.go @@ -23,21 +23,31 @@ func TestResourceVirtualEnvironmentVMInstantiation(t *testing.T) { func TestResourceVirtualEnvironmentVMSchema(t *testing.T) { s := resourceVirtualEnvironmentVM() + testRequiredArguments(t, s, []string{ + mkResourceVirtualEnvironmentVMNodeName, + }) + testOptionalArguments(t, s, []string{ + mkResourceVirtualEnvironmentVMCDROM, mkResourceVirtualEnvironmentVMCPU, mkResourceVirtualEnvironmentVMDisk, + mkResourceVirtualEnvironmentVMKeyboardLayout, mkResourceVirtualEnvironmentVMMemory, mkResourceVirtualEnvironmentVMName, mkResourceVirtualEnvironmentVMNetworkDevice, + mkResourceVirtualEnvironmentVMOSType, mkResourceVirtualEnvironmentVMVMID, }) testSchemaValueTypes(t, s, []string{ + mkResourceVirtualEnvironmentVMCDROM, mkResourceVirtualEnvironmentVMCPU, mkResourceVirtualEnvironmentVMDisk, + mkResourceVirtualEnvironmentVMKeyboardLayout, mkResourceVirtualEnvironmentVMMemory, mkResourceVirtualEnvironmentVMName, mkResourceVirtualEnvironmentVMNetworkDevice, + mkResourceVirtualEnvironmentVMOSType, mkResourceVirtualEnvironmentVMVMID, }, []schema.ValueType{ schema.TypeList, @@ -45,9 +55,27 @@ func TestResourceVirtualEnvironmentVMSchema(t *testing.T) { schema.TypeList, schema.TypeString, schema.TypeList, + schema.TypeString, + schema.TypeList, + schema.TypeString, schema.TypeInt, }) + cdromSchema := testNestedSchemaExistence(t, s, mkResourceVirtualEnvironmentVMCDROM) + + testOptionalArguments(t, cdromSchema, []string{ + mkResourceVirtualEnvironmentVMCDROMEnabled, + mkResourceVirtualEnvironmentVMCDROMFileID, + }) + + testSchemaValueTypes(t, cdromSchema, []string{ + mkResourceVirtualEnvironmentVMCDROMEnabled, + mkResourceVirtualEnvironmentVMCDROMFileID, + }, []schema.ValueType{ + schema.TypeBool, + schema.TypeString, + }) + cpuSchema := testNestedSchemaExistence(t, s, mkResourceVirtualEnvironmentVMCPU) testOptionalArguments(t, cpuSchema, []string{ @@ -67,15 +95,21 @@ func TestResourceVirtualEnvironmentVMSchema(t *testing.T) { testOptionalArguments(t, diskSchema, []string{ mkResourceVirtualEnvironmentVMDiskDatastoreID, + mkResourceVirtualEnvironmentVMDiskEnabled, + mkResourceVirtualEnvironmentVMDiskFileFormat, mkResourceVirtualEnvironmentVMDiskFileID, mkResourceVirtualEnvironmentVMDiskSize, }) testSchemaValueTypes(t, diskSchema, []string{ mkResourceVirtualEnvironmentVMDiskDatastoreID, + mkResourceVirtualEnvironmentVMDiskEnabled, + mkResourceVirtualEnvironmentVMDiskFileFormat, mkResourceVirtualEnvironmentVMDiskFileID, mkResourceVirtualEnvironmentVMDiskSize, }, []schema.ValueType{ + schema.TypeString, + schema.TypeBool, schema.TypeString, schema.TypeString, schema.TypeInt, @@ -103,6 +137,7 @@ func TestResourceVirtualEnvironmentVMSchema(t *testing.T) { testOptionalArguments(t, networkDeviceSchema, []string{ mkResourceVirtualEnvironmentVMNetworkDeviceBridge, + mkResourceVirtualEnvironmentVMNetworkDeviceEnabled, mkResourceVirtualEnvironmentVMNetworkDeviceMACAddress, mkResourceVirtualEnvironmentVMNetworkDeviceModel, mkResourceVirtualEnvironmentVMNetworkDeviceVLANID, @@ -110,11 +145,13 @@ func TestResourceVirtualEnvironmentVMSchema(t *testing.T) { testSchemaValueTypes(t, networkDeviceSchema, []string{ mkResourceVirtualEnvironmentVMNetworkDeviceBridge, + mkResourceVirtualEnvironmentVMNetworkDeviceEnabled, mkResourceVirtualEnvironmentVMNetworkDeviceMACAddress, mkResourceVirtualEnvironmentVMNetworkDeviceModel, mkResourceVirtualEnvironmentVMNetworkDeviceVLANID, }, []schema.ValueType{ schema.TypeString, + schema.TypeBool, schema.TypeString, schema.TypeString, schema.TypeInt, diff --git a/proxmoxtf/utils.go b/proxmoxtf/utils.go index e73b3891..c01d4a21 100644 --- a/proxmoxtf/utils.go +++ b/proxmoxtf/utils.go @@ -5,11 +5,161 @@ package proxmoxtf import ( + "fmt" + "regexp" "testing" "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" ) +func getFileFormatValidator() schema.SchemaValidateFunc { + return validation.StringInSlice([]string{"qcow2", "raw", "vmdk"}, false) +} + +func getFileIDValidator() schema.SchemaValidateFunc { + return func(i interface{}, k string) (ws []string, es []error) { + v, ok := i.(string) + + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + + if v != "" { + r := regexp.MustCompile(`^(?i)[a-z0-9\-_]+:([a-z0-9\-_]+/)?.+$`) + ok := r.MatchString(v) + + if !ok { + es = append(es, fmt.Errorf("expected %s to be a valid file identifier (datastore-name:iso/some-file.img), got %s", k, v)) + return + } + } + + return + } +} + +func getKeyboardLayoutValidator() schema.SchemaValidateFunc { + return validation.StringInSlice([]string{ + "da", + "de", + "de-ch", + "en-gb", + "en-us", + "es", + "fi", + "fr", + "fr-be", + "fr-ca", + "fr-ch", + "hu", + "is", + "it", + "ja", + "lt", + "mk", + "nl", + "no", + "pl", + "pt", + "pt-br", + "sl", + "sv", + "tr", + }, false) +} + +func getMACAddressValidator() schema.SchemaValidateFunc { + return func(i interface{}, k string) (ws []string, es []error) { + v, ok := i.(string) + + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be string", k)) + return + } + + if v != "" { + r := regexp.MustCompile(`^[A-Z0-9]{2}(:[A-Z0-9]{2}){5}$`) + ok := r.MatchString(v) + + if !ok { + es = append(es, fmt.Errorf("expected %s to be a valid MAC address (A0:B1:C2:D3:E4:F5), got %s", k, v)) + return + } + } + + return + } +} + +func getNetworkDeviceModelValidator() schema.SchemaValidateFunc { + return validation.StringInSlice([]string{"e1000", "rtl8139", "virtio", "vmxnet3"}, false) +} + +func getOSTypeValidator() schema.SchemaValidateFunc { + return validation.StringInSlice([]string{ + "l24", + "l26", + "other", + "solaris", + "w2k", + "w2k3", + "w2k8", + "win7", + "win8", + "win10", + "wvista", + "wxp", + }, false) +} + +func getVLANIDValidator() schema.SchemaValidateFunc { + return func(i interface{}, k string) (ws []string, es []error) { + min := 1 + max := 4094 + + v, ok := i.(int) + + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be int", k)) + return + } + + if v != -1 { + if v < min || v > max { + es = append(es, fmt.Errorf("expected %s to be in the range (%d - %d), got %d", k, min, max, v)) + return + } + } + + return + } +} + +func getVMIDValidator() schema.SchemaValidateFunc { + return func(i interface{}, k string) (ws []string, es []error) { + min := 1 + max := 2147483647 + + v, ok := i.(int) + + if !ok { + es = append(es, fmt.Errorf("expected type of %s to be int", k)) + return + } + + if v != -1 { + if v < min || v > max { + es = append(es, fmt.Errorf("expected %s to be in the range (%d - %d), got %d", k, min, max, v)) + return + } + } + + return + } +} + func testComputedAttributes(t *testing.T, s *schema.Resource, keys []string) { for _, v := range keys { if s.Schema[v] == nil {