Merge pull request #20540 from victortoso/usb-host-passthrough

qemu: add usb host passthrough
This commit is contained in:
openshift-merge-bot[bot]
2023-11-13 16:03:30 +00:00
committed by GitHub
15 changed files with 260 additions and 0 deletions

View File

@ -107,6 +107,11 @@ func init() {
flags.StringArrayVarP(&initOpts.Volumes, VolumeFlagName, "v", cfg.ContainersConfDefaultsRO.Machine.Volumes.Get(), "Volumes to mount, source:target")
_ = initCmd.RegisterFlagCompletionFunc(VolumeFlagName, completion.AutocompleteDefault)
USBFlagName := "usb"
flags.StringArrayVarP(&initOpts.USBs, USBFlagName, "", []string{},
"USB Host passthrough: bus=$1,devnum=$2 or vendor=$1,product=$2")
_ = initCmd.RegisterFlagCompletionFunc(USBFlagName, completion.AutocompleteDefault)
VolumeDriverFlagName := "volume-driver"
flags.StringVar(&initOpts.VolumeDriver, VolumeDriverFlagName, "", "Optional volume driver")
_ = initCmd.RegisterFlagCompletionFunc(VolumeDriverFlagName, completion.AutocompleteDefault)

View File

@ -37,6 +37,7 @@ type SetFlags struct {
Memory uint64
Rootful bool
UserModeNetworking bool
USBs []string
}
func init() {
@ -74,6 +75,13 @@ func init() {
)
_ = setCmd.RegisterFlagCompletionFunc(memoryFlagName, completion.AutocompleteNone)
usbFlagName := "usb"
flags.StringArrayVarP(
&setFlags.USBs,
usbFlagName, "", []string{},
"USBs bus=$1,devnum=$2 or vendor=$1,product=$2")
_ = setCmd.RegisterFlagCompletionFunc(usbFlagName, completion.AutocompleteNone)
userModeNetFlagName := "user-mode-networking"
flags.BoolVar(&setFlags.UserModeNetworking, userModeNetFlagName, false, // defaults not-relevant due to use of Changed()
"Whether this machine should use user-mode networking, routing traffic through a host user-space process")
@ -110,6 +118,9 @@ func setMachine(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("user-mode-networking") {
setOpts.UserModeNetworking = &setFlags.UserModeNetworking
}
if cmd.Flags().Changed("usb") {
setOpts.USBs = &setFlags.USBs
}
setErrs, lasterr := vm.Set(vmName, setOpts)
for _, err := range setErrs {

View File

@ -104,6 +104,20 @@ means to use the timezone of the machine host.
The timezone setting is not used with WSL. WSL automatically sets the timezone to the same
as the host Windows operating system.
#### **--usb**=*bus=number,devnum=number* or *vendor=hexadecimal,product=hexadecimal*
Assign a USB device from the host to the VM via USB passthrough.
Only supported for QEMU Machines.
The device needs to have proper permissions in order to be passed to the machine. This
means the device needs to be under your user group.
Note that using bus and device number are simpler but the values can change every boot
or when the device is unplugged.
When specifying a USB using vendor and product ID's, if more than one device has the
same vendor and product ID, the first available device is assigned.
@@option user-mode-networking
#### **--username**
@ -160,6 +174,8 @@ $ podman machine init --rootful
$ podman machine init --disk-size 50
$ podman machine init --memory=1024 myvm
$ podman machine init -v /Users:/mnt/Users
$ podman machine init --usb vendor=13d3,product=5406
$ podman machine init --usb bus=1,devnum=3
```
## SEE ALSO

View File

@ -52,6 +52,20 @@ are no longer visible with the default connection/socket. This is because the ro
users in the VM are completely separated and do not share any storage. The data however is not
lost and you can always change this option back or use the other connection to access it.
#### **--usb**=*bus=number,devnum=number* or *vendor=hexadecimal,product=hexadecimal* or *""*
Assign a USB device from the host to the VM.
Only supported for QEMU Machines.
The device needs to be present when the VM starts.
The device needs to have proper permissions in order to be assign to podman machine.
Use an empty string to remove all previously set USB devices.
Note that using bus and device number are simpler but the values can change every boot or when the
device is unplugged. Using vendor and product might lead to collision in the case of multiple
devices with the same vendor product value, the first available device is assigned.
@@option user-mode-networking
## EXAMPLES

View File

@ -113,6 +113,10 @@ func (v AppleHVVirtualization) LoadVMByName(name string) (machine.VM, error) {
func (v AppleHVVirtualization) NewMachine(opts machine.InitOptions) (machine.VM, error) {
m := MacMachine{Name: opts.Name}
if len(opts.USBs) > 0 {
return nil, fmt.Errorf("USB host passtrough not supported for applehv machines")
}
configDir, err := machine.GetConfDir(machine.AppleHvVirt)
if err != nil {
return nil, err

View File

@ -448,6 +448,9 @@ func (m *MacMachine) Set(name string, opts machine.SetOptions) ([]error, error)
}
}
}
if opts.USBs != nil {
setErrors = append(setErrors, errors.New("changing USBs not supported for applehv machines"))
}
// Write the machine config to the filesystem
err = m.writeConfig()

View File

@ -39,6 +39,7 @@ type InitOptions struct {
Rootful bool
UID string // uid of the user that called machine
UserModeNetworking *bool // nil = use backend/system default, false = disable, true = enable
USBs []string
}
type Status = string
@ -106,6 +107,7 @@ type SetOptions struct {
Memory *uint64
Rootful *bool
UserModeNetworking *bool
USBs *[]string
}
type SSHOptions struct {
@ -271,6 +273,13 @@ func ConfDirPrefix() (string, error) {
return confDir, nil
}
type USBConfig struct {
Bus string
DevNumber string
Vendor int
Product int
}
// ResourceConfig describes physical attributes of the machine
type ResourceConfig struct {
// CPUs to be assigned to the VM
@ -279,6 +288,8 @@ type ResourceConfig struct {
DiskSize uint64
// Memory in megabytes assigned to the vm
Memory uint64
// Usbs
USBs []USBConfig
}
type Mount struct {

View File

@ -111,6 +111,9 @@ func (v HyperVVirtualization) NewMachine(opts machine.InitOptions) (machine.VM,
if len(opts.ImagePath) < 1 {
return nil, errors.New("must define --image-path for hyperv support")
}
if len(opts.USBs) > 0 {
return nil, fmt.Errorf("USB host passtrough not supported for hyperv machines")
}
m.RemoteUsername = opts.Username

View File

@ -507,6 +507,10 @@ func (m *HyperVMachine) Set(name string, opts machine.SetOptions) ([]error, erro
memoryChanged = true
}
if opts.USBs != nil {
setErrors = append(setErrors, errors.New("changing USBs not supported for hyperv machines"))
}
if cpuChanged || memoryChanged {
err := vm.UpdateProcessorMemSettings(func(ps *hypervctl.ProcessorSettings) {
if cpuChanged {

View File

@ -4,6 +4,7 @@ import (
"fmt"
"strconv"
"github.com/containers/podman/v4/pkg/machine"
"github.com/containers/podman/v4/pkg/machine/define"
)
@ -46,6 +47,24 @@ func (q *QemuCmd) SetNetwork() {
*q = append(*q, "-netdev", "socket,id=vlan,fd=3", "-device", "virtio-net-pci,netdev=vlan,mac=5a:94:ef:e4:0c:ee")
}
// SetNetwork adds a network device to the machine
func (q *QemuCmd) SetUSBHostPassthrough(usbs []machine.USBConfig) {
if len(usbs) == 0 {
return
}
// Add xhci usb emulation first and then each usb device
*q = append(*q, "-device", "qemu-xhci")
for _, usb := range usbs {
var dev string
if usb.Bus != "" && usb.DevNumber != "" {
dev = fmt.Sprintf("usb-host,hostbus=%s,hostaddr=%s", usb.Bus, usb.DevNumber)
} else {
dev = fmt.Sprintf("usb-host,vendorid=%d,productid=%d", usb.Vendor, usb.Product)
}
*q = append(*q, "-device", dev)
}
}
// SetSerialPort adds a serial port to the machine for readiness
func (q *QemuCmd) SetSerialPort(readySocket, vmPidFile define.VMFile, name string) {
*q = append(*q,

View File

@ -6,6 +6,7 @@ import (
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@ -59,6 +60,78 @@ func (v *MachineVM) setNewMachineCMD(qemuBinary string, cmdOpts *setNewMachineCM
v.CmdLine.SetQmpMonitor(v.QMPMonitor)
v.CmdLine.SetNetwork()
v.CmdLine.SetSerialPort(v.ReadySocket, v.VMPidFilePath, v.Name)
v.CmdLine.SetUSBHostPassthrough(v.USBs)
}
func parseUSBs(usbs []string) ([]machine.USBConfig, error) {
configs := []machine.USBConfig{}
for _, str := range usbs {
if str == "" {
// Ignore --usb="" as it can be used to reset USBConfigs
continue
}
vals := strings.Split(str, ",")
if len(vals) != 2 {
return configs, fmt.Errorf("usb: fail to parse: missing ',': %s", str)
}
left := strings.Split(vals[0], "=")
if len(left) != 2 {
return configs, fmt.Errorf("usb: fail to parse: missing '=': %s", str)
}
right := strings.Split(vals[1], "=")
if len(right) != 2 {
return configs, fmt.Errorf("usb: fail to parse: missing '=': %s", str)
}
option := ""
if (left[0] == "bus" && right[0] == "devnum") ||
(right[0] == "bus" && left[0] == "devnum") {
option = "bus_devnum"
}
if (left[0] == "vendor" && right[0] == "product") ||
(right[0] == "vendor" && left[0] == "product") {
option = "vendor_product"
}
switch option {
case "bus_devnum":
bus, devnumber := left[1], right[1]
if right[0] == "bus" {
bus, devnumber = devnumber, bus
}
configs = append(configs, machine.USBConfig{
Bus: bus,
DevNumber: devnumber,
})
case "vendor_product":
vendorStr, productStr := left[1], right[1]
if right[0] == "vendor" {
vendorStr, productStr = productStr, vendorStr
}
vendor, err := strconv.ParseInt(vendorStr, 16, 0)
if err != nil {
return configs, fmt.Errorf("fail to convert vendor of %s: %s", str, err)
}
product, err := strconv.ParseInt(productStr, 16, 0)
if err != nil {
return configs, fmt.Errorf("fail to convert product of %s: %s", str, err)
}
configs = append(configs, machine.USBConfig{
Vendor: int(vendor),
Product: int(product),
})
default:
return configs, fmt.Errorf("usb: fail to parse: %s", str)
}
}
return configs, nil
}
// NewMachine initializes an instance of a virtual machine based on the qemu
@ -98,6 +171,9 @@ func (p *QEMUVirtualization) NewMachine(opts machine.InitOptions) (machine.VM, e
vm.CPUs = opts.CPUS
vm.Memory = opts.Memory
vm.DiskSize = opts.DiskSize
if vm.USBs, err = parseUSBs(opts.USBs); err != nil {
return nil, err
}
vm.Created = time.Now()

View File

@ -0,0 +1,79 @@
package qemu
import (
"reflect"
"testing"
"github.com/containers/podman/v4/pkg/machine"
)
func TestUSBParsing(t *testing.T) {
tests := []struct {
name string
args []string
result []machine.USBConfig
wantErr bool
}{
{
name: "Good vendor and product",
args: []string{"vendor=13d3,product=5406", "vendor=08ec,product=0016"},
result: []machine.USBConfig{
{
Vendor: 5075,
Product: 21510,
},
{
Vendor: 2284,
Product: 22,
},
},
wantErr: false,
},
{
name: "Good bus and device number",
args: []string{"bus=1,devnum=4", "bus=1,devnum=3"},
result: []machine.USBConfig{
{
Bus: "1",
DevNumber: "4",
},
{
Bus: "1",
DevNumber: "3",
},
},
wantErr: false,
},
{
name: "Bad vendor and product, not hexa",
args: []string{"vendor=13dk,product=5406"},
result: []machine.USBConfig{},
wantErr: true,
},
{
name: "Bad vendor and product, bad separator",
args: []string{"vendor=13d3:product=5406"},
result: []machine.USBConfig{},
wantErr: true,
},
{
name: "Bad vendor and product, missing equal",
args: []string{"vendor=13d3:product-5406"},
result: []machine.USBConfig{},
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := parseUSBs(test.args)
if (err != nil) != test.wantErr {
t.Errorf("parseUUBs error = %v, wantErr %v", err, test.wantErr)
return
}
if !reflect.DeepEqual(got, test.result) {
t.Errorf("parseUUBs got %v, want %v", got, test.result)
}
})
}
}

View File

@ -392,6 +392,14 @@ func (v *MachineVM) Set(_ string, opts machine.SetOptions) ([]error, error) {
}
}
if opts.USBs != nil {
if usbConfigs, err := parseUSBs(*opts.USBs); err != nil {
setErrors = append(setErrors, fmt.Errorf("failed to set usb: %w", err))
} else {
v.USBs = usbConfigs
}
}
err = v.writeConfig()
if err != nil {
setErrors = append(setErrors, err)

View File

@ -4,6 +4,7 @@
package wsl
import (
"fmt"
"io/fs"
"path/filepath"
"strings"
@ -29,6 +30,9 @@ func VirtualizationProvider() machine.VirtProvider {
// NewMachine initializes an instance of a wsl machine
func (p *WSLVirtualization) NewMachine(opts machine.InitOptions) (machine.VM, error) {
vm := new(MachineVM)
if len(opts.USBs) > 0 {
return nil, fmt.Errorf("USB host passtrough not supported for WSL machines")
}
if len(opts.Name) > 0 {
vm.Name = opts.Name
}

View File

@ -1135,7 +1135,10 @@ func (v *MachineVM) Set(_ string, opts machine.SetOptions) ([]error, error) {
if opts.Memory != nil {
setErrors = append(setErrors, errors.New("changing memory not supported for WSL machines"))
}
if opts.USBs != nil {
setErrors = append(setErrors, errors.New("changing USBs not supported for WSL machines"))
}
if opts.DiskSize != nil {