From 80cafa689f3795a808dc0c2ecbcf81608181e370 Mon Sep 17 00:00:00 2001 From: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Date: Sun, 9 Jun 2024 21:28:18 -0400 Subject: [PATCH] feat(vm2): add initial support for `cdrom` (#1370) feat(vm2): add initial support for `cdrom` This is a breaking change comparing to v1 - switching the cdrom schema from a nested block to a nested attribute map. Improvements comparing to v1: - support for `ide`, `sata`, `scsi` interfaces - support for multiple cdroms Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- docs/resources/virtual_environment_vm2.md | 9 + fwprovider/access/resource_user_token.go | 6 + fwprovider/hardwaremapping/datasource.go | 8 +- fwprovider/hardwaremapping/datasource_pci.go | 8 +- fwprovider/hardwaremapping/datasource_usb.go | 8 +- fwprovider/hardwaremapping/errors.go | 8 +- fwprovider/hardwaremapping/models.go | 8 +- fwprovider/hardwaremapping/resource_pci.go | 8 +- fwprovider/hardwaremapping/resource_usb.go | 8 +- fwprovider/hardwaremapping/shared.go | 6 + fwprovider/validators/file.go | 22 +++ fwprovider/validators/hardware_mapping.go | 8 +- fwprovider/vm/cdrom/model.go | 38 ++++ fwprovider/vm/cdrom/resource.go | 111 +++++++++++ fwprovider/vm/cdrom/resource_schema.go | 56 ++++++ fwprovider/vm/cdrom/resource_test.go | 172 ++++++++++++++++++ fwprovider/vm/cpu/datasource_schema.go | 2 +- .../vm/cpu/{resource_model.go => model.go} | 6 + fwprovider/vm/cpu/resource.go | 6 + fwprovider/vm/cpu/resource_schema.go | 15 +- fwprovider/vm/{vm_model.go => model.go} | 10 + fwprovider/vm/resource.go | 10 + fwprovider/vm/resource_schema.go | 10 +- fwprovider/vm/vga/datasource_schema.go | 2 +- .../vm/vga/{resource_model.go => model.go} | 0 fwprovider/vm/vga/resource.go | 6 + proxmox/nodes/vms/custom_storage_device.go | 6 +- .../nodes/vms/custom_storage_device_test.go | 3 +- proxmoxtf/resource/vm/disk/disk.go | 7 + utils/maps.go | 39 +++- utils/sets.go | 45 +++++ utils/sets_test.go | 91 +++++++++ 32 files changed, 694 insertions(+), 48 deletions(-) create mode 100644 fwprovider/validators/file.go create mode 100644 fwprovider/vm/cdrom/model.go create mode 100644 fwprovider/vm/cdrom/resource.go create mode 100644 fwprovider/vm/cdrom/resource_schema.go create mode 100644 fwprovider/vm/cdrom/resource_test.go rename fwprovider/vm/cpu/{resource_model.go => model.go} (83%) rename fwprovider/vm/{vm_model.go => model.go} (88%) rename fwprovider/vm/vga/{resource_model.go => model.go} (100%) create mode 100644 utils/sets.go create mode 100644 utils/sets_test.go diff --git a/docs/resources/virtual_environment_vm2.md b/docs/resources/virtual_environment_vm2.md index 375326f9..cc99ac60 100644 --- a/docs/resources/virtual_environment_vm2.md +++ b/docs/resources/virtual_environment_vm2.md @@ -30,6 +30,7 @@ The attributes are also marked as optional to allow the practitioner to set (or ### Optional +- `cdrom` (Attributes Map) The CD-ROM configuration. The key is the interface of the CD-ROM, could be one of `ideN`, `sataN`, `scsiN`, where N is the index of the interface. (see [below for nested schema](#nestedatt--cdrom)) - `clone` (Attributes) The cloning configuration. (see [below for nested schema](#nestedatt--clone)) - `cpu` (Attributes) The CPU configuration. (see [below for nested schema](#nestedatt--cpu)) - `description` (String) The description of the VM. @@ -40,6 +41,14 @@ The attributes are also marked as optional to allow the practitioner to set (or - `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts)) - `vga` (Attributes) Configure the VGA Hardware. If you want to use high resolution modes (>= 1280x1024x16) you may need to increase the vga memory option. Since QEMU 2.9 the default VGA display type is `std` for all OS types besides some Windows versions (XP and older) which use `cirrus`. The `qxl` option enables the SPICE display server. For win* OS you can select how many independent displays you want, Linux guests can add displays themself. You can also run without any graphic card, using a serial device as terminal. See the [Proxmox documentation](https://pve.proxmox.com/pve-docs/pve-admin-guide.html#qm_virtual_machines_settings) section 10.2.8 for more information and available configuration parameters. (see [below for nested schema](#nestedatt--vga)) + +### Nested Schema for `cdrom` + +Optional: + +- `file_id` (String) The file ID of the CD-ROM, or `cdrom|none`. Defaults to `none` to leave the CD-ROM empty. Use `cdrom` to connect to the physical drive. + + ### Nested Schema for `clone` diff --git a/fwprovider/access/resource_user_token.go b/fwprovider/access/resource_user_token.go index bd0aafc4..bf487c3d 100644 --- a/fwprovider/access/resource_user_token.go +++ b/fwprovider/access/resource_user_token.go @@ -1,3 +1,9 @@ +/* + * 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 access import ( diff --git a/fwprovider/hardwaremapping/datasource.go b/fwprovider/hardwaremapping/datasource.go index 830a5627..cccc0bad 100644 --- a/fwprovider/hardwaremapping/datasource.go +++ b/fwprovider/hardwaremapping/datasource.go @@ -1,8 +1,8 @@ /* - 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/. -*/ + * 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 hardwaremapping diff --git a/fwprovider/hardwaremapping/datasource_pci.go b/fwprovider/hardwaremapping/datasource_pci.go index 237e6fdc..11edf44e 100644 --- a/fwprovider/hardwaremapping/datasource_pci.go +++ b/fwprovider/hardwaremapping/datasource_pci.go @@ -1,8 +1,8 @@ /* - 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/. -*/ + * 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 hardwaremapping diff --git a/fwprovider/hardwaremapping/datasource_usb.go b/fwprovider/hardwaremapping/datasource_usb.go index 4f4129fc..86df0ce7 100644 --- a/fwprovider/hardwaremapping/datasource_usb.go +++ b/fwprovider/hardwaremapping/datasource_usb.go @@ -1,8 +1,8 @@ /* - 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/. -*/ + * 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 hardwaremapping diff --git a/fwprovider/hardwaremapping/errors.go b/fwprovider/hardwaremapping/errors.go index 170d6da7..695f2768 100644 --- a/fwprovider/hardwaremapping/errors.go +++ b/fwprovider/hardwaremapping/errors.go @@ -1,8 +1,8 @@ /* - 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/. -*/ + * 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 hardwaremapping diff --git a/fwprovider/hardwaremapping/models.go b/fwprovider/hardwaremapping/models.go index 2dc2e7f0..66ca5015 100644 --- a/fwprovider/hardwaremapping/models.go +++ b/fwprovider/hardwaremapping/models.go @@ -1,8 +1,8 @@ /* - 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/. -*/ + * 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 hardwaremapping diff --git a/fwprovider/hardwaremapping/resource_pci.go b/fwprovider/hardwaremapping/resource_pci.go index 2c908878..c04b450e 100644 --- a/fwprovider/hardwaremapping/resource_pci.go +++ b/fwprovider/hardwaremapping/resource_pci.go @@ -1,8 +1,8 @@ /* - 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/. -*/ + * 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 hardwaremapping diff --git a/fwprovider/hardwaremapping/resource_usb.go b/fwprovider/hardwaremapping/resource_usb.go index 6df9e864..6546cc2c 100644 --- a/fwprovider/hardwaremapping/resource_usb.go +++ b/fwprovider/hardwaremapping/resource_usb.go @@ -1,8 +1,8 @@ /* - 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/. -*/ + * 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 hardwaremapping diff --git a/fwprovider/hardwaremapping/shared.go b/fwprovider/hardwaremapping/shared.go index 0d37bab7..cedfb43c 100644 --- a/fwprovider/hardwaremapping/shared.go +++ b/fwprovider/hardwaremapping/shared.go @@ -1,3 +1,9 @@ +/* + * 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 hardwaremapping import ( diff --git a/fwprovider/validators/file.go b/fwprovider/validators/file.go new file mode 100644 index 00000000..c651c42a --- /dev/null +++ b/fwprovider/validators/file.go @@ -0,0 +1,22 @@ +/* + * 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 validators + +import ( + "regexp" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// FileID returns a validator that checks if a string is a valid file ID. +func FileID() validator.String { + return stringvalidator.RegexMatches( + regexp.MustCompile(`^(?i)[a-z\d\-_.]+:([a-z\d\-_]+/)?.+$`), + "must be in the format `:/`", + ) +} diff --git a/fwprovider/validators/hardware_mapping.go b/fwprovider/validators/hardware_mapping.go index 6bebe11d..6cfa74a2 100644 --- a/fwprovider/validators/hardware_mapping.go +++ b/fwprovider/validators/hardware_mapping.go @@ -1,8 +1,8 @@ /* - 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/. -*/ + * 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 validators diff --git a/fwprovider/vm/cdrom/model.go b/fwprovider/vm/cdrom/model.go new file mode 100644 index 00000000..b6c3db88 --- /dev/null +++ b/fwprovider/vm/cdrom/model.go @@ -0,0 +1,38 @@ +/* + * 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 cdrom + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/bpg/terraform-provider-proxmox/proxmox/helpers/ptr" + "github.com/bpg/terraform-provider-proxmox/proxmox/nodes/vms" +) + +// Model represents the CD-ROM model. +type Model struct { + FileID types.String `tfsdk:"file_id"` +} + +func attributeTypes() map[string]attr.Type { + return map[string]attr.Type{ + "file_id": types.StringType, + } +} + +func (m *Model) exportToCustomStorageDevice(iface string) vms.CustomStorageDevice { + return vms.CustomStorageDevice{ + FileVolume: m.FileID.ValueString(), + Interface: &iface, + Media: ptr.Ptr("cdrom"), + } +} + +func (m *Model) importFromCustomStorageDevice(d vms.CustomStorageDevice) { + m.FileID = types.StringValue(d.FileVolume) +} diff --git a/fwprovider/vm/cdrom/resource.go b/fwprovider/vm/cdrom/resource.go new file mode 100644 index 00000000..e1eed7c5 --- /dev/null +++ b/fwprovider/vm/cdrom/resource.go @@ -0,0 +1,111 @@ +/* + * 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 cdrom + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/bpg/terraform-provider-proxmox/proxmox/nodes/vms" + "github.com/bpg/terraform-provider-proxmox/utils" +) + +// Value represents the type for CD-ROM settings. +type Value = types.Map + +// NewValue returns a new Value with the given CD-ROM settings from the PVE API. +func NewValue(ctx context.Context, config *vms.GetResponseData, diags *diag.Diagnostics) Value { + // find storage devices with media=cdrom + cdroms := vms.MapCustomStorageDevices(*config).Filter(func(device *vms.CustomStorageDevice) bool { + return device.Media != nil && *device.Media == "cdrom" + }) + + elements := make(map[string]Model, len(cdroms)) + + for iface, cdrom := range cdroms { + m := Model{} + m.importFromCustomStorageDevice(*cdrom) + elements[iface] = m + } + + obj, d := types.MapValueFrom(ctx, types.ObjectType{}.WithAttributeTypes(attributeTypes()), elements) + diags.Append(d...) + + return obj +} + +// FillCreateBody fills the CreateRequestBody with the CD-ROM settings from the Value. +// +// In the 'create' context, v is the plan. +func FillCreateBody(ctx context.Context, planValue Value, body *vms.CreateRequestBody, diags *diag.Diagnostics) { + if planValue.IsNull() || planValue.IsUnknown() { + return + } + + var plan map[string]Model + d := planValue.ElementsAs(ctx, &plan, false) + diags.Append(d...) + + if d.HasError() { + return + } + + for iface, cdrom := range plan { + err := body.AddCustomStorageDevice(cdrom.exportToCustomStorageDevice(iface)) + if err != nil { + diags.AddError(err.Error(), "") + } + } +} + +// FillUpdateBody fills the UpdateRequestBody with the CD-ROM settings from the Value. +// +// In the 'update' context, v is the plan and stateValue is the current state. +func FillUpdateBody( + ctx context.Context, + planValue, stateValue Value, + updateBody *vms.UpdateRequestBody, + _ bool, + diags *diag.Diagnostics, +) { + if planValue.IsNull() || planValue.IsUnknown() || planValue.Equal(stateValue) { + return + } + + var plan, state map[string]Model + d := planValue.ElementsAs(ctx, &plan, false) + diags.Append(d...) + d = stateValue.ElementsAs(ctx, &state, false) + diags.Append(d...) + + if diags.HasError() { + return + } + + toCreate, toUpdate, toDelete := utils.MapDiff(plan, state) + + for iface, dev := range toCreate { + err := updateBody.AddCustomStorageDevice(dev.exportToCustomStorageDevice(iface)) + if err != nil { + diags.AddError(err.Error(), "") + } + } + + for iface, dev := range toUpdate { + // for CD-ROMs, the update fully override the existing device, we don't do per-attribute check + err := updateBody.AddCustomStorageDevice(dev.exportToCustomStorageDevice(iface)) + if err != nil { + diags.AddError(err.Error(), "") + } + } + + for iface := range toDelete { + updateBody.Delete = append(updateBody.Delete, iface) + } +} diff --git a/fwprovider/vm/cdrom/resource_schema.go b/fwprovider/vm/cdrom/resource_schema.go new file mode 100644 index 00000000..69c529f9 --- /dev/null +++ b/fwprovider/vm/cdrom/resource_schema.go @@ -0,0 +1,56 @@ +/* + * 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 cdrom + +import ( + "regexp" + + "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/bpg/terraform-provider-proxmox/fwprovider/validators" +) + +// ResourceSchema defines the schema for the CD-ROM resource. +func ResourceSchema() schema.Attribute { + return schema.MapNestedAttribute{ + Description: "The CD-ROM configuration", + MarkdownDescription: "The CD-ROM configuration. The key is the interface of the CD-ROM, " + + "could be one of `ideN`, `sataN`, `scsiN`, where N is the index of the interface.", + Optional: true, + Computed: true, + Validators: []validator.Map{ + mapvalidator.KeysAre( + stringvalidator.RegexMatches( + regexp.MustCompile(`^(ide[0-3]|sata[0-5]|scsi([0-9]|1[0-3]))$`), + "one of `ide[0-3]`, `sata[0-5]`, `scsi[0-13]`", + ), + ), + }, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "file_id": schema.StringAttribute{ + Description: "The file ID of the CD-ROM", + MarkdownDescription: "The file ID of the CD-ROM, or `cdrom|none`." + + " Defaults to `none` to leave the CD-ROM empty. Use `cdrom` to connect to the physical drive.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString("cdrom"), + Validators: []validator.String{ + stringvalidator.Any( + stringvalidator.OneOf("cdrom", "none"), + validators.FileID(), + ), + }, + }, + }, + }, + } +} diff --git a/fwprovider/vm/cdrom/resource_test.go b/fwprovider/vm/cdrom/resource_test.go new file mode 100644 index 00000000..5f6724f4 --- /dev/null +++ b/fwprovider/vm/cdrom/resource_test.go @@ -0,0 +1,172 @@ +/* + * 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 cdrom_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/bpg/terraform-provider-proxmox/fwprovider/test" +) + +const resourceName = "proxmox_virtual_environment_vm2.test_vm" + +func TestAccResourceVM2CDROM(t *testing.T) { + t.Parallel() + + te := test.InitEnvironment(t) + + tests := []struct { + name string + steps []resource.TestStep + }{ + {"create VM default CDROM", []resource.TestStep{{ + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_vm2" "test_vm" { + node_name = "{{.NodeName}}" + id = {{.RandomVMID}} + name = "test-cdrom" + cdrom = { + "ide3" = {} + } + }`), + Check: test.ResourceAttributes(resourceName, map[string]string{ + "cdrom.%": "1", + "cdrom.ide3.file_id": "cdrom", + }), + }}}, + {"create VM multiple CDROMs", []resource.TestStep{{ + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_vm2" "test_vm" { + node_name = "{{.NodeName}}" + id = {{.RandomVMID}} + name = "test-cdrom" + cdrom = { + "ide3" = {}, + "ide1" = { + file_id = "none" + } + } + }`), + Check: test.ResourceAttributes(resourceName, map[string]string{ + "cdrom.%": "2", + "cdrom.ide3.file_id": "cdrom", + "cdrom.ide1.file_id": "none", + }), + }}}, + {"create VM with CDROM and then update it", []resource.TestStep{ + { + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_vm2" "test_vm" { + node_name = "{{.NodeName}}" + id = {{.RandomVMID}} + name = "test-cdrom" + cdrom = { + "scsi2" = { + file_id = "none" + }, + "ide2" = { + file_id = "cdrom" + } + } + }`), + Check: test.ResourceAttributes(resourceName, map[string]string{ + "cdrom.%": "2", + "cdrom.scsi2.file_id": "none", + "cdrom.ide2.file_id": "cdrom", + }), + }, + { // now update the cdrom params and check if they are updated + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_vm2" "test_vm" { + node_name = "{{.NodeName}}" + name = "test-cdrom" + cdrom = { + "scsi2" = { + file_id = "cdrom" + } + } + }`), + Check: test.ResourceAttributes(resourceName, map[string]string{ + "cdrom.%": "1", + "cdrom.scsi2.file_id": "cdrom", + }), + }, + { + RefreshState: true, + }, + }}, + {"clone VM with CDROM", []resource.TestStep{{ + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_vm2" "template_vm" { + node_name = "{{.NodeName}}" + id = {{.RandomVMID1}} + name = "template-cdrom" + cdrom = { + "ide3" = { + file_id = "cdrom" + } + } + } + resource "proxmox_virtual_environment_vm2" "test_vm" { + node_name = "{{.NodeName}}" + id = {{.RandomVMID2}} + name = "test-cdrom" + clone = { + id = proxmox_virtual_environment_vm2.template_vm.id + } + }`), + Check: test.ResourceAttributes(resourceName, map[string]string{ + "cdrom.%": "1", + "cdrom.ide3.file_id": "cdrom", + }), + }}}, + {"clone VM with some CDROM params and updating them in the clone", []resource.TestStep{{ + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_vm2" "template_vm" { + node_name = "{{.NodeName}}" + id = {{.RandomVMID1}} + name = "template-cdrom" + cdrom = { + "ide1" = { + file_id = "none" + }, + "ide2" = { + file_id = "cdrom" + } + } + } + resource "proxmox_virtual_environment_vm2" "test_vm" { + node_name = "{{.NodeName}}" + id = {{.RandomVMID2}} + name = "test-cpu" + clone = { + id = proxmox_virtual_environment_vm2.template_vm.id + } + cdrom = { + "ide1" = { + file_id = "cdrom" + } + } + }`), + Check: test.ResourceAttributes(resourceName, map[string]string{ + "cdrom.%": "1", + "cdrom.ide1.file_id": "cdrom", + }), + }}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: te.AccProviders, + Steps: tt.steps, + }) + }) + } +} diff --git a/fwprovider/vm/cpu/datasource_schema.go b/fwprovider/vm/cpu/datasource_schema.go index 33f48fc7..4f8f300d 100644 --- a/fwprovider/vm/cpu/datasource_schema.go +++ b/fwprovider/vm/cpu/datasource_schema.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) -// DataSourceSchema defines the schema for the CPU resource. +// DataSourceSchema defines the schema for the CPU datasource. func DataSourceSchema() schema.Attribute { return schema.SingleNestedAttribute{ CustomType: basetypes.ObjectType{ diff --git a/fwprovider/vm/cpu/resource_model.go b/fwprovider/vm/cpu/model.go similarity index 83% rename from fwprovider/vm/cpu/resource_model.go rename to fwprovider/vm/cpu/model.go index 62490e3e..37e8a346 100644 --- a/fwprovider/vm/cpu/resource_model.go +++ b/fwprovider/vm/cpu/model.go @@ -1,3 +1,9 @@ +/* + * 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 cpu import ( diff --git a/fwprovider/vm/cpu/resource.go b/fwprovider/vm/cpu/resource.go index b7e3abf8..9b715850 100644 --- a/fwprovider/vm/cpu/resource.go +++ b/fwprovider/vm/cpu/resource.go @@ -1,3 +1,9 @@ +/* + * 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 cpu import ( diff --git a/fwprovider/vm/cpu/resource_schema.go b/fwprovider/vm/cpu/resource_schema.go index abafcca5..33be008b 100644 --- a/fwprovider/vm/cpu/resource_schema.go +++ b/fwprovider/vm/cpu/resource_schema.go @@ -1,3 +1,9 @@ +/* + * 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 cpu import ( @@ -8,25 +14,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) // ResourceSchema defines the schema for the CPU resource. func ResourceSchema() schema.Attribute { return schema.SingleNestedAttribute{ - CustomType: basetypes.ObjectType{ - AttrTypes: attributeTypes(), - }, Description: "The CPU configuration.", Optional: true, Computed: true, - PlanModifiers: []planmodifier.Object{ - objectplanmodifier.UseStateForUnknown(), - }, Attributes: map[string]schema.Attribute{ "affinity": schema.StringAttribute{ Description: "List of host cores used to execute guest processes, for example: '0,5,8-11'", diff --git a/fwprovider/vm/vm_model.go b/fwprovider/vm/model.go similarity index 88% rename from fwprovider/vm/vm_model.go rename to fwprovider/vm/model.go index 5943507d..7148e48f 100644 --- a/fwprovider/vm/vm_model.go +++ b/fwprovider/vm/model.go @@ -1,3 +1,9 @@ +/* + * 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 vm import ( @@ -10,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/bpg/terraform-provider-proxmox/fwprovider/types/stringset" + "github.com/bpg/terraform-provider-proxmox/fwprovider/vm/cdrom" "github.com/bpg/terraform-provider-proxmox/fwprovider/vm/cpu" "github.com/bpg/terraform-provider-proxmox/fwprovider/vm/vga" "github.com/bpg/terraform-provider-proxmox/proxmox" @@ -22,6 +29,7 @@ import ( // or a custom type in order to hold an unknown value. type Model struct { Description types.String `tfsdk:"description"` + CDROM cdrom.Value `tfsdk:"cdrom"` CPU cpu.Value `tfsdk:"cpu"` Clone *struct { ID types.Int64 `tfsdk:"id"` @@ -78,5 +86,7 @@ func read(ctx context.Context, client proxmox.Client, model *Model, diags *diag. model.CPU = cpu.NewValue(ctx, config, diags) model.VGA = vga.NewValue(ctx, config, diags) + model.CDROM = cdrom.NewValue(ctx, config, diags) + return true } diff --git a/fwprovider/vm/resource.go b/fwprovider/vm/resource.go index d88997ad..26e90e6f 100644 --- a/fwprovider/vm/resource.go +++ b/fwprovider/vm/resource.go @@ -1,3 +1,9 @@ +/* + * 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 vm import ( @@ -15,6 +21,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/bpg/terraform-provider-proxmox/fwprovider/vm/cdrom" "github.com/bpg/terraform-provider-proxmox/fwprovider/vm/cpu" "github.com/bpg/terraform-provider-proxmox/fwprovider/vm/vga" "github.com/bpg/terraform-provider-proxmox/proxmox" @@ -146,6 +153,7 @@ func (r *Resource) create(ctx context.Context, plan Model, diags *diag.Diagnosti } // fill out create body fields with values from other resource blocks + cdrom.FillCreateBody(ctx, plan.CDROM, createBody, diags) cpu.FillCreateBody(ctx, plan.CPU, createBody, diags) vga.FillCreateBody(ctx, plan.VGA, createBody, diags) @@ -322,6 +330,8 @@ func (r *Resource) update(ctx context.Context, plan, state Model, isClone bool, } } + // fill out update body fields with values from other resource blocks + cdrom.FillUpdateBody(ctx, plan.CDROM, state.CDROM, updateBody, isClone, diags) cpu.FillUpdateBody(ctx, plan.CPU, state.CPU, updateBody, isClone, diags) vga.FillUpdateBody(ctx, plan.VGA, state.VGA, updateBody, isClone, diags) diff --git a/fwprovider/vm/resource_schema.go b/fwprovider/vm/resource_schema.go index 4e270d68..d6170684 100644 --- a/fwprovider/vm/resource_schema.go +++ b/fwprovider/vm/resource_schema.go @@ -1,3 +1,9 @@ +/* + * 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 vm import ( @@ -16,6 +22,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/bpg/terraform-provider-proxmox/fwprovider/types/stringset" + "github.com/bpg/terraform-provider-proxmox/fwprovider/vm/cdrom" "github.com/bpg/terraform-provider-proxmox/fwprovider/vm/cpu" "github.com/bpg/terraform-provider-proxmox/fwprovider/vm/vga" ) @@ -51,7 +58,8 @@ func (r *Resource) Schema( }, }, }, - "cpu": cpu.ResourceSchema(), + "cdrom": cdrom.ResourceSchema(), + "cpu": cpu.ResourceSchema(), "description": schema.StringAttribute{ Description: "The description of the VM.", Optional: true, diff --git a/fwprovider/vm/vga/datasource_schema.go b/fwprovider/vm/vga/datasource_schema.go index fa88316d..d0d2f6b7 100644 --- a/fwprovider/vm/vga/datasource_schema.go +++ b/fwprovider/vm/vga/datasource_schema.go @@ -5,7 +5,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) -// DataSourceSchema defines the schema for the VGA resource. +// DataSourceSchema defines the schema for the VGA datasource. func DataSourceSchema() schema.Attribute { return schema.SingleNestedAttribute{ CustomType: basetypes.ObjectType{ diff --git a/fwprovider/vm/vga/resource_model.go b/fwprovider/vm/vga/model.go similarity index 100% rename from fwprovider/vm/vga/resource_model.go rename to fwprovider/vm/vga/model.go diff --git a/fwprovider/vm/vga/resource.go b/fwprovider/vm/vga/resource.go index fc4abe24..dfbb54e2 100644 --- a/fwprovider/vm/vga/resource.go +++ b/fwprovider/vm/vga/resource.go @@ -1,3 +1,9 @@ +/* + * 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 vga import ( diff --git a/proxmox/nodes/vms/custom_storage_device.go b/proxmox/nodes/vms/custom_storage_device.go index 957f5e8e..60e9ed21 100644 --- a/proxmox/nodes/vms/custom_storage_device.go +++ b/proxmox/nodes/vms/custom_storage_device.go @@ -400,6 +400,8 @@ func (d CustomStorageDevices) EncodeValues(_ string, v *url.Values) error { } // MapCustomStorageDevices maps the custom storage devices from the API response. +// +// NOTE: CustomStorageDevice.FileID and CustomStorageDevice.DatastoreID are not set in this function. func MapCustomStorageDevices(resp GetResponseData) CustomStorageDevices { csd := CustomStorageDevices{} @@ -411,13 +413,13 @@ func MapCustomStorageDevices(resp GetResponseData) CustomStorageDevices { return csd } -func mapDevice(csd CustomStorageDevices, resp GetResponseData, keyPrefix, fieldPrefix string, end int) { +func mapDevice(csds 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) + csds[keyPrefix+strconv.Itoa(i)] = val.(*CustomStorageDevice) } } } diff --git a/proxmox/nodes/vms/custom_storage_device_test.go b/proxmox/nodes/vms/custom_storage_device_test.go index 51a05c62..0301bfa7 100644 --- a/proxmox/nodes/vms/custom_storage_device_test.go +++ b/proxmox/nodes/vms/custom_storage_device_test.go @@ -267,7 +267,8 @@ func TestMapCustomStorageDevices(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - assert.Equalf(t, tt.want, MapCustomStorageDevices(tt.args.resp), "MapCustomStorageDevices(%v)", tt.args.resp) + devices := MapCustomStorageDevices(tt.args.resp) + assert.Equalf(t, tt.want, devices, "MapCustomStorageDevices(%v)", tt.args.resp) }) } } diff --git a/proxmoxtf/resource/vm/disk/disk.go b/proxmoxtf/resource/vm/disk/disk.go index 1373e35a..d2b7fda5 100644 --- a/proxmoxtf/resource/vm/disk/disk.go +++ b/proxmoxtf/resource/vm/disk/disk.go @@ -1,3 +1,9 @@ +/* + * 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 disk import ( @@ -23,6 +29,7 @@ import ( const supportedDiskInterfaces = "virtio, sata, scsi, ide" // GetInfo returns the disk information for a VM. +// Deprecated: use vms.MapCustomStorageDevices from proxmox/nodes/vms instead. func GetInfo(resp *vms.GetResponseData, d *schema.ResourceData) vms.CustomStorageDevices { storageDevices := vms.CustomStorageDevices{} diff --git a/utils/maps.go b/utils/maps.go index ba17b9df..e67cbbfd 100644 --- a/utils/maps.go +++ b/utils/maps.go @@ -1,6 +1,15 @@ +/* + * 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 utils -import "sort" +import ( + "reflect" + "sort" +) // OrderedListFromMap generates a list from a map's values. The values are sorted based on the map's keys. func OrderedListFromMap(inputMap map[string]interface{}) []interface{} { @@ -71,3 +80,31 @@ func OrderedListFromMapByKeyValues(inputMap map[string]interface{}, keyList []st return orderedList } + +// MapDiff compares the difference between two maps and returns the elements that are in the plan but not +// in the state (toCreate), the elements that are in the plan and in the state but are different (toUpdate), +// and the elements that are in the state but not in the plan (toDelete). +// The keyFunc is used to extract a unique key from each element to compare them. +func MapDiff[T any](plan map[string]T, state map[string]T) (map[string]T, map[string]T, map[string]T) { + toCreate := map[string]T{} + toUpdate := map[string]T{} + toDelete := map[string]T{} + + for key, p := range plan { + s, ok := state[key] + if !ok { + toCreate[key] = p + } else if !reflect.DeepEqual(p, s) { + toUpdate[key] = p + } + } + + for key, s := range state { + _, ok := plan[key] + if !ok { + toDelete[key] = s + } + } + + return toCreate, toUpdate, toDelete +} diff --git a/utils/sets.go b/utils/sets.go new file mode 100644 index 00000000..92ff58eb --- /dev/null +++ b/utils/sets.go @@ -0,0 +1,45 @@ +/* + * 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 utils + +import "reflect" + +// SetDiff compares two slices of elements and returns the elements that are in the plan but not +// in the state (toCreate), the elements that are in the plan and in the state but are different (toUpdate), +// and the elements that are in the state but not in the plan (toDelete). +// The keyFunc is used to extract a unique key from each element to compare them. +func SetDiff[T any](plan []T, state []T, keyFunc func(t T) string) ([]T, []T, []T) { + var toCreate, toUpdate, toDelete []T + + stateMap := map[string]T{} + for _, s := range state { + stateMap[keyFunc(s)] = s + } + + planMap := map[string]T{} + for _, p := range plan { + planMap[keyFunc(p)] = p + } + + for _, p := range plan { + s, ok := stateMap[keyFunc(p)] + if !ok { + toCreate = append(toCreate, p) + } else if !reflect.DeepEqual(p, s) { + toUpdate = append(toUpdate, p) + } + } + + for _, s := range state { + _, ok := planMap[keyFunc(s)] + if !ok { + toDelete = append(toDelete, s) + } + } + + return toCreate, toUpdate, toDelete +} diff --git a/utils/sets_test.go b/utils/sets_test.go new file mode 100644 index 00000000..5f605405 --- /dev/null +++ b/utils/sets_test.go @@ -0,0 +1,91 @@ +/* + * 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 utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSetDiff(t *testing.T) { + t.Parallel() + + type args[T any] struct { + plan []T + state []T + } + + type testCase[T any] struct { + name string + args args[T] + wantToCreate []T + wantToUpdate []T + wantToDelete []T + } + + type rec struct { + n string + v string + } + + tests := []testCase[rec]{ + { + name: "empty", + args: args[rec]{ + plan: []rec{}, + state: []rec{}, + }, + wantToCreate: nil, + wantToUpdate: nil, + wantToDelete: nil, + }, + { + name: "create", + args: args[rec]{ + plan: []rec{{"a", "1"}, {"b", "2"}, {"c", "3"}}, + state: []rec{}, + }, + wantToCreate: []rec{{"a", "1"}, {"b", "2"}, {"c", "3"}}, + wantToUpdate: nil, + wantToDelete: nil, + }, + { + name: "create and delete", + args: args[rec]{ + plan: []rec{{"a", "1"}, {"b", "2"}, {"c", "3"}}, + state: []rec{{"b", "2"}, {"c", "3"}, {"d", "4"}}, + }, + wantToCreate: []rec{{"a", "1"}}, + wantToUpdate: nil, + wantToDelete: []rec{{"d", "4"}}, + }, + { + name: "update", + args: args[rec]{ + plan: []rec{{"a", "1"}, {"b", "2"}, {"c", "3"}}, + state: []rec{{"a", "1"}, {"b", "2"}, {"c", "4"}}, + }, + wantToCreate: nil, + wantToUpdate: []rec{{"c", "3"}}, + wantToDelete: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + gotToCreate, gotToUpdate, gotToDelete := SetDiff( + tt.args.plan, tt.args.state, func(t rec) string { return t.n }, + ) + assert.Equalf(t, tt.wantToCreate, gotToCreate, "toCreate is different") + assert.Equalf(t, tt.wantToUpdate, gotToUpdate, "toUpdate is different") + assert.Equalf(t, tt.wantToDelete, gotToDelete, "toDelete is different") + }) + } +}