0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-07-01 02:52:58 +00:00
terraform-provider-proxmox/proxmoxtf/utils.go
Pascal Wiedenbeck 0df14f9d6a
feat: add the ability to clone to non-shared storage on different nodes (#178)
* feat: add workaround for cloning to non-shared storage

* fix: fix wrong API params used

* test: add new var to tests

* fix: lint issues

* docs: add new argument to docs

* docs: fix function documentation

* fix: better work with heterogeneous datastores

* docs: clarify clone behavior

* fix: go lint issues

Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
2022-12-12 16:28:53 -05:00

608 lines
14 KiB
Go

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
package proxmoxtf
import (
"errors"
"fmt"
"math"
"regexp"
"strconv"
"strings"
"testing"
"time"
"unicode"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/bpg/terraform-provider-proxmox/proxmox"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)
func getBIOSValidator() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(validation.StringInSlice([]string{
"ovmf",
"seabios",
}, false))
}
func getContentTypeValidator() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(validation.StringInSlice([]string{
"backup",
"iso",
"snippets",
"vztmpl",
}, false))
}
//nolint:unused
func getCPUFlagsValidator() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(func(i interface{}, k string) (ws []string, es []error) {
list, ok := i.([]interface{})
if !ok {
es = append(es, fmt.Errorf("expected type of %s to be []interface{}", k))
return
}
validator := validation.StringInSlice([]string{
"+aes",
"-aes",
"+amd-no-ssb",
"-amd-no-ssb",
"+amd-ssbd",
"-amd-ssbd",
"+hv-evmcs",
"-hv-evmcs",
"+hv-tlbflush",
"-hv-tlbflush",
"+ibpb",
"-ibpb",
"+md-clear",
"-md-clear",
"+pcid",
"-pcid",
"+pdpe1gb",
"-pdpe1gb",
"+spec-ctrl",
"-spec-ctrl",
"+ssbd",
"-ssbd",
"+virt-ssbd",
"-virt-ssbd",
}, false)
for li, lv := range list {
v, ok := lv.(string)
if !ok {
es = append(es, fmt.Errorf("expected type of %s[%d] to be string", k, li))
return
}
warns, errs := validator(v, k)
ws = append(ws, warns...)
es = append(es, errs...)
if len(es) > 0 {
return
}
}
return
})
}
func getCPUTypeValidator() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(validation.StringInSlice([]string{
"486",
"Broadwell",
"Broadwell-IBRS",
"Broadwell-noTSX",
"Broadwell-noTSX-IBRS",
"Cascadelake-Server",
"Conroe",
"EPYC",
"EPYC-IBPB",
"Haswell",
"Haswell-IBRS",
"Haswell-noTSX",
"Haswell-noTSX-IBRS",
"IvyBridge",
"IvyBridge-IBRS",
"KnightsMill",
"Nehalem",
"Nehalem-IBRS",
"Opteron_G1",
"Opteron_G2",
"Opteron_G3",
"Opteron_G4",
"Opteron_G5",
"Penryn",
"SandyBridge",
"SandyBridge-IBRS",
"Skylake-Client",
"Skylake-Client-IBRS",
"Skylake-Server",
"Skylake-Server-IBRS",
"Westmere",
"Westmere-IBRS",
"athlon",
"core2duo",
"coreduo",
"host",
"kvm32",
"kvm64",
"max",
"pentium",
"pentium2",
"pentium3",
"phenom",
"qemu32",
"qemu64",
}, false))
}
func getFileFormatValidator() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(validation.StringInSlice([]string{
"qcow2",
"raw",
"vmdk",
}, false))
}
func getFileIDValidator() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(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-z\d\-_]+:([a-z\d\-_]+/)?.+$`)
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.SchemaValidateDiagFunc {
return validation.ToDiagFunc(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 diskDigitPrefix(s string) string {
for i, r := range s {
if unicode.IsDigit(r) {
return s[:i]
}
}
return s
}
func getMACAddressValidator() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(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-Z\d]{2}(:[A-Z\d]{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.SchemaValidateDiagFunc {
return validation.ToDiagFunc(validation.StringInSlice([]string{"e1000", "rtl8139", "virtio", "vmxnet3"}, false))
}
func getQEMUAgentTypeValidator() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(validation.StringInSlice([]string{"isa", "virtio"}, false))
}
func getSchemaBlock(r *schema.Resource, d *schema.ResourceData, k []string, i int, allowDefault bool) (map[string]interface{}, error) {
var resourceBlock map[string]interface{}
var resourceData interface{}
var resourceSchema *schema.Schema
for ki, kv := range k {
if ki == 0 {
resourceData = d.Get(kv)
resourceSchema = r.Schema[kv]
} else {
mapValues := resourceData.([]interface{})
if len(mapValues) <= i {
return resourceBlock, fmt.Errorf("index out of bounds %d", i)
}
mapValue := mapValues[i].(map[string]interface{})
resourceData = mapValue[kv]
resourceSchema = resourceSchema.Elem.(*schema.Resource).Schema[kv]
}
}
list := resourceData.([]interface{})
if len(list) == 0 {
if allowDefault {
listDefault, err := resourceSchema.DefaultValue()
if err != nil {
return nil, err
}
list = listDefault.([]interface{})
}
}
if len(list) > i {
resourceBlock = list[i].(map[string]interface{})
}
return resourceBlock, nil
}
func getTimeoutValidator() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(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
}
_, err := time.ParseDuration(v)
if err != nil {
es = append(es, fmt.Errorf("expected value of %s to be a duration - got: %s", k, v))
return
}
return
})
}
func getVGAMemoryValidator() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(validation.IntBetween(4, 512))
}
func getVGATypeValidator() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(validation.StringInSlice([]string{
"cirrus",
"qxl",
"qxl2",
"qxl3",
"qxl4",
"serial0",
"serial1",
"serial2",
"serial3",
"std",
"virtio",
"vmware",
}, false))
}
//nolint:unused
func getVLANIDsValidator() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(func(i interface{}, k string) (ws []string, es []error) {
min := 1
max := 4094
list, ok := i.([]interface{})
if !ok {
es = append(es, fmt.Errorf("expected type of %s to be []interface{}", k))
return
}
for li, lv := range list {
v, ok := lv.(int)
if !ok {
es = append(es, fmt.Errorf("expected type of %s[%d] to be int", k, li))
return
}
if v != -1 {
if v < min || v > max {
es = append(es, fmt.Errorf("expected %s[%d] to be in the range (%d - %d), got %d", k, li, min, max, v))
return
}
}
}
return
})
}
func getVMIDValidator() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(func(i interface{}, k string) (ws []string, es []error) {
min := 100
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 getDiskInfo(vm *proxmox.VirtualEnvironmentVMGetResponseData, d *schema.ResourceData) map[string]*proxmox.CustomStorageDevice {
currentDisk := d.Get(mkResourceVirtualEnvironmentVMDisk)
currentDiskList := currentDisk.([]interface{})
currentDiskMap := map[string]map[string]interface{}{}
for _, v := range currentDiskList {
diskMap := v.(map[string]interface{})
diskInterface := diskMap[mkResourceVirtualEnvironmentVMDiskInterface].(string)
currentDiskMap[diskInterface] = diskMap
}
storageDevices := map[string]*proxmox.CustomStorageDevice{}
storageDevices["ide0"] = vm.IDEDevice0
storageDevices["ide1"] = vm.IDEDevice1
storageDevices["ide2"] = vm.IDEDevice2
storageDevices["sata0"] = vm.SATADevice0
storageDevices["sata1"] = vm.SATADevice1
storageDevices["sata2"] = vm.SATADevice2
storageDevices["sata3"] = vm.SATADevice3
storageDevices["sata4"] = vm.SATADevice4
storageDevices["sata5"] = vm.SATADevice5
storageDevices["scsi0"] = vm.SCSIDevice0
storageDevices["scsi1"] = vm.SCSIDevice1
storageDevices["scsi2"] = vm.SCSIDevice2
storageDevices["scsi3"] = vm.SCSIDevice3
storageDevices["scsi4"] = vm.SCSIDevice4
storageDevices["scsi5"] = vm.SCSIDevice5
storageDevices["scsi6"] = vm.SCSIDevice6
storageDevices["scsi7"] = vm.SCSIDevice7
storageDevices["scsi8"] = vm.SCSIDevice8
storageDevices["scsi9"] = vm.SCSIDevice9
storageDevices["scsi10"] = vm.SCSIDevice10
storageDevices["scsi11"] = vm.SCSIDevice11
storageDevices["scsi12"] = vm.SCSIDevice12
storageDevices["scsi13"] = vm.SCSIDevice13
storageDevices["virtio0"] = vm.VirtualIODevice0
storageDevices["virtio1"] = vm.VirtualIODevice1
storageDevices["virtio2"] = vm.VirtualIODevice2
storageDevices["virtio3"] = vm.VirtualIODevice3
storageDevices["virtio4"] = vm.VirtualIODevice4
storageDevices["virtio5"] = vm.VirtualIODevice5
storageDevices["virtio6"] = vm.VirtualIODevice6
storageDevices["virtio7"] = vm.VirtualIODevice7
storageDevices["virtio8"] = vm.VirtualIODevice8
storageDevices["virtio9"] = vm.VirtualIODevice9
storageDevices["virtio10"] = vm.VirtualIODevice10
storageDevices["virtio11"] = vm.VirtualIODevice11
storageDevices["virtio12"] = vm.VirtualIODevice12
storageDevices["virtio13"] = vm.VirtualIODevice13
storageDevices["virtio14"] = vm.VirtualIODevice14
storageDevices["virtio15"] = vm.VirtualIODevice15
for k, v := range storageDevices {
if v != nil {
if currentDiskMap[k] != nil {
if currentDiskMap[k][mkResourceVirtualEnvironmentVMDiskFileID] != nil {
fileID := currentDiskMap[k][mkResourceVirtualEnvironmentVMDiskFileID].(string)
v.FileID = &fileID
}
}
v.Interface = &k
}
}
return storageDevices
}
// getDiskDatastores returns a list of the used datastores in a VM
func getDiskDatastores(vm *proxmox.VirtualEnvironmentVMGetResponseData, d *schema.ResourceData) []string {
storageDevices := getDiskInfo(vm, d)
datastoresSet := map[string]int{}
for _, diskInfo := range storageDevices {
// Ignore empty storage devices and storage devices (like ide) which may not have any media mounted
if diskInfo == nil || diskInfo.FileVolume == "none" {
continue
}
fileIDParts := strings.Split(diskInfo.FileVolume, ":")
datastoresSet[fileIDParts[0]] = 1
}
datastores := []string{}
for datastore := range datastoresSet {
datastores = append(datastores, datastore)
}
return datastores
}
func parseDiskSize(size *string) (int, error) {
var diskSize int
var err error
if size != nil {
if strings.HasSuffix(*size, "T") {
diskSize, err = strconv.Atoi(strings.TrimSuffix(*size, "T"))
if err != nil {
return -1, err
}
diskSize = int(math.Ceil(float64(diskSize) * 1024))
} else if strings.HasSuffix(*size, "G") {
diskSize, err = strconv.Atoi(strings.TrimSuffix(*size, "G"))
if err != nil {
return -1, err
}
} else if strings.HasSuffix(*size, "M") {
diskSize, err = strconv.Atoi(strings.TrimSuffix(*size, "M"))
if err != nil {
return -1, err
}
diskSize = int(math.Ceil(float64(diskSize) / 1024))
} else {
return -1, fmt.Errorf("cannot parse storage size \"%s\"", *size)
}
}
return diskSize, err
}
func getCloudInitTypeValidator() schema.SchemaValidateDiagFunc {
return validation.ToDiagFunc(validation.StringInSlice([]string{
"configdrive2",
"nocloud",
}, false))
}
func testComputedAttributes(t *testing.T, s *schema.Resource, keys []string) {
for _, v := range keys {
if s.Schema[v] == nil {
t.Fatalf("Error in Schema: Missing definition for \"%s\"", v)
}
if !s.Schema[v].Computed {
t.Fatalf("Error in Schema: Attribute \"%s\" is not computed", v)
}
}
}
func testNestedSchemaExistence(t *testing.T, s *schema.Resource, key string) *schema.Resource {
sh, ok := s.Schema[key].Elem.(*schema.Resource)
if !ok {
t.Fatalf("Error in Schema: Missing nested schema for \"%s\"", key)
return nil
}
return sh
}
func testOptionalArguments(t *testing.T, s *schema.Resource, keys []string) {
for _, v := range keys {
if s.Schema[v] == nil {
t.Fatalf("Error in Schema: Missing definition for \"%s\"", v)
}
if !s.Schema[v].Optional {
t.Fatalf("Error in Schema: Argument \"%s\" is not optional", v)
}
}
}
func testRequiredArguments(t *testing.T, s *schema.Resource, keys []string) {
for _, v := range keys {
if s.Schema[v] == nil {
t.Fatalf("Error in Schema: Missing definition for \"%s\"", v)
}
if !s.Schema[v].Required {
t.Fatalf("Error in Schema: Argument \"%s\" is not required", v)
}
}
}
func testValueTypes(t *testing.T, s *schema.Resource, f map[string]schema.ValueType) {
for fn, ft := range f {
if s.Schema[fn] == nil {
t.Fatalf("Error in Schema: Missing definition for \"%s\"", fn)
}
if s.Schema[fn].Type != ft {
t.Fatalf("Error in Schema: Argument or attribute \"%s\" is not of type \"%v\"", fn, ft)
}
}
}
type ErrorDiags diag.Diagnostics
func (diags ErrorDiags) Errors() []error {
var es []error
for i := range diags {
if diags[i].Severity == diag.Error {
s := fmt.Sprintf("Error: %s", diags[i].Summary)
if diags[i].Detail != "" {
s = fmt.Sprintf("%s: %s", s, diags[i].Detail)
}
es = append(es, errors.New(s))
}
}
return es
}
func (diags ErrorDiags) Error() string {
return multierror.ListFormatFunc(diags.Errors())
}