Skip to content
Snippets Groups Projects
Commit 52c5c820 authored by Florent Gluck's avatar Florent Gluck
Browse files

Added reboot support along the VM_REBOOT and VM_REBOOT_ANY capabilities.

New routes:
/vms/reboot: GET method: returns VMs that can be rebooted
/vms/{id}/reboot: PUT method: reboot a VM
parent 95a1b158
Branches
No related tags found
No related merge requests found
......@@ -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` |
......
......@@ -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,
......
......@@ -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,
......
......@@ -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+`"}}`))
......
......@@ -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)
......
......@@ -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.
......
......@@ -6,8 +6,8 @@ import (
const (
major = 1
minor = 0
bugfix = 1
minor = 1
bugfix = 0
)
type Version struct {
......
......@@ -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
......
......@@ -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()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment