diff --git a/cmd/vfkit/main.go b/cmd/vfkit/main.go index 73f2e165..2f4cb85a 100644 --- a/cmd/vfkit/main.go +++ b/cmd/vfkit/main.go @@ -24,6 +24,7 @@ import ( "fmt" "os" "os/signal" + "runtime" "syscall" "time" @@ -113,10 +114,22 @@ 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 } + + if opts.UseGUI { + for _, gpuDev := range vmConfig.VirtioGPUDevices() { + if gpuDev.Device == config.VirtioGPUDisplayDevice { + gpuDev.UsesGUI = true + break + } + } + } + vm, err := vz.NewVirtualMachine(vzVMConfig) if err != nil { return err @@ -168,18 +181,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() + err := vm.StartGraphicApplication(float64(gpuDev.Height), float64(gpuDev.Width)) + runtime.UnlockOSThread() + if err != nil { + return err + } + break } - // errVMStateTimeout -> keep looping } - return nil + return <-errCh } diff --git a/doc/usage.md b/doc/usage.md index 494cf30d..9272f31d 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -214,6 +214,34 @@ 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. A graphical device may include a display which can be used to start a graphical application window. +This feature currently only supports the `display` option. + +#### 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,display,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 @@ -242,3 +270,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,display,height=1920,width=1000 --gui +``` diff --git a/pkg/cmdline/cmdline.go b/pkg/cmdline/cmdline.go index 31efb8b8..d912240b 100644 --- a/pkg/cmdline/cmdline.go +++ b/pkg/cmdline/cmdline.go @@ -21,6 +21,8 @@ type Options struct { RestfulURI string LogLevel string + + UseGUI bool } const DefaultRestfulURI = "none://" @@ -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") diff --git a/pkg/config/config.go b/pkg/config/config.go index 3c053e7f..43a3dc68 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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 { diff --git a/pkg/config/virtio.go b/pkg/config/virtio.go index 035fd452..f1f04bf4 100644 --- a/pkg/config/virtio.go +++ b/pkg/config/virtio.go @@ -11,6 +11,41 @@ import ( // The VirtioDevice interface is an interface which is implemented by all virtio devices. type VirtioDevice VMComponent +const ( + // Possible values for VirtioInput.InputType + VirtioInputPointingDevice = "pointing" + VirtioInputKeyboardDevice = "keyboard" + + // Possible values for VirtioGPU.Device + VirtioGPUDisplayDevice = "display" + + // 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 { + 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 { + Device string `json:"device"` // currently supports "display" as the device + 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 { @@ -114,6 +149,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]) } @@ -181,6 +220,120 @@ 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{ + Device: VirtioGPUDisplayDevice, + UsesGUI: false, + VirtioGPUResolution: VirtioGPUResolution{ + Height: defaultVirtioGPUResolutionHeight, + Width: defaultVirtioGPUResolutionWidth, + }, + }, nil +} + +func (dev *VirtioGPU) validate() error { + if dev.Device != VirtioGPUDisplayDevice { + return fmt.Errorf("Unknown option for virtio-gpu devices: %s", dev.Device) + } + + 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,%s,height=%d,width=%d", dev.Device, dev.Height, dev.Width)}, nil +} + +func (dev *VirtioGPU) FromOptions(options []option) error { + for _, option := range options { + switch option.key { + case VirtioGPUDisplayDevice: + if option.value != "" { + return fmt.Errorf(fmt.Sprintf("Unexpected value for virtio-gpu %s option: %s", option.key, option.value)) + } + dev.Device = 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) + } + } + + 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) { diff --git a/pkg/config/virtio_test.go b/pkg/config/virtio_test.go index 4b249b6e..02fae384 100644 --- a/pkg/config/virtio_test.go +++ b/pkg/config/virtio_test.go @@ -140,6 +140,45 @@ 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{ + "display", + false, + VirtioGPUResolution{800, 600}, + }, + expectedCmdLine: []string{"--device", "virtio-gpu,display,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{ + "display", + false, + VirtioGPUResolution{1920, 1080}, + }, + expectedCmdLine: []string{"--device", "virtio-gpu,display,height=1920,width=1080"}, + }, } func testVirtioDev(t *testing.T, test *virtioDevTest) { diff --git a/pkg/vf/virtio.go b/pkg/vf/virtio.go index 9b6f19bd..8e2d5cb8 100644 --- a/pkg/vf/virtio.go +++ b/pkg/vf/virtio.go @@ -20,6 +20,8 @@ type VirtioNet config.VirtioNet type VirtioRng config.VirtioRng type VirtioSerial config.VirtioSerial type VirtioVsock config.VirtioVsock +type VirtioInput config.VirtioInput +type VirtioGPU config.VirtioGPU func (dev *VirtioBlk) toVz() (vz.StorageDeviceConfiguration, error) { var storageConfig StorageConfig = StorageConfig(dev.StorageConfig) @@ -53,6 +55,78 @@ func (dev *VirtioBlk) AddToVirtualMachineConfig(vmConfig *vzVirtualMachineConfig return nil } +func (dev *VirtioInput) toVz() (interface{}, error) { + var inputConfig interface{} + if dev.InputType == config.VirtioInputPointingDevice { + inputConfig, err := vz.NewUSBScreenCoordinatePointingDeviceConfiguration() + if err != nil { + return nil, fmt.Errorf("failed to create pointing device configuration: %w", err) + } + + return inputConfig, nil + } + + inputConfig, err := vz.NewUSBKeyboardConfiguration() + if err != nil { + return nil, fmt.Errorf("failed to create keyboard device configuration: %w", err) + } + + return inputConfig, nil +} + +func (dev *VirtioInput) AddToVirtualMachineConfig(vmConfig *vzVirtualMachineConfiguration) error { + inputDeviceConfig, err := dev.toVz() + if err != nil { + return err + } + + log.Infof("Adding virtio-input device") + + switch conf := inputDeviceConfig.(type) { + case *vz.USBScreenCoordinatePointingDeviceConfiguration: + vmConfig.SetPointingDevicesVirtualMachineConfiguration([]vz.PointingDeviceConfiguration{ + conf, + }) + case *vz.USBKeyboardConfiguration: + vmConfig.SetKeyboardsVirtualMachineConfiguration([]vz.KeyboardConfiguration{ + conf, + }) + } + + return nil +} + +func (dev *VirtioGPU) toVZ() (vz.GraphicsDeviceConfiguration, error) { + gpuDeviceConfig, err := vz.NewVirtioGraphicsDeviceConfiguration() + if err != nil { + return nil, fmt.Errorf("failed to initialize virtio graphic device: %w", err) + } + graphicsScanoutConfig, err := vz.NewVirtioGraphicsScanoutConfiguration(int64(dev.Height), int64(dev.Width)) + if err != nil { + return nil, fmt.Errorf("failed to create graphics scanout: %w", err) + } + gpuDeviceConfig.SetScanouts( + graphicsScanoutConfig, + ) + + return gpuDeviceConfig, nil +} + +func (dev *VirtioGPU) AddToVirtualMachineConfig(vmConfig *vzVirtualMachineConfiguration) error { + gpuDeviceConfig, err := dev.toVZ() + if err != nil { + return err + } + + log.Infof("Adding virtio-gpu device") + + vmConfig.SetGraphicsDevicesVirtualMachineConfiguration([]vz.GraphicsDeviceConfiguration{ + gpuDeviceConfig, + }) + + return nil +} + func (dev *VirtioFs) toVz() (vz.DirectorySharingDeviceConfiguration, error) { if dev.SharedDir == "" { return nil, fmt.Errorf("missing mandatory 'sharedDir' option for virtio-fs device") @@ -249,6 +323,10 @@ func AddToVirtualMachineConfig(dev config.VirtioDevice, vmConfig *vzVirtualMachi return (*VirtioSerial)(d).AddToVirtualMachineConfig(vmConfig) case *config.VirtioVsock: return (*VirtioVsock)(d).AddToVirtualMachineConfig(vmConfig) + case *config.VirtioInput: + return (*VirtioInput)(d).AddToVirtualMachineConfig(vmConfig) + case *config.VirtioGPU: + return (*VirtioGPU)(d).AddToVirtualMachineConfig(vmConfig) default: return fmt.Errorf("Unexpected virtio device type: %T", d) }