Skip to content

Commit

Permalink
machine: qemu: add usb host passthrough
Browse files Browse the repository at this point in the history
QEMU usb-host driver which is the one for passthrough, supports two
options for selecting an USB devices in the host to provide it to the
VM:
 - Bus and Device number the device is plugged
 - Vendor and Product information of the USB devices

    https://qemu-project.gitlab.io/qemu/system/devices/usb.html

This commit allows a user to configure podman machine with either of
options, with new --usb command line option for podman machine init.

Examples
  podman machine init tosovm4 --usb vendor=13d3,product=5406
  podman machine init tosovm3 --usb bus=1,devnum=4 --usb bus=1,devnum=3

This commit also allows a user to change the USBs configured with
--usb command line option for podman machine set.

Note that this commit does not handle host device permissions nor
verify that the USB devices exists.

Signed-off-by: Victor Toso <victortoso@redhat.com>
  • Loading branch information
victortoso committed Nov 8, 2023
1 parent 6bb2edd commit c23963d
Show file tree
Hide file tree
Showing 15 changed files with 260 additions and 0 deletions.
5 changes: 5 additions & 0 deletions cmd/podman/machine/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions cmd/podman/machine/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type SetFlags struct {
Memory uint64
Rootful bool
UserModeNetworking bool
USBs []string
}

func init() {
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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 {
Expand Down
16 changes: 16 additions & 0 deletions docs/source/markdown/podman-machine-init.1.md.in
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions docs/source/markdown/podman-machine-set.1.md.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions pkg/machine/applehv/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions pkg/machine/applehv/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,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()
Expand Down
11 changes: 11 additions & 0 deletions pkg/machine/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -106,6 +107,7 @@ type SetOptions struct {
Memory *uint64
Rootful *bool
UserModeNetworking *bool
USBs *[]string
}

type SSHOptions struct {
Expand Down Expand Up @@ -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
Expand All @@ -279,6 +288,8 @@ type ResourceConfig struct {
DiskSize uint64
// Memory in megabytes assigned to the vm
Memory uint64
// Usbs
USBs []USBConfig
}

type Mount struct {
Expand Down
3 changes: 3 additions & 0 deletions pkg/machine/hyperv/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions pkg/machine/hyperv/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,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 {
Expand Down
19 changes: 19 additions & 0 deletions pkg/machine/qemu/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"strconv"

"github.com/containers/podman/v4/pkg/machine"
"github.com/containers/podman/v4/pkg/machine/define"
)

Expand Down Expand Up @@ -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,
Expand Down
76 changes: 76 additions & 0 deletions pkg/machine/qemu/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -104,6 +177,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()

Expand Down
79 changes: 79 additions & 0 deletions pkg/machine/qemu/config_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
Loading

0 comments on commit c23963d

Please sign in to comment.