diff --git a/README.md b/README.md index c31c3168580ca8250225f5373e319fbe09b73794..4c70dcf3710f191b4d3f4fa8a862908ae3776f19 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,7 @@ Here is the content of `vm.json`: "VM_LIST":1, "VM_START":1, "VM_STOP":1, + "VM_REBOOT":1, "VM_DESTROY":1, "VM_EDIT":1, "VM_SET_ACCESS":1, @@ -207,7 +208,8 @@ Here is the content of `vm.json`: "lukeskywalker@force.net" : { "VM_LIST":1, "VM_START":1, - "VM_STOP":1 + "VM_STOP":1, + "VM_REBOOT":1 }, "student@nexus.org" : { "VM_LIST":1 @@ -280,7 +282,8 @@ The table below lists all potential capabilities associated to a user: | VM_DESTROY_ANY | Can destoy **ANY** VM | | VM_EDIT_ANY | Can edit **ANY** VM | | VM_START_ANY | Can start **ANY** VM | -| VM_STOP_ANY | Can stop **ANY** VM | +| VM_STOP_ANY | Can kill/shutdown **ANY** VM | +| VM_REBOOT_ANY | Can Reboot **ANY** VM | | VM_LIST_ANY | Can list **ANY** VM | | VM_READFS_ANY | Can export files from **ANY** VM | | VM_WRITEFS_ANY | Can import files into **ANY** VM | @@ -307,7 +310,8 @@ These capabilities are called "VM access capabilities": | VM_DESTROY | User can destroy the VM | | VM_EDIT | User can edit the VM | | VM_START | User can start the VM | -| VM_STOP | User can stop/shutdown the VM | +| VM_STOP | User can kill/shutdown the VM | +| VM_REBOOT | User can reboot the VM | | VM_LIST | User can list or attach to the VM | | VM_READFS | User can export files from the VM | | VM_WRITEFS | User can import files into the VM | @@ -361,7 +365,8 @@ Legend for the "Done" column: | x | `/vms` | returns VMs that can be listed | GET | | `VM_LIST_ANY` | OR | `VM_LIST` | | x | `/vms/start` | returns VMs that can be started | GET | | `VM_START_ANY` | OR | `VM_START` | | x | `/vms/attach` | returns VMs that can be attached to | GET | | `VM_LIST_ANY` | OR | `VM_LIST` | -| x | `/vms/stop` | returns VMs that can be stopped | GET | | `VM_STOP_ANY` | OR | `VM_STOP` | +| x | `/vms/stop` | returns VMs that can be killed/shutdown | GET | | `VM_STOP_ANY` | OR | `VM_STOP` | +| x | `/vms/reboot` | returns VMs that can be rebooted | GET | | `VM_REBOOT_ANY` | OR | `VM_REBOOT` | | x | `/vms/edit` | returns VMs that can be edited | GET | | `VM_EDIT_ANY` | OR | `VM_EDIT` | | x | `/vms/editaccess` | returns VMs that can have their access changed | GET | | `VM_SET_ACCESS` | AND | `VM_SET_ACCESS` | | x | `/vms/del` | returns VMs that can be deleted | GET | | `VM_DESTROY_ANY` | OR | `VM_DESTROY` | @@ -374,7 +379,8 @@ Legend for the "Done" column: | x | `/vms/{id}` | delete a VM | DELETE | | `VM_DESTROY_ANY` | OR | `VM_DESTROY` | | x | `/vms/{id}` | edit a VM | PUT | name,cpus,ram,nic | `VM_EDIT_ANY` | OR | `VM_EDIT` | | x | `/vms/{id}/start` | start a VM | PUT | | `VM_START_ANY` | OR | `VM_START` | -| x | `/vms/{id}/stop` | stop a VM (by force) | PUT | | `VM_STOP_ANY` | OR | `VM_STOP` | +| x | `/vms/{id}/stop` | kill a VM | PUT | | `VM_STOP_ANY` | OR | `VM_STOP` | +| x | `/vms/{id}/reboot` | reboot a VM | PUT | | `VM_REBOOT_ANY` | OR | `VM_REBOOT` | | x | `/vms/{id}/shutdown` | gracefully shutdown a VM | PUT | | `VM_STOP_ANY` | OR | `VM_STOP` | | x | `/vms/{id}/access/{email}` | set VM access for a user | PUT | caps | `VM_SET_ACCESS` | AND | `VM_SET_ACCESS` | | x | `/vms/{id}/access/{email}` | del VM access for a user | DELETE | | `VM_SET_ACCESS` | AND | `VM_SET_ACCESS` | diff --git a/conf/users.json b/conf/users.json index a10cbd12008b8ef3a4bbadedae50787322345aeb..a407f1f5d1509234d407b079c1591f77d50db215 100644 --- a/conf/users.json +++ b/conf/users.json @@ -14,6 +14,7 @@ "VM_EDIT_ANY":1, "VM_START_ANY":1, "VM_STOP_ANY":1, + "VM_REBOOT_ANY":1, "VM_LIST_ANY":1, "VM_READFS_ANY":1, "VM_WRITEFS_ANY":1, diff --git a/src/caps/caps.go b/src/caps/caps.go index ecd7e3579c30fbe2f25fb7d92cba11a824cdff8b..2a5c3380a60865d0d5e6063da1e4245d4450644d 100644 --- a/src/caps/caps.go +++ b/src/caps/caps.go @@ -15,6 +15,8 @@ const ( CAP_VM_START_ANY = "VM_START_ANY" CAP_VM_STOP = "VM_STOP" CAP_VM_STOP_ANY = "VM_STOP_ANY" + CAP_VM_REBOOT = "VM_REBOOT" + CAP_VM_REBOOT_ANY = "VM_REBOOT_ANY" CAP_VM_CREATE = "VM_CREATE" CAP_VM_DESTROY = "VM_DESTROY" CAP_VM_DESTROY_ANY = "VM_DESTROY_ANY" @@ -47,6 +49,7 @@ var userCaps = Capabilities { CAP_VM_EDIT_ANY: 1, CAP_VM_START_ANY: 1, CAP_VM_STOP_ANY: 1, + CAP_VM_REBOOT_ANY: 1, CAP_VM_LIST_ANY: 1, CAP_VM_SET_ACCESS: 1, CAP_VM_READFS_ANY: 1, @@ -68,6 +71,7 @@ var VMAccessCaps = Capabilities { CAP_VM_EDIT: 1, CAP_VM_START: 1, CAP_VM_STOP: 1, + CAP_VM_REBOOT: 1, CAP_VM_LIST: 1, CAP_VM_READFS: 1, CAP_VM_WRITEFS: 1, diff --git a/src/qga/qga.go b/src/qga/qga.go index 0322d60ca79ab8510000abb87e26fbafbc63ac99..3cba9c4af0d4719d47fe1bc7534f8f0a281f6ea4 100644 --- a/src/qga/qga.go +++ b/src/qga/qga.go @@ -18,6 +18,10 @@ func New() *QGACon { return &QGACon{} } +// TODO +// Rewrite most of this code to use a json serializer +// instead of this ugly-dirty "json as string" code! + // sockAddr is the path to a UNIX domain socket. func (q *QGACon)Open(sockAddr string) error { con, err := net.Dial("unix", sockAddr) @@ -43,6 +47,12 @@ func (q *QGACon)SendReboot() error { return q.SendCmd("reboot") } +// Send a forced reboot command to the guest OS. +func (q *QGACon)SendForcedReboot() error { + _, err := q.sock.Write([]byte(`{"execute":"guest-exec", "arguments":{"path":"reboot", "arg": ["--force"]}}`)) + return err +} + // Send a command to the guest OS. func (q *QGACon)SendCmd(cmd string) error { _, err := q.sock.Write([]byte(`{"execute":"guest-exec", "arguments":{"path":"`+cmd+`"}}`)) diff --git a/src/router/router.go b/src/router/router.go index d86337ffcecdd8bf5826e2f6e610873edf4efee5..89454ff2f1617eac0d68d8b104bd2444070cce88 100644 --- a/src/router/router.go +++ b/src/router/router.go @@ -76,6 +76,7 @@ func (router *Router)Start(port int) { vmsGroup.GET("/del", router.vms.GetDeletableVMs) vmsGroup.GET("/start", router.vms.GetStartableVMs) vmsGroup.GET("/stop", router.vms.GetStoppableVMs) + vmsGroup.GET("/reboot", router.vms.GetRebootableVMs) vmsGroup.GET("/edit", router.vms.GetEditableVMs) vmsGroup.GET("/editaccess", router.vms.GetEditableVMAccessVMs) vmsGroup.GET("/exportdir", router.vms.GetDirExportableVMs) @@ -85,8 +86,9 @@ 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/stop", router.vms.StopVM) + vmsGroup.PUT("/:id/stop", router.vms.KillVM) vmsGroup.PUT("/:id/shutdown", router.vms.ShutdownVM) + vmsGroup.PUT("/:id/reboot", router.vms.RebootVM) vmsGroup.PUT("/:id/access/:email", router.vms.SetVMAccessForUser) vmsGroup.DELETE("/:id/access/:email", router.vms.DeleteVMAccessForUser) vmsGroup.GET("/:id/exportdir", router.vms.ExportVMDir) diff --git a/src/router/routerVMs.go b/src/router/routerVMs.go index 5aa34e2690b969260f3e9f5c55d1a961b72523a1..3ff8641bcbc1f80fb5da06611b9a8ebaaaeb6687 100644 --- a/src/router/routerVMs.go +++ b/src/router/routerVMs.go @@ -83,6 +83,17 @@ func (r *RouterVMs)GetStoppableVMs(c echo.Context) error { }) } +// Returns VMs that are running and that can be rebooted. +// Requires either capability: +// User cap: CAP_VM_REBOOT_ANY: returns all VMs. +// VM access cap: CAP_VM_REBOOT: returns all VMs with this cap for the logged user. +// curl --cacert ca.pem -X GET https://localhost:1077/vms/reboot -H "Authorization: Bearer <AccessToken>" +func (r *RouterVMs)GetRebootableVMs(c echo.Context) error { + return r.performVMsList(c, caps.CAP_VM_REBOOT_ANY, caps.CAP_VM_REBOOT, func(vm vms.VM) bool { + return vm.IsRunning() + }) +} + // Returns VMs that can be edited. // Requires either capability: // User cap: CAP_VM_EDIT_ANY: returns all VMs. @@ -216,24 +227,24 @@ func (r *RouterVMs)StartVM(c echo.Context) error { }) } -// Stops (by force) a VM based on its ID. +// Kills a VM based on its ID. // Requires either capability: -// User cap: CAP_VM_STOP_ANY: any VM can be stopped. -// VM access cap: CAP_VM_STOP: any of the VMs with this cap for the logged user can be stopped. +// User cap: CAP_VM_STOP_ANY: any VM can be killed. +// VM access cap: CAP_VM_STOP: any of the VMs with this cap for the logged user can be killed. // curl --cacert ca.pem -X PUT https://localhost:1077/vms/e41f3556-ca24-4658-bd79-8c85bd6bff59/stop -H "Authorization: Bearer <AccessToken>" -func (r *RouterVMs)StopVM(c echo.Context) error { +func (r *RouterVMs)KillVM(c echo.Context) error { return r.performVMAction(c, caps.CAP_VM_STOP_ANY, caps.CAP_VM_STOP, func(c echo.Context, vm *vms.VM) error { - if err := r.vms.StopVM(vm.ID); err != nil { + if err := r.vms.KillVM(vm.ID); err != nil { return echo.NewHTTPError(http.StatusNotFound, err.Error()) } return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") }) } -// Gracefully stops a VM based on its ID. +// Gracefully shutdown a VM based on its ID. // Requires either capability: -// User cap: CAP_VM_STOP_ANY: any VM can be stopped. -// VM access cap: CAP_VM_STOP: any of the VMs with this cap for the logged user can be stopped. +// User cap: CAP_VM_STOP_ANY: any VM can be shutdown. +// VM access cap: CAP_VM_STOP: any of the VMs with this cap for the logged user can be shutdown. // curl --cacert ca.pem -X PUT https://localhost:1077/vms/e41f3556-ca24-4658-bd79-8c85bd6bff59/shutdown -H "Authorization: Bearer <AccessToken>" func (r *RouterVMs)ShutdownVM(c echo.Context) error { return r.performVMAction(c, caps.CAP_VM_STOP_ANY, caps.CAP_VM_STOP, func(c echo.Context, vm *vms.VM) error { @@ -244,6 +255,20 @@ func (r *RouterVMs)ShutdownVM(c echo.Context) error { }) } +// Reboot a VM based on its ID. +// Requires either capability: +// User cap: CAP_VM_REBOOT_ANY: any VM can be rebooted. +// VM access cap: CAP_VM_REBOOT: any of the VMs with this cap for the logged user can be rebooted. +// curl --cacert ca.pem -X PUT https://localhost:1077/vms/e41f3556-ca24-4658-bd79-8c85bd6bff59/stop -H "Authorization: Bearer <AccessToken>" +func (r *RouterVMs)RebootVM(c echo.Context) error { + return r.performVMAction(c, caps.CAP_VM_REBOOT_ANY, caps.CAP_VM_REBOOT, func(c echo.Context, vm *vms.VM) error { + if err := r.vms.RebootVM(vm.ID); err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") + }) +} + // Edit a VM' specs: name, cpus, ram, nic // Requires either capability: // User cap: CAP_VM_EDIT_ANY: any VM can be edited. diff --git a/src/version/version.go b/src/version/version.go index 184ca5149d993c759986ef72d1fa20b679bd5c93..4bad79398d5274c865c75830e9bc2cad3dd066f6 100644 --- a/src/version/version.go +++ b/src/version/version.go @@ -6,8 +6,8 @@ import ( const ( major = 1 - minor = 0 - bugfix = 1 + minor = 1 + bugfix = 0 ) type Version struct { diff --git a/src/vms/vm.go b/src/vms/vm.go index 769bbd329dbff23fb2b09004f9b9f7852868c7eb..88ae4516472b544c7586de913e78b808be351934 100644 --- a/src/vms/vm.go +++ b/src/vms/vm.go @@ -327,8 +327,8 @@ func (vm *VM)removeSecretFile() { os.Remove(pwdFile) } -// Stops by force a running VM. -func (vm *VM)stop() error { +// Kills by force a running VM. +func (vm *VM)kill() error { if !vm.IsRunning() { return errors.New("Failed stopping VM: VM is not running") } @@ -343,34 +343,71 @@ func (vm *VM)stop() error { return nil } -// Gracefully stops a running VM. -// Uses QGA commands to talk to the VM, which means QEMU Guest Agent must be running -// in the VM, otherwise the VM won't shutdown. +// Gracefully shutdowns a running VM. +// Uses QGA commands to talk to the VM, which means QEMU Guest Agent must be +// running in the VM, otherwise it won't work. func (vm *VM)shutdown() error { + prefix := "Shutdown failed: " + if !vm.IsRunning() { - return errors.New("Shutdown failed: VM is not running") + return errors.New(prefix+"VM is not running") } if vm.DiskBusy { - return errors.New("Shutdown failed: VM disk is busy") + return errors.New(prefix+"VM disk is busy") } // Sends a QGA command to order the VM to shutdown. con := qga.New() if err := con.Open(vm.qgaSock); err != nil { - log.Error("VM shutdown failed (open): "+err.Error()) - return errors.New("VM shutdown failed (open): "+err.Error()) + log.Error(prefix+"(open): "+err.Error()) + return errors.New(prefix+"(open): "+err.Error()) } if err := con.SendShutdown(); err != nil { con.Close() - log.Error("VM shutdown failed (send): "+err.Error()) - return errors.New("VM shutdown failed (send): "+err.Error()) + log.Error(prefix+"(send): "+err.Error()) + return errors.New(prefix+"(send): "+err.Error()) + } + + if err := con.Close(); err != nil { + log.Error(prefix+"(close): "+err.Error()) + return errors.New(prefix+"(close): "+err.Error()) + } + + return nil +} + +// Reboots a running VM. +// Uses QGA commands to talk to the VM, which means QEMU Guest Agent must be +// running in the VM, otherwise it won't work. +func (vm *VM)reboot() error { + prefix := "Shutdown failed: " + + if !vm.IsRunning() { + return errors.New(prefix+"VM is not running") + } + + if vm.DiskBusy { + return errors.New(prefix+"VM disk is busy") + } + + // Sends a QGA command to order the VM to shutdown. + con := qga.New() + if err := con.Open(vm.qgaSock); err != nil { + log.Error(prefix+"(open): "+err.Error()) + return errors.New(prefix+"(open): "+err.Error()) + } + + if err := con.SendReboot(); err != nil { + con.Close() + log.Error(prefix+"(send): "+err.Error()) + return errors.New(prefix+"(send): "+err.Error()) } if err := con.Close(); err != nil { - log.Error("VM shutdown failed (close): "+err.Error()) - return errors.New("VM shutdown failed (close): "+err.Error()) + log.Error(prefix+"(close): "+err.Error()) + return errors.New(prefix+"(close): "+err.Error()) } return nil diff --git a/src/vms/vms.go b/src/vms/vms.go index 49ff2201dc2efd35c6ddfa0bbd88107f47c3cfac..109180db1b7158b615be1cb1ec71b482bbc23e99 100644 --- a/src/vms/vms.go +++ b/src/vms/vms.go @@ -222,8 +222,8 @@ func (vms *VMs)StartVM(vmID uuid.UUID) (int, string, error) { return port, pwd, err } -// Stops by force a VM by its ID. -func (vms *VMs)StopVM(vmID uuid.UUID) error { +// Kills a VM by its ID. +func (vms *VMs)KillVM(vmID uuid.UUID) error { vms.rwlock.RLock() defer vms.rwlock.RUnlock() @@ -233,7 +233,7 @@ func (vms *VMs)StopVM(vmID uuid.UUID) error { } vm.mutex.Lock() - err = vm.stop() + err = vm.kill() vm.mutex.Unlock() return err } @@ -254,6 +254,22 @@ func (vms *VMs)ShutdownVM(vmID uuid.UUID) error { return err } +// Reboots a VM by its ID. +func (vms *VMs)RebootVM(vmID uuid.UUID) error { + vms.rwlock.Lock() + defer vms.rwlock.Unlock() + + vm, err := vms.getVMUnsafe(vmID) + if err != nil { + return err + } + + vm.mutex.Lock() + err = vm.reboot() + vm.mutex.Unlock() + return err +} + // Returns true if the given template is used by a VM. func (vms *VMs)IsTemplateUsed(templateID string) bool { vms.rwlock.RLock()