0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-06-30 02:31:10 +00:00

feat(vm): add support for virtiofs (#1900)

Signed-off-by: Fina Wilke <code@felinira.net>
Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
Fina 2025-04-15 19:10:37 +02:00 committed by GitHub
parent ad41476962
commit 55b3f7391a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 479 additions and 3 deletions

View File

@ -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 arent 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).

View File

@ -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 {

View File

@ -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
}

View File

@ -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: "test",
},
},
{
name: "virtiofs share with more details",
line: `"folder,cache=always"`,
want: &CustomVirtiofsShare{
DirId: "folder",
Cache: ptr.Ptr("always"),
},
},
{
name: "virtiofs share with flags",
line: `"folder,cache=never,direct-io=1,expose-acl=1"`,
want: &CustomVirtiofsShare{
DirId: "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: "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)
}
})
}
}

View File

@ -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)

View File

@ -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) {

View File

@ -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 {

View File

@ -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 = ""
@ -142,6 +146,7 @@ const (
maxResourceVirtualEnvironmentVMHostUSBDevices = 4
// hardcoded /usr/share/perl5/PVE/QemuServer/Memory.pm: "our $MAX_NUMA = 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)

View File

@ -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{
mkVirtiofsMapping: 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{