mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-08-22 19:38:35 +00:00
feat(vm): add support for USB devices passthrough (#666)
* feat: support usb devices for vm; fixes #665 Signed-off-by: Daniel Muehlbachler-Pietrzykowski <daniel@muehlbachler.io> * chore: fix linter errors Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --------- Signed-off-by: Daniel Muehlbachler-Pietrzykowski <daniel@muehlbachler.io> 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:
parent
db842a2399
commit
cec4e65868
@ -301,6 +301,12 @@ output "ubuntu_vm_public_key" {
|
||||
is a relative path under `/usr/share/kvm/`.
|
||||
- `xvga` - (Optional) Marks the PCI(e) device as the primary GPU of the VM.
|
||||
With this enabled the `vga` configuration argument will be ignored.
|
||||
- `usb` - (Optional) A host USB device mapping (multiple blocks supported).
|
||||
- `host` - (Optional) The USB device ID. Use either this or `mapping`.
|
||||
- `mapping` - (Optional) The resource mapping name of the device, for
|
||||
example usbdevice. Use either this or `id`.
|
||||
- `usb3` - (Optional) Makes the USB device a USB3 device for the VM (defaults
|
||||
to `false`).
|
||||
- `initialization` - (Optional) The cloud-init configuration.
|
||||
- `datastore_id` - (Optional) The identifier for the datastore to create the
|
||||
cloud-init disk in (defaults to `local-lvm`).
|
||||
|
@ -165,9 +165,21 @@ resource "proxmox_virtual_environment_vm" "example" {
|
||||
# pcie = true
|
||||
#}
|
||||
|
||||
#usb {
|
||||
# host = "0000:1234"
|
||||
# mapping = "usbdevice1"
|
||||
# usb3 = false
|
||||
#}
|
||||
|
||||
#usb {
|
||||
# host = "0000:5678"
|
||||
# mapping = "usbdevice2"
|
||||
# usb3 = false
|
||||
#}
|
||||
|
||||
# attached disks from data_vm
|
||||
dynamic "disk" {
|
||||
for_each = {for idx, val in proxmox_virtual_environment_vm.data_vm.disk : idx => val}
|
||||
for_each = { for idx, val in proxmox_virtual_environment_vm.data_vm.disk : idx => val }
|
||||
iterator = data_disk
|
||||
content {
|
||||
datastore_id = data_disk.value["datastore_id"]
|
||||
|
@ -242,7 +242,8 @@ type CustomStorageDevices map[string]CustomStorageDevice
|
||||
|
||||
// CustomUSBDevice handles QEMU USB device parameters.
|
||||
type CustomUSBDevice struct {
|
||||
HostDevice string `json:"host" url:"host"`
|
||||
HostDevice *string `json:"host" url:"host"`
|
||||
Mapping *string `json:"mapping,omitempty" url:"mapping,omitempty"`
|
||||
USB3 *types.CustomBool `json:"usb3,omitempty" url:"usb3,omitempty,int"`
|
||||
}
|
||||
|
||||
@ -517,7 +518,10 @@ type GetResponseData struct {
|
||||
Tags *string `json:"tags,omitempty"`
|
||||
Template *types.CustomBool `json:"template,omitempty"`
|
||||
TimeDriftFixEnabled *types.CustomBool `json:"tdf,omitempty"`
|
||||
USBDevices *CustomUSBDevices `json:"usb,omitempty"`
|
||||
USBDevice0 *CustomUSBDevice `json:"usb0,omitempty"`
|
||||
USBDevice1 *CustomUSBDevice `json:"usb1,omitempty"`
|
||||
USBDevice2 *CustomUSBDevice `json:"usb2,omitempty"`
|
||||
USBDevice3 *CustomUSBDevice `json:"usb3,omitempty"`
|
||||
VGADevice *CustomVGADevice `json:"vga,omitempty"`
|
||||
VirtualCPUCount *int `json:"vcpus,omitempty"`
|
||||
VirtualIODevice0 *CustomStorageDevice `json:"virtio0,omitempty"`
|
||||
@ -1232,8 +1236,16 @@ func (r CustomStorageDevices) EncodeValues(_ string, v *url.Values) error {
|
||||
|
||||
// EncodeValues converts a CustomUSBDevice struct to a URL vlaue.
|
||||
func (r CustomUSBDevice) EncodeValues(key string, v *url.Values) error {
|
||||
if r.HostDevice == nil && r.Mapping == nil {
|
||||
return fmt.Errorf("either device ID or resource mapping must be set")
|
||||
}
|
||||
|
||||
values := []string{
|
||||
fmt.Sprintf("host=%s", r.HostDevice),
|
||||
fmt.Sprintf("host=%s", *(r.HostDevice)),
|
||||
}
|
||||
|
||||
if r.Mapping != nil {
|
||||
values = append(values, fmt.Sprintf("mapping=%s", *r.Mapping))
|
||||
}
|
||||
|
||||
if r.USB3 != nil {
|
||||
@ -1696,6 +1708,36 @@ func (r *CustomPCIDevice) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON converts a CustomUSBDevice string to an object.
|
||||
func (r *CustomUSBDevice) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal CustomUSBDevice: %w", err)
|
||||
}
|
||||
|
||||
pairs := strings.Split(s, ",")
|
||||
|
||||
for _, p := range pairs {
|
||||
v := strings.Split(strings.TrimSpace(p), "=")
|
||||
if len(v) == 1 {
|
||||
r.HostDevice = &v[1]
|
||||
} else if len(v) == 2 {
|
||||
switch v[0] {
|
||||
case "host":
|
||||
r.HostDevice = &v[1]
|
||||
case "mapping":
|
||||
r.Mapping = &v[1]
|
||||
case "usb3":
|
||||
bv := types.CustomBool(v[1] == "1")
|
||||
r.USB3 = &bv
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON converts a CustomSharedMemory string to an object.
|
||||
func (r *CustomSharedMemory) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
|
@ -124,3 +124,50 @@ func TestCustomPCIDevice_UnmarshalJSON(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomUSBDevice_UnmarshalJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
line string
|
||||
want *CustomUSBDevice
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "id only usb device",
|
||||
line: `"host=0000:81"`,
|
||||
want: &CustomUSBDevice{
|
||||
HostDevice: types.StrPtr("0000:81"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "usb device with more details",
|
||||
line: `"host=81:00,usb3=0"`,
|
||||
want: &CustomUSBDevice{
|
||||
HostDevice: types.StrPtr("81:00"),
|
||||
USB3: types.BoolPtr(false),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "usb device with mapping",
|
||||
line: `"mapping=mappeddevice,usb=0"`,
|
||||
want: &CustomUSBDevice{
|
||||
HostDevice: nil,
|
||||
Mapping: types.StrPtr("mappeddevice"),
|
||||
USB3: types.BoolPtr(false),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
r := &CustomUSBDevice{}
|
||||
if err := r.UnmarshalJSON([]byte(tt.line)); (err != nil) != tt.wantErr {
|
||||
t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -140,6 +140,7 @@ const (
|
||||
maxResourceVirtualEnvironmentVMNetworkDevices = 8
|
||||
maxResourceVirtualEnvironmentVMSerialDevices = 4
|
||||
maxResourceVirtualEnvironmentVMHostPCIDevices = 8
|
||||
maxResourceVirtualEnvironmentVMHostUSBDevices = 4
|
||||
|
||||
mkResourceVirtualEnvironmentVMRebootAfterCreation = "reboot"
|
||||
mkResourceVirtualEnvironmentVMOnBoot = "on_boot"
|
||||
@ -280,6 +281,10 @@ const (
|
||||
mkResourceVirtualEnvironmentVMTimeoutShutdownVM = "timeout_shutdown_vm"
|
||||
mkResourceVirtualEnvironmentVMTimeoutStartVM = "timeout_start_vm"
|
||||
mkResourceVirtualEnvironmentVMTimeoutStopVM = "timeout_stop_vm"
|
||||
mkResourceVirtualEnvironmentVMHostUSB = "usb"
|
||||
mkResourceVirtualEnvironmentVMHostUSBDevice = "host"
|
||||
mkResourceVirtualEnvironmentVMHostUSBDeviceMapping = "mapping"
|
||||
mkResourceVirtualEnvironmentVMHostUSBDeviceUSB3 = "usb3"
|
||||
mkResourceVirtualEnvironmentVMVGA = "vga"
|
||||
mkResourceVirtualEnvironmentVMVGAEnabled = "enabled"
|
||||
mkResourceVirtualEnvironmentVMVGAMemory = "memory"
|
||||
@ -1061,6 +1066,34 @@ func VM() *schema.Resource {
|
||||
},
|
||||
},
|
||||
},
|
||||
mkResourceVirtualEnvironmentVMHostUSB: {
|
||||
Type: schema.TypeList,
|
||||
Description: "The Host USB devices mapped to the VM",
|
||||
Optional: true,
|
||||
ForceNew: false,
|
||||
DefaultFunc: func() (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
},
|
||||
Elem: &schema.Resource{
|
||||
Schema: map[string]*schema.Schema{
|
||||
mkResourceVirtualEnvironmentVMHostUSBDevice: {
|
||||
Type: schema.TypeString,
|
||||
Description: "The USB device ID for Proxmox, in form of '<MANUFACTURER>:<ID>'",
|
||||
Required: true,
|
||||
},
|
||||
mkResourceVirtualEnvironmentVMHostUSBDeviceMapping: {
|
||||
Type: schema.TypeString,
|
||||
Description: "The resource mapping name of the device, for example usbdisk. Use either this or id.",
|
||||
Optional: true,
|
||||
},
|
||||
mkResourceVirtualEnvironmentVMHostUSBDeviceUSB3: {
|
||||
Type: schema.TypeBool,
|
||||
Description: "Makes the USB device a USB3 device for the machine. Default is false",
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mkResourceVirtualEnvironmentVMKeyboardLayout: {
|
||||
Type: schema.TypeString,
|
||||
Description: "The keyboard layout",
|
||||
@ -1831,6 +1864,7 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d
|
||||
cpu := d.Get(mkResourceVirtualEnvironmentVMCPU).([]interface{})
|
||||
initialization := d.Get(mkResourceVirtualEnvironmentVMInitialization).([]interface{})
|
||||
hostPCI := d.Get(mkResourceVirtualEnvironmentVMHostPCI).([]interface{})
|
||||
hostUSB := d.Get(mkResourceVirtualEnvironmentVMHostUSB).([]interface{})
|
||||
keyboardLayout := d.Get(mkResourceVirtualEnvironmentVMKeyboardLayout).(string)
|
||||
memory := d.Get(mkResourceVirtualEnvironmentVMMemory).([]interface{})
|
||||
networkDevice := d.Get(mkResourceVirtualEnvironmentVMNetworkDevice).([]interface{})
|
||||
@ -1997,6 +2031,10 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d
|
||||
updateBody.PCIDevices = vmGetHostPCIDeviceObjects(d)
|
||||
}
|
||||
|
||||
if len(hostUSB) > 0 {
|
||||
updateBody.USBDevices = vmGetHostUSBDeviceObjects(d)
|
||||
}
|
||||
|
||||
if len(cdrom) > 0 || len(initialization) > 0 {
|
||||
updateBody.IDEDevices = ideDevices
|
||||
}
|
||||
@ -2393,6 +2431,8 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{})
|
||||
|
||||
pciDeviceObjects := vmGetHostPCIDeviceObjects(d)
|
||||
|
||||
usbDeviceObjects := vmGetHostUSBDeviceObjects(d)
|
||||
|
||||
keyboardLayout := d.Get(mkResourceVirtualEnvironmentVMKeyboardLayout).(string)
|
||||
memoryBlock, err := structure.GetSchemaBlock(
|
||||
resource,
|
||||
@ -2562,6 +2602,7 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{})
|
||||
StartupOrder: startupOrder,
|
||||
TabletDeviceEnabled: &tabletDevice,
|
||||
Template: &template,
|
||||
USBDevices: usbDeviceObjects,
|
||||
VGADevice: vgaDevice,
|
||||
VMID: &vmID,
|
||||
}
|
||||
@ -3241,6 +3282,31 @@ func vmGetHostPCIDeviceObjects(d *schema.ResourceData) vms.CustomPCIDevices {
|
||||
return pciDeviceObjects
|
||||
}
|
||||
|
||||
func vmGetHostUSBDeviceObjects(d *schema.ResourceData) vms.CustomUSBDevices {
|
||||
usbDevice := d.Get(mkResourceVirtualEnvironmentVMHostUSB).([]interface{})
|
||||
usbDeviceObjects := make(vms.CustomUSBDevices, len(usbDevice))
|
||||
|
||||
for i, usbDeviceEntry := range usbDevice {
|
||||
block := usbDeviceEntry.(map[string]interface{})
|
||||
|
||||
host, _ := block[mkResourceVirtualEnvironmentVMHostUSBDevice].(string)
|
||||
usb3 := types.CustomBool(block[mkResourceVirtualEnvironmentVMHostUSBDeviceUSB3].(bool))
|
||||
mapping, _ := block[mkResourceVirtualEnvironmentVMHostUSBDeviceMapping].(string)
|
||||
|
||||
device := vms.CustomUSBDevice{
|
||||
HostDevice: &host,
|
||||
USB3: &usb3,
|
||||
}
|
||||
if mapping != "" {
|
||||
device.Mapping = &mapping
|
||||
}
|
||||
|
||||
usbDeviceObjects[i] = device
|
||||
}
|
||||
|
||||
return usbDeviceObjects
|
||||
}
|
||||
|
||||
func vmGetNetworkDeviceObjects(d *schema.ResourceData) vms.CustomNetworkDevices {
|
||||
networkDevice := d.Get(mkResourceVirtualEnvironmentVMNetworkDevice).([]interface{})
|
||||
networkDeviceObjects := make(vms.CustomNetworkDevices, len(networkDevice))
|
||||
@ -3417,33 +3483,38 @@ func vmGetStartupOrder(d *schema.ResourceData) *vms.CustomStartupOrder {
|
||||
}
|
||||
|
||||
func vmGetTagsString(d *schema.ResourceData) string {
|
||||
tags := d.Get(mkResourceVirtualEnvironmentVMTags).([]interface{})
|
||||
var sanitizedTags []string
|
||||
|
||||
tags := d.Get(mkResourceVirtualEnvironmentVMTags).([]interface{})
|
||||
for i := 0; i < len(tags); i++ {
|
||||
tag := strings.TrimSpace(tags[i].(string))
|
||||
if len(tag) > 0 {
|
||||
sanitizedTags = append(sanitizedTags, tag)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(sanitizedTags)
|
||||
|
||||
return strings.Join(sanitizedTags, ";")
|
||||
}
|
||||
|
||||
func vmGetSerialDeviceValidator() schema.SchemaValidateDiagFunc {
|
||||
return validation.ToDiagFunc(func(i interface{}, k string) (s []string, es []error) {
|
||||
return validation.ToDiagFunc(func(i interface{}, k string) ([]string, []error) {
|
||||
v, ok := i.(string)
|
||||
|
||||
var es []error
|
||||
|
||||
if !ok {
|
||||
es = append(es, fmt.Errorf("expected type of %s to be string", k))
|
||||
return
|
||||
return nil, es
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(v, "/dev/") && v != "socket" {
|
||||
es = append(es, fmt.Errorf("expected %s to be '/dev/*' or 'socket'", k))
|
||||
return
|
||||
return nil, es
|
||||
}
|
||||
|
||||
return
|
||||
return nil, es
|
||||
})
|
||||
}
|
||||
|
||||
@ -4068,12 +4139,50 @@ func vmReadCustom(
|
||||
}
|
||||
|
||||
if len(currentPCIList) > 0 {
|
||||
// todo: reordering of devices by PVE may cause an issue here
|
||||
orderedPCIList := orderedListFromMap(pciMap)
|
||||
err := d.Set(mkResourceVirtualEnvironmentVMHostPCI, orderedPCIList)
|
||||
diags = append(diags, diag.FromErr(err)...)
|
||||
}
|
||||
|
||||
currentUSBList := d.Get(mkResourceVirtualEnvironmentVMHostUSB).([]interface{})
|
||||
usbMap := map[string]interface{}{}
|
||||
|
||||
usbDevices := getUSBInfo(vmConfig, d)
|
||||
for pi, pp := range usbDevices {
|
||||
if (pp == nil) || (pp.HostDevice == nil && pp.Mapping == nil) {
|
||||
continue
|
||||
}
|
||||
|
||||
usb := map[string]interface{}{}
|
||||
|
||||
if pp.HostDevice != nil {
|
||||
usb[mkResourceVirtualEnvironmentVMHostUSBDevice] = *pp.HostDevice
|
||||
} else {
|
||||
usb[mkResourceVirtualEnvironmentVMHostUSBDevice] = ""
|
||||
}
|
||||
|
||||
if pp.USB3 != nil {
|
||||
usb[mkResourceVirtualEnvironmentVMHostUSBDeviceUSB3] = *pp.USB3
|
||||
} else {
|
||||
usb[mkResourceVirtualEnvironmentVMHostUSBDeviceUSB3] = false
|
||||
}
|
||||
|
||||
if pp.Mapping != nil {
|
||||
usb[mkResourceVirtualEnvironmentVMHostUSBDeviceMapping] = *pp.Mapping
|
||||
} else {
|
||||
usb[mkResourceVirtualEnvironmentVMHostUSBDeviceMapping] = ""
|
||||
}
|
||||
|
||||
usbMap[pi] = usb
|
||||
}
|
||||
|
||||
if len(currentUSBList) > 0 {
|
||||
// todo: reordering of devices by PVE may cause an issue here
|
||||
orderedUSBList := orderedListFromMap(usbMap)
|
||||
err := d.Set(mkResourceVirtualEnvironmentVMHostUSB, orderedUSBList)
|
||||
diags = append(diags, diag.FromErr(err)...)
|
||||
}
|
||||
|
||||
// Compare the initialization configuration to the one stored in the state.
|
||||
initialization := map[string]interface{}{}
|
||||
|
||||
@ -5410,6 +5519,17 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D
|
||||
rebootRequired = true
|
||||
}
|
||||
|
||||
// Prepare the new usb devices configuration.
|
||||
if d.HasChange(mkResourceVirtualEnvironmentVMHostUSB) {
|
||||
updateBody.USBDevices = vmGetHostUSBDeviceObjects(d)
|
||||
|
||||
for i := len(updateBody.USBDevices); i < maxResourceVirtualEnvironmentVMHostUSBDevices; i++ {
|
||||
del = append(del, fmt.Sprintf("usb%d", i))
|
||||
}
|
||||
|
||||
rebootRequired = true
|
||||
}
|
||||
|
||||
// Prepare the new memory configuration.
|
||||
if d.HasChange(mkResourceVirtualEnvironmentVMMemory) {
|
||||
memoryBlock, err := structure.GetSchemaBlock(
|
||||
@ -5922,6 +6042,17 @@ func getPCIInfo(resp *vms.GetResponseData, _ *schema.ResourceData) map[string]*v
|
||||
return pciDevices
|
||||
}
|
||||
|
||||
func getUSBInfo(resp *vms.GetResponseData, _ *schema.ResourceData) map[string]*vms.CustomUSBDevice {
|
||||
usbDevices := map[string]*vms.CustomUSBDevice{}
|
||||
|
||||
usbDevices["usb0"] = resp.USBDevice0
|
||||
usbDevices["usb1"] = resp.USBDevice1
|
||||
usbDevices["usb2"] = resp.USBDevice2
|
||||
usbDevices["usb3"] = resp.USBDevice3
|
||||
|
||||
return usbDevices
|
||||
}
|
||||
|
||||
func parseImportIDWithNodeName(id string) (string, string, error) {
|
||||
nodeName, id, found := strings.Cut(id, "/")
|
||||
|
||||
|
@ -49,6 +49,7 @@ func TestVMSchema(t *testing.T) {
|
||||
mkResourceVirtualEnvironmentVMEFIDisk,
|
||||
mkResourceVirtualEnvironmentVMInitialization,
|
||||
mkResourceVirtualEnvironmentVMHostPCI,
|
||||
mkResourceVirtualEnvironmentVMHostUSB,
|
||||
mkResourceVirtualEnvironmentVMKeyboardLayout,
|
||||
mkResourceVirtualEnvironmentVMKVMArguments,
|
||||
mkResourceVirtualEnvironmentVMMachine,
|
||||
@ -84,6 +85,7 @@ func TestVMSchema(t *testing.T) {
|
||||
mkResourceVirtualEnvironmentVMDisk: schema.TypeList,
|
||||
mkResourceVirtualEnvironmentVMEFIDisk: schema.TypeList,
|
||||
mkResourceVirtualEnvironmentVMHostPCI: schema.TypeList,
|
||||
mkResourceVirtualEnvironmentVMHostUSB: schema.TypeList,
|
||||
mkResourceVirtualEnvironmentVMInitialization: schema.TypeList,
|
||||
mkResourceVirtualEnvironmentVMIPv4Addresses: schema.TypeList,
|
||||
mkResourceVirtualEnvironmentVMIPv6Addresses: schema.TypeList,
|
||||
@ -278,6 +280,17 @@ func TestVMSchema(t *testing.T) {
|
||||
mkResourceVirtualEnvironmentVMHostPCIDeviceXVGA: schema.TypeBool,
|
||||
})
|
||||
|
||||
hostUSBSchema := test.AssertNestedSchemaExistence(t, s, mkResourceVirtualEnvironmentVMHostUSB)
|
||||
|
||||
test.AssertOptionalArguments(t, hostUSBSchema, []string{
|
||||
mkResourceVirtualEnvironmentVMHostUSBDeviceMapping,
|
||||
})
|
||||
|
||||
test.AssertValueTypes(t, hostUSBSchema, map[string]schema.ValueType{
|
||||
mkResourceVirtualEnvironmentVMHostUSBDevice: schema.TypeString,
|
||||
mkResourceVirtualEnvironmentVMHostUSBDeviceUSB3: schema.TypeBool,
|
||||
})
|
||||
|
||||
initializationDNSSchema := test.AssertNestedSchemaExistence(
|
||||
t,
|
||||
initializationSchema,
|
||||
|
Loading…
Reference in New Issue
Block a user