From 9a325924a4cf4dfa4dbfd128c616e1d58f4d9eca Mon Sep 17 00:00:00 2001 From: Fina Wilke Date: Tue, 15 Apr 2025 14:48:18 +0200 Subject: [PATCH 1/2] feat(vm): Implement virtiofs Signed-off-by: Fina Wilke --- docs/resources/virtual_environment_vm.md | 16 ++ example/resource_virtual_environment_vm.tf | 2 +- fwprovider/test/resource_vm_test.go | 45 +++++ proxmox/nodes/vms/custom_virtiofs_share.go | 131 ++++++++++++++ .../nodes/vms/custom_virtiofs_share_test.go | 79 +++++++++ proxmox/nodes/vms/vms_types.go | 12 ++ proxmox/nodes/vms/vms_types_test.go | 7 +- proxmoxtf/resource/vm/validators.go | 10 ++ proxmoxtf/resource/vm/vm.go | 161 +++++++++++++++++- proxmoxtf/resource/vm/vm_test.go | 19 +++ 10 files changed, 479 insertions(+), 3 deletions(-) create mode 100644 proxmox/nodes/vms/custom_virtiofs_share.go create mode 100644 proxmox/nodes/vms/custom_virtiofs_share_test.go diff --git a/docs/resources/virtual_environment_vm.md b/docs/resources/virtual_environment_vm.md index 536104277..f6f4ad472 100755 --- a/docs/resources/virtual_environment_vm.md +++ b/docs/resources/virtual_environment_vm.md @@ -80,6 +80,12 @@ resource "proxmox_virtual_environment_vm" "ubuntu_vm" { } serial_device {} + + virtiofs { + mapping = "data_share" + cache = "always" + direct_io = true + } } resource "proxmox_virtual_environment_download_file" "latest_ubuntu_22_jammy_qcow2_img" { @@ -559,6 +565,16 @@ output "ubuntu_vm_public_key" { - `virtio-gl` - VirtIO-GPU with 3D acceleration (VirGL). VirGL support needs some extra libraries that aren’t installed by default. 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. - `vmware` - VMware Compatible. - `clipboard` - (Optional) Enable VNC clipboard by setting to `vnc`. 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. +- `virtiofs` - (Optional) Virtiofs share + - `mapping` - Identifier of the directory mapping + - `cache` - (Optional) The caching mode + - `auto` + - `always` + - `metadata` + - `never` + - `direct_io` - (Optional) Whether to allow direct io + - `expose_acl` - (Optional) Enable POSIX ACLs, implies xattr support + - `expose_xattr` - (Optional) Enable support for extended attributes - `vm_id` - (Optional) The VM identifier. - `hook_script_file_id` - (Optional) The identifier for a file containing a hook script (needs to be executable, e.g. by using the `proxmox_virtual_environment_file.file_mode` attribute). - `watchdog` - (Optional) The watchdog configuration. Once enabled (by a guest action), the watchdog must be periodically polled by an agent inside the guest or else the watchdog will reset the guest (or execute the respective action specified). diff --git a/example/resource_virtual_environment_vm.tf b/example/resource_virtual_environment_vm.tf index b793029b9..d6d948c11 100644 --- a/example/resource_virtual_environment_vm.tf +++ b/example/resource_virtual_environment_vm.tf @@ -51,7 +51,7 @@ resource "proxmox_virtual_environment_vm" "example_template" { interface = "scsi0" discard = "on" cache = "writeback" - serial = "dead_beef" + serial = "dead_beef" ssd = true } diff --git a/fwprovider/test/resource_vm_test.go b/fwprovider/test/resource_vm_test.go index ae198179e..ce6108784 100644 --- a/fwprovider/test/resource_vm_test.go +++ b/fwprovider/test/resource_vm_test.go @@ -396,6 +396,51 @@ func TestAccResourceVM(t *testing.T) { ), }, }}, + // Depends on #1902 + // {"create virtiofs block", []resource.TestStep{ + // { + // Config: te.RenderConfig(` + // resource "proxmox_virtual_environment_hardware_mapping_dir" "test" { + // name = "test" + + // map { + // node = "{{.NodeName}}" + // path = "/mnt" + // } + // }`, WithRootUser()), + // Check: resource.ComposeTestCheckFunc( + // ResourceAttributes("proxmox_virtual_environment_hardware_mapping_dir.test", map[string]string{ + // "name": "test", + // "map.0.node": "{{.NodeName}}", + // "map.0.path": "/mnt", + // }), + // ), + // }, + // { + // Config: te.RenderConfig(` + // resource "proxmox_virtual_environment_vm" "test_vm" { + // node_name = "{{.NodeName}}" + // started = false + + // virtiofs { + // mapping = "test" + // cache = "always" + // direct_io = true + // expose_acl = false + // expose_xattr = false + // } + // }`, WithRootUser()), + // Check: resource.ComposeTestCheckFunc( + // ResourceAttributes("proxmox_virtual_environment_vm.test_vm", map[string]string{ + // "virtiofs.0.mapping": "test", + // "virtiofs.0.cache": "always", + // "virtiofs.0.direct_io": "true", + // "virtiofs.0.expose_acl": "false", + // "virtiofs.0.expose_xattr": "false", + // }), + // ), + // }, + // }}, } for _, tt := range tests { diff --git a/proxmox/nodes/vms/custom_virtiofs_share.go b/proxmox/nodes/vms/custom_virtiofs_share.go new file mode 100644 index 000000000..940661baf --- /dev/null +++ b/proxmox/nodes/vms/custom_virtiofs_share.go @@ -0,0 +1,131 @@ +/* + * 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" +) + +// CustomVirtiofsShare handles Virtiofs directory shares. +type CustomVirtiofsShare struct { + DirId string `json:"dirid" url:"dirid"` + Cache *string `json:"cache,omitempty" url:"cache,omitempty"` + DirectIo *types.CustomBool `json:"direct-io,omitempty" url:"direct-io,omitempty,int"` + ExposeAcl *types.CustomBool `json:"expose-acl,omitempty" url:"expose-acl,omitempty,int"` + ExposeXattr *types.CustomBool `json:"expose-xattr,omitempty" url:"expose-xattr,omitempty,int"` +} + +// CustomVirtiofsShares handles Virtiofs directory shares. +type CustomVirtiofsShares map[string]*CustomVirtiofsShare + +// EncodeValues converts a CustomVirtiofsShare struct to a URL value. +func (r *CustomVirtiofsShare) EncodeValues(key string, v *url.Values) error { + if r.ExposeAcl != nil && *r.ExposeAcl && r.ExposeXattr != nil && !*r.ExposeXattr { + // expose-xattr implies expose-acl + return errors.New("expose_xattr must be omitted or true when expose_acl is enabled") + } + + var values []string + values = append(values, fmt.Sprintf("dirid=%s", r.DirId)) + + if r.Cache != nil { + values = append(values, fmt.Sprintf("cache=%s", *r.Cache)) + } + + if r.DirectIo != nil { + if *r.DirectIo { + values = append(values, "direct-io=1") + } else { + values = append(values, "direct-io=0") + } + } + + if r.ExposeAcl != nil { + if *r.ExposeAcl { + values = append(values, "expose-acl=1") + } else { + values = append(values, "expose-acl=0") + } + } + + if r.ExposeXattr != nil && (r.ExposeAcl == nil || !*r.ExposeAcl) { + // expose-acl implies expose-xattr, omit it when unnecessary for consistency + if *r.ExposeXattr { + values = append(values, "expose-xattr=1") + } else { + values = append(values, "expose-xattr=0") + } + } + + v.Add(key, strings.Join(values, ",")) + + return nil +} + +// EncodeValues converts a CustomVirtiofsShares dict to multiple URL values. +func (r CustomVirtiofsShares) EncodeValues(key string, v *url.Values) error { + for s, d := range r { + if err := d.EncodeValues(s, v); err != nil { + return fmt.Errorf("failed to encode virtiofs share %s: %w", s, err) + } + } + + return nil +} + +// UnmarshalJSON converts a CustomVirtiofsShare string to an object. +func (r *CustomVirtiofsShare) UnmarshalJSON(b []byte) error { + var s string + + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("failed to unmarshal CustomVirtiofsShare: %w", err) + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + if len(v) == 1 { + r.DirId = v[0] + } else if len(v) == 2 { + switch v[0] { + case "dirid": + r.DirId = v[1] + case "cache": + r.Cache = &v[1] + case "direct-io": + bv := types.CustomBool(v[1] == "1") + r.DirectIo = &bv + case "expose-acl": + bv := types.CustomBool(v[1] == "1") + r.ExposeAcl = &bv + case "expose-xattr": + bv := types.CustomBool(v[1] == "1") + r.ExposeXattr = &bv + } + } + } + + // expose-acl implies expose-xattr + if r.ExposeAcl != nil && *r.ExposeAcl { + if r.ExposeXattr == nil { + bv := types.CustomBool(true) + r.ExposeAcl = &bv + } else if !*r.ExposeXattr { + return fmt.Errorf("failed to unmarshal CustomVirtiofsShare: expose-xattr contradicts the value of expose-acl") + } + } + + return nil +} diff --git a/proxmox/nodes/vms/custom_virtiofs_share_test.go b/proxmox/nodes/vms/custom_virtiofs_share_test.go new file mode 100644 index 000000000..885e23167 --- /dev/null +++ b/proxmox/nodes/vms/custom_virtiofs_share_test.go @@ -0,0 +1,79 @@ +/* + * 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 TestCustomVirtiofsShare_UnmarshalJSON(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + line string + want *CustomVirtiofsShare + wantErr bool + }{ + { + name: "id only virtiofs share", + line: `"test"`, + want: &CustomVirtiofsShare{ + DirId: ptr.Ptr("test"), + }, + }, + { + name: "virtiofs share with more details", + line: `"folder,cache=always"`, + want: &CustomVirtiofsShare{ + DirId: ptr.Ptr("folder"), + Cache: ptr.Ptr("always"), + }, + }, + { + name: "virtiofs share with flags", + line: `"folder,cache=never,direct-io=1,expose-acl=1"`, + want: &CustomVirtiofsShare{ + DirId: ptr.Ptr("folder"), + Cache: ptr.Ptr("never"), + DirectIo: types.CustomBool(true).Pointer(), + ExposeAcl: types.CustomBool(true).Pointer(), + ExposeXattr: types.CustomBool(true).Pointer(), + }, + }, + { + name: "virtiofs share with xattr", + line: `"folder,expose-xattr=1"`, + want: &CustomVirtiofsShare{ + DirId: ptr.Ptr("folder"), + Cache: nil, + DirectIo: types.CustomBool(false).Pointer(), + ExposeAcl: types.CustomBool(false).Pointer(), + ExposeXattr: types.CustomBool(true).Pointer(), + }, + }, + { + name: "virtiofs share invalid combination", + line: `"folder,expose-acl=1,expose-xattr=0"`, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + r := &CustomVirtiofsShare{} + 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/vms_types.go b/proxmox/nodes/vms/vms_types.go index cce2d6cfb..8a0b4fe24 100644 --- a/proxmox/nodes/vms/vms_types.go +++ b/proxmox/nodes/vms/vms_types.go @@ -98,6 +98,7 @@ type CreateRequestBody struct { USBDevices CustomUSBDevices `json:"usb,omitempty" url:"usb,omitempty"` VGADevice *CustomVGADevice `json:"vga,omitempty" url:"vga,omitempty"` VirtualCPUCount *int64 `json:"vcpus,omitempty" url:"vcpus,omitempty"` + VirtiofsShares CustomVirtiofsShares `json:"virtiofs,omitempty" url:"virtiofs,omitempty"` VMGenerationID *string `json:"vmgenid,omitempty" url:"vmgenid,omitempty"` VMID int `json:"vmid,omitempty" url:"vmid,omitempty"` VMStateDatastoreID *string `json:"vmstatestorage,omitempty" url:"vmstatestorage,omitempty"` @@ -320,6 +321,7 @@ type GetResponseData struct { WatchdogDevice *CustomWatchdogDevice `json:"watchdog,omitempty"` StorageDevices CustomStorageDevices `json:"-"` PCIDevices CustomPCIDevices `json:"-"` + VirtiofsShares CustomVirtiofsShares `json:"-"` } // GetStatusResponseBody contains the body from a VM get status response. @@ -469,6 +471,7 @@ func (d *GetResponseData) UnmarshalJSON(b []byte) error { data.StorageDevices = make(CustomStorageDevices) data.PCIDevices = make(CustomPCIDevices) + data.VirtiofsShares = make(CustomVirtiofsShares) for key, value := range byAttr { for _, prefix := range StorageInterfaces { @@ -493,6 +496,15 @@ func (d *GetResponseData) UnmarshalJSON(b []byte) error { data.PCIDevices[key] = &device } + + if r := regexp.MustCompile(`^virtiofs\d+$`); r.MatchString(key) { + var share CustomVirtiofsShare + if err := json.Unmarshal([]byte(`"`+value.(string)+`"`), &share); err != nil { + return fmt.Errorf("failed to unmarshal %s: %w", key, err) + } + + data.VirtiofsShares[key] = &share + } } *d = GetResponseData(data) diff --git a/proxmox/nodes/vms/vms_types_test.go b/proxmox/nodes/vms/vms_types_test.go index e18936129..7343bc104 100644 --- a/proxmox/nodes/vms/vms_types_test.go +++ b/proxmox/nodes/vms/vms_types_test.go @@ -28,7 +28,8 @@ func TestUnmarshalGetResponseData(t *testing.T) { "scsi22": "%[1]s", "hostpci0": "0000:81:00.2", "hostpci1": "host=81:00.4,pcie=0,rombar=1,x-vga=0", - "hostpci12": "mapping=mappeddevice,pcie=0,rombar=1,x-vga=0" + "hostpci12": "mapping=mappeddevice,pcie=0,rombar=1,x-vga=0", + "virtiofs0":"test,cache=always,direct-io=1,expose-acl=1" }`, "local-lvm:vm-100-disk-0,aio=io_uring,backup=1,cache=none,discard=ignore,replicate=1,size=8G,ssd=1") var data GetResponseData @@ -57,6 +58,10 @@ func TestUnmarshalGetResponseData(t *testing.T) { assert.NotNil(t, data.PCIDevices["hostpci0"]) assert.NotNil(t, data.PCIDevices["hostpci1"]) assert.NotNil(t, data.PCIDevices["hostpci12"]) + + assert.NotNil(t, data.VirtiofsShares) + assert.Len(t, data.VirtiofsShares, 1) + assert.Equal(t, "always", *data.VirtiofsShares["virtiofs0"].Cache) } func assertDevice(t *testing.T, dev *CustomStorageDevice) { diff --git a/proxmoxtf/resource/vm/validators.go b/proxmoxtf/resource/vm/validators.go index d610403a8..d551fd47e 100755 --- a/proxmoxtf/resource/vm/validators.go +++ b/proxmoxtf/resource/vm/validators.go @@ -268,6 +268,16 @@ func IDEInterfaceValidator() schema.SchemaValidateDiagFunc { }, false)) } +// VirtiofsCacheValidator is a schema validation function for virtiofs cache configs. +func VirtiofsCacheValidator() schema.SchemaValidateDiagFunc { + return validation.ToDiagFunc(validation.StringInSlice([]string{ + "auto", + "always", + "metadata", + "never", + }, false)) +} + // CloudInitInterfaceValidator is a schema validation function that accepts either an IDE interface identifier or an // empty string, which is used as the default and means "detect which interface should be used automatically". func CloudInitInterfaceValidator() schema.SchemaValidateDiagFunc { diff --git a/proxmoxtf/resource/vm/vm.go b/proxmoxtf/resource/vm/vm.go index 347ed67d7..723ac833a 100644 --- a/proxmoxtf/resource/vm/vm.go +++ b/proxmoxtf/resource/vm/vm.go @@ -129,6 +129,10 @@ const ( dvVGAClipboard = "" dvVGAMemory = 16 dvVGAType = "std" + dvVirtiofsCache = "auto" + dvVirtiofsDirectIo = false + dvVirtiofsExposeAcl = false + dvVirtiofsExposeXattr = false dvSCSIHardware = "virtio-scsi-pci" dvStopOnDestroy = false dvHookScript = "" @@ -141,7 +145,8 @@ const ( maxResourceVirtualEnvironmentVMHostPCIDevices = 16 maxResourceVirtualEnvironmentVMHostUSBDevices = 4 // hardcoded /usr/share/perl5/PVE/QemuServer/Memory.pm: "our $MAX_NUMA = 8". - maxResourceVirtualEnvironmentVMNUMADevices = 8 + maxResourceVirtualEnvironmentVMNUMADevices = 8 + maxResourceVirtualEnvironmentVirtiofsShares = 8 mkRebootAfterCreation = "reboot" mkRebootAfterUpdate = "reboot_after_update" @@ -286,6 +291,12 @@ const ( mkSCSIHardware = "scsi_hardware" mkHookScriptFileID = "hook_script_file_id" mkStopOnDestroy = "stop_on_destroy" + mkVirtiofs = "virtiofs" + mkVirtiofsMapping = "mapping" + mkVirtiofsCache = "cache" + mkVirtiofsDirectIo = "direct_io" + mkVirtiofsExposeAcl = "expose_acl" + mkVirtiofsExposeXattr = "expose_xattr" mkWatchdog = "watchdog" // a workaround for the lack of proper support of default and undefined values in SDK. mkWatchdogEnabled = "enabled" @@ -1465,6 +1476,51 @@ func VM() *schema.Resource { MaxItems: 1, MinItems: 0, }, + mkVirtiofs: { + Type: schema.TypeList, + Description: "Virtiofs share configuration", + Optional: true, + DefaultFunc: func() (interface{}, error) { + return []interface{}{}, nil + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + mkVirtiofsMapping: { + Type: schema.TypeString, + Description: "Directory mapping identifier", + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + mkVirtiofsCache: { + Type: schema.TypeString, + Description: "The caching mode", + Optional: true, + Default: dvVirtiofsCache, + ValidateDiagFunc: VirtiofsCacheValidator(), + }, + mkVirtiofsDirectIo: { + Type: schema.TypeBool, + Description: "Whether to allow direct io", + Optional: true, + Default: dvVirtiofsDirectIo, + }, + mkVirtiofsExposeAcl: { + Type: schema.TypeBool, + Description: "Enable POSIX ACLs, implies xattr support", + Optional: true, + Default: dvVirtiofsExposeAcl, + }, + mkVirtiofsExposeXattr: { + Type: schema.TypeBool, + Description: "Enable support for extended attributes", + Optional: true, + Default: dvVirtiofsExposeXattr, + }, + }, + }, + MaxItems: maxResourceVirtualEnvironmentVirtiofsShares, + MinItems: 0, + }, mkVMID: { Type: schema.TypeInt, Description: "The VM identifier", @@ -1899,6 +1955,7 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d tabletDevice := types.CustomBool(d.Get(mkTabletDevice).(bool)) template := types.CustomBool(d.Get(mkTemplate).(bool)) vga := d.Get(mkVGA).([]interface{}) + virtiofs := d.Get(mkVirtiofs).([]interface{}) watchdog := d.Get(mkWatchdog).([]interface{}) updateBody := &vms.UpdateRequestBody{ @@ -2155,6 +2212,11 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d updateBody.VGADevice = vgaDevice } + if len(virtiofs) > 0 { + virtiofsShares := vmGetVirtiofsShares(d) + updateBody.VirtiofsShares = virtiofsShares + } + hookScript := d.Get(mkHookScriptFileID).(string) currentHookScript := vmConfig.HookScript @@ -2535,6 +2597,7 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) tabletDevice := types.CustomBool(d.Get(mkTabletDevice).(bool)) template := types.CustomBool(d.Get(mkTemplate).(bool)) + virtiofsShares := vmGetVirtiofsShares(d) vgaDevice := vmGetVGADeviceObject(d) vmIDUntyped, hasVMID := d.GetOk(mkVMID) @@ -2693,6 +2756,7 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) TabletDeviceEnabled: &tabletDevice, Template: &template, USBDevices: usbDeviceObjects, + VirtiofsShares: virtiofsShares, VGADevice: vgaDevice, VMID: vmID, WatchdogDevice: watchdogObject, @@ -3347,6 +3411,41 @@ func vmGetTagsString(d *schema.ResourceData) string { return strings.Join(sanitizedTags, ";") } +func vmGetVirtiofsShares(d *schema.ResourceData) vms.CustomVirtiofsShares { + virtiofs := d.Get(mkVirtiofs).([]interface{}) + virtiofsShares := make(vms.CustomVirtiofsShares, len(virtiofs)) + + for i, virtiofsShare := range virtiofs { + block := virtiofsShare.(map[string]interface{}) + + mapping, _ := block[mkVirtiofsMapping].(string) + cache, _ := block[mkVirtiofsCache].(string) + direct_io := types.CustomBool(block[mkVirtiofsDirectIo].(bool)) + expose_acl := types.CustomBool(block[mkVirtiofsExposeAcl].(bool)) + expose_xattr := types.CustomBool(block[mkVirtiofsExposeXattr].(bool)) + + share := vms.CustomVirtiofsShare{ + DirId: mapping, + DirectIo: &direct_io, + ExposeAcl: &expose_acl, + ExposeXattr: &expose_xattr, + } + + if cache != "" { + share.Cache = &cache + } + + if share.ExposeAcl != nil && *share.ExposeAcl && share.ExposeXattr == nil { + bv := types.CustomBool(true) + share.ExposeXattr = &bv + } + + virtiofsShares[fmt.Sprintf("virtiofs%d", i)] = &share + } + + return virtiofsShares +} + func vmGetVGADeviceObject(d *schema.ResourceData) *vms.CustomVGADevice { vga := d.Get(mkVGA).([]interface{}) if len(vga) > 0 && vga[0] != nil { @@ -3946,6 +4045,55 @@ func vmReadCustom( diags = append(diags, diag.FromErr(err)...) } + currentVirtiofsList := d.Get(mkVirtiofs).([]interface{}) + virtiofsMap := map[string]interface{}{} + + for pi, pp := range vmConfig.VirtiofsShares { + if (pp == nil) { + continue + } + + share := map[string]interface{}{} + + share[mkVirtiofsMapping] = pp.DirId + + if pp.Cache != nil { + share[mkVirtiofsCache] = *pp.Cache + } else { + share[mkVirtiofsCache] = dvVirtiofsCache + } + + if pp.DirectIo != nil { + share[mkVirtiofsDirectIo] = *pp.DirectIo + } else { + share[mkVirtiofsDirectIo] = dvVirtiofsDirectIo + } + + if pp.ExposeAcl != nil { + share[mkVirtiofsExposeAcl] = *pp.ExposeAcl + } else { + share[mkVirtiofsExposeAcl] = dvVirtiofsExposeAcl + } + + switch { + case pp.ExposeXattr != nil: + share[mkVirtiofsExposeXattr] = *pp.ExposeXattr + case pp.ExposeAcl != nil && bool(*pp.ExposeAcl): + // expose-xattr implies expose-acl + share[mkVirtiofsExposeXattr] = true + default: + share[mkVirtiofsExposeXattr] = dvVirtiofsExposeXattr + } + + virtiofsMap[pi] = share + } + + if len(clone) == 0 || len(currentVirtiofsList) > 0 { + orderedVirtiofsList := utils.OrderedListFromMap(virtiofsMap) + err := d.Set(mkVirtiofs, orderedVirtiofsList) + diags = append(diags, diag.FromErr(err)...) + } + // Compare the initialization configuration to the one stored in the state. initialization := map[string]interface{}{} @@ -5339,6 +5487,17 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D rebootRequired = true } + // Prepare the new Virtiofs shares configuration. + if d.HasChange(mkVirtiofs) { + updateBody.VirtiofsShares = vmGetVirtiofsShares(d) + + for i := len(updateBody.VirtiofsShares); i < maxResourceVirtualEnvironmentVirtiofsShares; i++ { + del = append(del, fmt.Sprintf("virtiofs%d", i)) + } + + rebootRequired = true + } + // Prepare the new SCSI hardware type if d.HasChange(mkSCSIHardware) { scsiHardware := d.Get(mkSCSIHardware).(string) diff --git a/proxmoxtf/resource/vm/vm_test.go b/proxmoxtf/resource/vm/vm_test.go index 5e1227004..7f78eae25 100644 --- a/proxmoxtf/resource/vm/vm_test.go +++ b/proxmoxtf/resource/vm/vm_test.go @@ -64,6 +64,7 @@ func TestVMSchema(t *testing.T) { mkStarted, mkTabletDevice, mkTemplate, + mkVirtiofs, mkVMID, mkSCSIHardware, }) @@ -93,6 +94,7 @@ func TestVMSchema(t *testing.T) { mkStarted: schema.TypeBool, mkTabletDevice: schema.TypeBool, mkTemplate: schema.TypeBool, + mkVirtiofs: schema.TypeList, mkVMID: schema.TypeInt, mkSCSIHardware: schema.TypeString, }) @@ -382,6 +384,23 @@ func TestVMSchema(t *testing.T) { mkSerialDeviceDevice: schema.TypeString, }) + virtiofsSchema := test.AssertNestedSchemaExistence(t, s, mkVirtiofs) + + test.AssertOptionalArguments(t, virtiofsSchema, []string{ + mkVirtiofsCache, + mkVirtiofsDirectIo, + mkVirtiofsExposeAcl, + mkVirtiofsExposeXattr, + }) + + test.AssertValueTypes(t, virtiofsSchema, map[string]schema.ValueType{ + mkVirtiofsDirId: schema.TypeString, + mkVirtiofsCache: schema.TypeString, + mkVirtiofsDirectIo: schema.TypeBool, + mkVirtiofsExposeAcl: schema.TypeBool, + mkVirtiofsExposeXattr: schema.TypeBool, + }) + vgaSchema := test.AssertNestedSchemaExistence(t, s, mkVGA) test.AssertOptionalArguments(t, vgaSchema, []string{ From abab01bff3866f1ba234aee646814e67480e91d2 Mon Sep 17 00:00:00 2001 From: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Date: Tue, 15 Apr 2025 13:06:47 -0400 Subject: [PATCH 2/2] fix tests Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- proxmox/nodes/vms/custom_virtiofs_share_test.go | 8 ++++---- proxmoxtf/resource/vm/vm.go | 4 ++-- proxmoxtf/resource/vm/vm_test.go | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/proxmox/nodes/vms/custom_virtiofs_share_test.go b/proxmox/nodes/vms/custom_virtiofs_share_test.go index 885e23167..d8b196d48 100644 --- a/proxmox/nodes/vms/custom_virtiofs_share_test.go +++ b/proxmox/nodes/vms/custom_virtiofs_share_test.go @@ -26,14 +26,14 @@ func TestCustomVirtiofsShare_UnmarshalJSON(t *testing.T) { name: "id only virtiofs share", line: `"test"`, want: &CustomVirtiofsShare{ - DirId: ptr.Ptr("test"), + DirId: "test", }, }, { name: "virtiofs share with more details", line: `"folder,cache=always"`, want: &CustomVirtiofsShare{ - DirId: ptr.Ptr("folder"), + DirId: "folder", Cache: ptr.Ptr("always"), }, }, @@ -41,7 +41,7 @@ func TestCustomVirtiofsShare_UnmarshalJSON(t *testing.T) { name: "virtiofs share with flags", line: `"folder,cache=never,direct-io=1,expose-acl=1"`, want: &CustomVirtiofsShare{ - DirId: ptr.Ptr("folder"), + DirId: "folder", Cache: ptr.Ptr("never"), DirectIo: types.CustomBool(true).Pointer(), ExposeAcl: types.CustomBool(true).Pointer(), @@ -52,7 +52,7 @@ func TestCustomVirtiofsShare_UnmarshalJSON(t *testing.T) { name: "virtiofs share with xattr", line: `"folder,expose-xattr=1"`, want: &CustomVirtiofsShare{ - DirId: ptr.Ptr("folder"), + DirId: "folder", Cache: nil, DirectIo: types.CustomBool(false).Pointer(), ExposeAcl: types.CustomBool(false).Pointer(), diff --git a/proxmoxtf/resource/vm/vm.go b/proxmoxtf/resource/vm/vm.go index 723ac833a..d4dcfe04d 100644 --- a/proxmoxtf/resource/vm/vm.go +++ b/proxmoxtf/resource/vm/vm.go @@ -292,7 +292,7 @@ const ( mkHookScriptFileID = "hook_script_file_id" mkStopOnDestroy = "stop_on_destroy" mkVirtiofs = "virtiofs" - mkVirtiofsMapping = "mapping" + mkVirtiofsMapping = "mapping" mkVirtiofsCache = "cache" mkVirtiofsDirectIo = "direct_io" mkVirtiofsExposeAcl = "expose_acl" @@ -4049,7 +4049,7 @@ func vmReadCustom( virtiofsMap := map[string]interface{}{} for pi, pp := range vmConfig.VirtiofsShares { - if (pp == nil) { + if pp == nil { continue } diff --git a/proxmoxtf/resource/vm/vm_test.go b/proxmoxtf/resource/vm/vm_test.go index 7f78eae25..c072ae705 100644 --- a/proxmoxtf/resource/vm/vm_test.go +++ b/proxmoxtf/resource/vm/vm_test.go @@ -394,7 +394,7 @@ func TestVMSchema(t *testing.T) { }) test.AssertValueTypes(t, virtiofsSchema, map[string]schema.ValueType{ - mkVirtiofsDirId: schema.TypeString, + mkVirtiofsMapping: schema.TypeString, mkVirtiofsCache: schema.TypeString, mkVirtiofsDirectIo: schema.TypeBool, mkVirtiofsExposeAcl: schema.TypeBool,