Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for a GUI, virtio-input and virtio-gpu devices #44

Merged
merged 1 commit into from
Jun 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 36 additions & 10 deletions cmd/vfkit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"fmt"
"os"
"os/signal"
"runtime"
"syscall"
"time"

Expand Down Expand Up @@ -113,10 +114,18 @@ func waitForVMState(vm *vz.VirtualMachine, state vz.VirtualMachineState) error {
}

func runVFKit(vmConfig *config.VirtualMachine, opts *cmdline.Options) error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
vzVMConfig, err := vf.ToVzVirtualMachineConfig(vmConfig)
if err != nil {
return err
}

gpuDevs := vmConfig.VirtioGPUDevices()
if opts.UseGUI && len(gpuDevs) > 0 {
gpuDevs[0].UsesGUI = true
}

vm, err := vz.NewVirtualMachine(vzVMConfig)
if err != nil {
return err
Expand Down Expand Up @@ -168,18 +177,35 @@ func runVirtualMachine(vmConfig *config.VirtualMachine, vm *vz.VirtualMachine) e
}

log.Infof("waiting for VM to stop")
for {
err := waitForVMState(vm, vz.VirtualMachineStateStopped)
if err == nil {
log.Infof("VM is stopped")
break

errCh := make(chan error, 1)
go func() {
for {
err := waitForVMState(vm, vz.VirtualMachineStateStopped)
if err == nil {
log.Infof("VM is stopped")
errCh <- nil
return
}
if !errors.Is(err, errVMStateTimeout) {
errCh <- fmt.Errorf("virtualization error: %v", err)
return
}
// errVMStateTimeout -> keep looping
}
if !errors.Is(err, errVMStateTimeout) {
log.Infof("virtualization error: %v", err)
return err
}()

for _, gpuDev := range vmConfig.VirtioGPUDevices() {
if gpuDev.UsesGUI {
runtime.LockOSThread()
jakecorrenti marked this conversation as resolved.
Show resolved Hide resolved
err := vm.StartGraphicApplication(float64(gpuDev.Height), float64(gpuDev.Width))
runtime.UnlockOSThread()
if err != nil {
return err
}
break
}
// errVMStateTimeout -> keep looping
}

return nil
return <-errCh
}
44 changes: 44 additions & 0 deletions doc/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,33 @@ The share can be mounted in the guest with `mount -t virtiofs vfkitTag /mnt`, wi
#### Example
`--device virtio-fs,sharedDir=/Users/virtuser/vfkit/,mountTag=vfkit-share`

### GPU

#### Description

The `--device virtio-gpu` option allows the user to add graphical devices to the virtual machine.

#### Arguments
- `height`: the vertical resolution of the graphical device's resolution. Defaults to 800
- `width`: the horizontal resolution of the graphical device's resolution. Defaults to 600

#### Example
`--device virtio-gpu,height=1920,width=1080`

### Input

#### Description

The `--device virtio-input` option allows the user to add an input device to the virtual machine. This currently supports `pointing` and `keyboard` devices.

#### Arguments

None

#### Example

`--device virtio-input,pointing`


## Restful Service

Expand Down Expand Up @@ -242,3 +269,20 @@ Get description of the virtual machine

GET `/vm/inspect`
Response: { "cpus": uint, "memory": uint64, "devices": []config.VirtIODevice }

## Enabling a Graphical User Interface

### Add a virtio-gpu device

In order to successfully start a graphical application window, a virtio-gpu device must be added to the virtual machine.

### Pass the `--gui` flag

In order to tell vfkit that you want to start a graphical application window, you need to pass the `--gui` flag in your command.

### Usage

Proper use of this flag may look similar to the following section of a command:
```bash
--device virtio-input,keyboard --device virtio-input,pointing --device virtio-gpu,height=1920,width=1000 --gui
```
3 changes: 3 additions & 0 deletions pkg/cmdline/cmdline.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type Options struct {
RestfulURI string

LogLevel string

UseGUI bool
}

const DefaultRestfulURI = "none://"
Expand All @@ -31,6 +33,7 @@ func AddFlags(cmd *cobra.Command, opts *Options) {
cmd.Flags().StringVarP(&opts.InitrdPath, "initrd", "i", "", "path to the virtual machine initrd")

cmd.Flags().VarP(&opts.Bootloader, "bootloader", "b", "bootloader configuration")
cmd.Flags().BoolVar(&opts.UseGUI, "gui", false, "display the contents of the virtual machine onto a graphical user interface")

cmd.MarkFlagsMutuallyExclusive("kernel", "bootloader")
cmd.MarkFlagsMutuallyExclusive("initrd", "bootloader")
Expand Down
11 changes: 11 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,17 @@ func (vm *VirtualMachine) AddDevicesFromCmdLine(cmdlineOpts []string) error {
return nil
}

func (vm *VirtualMachine) VirtioGPUDevices() []*VirtioGPU {
gpuDevs := []*VirtioGPU{}
for _, dev := range vm.Devices {
if gpuDev, isVirtioGPU := dev.(*VirtioGPU); isVirtioGPU {
gpuDevs = append(gpuDevs, gpuDev)
}
}

return gpuDevs
}

func (vm *VirtualMachine) VirtioVsockDevices() []*VirtioVsock {
vsockDevs := []*VirtioVsock{}
for _, dev := range vm.Devices {
Expand Down
139 changes: 139 additions & 0 deletions pkg/config/virtio.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,37 @@ import (
// The VirtioDevice interface is an interface which is implemented by all virtio devices.
type VirtioDevice VMComponent

const (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it makes sense to break this group of devices into something like graphics.go or display.go. Dealer's choice.

// Possible values for VirtioInput.InputType
VirtioInputPointingDevice = "pointing"
VirtioInputKeyboardDevice = "keyboard"

// Options for VirtioGPUResolution
VirtioGPUResolutionHeight = "height"
VirtioGPUResolutionWidth = "width"

// Default VirtioGPU Resolution
defaultVirtioGPUResolutionHeight = 800
defaultVirtioGPUResolutionWidth = 600
)

// VirtioInput configures an input device, such as a keyboard or pointing device
// (mouse) that the virtual machine can use
type VirtioInput struct {
jakecorrenti marked this conversation as resolved.
Show resolved Hide resolved
InputType string `json:"inputType"` // currently supports "pointing" and "keyboard" input types
}

type VirtioGPUResolution struct {
Height int `json:"height"`
Width int `json:"width"`
}

// VirtioGPU configures a GPU device, such as the host computer's display
type VirtioGPU struct {
UsesGUI bool `json:"usesGUI"`
VirtioGPUResolution
}

// VirtioVsock configures of a virtio-vsock device allowing 2-way communication
// between the host and the virtual machine type
type VirtioVsock struct {
Expand Down Expand Up @@ -114,6 +145,10 @@ func deviceFromCmdLine(deviceOpts string) (VirtioDevice, error) {
dev = &VirtioVsock{}
case "usb-mass-storage":
dev = usbMassStorageNewEmpty()
case "virtio-input":
dev = &VirtioInput{}
case "virtio-gpu":
dev = &VirtioGPU{}
default:
return nil, fmt.Errorf("unknown device type: %s", opts[0])
}
Expand Down Expand Up @@ -181,6 +216,110 @@ func (dev *VirtioSerial) FromOptions(options []option) error {
return dev.validate()
}

// VirtioInputNew creates a new input device for the virtual machine.
// The inputType parameter is the type of virtio-input device that will be added
// to the machine.
func VirtioInputNew(inputType string) (VirtioDevice, error) {
dev := &VirtioInput{
InputType: inputType,
}
if err := dev.validate(); err != nil {
return nil, err
}

return dev, nil
}

func (dev *VirtioInput) validate() error {
if dev.InputType != VirtioInputPointingDevice && dev.InputType != VirtioInputKeyboardDevice {
return fmt.Errorf("Unknown option for virtio-input devices: %s", dev.InputType)
}

return nil
}

func (dev *VirtioInput) ToCmdLine() ([]string, error) {
if err := dev.validate(); err != nil {
return nil, err
}

return []string{"--device", fmt.Sprintf("virtio-input,%s", dev.InputType)}, nil
}

func (dev *VirtioInput) FromOptions(options []option) error {
for _, option := range options {
switch option.key {
case VirtioInputPointingDevice, VirtioInputKeyboardDevice:
if option.value != "" {
return fmt.Errorf(fmt.Sprintf("Unexpected value for virtio-input %s option: %s", option.key, option.value))
}
dev.InputType = option.key
default:
return fmt.Errorf("Unknown option for virtio-input devices: %s", option.key)
}
}
return dev.validate()
}

// VirtioGPUNew creates a new gpu device for the virtual machine.
// The usesGUI parameter determines whether a graphical application window will
// be displayed
func VirtioGPUNew() (VirtioDevice, error) {
return &VirtioGPU{
UsesGUI: false,
VirtioGPUResolution: VirtioGPUResolution{
Height: defaultVirtioGPUResolutionHeight,
Width: defaultVirtioGPUResolutionWidth,
},
}, nil
}

func (dev *VirtioGPU) validate() error {
if dev.Height < 1 || dev.Width < 1 {
return fmt.Errorf("Invalid dimensions for virtio-gpu device resolution: %dx%d", dev.Height, dev.Width)
}

return nil
}

func (dev *VirtioGPU) ToCmdLine() ([]string, error) {
if err := dev.validate(); err != nil {
return nil, err
}

return []string{"--device", fmt.Sprintf("virtio-gpu,height=%d,width=%d", dev.Height, dev.Width)}, nil
}

func (dev *VirtioGPU) FromOptions(options []option) error {
for _, option := range options {
switch option.key {
case VirtioGPUResolutionHeight:
height, err := strconv.Atoi(option.value)
if err != nil || height < 1 {
return fmt.Errorf(fmt.Sprintf("Invalid value for virtio-gpu %s: %s", option.key, option.value))
}

dev.Height = height
case VirtioGPUResolutionWidth:
width, err := strconv.Atoi(option.value)
if err != nil || width < 1 {
return fmt.Errorf(fmt.Sprintf("Invalid value for virtio-gpu %s: %s", option.key, option.value))
}

dev.Width = width
default:
return fmt.Errorf("Unknown option for virtio-gpu devices: %s", option.key)
jakecorrenti marked this conversation as resolved.
Show resolved Hide resolved
}
}

if dev.Width == 0 && dev.Height == 0 {
dev.Width = defaultVirtioGPUResolutionWidth
dev.Height = defaultVirtioGPUResolutionHeight
}

return dev.validate()
}

// VirtioNetNew creates a new network device for the virtual machine. It will
// use macAddress as its MAC address.
func VirtioNetNew(macAddress string) (*VirtioNet, error) {
Expand Down
37 changes: 37 additions & 0 deletions pkg/config/virtio_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,43 @@ var virtioDevTests = map[string]virtioDevTest{
},
expectedCmdLine: []string{"--device", "usb-mass-storage,path=/foo/bar"},
},
"NewVirtioInputWithPointingDevice": {
newDev: func() (VirtioDevice, error) { return VirtioInputNew("pointing") },
expectedDev: &VirtioInput{
InputType: "pointing",
},
expectedCmdLine: []string{"--device", "virtio-input,pointing"},
},
"NewVirtioInputWithKeyboardDevice": {
newDev: func() (VirtioDevice, error) { return VirtioInputNew("keyboard") },
expectedDev: &VirtioInput{
InputType: "keyboard",
},
expectedCmdLine: []string{"--device", "virtio-input,keyboard"},
},
"NewVirtioGPUDevice": {
newDev: VirtioGPUNew,
expectedDev: &VirtioGPU{
false,
VirtioGPUResolution{800, 600},
},
expectedCmdLine: []string{"--device", "virtio-gpu,height=800,width=600"},
},
"NewVirtioGPUDeviceWithDimensions": {
newDev: func() (VirtioDevice, error) {
dev, err := VirtioGPUNew()
if err != nil {
return nil, err
}
dev.(*VirtioGPU).VirtioGPUResolution = VirtioGPUResolution{1920, 1080}
return dev, nil
},
expectedDev: &VirtioGPU{
false,
VirtioGPUResolution{1920, 1080},
},
expectedCmdLine: []string{"--device", "virtio-gpu,height=1920,width=1080"},
},
}

func testVirtioDev(t *testing.T, test *virtioDevTest) {
Expand Down
Loading