mirror of
https://github.com/containers/podman.git
synced 2025-05-20 08:36:23 +08:00
Merge pull request #20540 from victortoso/usb-host-passthrough
qemu: add usb host passthrough
This commit is contained in:
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
||||
|
79
pkg/machine/qemu/config_test.go
Normal file
79
pkg/machine/qemu/config_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user