mirror of
https://github.com/containers/podman.git
synced 2025-11-01 02:42:11 +08:00
Merge pull request #20540 from victortoso/usb-host-passthrough
qemu: add usb host passthrough
This commit is contained in:
@ -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)
|
||||
|
||||
Reference in New Issue
Block a user