From 7cbd1b46fad999fe8a0ae5608d57ad708d9635c5 Mon Sep 17 00:00:00 2001 From: Petr Gajdosik Date: Thu, 16 Jan 2025 23:58:49 +0100 Subject: [PATCH] feat(lxc): add support for `device_passthrough` config option (#1706) Signed-off-by: Tarasa24 --- .../virtual_environment_container.md | 7 + fwprovider/test/resource_container_test.go | 6 + proxmox/nodes/containers/containers_types.go | 282 +++++++++++++----- proxmoxtf/resource/container/container.go | 173 +++++++++++ .../resource/container/container_test.go | 57 ++-- 5 files changed, 427 insertions(+), 98 deletions(-) diff --git a/docs/resources/virtual_environment_container.md b/docs/resources/virtual_environment_container.md index f443b6bb..1ee286d2 100644 --- a/docs/resources/virtual_environment_container.md +++ b/docs/resources/virtual_environment_container.md @@ -170,6 +170,13 @@ output "ubuntu_container_public_key" { Can be specified with a unit suffix (e.g. `10G`). - `volume` (Required) Volume, device or directory to mount into the container. +- `device_passthrough` - (Optional) Device to pass through to the container (multiple blocks supported). + - `deny_write` - (Optional) Deny the container to write to the device (defaults to `false`). + - `gid` - (Optional) Group ID to be assigned to the device node. + - `mode` - (Optional) Access mode to be set on the device node. Must be a + 4-digit octal number. + - `path` - (Required) Device to pass through to the container (e.g. `/dev/sda`). + - `uid` - (Optional) User ID to be assigned to the device node. - `network_interface` - (Optional) A network interface (multiple blocks supported). - `bridge` - (Optional) The name of the network bridge (defaults diff --git a/fwprovider/test/resource_container_test.go b/fwprovider/test/resource_container_test.go index 9415262c..4e94970a 100644 --- a/fwprovider/test/resource_container_test.go +++ b/fwprovider/test/resource_container_test.go @@ -75,6 +75,9 @@ func TestAccResourceContainer(t *testing.T) { size = "4G" path = "mnt/local" } + device_passthrough { + path = "/dev/zero" + } description = <<-EOT my description @@ -199,6 +202,9 @@ func TestAccResourceContainer(t *testing.T) { size = "4G" path = "mnt/local" } + device_passthrough { + path = "/dev/zero" + } initialization { hostname = "test" ip_config { diff --git a/proxmox/nodes/containers/containers_types.go b/proxmox/nodes/containers/containers_types.go index acae6434..48cc66f7 100644 --- a/proxmox/nodes/containers/containers_types.go +++ b/proxmox/nodes/containers/containers_types.go @@ -31,45 +31,46 @@ type CloneRequestBody struct { // CreateRequestBody contains the data for a user create request. type CreateRequestBody struct { - BandwidthLimit *float64 `json:"bwlimit,omitempty" url:"bwlimit,omitempty"` - ConsoleEnabled *types.CustomBool `json:"console,omitempty" url:"console,omitempty,int"` - ConsoleMode *string `json:"cmode,omitempty" url:"cmode,omitempty"` - CPUArchitecture *string `json:"arch,omitempty" url:"arch,omitempty"` - CPUCores *int `json:"cores,omitempty" url:"cores,omitempty"` - CPULimit *int `json:"cpulimit,omitempty" url:"cpulimit,omitempty"` - CPUUnits *int `json:"cpuunits,omitempty" url:"cpuunits,omitempty"` - DatastoreID *string `json:"storage,omitempty" url:"storage,omitempty"` - DedicatedMemory *int `json:"memory,omitempty" url:"memory,omitempty"` - Delete []string `json:"delete,omitempty" url:"delete,omitempty"` - Description *string `json:"description,omitempty" url:"description,omitempty"` - DNSDomain *string `json:"searchdomain,omitempty" url:"searchdomain,omitempty"` - DNSServer *string `json:"nameserver,omitempty" url:"nameserver,omitempty"` - Features *CustomFeatures `json:"features,omitempty" url:"features,omitempty"` - Force *types.CustomBool `json:"force,omitempty" url:"force,omitempty,int"` - HookScript *string `json:"hookscript,omitempty" url:"hookscript,omitempty"` - Hostname *string `json:"hostname,omitempty" url:"hostname,omitempty"` - IgnoreUnpackErrors *types.CustomBool `json:"ignore-unpack-errors,omitempty" url:"force,omitempty,int"` - Lock *string `json:"lock,omitempty" url:"lock,omitempty,int"` - MountPoints CustomMountPointArray `json:"mp,omitempty" url:"mp,omitempty,numbered"` - NetworkInterfaces CustomNetworkInterfaceArray `json:"net,omitempty" url:"net,omitempty,numbered"` - OSTemplateFileVolume *string `json:"ostemplate,omitempty" url:"ostemplate,omitempty"` - OSType *string `json:"ostype,omitempty" url:"ostype,omitempty"` - Password *string `json:"password,omitempty" url:"password,omitempty"` - PoolID *string `json:"pool,omitempty" url:"pool,omitempty"` - Protection *types.CustomBool `json:"protection,omitempty" url:"protection,omitempty,int"` - Restore *types.CustomBool `json:"restore,omitempty" url:"restore,omitempty,int"` - RootFS *CustomRootFS `json:"rootfs,omitempty" url:"rootfs,omitempty"` - SSHKeys *CustomSSHKeys `json:"ssh-public-keys,omitempty" url:"ssh-public-keys,omitempty"` - Start *types.CustomBool `json:"start,omitempty" url:"start,omitempty,int"` - StartOnBoot *types.CustomBool `json:"onboot,omitempty" url:"onboot,omitempty,int"` - StartupBehavior *CustomStartupBehavior `json:"startup,omitempty" url:"startup,omitempty"` - Swap *int `json:"swap,omitempty" url:"swap,omitempty"` - Tags *string `json:"tags,omitempty" url:"tags,omitempty"` - Template *types.CustomBool `json:"template,omitempty" url:"template,omitempty,int"` - TTY *int `json:"tty,omitempty" url:"tty,omitempty"` - Unique *types.CustomBool `json:"unique,omitempty" url:"unique,omitempty,int"` - Unprivileged *types.CustomBool `json:"unprivileged,omitempty" url:"unprivileged,omitempty,int"` - VMID *int `json:"vmid,omitempty" url:"vmid,omitempty"` + BandwidthLimit *float64 `json:"bwlimit,omitempty" url:"bwlimit,omitempty"` + ConsoleEnabled *types.CustomBool `json:"console,omitempty" url:"console,omitempty,int"` + ConsoleMode *string `json:"cmode,omitempty" url:"cmode,omitempty"` + CPUArchitecture *string `json:"arch,omitempty" url:"arch,omitempty"` + CPUCores *int `json:"cores,omitempty" url:"cores,omitempty"` + CPULimit *int `json:"cpulimit,omitempty" url:"cpulimit,omitempty"` + CPUUnits *int `json:"cpuunits,omitempty" url:"cpuunits,omitempty"` + DatastoreID *string `json:"storage,omitempty" url:"storage,omitempty"` + DedicatedMemory *int `json:"memory,omitempty" url:"memory,omitempty"` + Delete []string `json:"delete,omitempty" url:"delete,omitempty"` + Description *string `json:"description,omitempty" url:"description,omitempty"` + DNSDomain *string `json:"searchdomain,omitempty" url:"searchdomain,omitempty"` + DNSServer *string `json:"nameserver,omitempty" url:"nameserver,omitempty"` + Features *CustomFeatures `json:"features,omitempty" url:"features,omitempty"` + Force *types.CustomBool `json:"force,omitempty" url:"force,omitempty,int"` + HookScript *string `json:"hookscript,omitempty" url:"hookscript,omitempty"` + Hostname *string `json:"hostname,omitempty" url:"hostname,omitempty"` + IgnoreUnpackErrors *types.CustomBool `json:"ignore-unpack-errors,omitempty" url:"force,omitempty,int"` + Lock *string `json:"lock,omitempty" url:"lock,omitempty,int"` + MountPoints CustomMountPointArray `json:"mp,omitempty" url:"mp,omitempty,numbered"` + DevicePassthrough CustomDevicePassthroughArray `json:"dev,omitempty" url:"dev,omitempty,numbered"` + NetworkInterfaces CustomNetworkInterfaceArray `json:"net,omitempty" url:"net,omitempty,numbered"` + OSTemplateFileVolume *string `json:"ostemplate,omitempty" url:"ostemplate,omitempty"` + OSType *string `json:"ostype,omitempty" url:"ostype,omitempty"` + Password *string `json:"password,omitempty" url:"password,omitempty"` + PoolID *string `json:"pool,omitempty" url:"pool,omitempty"` + Protection *types.CustomBool `json:"protection,omitempty" url:"protection,omitempty,int"` + Restore *types.CustomBool `json:"restore,omitempty" url:"restore,omitempty,int"` + RootFS *CustomRootFS `json:"rootfs,omitempty" url:"rootfs,omitempty"` + SSHKeys *CustomSSHKeys `json:"ssh-public-keys,omitempty" url:"ssh-public-keys,omitempty"` + Start *types.CustomBool `json:"start,omitempty" url:"start,omitempty,int"` + StartOnBoot *types.CustomBool `json:"onboot,omitempty" url:"onboot,omitempty,int"` + StartupBehavior *CustomStartupBehavior `json:"startup,omitempty" url:"startup,omitempty"` + Swap *int `json:"swap,omitempty" url:"swap,omitempty"` + Tags *string `json:"tags,omitempty" url:"tags,omitempty"` + Template *types.CustomBool `json:"template,omitempty" url:"template,omitempty,int"` + TTY *int `json:"tty,omitempty" url:"tty,omitempty"` + Unique *types.CustomBool `json:"unique,omitempty" url:"unique,omitempty,int"` + Unprivileged *types.CustomBool `json:"unprivileged,omitempty" url:"unprivileged,omitempty,int"` + VMID *int `json:"vmid,omitempty" url:"vmid,omitempty"` } // CustomFeatures contains the values for the "features" property. @@ -116,6 +117,18 @@ type CustomNetworkInterface struct { Type *string `json:"type,omitempty" url:"type,omitempty"` } +// CustomDevicePassthroughArray is an array of CustomDevicePassthrough. +type CustomDevicePassthroughArray []CustomDevicePassthrough + +// CustomDevicePassthrough contains the values for the "dev[n]" properties. +type CustomDevicePassthrough struct { + DenyWrite *types.CustomBool `json:"deny-write,omitempty" url:"deny-write,omitempty,int"` + Path string `json:"path" url:"path"` + UID *int `json:"uid,omitempty" url:"uid,omitempty"` + GID *int `json:"gid,omitempty" url:"gid,omitempty"` + Mode *string `json:"mode,omitempty" url:"mode,omitempty"` +} + // CustomNetworkInterfaceArray is an array of CustomNetworkInterface. type CustomNetworkInterfaceArray []CustomNetworkInterface @@ -153,48 +166,56 @@ type GetResponseBody struct { // GetResponseData contains the data from a user get response. type GetResponseData struct { - ConsoleEnabled *types.CustomBool `json:"console,omitempty"` - ConsoleMode *string `json:"cmode,omitempty"` - CPUArchitecture *string `json:"arch,omitempty"` - CPUCores *int `json:"cores,omitempty"` - CPULimit *types.CustomInt `json:"cpulimit,omitempty"` - CPUUnits *int `json:"cpuunits,omitempty"` - DedicatedMemory *int `json:"memory,omitempty"` - Description *string `json:"description,omitempty"` - Digest string `json:"digest"` - DNSDomain *string `json:"searchdomain,omitempty"` - DNSServer *string `json:"nameserver,omitempty"` - Features *CustomFeatures `json:"features,omitempty"` - HookScript *string `json:"hookscript,omitempty"` - Hostname *string `json:"hostname,omitempty"` - Lock *types.CustomBool `json:"lock,omitempty"` - LXCConfiguration *[][2]string `json:"lxc,omitempty"` - MountPoint0 *CustomMountPoint `json:"mp0,omitempty"` - MountPoint1 *CustomMountPoint `json:"mp1,omitempty"` - MountPoint2 *CustomMountPoint `json:"mp2,omitempty"` - MountPoint3 *CustomMountPoint `json:"mp3,omitempty"` - MountPoint4 *CustomMountPoint `json:"mp4,omitempty"` - MountPoint5 *CustomMountPoint `json:"mp5,omitempty"` - MountPoint6 *CustomMountPoint `json:"mp6,omitempty"` - MountPoint7 *CustomMountPoint `json:"mp7,omitempty"` - NetworkInterface0 *CustomNetworkInterface `json:"net0,omitempty"` - NetworkInterface1 *CustomNetworkInterface `json:"net1,omitempty"` - NetworkInterface2 *CustomNetworkInterface `json:"net2,omitempty"` - NetworkInterface3 *CustomNetworkInterface `json:"net3,omitempty"` - NetworkInterface4 *CustomNetworkInterface `json:"net4,omitempty"` - NetworkInterface5 *CustomNetworkInterface `json:"net5,omitempty"` - NetworkInterface6 *CustomNetworkInterface `json:"net6,omitempty"` - NetworkInterface7 *CustomNetworkInterface `json:"net7,omitempty"` - OSType *string `json:"ostype,omitempty"` - Protection *types.CustomBool `json:"protection,omitempty"` - RootFS *CustomRootFS `json:"rootfs,omitempty"` - StartOnBoot *types.CustomBool `json:"onboot,omitempty"` - StartupBehavior *CustomStartupBehavior `json:"startup,omitempty"` - Swap *int `json:"swap,omitempty"` - Tags *string `json:"tags,omitempty"` - Template *types.CustomBool `json:"template,omitempty"` - TTY *int `json:"tty,omitempty"` - Unprivileged *types.CustomBool `json:"unprivileged,omitempty"` + ConsoleEnabled *types.CustomBool `json:"console,omitempty"` + ConsoleMode *string `json:"cmode,omitempty"` + CPUArchitecture *string `json:"arch,omitempty"` + CPUCores *int `json:"cores,omitempty"` + CPULimit *types.CustomInt `json:"cpulimit,omitempty"` + CPUUnits *int `json:"cpuunits,omitempty"` + DedicatedMemory *int `json:"memory,omitempty"` + Description *string `json:"description,omitempty"` + Digest string `json:"digest"` + DNSDomain *string `json:"searchdomain,omitempty"` + DNSServer *string `json:"nameserver,omitempty"` + Features *CustomFeatures `json:"features,omitempty"` + HookScript *string `json:"hookscript,omitempty"` + Hostname *string `json:"hostname,omitempty"` + Lock *types.CustomBool `json:"lock,omitempty"` + LXCConfiguration *[][2]string `json:"lxc,omitempty"` + DevicePassthrough0 *CustomDevicePassthrough `json:"dev0,omitempty"` + DevicePassthrough1 *CustomDevicePassthrough `json:"dev1,omitempty"` + DevicePassthrough2 *CustomDevicePassthrough `json:"dev2,omitempty"` + DevicePassthrough3 *CustomDevicePassthrough `json:"dev3,omitempty"` + DevicePassthrough4 *CustomDevicePassthrough `json:"dev4,omitempty"` + DevicePassthrough5 *CustomDevicePassthrough `json:"dev5,omitempty"` + DevicePassthrough6 *CustomDevicePassthrough `json:"dev6,omitempty"` + DevicePassthrough7 *CustomDevicePassthrough `json:"dev7,omitempty"` + MountPoint0 *CustomMountPoint `json:"mp0,omitempty"` + MountPoint1 *CustomMountPoint `json:"mp1,omitempty"` + MountPoint2 *CustomMountPoint `json:"mp2,omitempty"` + MountPoint3 *CustomMountPoint `json:"mp3,omitempty"` + MountPoint4 *CustomMountPoint `json:"mp4,omitempty"` + MountPoint5 *CustomMountPoint `json:"mp5,omitempty"` + MountPoint6 *CustomMountPoint `json:"mp6,omitempty"` + MountPoint7 *CustomMountPoint `json:"mp7,omitempty"` + NetworkInterface0 *CustomNetworkInterface `json:"net0,omitempty"` + NetworkInterface1 *CustomNetworkInterface `json:"net1,omitempty"` + NetworkInterface2 *CustomNetworkInterface `json:"net2,omitempty"` + NetworkInterface3 *CustomNetworkInterface `json:"net3,omitempty"` + NetworkInterface4 *CustomNetworkInterface `json:"net4,omitempty"` + NetworkInterface5 *CustomNetworkInterface `json:"net5,omitempty"` + NetworkInterface6 *CustomNetworkInterface `json:"net6,omitempty"` + NetworkInterface7 *CustomNetworkInterface `json:"net7,omitempty"` + OSType *string `json:"ostype,omitempty"` + Protection *types.CustomBool `json:"protection,omitempty"` + RootFS *CustomRootFS `json:"rootfs,omitempty"` + StartOnBoot *types.CustomBool `json:"onboot,omitempty"` + StartupBehavior *CustomStartupBehavior `json:"startup,omitempty"` + Swap *int `json:"swap,omitempty"` + Tags *string `json:"tags,omitempty"` + Template *types.CustomBool `json:"template,omitempty"` + TTY *int `json:"tty,omitempty"` + Unprivileged *types.CustomBool `json:"unprivileged,omitempty"` } // GetStatusResponseBody contains the body from a container get status response. @@ -276,6 +297,55 @@ func (r *CustomFeatures) EncodeValues(key string, v *url.Values) error { return nil } +// EncodeValues converts a CustomDevicePassthrough struct to a URL value. +func (r *CustomDevicePassthrough) EncodeValues(key string, v *url.Values) error { + var values []string + + if r.DenyWrite != nil { + if *r.DenyWrite { + values = append(values, "deny-write=1") + } else { + values = append(values, "deny-write=0") + } + } + + if r.Path != "" { + values = append(values, fmt.Sprintf("path=%s", r.Path)) + } + + if r.UID != nil { + values = append(values, fmt.Sprintf("uid=%d", *r.UID)) + } + + if r.GID != nil { + values = append(values, fmt.Sprintf("gid=%d", *r.GID)) + } + + if r.Mode != nil && *r.Mode != "" { + values = append(values, fmt.Sprintf("mode=%s", *r.Mode)) + } + + if len(values) > 0 { + v.Add(key, strings.Join(values, ",")) + } + + return nil +} + +// EncodeValues converts a CustomDevicePassthroughArray array to multiple URL values. +func (r CustomDevicePassthroughArray) EncodeValues( + key string, + v *url.Values, +) error { + for i, d := range r { + if err := d.EncodeValues(fmt.Sprintf("%s%d", key, i), v); err != nil { + return fmt.Errorf("failed to encode CustomDevicePassthroughArray: %w", err) + } + } + + return nil +} + // EncodeValues converts a CustomMountPoint struct to a URL value. func (r *CustomMountPoint) EncodeValues(key string, v *url.Values) error { var values []string @@ -587,6 +657,56 @@ func (r *CustomFeatures) UnmarshalJSON(b []byte) error { return nil } +// UnmarshalJSON converts a CustomDevicePassthrough string to an object. +func (r *CustomDevicePassthrough) UnmarshalJSON(b []byte) error { + var s string + + err := json.Unmarshal(b, &s) + if err != nil { + return fmt.Errorf("unable to unmarshal CustomDevicePassthrough: %w", err) + } + + pairs := strings.Split(s, ",") + + var path string + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + if len(v) == 1 { + path = v[0] + } else if len(v) == 2 { + switch v[0] { + case "deny-write": + bv := types.CustomBool(v[1] == "1") + r.DenyWrite = &bv + case "path": + path = v[1] + case "uid": + iv, err := strconv.Atoi(v[1]) + if err != nil { + return fmt.Errorf("unable to unmarshal 'uid': %w", err) + } + + r.UID = &iv + case "gid": + iv, err := strconv.Atoi(v[1]) + if err != nil { + return fmt.Errorf("unable to unmarshal 'gid': %w", err) + } + + r.GID = &iv + case "mode": + r.Mode = &v[1] + } + } + } + + r.Path = path + + return nil +} + // UnmarshalJSON converts a CustomMountPoint string to an object. func (r *CustomMountPoint) UnmarshalJSON(b []byte) error { var s string diff --git a/proxmoxtf/resource/container/container.go b/proxmoxtf/resource/container/container.go index c92e9a97..ae90b31a 100644 --- a/proxmoxtf/resource/container/container.go +++ b/proxmoxtf/resource/container/container.go @@ -10,6 +10,7 @@ import ( "context" "errors" "fmt" + "regexp" "sort" "strconv" "strings" @@ -140,6 +141,12 @@ const ( mkMountPointShared = "shared" mkMountPointSize = "size" mkMountPointVolume = "volume" + mkDevicePassthroughDenyWrite = "deny_write" + mkDevicePassthrough = "device_passthrough" // #nosec G101 + mkDevicePassthroughPath = "path" + mkDevicePassthroughUID = "uid" + mkDevicePassthroughGID = "gid" + mkDevicePassthroughMode = "mode" mkNetworkInterface = "network_interface" mkNetworkInterfaceBridge = "bridge" mkNetworkInterfaceEnabled = "enabled" @@ -680,6 +687,49 @@ func Container() *schema.Resource { MaxItems: 8, MinItems: 0, }, + mkDevicePassthrough: { + Type: schema.TypeList, + Description: "Device to pass through to the container", + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + mkDevicePassthroughDenyWrite: { + Type: schema.TypeBool, + Description: "Deny the container to write to the device", + Optional: true, + Default: false, + }, + mkDevicePassthroughGID: { + Type: schema.TypeInt, + Description: "Group ID to be assigned to the device node", + Optional: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), + }, + mkDevicePassthroughMode: { + Type: schema.TypeString, + Description: "Access mode to be set on the device node (e.g. 0666)", + Optional: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringMatch( + regexp.MustCompile(`0[0-7]{3}`), "Octal access mode", + )), + }, + mkDevicePassthroughPath: { + Type: schema.TypeString, + Description: "Device to pass through to the container", + Required: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), + }, + mkDevicePassthroughUID: { + Type: schema.TypeInt, + Description: "Device UID in the container", + Optional: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), + }, + }, + }, + MaxItems: 8, + MinItems: 0, + }, mkNetworkInterface: { Type: schema.TypeList, Description: "The network interfaces", @@ -1209,6 +1259,36 @@ func containerCreateClone(ctx context.Context, d *schema.ResourceData, m interfa updateBody.Swap = &memorySwap } + devicePassthrough := d.Get(mkDevicePassthrough).([]interface{}) + + devicePassthroughArray := make( + containers.CustomDevicePassthroughArray, + len(devicePassthrough), + ) + + for di, dv := range devicePassthrough { + devicePassthroughMap := dv.(map[string]interface{}) + devicePassthroughObject := containers.CustomDevicePassthrough{} + + denyWrite := types.CustomBool( + devicePassthroughMap[mkDevicePassthroughDenyWrite].(bool), + ) + gid := devicePassthroughMap[mkDevicePassthroughGID].(int) + mode := devicePassthroughMap[mkDevicePassthroughMode].(string) + path := devicePassthroughMap[mkDevicePassthroughPath].(string) + uid := devicePassthroughMap[mkDevicePassthroughUID].(int) + + devicePassthroughObject.DenyWrite = &denyWrite + devicePassthroughObject.GID = &gid + devicePassthroughObject.Mode = &mode + devicePassthroughObject.Path = path + devicePassthroughObject.UID = &uid + + devicePassthroughArray[di] = devicePassthroughObject + } + + updateBody.DevicePassthrough = devicePassthroughArray + networkInterface := d.Get(mkNetworkInterface).([]interface{}) if len(networkInterface) == 0 { @@ -2232,6 +2312,65 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d initialization[mkInitializationHostname] = "" } + devicePassthroughArray := []*containers.CustomDevicePassthrough{ + containerConfig.DevicePassthrough0, + containerConfig.DevicePassthrough1, + containerConfig.DevicePassthrough2, + containerConfig.DevicePassthrough3, + containerConfig.DevicePassthrough4, + containerConfig.DevicePassthrough5, + containerConfig.DevicePassthrough6, + containerConfig.DevicePassthrough7, + } + + devicePassthroughList := make([]interface{}, 0, len(devicePassthroughArray)) + + for _, dp := range devicePassthroughArray { + if dp == nil { + continue + } + + devicePassthrough := map[string]interface{}{} + + if dp.DenyWrite != nil { + devicePassthrough[mkDevicePassthroughDenyWrite] = *dp.DenyWrite + } else { + devicePassthrough[mkDevicePassthroughDenyWrite] = false + } + + if dp.GID != nil { + devicePassthrough[mkDevicePassthroughGID] = *dp.GID + } else { + devicePassthrough[mkDevicePassthroughGID] = 0 + } + + if dp.Mode != nil { + devicePassthrough[mkDevicePassthroughMode] = *dp.Mode + } else { + devicePassthrough[mkDevicePassthroughMode] = "" + } + + devicePassthrough[mkDevicePassthroughPath] = dp.Path + + if dp.UID != nil { + devicePassthrough[mkDevicePassthroughUID] = *dp.UID + } else { + devicePassthrough[mkDevicePassthroughUID] = 0 + } + + devicePassthroughList = append(devicePassthroughList, devicePassthrough) + } + + if len(clone) > 0 { + if len(devicePassthroughList) > 0 { + err := d.Set(mkDevicePassthrough, devicePassthroughList) + diags = append(diags, diag.FromErr(err)...) + } + } else if len(devicePassthroughList) > 0 { + err := d.Set(mkDevicePassthrough, devicePassthroughList) + diags = append(diags, diag.FromErr(err)...) + } + mountPointArray := []*containers.CustomMountPoint{ containerConfig.MountPoint0, containerConfig.MountPoint1, @@ -2860,6 +2999,40 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) rebootRequired = true } + // Prepare the new device passthrough configuration. + if d.HasChange(mkDevicePassthrough) { + _, newDevicePassthrough := d.GetChange(mkDevicePassthrough) + + devicePassthrough := newDevicePassthrough.([]interface{}) + devicePassthroughArray := make( + containers.CustomDevicePassthroughArray, + len(devicePassthrough), + ) + + for i, dp := range devicePassthrough { + devicePassthroughMap := dp.(map[string]interface{}) + devicePassthroughObject := containers.CustomDevicePassthrough{} + + denyWrite := types.CustomBool(devicePassthroughMap[mkDevicePassthroughDenyWrite].(bool)) + gid := devicePassthroughMap[mkDevicePassthroughGID].(int) + mode := devicePassthroughMap[mkDevicePassthroughMode].(string) + path := devicePassthroughMap[mkDevicePassthroughPath].(string) + uid := devicePassthroughMap[mkDevicePassthroughUID].(int) + + devicePassthroughObject.DenyWrite = &denyWrite + devicePassthroughObject.GID = &gid + devicePassthroughObject.Mode = &mode + devicePassthroughObject.Path = path + devicePassthroughObject.UID = &uid + + devicePassthroughArray[i] = devicePassthroughObject + } + + updateBody.DevicePassthrough = devicePassthroughArray + + rebootRequired = true + } + // Prepare the new mount point configuration. if d.HasChange(mkMountPoint) { _, newMountPoints := d.GetChange(mkMountPoint) diff --git a/proxmoxtf/resource/container/container_test.go b/proxmoxtf/resource/container/container_test.go index 3cb4b945..7aa70227 100644 --- a/proxmoxtf/resource/container/container_test.go +++ b/proxmoxtf/resource/container/container_test.go @@ -41,6 +41,7 @@ func TestContainerSchema(t *testing.T) { mkInitialization, mkHookScriptFileID, mkMemory, + mkDevicePassthrough, mkMountPoint, mkOperatingSystem, mkPoolID, @@ -55,23 +56,24 @@ func TestContainerSchema(t *testing.T) { }) test.AssertValueTypes(t, s, map[string]schema.ValueType{ - mkCPU: schema.TypeList, - mkDescription: schema.TypeString, - mkDisk: schema.TypeList, - mkInitialization: schema.TypeList, - mkHookScriptFileID: schema.TypeString, - mkMemory: schema.TypeList, - mkMountPoint: schema.TypeList, - mkOperatingSystem: schema.TypeList, - mkPoolID: schema.TypeString, - mkProtection: schema.TypeBool, - mkStarted: schema.TypeBool, - mkTags: schema.TypeList, - mkTemplate: schema.TypeBool, - mkUnprivileged: schema.TypeBool, - mkStartOnBoot: schema.TypeBool, - mkFeatures: schema.TypeList, - mkVMID: schema.TypeInt, + mkCPU: schema.TypeList, + mkDescription: schema.TypeString, + mkDisk: schema.TypeList, + mkInitialization: schema.TypeList, + mkHookScriptFileID: schema.TypeString, + mkMemory: schema.TypeList, + mkDevicePassthrough: schema.TypeList, + mkMountPoint: schema.TypeList, + mkOperatingSystem: schema.TypeList, + mkPoolID: schema.TypeString, + mkProtection: schema.TypeBool, + mkStarted: schema.TypeBool, + mkTags: schema.TypeList, + mkTemplate: schema.TypeBool, + mkUnprivileged: schema.TypeBool, + mkStartOnBoot: schema.TypeBool, + mkFeatures: schema.TypeList, + mkVMID: schema.TypeInt, }) cloneSchema := test.AssertNestedSchemaExistence(t, s, mkClone) @@ -243,6 +245,27 @@ func TestContainerSchema(t *testing.T) { mkMemorySwap: schema.TypeInt, }) + devicePassthroughSchema := test.AssertNestedSchemaExistence(t, s, mkDevicePassthrough) + + test.AssertRequiredArguments(t, devicePassthroughSchema, []string{ + mkDevicePassthroughPath, + }) + + test.AssertOptionalArguments(t, devicePassthroughSchema, []string{ + mkDevicePassthroughDenyWrite, + mkDevicePassthroughGID, + mkDevicePassthroughMode, + mkDevicePassthroughUID, + }) + + test.AssertValueTypes(t, devicePassthroughSchema, map[string]schema.ValueType{ + mkDevicePassthroughDenyWrite: schema.TypeBool, + mkDevicePassthroughGID: schema.TypeInt, + mkDevicePassthroughMode: schema.TypeString, + mkDevicePassthroughPath: schema.TypeString, + mkDevicePassthroughUID: schema.TypeInt, + }) + mountPointSchema := test.AssertNestedSchemaExistence(t, s, mkMountPoint) test.AssertOptionalArguments(t, mountPointSchema, []string{