Skip to content
Snippets Groups Projects
Commit af912ecc authored by Florent Gluck's avatar Florent Gluck
Browse files

Ongoing work on usb redirection

parent 03aa6c5e
No related branches found
No related tags found
No related merge requests found
......@@ -38,7 +38,8 @@ func CheckQemuSystem() error {
// Creates a qemu-system command.
// The nic argument must be either "none" or "user".
func NewQemuSystem(qgaSock string, cpus, ram int, nic, diskFile string, spicePort int, secretPwdFile, certDir string) (*exec.Cmd, error) {
func NewQemuSystem(qgaSock string, cpus, ram int, nic string, usbDevs []string, diskFile string, spicePort int, secretPwdFile, certDir string) (*exec.Cmd, error) {
// Network config
nicArgs := "none"
if nic == "user" {
addr, err := utils.RandMacAddress()
......@@ -48,6 +49,35 @@ func NewQemuSystem(qgaSock string, cpus, ram int, nic, diskFile string, spicePor
nicArgs = "user,mac="+addr+",model=virtio-net-pci"
}
// If there are USB devices, generates the QEMU command line to support them
usb := []string{}
if len(usbDevs) > 0 {
// A qemu-xhci device is required
usb = append(usb, "-device qemu-xhci")
// Generate the USB devices' filter list, for instance:
// "-1:0x0781:0x5567:-1:1|-1:0x067b:0x2303:-1:1|-1:-1:-1:-1:0"
var filter strings.Builder
for _, dev := range usbDevs {
filter.WriteString("-1:")
ids := strings.Split(dev, ":") // Extracts vendorID (vid) and productID (pid)
usbDev := "0x"+ids[0]+":0x"+ids[1]
filter.WriteString(usbDev)
filter.WriteString(":-1:1|")
}
filter.WriteString("-1:-1:-1:-1:0")
// Generate QEMU's arguments for the USB devices
for i, _ := range usbDevs {
idx := strconv.Itoa(i+1)
usb = append(usb, "-chardev")
usb = append(usb, "spicevmc,name=usbredir,id=usbredir"+idx)
usb = append(usb, "-device")
usb = append(usb, "usb-redir,filter='"+filter.String()+"',chardev=usbredir"+idx)
}
}
// Remarks:
// 1) This device is to allow copy/paste between guest & client
// -device virtserialport,chardev=spicechannel0,name=com.redhat.spice.0
......@@ -57,7 +87,32 @@ func NewQemuSystem(qgaSock string, cpus, ram int, nic, diskFile string, spicePor
// - In the guest: mkdir shared && sudo mount -t 9p sharedfs shared
// 4) For QEMU Guest Agent support:
// -device virtio-serial -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0 -chardev socket,path=path_to_sock_file,server=on,wait=off,id=qga0
cmd := exec.Command(qemusystemBinary, "-enable-kvm", "-cpu", "host", "-smp", "cpus="+strconv.Itoa(cpus), "-m", strconv.Itoa(ram), "-drive", "file="+diskFile+",index=0,media=disk,format=qcow2,discard=unmap,detect-zeroes=unmap,if=virtio", "-vga", "virtio", "-device", "virtio-serial-pci", "-object", "secret,id=sec,file="+secretPwdFile+",format=base64", "-spice", "tls-port="+strconv.Itoa(spicePort)+",password-secret=sec,x509-dir="+certDir, "-device", "virtserialport,chardev=spicechannel0,name=com.redhat.spice.0", "-chardev", "spicevmc,id=spicechannel0,name=vdagent", "-nic", nicArgs, "-device", "virtio-serial", "-device", "virtserialport,chardev=qga0,name=org.qemu.guest_agent.0", "-chardev", "socket,path="+qgaSock+",server=on,wait=off,id=qga0")
// Enable KVM
args := []string{"-enable-kvm"}
// CPU
args = append(args, "-cpu", "host", "-smp", "cpus="+strconv.Itoa(cpus))
// RAM
args = append(args, "-m", strconv.Itoa(ram))
// Harddrive
args = append(args, "-drive", "file="+diskFile+",index=0,media=disk,format=qcow2,discard=unmap,detect-zeroes=unmap,if=virtio")
// Display
args = append(args, "-vga", "virtio")
// Virtio serial
args = append(args, "-device", "virtio-serial-pci")
// Spice
args = append(args, "-object", "secret,id=sec,file="+secretPwdFile+",format=base64", "-spice", "tls-port="+strconv.Itoa(spicePort)+",password-secret=sec,x509-dir="+certDir, "-device", "virtserialport,chardev=spicechannel0,name=com.redhat.spice.0", "-chardev", "spicevmc,id=spicechannel0,name=vdagent")
// Network
args = append(args, "-nic", nicArgs)
// QEMU Guest Agent
args = append(args, "-device", "virtio-serial", "-device", "virtserialport,chardev=qga0,name=org.qemu.guest_agent.0", "-chardev", "socket,path="+qgaSock+",server=on,wait=off,id=qga0")
// USB redirection
for _, u := range usb {
args = append(args, u)
}
//cmd := exec.Command(qemusystemBinary, "-enable-kvm", "-cpu", "host", "-smp", "cpus="+strconv.Itoa(cpus), "-m", strconv.Itoa(ram), "-drive", "file="+diskFile+",index=0,media=disk,format=qcow2,discard=unmap,detect-zeroes=unmap,if=virtio", "-vga", "virtio", "-device", "virtio-serial-pci", "-object", "secret,id=sec,file="+secretPwdFile+",format=base64", "-spice", "tls-port="+strconv.Itoa(spicePort)+",password-secret=sec,x509-dir="+certDir, "-device", "virtserialport,chardev=spicechannel0,name=com.redhat.spice.0", "-chardev", "spicevmc,id=spicechannel0,name=vdagent", "-nic", nicArgs, "-device", "virtio-serial", "-device", "virtserialport,chardev=qga0,name=org.qemu.guest_agent.0", "-chardev", "socket,path="+qgaSock+",server=on,wait=off,id=qga0", usb)
cmd := exec.Command(qemusystemBinary, args...)
return cmd, nil
}
......@@ -181,8 +181,9 @@ func (r *RouterVMs)CreateVM(c echo.Context) error {
type Parameters struct {
Name string `json:"name" validate:"required,min=4,max=256"`
Cpus int `json:"cpus" validate:"required,gte=1,lte=16"`
Ram int `json:"ram" validate:"required,gte=512,lte=32768"` // in MB
Nic vms.NicType `json:"nic" validate:"required"` // "none" or "user"
Ram int `json:"ram" validate:"required,gte=512,lte=32768"`
Nic vms.NicType `json:"nic" validate:"required`
UsbDevs []string `json:"usbDevs" validate:"required`
TemplateID uuid.UUID `json:"templateID" validate:"required"`
}
p := new(Parameters)
......@@ -191,7 +192,7 @@ func (r *RouterVMs)CreateVM(c echo.Context) error {
}
// Creates a new VM from the client's parameters.
vm, err := vms.NewVM(user.Email, p.Name, p.Cpus, p.Ram, p.Nic, p.TemplateID, user.Email)
vm, err := vms.NewVM(user.Email, p.Name, p.Cpus, p.Ram, p.Nic, p.UsbDevs, p.TemplateID, user.Email)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
......
......@@ -6,7 +6,7 @@ import (
const (
major = 1
minor = 5
minor = 6
bugfix = 0
)
......
......@@ -5,6 +5,7 @@ import (
"sync"
"path"
"errors"
"strings"
"syscall"
"net/mail"
"io/ioutil"
......@@ -13,8 +14,8 @@ import (
"encoding/base64"
"nexus-server/qga"
"nexus-server/caps"
"nexus-server/paths"
"nexus-server/exec"
"nexus-server/paths"
"github.com/google/uuid"
"github.com/go-playground/validator/v10"
"github.com/sethvargo/go-password/password"
......@@ -28,9 +29,10 @@ type (
Name string `json:"name" validate:"required,min=2,max=256"`
Cpus int `json:"cpus" validate:"required,gte=1,lte=16"`
Ram int `json:"ram" validate:"required,gte=512,lte=32768"` // in MB
Nic NicType `json:"nic" validate:"required"` // "none" or "user"
Nic NicType `json:"nic" validate:"required"`
UsbDevs []string `json:"usbDevs" validate:"required"`
TemplateID uuid.UUID `json:"templateID" validate:"required"`
Access map[string]caps.Capabilities `json:"access" validate:"required"`
Access map[string]caps.Capabilities `json:"access"`
// None of the fields below are serialized to disk.
dir string // VM directory
......@@ -40,44 +42,46 @@ type (
mutex *sync.Mutex
}
runStates struct {
State VMState
Pid int
Port int
Pwd string
}
VMState string
NicType string
// VM fields to be serialized to disk.
VMDiskSerialized struct {
ID uuid.UUID `json:"id"`
Owner string `json:"owner"`
Name string `json:"name"`
Cpus int `json:"cpus"`
Ram int `json:"ram"` // in MB
Ram int `json:"ram"`
Nic NicType `json:"nic"`
UsbDevs []string `json:"usbDevs"`
TemplateID uuid.UUID `json:"templateID"`
Access map[string]caps.Capabilities `json:"access"`
}
// VM fields to be serialized over the network (sent to the client).
VMNetworkSerialized struct {
ID uuid.UUID
Owner string
Name string
Cpus int
Ram int
Nic NicType
TemplateID uuid.UUID
Access map[string]caps.Capabilities
ID uuid.UUID `json:"id"`
Owner string `json:"owner"`
Name string `json:"name"`
Cpus int `json:"cpus"`
Ram int `json:"ram"`
Nic NicType `json:"nic"`
UsbDevs []string `json:"usbDevs"`
TemplateID uuid.UUID `json:"templateID"`
Access map[string]caps.Capabilities `json:"access"`
State VMState `json:"state"`
Port int `json:"port"`
Pwd string `json:"pwd"`
DiskBusy bool `json:"diskBusy"`
}
runStates struct {
State VMState
Pid int
Port int
Pwd string
DiskBusy bool // If true the VM's disk is busy (cannot be modified or deleted)
}
VMState string // see stateXXX defined below
NicType string // see nicXXX defined below
endOfExecCallback func(vm *VM)
)
......@@ -104,7 +108,7 @@ var passwordGen, _ = password.NewGenerator(&password.GeneratorInput{
})
// Creates a VM.
func NewVM(creatorEmail string, name string, cpus, ram int, nic NicType, templateID uuid.UUID, owner string) (*VM, error) {
func NewVM(creatorEmail string, name string, cpus, ram int, nic NicType, usbDevs []string, templateID uuid.UUID, owner string) (*VM, error) {
vmID, err := uuid.NewRandom()
if err != nil {
......@@ -118,6 +122,7 @@ func NewVM(creatorEmail string, name string, cpus, ram int, nic NicType, templat
vm.Cpus = cpus
vm.Ram = ram
vm.Nic = nic
vm.UsbDevs = usbDevs
vm.TemplateID = templateID
id := vmID.String()
vm.dir = filepath.Join(vms.dir, id[0:3], id[3:6], id)
......@@ -139,6 +144,7 @@ func (vm *VM)SerializeToDisk() VMDiskSerialized {
Cpus: vm.Cpus,
Ram: vm.Ram,
Nic: vm.Nic,
UsbDevs: vm.UsbDevs,
TemplateID: vm.TemplateID,
Access: vm.Access,
}
......@@ -152,6 +158,7 @@ func (vm *VM)SerializeToNetwork() VMNetworkSerialized {
Cpus: vm.Cpus,
Ram: vm.Ram,
Nic: vm.Nic,
UsbDevs: vm.UsbDevs,
TemplateID: vm.TemplateID,
Access: vm.Access,
State: vm.Run.State,
......@@ -180,6 +187,7 @@ func newEmptyVM() *VM {
Cpus: 0,
Ram: 0,
Nic: "",
UsbDevs: []string{},
TemplateID: uuid.Nil,
Access: make(map[string]caps.Capabilities),
......@@ -237,6 +245,10 @@ func (vm *VM)validate() error {
return errors.New("Invalid nic value: "+string(vm.Nic))
}
if err := validateUsbDevs(vm.UsbDevs); err != nil {
return err
}
if err := validator.New().Struct(vm); err != nil {
return err
}
......@@ -473,7 +485,7 @@ func (vm *VM)reboot() error {
// Executes the VM in QEMU using the specified spice port and password.
func (vm *VM)runQEMU(port int, pwd, pwdFile string, endofExecFn endOfExecCallback) error {
pkiDir := paths.GetInstance().NexusPkiDir
cmd, err := exec.NewQemuSystem(vm.qgaSock, vm.Cpus, vm.Ram, string(vm.Nic), filepath.Join(vm.dir, vmDiskFile), port, pwdFile, pkiDir)
cmd, err := exec.NewQemuSystem(vm.qgaSock, vm.Cpus, vm.Ram, string(vm.Nic), vm.UsbDevs, filepath.Join(vm.dir, vmDiskFile), port, pwdFile, pkiDir)
if err != nil {
return err
}
......@@ -503,3 +515,33 @@ func (vm *VM)runQEMU(port int, pwd, pwdFile string, endofExecFn endOfExecCallbac
func (vm *VM)resetStates() {
vm.Run = runStates { State: stateStopped, Pid: 0, Port: 0, Pwd: "" }
}
// Validates and convert a string of USB devices of the form "1fc9:001d,067b:2303"
// into a slice of string where each element is a string of the form "1fc9:001d".
func validateUsbDevs(devs []string) error {
for _, dev := range devs {
ids := strings.Split(dev, ":") // Extracts vendorID (vid) and productID (pid)
if len(ids) != 2 { // expect exactly two values: vid and pid
return errors.New("Invalid USB device syntax")
}
vid, pid := ids[0], ids[1]
if !isUsbId(vid) || !isUsbId(pid) {
return errors.New("Invalid USB vendor/product ID")
}
}
return nil
}
// A USB ID is either a vendor ID or a product ID.
// It's a string containing exactly 4 hexadecimal digits.
func isUsbId(s string) (bool) {
if len(s) != 4 {
return false
}
for _, r := range s {
if !(r >= '0' && r <= '9' || r >= 'a' && r <= 'f' || r >= 'A' && r <= 'F') {
return false
}
}
return true
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment