From 580381f89248f40ad645d8f9208e2223c66d6e4f Mon Sep 17 00:00:00 2001 From: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Date: Sun, 9 Jun 2024 00:11:16 -0400 Subject: [PATCH] chore(api): refactor `nodes/vms/vms_types.go`: split into multiple files (#1368) Split all `Custom*` structs and marshaling code into separate files from `vms_types.go` Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- proxmox/nodes/vms/custom_agent.go | 87 + proxmox/nodes/vms/custom_audio_device.go | 76 + proxmox/nodes/vms/custom_boot.go | 52 + proxmox/nodes/vms/custom_cloud_init.go | 210 +++ proxmox/nodes/vms/custom_cpu_emulation.go | 95 + proxmox/nodes/vms/custom_efi_disk.go | 87 + proxmox/nodes/vms/custom_network_device.go | 191 ++ proxmox/nodes/vms/custom_numa_device.go | 95 + proxmox/nodes/vms/custom_numa_device_test.go | 54 + proxmox/nodes/vms/custom_pci_device.go | 136 ++ proxmox/nodes/vms/custom_pci_device_test.go | 73 + proxmox/nodes/vms/custom_serial_device.go | 24 + proxmox/nodes/vms/custom_shared_memory.go | 67 + proxmox/nodes/vms/custom_smbios.go | 114 ++ .../nodes/vms/custom_spice_enhancements.go | 72 + proxmox/nodes/vms/custom_startup_order.go | 88 + ...ragedevice.go => custom_storage_device.go} | 197 +- ..._test.go => custom_storage_device_test.go} | 161 +- proxmox/nodes/vms/custom_tpm_state.go | 62 + proxmox/nodes/vms/custom_usb_device.go | 95 + proxmox/nodes/vms/custom_usb_device_test.go | 61 + proxmox/nodes/vms/custom_vga_device.go | 83 + proxmox/nodes/vms/custom_virtualio_device.go | 62 + proxmox/nodes/vms/custom_watchdog_device.go | 67 + proxmox/nodes/vms/vms_types.go | 1651 +---------------- proxmoxtf/resource/vm/vm.go | 2 - 26 files changed, 2224 insertions(+), 1738 deletions(-) create mode 100644 proxmox/nodes/vms/custom_agent.go create mode 100644 proxmox/nodes/vms/custom_audio_device.go create mode 100644 proxmox/nodes/vms/custom_boot.go create mode 100644 proxmox/nodes/vms/custom_cloud_init.go create mode 100644 proxmox/nodes/vms/custom_cpu_emulation.go create mode 100644 proxmox/nodes/vms/custom_efi_disk.go create mode 100644 proxmox/nodes/vms/custom_network_device.go create mode 100644 proxmox/nodes/vms/custom_numa_device.go create mode 100644 proxmox/nodes/vms/custom_numa_device_test.go create mode 100644 proxmox/nodes/vms/custom_pci_device.go create mode 100644 proxmox/nodes/vms/custom_pci_device_test.go create mode 100644 proxmox/nodes/vms/custom_serial_device.go create mode 100644 proxmox/nodes/vms/custom_shared_memory.go create mode 100644 proxmox/nodes/vms/custom_smbios.go create mode 100644 proxmox/nodes/vms/custom_spice_enhancements.go create mode 100644 proxmox/nodes/vms/custom_startup_order.go rename proxmox/nodes/vms/{customstoragedevice.go => custom_storage_device.go} (62%) rename proxmox/nodes/vms/{vms_types_test.go => custom_storage_device_test.go} (60%) create mode 100644 proxmox/nodes/vms/custom_tpm_state.go create mode 100644 proxmox/nodes/vms/custom_usb_device.go create mode 100644 proxmox/nodes/vms/custom_usb_device_test.go create mode 100644 proxmox/nodes/vms/custom_vga_device.go create mode 100644 proxmox/nodes/vms/custom_virtualio_device.go create mode 100644 proxmox/nodes/vms/custom_watchdog_device.go diff --git a/proxmox/nodes/vms/custom_agent.go b/proxmox/nodes/vms/custom_agent.go new file mode 100644 index 00000000..0ec2457f --- /dev/null +++ b/proxmox/nodes/vms/custom_agent.go @@ -0,0 +1,87 @@ +/* + * 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 vms + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + + "github.com/bpg/terraform-provider-proxmox/proxmox/types" +) + +// CustomAgent handles QEMU agent parameters. +type CustomAgent struct { + Enabled *types.CustomBool `json:"enabled,omitempty" url:"enabled,int"` + TrimClonedDisks *types.CustomBool `json:"fstrim_cloned_disks" url:"fstrim_cloned_disks,int"` + Type *string `json:"type" url:"type"` +} + +// EncodeValues converts a CustomAgent struct to a URL value. +func (r *CustomAgent) EncodeValues(key string, v *url.Values) error { + var values []string + + if r.Enabled != nil { + if *r.Enabled { + values = append(values, "enabled=1") + } else { + values = append(values, "enabled=0") + } + } + + if r.TrimClonedDisks != nil { + if *r.TrimClonedDisks { + values = append(values, "fstrim_cloned_disks=1") + } else { + values = append(values, "fstrim_cloned_disks=0") + } + } + + 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 +} + +// UnmarshalJSON converts a CustomAgent string to an object. +func (r *CustomAgent) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("error unmarshalling CustomAgent: %w", err) + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + if len(v) == 1 { + enabled := types.CustomBool(v[0] == "1") + r.Enabled = &enabled + } else if len(v) == 2 { + switch v[0] { + case "enabled": + enabled := types.CustomBool(v[1] == "1") + r.Enabled = &enabled + case "fstrim_cloned_disks": + fstrim := types.CustomBool(v[1] == "1") + r.TrimClonedDisks = &fstrim + case "type": + r.Type = &v[1] + } + } + } + + return nil +} diff --git a/proxmox/nodes/vms/custom_audio_device.go b/proxmox/nodes/vms/custom_audio_device.go new file mode 100644 index 00000000..c4acd3be --- /dev/null +++ b/proxmox/nodes/vms/custom_audio_device.go @@ -0,0 +1,76 @@ +/* + * 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 vms + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" +) + +// CustomAudioDevice handles QEMU audio parameters. +type CustomAudioDevice struct { + Device string `json:"device" url:"device"` + Driver *string `json:"driver" url:"driver"` + Enabled bool `json:"-" url:"-"` +} + +// CustomAudioDevices handles QEMU audio device parameters. +type CustomAudioDevices []CustomAudioDevice + +// EncodeValues converts a CustomAudioDevice struct to a URL value. +func (r *CustomAudioDevice) EncodeValues(key string, v *url.Values) error { + values := []string{fmt.Sprintf("device=%s", r.Device)} + + if r.Driver != nil { + values = append(values, fmt.Sprintf("driver=%s", *r.Driver)) + } + + v.Add(key, strings.Join(values, ",")) + + return nil +} + +// UnmarshalJSON converts a CustomAudioDevice string to an object. +func (r *CustomAudioDevice) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("error unmarshalling CustomAudioDevice: %w", err) + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + if len(v) == 2 { + switch v[0] { + case "device": + r.Device = v[1] + case "driver": + r.Driver = &v[1] + } + } + } + + return nil +} + +// EncodeValues converts a CustomAudioDevices array to multiple URL values. +func (r CustomAudioDevices) EncodeValues(key string, v *url.Values) error { + for i, d := range r { + if d.Enabled { + if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil { + return fmt.Errorf("unable to encode audio device %d: %w", i, err) + } + } + } + + return nil +} diff --git a/proxmox/nodes/vms/custom_boot.go b/proxmox/nodes/vms/custom_boot.go new file mode 100644 index 00000000..ad8ba21c --- /dev/null +++ b/proxmox/nodes/vms/custom_boot.go @@ -0,0 +1,52 @@ +/* + * 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 vms + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" +) + +// CustomBoot handles QEMU boot parameters. +type CustomBoot struct { + Order *[]string `json:"order,omitempty" url:"order,omitempty,semicolon"` +} + +// EncodeValues converts a CustomBoot struct to multiple URL values. +func (r *CustomBoot) EncodeValues(key string, v *url.Values) error { + if r.Order != nil && len(*r.Order) > 0 { + v.Add(key, fmt.Sprintf("order=%s", strings.Join(*r.Order, ";"))) + } + + return nil +} + +// UnmarshalJSON converts a CustomBoot string to an object. +func (r *CustomBoot) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("error unmarshalling CustomBoot: %w", err) + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + if len(v) == 2 { + if v[0] == "order" { + o := strings.Split(strings.TrimSpace(v[1]), ";") + r.Order = &o + } + } + } + + return nil +} diff --git a/proxmox/nodes/vms/custom_cloud_init.go b/proxmox/nodes/vms/custom_cloud_init.go new file mode 100644 index 00000000..3226e4c2 --- /dev/null +++ b/proxmox/nodes/vms/custom_cloud_init.go @@ -0,0 +1,210 @@ +/* + * 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 vms + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" +) + +// CustomCloudInitConfig handles QEMU cloud-init parameters. +type CustomCloudInitConfig struct { + Files *CustomCloudInitFiles `json:"cicustom,omitempty" url:"cicustom,omitempty"` + IPConfig []CustomCloudInitIPConfig `json:"ipconfig,omitempty" url:"ipconfig,omitempty,numbered"` + Nameserver *string `json:"nameserver,omitempty" url:"nameserver,omitempty"` + Password *string `json:"cipassword,omitempty" url:"cipassword,omitempty"` + SearchDomain *string `json:"searchdomain,omitempty" url:"searchdomain,omitempty"` + SSHKeys *CustomCloudInitSSHKeys `json:"sshkeys,omitempty" url:"sshkeys,omitempty"` + Type *string `json:"citype,omitempty" url:"citype,omitempty"` + // Can't be reliably set, it is TRUE by default in PVE + // Upgrade *types.CustomBool `json:"ciupgrade,omitempty" url:"ciupgrade,omitempty,int"` + Username *string `json:"ciuser,omitempty" url:"ciuser,omitempty"` +} + +// CustomCloudInitFiles handles QEMU cloud-init custom files parameters. +type CustomCloudInitFiles struct { + MetaVolume *string `json:"meta,omitempty" url:"meta,omitempty"` + NetworkVolume *string `json:"network,omitempty" url:"network,omitempty"` + UserVolume *string `json:"user,omitempty" url:"user,omitempty"` + VendorVolume *string `json:"vendor,omitempty" url:"vendor,omitempty"` +} + +// CustomCloudInitIPConfig handles QEMU cloud-init IP configuration parameters. +type CustomCloudInitIPConfig struct { + GatewayIPv4 *string `json:"gw,omitempty" url:"gw,omitempty"` + GatewayIPv6 *string `json:"gw6,omitempty" url:"gw6,omitempty"` + IPv4 *string `json:"ip,omitempty" url:"ip,omitempty"` + IPv6 *string `json:"ip6,omitempty" url:"ip6,omitempty"` +} + +// CustomCloudInitSSHKeys handles QEMU cloud-init SSH keys parameters. +type CustomCloudInitSSHKeys []string + +// EncodeValues converts a CustomCloudInitConfig struct to multiple URL values. +func (r CustomCloudInitConfig) EncodeValues(_ string, v *url.Values) error { + //nolint:nestif + if r.Files != nil { + var volumes []string + + if r.Files.MetaVolume != nil { + volumes = append(volumes, fmt.Sprintf("meta=%s", *r.Files.MetaVolume)) + } + + if r.Files.NetworkVolume != nil { + volumes = append(volumes, fmt.Sprintf("network=%s", *r.Files.NetworkVolume)) + } + + if r.Files.UserVolume != nil { + volumes = append(volumes, fmt.Sprintf("user=%s", *r.Files.UserVolume)) + } + + if r.Files.VendorVolume != nil { + volumes = append(volumes, fmt.Sprintf("vendor=%s", *r.Files.VendorVolume)) + } + + if len(volumes) > 0 { + v.Add("cicustom", strings.Join(volumes, ",")) + } + } + + for i, c := range r.IPConfig { + var config []string + + if c.GatewayIPv4 != nil { + config = append(config, fmt.Sprintf("gw=%s", *c.GatewayIPv4)) + } + + if c.GatewayIPv6 != nil { + config = append(config, fmt.Sprintf("gw6=%s", *c.GatewayIPv6)) + } + + if c.IPv4 != nil { + config = append(config, fmt.Sprintf("ip=%s", *c.IPv4)) + } + + if c.IPv6 != nil { + config = append(config, fmt.Sprintf("ip6=%s", *c.IPv6)) + } + + if len(config) > 0 { + v.Add(fmt.Sprintf("ipconfig%d", i), strings.Join(config, ",")) + } + } + + if r.Nameserver != nil { + v.Add("nameserver", *r.Nameserver) + } + + if r.Password != nil { + v.Add("cipassword", *r.Password) + } + + if r.SearchDomain != nil { + v.Add("searchdomain", *r.SearchDomain) + } + + if r.SSHKeys != nil { + v.Add( + "sshkeys", + strings.ReplaceAll(url.QueryEscape(strings.Join(*r.SSHKeys, "\n")), "+", "%20"), + ) + } + + if r.Type != nil { + v.Add("citype", *r.Type) + } + + if r.Username != nil { + v.Add("ciuser", *r.Username) + } + + return nil +} + +// UnmarshalJSON converts a CustomCloudInitFiles string to an object. +func (r *CustomCloudInitFiles) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("error unmarshalling CustomCloudInitFiles: %w", err) + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + if len(v) == 2 { + switch v[0] { + case "meta": + r.MetaVolume = &v[1] + case "network": + r.NetworkVolume = &v[1] + case "user": + r.UserVolume = &v[1] + case "vendor": + r.VendorVolume = &v[1] + } + } + } + + return nil +} + +// UnmarshalJSON converts a CustomCloudInitIPConfig string to an object. +func (r *CustomCloudInitIPConfig) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("error unmarshalling CustomCloudInitIPConfig: %w", err) + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + if len(v) == 2 { + switch v[0] { + case "gw": + r.GatewayIPv4 = &v[1] + case "gw6": + r.GatewayIPv6 = &v[1] + case "ip": + r.IPv4 = &v[1] + case "ip6": + r.IPv6 = &v[1] + } + } + } + + return nil +} + +// UnmarshalJSON converts a CustomCloudInitFiles string to an object. +func (r *CustomCloudInitSSHKeys) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("error unmarshalling CustomCloudInitSSHKeys: %w", err) + } + + s, err := url.QueryUnescape(s) + if err != nil { + return fmt.Errorf("error unescaping CustomCloudInitSSHKeys: %w", err) + } + + if s != "" { + *r = strings.Split(strings.TrimSpace(s), "\n") + } else { + *r = []string{} + } + + return nil +} diff --git a/proxmox/nodes/vms/custom_cpu_emulation.go b/proxmox/nodes/vms/custom_cpu_emulation.go new file mode 100644 index 00000000..9222a3c9 --- /dev/null +++ b/proxmox/nodes/vms/custom_cpu_emulation.go @@ -0,0 +1,95 @@ +/* + * 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 vms + +import ( + "encoding/json" + "errors" + "fmt" + "net/url" + "strings" + + "github.com/bpg/terraform-provider-proxmox/proxmox/types" +) + +// CustomCPUEmulation handles QEMU CPU emulation parameters. +type CustomCPUEmulation struct { + Flags *[]string `json:"flags,omitempty" url:"flags,omitempty,semicolon"` + Hidden *types.CustomBool `json:"hidden,omitempty" url:"hidden,omitempty,int"` + HVVendorID *string `json:"hv-vendor-id,omitempty" url:"hv-vendor-id,omitempty"` + Type string `json:"cputype,omitempty" url:"cputype,omitempty"` +} + +// EncodeValues converts a CustomCPUEmulation struct to a URL value. +func (r *CustomCPUEmulation) EncodeValues(key string, v *url.Values) error { + values := []string{ + fmt.Sprintf("cputype=%s", r.Type), + } + + if r.Flags != nil && len(*r.Flags) > 0 { + values = append(values, fmt.Sprintf("flags=%s", strings.Join(*r.Flags, ";"))) + } + + if r.Hidden != nil { + if *r.Hidden { + values = append(values, "hidden=1") + } else { + values = append(values, "hidden=0") + } + } + + if r.HVVendorID != nil { + values = append(values, fmt.Sprintf("hv-vendor-id=%s", *r.HVVendorID)) + } + + v.Add(key, strings.Join(values, ",")) + + return nil +} + +// UnmarshalJSON converts a CustomCPUEmulation string to an object. +func (r *CustomCPUEmulation) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("error unmarshalling CustomCPUEmulation: %w", err) + } + + if s == "" { + return errors.New("unexpected empty string") + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + if len(v) == 1 { + r.Type = v[0] + } else if len(v) == 2 { + switch v[0] { + case "cputype": + r.Type = v[1] + case "flags": + if v[1] != "" { + f := strings.Split(v[1], ";") + r.Flags = &f + } else { + var f []string + r.Flags = &f + } + case "hidden": + bv := types.CustomBool(v[1] == "1") + r.Hidden = &bv + case "hv-vendor-id": + r.HVVendorID = &v[1] + } + } + } + + return nil +} diff --git a/proxmox/nodes/vms/custom_efi_disk.go b/proxmox/nodes/vms/custom_efi_disk.go new file mode 100644 index 00000000..f1b2e461 --- /dev/null +++ b/proxmox/nodes/vms/custom_efi_disk.go @@ -0,0 +1,87 @@ +/* + * 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 vms + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + + "github.com/bpg/terraform-provider-proxmox/proxmox/types" +) + +// CustomEFIDisk handles QEMU EFI disk parameters. +type CustomEFIDisk struct { + FileVolume string `json:"file" url:"file"` + Format *string `json:"format,omitempty" url:"format,omitempty"` + Type *string `json:"efitype,omitempty" url:"efitype,omitempty"` + PreEnrolledKeys *types.CustomBool `json:"pre-enrolled-keys,omitempty" url:"pre-enrolled-keys,omitempty,int"` +} + +// EncodeValues converts a CustomEFIDisk struct to a URL value. +func (r *CustomEFIDisk) EncodeValues(key string, v *url.Values) error { + values := []string{ + fmt.Sprintf("file=%s", r.FileVolume), + } + + if r.Format != nil { + values = append(values, fmt.Sprintf("format=%s", *r.Format)) + } + + if r.Type != nil { + values = append(values, fmt.Sprintf("efitype=%s", *r.Type)) + } + + if r.PreEnrolledKeys != nil { + if *r.PreEnrolledKeys { + values = append(values, "pre-enrolled-keys=1") + } else { + values = append(values, "pre-enrolled-keys=0") + } + } + + v.Add(key, strings.Join(values, ",")) + + return nil +} + +// UnmarshalJSON converts a CustomEFIDisk string to an object. +func (r *CustomEFIDisk) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("failed to unmarshal CustomEFIDisk: %w", err) + } + + pairs := strings.Split(s, ",") + + for i, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + if len(v) == 1 && i == 0 { + r.FileVolume = v[0] + } + + if len(v) == 2 { + switch v[0] { + case "file": + r.FileVolume = v[1] + case "format": + r.Format = &v[1] + case "efitype": + t := strings.ToLower(v[1]) + r.Type = &t + case "pre-enrolled-keys": + bv := types.CustomBool(v[1] == "1") + r.PreEnrolledKeys = &bv + } + } + } + + return nil +} diff --git a/proxmox/nodes/vms/custom_network_device.go b/proxmox/nodes/vms/custom_network_device.go new file mode 100644 index 00000000..48dc97a2 --- /dev/null +++ b/proxmox/nodes/vms/custom_network_device.go @@ -0,0 +1,191 @@ +/* + * 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 vms + +import ( + "encoding/json" + "fmt" + "net/url" + "strconv" + "strings" + + "github.com/bpg/terraform-provider-proxmox/proxmox/types" +) + +// CustomNetworkDevice handles QEMU network device parameters. +type CustomNetworkDevice struct { + Enabled bool `json:"-" url:"-"` + Bridge *string `json:"bridge,omitempty" url:"bridge,omitempty"` + Firewall *types.CustomBool `json:"firewall,omitempty" url:"firewall,omitempty,int"` + LinkDown *types.CustomBool `json:"link_down,omitempty" url:"link_down,omitempty,int"` + MACAddress *string `json:"macaddr,omitempty" url:"macaddr,omitempty"` + MTU *int `json:"mtu,omitempty" url:"mtu,omitempty"` + Model string `json:"model" url:"model"` + Queues *int `json:"queues,omitempty" url:"queues,omitempty"` + RateLimit *float64 `json:"rate,omitempty" url:"rate,omitempty"` + Tag *int `json:"tag,omitempty" url:"tag,omitempty"` + Trunks []int `json:"trunks,omitempty" url:"trunks,omitempty"` +} + +// CustomNetworkDevices handles QEMU network device parameters. +type CustomNetworkDevices []CustomNetworkDevice + +// EncodeValues converts a CustomNetworkDevice struct to a URL value. +func (r *CustomNetworkDevice) EncodeValues(key string, v *url.Values) error { + values := []string{ + fmt.Sprintf("model=%s", r.Model), + } + + if r.Bridge != nil { + values = append(values, fmt.Sprintf("bridge=%s", *r.Bridge)) + } + + if r.Firewall != nil { + if *r.Firewall { + values = append(values, "firewall=1") + } else { + values = append(values, "firewall=0") + } + } + + if r.LinkDown != nil { + if *r.LinkDown { + values = append(values, "link_down=1") + } else { + values = append(values, "link_down=0") + } + } + + if r.MACAddress != nil { + values = append(values, fmt.Sprintf("macaddr=%s", *r.MACAddress)) + } + + if r.Queues != nil { + values = append(values, fmt.Sprintf("queues=%d", *r.Queues)) + } + + if r.RateLimit != nil { + values = append(values, fmt.Sprintf("rate=%f", *r.RateLimit)) + } + + if r.Tag != nil { + values = append(values, fmt.Sprintf("tag=%d", *r.Tag)) + } + + if r.MTU != nil { + values = append(values, fmt.Sprintf("mtu=%d", *r.MTU)) + } + + if len(r.Trunks) > 0 { + trunks := make([]string, len(r.Trunks)) + + for i, v := range r.Trunks { + trunks[i] = strconv.Itoa(v) + } + + values = append(values, fmt.Sprintf("trunks=%s", strings.Join(trunks, ";"))) + } + + v.Add(key, strings.Join(values, ",")) + + return nil +} + +// EncodeValues converts a CustomNetworkDevices array to multiple URL values. +func (r CustomNetworkDevices) EncodeValues(key string, v *url.Values) error { + for i, d := range r { + if d.Enabled { + if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil { + return fmt.Errorf("failed to encode network device %d: %w", i, err) + } + } + } + + return nil +} + +// UnmarshalJSON converts a CustomNetworkDevice string to an object. +func (r *CustomNetworkDevice) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("failed to unmarshal CustomNetworkDevice: %w", err) + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + //nolint:nestif + if len(v) == 2 { + switch v[0] { + case "bridge": + r.Bridge = &v[1] + case "firewall": + bv := types.CustomBool(v[1] == "1") + r.Firewall = &bv + case "link_down": + bv := types.CustomBool(v[1] == "1") + r.LinkDown = &bv + case "macaddr": + r.MACAddress = &v[1] + case "model": + r.Model = v[1] + case "queues": + iv, err := strconv.Atoi(v[1]) + if err != nil { + return fmt.Errorf("failed to parse queues: %w", err) + } + + r.Queues = &iv + case "rate": + fv, err := strconv.ParseFloat(v[1], 64) + if err != nil { + return fmt.Errorf("failed to parse rate: %w", err) + } + + r.RateLimit = &fv + + case "mtu": + iv, err := strconv.Atoi(v[1]) + if err != nil { + return fmt.Errorf("failed to parse mtu: %w", err) + } + + r.MTU = &iv + + case "tag": + iv, err := strconv.Atoi(v[1]) + if err != nil { + return fmt.Errorf("failed to parse tag: %w", err) + } + + r.Tag = &iv + case "trunks": + trunks := strings.Split(v[1], ";") + r.Trunks = make([]int, len(trunks)) + + for i, trunk := range trunks { + iv, err := strconv.Atoi(trunk) + if err != nil { + return fmt.Errorf("failed to parse trunk %d: %w", i, err) + } + + r.Trunks[i] = iv + } + default: + r.MACAddress = &v[1] + r.Model = v[0] + } + } + } + + r.Enabled = true + + return nil +} diff --git a/proxmox/nodes/vms/custom_numa_device.go b/proxmox/nodes/vms/custom_numa_device.go new file mode 100644 index 00000000..2bfa4b28 --- /dev/null +++ b/proxmox/nodes/vms/custom_numa_device.go @@ -0,0 +1,95 @@ +/* + * 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 vms + +import ( + "encoding/json" + "fmt" + "net/url" + "strconv" + "strings" +) + +// CustomNUMADevice handles QEMU NUMA device parameters. +type CustomNUMADevice struct { + CPUIDs []string `json:"cpus" url:"cpus,semicolon"` + HostNodeNames *[]string `json:"hostnodes,omitempty" url:"hostnodes,omitempty,semicolon"` + Memory *int `json:"memory,omitempty" url:"memory,omitempty"` + Policy *string `json:"policy,omitempty" url:"policy,omitempty"` +} + +// CustomNUMADevices handles QEMU NUMA device parameters. +type CustomNUMADevices []CustomNUMADevice + +// EncodeValues converts a CustomNUMADevice struct to a URL value. +func (r *CustomNUMADevice) EncodeValues(key string, v *url.Values) error { + values := []string{ + fmt.Sprintf("cpus=%s", strings.Join(r.CPUIDs, ";")), + } + + if r.HostNodeNames != nil { + values = append(values, fmt.Sprintf("hostnodes=%s", strings.Join(*r.HostNodeNames, ";"))) + } + + if r.Memory != nil { + values = append(values, fmt.Sprintf("memory=%d", *r.Memory)) + } + + if r.Policy != nil { + values = append(values, fmt.Sprintf("policy=%s", *r.Policy)) + } + + v.Add(key, strings.Join(values, ",")) + + return nil +} + +// EncodeValues converts a CustomNUMADevices array to multiple URL values. +func (r CustomNUMADevices) EncodeValues(key string, v *url.Values) error { + for i, d := range r { + if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil { + return fmt.Errorf("failed to encode NUMA device %d: %w", i, err) + } + } + + return nil +} + +// UnmarshalJSON converts a CustomNUMADevice string to an object. +func (r *CustomNUMADevice) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("failed to unmarshal CustomNUMADevice: %w", err) + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + if len(v) == 2 { + switch v[0] { + case "cpus": + r.CPUIDs = strings.Split(v[1], ";") + case "hostnodes": + hostnodes := strings.Split(v[1], ";") + r.HostNodeNames = &hostnodes + case "memory": + memory, err := strconv.Atoi(v[1]) + if err != nil { + return fmt.Errorf("failed to parse memory size: %w", err) + } + + r.Memory = &memory + case "policy": + r.Policy = &v[1] + } + } + } + + return nil +} diff --git a/proxmox/nodes/vms/custom_numa_device_test.go b/proxmox/nodes/vms/custom_numa_device_test.go new file mode 100644 index 00000000..4ff2150d --- /dev/null +++ b/proxmox/nodes/vms/custom_numa_device_test.go @@ -0,0 +1,54 @@ +/* + * 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 vms + +import ( + "testing" + + "github.com/bpg/terraform-provider-proxmox/proxmox/helpers/ptr" +) + +func TestCustomNUMADevice_UnmarshalJSON(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + line string + want *CustomNUMADevice + wantErr bool + }{ + { + name: "numa device all options", + line: `"cpus=1-2;3-4,hostnodes=1-2,memory=1024,policy=preferred"`, + want: &CustomNUMADevice{ + CPUIDs: []string{"1-2", "3-4"}, + HostNodeNames: &[]string{"1-2"}, + Memory: ptr.Ptr(1024), + Policy: ptr.Ptr("preferred"), + }, + }, + { + name: "numa device cpus/memory only", + line: `"cpus=1-2,memory=1024"`, + want: &CustomNUMADevice{ + CPUIDs: []string{"1-2"}, + Memory: ptr.Ptr(1024), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + r := &CustomNUMADevice{} + if err := r.UnmarshalJSON([]byte(tt.line)); (err != nil) != tt.wantErr { + t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/proxmox/nodes/vms/custom_pci_device.go b/proxmox/nodes/vms/custom_pci_device.go new file mode 100644 index 00000000..30cc93c3 --- /dev/null +++ b/proxmox/nodes/vms/custom_pci_device.go @@ -0,0 +1,136 @@ +/* + * 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 vms + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + + "github.com/bpg/terraform-provider-proxmox/proxmox/types" +) + +// CustomPCIDevice handles QEMU host PCI device mapping parameters. +type CustomPCIDevice struct { + DeviceIDs *[]string `json:"host,omitempty" url:"host,omitempty,semicolon"` + Mapping *string `json:"mapping,omitempty" url:"mapping,omitempty"` + MDev *string `json:"mdev,omitempty" url:"mdev,omitempty"` + PCIExpress *types.CustomBool `json:"pcie,omitempty" url:"pcie,omitempty,int"` + ROMBAR *types.CustomBool `json:"rombar,omitempty" url:"rombar,omitempty,int"` + ROMFile *string `json:"romfile,omitempty" url:"romfile,omitempty"` + XVGA *types.CustomBool `json:"x-vga,omitempty" url:"x-vga,omitempty,int"` +} + +// CustomPCIDevices handles QEMU host PCI device mapping parameters. +type CustomPCIDevices []CustomPCIDevice + +// EncodeValues converts a CustomPCIDevice struct to a URL value. +func (r *CustomPCIDevice) EncodeValues(key string, v *url.Values) error { + var values []string + + if r.DeviceIDs == nil && r.Mapping == nil { + return fmt.Errorf("either device ID or resource mapping must be set") + } + + if r.DeviceIDs != nil { + values = append(values, fmt.Sprintf("host=%s", strings.Join(*r.DeviceIDs, ";"))) + } + + if r.Mapping != nil { + values = append(values, fmt.Sprintf("mapping=%s", *r.Mapping)) + } + + if r.MDev != nil { + values = append(values, fmt.Sprintf("mdev=%s", *r.MDev)) + } + + if r.PCIExpress != nil { + if *r.PCIExpress { + values = append(values, "pcie=1") + } else { + values = append(values, "pcie=0") + } + } + + if r.ROMBAR != nil { + if *r.ROMBAR { + values = append(values, "rombar=1") + } else { + values = append(values, "rombar=0") + } + } + + if r.ROMFile != nil { + values = append(values, fmt.Sprintf("romfile=%s", *r.ROMFile)) + } + + if r.XVGA != nil { + if *r.XVGA { + values = append(values, "x-vga=1") + } else { + values = append(values, "x-vga=0") + } + } + + v.Add(key, strings.Join(values, ",")) + + return nil +} + +// EncodeValues converts a CustomPCIDevices array to multiple URL values. +func (r CustomPCIDevices) EncodeValues(key string, v *url.Values) error { + for i, d := range r { + if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil { + return fmt.Errorf("failed to encode PCI device %d: %w", i, err) + } + } + + return nil +} + +// UnmarshalJSON converts a CustomPCIDevice string to an object. +func (r *CustomPCIDevice) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("failed to unmarshal CustomPCIDevice: %w", err) + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + if len(v) == 1 { + dIDs := strings.Split(v[0], ";") + r.DeviceIDs = &dIDs + } else if len(v) == 2 { + switch v[0] { + case "host": + dIDs := strings.Split(v[1], ";") + r.DeviceIDs = &dIDs + case "mapping": + r.Mapping = &v[1] + case "mdev": + r.MDev = &v[1] + case "pcie": + bv := types.CustomBool(v[1] == "1") + r.PCIExpress = &bv + case "rombar": + bv := types.CustomBool(v[1] == "1") + r.ROMBAR = &bv + case "romfile": + r.ROMFile = &v[1] + case "x-vga": + bv := types.CustomBool(v[1] == "1") + r.XVGA = &bv + } + } + } + + return nil +} diff --git a/proxmox/nodes/vms/custom_pci_device_test.go b/proxmox/nodes/vms/custom_pci_device_test.go new file mode 100644 index 00000000..70f8dcef --- /dev/null +++ b/proxmox/nodes/vms/custom_pci_device_test.go @@ -0,0 +1,73 @@ +/* + * 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 vms + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/bpg/terraform-provider-proxmox/proxmox/helpers/ptr" + "github.com/bpg/terraform-provider-proxmox/proxmox/types" +) + +func TestCustomPCIDevice_UnmarshalJSON(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + line string + want *CustomPCIDevice + wantErr bool + }{ + { + name: "id only pci device", + line: `"0000:81:00.2"`, + want: &CustomPCIDevice{ + DeviceIDs: &[]string{"0000:81:00.2"}, + }, + }, + { + name: "pci device with more details", + line: `"host=81:00.4,pcie=0,rombar=1,x-vga=0"`, + want: &CustomPCIDevice{ + DeviceIDs: &[]string{"81:00.4"}, + MDev: nil, + PCIExpress: types.CustomBool(false).Pointer(), + ROMBAR: types.CustomBool(true).Pointer(), + ROMFile: nil, + XVGA: types.CustomBool(false).Pointer(), + }, + }, + { + name: "pci device with mapping", + line: `"mapping=mappeddevice,pcie=0,rombar=1,x-vga=0"`, + want: &CustomPCIDevice{ + DeviceIDs: nil, + Mapping: ptr.Ptr("mappeddevice"), + MDev: nil, + PCIExpress: types.CustomBool(false).Pointer(), + ROMBAR: types.CustomBool(true).Pointer(), + ROMFile: nil, + XVGA: types.CustomBool(false).Pointer(), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + r := &CustomPCIDevice{} + if err := r.UnmarshalJSON([]byte(tt.line)); (err != nil) != tt.wantErr { + t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + } + + require.Equal(t, tt.want, r) + }) + } +} diff --git a/proxmox/nodes/vms/custom_serial_device.go b/proxmox/nodes/vms/custom_serial_device.go new file mode 100644 index 00000000..377cf296 --- /dev/null +++ b/proxmox/nodes/vms/custom_serial_device.go @@ -0,0 +1,24 @@ +/* + * 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 vms + +import ( + "fmt" + "net/url" +) + +// CustomSerialDevices handles QEMU serial device parameters. +type CustomSerialDevices []string + +// EncodeValues converts a CustomSerialDevices array to multiple URL values. +func (r CustomSerialDevices) EncodeValues(key string, v *url.Values) error { + for i, d := range r { + v.Add(fmt.Sprintf("%s%d", key, i), d) + } + + return nil +} diff --git a/proxmox/nodes/vms/custom_shared_memory.go b/proxmox/nodes/vms/custom_shared_memory.go new file mode 100644 index 00000000..e84a9ed9 --- /dev/null +++ b/proxmox/nodes/vms/custom_shared_memory.go @@ -0,0 +1,67 @@ +/* + * 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 vms + +import ( + "encoding/json" + "fmt" + "net/url" + "strconv" + "strings" +) + +// CustomSharedMemory handles QEMU Inter-VM shared memory parameters. +type CustomSharedMemory struct { + Name *string `json:"name,omitempty" url:"name,omitempty"` + Size int `json:"size" url:"size"` +} + +// EncodeValues converts a CustomSharedMemory struct to a URL value. +func (r *CustomSharedMemory) EncodeValues(key string, v *url.Values) error { + values := []string{ + fmt.Sprintf("size=%d", r.Size), + } + + if r.Name != nil { + values = append(values, fmt.Sprintf("name=%s", *r.Name)) + } + + v.Add(key, strings.Join(values, ",")) + + return nil +} + +// UnmarshalJSON converts a CustomSharedMemory string to an object. +func (r *CustomSharedMemory) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("failed to unmarshal CustomSharedMemory: %w", err) + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + if len(v) == 2 { + switch v[0] { + case "name": + r.Name = &v[1] + case "size": + var err error + + r.Size, err = strconv.Atoi(v[1]) + if err != nil { + return fmt.Errorf("failed to parse shared memory size: %w", err) + } + } + } + } + + return nil +} diff --git a/proxmox/nodes/vms/custom_smbios.go b/proxmox/nodes/vms/custom_smbios.go new file mode 100644 index 00000000..f448610a --- /dev/null +++ b/proxmox/nodes/vms/custom_smbios.go @@ -0,0 +1,114 @@ +/* + * 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 vms + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + + "github.com/bpg/terraform-provider-proxmox/proxmox/types" +) + +// CustomSMBIOS handles QEMU SMBIOS parameters. +type CustomSMBIOS struct { + Base64 *types.CustomBool `json:"base64,omitempty" url:"base64,omitempty,int"` + Family *string `json:"family,omitempty" url:"family,omitempty"` + Manufacturer *string `json:"manufacturer,omitempty" url:"manufacturer,omitempty"` + Product *string `json:"product,omitempty" url:"product,omitempty"` + Serial *string `json:"serial,omitempty" url:"serial,omitempty"` + SKU *string `json:"sku,omitempty" url:"sku,omitempty"` + UUID *string `json:"uuid,omitempty" url:"uuid,omitempty"` + Version *string `json:"version,omitempty" url:"version,omitempty"` +} + +// EncodeValues converts a CustomSMBIOS struct to a URL value. +func (r *CustomSMBIOS) EncodeValues(key string, v *url.Values) error { + var values []string + + if r.Base64 != nil { + if *r.Base64 { + values = append(values, "base64=1") + } else { + values = append(values, "base64=0") + } + } + + if r.Family != nil { + values = append(values, fmt.Sprintf("family=%s", *r.Family)) + } + + if r.Manufacturer != nil { + values = append(values, fmt.Sprintf("manufacturer=%s", *r.Manufacturer)) + } + + if r.Product != nil { + values = append(values, fmt.Sprintf("product=%s", *r.Product)) + } + + if r.Serial != nil { + values = append(values, fmt.Sprintf("serial=%s", *r.Serial)) + } + + if r.SKU != nil { + values = append(values, fmt.Sprintf("sku=%s", *r.SKU)) + } + + if r.UUID != nil { + values = append(values, fmt.Sprintf("uuid=%s", *r.UUID)) + } + + if r.Version != nil { + values = append(values, fmt.Sprintf("version=%s", *r.Version)) + } + + if len(values) > 0 { + v.Add(key, strings.Join(values, ",")) + } + + return nil +} + +// UnmarshalJSON converts a CustomSMBIOS string to an object. +func (r *CustomSMBIOS) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("failed to unmarshal CustomSMBIOS: %w", err) + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.SplitN(strings.TrimSpace(p), "=", 2) + + if len(v) == 2 { + switch v[0] { + case "base64": + base64 := types.CustomBool(v[1] == "1") + r.Base64 = &base64 + case "family": + r.Family = &v[1] + case "manufacturer": + r.Manufacturer = &v[1] + case "product": + r.Product = &v[1] + case "serial": + r.Serial = &v[1] + case "sku": + r.SKU = &v[1] + case "uuid": + r.UUID = &v[1] + case "version": + r.Version = &v[1] + } + } + } + + return nil +} diff --git a/proxmox/nodes/vms/custom_spice_enhancements.go b/proxmox/nodes/vms/custom_spice_enhancements.go new file mode 100644 index 00000000..8b899d55 --- /dev/null +++ b/proxmox/nodes/vms/custom_spice_enhancements.go @@ -0,0 +1,72 @@ +/* + * 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 vms + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + + "github.com/bpg/terraform-provider-proxmox/proxmox/types" +) + +// CustomSpiceEnhancements handles QEMU spice enhancement parameters. +type CustomSpiceEnhancements struct { + FolderSharing *types.CustomBool `json:"foldersharing,omitempty" url:"foldersharing,omitempty"` + VideoStreaming *string `json:"videostreaming,omitempty" url:"videostreaming,omitempty"` +} + +// EncodeValues converts a CustomSpiceEnhancements struct to a URL value. +func (r *CustomSpiceEnhancements) EncodeValues(key string, v *url.Values) error { + var values []string + + if r.FolderSharing != nil { + if *r.FolderSharing { + values = append(values, "foldersharing=1") + } else { + values = append(values, "foldersharing=0") + } + } + + if r.VideoStreaming != nil { + values = append(values, fmt.Sprintf("videostreaming=%s", *r.VideoStreaming)) + } + + if len(values) > 0 { + v.Add(key, strings.Join(values, ",")) + } + + return nil +} + +// UnmarshalJSON converts JSON to a CustomSpiceEnhancements struct. +func (r *CustomSpiceEnhancements) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("failed to unmarshal CustomSpiceEnhancements: %w", err) + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + if len(v) == 2 { + switch v[0] { + case "foldersharing": + v := types.CustomBool(v[1] == "1") + r.FolderSharing = &v + case "videostreaming": + r.VideoStreaming = &v[1] + } + } + } + + return nil +} diff --git a/proxmox/nodes/vms/custom_startup_order.go b/proxmox/nodes/vms/custom_startup_order.go new file mode 100644 index 00000000..2c6c9456 --- /dev/null +++ b/proxmox/nodes/vms/custom_startup_order.go @@ -0,0 +1,88 @@ +/* + * 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 vms + +import ( + "encoding/json" + "fmt" + "net/url" + "strconv" + "strings" +) + +// CustomStartupOrder handles QEMU startup order parameters. +type CustomStartupOrder struct { + Down *int `json:"down,omitempty" url:"down,omitempty"` + Order *int `json:"order,omitempty" url:"order,omitempty"` + Up *int `json:"up,omitempty" url:"up,omitempty"` +} + +// EncodeValues converts a CustomStartupOrder struct to a URL value. +func (r *CustomStartupOrder) EncodeValues(key string, v *url.Values) error { + var values []string + + if r.Order != nil { + values = append(values, fmt.Sprintf("order=%d", *r.Order)) + } + + if r.Up != nil { + values = append(values, fmt.Sprintf("up=%d", *r.Up)) + } + + if r.Down != nil { + values = append(values, fmt.Sprintf("down=%d", *r.Down)) + } + + if len(values) > 0 { + v.Add(key, strings.Join(values, ",")) + } + + return nil +} + +// UnmarshalJSON converts a CustomStartupOrder string to an object. +func (r *CustomStartupOrder) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("failed to unmarshal CustomStartupOrder: %w", err) + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + if len(v) == 2 { + switch v[0] { + case "order": + order, err := strconv.Atoi(v[1]) + if err != nil { + return fmt.Errorf("failed to parse int: %w", err) + } + + r.Order = &order + case "up": + up, err := strconv.Atoi(v[1]) + if err != nil { + return fmt.Errorf("failed to parse int: %w", err) + } + + r.Up = &up + case "down": + down, err := strconv.Atoi(v[1]) + if err != nil { + return fmt.Errorf("failed to parse int: %w", err) + } + + r.Down = &down + } + } + } + + return nil +} diff --git a/proxmox/nodes/vms/customstoragedevice.go b/proxmox/nodes/vms/custom_storage_device.go similarity index 62% rename from proxmox/nodes/vms/customstoragedevice.go rename to proxmox/nodes/vms/custom_storage_device.go index cfe03134..957f5e8e 100644 --- a/proxmox/nodes/vms/customstoragedevice.go +++ b/proxmox/nodes/vms/custom_storage_device.go @@ -1,8 +1,18 @@ +/* + * 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 vms import ( + "encoding/json" "fmt" "net/url" + "path/filepath" + "reflect" + "strconv" "strings" "unicode" @@ -36,8 +46,11 @@ type CustomStorageDevice struct { Interface *string `json:"-" url:"-"` } +// CustomStorageDevices handles map of QEMU storage device per disk interface. +type CustomStorageDevices map[string]*CustomStorageDevice + // PathInDatastore returns path part of FileVolume or nil if it is not yet allocated. -func (d CustomStorageDevice) PathInDatastore() *string { +func (d *CustomStorageDevice) PathInDatastore() *string { probablyDatastoreID, pathInDatastore, hasDatastoreID := strings.Cut(d.FileVolume, ":") if !hasDatastoreID { // when no ':' separator is found, 'Cut' places the whole string to 'probablyDatastoreID', @@ -67,7 +80,7 @@ func (d CustomStorageDevice) PathInDatastore() *string { // IsOwnedBy returns true, if CustomStorageDevice is owned by given VM. // Not yet allocated volumes are not owned by any VM. -func (d CustomStorageDevice) IsOwnedBy(vmID int) bool { +func (d *CustomStorageDevice) IsOwnedBy(vmID int) bool { pathInDatastore := d.PathInDatastore() if pathInDatastore == nil { // not yet allocated volume, consider disk not owned by any VM @@ -89,14 +102,14 @@ func (d CustomStorageDevice) IsOwnedBy(vmID int) bool { } // IsCloudInitDrive returns true, if CustomStorageDevice is a cloud-init drive. -func (d CustomStorageDevice) IsCloudInitDrive(vmID int) bool { +func (d *CustomStorageDevice) IsCloudInitDrive(vmID int) bool { return d.Media != nil && *d.Media == "cdrom" && strings.Contains(d.FileVolume, fmt.Sprintf("vm-%d-cloudinit", vmID)) } // StorageInterface returns the storage interface of the CustomStorageDevice, // e.g. "virtio" or "scsi" for "virtio0" or "scsi2". -func (d CustomStorageDevice) StorageInterface() string { +func (d *CustomStorageDevice) StorageInterface() string { for i, r := range *d.Interface { if unicode.IsDigit(r) { return (*d.Interface)[:i] @@ -108,7 +121,7 @@ func (d CustomStorageDevice) StorageInterface() string { } // EncodeOptions converts a CustomStorageDevice's common options a URL value. -func (d CustomStorageDevice) EncodeOptions() string { +func (d *CustomStorageDevice) EncodeOptions() string { values := []string{} if d.AIO != nil { @@ -191,7 +204,7 @@ func (d CustomStorageDevice) EncodeOptions() string { } // EncodeValues converts a CustomStorageDevice struct to a URL value. -func (d CustomStorageDevice) EncodeValues(key string, v *url.Values) error { +func (d *CustomStorageDevice) EncodeValues(key string, v *url.Values) error { values := []string{ fmt.Sprintf("file=%s", d.FileVolume), } @@ -215,15 +228,157 @@ func (d CustomStorageDevice) EncodeValues(key string, v *url.Values) error { return nil } -// CustomStorageDevices handles map of QEMU storage device per disk interface. -type CustomStorageDevices map[string]*CustomStorageDevice +// UnmarshalJSON converts a CustomStorageDevice string to an object. +func (d *CustomStorageDevice) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("failed to unmarshal CustomStorageDevice: %w", err) + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + //nolint:nestif + if len(v) == 1 { + d.FileVolume = v[0] + + ext := filepath.Ext(v[0]) + if ext != "" { + format := string([]byte(ext)[1:]) + d.Format = &format + } + } else if len(v) == 2 { + switch v[0] { + case "aio": + d.AIO = &v[1] + + case "backup": + bv := types.CustomBool(v[1] == "1") + d.Backup = &bv + + case "cache": + d.Cache = &v[1] + + case "discard": + d.Discard = &v[1] + + case "file": + d.FileVolume = v[1] + + case "format": + d.Format = &v[1] + + case "iops_rd": + iv, err := strconv.Atoi(v[1]) + if err != nil { + return fmt.Errorf("failed to convert iops_rd to int: %w", err) + } + + d.IopsRead = &iv + + case "iops_rd_max": + iv, err := strconv.Atoi(v[1]) + if err != nil { + return fmt.Errorf("failed to convert iops_rd_max to int: %w", err) + } + + d.MaxIopsRead = &iv + + case "iops_wr": + iv, err := strconv.Atoi(v[1]) + if err != nil { + return fmt.Errorf("failed to convert iops_wr to int: %w", err) + } + + d.IopsWrite = &iv + + case "iops_wr_max": + iv, err := strconv.Atoi(v[1]) + if err != nil { + return fmt.Errorf("failed to convert iops_wr_max to int: %w", err) + } + + d.MaxIopsWrite = &iv + + case "iothread": + bv := types.CustomBool(v[1] == "1") + d.IOThread = &bv + + case "mbps_rd": + iv, err := strconv.Atoi(v[1]) + if err != nil { + return fmt.Errorf("failed to convert mbps_rd to int: %w", err) + } + + d.MaxReadSpeedMbps = &iv + + case "mbps_rd_max": + iv, err := strconv.Atoi(v[1]) + if err != nil { + return fmt.Errorf("failed to convert mbps_rd_max to int: %w", err) + } + + d.BurstableReadSpeedMbps = &iv + + case "mbps_wr": + iv, err := strconv.Atoi(v[1]) + if err != nil { + return fmt.Errorf("failed to convert mbps_wr to int: %w", err) + } + + d.MaxWriteSpeedMbps = &iv + + case "mbps_wr_max": + iv, err := strconv.Atoi(v[1]) + if err != nil { + return fmt.Errorf("failed to convert mbps_wr_max to int: %w", err) + } + + d.BurstableWriteSpeedMbps = &iv + + case "media": + d.Media = &v[1] + + case "replicate": + bv := types.CustomBool(v[1] == "1") + d.Replicate = &bv + + case "size": + d.Size = new(types.DiskSize) + + err := d.Size.UnmarshalJSON([]byte(v[1])) + if err != nil { + return fmt.Errorf("failed to unmarshal disk size: %w", err) + } + + case "ssd": + bv := types.CustomBool(v[1] == "1") + d.SSD = &bv + } + } + } + + d.Enabled = true + + return nil +} // ByStorageInterface returns a map of CustomStorageDevices filtered by the given storage interface. func (d CustomStorageDevices) ByStorageInterface(storageInterface string) CustomStorageDevices { + return d.Filter(func(d *CustomStorageDevice) bool { + return d.StorageInterface() == storageInterface + }) +} + +// Filter returns a map of CustomStorageDevices filtered by the given function. +func (d CustomStorageDevices) Filter(fn func(*CustomStorageDevice) bool) CustomStorageDevices { result := make(CustomStorageDevices) for k, v := range d { - if v.StorageInterface() == storageInterface { + if fn(v) { result[k] = v } } @@ -243,3 +398,27 @@ func (d CustomStorageDevices) EncodeValues(_ string, v *url.Values) error { return nil } + +// MapCustomStorageDevices maps the custom storage devices from the API response. +func MapCustomStorageDevices(resp GetResponseData) CustomStorageDevices { + csd := CustomStorageDevices{} + + mapDevice(csd, resp, "ide", "IDE", 3) + mapDevice(csd, resp, "sata", "SATA", 5) + mapDevice(csd, resp, "scsi", "SCSI", 13) + mapDevice(csd, resp, "virtio", "VirtualIO", 15) + + return csd +} + +func mapDevice(csd CustomStorageDevices, resp GetResponseData, keyPrefix, fieldPrefix string, end int) { + for i := 0; i <= end; i++ { + field := reflect.ValueOf(resp).FieldByName(fieldPrefix + "Device" + strconv.Itoa(i)) + if !field.IsZero() { + val := field.Interface() + if val != nil { + csd[keyPrefix+strconv.Itoa(i)] = val.(*CustomStorageDevice) + } + } + } +} diff --git a/proxmox/nodes/vms/vms_types_test.go b/proxmox/nodes/vms/custom_storage_device_test.go similarity index 60% rename from proxmox/nodes/vms/vms_types_test.go rename to proxmox/nodes/vms/custom_storage_device_test.go index d0b1a4c9..51a05c62 100644 --- a/proxmox/nodes/vms/vms_types_test.go +++ b/proxmox/nodes/vms/custom_storage_device_test.go @@ -171,7 +171,7 @@ func TestCustomStorageDevices_ByStorageInterface(t *testing.T) { want: CustomStorageDevices{}, }, { - name: "not in the list", + name: "nothing matches", iface: "sata", devices: CustomStorageDevices{ "virtio0": &CustomStorageDevice{ @@ -184,7 +184,7 @@ func TestCustomStorageDevices_ByStorageInterface(t *testing.T) { want: CustomStorageDevices{}, }, { - name: "not in the list", + name: "partially matches", iface: "virtio", devices: CustomStorageDevices{ "virtio0": &CustomStorageDevice{ @@ -218,45 +218,47 @@ func TestCustomStorageDevices_ByStorageInterface(t *testing.T) { } } -func TestCustomPCIDevice_UnmarshalJSON(t *testing.T) { +func TestMapCustomStorageDevices(t *testing.T) { t.Parallel() + type args struct { + resp GetResponseData + } + tests := []struct { - name string - line string - want *CustomPCIDevice - wantErr bool + name string + args args + want CustomStorageDevices }{ + {"no storage devices", args{GetResponseData{}}, CustomStorageDevices{}}, { - name: "id only pci device", - line: `"0000:81:00.2"`, - want: &CustomPCIDevice{ - DeviceIDs: &[]string{"0000:81:00.2"}, - }, + "ide0 storage devices", + args{GetResponseData{IDEDevice0: &CustomStorageDevice{}}}, + map[string]*CustomStorageDevice{"ide0": {}}, }, { - name: "pci device with more details", - line: `"host=81:00.4,pcie=0,rombar=1,x-vga=0"`, - want: &CustomPCIDevice{ - DeviceIDs: &[]string{"81:00.4"}, - MDev: nil, - PCIExpress: types.CustomBool(false).Pointer(), - ROMBAR: types.CustomBool(true).Pointer(), - ROMFile: nil, - XVGA: types.CustomBool(false).Pointer(), - }, + "multiple ide storage devices", + args{GetResponseData{ + IDEDevice1: &CustomStorageDevice{}, + IDEDevice3: &CustomStorageDevice{}, + }}, + map[string]*CustomStorageDevice{"ide1": {}, "ide3": {}}, }, { - name: "pci device with mapping", - line: `"mapping=mappeddevice,pcie=0,rombar=1,x-vga=0"`, - want: &CustomPCIDevice{ - DeviceIDs: nil, - Mapping: ptr.Ptr("mappeddevice"), - MDev: nil, - PCIExpress: types.CustomBool(false).Pointer(), - ROMBAR: types.CustomBool(true).Pointer(), - ROMFile: nil, - XVGA: types.CustomBool(false).Pointer(), + "mixed storage devices", + args{GetResponseData{ + IDEDevice1: &CustomStorageDevice{}, + VirtualIODevice5: &CustomStorageDevice{}, + SATADevice0: &CustomStorageDevice{}, + IDEDevice3: &CustomStorageDevice{}, + SCSIDevice10: &CustomStorageDevice{}, + }}, + map[string]*CustomStorageDevice{ + "ide1": {}, + "virtio5": {}, + "sata0": {}, + "ide3": {}, + "scsi10": {}, }, }, } @@ -265,100 +267,7 @@ func TestCustomPCIDevice_UnmarshalJSON(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - r := &CustomPCIDevice{} - if err := r.UnmarshalJSON([]byte(tt.line)); (err != nil) != tt.wantErr { - t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) - } - - require.Equal(t, tt.want, r) - }) - } -} - -func TestCustomNUMADevice_UnmarshalJSON(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - line string - want *CustomNUMADevice - wantErr bool - }{ - { - name: "numa device all options", - line: `"cpus=1-2;3-4,hostnodes=1-2,memory=1024,policy=preferred"`, - want: &CustomNUMADevice{ - CPUIDs: []string{"1-2", "3-4"}, - HostNodeNames: &[]string{"1-2"}, - Memory: ptr.Ptr(1024), - Policy: ptr.Ptr("preferred"), - }, - }, - { - name: "numa device cpus/memory only", - line: `"cpus=1-2,memory=1024"`, - want: &CustomNUMADevice{ - CPUIDs: []string{"1-2"}, - Memory: ptr.Ptr(1024), - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - r := &CustomNUMADevice{} - if err := r.UnmarshalJSON([]byte(tt.line)); (err != nil) != tt.wantErr { - t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestCustomUSBDevice_UnmarshalJSON(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - line string - want *CustomUSBDevice - wantErr bool - }{ - { - name: "id only usb device", - line: `"host=0000:81"`, - want: &CustomUSBDevice{ - HostDevice: ptr.Ptr("0000:81"), - }, - }, - { - name: "usb device with more details", - line: `"host=81:00,usb3=0"`, - want: &CustomUSBDevice{ - HostDevice: ptr.Ptr("81:00"), - USB3: types.CustomBool(false).Pointer(), - }, - }, - { - name: "usb device with mapping", - line: `"mapping=mappeddevice,usb=0"`, - want: &CustomUSBDevice{ - HostDevice: nil, - Mapping: ptr.Ptr("mappeddevice"), - USB3: types.CustomBool(false).Pointer(), - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - r := &CustomUSBDevice{} - if err := r.UnmarshalJSON([]byte(tt.line)); (err != nil) != tt.wantErr { - t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) - } + assert.Equalf(t, tt.want, MapCustomStorageDevices(tt.args.resp), "MapCustomStorageDevices(%v)", tt.args.resp) }) } } diff --git a/proxmox/nodes/vms/custom_tpm_state.go b/proxmox/nodes/vms/custom_tpm_state.go new file mode 100644 index 00000000..78f485b2 --- /dev/null +++ b/proxmox/nodes/vms/custom_tpm_state.go @@ -0,0 +1,62 @@ +/* + * 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 vms + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" +) + +// CustomTPMState handles QEMU TPM state parameters. +type CustomTPMState struct { + FileVolume string `json:"file" url:"file"` + Version *string `json:"version,omitempty" url:"version,omitempty"` +} + +// EncodeValues converts a CustomTPMState struct to a URL value. +func (r *CustomTPMState) EncodeValues(key string, v *url.Values) error { + values := []string{ + fmt.Sprintf("file=%s", r.FileVolume), + } + + if r.Version != nil { + values = append(values, fmt.Sprintf("version=%s", *r.Version)) + } + + v.Add(key, strings.Join(values, ",")) + + return nil +} + +// UnmarshalJSON converts a CustomTPMState string to an object. +func (r *CustomTPMState) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("failed to unmarshal CustomTPMState: %w", err) + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + if len(v) == 1 { + r.FileVolume = v[0] + } else if len(v) == 2 { + switch v[0] { + case "file": + r.FileVolume = v[1] + case "version": + r.Version = &v[1] + } + } + } + + return nil +} diff --git a/proxmox/nodes/vms/custom_usb_device.go b/proxmox/nodes/vms/custom_usb_device.go new file mode 100644 index 00000000..28605f8f --- /dev/null +++ b/proxmox/nodes/vms/custom_usb_device.go @@ -0,0 +1,95 @@ +/* + * 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 vms + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + + "github.com/bpg/terraform-provider-proxmox/proxmox/types" +) + +// CustomUSBDevice handles QEMU USB device parameters. +type CustomUSBDevice struct { + HostDevice *string `json:"host" url:"host"` + Mapping *string `json:"mapping,omitempty" url:"mapping,omitempty"` + USB3 *types.CustomBool `json:"usb3,omitempty" url:"usb3,omitempty,int"` +} + +// CustomUSBDevices handles QEMU USB device parameters. +type CustomUSBDevices []CustomUSBDevice + +// EncodeValues converts a CustomUSBDevice struct to a URL value. +func (r *CustomUSBDevice) EncodeValues(key string, v *url.Values) error { + if r.HostDevice == nil && r.Mapping == nil { + return fmt.Errorf("either device ID or resource mapping must be set") + } + + values := []string{} + if r.HostDevice != nil { + values = append(values, fmt.Sprintf("host=%s", *(r.HostDevice))) + } + + if r.Mapping != nil { + values = append(values, fmt.Sprintf("mapping=%s", *r.Mapping)) + } + + if r.USB3 != nil { + if *r.USB3 { + values = append(values, "usb3=1") + } else { + values = append(values, "usb3=0") + } + } + + v.Add(key, strings.Join(values, ",")) + + return nil +} + +// EncodeValues converts a CustomUSBDevices array to multiple URL values. +func (r CustomUSBDevices) EncodeValues(key string, v *url.Values) error { + for i, d := range r { + if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil { + return fmt.Errorf("error encoding USB device %d: %w", i, err) + } + } + + return nil +} + +// UnmarshalJSON converts a CustomUSBDevice string to an object. +func (r *CustomUSBDevice) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("failed to unmarshal CustomUSBDevice: %w", err) + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + if len(v) == 1 { + r.HostDevice = &v[1] + } else if len(v) == 2 { + switch v[0] { + case "host": + r.HostDevice = &v[1] + case "mapping": + r.Mapping = &v[1] + case "usb3": + bv := types.CustomBool(v[1] == "1") + r.USB3 = &bv + } + } + } + + return nil +} diff --git a/proxmox/nodes/vms/custom_usb_device_test.go b/proxmox/nodes/vms/custom_usb_device_test.go new file mode 100644 index 00000000..d1629ba9 --- /dev/null +++ b/proxmox/nodes/vms/custom_usb_device_test.go @@ -0,0 +1,61 @@ +/* + * 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 vms + +import ( + "testing" + + "github.com/bpg/terraform-provider-proxmox/proxmox/helpers/ptr" + "github.com/bpg/terraform-provider-proxmox/proxmox/types" +) + +func TestCustomUSBDevice_UnmarshalJSON(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + line string + want *CustomUSBDevice + wantErr bool + }{ + { + name: "id only usb device", + line: `"host=0000:81"`, + want: &CustomUSBDevice{ + HostDevice: ptr.Ptr("0000:81"), + }, + }, + { + name: "usb device with more details", + line: `"host=81:00,usb3=0"`, + want: &CustomUSBDevice{ + HostDevice: ptr.Ptr("81:00"), + USB3: types.CustomBool(false).Pointer(), + }, + }, + { + name: "usb device with mapping", + line: `"mapping=mappeddevice,usb=0"`, + want: &CustomUSBDevice{ + HostDevice: nil, + Mapping: ptr.Ptr("mappeddevice"), + USB3: types.CustomBool(false).Pointer(), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + r := &CustomUSBDevice{} + if err := r.UnmarshalJSON([]byte(tt.line)); (err != nil) != tt.wantErr { + t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/proxmox/nodes/vms/custom_vga_device.go b/proxmox/nodes/vms/custom_vga_device.go new file mode 100644 index 00000000..2e11ab72 --- /dev/null +++ b/proxmox/nodes/vms/custom_vga_device.go @@ -0,0 +1,83 @@ +/* + * 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 vms + +import ( + "encoding/json" + "fmt" + "net/url" + "strconv" + "strings" +) + +// CustomVGADevice handles QEMU VGA device parameters. +type CustomVGADevice struct { + Clipboard *string `json:"clipboard,omitempty" url:"memory,omitempty"` + Memory *int64 `json:"memory,omitempty" url:"memory,omitempty"` + Type *string `json:"type,omitempty" url:"type,omitempty"` +} + +// EncodeValues converts a CustomVGADevice struct to a URL value. +func (r CustomVGADevice) EncodeValues(key string, v *url.Values) error { + var values []string + + if r.Clipboard != nil { + values = append(values, fmt.Sprintf("clipboard=%s", *r.Clipboard)) + } + + if r.Memory != nil { + values = append(values, fmt.Sprintf("memory=%d", *r.Memory)) + } + + if r.Type != nil { + values = append(values, fmt.Sprintf("type=%s", *r.Type)) + } + + v.Add(key, strings.Join(values, ",")) + + return nil +} + +// UnmarshalJSON converts a CustomVGADevice string to an object. +func (r *CustomVGADevice) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("failed to unmarshal CustomVGADevice: %w", err) + } + + if s == "" { + return nil + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + if len(v) == 1 { + r.Type = &v[0] + } else if len(v) == 2 { + switch v[0] { + case "clipboard": + r.Clipboard = &v[1] + + case "memory": + m, err := strconv.ParseInt(v[1], 10, 64) + if err != nil { + return fmt.Errorf("failed to convert memory to int: %w", err) + } + + r.Memory = &m + case "type": + r.Type = &v[1] + } + } + } + + return nil +} diff --git a/proxmox/nodes/vms/custom_virtualio_device.go b/proxmox/nodes/vms/custom_virtualio_device.go new file mode 100644 index 00000000..93753677 --- /dev/null +++ b/proxmox/nodes/vms/custom_virtualio_device.go @@ -0,0 +1,62 @@ +/* + * 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 vms + +import ( + "fmt" + "net/url" + "strings" + + "github.com/bpg/terraform-provider-proxmox/proxmox/types" +) + +// CustomVirtualIODevice handles QEMU VirtIO device parameters. +type CustomVirtualIODevice struct { + AIO *string `json:"aio,omitempty" url:"aio,omitempty"` + BackupEnabled *types.CustomBool `json:"backup,omitempty" url:"backup,omitempty,int"` + Enabled bool `json:"-" url:"-"` + FileVolume string `json:"file" url:"file"` +} + +// CustomVirtualIODevices handles QEMU VirtIO device parameters. +type CustomVirtualIODevices []CustomVirtualIODevice + +// EncodeValues converts a CustomVirtualIODevice struct to a URL value. +func (r CustomVirtualIODevice) EncodeValues(key string, v *url.Values) error { + values := []string{ + fmt.Sprintf("file=%s", r.FileVolume), + } + + if r.AIO != nil { + values = append(values, fmt.Sprintf("aio=%s", *r.AIO)) + } + + if r.BackupEnabled != nil { + if *r.BackupEnabled { + values = append(values, "backup=1") + } else { + values = append(values, "backup=0") + } + } + + v.Add(key, strings.Join(values, ",")) + + return nil +} + +// EncodeValues converts a CustomVirtualIODevices array to multiple URL values. +func (r CustomVirtualIODevices) EncodeValues(key string, v *url.Values) error { + for i, d := range r { + if d.Enabled { + if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil { + return fmt.Errorf("error encoding virtual IO device %d: %w", i, err) + } + } + } + + return nil +} diff --git a/proxmox/nodes/vms/custom_watchdog_device.go b/proxmox/nodes/vms/custom_watchdog_device.go new file mode 100644 index 00000000..b1b5071a --- /dev/null +++ b/proxmox/nodes/vms/custom_watchdog_device.go @@ -0,0 +1,67 @@ +/* + * 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 vms + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" +) + +// CustomWatchdogDevice handles QEMU watchdog device parameters. +type CustomWatchdogDevice struct { + Action *string `json:"action,omitempty" url:"action,omitempty"` + Model *string `json:"model" url:"model"` +} + +// EncodeValues converts a CustomWatchdogDevice struct to a URL value. +func (r *CustomWatchdogDevice) EncodeValues(key string, v *url.Values) error { + values := []string{ + fmt.Sprintf("model=%+v", r.Model), + } + + if r.Action != nil { + values = append(values, fmt.Sprintf("action=%s", *r.Action)) + } + + v.Add(key, strings.Join(values, ",")) + + return nil +} + +// UnmarshalJSON converts a CustomWatchdogDevice string to an object. +func (r *CustomWatchdogDevice) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("failed to unmarshal CustomWatchdogDevice: %w", err) + } + + if s == "" { + return nil + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + if len(v) == 1 { + r.Model = &v[0] + } else if len(v) == 2 { + switch v[0] { + case "action": + r.Action = &v[1] + case "model": + r.Model = &v[1] + } + } + } + + return nil +} diff --git a/proxmox/nodes/vms/vms_types.go b/proxmox/nodes/vms/vms_types.go index b908c9c1..0bc3e2ba 100644 --- a/proxmox/nodes/vms/vms_types.go +++ b/proxmox/nodes/vms/vms_types.go @@ -7,206 +7,14 @@ package vms import ( - "encoding/json" "errors" "fmt" - "net/url" - "path/filepath" "reflect" - "strconv" "strings" "github.com/bpg/terraform-provider-proxmox/proxmox/types" ) -// CustomAgent handles QEMU agent parameters. -type CustomAgent struct { - Enabled *types.CustomBool `json:"enabled,omitempty" url:"enabled,int"` - TrimClonedDisks *types.CustomBool `json:"fstrim_cloned_disks" url:"fstrim_cloned_disks,int"` - Type *string `json:"type" url:"type"` -} - -// CustomAudioDevice handles QEMU audio parameters. -type CustomAudioDevice struct { - Device string `json:"device" url:"device"` - Driver *string `json:"driver" url:"driver"` - Enabled bool `json:"-" url:"-"` -} - -// CustomAudioDevices handles QEMU audio device parameters. -type CustomAudioDevices []CustomAudioDevice - -// CustomBoot handles QEMU boot parameters. -type CustomBoot struct { - Order *[]string `json:"order,omitempty" url:"order,omitempty,semicolon"` -} - -// CustomCloudInitConfig handles QEMU cloud-init parameters. -type CustomCloudInitConfig struct { - Files *CustomCloudInitFiles `json:"cicustom,omitempty" url:"cicustom,omitempty"` - IPConfig []CustomCloudInitIPConfig `json:"ipconfig,omitempty" url:"ipconfig,omitempty,numbered"` - Nameserver *string `json:"nameserver,omitempty" url:"nameserver,omitempty"` - Password *string `json:"cipassword,omitempty" url:"cipassword,omitempty"` - SearchDomain *string `json:"searchdomain,omitempty" url:"searchdomain,omitempty"` - SSHKeys *CustomCloudInitSSHKeys `json:"sshkeys,omitempty" url:"sshkeys,omitempty"` - Type *string `json:"citype,omitempty" url:"citype,omitempty"` - // Can't be reliably set, it is TRUE by default in PVE - // Upgrade *types.CustomBool `json:"ciupgrade,omitempty" url:"ciupgrade,omitempty,int"` - Username *string `json:"ciuser,omitempty" url:"ciuser,omitempty"` -} - -// CustomCloudInitFiles handles QEMU cloud-init custom files parameters. -type CustomCloudInitFiles struct { - MetaVolume *string `json:"meta,omitempty" url:"meta,omitempty"` - NetworkVolume *string `json:"network,omitempty" url:"network,omitempty"` - UserVolume *string `json:"user,omitempty" url:"user,omitempty"` - VendorVolume *string `json:"vendor,omitempty" url:"vendor,omitempty"` -} - -// CustomCloudInitIPConfig handles QEMU cloud-init IP configuration parameters. -type CustomCloudInitIPConfig struct { - GatewayIPv4 *string `json:"gw,omitempty" url:"gw,omitempty"` - GatewayIPv6 *string `json:"gw6,omitempty" url:"gw6,omitempty"` - IPv4 *string `json:"ip,omitempty" url:"ip,omitempty"` - IPv6 *string `json:"ip6,omitempty" url:"ip6,omitempty"` -} - -// CustomCloudInitSSHKeys handles QEMU cloud-init SSH keys parameters. -type CustomCloudInitSSHKeys []string - -// CustomCPUEmulation handles QEMU CPU emulation parameters. -type CustomCPUEmulation struct { - Flags *[]string `json:"flags,omitempty" url:"flags,omitempty,semicolon"` - Hidden *types.CustomBool `json:"hidden,omitempty" url:"hidden,omitempty,int"` - HVVendorID *string `json:"hv-vendor-id,omitempty" url:"hv-vendor-id,omitempty"` - Type string `json:"cputype,omitempty" url:"cputype,omitempty"` -} - -// CustomEFIDisk handles QEMU EFI disk parameters. -type CustomEFIDisk struct { - FileVolume string `json:"file" url:"file"` - Format *string `json:"format,omitempty" url:"format,omitempty"` - Type *string `json:"efitype,omitempty" url:"efitype,omitempty"` - PreEnrolledKeys *types.CustomBool `json:"pre-enrolled-keys,omitempty" url:"pre-enrolled-keys,omitempty,int"` -} - -// CustomNetworkDevice handles QEMU network device parameters. -type CustomNetworkDevice struct { - Enabled bool `json:"-" url:"-"` - Bridge *string `json:"bridge,omitempty" url:"bridge,omitempty"` - Firewall *types.CustomBool `json:"firewall,omitempty" url:"firewall,omitempty,int"` - LinkDown *types.CustomBool `json:"link_down,omitempty" url:"link_down,omitempty,int"` - MACAddress *string `json:"macaddr,omitempty" url:"macaddr,omitempty"` - MTU *int `json:"mtu,omitempty" url:"mtu,omitempty"` - Model string `json:"model" url:"model"` - Queues *int `json:"queues,omitempty" url:"queues,omitempty"` - RateLimit *float64 `json:"rate,omitempty" url:"rate,omitempty"` - Tag *int `json:"tag,omitempty" url:"tag,omitempty"` - Trunks []int `json:"trunks,omitempty" url:"trunks,omitempty"` -} - -// CustomNetworkDevices handles QEMU network device parameters. -type CustomNetworkDevices []CustomNetworkDevice - -// CustomNUMADevice handles QEMU NUMA device parameters. -type CustomNUMADevice struct { - CPUIDs []string `json:"cpus" url:"cpus,semicolon"` - HostNodeNames *[]string `json:"hostnodes,omitempty" url:"hostnodes,omitempty,semicolon"` - Memory *int `json:"memory,omitempty" url:"memory,omitempty"` - Policy *string `json:"policy,omitempty" url:"policy,omitempty"` -} - -// CustomNUMADevices handles QEMU NUMA device parameters. -type CustomNUMADevices []CustomNUMADevice - -// CustomPCIDevice handles QEMU host PCI device mapping parameters. -type CustomPCIDevice struct { - DeviceIDs *[]string `json:"host,omitempty" url:"host,omitempty,semicolon"` - Mapping *string `json:"mapping,omitempty" url:"mapping,omitempty"` - MDev *string `json:"mdev,omitempty" url:"mdev,omitempty"` - PCIExpress *types.CustomBool `json:"pcie,omitempty" url:"pcie,omitempty,int"` - ROMBAR *types.CustomBool `json:"rombar,omitempty" url:"rombar,omitempty,int"` - ROMFile *string `json:"romfile,omitempty" url:"romfile,omitempty"` - XVGA *types.CustomBool `json:"x-vga,omitempty" url:"x-vga,omitempty,int"` -} - -// CustomPCIDevices handles QEMU host PCI device mapping parameters. -type CustomPCIDevices []CustomPCIDevice - -// CustomSerialDevices handles QEMU serial device parameters. -type CustomSerialDevices []string - -// CustomSharedMemory handles QEMU Inter-VM shared memory parameters. -type CustomSharedMemory struct { - Name *string `json:"name,omitempty" url:"name,omitempty"` - Size int `json:"size" url:"size"` -} - -// CustomSMBIOS handles QEMU SMBIOS parameters. -type CustomSMBIOS struct { - Base64 *types.CustomBool `json:"base64,omitempty" url:"base64,omitempty,int"` - Family *string `json:"family,omitempty" url:"family,omitempty"` - Manufacturer *string `json:"manufacturer,omitempty" url:"manufacturer,omitempty"` - Product *string `json:"product,omitempty" url:"product,omitempty"` - Serial *string `json:"serial,omitempty" url:"serial,omitempty"` - SKU *string `json:"sku,omitempty" url:"sku,omitempty"` - UUID *string `json:"uuid,omitempty" url:"uuid,omitempty"` - Version *string `json:"version,omitempty" url:"version,omitempty"` -} - -// CustomSpiceEnhancements handles QEMU spice enhancement parameters. -type CustomSpiceEnhancements struct { - FolderSharing *types.CustomBool `json:"foldersharing,omitempty" url:"foldersharing,omitempty"` - VideoStreaming *string `json:"videostreaming,omitempty" url:"videostreaming,omitempty"` -} - -// CustomStartupOrder handles QEMU startup order parameters. -type CustomStartupOrder struct { - Down *int `json:"down,omitempty" url:"down,omitempty"` - Order *int `json:"order,omitempty" url:"order,omitempty"` - Up *int `json:"up,omitempty" url:"up,omitempty"` -} - -// CustomTPMState handles QEMU TPM state parameters. -type CustomTPMState struct { - FileVolume string `json:"file" url:"file"` - Version *string `json:"version,omitempty" url:"version,omitempty"` -} - -// CustomUSBDevice handles QEMU USB device parameters. -type CustomUSBDevice struct { - HostDevice *string `json:"host" url:"host"` - Mapping *string `json:"mapping,omitempty" url:"mapping,omitempty"` - USB3 *types.CustomBool `json:"usb3,omitempty" url:"usb3,omitempty,int"` -} - -// CustomUSBDevices handles QEMU USB device parameters. -type CustomUSBDevices []CustomUSBDevice - -// CustomVGADevice handles QEMU VGA device parameters. -type CustomVGADevice struct { - Clipboard *string `json:"clipboard,omitempty" url:"memory,omitempty"` - Memory *int64 `json:"memory,omitempty" url:"memory,omitempty"` - Type *string `json:"type,omitempty" url:"type,omitempty"` -} - -// CustomVirtualIODevice handles QEMU VirtIO device parameters. -type CustomVirtualIODevice struct { - AIO *string `json:"aio,omitempty" url:"aio,omitempty"` - BackupEnabled *types.CustomBool `json:"backup,omitempty" url:"backup,omitempty,int"` - Enabled bool `json:"-" url:"-"` - FileVolume string `json:"file" url:"file"` -} - -// CustomVirtualIODevices handles QEMU VirtIO device parameters. -type CustomVirtualIODevices []CustomVirtualIODevice - -// CustomWatchdogDevice handles QEMU watchdog device parameters. -type CustomWatchdogDevice struct { - Action *string `json:"action,omitempty" url:"action,omitempty"` - Model *string `json:"model" url:"model"` -} - // CloneRequestBody contains the data for an virtual machine clone request. type CloneRequestBody struct { BandwidthLimit *int `json:"bwlimit,omitempty" url:"bwlimit,omitempty"` @@ -297,6 +105,46 @@ type CreateRequestBody struct { WatchdogDevice *CustomWatchdogDevice `json:"watchdog,omitempty" url:"watchdog,omitempty"` } +// AddCustomStorageDevice adds a custom storage device to the create request body. +func (b *CreateRequestBody) AddCustomStorageDevice(device CustomStorageDevice) error { + device.Enabled = true + + switch device.StorageInterface() { + case "ide": + if b.IDEDevices == nil { + b.IDEDevices = make(CustomStorageDevices, 1) + } + + b.IDEDevices[*device.Interface] = &device + + case "sata": + if b.SATADevices == nil { + b.SATADevices = make(CustomStorageDevices, 1) + } + + b.SATADevices[*device.Interface] = &device + + case "scsi": + if b.SCSIDevices == nil { + b.SCSIDevices = make(CustomStorageDevices, 1) + } + + b.SCSIDevices[*device.Interface] = &device + + case "virtio": + if b.VirtualIODevices == nil { + b.VirtualIODevices = make(CustomStorageDevices, 1) + } + + b.VirtualIODevices[*device.Interface] = &device + + default: + return fmt.Errorf("unsupported storage interface: %s", device.StorageInterface()) + } + + return nil +} + // CreateResponseBody contains the body from a create response. type CreateResponseBody struct { TaskID *string `json:"data,omitempty"` @@ -480,7 +328,7 @@ type GetResponseData struct { PCIDevice1 *CustomPCIDevice `json:"hostpci1,omitempty"` PCIDevice2 *CustomPCIDevice `json:"hostpci2,omitempty"` PCIDevice3 *CustomPCIDevice `json:"hostpci3,omitempty"` - PoolID *string `json:"pool,omitempty" url:"pool,omitempty"` + PoolID *string `json:"pool,omitempty"` Revert *string `json:"revert,omitempty"` SATADevice0 *CustomStorageDevice `json:"sata0,omitempty"` SATADevice1 *CustomStorageDevice `json:"sata1,omitempty"` @@ -668,1429 +516,30 @@ type UpdateAsyncResponseBody struct { } // UpdateRequestBody contains the data for an virtual machine update request. -type UpdateRequestBody CreateRequestBody +type UpdateRequestBody = CreateRequestBody // ToDelete adds a field to the delete list. The field name should be the **actual** field name in the struct. -func (u *UpdateRequestBody) ToDelete(fieldName string) error { - if u == nil { +func (b *UpdateRequestBody) ToDelete(fieldName string) error { + if b == nil { return errors.New("update request body is nil") } - if field, ok := reflect.TypeOf(*u).FieldByName(fieldName); ok { + if field, ok := reflect.TypeOf(*b).FieldByName(fieldName); ok { fieldTag := field.Tag.Get("url") name := strings.Split(fieldTag, ",")[0] - u.Delete = append(u.Delete, name) + b.Delete = append(b.Delete, name) } else { - return fmt.Errorf("field %s not found in struct %s", fieldName, reflect.TypeOf(u).Name()) + return fmt.Errorf("field %s not found in struct %s", fieldName, reflect.TypeOf(b).Name()) } return nil } // IsEmpty checks if the update request body is empty. -func (u *UpdateRequestBody) IsEmpty() bool { - if u == nil { +func (b *UpdateRequestBody) IsEmpty() bool { + if b == nil { return true } - return reflect.DeepEqual(*u, UpdateRequestBody{}) -} - -// EncodeValues converts a CustomAgent struct to a URL value. -func (r CustomAgent) EncodeValues(key string, v *url.Values) error { - var values []string - - if r.Enabled != nil { - if *r.Enabled { - values = append(values, "enabled=1") - } else { - values = append(values, "enabled=0") - } - } - - if r.TrimClonedDisks != nil { - if *r.TrimClonedDisks { - values = append(values, "fstrim_cloned_disks=1") - } else { - values = append(values, "fstrim_cloned_disks=0") - } - } - - 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 -} - -// EncodeValues converts a CustomAudioDevice struct to a URL value. -func (r CustomAudioDevice) EncodeValues(key string, v *url.Values) error { - values := []string{fmt.Sprintf("device=%s", r.Device)} - - if r.Driver != nil { - values = append(values, fmt.Sprintf("driver=%s", *r.Driver)) - } - - v.Add(key, strings.Join(values, ",")) - - return nil -} - -// EncodeValues converts a CustomAudioDevices array to multiple URL values. -func (r CustomAudioDevices) EncodeValues(key string, v *url.Values) error { - for i, d := range r { - if d.Enabled { - if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil { - return fmt.Errorf("unable to encode audio device %d: %w", i, err) - } - } - } - - return nil -} - -// EncodeValues converts a CustomBoot struct to multiple URL values. -func (r CustomBoot) EncodeValues(key string, v *url.Values) error { - if r.Order != nil && len(*r.Order) > 0 { - v.Add(key, fmt.Sprintf("order=%s", strings.Join(*r.Order, ";"))) - } - - return nil -} - -// EncodeValues converts a CustomCloudInitConfig struct to multiple URL values. -func (r CustomCloudInitConfig) EncodeValues(_ string, v *url.Values) error { - //nolint:nestif - if r.Files != nil { - var volumes []string - - if r.Files.MetaVolume != nil { - volumes = append(volumes, fmt.Sprintf("meta=%s", *r.Files.MetaVolume)) - } - - if r.Files.NetworkVolume != nil { - volumes = append(volumes, fmt.Sprintf("network=%s", *r.Files.NetworkVolume)) - } - - if r.Files.UserVolume != nil { - volumes = append(volumes, fmt.Sprintf("user=%s", *r.Files.UserVolume)) - } - - if r.Files.VendorVolume != nil { - volumes = append(volumes, fmt.Sprintf("vendor=%s", *r.Files.VendorVolume)) - } - - if len(volumes) > 0 { - v.Add("cicustom", strings.Join(volumes, ",")) - } - } - - for i, c := range r.IPConfig { - var config []string - - if c.GatewayIPv4 != nil { - config = append(config, fmt.Sprintf("gw=%s", *c.GatewayIPv4)) - } - - if c.GatewayIPv6 != nil { - config = append(config, fmt.Sprintf("gw6=%s", *c.GatewayIPv6)) - } - - if c.IPv4 != nil { - config = append(config, fmt.Sprintf("ip=%s", *c.IPv4)) - } - - if c.IPv6 != nil { - config = append(config, fmt.Sprintf("ip6=%s", *c.IPv6)) - } - - if len(config) > 0 { - v.Add(fmt.Sprintf("ipconfig%d", i), strings.Join(config, ",")) - } - } - - if r.Nameserver != nil { - v.Add("nameserver", *r.Nameserver) - } - - if r.Password != nil { - v.Add("cipassword", *r.Password) - } - - if r.SearchDomain != nil { - v.Add("searchdomain", *r.SearchDomain) - } - - if r.SSHKeys != nil { - v.Add( - "sshkeys", - strings.ReplaceAll(url.QueryEscape(strings.Join(*r.SSHKeys, "\n")), "+", "%20"), - ) - } - - if r.Type != nil { - v.Add("citype", *r.Type) - } - - if r.Username != nil { - v.Add("ciuser", *r.Username) - } - - return nil -} - -// EncodeValues converts a CustomCPUEmulation struct to a URL value. -func (r CustomCPUEmulation) EncodeValues(key string, v *url.Values) error { - values := []string{ - fmt.Sprintf("cputype=%s", r.Type), - } - - if r.Flags != nil && len(*r.Flags) > 0 { - values = append(values, fmt.Sprintf("flags=%s", strings.Join(*r.Flags, ";"))) - } - - if r.Hidden != nil { - if *r.Hidden { - values = append(values, "hidden=1") - } else { - values = append(values, "hidden=0") - } - } - - if r.HVVendorID != nil { - values = append(values, fmt.Sprintf("hv-vendor-id=%s", *r.HVVendorID)) - } - - v.Add(key, strings.Join(values, ",")) - - return nil -} - -// EncodeValues converts a CustomEFIDisk struct to a URL value. -func (r CustomEFIDisk) EncodeValues(key string, v *url.Values) error { - values := []string{ - fmt.Sprintf("file=%s", r.FileVolume), - } - - if r.Format != nil { - values = append(values, fmt.Sprintf("format=%s", *r.Format)) - } - - if r.Type != nil { - values = append(values, fmt.Sprintf("efitype=%s", *r.Type)) - } - - if r.PreEnrolledKeys != nil { - if *r.PreEnrolledKeys { - values = append(values, "pre-enrolled-keys=1") - } else { - values = append(values, "pre-enrolled-keys=0") - } - } - - v.Add(key, strings.Join(values, ",")) - - return nil -} - -// EncodeValues converts a CustomNetworkDevice struct to a URL value. -func (r CustomNetworkDevice) EncodeValues(key string, v *url.Values) error { - values := []string{ - fmt.Sprintf("model=%s", r.Model), - } - - if r.Bridge != nil { - values = append(values, fmt.Sprintf("bridge=%s", *r.Bridge)) - } - - if r.Firewall != nil { - if *r.Firewall { - values = append(values, "firewall=1") - } else { - values = append(values, "firewall=0") - } - } - - if r.LinkDown != nil { - if *r.LinkDown { - values = append(values, "link_down=1") - } else { - values = append(values, "link_down=0") - } - } - - if r.MACAddress != nil { - values = append(values, fmt.Sprintf("macaddr=%s", *r.MACAddress)) - } - - if r.Queues != nil { - values = append(values, fmt.Sprintf("queues=%d", *r.Queues)) - } - - if r.RateLimit != nil { - values = append(values, fmt.Sprintf("rate=%f", *r.RateLimit)) - } - - if r.Tag != nil { - values = append(values, fmt.Sprintf("tag=%d", *r.Tag)) - } - - if r.MTU != nil { - values = append(values, fmt.Sprintf("mtu=%d", *r.MTU)) - } - - if len(r.Trunks) > 0 { - trunks := make([]string, len(r.Trunks)) - - for i, v := range r.Trunks { - trunks[i] = strconv.Itoa(v) - } - - values = append(values, fmt.Sprintf("trunks=%s", strings.Join(trunks, ";"))) - } - - v.Add(key, strings.Join(values, ",")) - - return nil -} - -// EncodeValues converts a CustomNetworkDevices array to multiple URL values. -func (r CustomNetworkDevices) EncodeValues(key string, v *url.Values) error { - for i, d := range r { - if d.Enabled { - if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil { - return fmt.Errorf("failed to encode network device %d: %w", i, err) - } - } - } - - return nil -} - -// EncodeValues converts a CustomNUMADevice struct to a URL value. -func (r CustomNUMADevice) EncodeValues(key string, v *url.Values) error { - values := []string{ - fmt.Sprintf("cpus=%s", strings.Join(r.CPUIDs, ";")), - } - - if r.HostNodeNames != nil { - values = append(values, fmt.Sprintf("hostnodes=%s", strings.Join(*r.HostNodeNames, ";"))) - } - - if r.Memory != nil { - values = append(values, fmt.Sprintf("memory=%d", *r.Memory)) - } - - if r.Policy != nil { - values = append(values, fmt.Sprintf("policy=%s", *r.Policy)) - } - - v.Add(key, strings.Join(values, ",")) - - return nil -} - -// EncodeValues converts a CustomNUMADevices array to multiple URL values. -func (r CustomNUMADevices) EncodeValues(key string, v *url.Values) error { - for i, d := range r { - if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil { - return fmt.Errorf("failed to encode NUMA device %d: %w", i, err) - } - } - - return nil -} - -// EncodeValues converts a CustomPCIDevice struct to a URL value. -func (r CustomPCIDevice) EncodeValues(key string, v *url.Values) error { - values := []string{} - - if r.DeviceIDs == nil && r.Mapping == nil { - return fmt.Errorf("either device ID or resource mapping must be set") - } - - if r.DeviceIDs != nil { - values = append(values, fmt.Sprintf("host=%s", strings.Join(*r.DeviceIDs, ";"))) - } - - if r.Mapping != nil { - values = append(values, fmt.Sprintf("mapping=%s", *r.Mapping)) - } - - if r.MDev != nil { - values = append(values, fmt.Sprintf("mdev=%s", *r.MDev)) - } - - if r.PCIExpress != nil { - if *r.PCIExpress { - values = append(values, "pcie=1") - } else { - values = append(values, "pcie=0") - } - } - - if r.ROMBAR != nil { - if *r.ROMBAR { - values = append(values, "rombar=1") - } else { - values = append(values, "rombar=0") - } - } - - if r.ROMFile != nil { - values = append(values, fmt.Sprintf("romfile=%s", *r.ROMFile)) - } - - if r.XVGA != nil { - if *r.XVGA { - values = append(values, "x-vga=1") - } else { - values = append(values, "x-vga=0") - } - } - - v.Add(key, strings.Join(values, ",")) - - return nil -} - -// EncodeValues converts a CustomPCIDevices array to multiple URL values. -func (r CustomPCIDevices) EncodeValues(key string, v *url.Values) error { - for i, d := range r { - if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil { - return fmt.Errorf("failed to encode PCI device %d: %w", i, err) - } - } - - return nil -} - -// EncodeValues converts a CustomSerialDevices array to multiple URL values. -func (r CustomSerialDevices) EncodeValues(key string, v *url.Values) error { - for i, d := range r { - v.Add(fmt.Sprintf("%s%d", key, i), d) - } - - return nil -} - -// EncodeValues converts a CustomSharedMemory struct to a URL value. -func (r CustomSharedMemory) EncodeValues(key string, v *url.Values) error { - values := []string{ - fmt.Sprintf("size=%d", r.Size), - } - - if r.Name != nil { - values = append(values, fmt.Sprintf("name=%s", *r.Name)) - } - - v.Add(key, strings.Join(values, ",")) - - return nil -} - -// EncodeValues converts a CustomSMBIOS struct to a URL value. -func (r CustomSMBIOS) EncodeValues(key string, v *url.Values) error { - var values []string - - if r.Base64 != nil { - if *r.Base64 { - values = append(values, "base64=1") - } else { - values = append(values, "base64=0") - } - } - - if r.Family != nil { - values = append(values, fmt.Sprintf("family=%s", *r.Family)) - } - - if r.Manufacturer != nil { - values = append(values, fmt.Sprintf("manufacturer=%s", *r.Manufacturer)) - } - - if r.Product != nil { - values = append(values, fmt.Sprintf("product=%s", *r.Product)) - } - - if r.Serial != nil { - values = append(values, fmt.Sprintf("serial=%s", *r.Serial)) - } - - if r.SKU != nil { - values = append(values, fmt.Sprintf("sku=%s", *r.SKU)) - } - - if r.UUID != nil { - values = append(values, fmt.Sprintf("uuid=%s", *r.UUID)) - } - - if r.Version != nil { - values = append(values, fmt.Sprintf("version=%s", *r.Version)) - } - - if len(values) > 0 { - v.Add(key, strings.Join(values, ",")) - } - - return nil -} - -// EncodeValues converts a CustomSpiceEnhancements struct to a URL value. -func (r CustomSpiceEnhancements) EncodeValues(key string, v *url.Values) error { - var values []string - - if r.FolderSharing != nil { - if *r.FolderSharing { - values = append(values, "foldersharing=1") - } else { - values = append(values, "foldersharing=0") - } - } - - if r.VideoStreaming != nil { - values = append(values, fmt.Sprintf("videostreaming=%s", *r.VideoStreaming)) - } - - if len(values) > 0 { - v.Add(key, strings.Join(values, ",")) - } - - return nil -} - -// EncodeValues converts a CustomStartupOrder struct to a URL value. -func (r CustomStartupOrder) EncodeValues(key string, v *url.Values) error { - var values []string - - if r.Order != nil { - values = append(values, fmt.Sprintf("order=%d", *r.Order)) - } - - if r.Up != nil { - values = append(values, fmt.Sprintf("up=%d", *r.Up)) - } - - if r.Down != nil { - values = append(values, fmt.Sprintf("down=%d", *r.Down)) - } - - if len(values) > 0 { - v.Add(key, strings.Join(values, ",")) - } - - return nil -} - -// EncodeValues converts a CustomTPMState struct to a URL value. -func (r CustomTPMState) EncodeValues(key string, v *url.Values) error { - values := []string{ - fmt.Sprintf("file=%s", r.FileVolume), - } - - if r.Version != nil { - values = append(values, fmt.Sprintf("version=%s", *r.Version)) - } - - v.Add(key, strings.Join(values, ",")) - - return nil -} - -// EncodeValues converts a CustomUSBDevice struct to a URL value. -func (r CustomUSBDevice) EncodeValues(key string, v *url.Values) error { - if r.HostDevice == nil && r.Mapping == nil { - return fmt.Errorf("either device ID or resource mapping must be set") - } - - values := []string{} - if r.HostDevice != nil { - values = append(values, fmt.Sprintf("host=%s", *(r.HostDevice))) - } - - if r.Mapping != nil { - values = append(values, fmt.Sprintf("mapping=%s", *r.Mapping)) - } - - if r.USB3 != nil { - if *r.USB3 { - values = append(values, "usb3=1") - } else { - values = append(values, "usb3=0") - } - } - - v.Add(key, strings.Join(values, ",")) - - return nil -} - -// EncodeValues converts a CustomUSBDevices array to multiple URL values. -func (r CustomUSBDevices) EncodeValues(key string, v *url.Values) error { - for i, d := range r { - if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil { - return fmt.Errorf("error encoding USB device %d: %w", i, err) - } - } - - return nil -} - -// EncodeValues converts a CustomVGADevice struct to a URL value. -func (r CustomVGADevice) EncodeValues(key string, v *url.Values) error { - var values []string - - if r.Clipboard != nil { - values = append(values, fmt.Sprintf("clipboard=%s", *r.Clipboard)) - } - - if r.Memory != nil { - values = append(values, fmt.Sprintf("memory=%d", *r.Memory)) - } - - if r.Type != nil { - values = append(values, fmt.Sprintf("type=%s", *r.Type)) - } - - v.Add(key, strings.Join(values, ",")) - - return nil -} - -// EncodeValues converts a CustomVirtualIODevice struct to a URL value. -func (r CustomVirtualIODevice) EncodeValues(key string, v *url.Values) error { - values := []string{ - fmt.Sprintf("file=%s", r.FileVolume), - } - - if r.AIO != nil { - values = append(values, fmt.Sprintf("aio=%s", *r.AIO)) - } - - if r.BackupEnabled != nil { - if *r.BackupEnabled { - values = append(values, "backup=1") - } else { - values = append(values, "backup=0") - } - } - - v.Add(key, strings.Join(values, ",")) - - return nil -} - -// EncodeValues converts a CustomVirtualIODevices array to multiple URL values. -func (r CustomVirtualIODevices) EncodeValues(key string, v *url.Values) error { - for i, d := range r { - if d.Enabled { - if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil { - return fmt.Errorf("error encoding virtual IO device %d: %w", i, err) - } - } - } - - return nil -} - -// EncodeValues converts a CustomWatchdogDevice struct to a URL value. -func (r CustomWatchdogDevice) EncodeValues(key string, v *url.Values) error { - values := []string{ - fmt.Sprintf("model=%+v", r.Model), - } - - if r.Action != nil { - values = append(values, fmt.Sprintf("action=%s", *r.Action)) - } - - v.Add(key, strings.Join(values, ",")) - - return nil -} - -// UnmarshalJSON converts a CustomAgent string to an object. -func (r *CustomAgent) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("error unmarshalling CustomAgent: %w", err) - } - - pairs := strings.Split(s, ",") - - for _, p := range pairs { - v := strings.Split(strings.TrimSpace(p), "=") - - if len(v) == 1 { - enabled := types.CustomBool(v[0] == "1") - r.Enabled = &enabled - } else if len(v) == 2 { - switch v[0] { - case "enabled": - enabled := types.CustomBool(v[1] == "1") - r.Enabled = &enabled - case "fstrim_cloned_disks": - fstrim := types.CustomBool(v[1] == "1") - r.TrimClonedDisks = &fstrim - case "type": - r.Type = &v[1] - } - } - } - - return nil -} - -// UnmarshalJSON converts a CustomAgent string to an object. -func (r *CustomAudioDevice) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("error unmarshalling CustomAudioDevice: %w", err) - } - - pairs := strings.Split(s, ",") - - for _, p := range pairs { - v := strings.Split(strings.TrimSpace(p), "=") - - if len(v) == 2 { - switch v[0] { - case "device": - r.Device = v[1] - case "driver": - r.Driver = &v[1] - } - } - } - - return nil -} - -// UnmarshalJSON converts a CustomBoot string to an object. -func (r *CustomBoot) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("error unmarshalling CustomBoot: %w", err) - } - - pairs := strings.Split(s, ",") - - for _, p := range pairs { - v := strings.Split(strings.TrimSpace(p), "=") - - if len(v) == 2 { - if v[0] == "order" { - o := strings.Split(strings.TrimSpace(v[1]), ";") - r.Order = &o - } - } - } - - return nil -} - -// UnmarshalJSON converts a CustomCloudInitFiles string to an object. -func (r *CustomCloudInitFiles) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("error unmarshalling CustomCloudInitFiles: %w", err) - } - - pairs := strings.Split(s, ",") - - for _, p := range pairs { - v := strings.Split(strings.TrimSpace(p), "=") - - if len(v) == 2 { - switch v[0] { - case "meta": - r.MetaVolume = &v[1] - case "network": - r.NetworkVolume = &v[1] - case "user": - r.UserVolume = &v[1] - case "vendor": - r.VendorVolume = &v[1] - } - } - } - - return nil -} - -// UnmarshalJSON converts a CustomCloudInitIPConfig string to an object. -func (r *CustomCloudInitIPConfig) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("error unmarshalling CustomCloudInitIPConfig: %w", err) - } - - pairs := strings.Split(s, ",") - - for _, p := range pairs { - v := strings.Split(strings.TrimSpace(p), "=") - - if len(v) == 2 { - switch v[0] { - case "gw": - r.GatewayIPv4 = &v[1] - case "gw6": - r.GatewayIPv6 = &v[1] - case "ip": - r.IPv4 = &v[1] - case "ip6": - r.IPv6 = &v[1] - } - } - } - - return nil -} - -// UnmarshalJSON converts a CustomCloudInitFiles string to an object. -func (r *CustomCloudInitSSHKeys) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("error unmarshalling CustomCloudInitSSHKeys: %w", err) - } - - s, err := url.QueryUnescape(s) - if err != nil { - return fmt.Errorf("error unescaping CustomCloudInitSSHKeys: %w", err) - } - - if s != "" { - *r = strings.Split(strings.TrimSpace(s), "\n") - } else { - *r = []string{} - } - - return nil -} - -// UnmarshalJSON converts a CustomCPUEmulation string to an object. -func (r *CustomCPUEmulation) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("error unmarshalling CustomCPUEmulation: %w", err) - } - - if s == "" { - return errors.New("unexpected empty string") - } - - pairs := strings.Split(s, ",") - - for _, p := range pairs { - v := strings.Split(strings.TrimSpace(p), "=") - - if len(v) == 1 { - r.Type = v[0] - } else if len(v) == 2 { - switch v[0] { - case "cputype": - r.Type = v[1] - case "flags": - if v[1] != "" { - f := strings.Split(v[1], ";") - r.Flags = &f - } else { - var f []string - r.Flags = &f - } - case "hidden": - bv := types.CustomBool(v[1] == "1") - r.Hidden = &bv - case "hv-vendor-id": - r.HVVendorID = &v[1] - } - } - } - - return nil -} - -// UnmarshalJSON converts a CustomEFIDisk string to an object. -func (r *CustomEFIDisk) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("failed to unmarshal CustomEFIDisk: %w", err) - } - - pairs := strings.Split(s, ",") - - for i, p := range pairs { - v := strings.Split(strings.TrimSpace(p), "=") - - if len(v) == 1 && i == 0 { - r.FileVolume = v[0] - } - - if len(v) == 2 { - switch v[0] { - case "file": - r.FileVolume = v[1] - case "format": - r.Format = &v[1] - case "efitype": - t := strings.ToLower(v[1]) - r.Type = &t - case "pre-enrolled-keys": - bv := types.CustomBool(v[1] == "1") - r.PreEnrolledKeys = &bv - } - } - } - - return nil -} - -// UnmarshalJSON converts a CustomNetworkDevice string to an object. -func (r *CustomNetworkDevice) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("failed to unmarshal CustomNetworkDevice: %w", err) - } - - pairs := strings.Split(s, ",") - - for _, p := range pairs { - v := strings.Split(strings.TrimSpace(p), "=") - - //nolint:nestif - if len(v) == 2 { - switch v[0] { - case "bridge": - r.Bridge = &v[1] - case "firewall": - bv := types.CustomBool(v[1] == "1") - r.Firewall = &bv - case "link_down": - bv := types.CustomBool(v[1] == "1") - r.LinkDown = &bv - case "macaddr": - r.MACAddress = &v[1] - case "model": - r.Model = v[1] - case "queues": - iv, err := strconv.Atoi(v[1]) - if err != nil { - return fmt.Errorf("failed to parse queues: %w", err) - } - - r.Queues = &iv - case "rate": - fv, err := strconv.ParseFloat(v[1], 64) - if err != nil { - return fmt.Errorf("failed to parse rate: %w", err) - } - - r.RateLimit = &fv - - case "mtu": - iv, err := strconv.Atoi(v[1]) - if err != nil { - return fmt.Errorf("failed to parse mtu: %w", err) - } - - r.MTU = &iv - - case "tag": - iv, err := strconv.Atoi(v[1]) - if err != nil { - return fmt.Errorf("failed to parse tag: %w", err) - } - - r.Tag = &iv - case "trunks": - trunks := strings.Split(v[1], ";") - r.Trunks = make([]int, len(trunks)) - - for i, trunk := range trunks { - iv, err := strconv.Atoi(trunk) - if err != nil { - return fmt.Errorf("failed to parse trunk %d: %w", i, err) - } - - r.Trunks[i] = iv - } - default: - r.MACAddress = &v[1] - r.Model = v[0] - } - } - } - - r.Enabled = true - - return nil -} - -// UnmarshalJSON converts a CustomNUMADevice string to an object. -func (r *CustomNUMADevice) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("failed to unmarshal CustomNUMADevice: %w", err) - } - - pairs := strings.Split(s, ",") - - for _, p := range pairs { - v := strings.Split(strings.TrimSpace(p), "=") - if len(v) == 2 { - switch v[0] { - case "cpus": - r.CPUIDs = strings.Split(v[1], ";") - case "hostnodes": - hostnodes := strings.Split(v[1], ";") - r.HostNodeNames = &hostnodes - case "memory": - memory, err := strconv.Atoi(v[1]) - if err != nil { - return fmt.Errorf("failed to parse memory size: %w", err) - } - - r.Memory = &memory - case "policy": - r.Policy = &v[1] - } - } - } - - return nil -} - -// UnmarshalJSON converts a CustomPCIDevice string to an object. -func (r *CustomPCIDevice) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("failed to unmarshal CustomPCIDevice: %w", err) - } - - pairs := strings.Split(s, ",") - - for _, p := range pairs { - v := strings.Split(strings.TrimSpace(p), "=") - if len(v) == 1 { - dIDs := strings.Split(v[0], ";") - r.DeviceIDs = &dIDs - } else if len(v) == 2 { - switch v[0] { - case "host": - dIDs := strings.Split(v[1], ";") - r.DeviceIDs = &dIDs - case "mapping": - r.Mapping = &v[1] - case "mdev": - r.MDev = &v[1] - case "pcie": - bv := types.CustomBool(v[1] == "1") - r.PCIExpress = &bv - case "rombar": - bv := types.CustomBool(v[1] == "1") - r.ROMBAR = &bv - case "romfile": - r.ROMFile = &v[1] - case "x-vga": - bv := types.CustomBool(v[1] == "1") - r.XVGA = &bv - } - } - } - - return nil -} - -// UnmarshalJSON converts a CustomTPMState string to an object. -func (r *CustomTPMState) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("failed to unmarshal CustomTPMState: %w", err) - } - - pairs := strings.Split(s, ",") - - for _, p := range pairs { - v := strings.Split(strings.TrimSpace(p), "=") - if len(v) == 1 { - r.FileVolume = v[0] - } else if len(v) == 2 { - switch v[0] { - case "file": - r.FileVolume = v[1] - case "version": - r.Version = &v[1] - } - } - } - - return nil -} - -// UnmarshalJSON converts a CustomUSBDevice string to an object. -func (r *CustomUSBDevice) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("failed to unmarshal CustomUSBDevice: %w", err) - } - - pairs := strings.Split(s, ",") - - for _, p := range pairs { - v := strings.Split(strings.TrimSpace(p), "=") - if len(v) == 1 { - r.HostDevice = &v[1] - } else if len(v) == 2 { - switch v[0] { - case "host": - r.HostDevice = &v[1] - case "mapping": - r.Mapping = &v[1] - case "usb3": - bv := types.CustomBool(v[1] == "1") - r.USB3 = &bv - } - } - } - - return nil -} - -// UnmarshalJSON converts a CustomSharedMemory string to an object. -func (r *CustomSharedMemory) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("failed to unmarshal CustomSharedMemory: %w", err) - } - - pairs := strings.Split(s, ",") - - for _, p := range pairs { - v := strings.Split(strings.TrimSpace(p), "=") - - if len(v) == 2 { - switch v[0] { - case "name": - r.Name = &v[1] - case "size": - var err error - - r.Size, err = strconv.Atoi(v[1]) - if err != nil { - return fmt.Errorf("failed to parse shared memory size: %w", err) - } - } - } - } - - return nil -} - -// UnmarshalJSON converts a CustomSMBIOS string to an object. -func (r *CustomSMBIOS) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("failed to unmarshal CustomSMBIOS: %w", err) - } - - pairs := strings.Split(s, ",") - - for _, p := range pairs { - v := strings.SplitN(strings.TrimSpace(p), "=", 2) - - if len(v) == 2 { - switch v[0] { - case "base64": - base64 := types.CustomBool(v[1] == "1") - r.Base64 = &base64 - case "family": - r.Family = &v[1] - case "manufacturer": - r.Manufacturer = &v[1] - case "product": - r.Product = &v[1] - case "serial": - r.Serial = &v[1] - case "sku": - r.SKU = &v[1] - case "uuid": - r.UUID = &v[1] - case "version": - r.Version = &v[1] - } - } - } - - return nil -} - -// UnmarshalJSON converts a CustomStartupOrder string to an object. -func (r *CustomStartupOrder) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("failed to unmarshal CustomStartupOrder: %w", err) - } - - pairs := strings.Split(s, ",") - - for _, p := range pairs { - v := strings.Split(strings.TrimSpace(p), "=") - - if len(v) == 2 { - switch v[0] { - case "order": - order, err := strconv.Atoi(v[1]) - if err != nil { - return fmt.Errorf("failed to parse int: %w", err) - } - - r.Order = &order - case "up": - up, err := strconv.Atoi(v[1]) - if err != nil { - return fmt.Errorf("failed to parse int: %w", err) - } - - r.Up = &up - case "down": - down, err := strconv.Atoi(v[1]) - if err != nil { - return fmt.Errorf("failed to parse int: %w", err) - } - - r.Down = &down - } - } - } - - return nil -} - -// UnmarshalJSON converts a CustomStorageDevice string to an object. -func (d *CustomStorageDevice) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("failed to unmarshal CustomStorageDevice: %w", err) - } - - pairs := strings.Split(s, ",") - - for _, p := range pairs { - v := strings.Split(strings.TrimSpace(p), "=") - - //nolint:nestif - if len(v) == 1 { - d.FileVolume = v[0] - - ext := filepath.Ext(v[0]) - if ext != "" { - format := string([]byte(ext)[1:]) - d.Format = &format - } - } else if len(v) == 2 { - switch v[0] { - case "aio": - d.AIO = &v[1] - - case "backup": - bv := types.CustomBool(v[1] == "1") - d.Backup = &bv - - case "cache": - d.Cache = &v[1] - - case "discard": - d.Discard = &v[1] - - case "file": - d.FileVolume = v[1] - - case "format": - d.Format = &v[1] - - case "iops_rd": - iv, err := strconv.Atoi(v[1]) - if err != nil { - return fmt.Errorf("failed to convert iops_rd to int: %w", err) - } - - d.IopsRead = &iv - - case "iops_rd_max": - iv, err := strconv.Atoi(v[1]) - if err != nil { - return fmt.Errorf("failed to convert iops_rd_max to int: %w", err) - } - - d.MaxIopsRead = &iv - - case "iops_wr": - iv, err := strconv.Atoi(v[1]) - if err != nil { - return fmt.Errorf("failed to convert iops_wr to int: %w", err) - } - - d.IopsWrite = &iv - - case "iops_wr_max": - iv, err := strconv.Atoi(v[1]) - if err != nil { - return fmt.Errorf("failed to convert iops_wr_max to int: %w", err) - } - - d.MaxIopsWrite = &iv - - case "iothread": - bv := types.CustomBool(v[1] == "1") - d.IOThread = &bv - - case "mbps_rd": - iv, err := strconv.Atoi(v[1]) - if err != nil { - return fmt.Errorf("failed to convert mbps_rd to int: %w", err) - } - - d.MaxReadSpeedMbps = &iv - - case "mbps_rd_max": - iv, err := strconv.Atoi(v[1]) - if err != nil { - return fmt.Errorf("failed to convert mbps_rd_max to int: %w", err) - } - - d.BurstableReadSpeedMbps = &iv - - case "mbps_wr": - iv, err := strconv.Atoi(v[1]) - if err != nil { - return fmt.Errorf("failed to convert mbps_wr to int: %w", err) - } - - d.MaxWriteSpeedMbps = &iv - - case "mbps_wr_max": - iv, err := strconv.Atoi(v[1]) - if err != nil { - return fmt.Errorf("failed to convert mbps_wr_max to int: %w", err) - } - - d.BurstableWriteSpeedMbps = &iv - - case "media": - d.Media = &v[1] - - case "replicate": - bv := types.CustomBool(v[1] == "1") - d.Replicate = &bv - - case "size": - d.Size = new(types.DiskSize) - - err := d.Size.UnmarshalJSON([]byte(v[1])) - if err != nil { - return fmt.Errorf("failed to unmarshal disk size: %w", err) - } - - case "ssd": - bv := types.CustomBool(v[1] == "1") - d.SSD = &bv - } - } - } - - d.Enabled = true - - return nil -} - -// UnmarshalJSON converts a CustomVGADevice string to an object. -func (r *CustomVGADevice) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("failed to unmarshal CustomVGADevice: %w", err) - } - - if s == "" { - return nil - } - - pairs := strings.Split(s, ",") - - for _, p := range pairs { - v := strings.Split(strings.TrimSpace(p), "=") - - if len(v) == 1 { - r.Type = &v[0] - } else if len(v) == 2 { - switch v[0] { - case "clipboard": - r.Clipboard = &v[1] - - case "memory": - m, err := strconv.ParseInt(v[1], 10, 64) - if err != nil { - return fmt.Errorf("failed to convert memory to int: %w", err) - } - - r.Memory = &m - case "type": - r.Type = &v[1] - } - } - } - - return nil -} - -// UnmarshalJSON converts a CustomWatchdogDevice string to an object. -func (r *CustomWatchdogDevice) UnmarshalJSON(b []byte) error { - var s string - - if err := json.Unmarshal(b, &s); err != nil { - return fmt.Errorf("failed to unmarshal CustomWatchdogDevice: %w", err) - } - - if s == "" { - return nil - } - - pairs := strings.Split(s, ",") - - for _, p := range pairs { - v := strings.Split(strings.TrimSpace(p), "=") - - if len(v) == 1 { - r.Model = &v[0] - } else if len(v) == 2 { - switch v[0] { - case "action": - r.Action = &v[1] - case "model": - r.Model = &v[1] - } - } - } - - return nil + return reflect.DeepEqual(*b, UpdateRequestBody{}) } diff --git a/proxmoxtf/resource/vm/vm.go b/proxmoxtf/resource/vm/vm.go index f546c7f0..e1e51496 100644 --- a/proxmoxtf/resource/vm/vm.go +++ b/proxmoxtf/resource/vm/vm.go @@ -2215,8 +2215,6 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d return diag.FromErr(e) } - ///////////////// - allDiskInfo := disk.GetInfo(vmConfig, d) // from the cloned VM planDisks, e := disk.GetDiskDeviceObjects(d, VM(), nil) // from the resource config