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

@ -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)