Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
S
Secure solution for nexus infrastructure
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Package registry
Model registry
Operate
Environments
Terraform modules
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
GitLab community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
flg_masters
TM
Secure solution for nexus infrastructure
Commits
af912ecc
Commit
af912ecc
authored
2 years ago
by
Florent Gluck
Browse files
Options
Downloads
Patches
Plain Diff
Ongoing work on usb redirection
parent
03aa6c5e
No related branches found
No related tags found
No related merge requests found
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
src/exec/QemuSystem.go
+57
-2
57 additions, 2 deletions
src/exec/QemuSystem.go
src/router/routerVMs.go
+4
-3
4 additions, 3 deletions
src/router/routerVMs.go
src/version/version.go
+1
-1
1 addition, 1 deletion
src/version/version.go
src/vms/vm.go
+67
-25
67 additions, 25 deletions
src/vms/vm.go
with
129 additions
and
31 deletions
src/exec/QemuSystem.go
+
57
−
2
View file @
af912ecc
...
...
@@ -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
}
This diff is collapsed.
Click to expand it.
src/router/routerVMs.go
+
4
−
3
View file @
af912ecc
...
...
@@ -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
())
}
...
...
This diff is collapsed.
Click to expand it.
src/version/version.go
+
1
−
1
View file @
af912ecc
...
...
@@ -6,7 +6,7 @@ import (
const
(
major
=
1
minor
=
5
minor
=
6
bugfix
=
0
)
...
...
This diff is collapsed.
Click to expand it.
src/vms/vm.go
+
67
−
25
View file @
af912ecc
...
...
@@ -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
}
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment