Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

qemu: add usb host passthrough #20540

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think usb: adds more context and consistent with other error message.

Suggested change
return configs, fmt.Errorf("fail to convert vendor of %s: %s", str, err)
return configs, fmt.Errorf("usb: fail to convert vendor of %s: %s", str, err)

Comment on lines +89 to +118
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be simplified

option := left[0] +"_"+left[1] // and switch can be match against "vendor_product" and "product_vendor" both.

switch

}

product, err := strconv.ParseInt(productStr, 16, 0)
if err != nil {
return configs, fmt.Errorf("fail to convert product of %s: %s", str, err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

Suggested change
return configs, fmt.Errorf("fail to convert product of %s: %s", str, err)
return configs, fmt.Errorf("usb: 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