Skip to content

Commit 3c82121

Browse files
committed
feat(vm): Implement virtiofs
Signed-off-by: Fina Wilke <code@felinira.net>
1 parent 35a5296 commit 3c82121

File tree

10 files changed

+462
-3
lines changed

10 files changed

+462
-3
lines changed

docs/resources/virtual_environment_vm.md

+16
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ resource "proxmox_virtual_environment_vm" "ubuntu_vm" {
8080
}
8181
8282
serial_device {}
83+
84+
virtiofs {
85+
dir_id = "data_share"
86+
cache = "always"
87+
direct-io = true
88+
}
8389
}
8490
8591
resource "proxmox_virtual_environment_download_file" "latest_ubuntu_22_jammy_qcow2_img" {
@@ -559,6 +565,16 @@ output "ubuntu_vm_public_key" {
559565
- `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.
560566
- `vmware` - VMware Compatible.
561567
- `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.
568+
- `virtiofs` - (Optional) Virtiofs share
569+
- `dir_id` - Identifier of the directory mapping
570+
- `cache` - (Optional) The caching mode
571+
- `auto`
572+
- `always`
573+
- `metadata`
574+
- `never`
575+
- `direct_io` - (Optional) Whether to allow direct io
576+
- `expose_acl` - (Optional) Enable POSIX ACLs, implies xattr support
577+
- `expose_xattr` - (Optional) Enable support for extended attributes
562578
- `vm_id` - (Optional) The VM identifier.
563579
- `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).
564580
- `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).

example/resource_virtual_environment_vm.tf

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ resource "proxmox_virtual_environment_vm" "example_template" {
5151
interface = "scsi0"
5252
discard = "on"
5353
cache = "writeback"
54-
serial = "dead_beef"
54+
serial = "dead_beef"
5555
ssd = true
5656
}
5757

fwprovider/test/resource_vm_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,32 @@ func TestAccResourceVM(t *testing.T) {
396396
),
397397
},
398398
}},
399+
{"create virtiofs block", []resource.TestStep{
400+
{
401+
Config: te.RenderConfig(`
402+
resource "proxmox_virtual_environment_vm" "test_vm" {
403+
node_name = "{{.NodeName}}"
404+
started = false
405+
406+
virtiofs {
407+
dir_id = "test"
408+
cache = "always"
409+
direct_io = true
410+
expose_acl = false
411+
expose_xattr = false
412+
}
413+
}`, WithRootUser()),
414+
Check: resource.ComposeTestCheckFunc(
415+
ResourceAttributes("proxmox_virtual_environment_vm.test_vm", map[string]string{
416+
"virtiofs.0.dir_id": "test",
417+
"virtiofs.0.cache": "always",
418+
"virtiofs.0.direct_io": "true",
419+
"virtiofs.0.expose_acl": "false",
420+
"virtiofs.0.expose_xattr": "false",
421+
}),
422+
),
423+
},
424+
}},
399425
}
400426

