Skip to content

feat(vm): add support for virtiofs #1900

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/resources/virtual_environment_vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion example/resource_virtual_environment_vm.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
45 changes: 45 additions & 0 deletions fwprovider/test/resource_vm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
131 changes: 131 additions & 0 deletions proxmox/nodes/vms/custom_virtiofs_share.go
Original file line number Diff line number Diff line change
@@ -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
}
79 changes: 79 additions & 0 deletions proxmox/nodes/vms/custom_virtiofs_share_test.go
Original file line number Diff line number Diff line change
@@ -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"),

Check failure on line 29 in proxmox/nodes/vms/custom_virtiofs_share_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

cannot use ptr.Ptr("test") (value of type *string) as string value in struct literal
},
},
{
name: "virtiofs share with more details",
line: `"folder,cache=always"`,
want: &CustomVirtiofsShare{
DirId: ptr.Ptr("folder"),

Check failure on line 36 in proxmox/nodes/vms/custom_virtiofs_share_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

cannot use ptr.Ptr("folder") (value of type *string) as string value in struct literal
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"),

Check failure on line 44 in proxmox/nodes/vms/custom_virtiofs_share_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

cannot use ptr.Ptr("folder") (value of type *string) as string value in struct literal
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"),

Check failure on line 55 in proxmox/nodes/vms/custom_virtiofs_share_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

cannot use ptr.Ptr("folder") (value of type *string) as string value in struct literal (typecheck)
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)
}
})
}
}
12 changes: 12 additions & 0 deletions proxmox/nodes/vms/vms_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion proxmox/nodes/vms/vms_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
10 changes: 10 additions & 0 deletions proxmoxtf/resource/vm/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading