Skip to content

Commit

Permalink
Add Restful service
Browse files Browse the repository at this point in the history
add restful service to VFKit which defaults to `tcp://localhost:8081`.  The URL can be changed using the newly introduced --restful-uri
option.  The schemes may be `tcp` or `unix` (unix domain socket).  The service for the unix domain implementation is incomplete and
should be considered a TODO still.  users can also opt out of the service with a `--restful-uri none`.

the restful service now replies to three endpoints:

/vm/state GET which returns the state of the vm
/vm/state POST which allows for a state change
/vm/inspect GET which returns basic information about the virtual machine

error handling for the endpoints needs to be completed still.  these endpoints should return http response codes (like 200) based on
the error handling.  this can be completed in a subsequent PR.

/version GET should also probably be added. I was unsure if we should return the version of vfkit (my first inclination) or some
sort of API version.  maybe we should return both?

--log-level option added which can be set to 'debug' or 'error' or other well known log levels

added tests for new content where applicable

Signed-off-by: Brent Baude <brentbaude@Brents-Mini-2.localdomain>
  • Loading branch information
Brent Baude authored and Brent Baude committed Apr 23, 2023
1 parent 223460b commit d87afd0
Show file tree
Hide file tree
Showing 15 changed files with 682 additions and 13 deletions.
35 changes: 32 additions & 3 deletions cmd/vfkit/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build darwin
// +build darwin