401427
for _, tt := range tests {
+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
*/
6+
7+
package vms
8+
9+
import (
10+
"encoding/json"
11+
"fmt"
12+
"net/url"
13+
"strings"
14+
15+
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
16+
)
17+
18+
// CustomVirtiofsShare handles Virtiofs directory shares.
19+
type CustomVirtiofsShare struct {
20+
DirId *string `json:"dirid" url:"dirid"`
21+
Cache *string `json:"cache,omitempty" url:"cache,omitempty"`
22+
DirectIo *types.CustomBool `json:"direct-io,omitempty" url:"direct-io,omitempty,int"`
23+
ExposeAcl *types.CustomBool `json:"expose-acl,omitempty" url:"expose-acl,omitempty,int"`
24+
ExposeXattr *types.CustomBool `json:"expose-xattr,omitempty" url:"expose-xattr,omitempty,int"`
25+
}
26+
27+
// CustomVirtiofsShares handles Virtiofs directory shares.
28+
type CustomVirtiofsShares map[string]*CustomVirtiofsShare
29+
30+
// EncodeValues converts a CustomVirtiofsShare struct to a URL value.
31+
func (r *CustomVirtiofsShare) EncodeValues(key string, v *url.Values) error {
32+
if r.DirId == nil {
33+
return fmt.Errorf("dir_id must be set")
34+
}
35+
36+
if r.ExposeAcl != nil && *r.ExposeAcl && r.ExposeXattr != nil && !*r.ExposeXattr {
37+
// expose-xattr implies expose-acl
38+
return fmt.Errorf("expose_xattr must be omitted or true when expose_acl is enabled")
39+
}
40+
41+
var values []string
42+
values = append(values, fmt.Sprintf("dirid=%s", *(r.DirId)))
43+
44+
if r.Cache != nil {
45+
values = append(values, fmt.Sprintf("cache=%s", *r.Cache))
46+
}
47+
48+
if r.DirectIo != nil {
49+
if *r.DirectIo {
50+
values = append(values, "direct-io=1")
51+
} else {
52+
values = append(values, "direct-io=0")
53+
}
54+
}
55+
56+
if r.ExposeAcl != nil {
57+
if *r.ExposeAcl {
58+
values = append(values, "expose-acl=1")
59+
} else {
60+
values = append(values, "expose-acl=0")
61+
}
62+
}
63+
64+
if r.ExposeXattr != nil && (r.ExposeAcl == nil || !*r.ExposeAcl) {
65+
// expose-acl implies expose-xattr, omit it when unnecessary for consistency
66+
if *r.ExposeXattr {
67+
values = append(values, "expose-xattr=1")
68+
} else {
69+
values = append(values, "expose-xattr=0")
70+
}
71+
}
72+
73+
v.Add(key, strings.Join(values, ","))
74+
75+
return nil
76+
}
77+
78+
// EncodeValues converts a CustomVirtiofsShares dict to multiple URL values.
79+
func (r CustomVirtiofsShares) EncodeValues(key string, v *url.Values) error {
80+
for s, d := range r {
81+
if err := d.EncodeValues(s, v); err != nil {
82+
return fmt.Errorf("failed to encode virtiofs share %s: %w", s, err)
83+
}
84+
}
85+
86+
return nil
87+
}
88+
89+
// UnmarshalJSON converts a CustomVirtiofsShare string to an object.
90+
func (r *CustomVirtiofsShare) UnmarshalJSON(b []byte) error {
91+
var s string
92+
93+
if err := json.Unmarshal(b, &s); err != nil {
94+
return fmt.Errorf("failed to unmarshal CustomVirtiofsShare: %w", err)
95+
}
96+
97+
pairs := strings.Split(s, ",")
98+
99+
for _, p := range pairs {
100+
v := strings.Split(strings.TrimSpace(p), "=")
101+
102+
if len(v) == 1 {
103+
r.DirId = &v[0]
104+
} else if len(v) == 2 {
105+
switch v[0] {
106+
case "dirid":
107+
r.DirId = &v[1]
108+
case "cache":
109+
r.Cache = &v[1]
110+
case "direct-io":
111+
bv := types.CustomBool(v[1] == "1")
112+
r.DirectIo = &bv
113+
case "expose-acl":
114+
bv := types.CustomBool(v[1] == "1")
115+
r.ExposeAcl = &bv
116+
case "expose-xattr":
117+
bv := types.CustomBool(v[1] == "1")
118+
r.ExposeXattr = &bv
119+
}
120+
}
121+
}
122+
123+
// expose-acl implies expose-xattr
124+
if r.ExposeAcl != nil && *r.ExposeAcl {
125+
if r.ExposeXattr == nil {
126+
bv := types.CustomBool(true)
127+
r.ExposeAcl = &bv
128+
} else if !*r.ExposeXattr {
129+
return fmt.Errorf("failed to unmarshal CustomVirtiofsShare: expose-xattr contradicts the value of expose-acl")
130+
}
131+
}
132+
133+
return nil
134+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
*/
6+
7+
package vms
8+
9+
import (
10+
"testing"
11+
12+
"github.com/bpg/terraform-provider-proxmox/proxmox/helpers/ptr"
13+
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
14+
)
15+
16+
func TestCustomVirtiofsShare_UnmarshalJSON(t *testing.T) {
17+
t.Parallel()
18+
19+
tests := []struct {
20+
name string
21+
line string
22+
want *CustomVirtiofsShare
23+
wantErr bool
24+
}{
25+
{
26+
name: "id only virtiofs share",
27+
line: `"test"`,
28+
want: &CustomVirtiofsShare{
29+
DirId: ptr.Ptr("test"),
30+
},
31+
},
32+
{
33+
name: "virtiofs share with more details",
34+
line: `"folder,cache=always"`,
35+
want: &CustomVirtiofsShare{
36+
DirId: ptr.Ptr("folder"),
37+
Cache: ptr.Ptr("always"),
38+
},
39+
},
40+
{
41+
name: "virtiofs share with flags",
42+
line: `"folder,cache=never,direct-io=1,expose-acl=1"`,
43+
want: &CustomVirtiofsShare{
44+
DirId: ptr.Ptr("folder"),
45+
Cache: ptr.Ptr("never"),
46+
DirectIo: types.CustomBool(true).Pointer(),
47+
ExposeAcl: types.CustomBool(true).Pointer(),
48+
ExposeXattr: types.CustomBool(true).Pointer(),
49+
},
50+
},
51+
{
52+
name: "virtiofs share with xattr",
53+
line: `"folder,expose-xattr=1"`,
54+
want: &CustomVirtiofsShare{
55+
DirId: ptr.Ptr("folder"),
56+
Cache: nil,
57+
DirectIo: types.CustomBool(false).Pointer(),
58+
ExposeAcl: types.CustomBool(false).Pointer(),
59+
ExposeXattr: types.CustomBool(true).Pointer(),
60+
},
61+
},
62+
{
63+
name: "virtiofs share invalid combination",
64+
line: `"folder,expose-acl=1,expose-xattr=0"`,
65+
wantErr: true,
66+
},
67+
}
68+
69+
for _, tt := range tests {
70+
t.Run(tt.name, func(t *testing.T) {
71+
t.Parallel()
72+
73+
r := &CustomVirtiofsShare{}
74+
if err := r.UnmarshalJSON([]byte(tt.line)); (err != nil) != tt.wantErr {
75+
t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
76+
}
77+
})
78+
}
79+
}

proxmox/nodes/vms/vms_types.go

+12
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ type CreateRequestBody struct {
9898
USBDevices CustomUSBDevices `json:"usb,omitempty" url:"usb,omitempty"`
9999
VGADevice *CustomVGADevice `json:"vga,omitempty" url:"vga,omitempty"`
100100
VirtualCPUCount *int64 `json:"vcpus,omitempty" url:"vcpus,omitempty"`
101+
VirtiofsShares CustomVirtiofsShares `json:"virtiofs,omitempty" url:"virtiofs,omitempty"`
101102
VMGenerationID *string `json:"vmgenid,omitempty" url:"vmgenid,omitempty"`
102103
VMID int `json:"vmid,omitempty" url:"vmid,omitempty"`
103104
VMStateDatastoreID *string `json:"vmstatestorage,omitempty" url:"vmstatestorage,omitempty"`
@@ -320,6 +321,7 @@ type GetResponseData struct {
320321
WatchdogDevice *CustomWatchdogDevice `json:"watchdog,omitempty"`
321322
StorageDevices CustomStorageDevices `json:"-"`
322323
PCIDevices CustomPCIDevices `json:"-"`
324+
VirtiofsShares CustomVirtiofsShares `json:"-"`
323325
}
324326

325327
// GetStatusResponseBody contains the body from a VM get status response.
@@ -469,6 +471,7 @@ func (d *GetResponseData) UnmarshalJSON(b []byte) error {
469471

470472
data.StorageDevices = make(CustomStorageDevices)
471473
data.PCIDevices = make(CustomPCIDevices)
474+
data.VirtiofsShares = make(CustomVirtiofsShares)
472475

473476
for key, value := range byAttr {
474477
for _, prefix := range StorageInterfaces {
@@ -493,6 +496,15 @@ func (d *GetResponseData) UnmarshalJSON(b []byte) error {
493496

494497
data.PCIDevices[key] = &device
495498
}
499+
500+
if r := regexp.MustCompile(`^virtiofs\d+$`); r.MatchString(key) {
501+
var share CustomVirtiofsShare
502+
if err := json.Unmarshal([]byte(`"`+value.(string)+`"`), &share); err != nil {
503+
return fmt.Errorf("failed to unmarshal %s: %w", key, err)
504+
}
505+
506+
data.VirtiofsShares[key] = &share
507+
}
496508
}
497509

498510
*d = GetResponseData(data)

proxmox/nodes/vms/vms_types_test.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ func TestUnmarshalGetResponseData(t *testing.T) {
2828
"scsi22": "%[1]s",
2929
"hostpci0": "0000:81:00.2",
3030
"hostpci1": "host=81:00.4,pcie=0,rombar=1,x-vga=0",
31-
"hostpci12": "mapping=mappeddevice,pcie=0,rombar=1,x-vga=0"
31+
"hostpci12": "mapping=mappeddevice,pcie=0,rombar=1,x-vga=0",
32+
"virtiofs0":"test,cache=always,direct-io=1,expose-acl=1"
3233
}`, "local-lvm:vm-100-disk-0,aio=io_uring,backup=1,cache=none,discard=ignore,replicate=1,size=8G,ssd=1")
3334

3435
var data GetResponseData
@@ -57,6 +58,10 @@ func TestUnmarshalGetResponseData(t *testing.T) {
5758
assert.NotNil(t, data.PCIDevices["hostpci0"])
5859
assert.NotNil(t, data.PCIDevices["hostpci1"])
5960
assert.NotNil(t, data.PCIDevices["hostpci12"])
61+
62+
assert.NotNil(t, data.VirtiofsShares)
63+
assert.Len(t, data.VirtiofsShares, 1)
64+
assert.Equal(t, "always", *data.VirtiofsShares["virtiofs0"].Cache)
6065
}
6166

6267
func assertDevice(t *testing.T, dev *CustomStorageDevice) {

proxmoxtf/resource/vm/validators.go

+10
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,16 @@ func IDEInterfaceValidator() schema.SchemaValidateDiagFunc {
268268
}, false))
269269
}
270270

271+
// VirtiofsCacheValidator is a schema validation function for virtiofs cache configs.
272+
func VirtiofsCacheValidator() schema.SchemaValidateDiagFunc {
273+
return validation.ToDiagFunc(validation.StringInSlice([]string{
274+
"auto",
275+
"always",
276+
"metadata",
277+
"never",
278+
}, false))
279+
}
280+
271281
// CloudInitInterfaceValidator is a schema validation function that accepts either an IDE interface identifier or an
272282
// empty string, which is used as the default and means "detect which interface should be used automatically".
273283
func CloudInitInterfaceValidator() schema.SchemaValidateDiagFunc {

0 commit comments

Comments
 (0)