diff --git a/docs/README_server.md b/docs/README_server.md index 807b8100c9711e620c7b1d7f540b584b26e3eb01..b36219f3162716da2cfe1442ed2537359064bb00 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 eb756406f8ff6f98f40052902999e254a412c6c6..1ae9f45b43ce8ee5eef21517a9b13e6720417fda 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 fffa9d9b7a5a8bbc2734289ba79b0904011292fe..a055bc20b30a5e96ba6aedc5819c440ceb4dda22 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 03a957ec93512f72d7652cd8f13188d04c2a6762..aefcf26ab774d46667125793a65910b41e1ebd48 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 eb756406f8ff6f98f40052902999e254a412c6c6..1ae9f45b43ce8ee5eef21517a9b13e6720417fda 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 fffa9d9b7a5a8bbc2734289ba79b0904011292fe..a055bc20b30a5e96ba6aedc5819c440ceb4dda22 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 767f1d3875245e9473b4d8f196486754a2277d1c..804daca07e8fc1bbd940789f4839ac10a294bbdb 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 5402ec0cc4d2ddc89acd5172d64216ab35f523c3..348ae345d1e0716898edc51be381f0c84b1dba7a 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 4bd70fba2f343bd0452e54a61ea387e5c639be63..6467f70a8370b479e03bc6b11cb486cdd5545835 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 203ab32e6dca0cf53cb1ff1639e29f0952c6e42a..327b2fa5659cdce1c104c226b32973c837d2782b 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 43dcaa9cf488e86aa7eff198453b44afd2957824..6d2edadd6acc7d3a428b1e8d1fc0a0b103ceb809 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.