From e2ae4057de6e10d1f29f3bf8505dd40331b8535c Mon Sep 17 00:00:00 2001 From: Florent Gluck <florent.gluck@hesge.ch> Date: Wed, 17 May 2023 14:01:24 +0200 Subject: [PATCH] server side: implemented a new route: /vms/{id}/startwithcreds which start a VM with credentials (port, pwd) updated README_server.md accordingly client side: updated run_dev and run_prod so that arguments with " are handled properly updated validate script so that it correctly builds nexus-cli regardless of the current environment (dev or prod) --- docs/README_server.md | 2 + src/client/nexus-cli/run_dev | 4 +- src/client/nexus-cli/run_prod | 4 +- src/client/nexus-cli/validate | 2 +- src/client/nexush/run_dev | 4 +- src/client/nexush/run_prod | 4 +- src/server/consts/consts.go | 11 ++++-- src/server/router/router.go | 1 + src/server/router/routerVMs.go | 26 +++++++++++++ src/server/vms/vm.go | 21 +++-------- src/server/vms/vms.go | 67 ++++++++++++++++++++++------------ 11 files changed, 95 insertions(+), 51 deletions(-) diff --git a/docs/README_server.md b/docs/README_server.md index 807b810..b36219f 100644 --- a/docs/README_server.md +++ b/docs/README_server.md @@ -366,6 +366,8 @@ These capabilities are called "VM access capabilities": | `/vms/{id}` | delete a VM | DELETE | | `VM_DESTROY_ANY` | OR | `VM_DESTROY` | | `/vms/{id}` | edit a VM | PUT | name,cpus,ram,nic,usb | `VM_EDIT_ANY` | OR | `VM_EDIT` | | `/vms/{id}/start` | start a VM | PUT | | `VM_START_ANY` | OR | `VM_START` | +| `/vms/{id}/start` | start a VM | PUT | | `VM_START_ANY` | OR | `VM_START` | +| `/vms/{id}/startwithcreds` | start a VM with credentials | PUT | port,pwd | `VM_START_ANY` | OR | `VM_START` | | `/vms/{id}/stop` | kill a VM | PUT | | `VM_STOP_ANY` | OR | `VM_STOP` | | `/vms/{id}/reboot` | reboot a VM | PUT | | `VM_REBOOT_ANY` | OR | `VM_REBOOT` | | `/vms/{id}/shutdown` | gracefully shutdown a VM | PUT | | `VM_STOP_ANY` | OR | `VM_STOP` | diff --git a/src/client/nexus-cli/run_dev b/src/client/nexus-cli/run_dev index eb75640..1ae9f45 100755 --- a/src/client/nexus-cli/run_dev +++ b/src/client/nexus-cli/run_dev @@ -1,4 +1,4 @@ #!/bin/bash -make prepare_dev -go run . $@ +make prepare_dev || exit 1 +go run . "$@" diff --git a/src/client/nexus-cli/run_prod b/src/client/nexus-cli/run_prod index fffa9d9..a055bc2 100755 --- a/src/client/nexus-cli/run_prod +++ b/src/client/nexus-cli/run_prod @@ -1,4 +1,4 @@ #!/bin/bash -make prepare_prod -go run . $@ \ No newline at end of file +make prepare_prod || exit 1 +go run . "$@" \ No newline at end of file diff --git a/src/client/nexus-cli/validate b/src/client/nexus-cli/validate index 03a957e..aefcf26 100755 --- a/src/client/nexus-cli/validate +++ b/src/client/nexus-cli/validate @@ -1,7 +1,7 @@ #!/bin/bash +make prepare_dev go build . - nexus_cli="./nexus-cli" partial_name="exam 328a2d0eff08" diff --git a/src/client/nexush/run_dev b/src/client/nexush/run_dev index eb75640..1ae9f45 100755 --- a/src/client/nexush/run_dev +++ b/src/client/nexush/run_dev @@ -1,4 +1,4 @@ #!/bin/bash -make prepare_dev -go run . $@ +make prepare_dev || exit 1 +go run . "$@" diff --git a/src/client/nexush/run_prod b/src/client/nexush/run_prod index fffa9d9..a055bc2 100755 --- a/src/client/nexush/run_prod +++ b/src/client/nexush/run_prod @@ -1,4 +1,4 @@ #!/bin/bash -make prepare_prod -go run . $@ \ No newline at end of file +make prepare_prod || exit 1 +go run . "$@" \ No newline at end of file diff --git a/src/server/consts/consts.go b/src/server/consts/consts.go index 767f1d3..804daca 100644 --- a/src/server/consts/consts.go +++ b/src/server/consts/consts.go @@ -10,12 +10,17 @@ const ( APIPortMax = 1099 VMSpiceMinPort = 1100 - VMSpiceMaxPort = 65000 + VMSpiceMaxPort = 65535 + + VMPwdLength = 8 + VMPwdDigitCount = 4 + VMPwdSymbolCount = 0 + VMPwdRepeatChars = false MaxUploadSize = "30G" - // We estimate that KVM allows for this amount of RAM saving in % - // (due to page sharing across VMs). + // We estimate that KVM allows for this amount of RAM saving in % (due to page sharing across VMs). + // 30% seems to be a pretty conservative estimate. KsmRamSaving = 0.3 // To prevent RAM saturation, we refuse running new VMs if more than diff --git a/src/server/router/router.go b/src/server/router/router.go index 5402ec0..348ae34 100644 --- a/src/server/router/router.go +++ b/src/server/router/router.go @@ -86,6 +86,7 @@ func (router *Router)Start(port int) { vmsGroup.DELETE("/:id", router.vms.DeleteVMByID) vmsGroup.PUT("/:id", router.vms.EditVMByID) vmsGroup.PUT("/:id/start", router.vms.StartVM) + vmsGroup.PUT("/:id/startwithcreds", router.vms.StartVMWithCreds) vmsGroup.PUT("/:id/stop", router.vms.KillVM) vmsGroup.PUT("/:id/shutdown", router.vms.ShutdownVM) vmsGroup.PUT("/:id/reboot", router.vms.RebootVM) diff --git a/src/server/router/routerVMs.go b/src/server/router/routerVMs.go index 4bd70fb..6467f70 100644 --- a/src/server/router/routerVMs.go +++ b/src/server/router/routerVMs.go @@ -234,6 +234,32 @@ func (r *RouterVMs)StartVM(c echo.Context) error { }) } +// Starts a VM based on its ID using the specified credentials (port and password). +// Requires to be the VM's owner, or either capability: +// User cap: CAP_VM_START_ANY: any VM can be started. +// VM access cap: CAP_VM_START: any of the VMs with this cap for the logged user can be started. +// curl --cacert ca.pem -X PUT https://localhost:1077/vms/e41f3556-ca24-4658-bd79-8c85bd6bff59/startwithcreds -H "Authorization: Bearer <AccessToken>" +func (r *RouterVMs)StartVMWithCreds(c echo.Context) error { + return r.performVMAction(c, caps.CAP_VM_START_ANY, caps.CAP_VM_START, func(c echo.Context, vm *vms.VM) error { + + // Deserializes and validates client's parameters. + type Parameters struct { + port int `json:"port" validate:"required,gte=1100,lte=65535"` + pwd string `json:"pwd" validate:"required,min=8,max=64"` + } + p := new(Parameters) + if err := decodeJson(c, &p); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + _, _, err := r.vms.StartVM(vm.GetID()) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") + }) +} + // Kills a VM based on its ID. // Requires to be the VM's owner, or either capability: // User cap: CAP_VM_STOP_ANY: any VM can be killed. diff --git a/src/server/vms/vm.go b/src/server/vms/vm.go index 203ab32..327b2fa 100644 --- a/src/server/vms/vm.go +++ b/src/server/vms/vm.go @@ -306,35 +306,26 @@ func (vm *VM)delete() error { return nil } -// Starts a VM and returns the access password. -// Password is randomly generated. -func (vm *VM)start(port int, endofExecFn endOfExecCallback) (string, error) { +// Starts a VM on the given port with the given password. +func (vm *VM)start(port int, pwd string, endofExecFn endOfExecCallback) error { if vm.DiskBusy { - return "", errors.New("Failed starting VM: disk is busy") - } - - // Generates a 8 characters long password with 4 digits, 0 symbols, - // allowing upper and lower case letters, disallowing repeat characters. - pwd, err := passwordGen.Generate(8, 4, 0, false, false) - if err != nil { - log.Error("Failed starting VM: password generation error: "+err.Error()) - return "", errors.New("Failed starting VM: password generation error: "+err.Error()) + return errors.New("Failed starting VM: disk is busy") } // Writes the password in a "secret" file. pwdFile, err := vm.writeSecretFile(pwd) if err != nil { log.Error("Failed starting VM: error creating secret file: "+err.Error()) - return "", errors.New("Failed starting VM: error creating secret file: "+err.Error()) + return errors.New("Failed starting VM: error creating secret file: "+err.Error()) } if err = vm.runQEMU(port, pwd, pwdFile, endofExecFn); err != nil { vm.removeSecretFile() os.Remove(vm.qgaSock) // If QEMU fails it's likely the Guest Agent file it created is still there. - return "", errors.New("Failed starting VM: error running QEMU: "+err.Error()) + return errors.New("Failed starting VM: error running QEMU: "+err.Error()) } - return pwd, nil + return nil } // Writes the specified password in Base64 in a "secret" file inside the VM directory. diff --git a/src/server/vms/vms.go b/src/server/vms/vms.go index 43dcaa9..6d2edad 100644 --- a/src/server/vms/vms.go +++ b/src/server/vms/vms.go @@ -13,7 +13,7 @@ import ( "nexus-server/paths" "nexus-server/utils" "nexus-server/logger" - "nexus-server/consts" + c "nexus-server/consts" "github.com/google/uuid" ) @@ -172,45 +172,66 @@ func (vms *VMs)AddVM(vm *VM) error { return nil } -// Starts a VM by its ID. -// Returns the port on which the VM is running and the access password. -func (vms *VMs)StartVM(vmID uuid.UUID) (int, string, error) { +// Starts a VM by its ID, using the specified port and password. +func (vms *VMs)StartVMWithCreds(vmID uuid.UUID, port int, pwd string) error { vms.rwlock.Lock() defer vms.rwlock.Unlock() vm, err := vms.getVMUnsafe(vmID) if err != nil { - return 0, "", err + return err } vm.mutex.Lock() defer vm.mutex.Unlock() if vm.IsRunning() { - return 0, "", errors.New("Failed starting VM: VM already running") + return errors.New("Failed starting VM: VM already running") } totalRAM, availRAM, err := utils.GetRAM() if err != nil { - return -1, "", errors.New("Failed starting VM: failed obtaining memory info: "+err.Error()) + return errors.New("Failed starting VM: failed obtaining memory info: "+err.Error()) } // We estimate that KVM allows for ~30% RAM saving (due to page sharing across VMs). - estimatedVmRAM := int(math.Round(float64(vm.v.Ram)*(1.-consts.KsmRamSaving))) + estimatedVmRAM := int(math.Round(float64(vm.v.Ram)*(1.-c.KsmRamSaving))) vms.usedRAM += estimatedVmRAM - // Checks that at least 15% of the available RAM is left after the VM has started, + // Checks that enough available RAM would be left after the VM has started, // otherwise, refuses to run it in order to avoid RAM saturation. - if availRAM - vms.usedRAM <= int(math.Round(float64(totalRAM)*(1.-consts.RamUsageLimit))) { + if availRAM - vms.usedRAM <= int(math.Round(float64(totalRAM)*(1.-c.RamUsageLimit))) { + vms.usedRAM -= estimatedVmRAM + return errors.New("Failed starting VM: insufficient free RAM") + } + + // This callback is called once the VM started with vm.start terminates. + endofExecFn := func (vm *VM) { + vm.mutex.Lock() + vm.removeSecretFile() + vm.resetStates() + vm.mutex.Unlock() + vms.rwlock.Lock() + vms.usedPorts[vm.Run.Port] = false vms.usedRAM -= estimatedVmRAM - return -1, "", errors.New("Failed starting VM: insufficient free RAM") + vms.rwlock.Unlock() } + if err = vm.start(port, pwd, endofExecFn); err != nil { + return err + } + return nil +} + +// Starts a VM by its ID using randomly generated port number and password. +// Returns the port on which the VM is running and the access password. +func (vms *VMs)StartVM(vmID uuid.UUID) (int, string, error) { + // Locates a free port randomly chosen between VMSpiceMinPort and VMSpiceMaxPort (inclusive). var port int for { - port = utils.Rand(consts.VMSpiceMinPort, consts.VMSpiceMaxPort) + port = utils.Rand(c.VMSpiceMinPort, c.VMSpiceMaxPort) if !vms.usedPorts[port] { if utils.IsPortAvailable(port) { vms.usedPorts[port] = true @@ -219,20 +240,18 @@ func (vms *VMs)StartVM(vmID uuid.UUID) (int, string, error) { } } - // This callback is called once the VM started with vm.start terminates. - endofExecFn := func (vm *VM) { - vm.mutex.Lock() - vm.removeSecretFile() - vm.resetStates() - vm.mutex.Unlock() - vms.rwlock.Lock() - vms.usedPorts[vm.Run.Port] = false - vms.usedRAM -= estimatedVmRAM - vms.rwlock.Unlock() + // Randomly generates a 8 characters long password with 4 digits, 0 symbols, + // allowing upper and lower case letters, disallowing repeat characters. + pwd, err := passwordGen.Generate(c.VMPwdLength, c.VMPwdDigitCount, c.VMPwdSymbolCount, false, c.VMPwdRepeatChars) + if err != nil { + log.Error("Failed starting VM: password generation error: "+err.Error()) + return -1, "", errors.New("Failed starting VM: password generation error: "+err.Error()) } - pwd, err := vm.start(port, endofExecFn) - return port, pwd, err + if err = vms.StartVMWithCreds(vmID, port, pwd); err != nil { + return -1, "", err + } + return port, pwd, nil } // Kills a VM by its ID. -- GitLab