/*
Expand Down Expand Up @@ -28,6 +29,7 @@ import (
"github.com/Code-Hex/vz/v3"
"github.com/crc-org/vfkit/pkg/cmdline"
"github.com/crc-org/vfkit/pkg/config"
"github.com/crc-org/vfkit/pkg/rest"
"github.com/crc-org/vfkit/pkg/vf"
"github.com/docker/go-units"
log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -107,18 +109,45 @@ func waitForVMState(vm *vz.VirtualMachine, state vz.VirtualMachineState) error {
}
}

func runVirtualMachine(vmConfig *config.VirtualMachine) error {
vzVMConfig, err := vf.ToVzVirtualMachineConfig(vmConfig)
func runVFKit(vmConfig *config.VirtualMachine, opts *cmdline.Options) error {
uri, err := rest.ParseRestfulURI(opts.RestfulURI)
if err != nil {
return err
}

scheme, err := rest.ToRestScheme(uri.Scheme)
if err != nil {
return err
}

vzVMConfig, err := vf.ToVzVirtualMachineConfig(vmConfig)
if err != nil {
return err
}
vm, err := vz.NewVirtualMachine(vzVMConfig)
if err != nil {
return err
}
// Tuck a vm instance in the vmConfig for ability to call
// methods like state, etc.
vmConfig.VzVM = vm

// Do not enable the rests server if user sets scheme to None
if scheme != rest.NONE {
go func() {
// start the restful service
srv := rest.NewServer(vmConfig, scheme, uri)
if err := srv.Start(); err != nil {
log.Error(err)
}
}()
}
return runVirtualMachine(vmConfig)
}

err = vm.Start()
func runVirtualMachine(vmConfig *config.VirtualMachine) error {
vm := vmConfig.VzVM
err := vm.Start()
if err != nil {
return err
}
Expand Down
24 changes: 23 additions & 1 deletion cmd/vfkit/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"

"github.com/crc-org/vfkit/pkg/cmdline"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

Expand All @@ -18,11 +19,21 @@ var rootCmd = &cobra.Command{
Long: `A hypervisor written in Go using Apple's virtualization framework to run linux virtual machines.
Complete documentation is available at https://github.com/crc-org/vfkit`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := cmdline.ValidateInput(opts); err != nil {
return err
}
if len(opts.LogLevel) > 0 {
ll, err := getLogLevel()
if err != nil {
return err
}
logrus.SetLevel(ll)
}
vmConfig, err := newVMConfiguration(opts)
if err != nil {
return err
}
return runVirtualMachine(vmConfig)
return runVFKit(vmConfig, opts)
},
Version: vfkitVersion,
}
Expand All @@ -43,6 +54,17 @@ func Execute() {
}
}

func getLogLevel() (logrus.Level, error) {
switch opts.LogLevel {
case "error":
return logrus.ErrorLevel, nil
case "debug":
return logrus.DebugLevel, nil
case "info":
return logrus.InfoLevel, nil
}
return 0, fmt.Errorf("unknown log level: %s", opts.LogLevel)
}
func main() {
Execute()
}
40 changes: 40 additions & 0 deletions doc/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ Specifying VM bootloader configuration is mandatory.
Device configuration is optional, but most VM will need a disk image and a network interface to be configured.

## Generic Options

- `--restful-URI`

The URI (address) of the restful service. The default is `tcp://localhost:8081`. Valid schemes are
`tcp`, `none`, or `unix`. In the case of unix, the "host" portion would be a path to where the unix domain
socket will be stored. A scheme of `none` disables the restful service.

- `--log-level`

Set the log-level for VFKit. Values are the typical golang log levels such as `debug`, `info`, `error`, `warn`,
and `trace` among others.

### Virtual Machine Resources

These options specify the amount of RAM and the number of CPUs which will be available to the virtual machine.
Expand Down Expand Up @@ -203,3 +215,31 @@ The share can be mounted in the guest with `mount -t virtio-fs vfkitTag /mnt`, w
#### Example
`--device virtio-fs,sharedDir=/Users/virtuser/vfkit/,mountTag=vfkit-share`


## Restful Service

### Get VM state

Used to obtain the state of the virtual machine that is being run by VFKit.

GET `/vm/state`
Response: {"state": "string"}

### Change VM State

Change the state of the virtual machine. Valid states are:
* Hardstop
* Pause
* Resume
* Stop

POST `/vm/state` {"new_state": "new value"}

Response: http 200

### Inspect VM

Get description of the virtual machine

GET `/vm/inspect`
Response: { "cpus": uint, "memory": uint64, "devices": []config.VirtIODevice }
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@ go 1.17
require (
github.com/Code-Hex/vz/v3 v3.0.4
github.com/docker/go-units v0.4.0
github.com/gorilla/mux v1.8.0
github.com/h2non/filetype v1.1.3
github.com/prashantgupta24/mac-sleep-notifier v1.0.1
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.1
golang.org/x/sys v0.3.0
inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/sys v0.3.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 6 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
Expand All @@ -23,8 +25,8 @@ github.com/prashantgupta24/mac-sleep-notifier v1.0.1/go.mod h1:bcfTio1xW+rjjZzdF
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
33 changes: 31 additions & 2 deletions pkg/cmdline/cmdline.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package cmdline

import "github.com/spf13/cobra"
import (
"github.com/crc-org/vfkit/pkg/rest"
"github.com/spf13/cobra"
)

type Options struct {
Vcpus uint
Expand All @@ -15,8 +18,14 @@ type Options struct {
TimeSync string

Devices []string

RestfulURI string

LogLevel string
}

const DefaultRestfulURI = "tcp://localhost:8081"

func AddFlags(cmd *cobra.Command, opts *Options) {
cmd.Flags().StringVarP(&opts.VmlinuzPath, "kernel", "k", "", "path to the virtual machine linux kernel")
cmd.Flags().StringVarP(&opts.KernelCmdline, "kernel-cmdline", "C", "", "linux kernel command line")
Expand All @@ -34,6 +43,26 @@ func AddFlags(cmd *cobra.Command, opts *Options) {
cmd.Flags().UintVarP(&opts.MemoryMiB, "memory", "m", 512, "virtual machine RAM size in mibibytes")

cmd.Flags().StringVarP(&opts.TimeSync, "timesync", "t", "", "sync guest time when host wakes up from sleep")

cmd.Flags().StringArrayVarP(&opts.Devices, "device", "d", []string{}, "devices")

cmd.Flags().StringVar(&opts.RestfulURI, "restful-uri", DefaultRestfulURI, "URI address for RestFul services")
cmd.Flags().StringVar(&opts.LogLevel, "log-level", "", "set log level")

}

func ValidateInput(opts *Options) error {
if err := validateRestfulURI(opts.RestfulURI); err != nil {
return err
}
// more parsing of opts can be done here
return nil
}

func validateRestfulURI(inputURI string) error {
if inputURI != DefaultRestfulURI {
if _, err := rest.ParseRestfulURI(inputURI); err != nil {
return err
}
}
return nil
}
64 changes: 64 additions & 0 deletions pkg/cmdline/cmdline_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package cmdline

import "testing"

func Test_validateRestfulURI(t *testing.T) {
type args struct {
inputURI string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "valid tcp",
args: args{
inputURI: "tcp://localhost:8080",
},
wantErr: false,
},
{
name: "invalid tcp - no host",
args: args{
inputURI: "tcp://",
},
wantErr: true,
},
{
name: "invalid scheme",
args: args{
inputURI: "http://localhost",
},
wantErr: true,
},
{
name: "valid uds",
args: args{
inputURI: "unix:///my/socket/goes/here/vfkit.sock",
},
wantErr: false,
},
{
name: "invalid uds - no host",
args: args{
inputURI: "unix://",
},
wantErr: true,
},
{
name: "none",
args: args{
inputURI: "none://",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateRestfulURI(tt.args.inputURI); (err != nil) != tt.wantErr {
t.Errorf("validateRestfulURI() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
19 changes: 19 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"fmt"
"github.com/Code-Hex/vz/v3"
"os"
"os/exec"
"strconv"
Expand All @@ -16,6 +17,7 @@ type VirtualMachine struct {
Bootloader Bootloader
Devices []VirtioDevice
Timesync *TimeSync
VzVM *vz.VirtualMachine
}

// TimeSync enables synchronization of the host time to the linux guest after the host was suspended.
Expand All @@ -31,6 +33,9 @@ type VMComponent interface {
ToCmdLine() ([]string, error)
}

// VFVMState is so we can extend vz.VirtualMachineState with handy methods
type VFVMState vz.VirtualMachineState

// NewVirtualMachine creates a new VirtualMachine instance. The virtual machine
// will use vcpus virtual CPUs and it will be allocated memoryBytes bytes of
// RAM. bootloader specifies which kernel/initrd/kernel args it will be using.
Expand Down Expand Up @@ -201,3 +206,17 @@ func timesyncFromCmdLine(optsStr string) (*TimeSync, error) {

return &timesync, nil
}

// String is a simple wrapper to get a string representation of the VMState
func (s VFVMState) String() string {
switch vz.VirtualMachineState(s) {
case vz.VirtualMachineStatePaused:
return "Paused"
case vz.VirtualMachineStateRunning:
return "Running"
case vz.VirtualMachineStateStopped:
return "Stopped"
}
// I debated on what to do here but for now, unknown?
return "Unknown"
}
Loading

0 comments on commit d87afd0

Please sign in to comment.