diff --git a/src/caps/caps.go b/src/caps/caps.go index 120aee103da9c7239c823d2850181bb67f462f98..e59ca2f3bc33e6f9ae15678d20a9a9829409b8f5 100644 --- a/src/caps/caps.go +++ b/src/caps/caps.go @@ -1,118 +1,118 @@ package caps import ( - "errors" + "errors" ) type Capabilities map[string]int const ( - CAP_USER_CREATE string = "USER_CREATE" - CAP_USER_DESTROY = "USER_DESTROY" - CAP_USER_SET_CAPS = "USER_SET_CAPS" - CAP_USER_SET_PWD = "USER_SET_PWD" - CAP_USER_LIST = "USER_LIST" + CAP_USER_CREATE string = "USER_CREATE" + CAP_USER_DESTROY = "USER_DESTROY" + CAP_USER_SET_CAPS = "USER_SET_CAPS" + CAP_USER_SET_PWD = "USER_SET_PWD" + CAP_USER_LIST = "USER_LIST" - CAP_VM_LIST = "VM_LIST" - CAP_VM_LIST_ANY = "VM_LIST_ANY" - CAP_VM_START = "VM_START" - 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" - CAP_VM_EDIT = "VM_EDIT" - CAP_VM_EDIT_ANY = "VM_EDIT_ANY" - CAP_VM_SET_ACCESS = "VM_SET_ACCESS" - CAP_VM_READFS = "VM_READFS" - CAP_VM_READFS_ANY = "VM_READFS_ANY" - CAP_VM_WRITEFS = "VM_WRITEFS" - CAP_VM_WRITEFS_ANY = "VM_WRITEFS_ANY" + CAP_VM_LIST = "VM_LIST" + CAP_VM_LIST_ANY = "VM_LIST_ANY" + CAP_VM_START = "VM_START" + 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" + CAP_VM_EDIT = "VM_EDIT" + CAP_VM_EDIT_ANY = "VM_EDIT_ANY" + CAP_VM_SET_ACCESS = "VM_SET_ACCESS" + CAP_VM_READFS = "VM_READFS" + CAP_VM_READFS_ANY = "VM_READFS_ANY" + CAP_VM_WRITEFS = "VM_WRITEFS" + CAP_VM_WRITEFS_ANY = "VM_WRITEFS_ANY" - CAP_TPL_CREATE = "TPL_CREATE" - CAP_TPL_EDIT = "TPL_EDIT" - CAP_TPL_EDIT_ANY = "TPL_EDIT_ANY" - CAP_TPL_LIST = "TPL_LIST" - CAP_TPL_LIST_ANY = "TPL_LIST_ANY" - CAP_TPL_DESTROY = "TPL_DESTROY" - CAP_TPL_DESTROY_ANY = "TPL_DESTROY_ANY" - CAP_TPL_READFS = "TPL_READFS" - CAP_TPL_READFS_ANY = "TPL_READFS_ANY" + CAP_TPL_CREATE = "TPL_CREATE" + CAP_TPL_EDIT = "TPL_EDIT" + CAP_TPL_EDIT_ANY = "TPL_EDIT_ANY" + CAP_TPL_LIST = "TPL_LIST" + CAP_TPL_LIST_ANY = "TPL_LIST_ANY" + CAP_TPL_DESTROY = "TPL_DESTROY" + CAP_TPL_DESTROY_ANY = "TPL_DESTROY_ANY" + CAP_TPL_READFS = "TPL_READFS" + CAP_TPL_READFS_ANY = "TPL_READFS_ANY" ) // Capabilities stored in the user config var userCaps = Capabilities { - CAP_USER_CREATE: 1, - CAP_USER_DESTROY: 1, - CAP_USER_SET_CAPS: 1, - CAP_USER_LIST: 1, + CAP_USER_CREATE: 1, + CAP_USER_DESTROY: 1, + CAP_USER_SET_CAPS: 1, + CAP_USER_LIST: 1, - CAP_VM_CREATE: 1, - CAP_VM_DESTROY_ANY: 1, - 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, - CAP_VM_WRITEFS_ANY: 1, + CAP_VM_CREATE: 1, + CAP_VM_DESTROY_ANY: 1, + 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, + CAP_VM_WRITEFS_ANY: 1, - CAP_TPL_CREATE: 1, - CAP_TPL_EDIT: 1, - CAP_TPL_EDIT_ANY: 1, - CAP_TPL_DESTROY: 1, - CAP_TPL_DESTROY_ANY: 1, - CAP_TPL_LIST: 1, - CAP_TPL_LIST_ANY: 1, - CAP_TPL_READFS: 1, - CAP_TPL_READFS_ANY: 1, + CAP_TPL_CREATE: 1, + CAP_TPL_EDIT: 1, + CAP_TPL_EDIT_ANY: 1, + CAP_TPL_DESTROY: 1, + CAP_TPL_DESTROY_ANY: 1, + CAP_TPL_LIST: 1, + CAP_TPL_LIST_ANY: 1, + CAP_TPL_READFS: 1, + CAP_TPL_READFS_ANY: 1, } // Capabilities stored in the VM config (access) var VMAccessCaps = Capabilities { - CAP_VM_SET_ACCESS: 1, - CAP_VM_DESTROY: 1, - 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, + CAP_VM_SET_ACCESS: 1, + CAP_VM_DESTROY: 1, + 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, } // Returns true if the string cap matches a user capability func IsUserCapValid(cap string) bool { - _, exists := userCaps[cap] - return exists + _, exists := userCaps[cap] + return exists } // Returns true if the string cap matches a VM access capability func IsVMAccessCapValid(cap string) bool { - _, exists := VMAccessCaps[cap] - return exists + _, exists := VMAccessCaps[cap] + return exists } // Validates all user capabilities. func ValidateUserCaps(caps Capabilities) error { - for cap, _ := range caps { - if (!IsUserCapValid(cap)) { - return errors.New("Invalid capability: "+cap) - } - } - return nil + for cap, _ := range caps { + if (!IsUserCapValid(cap)) { + return errors.New("Invalid capability: "+cap) + } + } + return nil } // Validates all VM access capabilities. func ValidateVMAccessCaps(caps Capabilities) error { - for cap, _ := range caps { - if (!IsVMAccessCapValid(cap)) { - return errors.New("Invalid capability: "+cap) - } - } - return nil + for cap, _ := range caps { + if (!IsVMAccessCapValid(cap)) { + return errors.New("Invalid capability: "+cap) + } + } + return nil } diff --git a/src/cleaner/cleaner.go b/src/cleaner/cleaner.go index 187255c5d93f4c43f34c3cacc6d3d08deae66676..977417f0349503445dbe714b4200e23f907b3478 100644 --- a/src/cleaner/cleaner.go +++ b/src/cleaner/cleaner.go @@ -1,11 +1,11 @@ package cleaner import ( - "os" - "time" - "strings" - "path/filepath" - "nexus-server/logger" + "os" + "time" + "strings" + "path/filepath" + "nexus-server/logger" ) // Due to a bug in Echo's upload via HTTPS, many files whose's names start @@ -13,51 +13,51 @@ import ( // This thread deletes them to prevent saturating the system's disk space. const ( - periodTimeInMin = 45 - deleteOlderThanInMin = 30 + periodTimeInMin = 45 + deleteOlderThanInMin = 30 ) var log = logger.GetInstance() func isOlderThanMinutes(fi os.FileInfo, n time.Duration) bool { - return time.Now().Sub(fi.ModTime()) > n*time.Minute + return time.Now().Sub(fi.ModTime()) > n*time.Minute } -func cleanup() { - dir := "/tmp" - - log.Info("Cleanup thread configured to run every ", periodTimeInMin, " minutes and to delete uploaded files in ", dir, " last accessed more than ",deleteOlderThanInMin, " minutes ago") - - for { - log.Debug("Cleanup thread: checking for files to delete...") - openDir, err := os.Open(dir) - if err != nil { - log.Error("Cleanup thread exited with error: "+err.Error()) - return - } - // Retrieves all files entries in the directory (0 = all files in the directory). - files, err := openDir.Readdir(0) - if err != nil { - log.Error("Cleanup thread error: "+err.Error()) - } else { - for _, f := range(files) { - if !f.IsDir() && f.Mode().IsRegular() { - path := filepath.Join(dir, f.Name()) - if strings.HasPrefix(f.Name(), "multipart-") && isOlderThanMinutes(f, deleteOlderThanInMin) { - if err := os.Remove(path); err != nil { - log.Error("Cleanup thread error: "+err.Error()) - } else { - log.Info("Cleanup thread: deleted ", path) - } - } - } - } - openDir.Close() - } - time.Sleep(periodTimeInMin*time.Minute) - } +func cleanup() { + dir := "/tmp" + + log.Info("Cleanup thread configured to run every ", periodTimeInMin, " minutes and to delete uploaded files in ", dir, " last accessed more than ",deleteOlderThanInMin, " minutes ago") + + for { + log.Debug("Cleanup thread: checking for files to delete...") + openDir, err := os.Open(dir) + if err != nil { + log.Error("Cleanup thread exited with error: "+err.Error()) + return + } + // Retrieves all files entries in the directory (0 = all files in the directory). + files, err := openDir.Readdir(0) + if err != nil { + log.Error("Cleanup thread error: "+err.Error()) + } else { + for _, f := range(files) { + if !f.IsDir() && f.Mode().IsRegular() { + path := filepath.Join(dir, f.Name()) + if strings.HasPrefix(f.Name(), "multipart-") && isOlderThanMinutes(f, deleteOlderThanInMin) { + if err := os.Remove(path); err != nil { + log.Error("Cleanup thread error: "+err.Error()) + } else { + log.Info("Cleanup thread: deleted ", path) + } + } + } + } + openDir.Close() + } + time.Sleep(periodTimeInMin*time.Minute) + } } func Start() { - go cleanup() + go cleanup() } \ No newline at end of file diff --git a/src/consts/consts.go b/src/consts/consts.go index 5d8b3714f7eaf4a6fcd142b0f2292564683d575a..9f197530a362515edc7a45282b5c1158084167dc 100644 --- a/src/consts/consts.go +++ b/src/consts/consts.go @@ -1,22 +1,22 @@ package consts const ( - DefaultLogLevel = "info" + DefaultLogLevel = "info" - APIDefaultPort = 1077 - APIPortMin = 1025 - APIPortMax = 1099 + APIDefaultPort = 1077 + APIPortMin = 1025 + APIPortMax = 1099 - VMSpiceMinPort = 1100 - VMSpiceMaxPort = 65000 + VMSpiceMinPort = 1100 + VMSpiceMaxPort = 65000 - MaxUploadSize = "30G" + MaxUploadSize = "30G" - // We estimate that KVM allows for this amount of RAM saving in % - // (due to page sharing across VMs). - KsmRamSaving = 0.3 + // We estimate that KVM allows for this amount of RAM saving in % + // (due to page sharing across VMs). + KsmRamSaving = 0.3 - // To prevent RAM saturation, we refuse running new VMs if more than - // this amount of memory is being used (in %). - RamUsageLimit = .85 + // To prevent RAM saturation, we refuse running new VMs if more than + // this amount of memory is being used (in %). + RamUsageLimit = .85 ) diff --git a/src/exec/Guestfish.go b/src/exec/Guestfish.go index e366bcb3cf3b9f6773147e1f80751a25c5e6ffca..751707250d6feea0dfa9ec517b30a3a6fd9e6851 100644 --- a/src/exec/Guestfish.go +++ b/src/exec/Guestfish.go @@ -1,57 +1,57 @@ package exec import ( - "fmt" - "os/exec" - "strings" - "errors" + "fmt" + "os/exec" + "strings" + "errors" ) const ( - guestfishBinary = "guestfish" + guestfishBinary = "guestfish" ) // Checks guestfish is available. func CheckGuestfish() error { - output, err := exec.Command(guestfishBinary, "--version").Output() + output, err := exec.Command(guestfishBinary, "--version").Output() if err != nil { return errors.New(guestfishBinary+" is required but not found. On Ubuntu/Debian, it can be installed with \"sudo apt-get install guestfish\".") - } - out := string(output) - lines := strings.Split(out, "\n") - fields := strings.Split(lines[0], " ") - if len(fields) < 2 { - return errors.New("Failed extracting "+guestfishBinary+" version number!") - } - cmd := fields[0] - if cmd != guestfishBinary { - return errors.New(guestfishBinary+" is required, but not found.") - } - return nil + } + out := string(output) + lines := strings.Split(out, "\n") + fields := strings.Split(lines[0], " ") + if len(fields) < 2 { + return errors.New("Failed extracting "+guestfishBinary+" version number!") + } + cmd := fields[0] + if cmd != guestfishBinary { + return errors.New(guestfishBinary+" is required, but not found.") + } + return nil } // Copies and unarchives a local tar.gz archive into a directory (vmDir) inside the VM's filesystem. func CopyToVM(vmDiskFile, tarGzFile, vmDir string) error { - cmd := exec.Command(guestfishBinary, "--rw", "-i", "tar-in", "-a", vmDiskFile, tarGzFile, vmDir, "compress:gzip") - stdoutStderr, err := cmd.CombinedOutput() - if err != nil { - output := fmt.Sprintf("[%s]", stdoutStderr) - msg := "Failed writing to \""+vmDir+"\" in qcow ("+vmDiskFile+")" - log.Error(msg+": "+output) - return errors.New(msg) - } - return nil + cmd := exec.Command(guestfishBinary, "--rw", "-i", "tar-in", "-a", vmDiskFile, tarGzFile, vmDir, "compress:gzip") + stdoutStderr, err := cmd.CombinedOutput() + if err != nil { + output := fmt.Sprintf("[%s]", stdoutStderr) + msg := "Failed writing to \""+vmDir+"\" in qcow ("+vmDiskFile+")" + log.Error(msg+": "+output) + return errors.New(msg) + } + return nil } // Recursively copies a directory in the VM's filesystem (vmDir) into a tar.gz archive. func CopyFromVM(vmDiskFile, vmDir, tarGzFile string) error { - cmd := exec.Command(guestfishBinary, "--ro", "-i", "tar-out", "-a", vmDiskFile, vmDir, tarGzFile, "compress:gzip") - stdoutStderr, err := cmd.CombinedOutput() - if err != nil { - output := fmt.Sprintf("[%s]", stdoutStderr) - msg := "Failed reading \""+vmDir+"\" in qcow ("+vmDiskFile+"): "+output - log.Error(msg) - return errors.New(msg) - } - return nil + cmd := exec.Command(guestfishBinary, "--ro", "-i", "tar-out", "-a", vmDiskFile, vmDir, tarGzFile, "compress:gzip") + stdoutStderr, err := cmd.CombinedOutput() + if err != nil { + output := fmt.Sprintf("[%s]", stdoutStderr) + msg := "Failed reading \""+vmDir+"\" in qcow ("+vmDiskFile+"): "+output + log.Error(msg) + return errors.New(msg) + } + return nil } diff --git a/src/exec/QemuImg.go b/src/exec/QemuImg.go index 3335301ff6d05cc4079ad58f66a9b8899b1cef8b..c27d4f349061e3c6e6f0976156674c3036566d16 100644 --- a/src/exec/QemuImg.go +++ b/src/exec/QemuImg.go @@ -1,64 +1,64 @@ package exec import ( - "fmt" - "os/exec" - "strings" - "errors" - "strconv" - "nexus-server/logger" + "fmt" + "os/exec" + "strings" + "errors" + "strconv" + "nexus-server/logger" ) const ( - qemuimgBinary = "qemu-img" - qemuimgBinaryMinVersion = 4 + qemuimgBinary = "qemu-img" + qemuimgBinaryMinVersion = 4 ) var log = logger.GetInstance() // Checks qemu-img is available and the version is recent enough. func CheckQemuImg() error { - output, err := exec.Command(qemuimgBinary, "--version").Output() + output, err := exec.Command(qemuimgBinary, "--version").Output() if err != nil { return errors.New(qemuimgBinary+" is required but not found. On Ubuntu/Debian, it can be installed with \"sudo apt-get install qemu-utils\".") - } - out := string(output) - lines := strings.Split(out, "\n") - fields := strings.Split(lines[0], " ") - if len(fields) < 3 { - return errors.New("Failed extracting "+qemuimgBinary+" version number!") - } - v := fields[2] - majorVersionNumber, err := strconv.Atoi(string(v[0])) + } + out := string(output) + lines := strings.Split(out, "\n") + fields := strings.Split(lines[0], " ") + if len(fields) < 3 { + return errors.New("Failed extracting "+qemuimgBinary+" version number!") + } + v := fields[2] + majorVersionNumber, err := strconv.Atoi(string(v[0])) if err != nil { return errors.New("Failed extracting "+qemuimgBinary+" version number: "+err.Error()) - } - if majorVersionNumber < qemuimgBinaryMinVersion { - return errors.New(qemuimgBinary+" "+strconv.Itoa(qemuimgBinaryMinVersion)+".0.0 or newer is required. Found version "+v+".") - } - return nil + } + if majorVersionNumber < qemuimgBinaryMinVersion { + return errors.New(qemuimgBinary+" "+strconv.Itoa(qemuimgBinaryMinVersion)+".0.0 or newer is required. Found version "+v+".") + } + return nil } // Creates a VM disk image baked by a base image (template). func QemuImgCreate(templateDiskFile, vmDiskFile string) error { - cmd := exec.Command(qemuimgBinary, "create", "-F", "qcow2", "-b", templateDiskFile, "-f", "qcow2", vmDiskFile) - stdoutStderr, err := cmd.CombinedOutput() - if err != nil { - output := fmt.Sprintf("[%s]", stdoutStderr) - log.Error("Failed creating VM disk image: "+output) - return errors.New("Failed creating VM disk image") - } - return nil + cmd := exec.Command(qemuimgBinary, "create", "-F", "qcow2", "-b", templateDiskFile, "-f", "qcow2", vmDiskFile) + stdoutStderr, err := cmd.CombinedOutput() + if err != nil { + output := fmt.Sprintf("[%s]", stdoutStderr) + log.Error("Failed creating VM disk image: "+output) + return errors.New("Failed creating VM disk image") + } + return nil } // Rebases a VM overlay image in order to become a standalone image. func QemuImgRebase(overlayFile string) error { - cmd := exec.Command(qemuimgBinary, "rebase", "-b", "", overlayFile) - stdoutStderr, err := cmd.CombinedOutput() - if err != nil { - output := fmt.Sprintf("[%s]", stdoutStderr) - log.Error("Failed rebasing template disk from VM: "+": "+output) - return errors.New("Failed rebasing template disk from VM") - } - return nil + cmd := exec.Command(qemuimgBinary, "rebase", "-b", "", overlayFile) + stdoutStderr, err := cmd.CombinedOutput() + if err != nil { + output := fmt.Sprintf("[%s]", stdoutStderr) + log.Error("Failed rebasing template disk from VM: "+": "+output) + return errors.New("Failed rebasing template disk from VM") + } + return nil } diff --git a/src/exec/QemuSystem.go b/src/exec/QemuSystem.go index aae61a3c64d9827f77dae24da80ba392093fca67..e8524aa9c31e9f072702eba3b68a342df209b6d9 100644 --- a/src/exec/QemuSystem.go +++ b/src/exec/QemuSystem.go @@ -1,63 +1,63 @@ package exec import ( - "os/exec" - "strings" - "errors" - "strconv" - "nexus-server/utils" + "os/exec" + "strings" + "errors" + "strconv" + "nexus-server/utils" ) const ( - qemusystemBinary = "qemu-system-x86_64" - qemusystemMinVersion = 6 + qemusystemBinary = "qemu-system-x86_64" + qemusystemMinVersion = 6 ) // Check qemu-system-x86_64 is available and the version is recent enough. func CheckQemuSystem() error { - output, err := exec.Command(qemusystemBinary, "-version").Output() + output, err := exec.Command(qemusystemBinary, "-version").Output() if err != nil { return errors.New(qemusystemBinary+" is required but not found. On Ubuntu/Debian, it can be installed with \"sudo apt-get install qemu-system-x86\".") - } - out := string(output) - lines := strings.Split(out, "\n") - fields := strings.Split(lines[0], " ") - if len(fields) < 4 { - return errors.New("Failed extracting "+qemusystemBinary+" version number!") - } - v := fields[3] - majorVersionNumber, err := strconv.Atoi(string(v[0])) + } + out := string(output) + lines := strings.Split(out, "\n") + fields := strings.Split(lines[0], " ") + if len(fields) < 4 { + return errors.New("Failed extracting "+qemusystemBinary+" version number!") + } + v := fields[3] + majorVersionNumber, err := strconv.Atoi(string(v[0])) if err != nil { return errors.New("Failed extracting "+qemusystemBinary+" version number: "+err.Error()) - } - if majorVersionNumber < qemusystemMinVersion { - return errors.New(qemusystemBinary+" "+strconv.Itoa(qemusystemMinVersion)+".0.0 or newer is required. Found version "+v+".") - } - return nil + } + if majorVersionNumber < qemusystemMinVersion { + return errors.New(qemusystemBinary+" "+strconv.Itoa(qemusystemMinVersion)+".0.0 or newer is required. Found version "+v+".") + } + return nil } // Creates a qemu-system command. // The nic argument must be either "none" or "user". func NewQemuSystem(qgaSock string, cpus, ram int, nic, diskFile string, spicePort int, secretPwdFile, certDir string) (*exec.Cmd, error) { - nicArgs := "none" - if nic == "user" { - addr, err := utils.RandMacAddress() - if err != nil { - return nil, errors.New("Failed executing VM: MAC address generation error") - } - nicArgs = "user,mac="+addr+",model=virtio-net-pci" - } + nicArgs := "none" + if nic == "user" { + addr, err := utils.RandMacAddress() + if err != nil { + return nil, errors.New("Failed executing VM: MAC address generation error") + } + nicArgs = "user,mac="+addr+",model=virtio-net-pci" + } - // Remarks: - // 1) This device is to allow copy/paste between guest & client - // -device virtserialport,chardev=spicechannel0,name=com.redhat.spice.0 - // 2) For sound support: "-soundhw hda" - // 3) For shared folder with the host: - // - QEMU: virtfs local,path=/tmp/pipo,mount_tag=sharedfs,security_model=none - // - In the guest: mkdir shared && sudo mount -t 9p sharedfs shared - // 4) For QEMU Guest Agent support: - // -device virtio-serial -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0 -chardev socket,path=path_to_sock_file,server=on,wait=off,id=qga0 - cmd := exec.Command(qemusystemBinary, "-enable-kvm", "-cpu", "host", "-smp", "cpus="+strconv.Itoa(cpus), "-m", strconv.Itoa(ram), "-drive", "file="+diskFile+",index=0,media=disk,format=qcow2,discard=unmap,detect-zeroes=unmap,if=virtio", "-vga", "virtio", "-device", "virtio-serial-pci", "-object", "secret,id=sec,file="+secretPwdFile+",format=base64", "-spice", "tls-port="+strconv.Itoa(spicePort)+",password-secret=sec,x509-dir="+certDir, "-device", "virtserialport,chardev=spicechannel0,name=com.redhat.spice.0", "-chardev", "spicevmc,id=spicechannel0,name=vdagent", "-nic", nicArgs, "-device", "virtio-serial", "-device", "virtserialport,chardev=qga0,name=org.qemu.guest_agent.0", "-chardev", "socket,path="+qgaSock+",server=on,wait=off,id=qga0") + // Remarks: + // 1) This device is to allow copy/paste between guest & client + // -device virtserialport,chardev=spicechannel0,name=com.redhat.spice.0 + // 2) For sound support: "-soundhw hda" + // 3) For shared folder with the host: + // - QEMU: virtfs local,path=/tmp/pipo,mount_tag=sharedfs,security_model=none + // - In the guest: mkdir shared && sudo mount -t 9p sharedfs shared + // 4) For QEMU Guest Agent support: + // -device virtio-serial -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0 -chardev socket,path=path_to_sock_file,server=on,wait=off,id=qga0 + cmd := exec.Command(qemusystemBinary, "-enable-kvm", "-cpu", "host", "-smp", "cpus="+strconv.Itoa(cpus), "-m", strconv.Itoa(ram), "-drive", "file="+diskFile+",index=0,media=disk,format=qcow2,discard=unmap,detect-zeroes=unmap,if=virtio", "-vga", "virtio", "-device", "virtio-serial-pci", "-object", "secret,id=sec,file="+secretPwdFile+",format=base64", "-spice", "tls-port="+strconv.Itoa(spicePort)+",password-secret=sec,x509-dir="+certDir, "-device", "virtserialport,chardev=spicechannel0,name=com.redhat.spice.0", "-chardev", "spicevmc,id=spicechannel0,name=vdagent", "-nic", nicArgs, "-device", "virtio-serial", "-device", "virtserialport,chardev=qga0,name=org.qemu.guest_agent.0", "-chardev", "socket,path="+qgaSock+",server=on,wait=off,id=qga0") - return cmd, nil + return cmd, nil } diff --git a/src/logger/logger.go b/src/logger/logger.go index 5a86f38ab972e50806448fa49fac4adaa48e8d0a..7aa4884e09a9dc86af2b10d968d7fd37ec949962 100644 --- a/src/logger/logger.go +++ b/src/logger/logger.go @@ -1,22 +1,22 @@ package logger import ( - "sync" - "github.com/sirupsen/logrus" + "sync" + "github.com/sirupsen/logrus" ) var log *logrus.Logger var once sync.Once func GetInstance() *logrus.Logger { - once.Do(func() { - log = logrus.New() - //log.SetReportCaller(true) - log.SetFormatter(&logrus.TextFormatter{ - //FullTimestamp: true, - DisableTimestamp: true, - }) - }) + once.Do(func() { + log = logrus.New() + //log.SetReportCaller(true) + log.SetFormatter(&logrus.TextFormatter{ + //FullTimestamp: true, + DisableTimestamp: true, + }) + }) return log } diff --git a/src/nexus-server.go b/src/nexus-server.go index 17df7bbbfb52939d8b34beb9fa3304502184dd60..83cd34b821aed66a21fc070801a87415f2ba8031 100644 --- a/src/nexus-server.go +++ b/src/nexus-server.go @@ -1,99 +1,99 @@ package main import ( - "os" - "fmt" - "path" - "flag" - "strconv" - "strings" - "nexus-server/vms" - "nexus-server/exec" - "nexus-server/paths" - "nexus-server/users" - "nexus-server/utils" - "nexus-server/router" - "nexus-server/logger" - "nexus-server/consts" - "nexus-server/cleaner" - "nexus-server/version" - "github.com/sirupsen/logrus" + "os" + "fmt" + "path" + "flag" + "strconv" + "strings" + "nexus-server/vms" + "nexus-server/exec" + "nexus-server/paths" + "nexus-server/users" + "nexus-server/utils" + "nexus-server/router" + "nexus-server/logger" + "nexus-server/consts" + "nexus-server/cleaner" + "nexus-server/version" + "github.com/sirupsen/logrus" ) var log = logger.GetInstance() func main() { - log.SetLevel(logrus.InfoLevel) + log.SetLevel(logrus.InfoLevel) - var appname = path.Base(os.Args[0]) - log.Info(appname+" version "+version.Get().String()) + var appname = path.Base(os.Args[0]) + log.Info(appname+" version "+version.Get().String()) - // These two lines must be executed very early on (basically, here). - paths.Init() - utils.RandInit() + // These two lines must be executed very early on (basically, here). + paths.Init() + utils.RandInit() - if err := exec.CheckQemuSystem(); err != nil { - log.Fatal(err) - } - if err := exec.CheckQemuImg(); err != nil { - log.Fatal(err) - } - if err := exec.CheckGuestfish(); err != nil { - log.Fatal(err) - } + if err := exec.CheckQemuSystem(); err != nil { + log.Fatal(err) + } + if err := exec.CheckQemuImg(); err != nil { + log.Fatal(err) + } + if err := exec.CheckGuestfish(); err != nil { + log.Fatal(err) + } - usage := func() { - fmt.Println("Usage of "+appname+":") - flag.PrintDefaults() - os.Exit(1) - } + usage := func() { + fmt.Println("Usage of "+appname+":") + flag.PrintDefaults() + os.Exit(1) + } - loglevelFlag := flag.String("l", consts.DefaultLogLevel, "Log level: debug, info, warn, error, fatal") - portMin := strconv.Itoa(consts.APIPortMin) - portMax := strconv.Itoa(consts.APIPortMax) - portFlag := flag.Int("p", consts.APIDefaultPort, "Port on which to listen to (between "+portMin+" and "+portMax+")") - flag.Parse() + loglevelFlag := flag.String("l", consts.DefaultLogLevel, "Log level: debug, info, warn, error, fatal") + portMin := strconv.Itoa(consts.APIPortMin) + portMax := strconv.Itoa(consts.APIPortMax) + portFlag := flag.Int("p", consts.APIDefaultPort, "Port on which to listen to (between "+portMin+" and "+portMax+")") + flag.Parse() - loglevelStr := strings.ToLower(*loglevelFlag) - switch loglevelStr { - case "debug": - log.SetLevel(logrus.DebugLevel) - case "info": - log.SetLevel(logrus.InfoLevel) - case "warn": - log.SetLevel(logrus.WarnLevel) - case "error": - log.SetLevel(logrus.ErrorLevel) - case "fatal": - log.SetLevel(logrus.FatalLevel) - default: - fmt.Println("Invalid log level!") - usage() - } + loglevelStr := strings.ToLower(*loglevelFlag) + switch loglevelStr { + case "debug": + log.SetLevel(logrus.DebugLevel) + case "info": + log.SetLevel(logrus.InfoLevel) + case "warn": + log.SetLevel(logrus.WarnLevel) + case "error": + log.SetLevel(logrus.ErrorLevel) + case "fatal": + log.SetLevel(logrus.FatalLevel) + default: + fmt.Println("Invalid log level!") + usage() + } - port := *portFlag - if port < consts.APIPortMin || port > consts.APIPortMax { - fmt.Println("Invalid port number!") - usage() - } + port := *portFlag + if port < consts.APIPortMin || port > consts.APIPortMax { + fmt.Println("Invalid port number!") + usage() + } - err := users.InitUsers() - if err != nil { - log.Fatal(err.Error()) - } + err := users.InitUsers() + if err != nil { + log.Fatal(err.Error()) + } - // Templates are required by VMs hence they're loaded first. - err = vms.InitTemplates() - if err != nil { - log.Fatal(err.Error()) - } + // Templates are required by VMs hence they're loaded first. + err = vms.InitTemplates() + if err != nil { + log.Fatal(err.Error()) + } - err = vms.InitVMs() - if err != nil { - log.Fatal(err.Error()) - } + err = vms.InitVMs() + if err != nil { + log.Fatal(err.Error()) + } - cleaner.Start() + cleaner.Start() - router.New().Start(port) + router.New().Start(port) } diff --git a/src/paths/paths.go b/src/paths/paths.go index 766851c66de040bcf09ecfa72bb24d2d114d0dd8..12f78fb40560ec1935012e66227736b18637ff1a 100644 --- a/src/paths/paths.go +++ b/src/paths/paths.go @@ -1,37 +1,37 @@ package paths import ( - "path/filepath" - "nexus-server/logger" + "path/filepath" + "nexus-server/logger" ) type Paths struct { - ConfigDir string - UsersFile string - DataDir string - VMsDir string - TemplatesDir string - NexusPkiDir string - TmpDir string + ConfigDir string + UsersFile string + DataDir string + VMsDir string + TemplatesDir string + NexusPkiDir string + TmpDir string } var log = logger.GetInstance() var paths *Paths func GetInstance() *Paths { - return paths + return paths } func Init() { - config := "../config" - data := "../data" - paths = &Paths { - ConfigDir: config, - UsersFile: filepath.Join(config, "/users.json"), - DataDir: data, - VMsDir: filepath.Join(data, "/vms"), - TemplatesDir: filepath.Join(data, "/templates"), - NexusPkiDir: filepath.Join(config, "/pki/server-nexus"), - TmpDir: filepath.Join(data, "/tmp"), - } + config := "../config" + data := "../data" + paths = &Paths { + ConfigDir: config, + UsersFile: filepath.Join(config, "/users.json"), + DataDir: data, + VMsDir: filepath.Join(data, "/vms"), + TemplatesDir: filepath.Join(data, "/templates"), + NexusPkiDir: filepath.Join(config, "/pki/server-nexus"), + TmpDir: filepath.Join(data, "/tmp"), + } } diff --git a/src/qga/qga.go b/src/qga/qga.go index ae4195d55cc46311f0e4bdcc039402f74764bde0..3a7a4d6ba85a380c8f7bf3c6204cc37212f784da 100644 --- a/src/qga/qga.go +++ b/src/qga/qga.go @@ -1,21 +1,21 @@ package qga import ( - "fmt" - "net" - "errors" - "encoding/base64" - "nexus-server/logger" + "fmt" + "net" + "errors" + "encoding/base64" + "nexus-server/logger" ) type QGACon struct { - sock net.Conn + sock net.Conn } var log = logger.GetInstance() func New() *QGACon { - return &QGACon{} + return &QGACon{} } // TODO @@ -24,102 +24,102 @@ func New() *QGACon { // sockAddr is the path to a UNIX domain socket. func (q *QGACon)Open(sockAddr string) error { - con, err := net.Dial("unix", sockAddr) - if err != nil { - return err - } - q.sock = con - return nil + con, err := net.Dial("unix", sockAddr) + if err != nil { + return err + } + q.sock = con + return nil } func (q *QGACon)Close() error { - return q.sock.Close() + return q.sock.Close() } // Send a shutdown command to the guest OS. func (q *QGACon)SendShutdown() error { - _, err := q.sock.Write([]byte(` - { - "execute": "guest-shutdown" - } - `)) - return err + _, err := q.sock.Write([]byte(` + { + "execute": "guest-shutdown" + } + `)) + return err } // Send a reboot command to the guest OS. func (q *QGACon)SendReboot() error { - return q.SendCmd("reboot") + 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 + _, 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+`" - } - } - `)) - return err + _, err := q.sock.Write([]byte(` + { + "execute":"guest-exec", + "arguments": { + "path":"`+cmd+`" + } + } + `)) + return err } // TODO: work in progress... func (q *QGACon)WriteFile(filePath string) error { - filePath = "/home/nexus/pipo.txt" - _, err := q.sock.Write([]byte(` - { - "execute":"guest-file-open", - "arguments": { - "path":"`+filePath+`", - "mode":"w+" - } - } - `)) - buf := make([]byte, 128) - _, err = q.sock.Read(buf) - if err != nil { - return err - } - handle := string(buf[:]) - fmt.Println("handle: ",handle) + filePath = "/home/nexus/pipo.txt" + _, err := q.sock.Write([]byte(` + { + "execute":"guest-file-open", + "arguments": { + "path":"`+filePath+`", + "mode":"w+" + } + } + `)) + buf := make([]byte, 128) + _, err = q.sock.Read(buf) + if err != nil { + return err + } + handle := string(buf[:]) + fmt.Println("handle: ",handle) - // content must be encoded in base64 - content := "Hello ma poule!" - contentB64 := base64.StdEncoding.EncodeToString([]byte(content)) - status, err := q.sock.Write([]byte(` - { - "execute":"guest-file-write", - "arguments": { - "handle":1000, - "buf-b64":"`+contentB64+`" - } - } - `)) - status, err = q.sock.Write([]byte(` - { - "execute":"guest-file-close", - "arguments": { - "handle":1000 - } - } - `)) - if status < 0 { - return errors.New("write error") - } - return err + // content must be encoded in base64 + content := "Hello ma poule!" + contentB64 := base64.StdEncoding.EncodeToString([]byte(content)) + status, err := q.sock.Write([]byte(` + { + "execute":"guest-file-write", + "arguments": { + "handle":1000, + "buf-b64":"`+contentB64+`" + } + } + `)) + status, err = q.sock.Write([]byte(` + { + "execute":"guest-file-close", + "arguments": { + "handle":1000 + } + } + `)) + if status < 0 { + return errors.New("write error") + } + return err } diff --git a/src/router/auth.go b/src/router/auth.go index ffcee8fc5b9a1c73ec7c310d35ebb7f019858a45..049a21829a4848399101fe2df91f273fdd6682cb 100644 --- a/src/router/auth.go +++ b/src/router/auth.go @@ -1,43 +1,43 @@ package router import ( - "time" - "errors" - "net/http" - "nexus-server/users" - "golang.org/x/crypto/bcrypt" - "github.com/golang-jwt/jwt" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - "github.com/go-playground/validator/v10" - "github.com/sethvargo/go-password/password" + "time" + "errors" + "net/http" + "nexus-server/users" + "golang.org/x/crypto/bcrypt" + "github.com/golang-jwt/jwt" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/go-playground/validator/v10" + "github.com/sethvargo/go-password/password" ) const ( - // Access token expiration time. - accessTokenExpirationTimeInHours = 12 + // Access token expiration time. + accessTokenExpirationTimeInHours = 12 - // Number of login attemps after which an account is locked. - maxloginAttempts = 5 - // How long an account is locked for. - lockedTimeInHours = 4.0 + // Number of login attemps after which an account is locked. + maxloginAttempts = 5 + // How long an account is locked for. + lockedTimeInHours = 4.0 ) // jwt.StandardClaims is added as an embedded type to provide fields like user email and expiration time. type CustomClaims struct { - Email string - jwt.StandardClaims + Email string + jwt.StandardClaims } // These fields must match the ones in user.User type Credentials struct { - Email string `json:"email" validate:"required,email"` - Pwd string `json:"pwd" validate:"required,min=8"` + Email string `json:"email" validate:"required,email"` + Pwd string `json:"pwd" validate:"required,min=8"` } type Auth struct { - users *users.Users - tokenAccess middleware.JWTConfig + users *users.Users + tokenAccess middleware.JWTConfig } var loginAttempts map[string]int // Keeps track of the number of wrong logins. @@ -46,142 +46,142 @@ var jwtSecretKey string // Creates an Auth object. func NewAuth(u *users.Users) (*Auth, error) { - loginAttempts = make(map[string]int) - loginLocked = make(map[string]time.Time) - - // TODO: better to store jwtSecretKey in an env. variable? - // Generates a 64 characters long key with 20 digits, 0 symbols, - // allowing upper and lower case letters, and allowing repeat characters. - secret, err := password.Generate(64, 20, 0, false, true) - if err != nil { - return nil, errors.New("Secret generation error: "+err.Error()) - } - jwtSecretKey = secret - - return &Auth { - users : u, - tokenAccess : middleware.JWTConfig { - ContextKey: "user", - SigningKey: []byte(jwtSecretKey), - Claims: &CustomClaims{}, - ErrorHandlerWithContext: func(err error, c echo.Context) error { - return echo.NewHTTPError(http.StatusUnauthorized, "access denied") - }, - }, - }, nil + loginAttempts = make(map[string]int) + loginLocked = make(map[string]time.Time) + + // TODO: better to store jwtSecretKey in an env. variable? + // Generates a 64 characters long key with 20 digits, 0 symbols, + // allowing upper and lower case letters, and allowing repeat characters. + secret, err := password.Generate(64, 20, 0, false, true) + if err != nil { + return nil, errors.New("Secret generation error: "+err.Error()) + } + jwtSecretKey = secret + + return &Auth { + users : u, + tokenAccess : middleware.JWTConfig { + ContextKey: "user", + SigningKey: []byte(jwtSecretKey), + Claims: &CustomClaims{}, + ErrorHandlerWithContext: func(err error, c echo.Context) error { + return echo.NewHTTPError(http.StatusUnauthorized, "access denied") + }, + }, + }, nil } // Token used by the JWT middleware to protect access to the specified group of routes. func (auth *Auth)GetTokenAccess() middleware.JWTConfig { - return auth.tokenAccess + return auth.tokenAccess } // curl --cacert ca.pem -X POST https://localhost:1077/login -H 'Content-Type: application/json' -d '{"email": "johndoe@nexus.org", "pwd":"the_user_password"}' // Returns a jwt token if authentication was successful: // {"token":"<jwt token>"} func (auth *Auth)Login(c echo.Context) error { - // Deserializes the JSON body and checks its validity. - reqUser := new(Credentials) - if err := decodeJson(c, &reqUser); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - if err := validator.New().Struct(reqUser); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - // User exists? - user, err := auth.users.GetUserByEmail(reqUser.Email) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - - // Is user locked? - lockedTime, exists := loginLocked[user.Email] - if exists { - // If locked time has elapsed, unlock the account. - t := time.Now() - elapsedTime := t.Sub(lockedTime) - if elapsedTime.Hours() > lockedTimeInHours { - delete(loginLocked, user.Email) - delete(loginAttempts, user.Email) - } else { - return echo.NewHTTPError(http.StatusUnauthorized, "Account is locked") - } - } - - // Checks the user password is correct. - // If a user enters the wrong password too many times, the account will be locked for a number of hours. - if err := bcrypt.CompareHashAndPassword([]byte(user.Pwd), []byte(reqUser.Pwd)); err != nil { - // Stores the number of attempted logins. - count, exists := loginAttempts[user.Email] - if exists { - loginAttempts[user.Email] = count+1 - if count >= maxloginAttempts { - // Locks the account. - loginLocked[user.Email] = time.Now() - return echo.NewHTTPError(http.StatusUnauthorized, "Account is locked") - } - } else { - loginAttempts[user.Email] = 1 - } - - // If the two passwords don't match, return a 401 status. - return echo.NewHTTPError(http.StatusUnauthorized, "Invalid password") - } else { - loginAttempts[user.Email] = 0 - } - - // Retrieves user from its email. - user, err = auth.users.GetUserByEmail(reqUser.Email) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, "User not found") - } - - token, err := createToken(user.Email) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - - return c.JSON(http.StatusOK, echo.Map{"token": token}) + // Deserializes the JSON body and checks its validity. + reqUser := new(Credentials) + if err := decodeJson(c, &reqUser); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + if err := validator.New().Struct(reqUser); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + // User exists? + user, err := auth.users.GetUserByEmail(reqUser.Email) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + + // Is user locked? + lockedTime, exists := loginLocked[user.Email] + if exists { + // If locked time has elapsed, unlock the account. + t := time.Now() + elapsedTime := t.Sub(lockedTime) + if elapsedTime.Hours() > lockedTimeInHours { + delete(loginLocked, user.Email) + delete(loginAttempts, user.Email) + } else { + return echo.NewHTTPError(http.StatusUnauthorized, "Account is locked") + } + } + + // Checks the user password is correct. + // If a user enters the wrong password too many times, the account will be locked for a number of hours. + if err := bcrypt.CompareHashAndPassword([]byte(user.Pwd), []byte(reqUser.Pwd)); err != nil { + // Stores the number of attempted logins. + count, exists := loginAttempts[user.Email] + if exists { + loginAttempts[user.Email] = count+1 + if count >= maxloginAttempts { + // Locks the account. + loginLocked[user.Email] = time.Now() + return echo.NewHTTPError(http.StatusUnauthorized, "Account is locked") + } + } else { + loginAttempts[user.Email] = 1 + } + + // If the two passwords don't match, return a 401 status. + return echo.NewHTTPError(http.StatusUnauthorized, "Invalid password") + } else { + loginAttempts[user.Email] = 0 + } + + // Retrieves user from its email. + user, err = auth.users.GetUserByEmail(reqUser.Email) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, "User not found") + } + + token, err := createToken(user.Email) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + + return c.JSON(http.StatusOK, echo.Map{"token": token}) } // Creates a JWT token and claims with the expiration time for the given user. func createToken(userEmail string) (string, error) { - expirationTime := time.Now().Add(accessTokenExpirationTimeInHours*time.Hour) - - // Creates the claims - claims := &CustomClaims{ - Email: userEmail, - StandardClaims: jwt.StandardClaims { - // JWT expresses expiration time in Unix milliseconds - ExpiresAt: expirationTime.Unix(), - }, - } - - // Declares the token with the algorithm used for signing, and the claims. - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - // Signs the token and gets a string version of the token. - secret := []byte(jwtSecretKey) - tokenString, err := token.SignedString(secret) - if err != nil { - return "", err - } - - return tokenString, nil + expirationTime := time.Now().Add(accessTokenExpirationTimeInHours*time.Hour) + + // Creates the claims + claims := &CustomClaims{ + Email: userEmail, + StandardClaims: jwt.StandardClaims { + // JWT expresses expiration time in Unix milliseconds + ExpiresAt: expirationTime.Unix(), + }, + } + + // Declares the token with the algorithm used for signing, and the claims. + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Signs the token and gets a string version of the token. + secret := []byte(jwtSecretKey) + tokenString, err := token.SignedString(secret) + if err != nil { + return "", err + } + + return tokenString, nil } // Returns user email from the Echo context. // If an error occured, returns the error. func GetUserFromContext(c echo.Context) (string, error) { - if c.Get("user") == nil { - return "", errors.New("user not authenticated") - } + if c.Get("user") == nil { + return "", errors.New("user not authenticated") + } - user := c.Get("user").(*jwt.Token) - claims := user.Claims.(*CustomClaims) + user := c.Get("user").(*jwt.Token) + claims := user.Claims.(*CustomClaims) - return claims.Email, nil + return claims.Email, nil } /* @@ -194,16 +194,16 @@ func GetUserFromContext(c echo.Context) (string, error) { usersGroup.Use(auth.OnlyJoe) // apply the middleware (to be added in router.go) func OnlyJoe(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - // Gets user token from context. + return func(c echo.Context) error { + // Gets user token from context. if c.Get("user") == nil { - return echo.NewHTTPError(http.StatusUnauthorized, "invalid user token\n") + return echo.NewHTTPError(http.StatusUnauthorized, "invalid user token\n") } user := c.Get("user").(*jwt.Token) claims := user.Claims.(*CustomClaims) username := claims.Username if username != "joe" { - return echo.ErrUnauthorized + return echo.ErrUnauthorized } return next(c) } diff --git a/src/router/helper.go b/src/router/helper.go index 5ad241bcf148b2cca58459a1365f7f71c51f9c56..dea451098945bb0ff24f565b7e1d6df8d98bd10f 100644 --- a/src/router/helper.go +++ b/src/router/helper.go @@ -1,40 +1,40 @@ package router import ( - "encoding/json" - "nexus-server/users" - "github.com/labstack/echo/v4" + "encoding/json" + "nexus-server/users" + "github.com/labstack/echo/v4" ) const ( - msgInsufficientCaps = "Permission denied: insufficient capabilities" + msgInsufficientCaps = "Permission denied: insufficient capabilities" ) func jsonMsg(msg string) map[string]interface{} { - // The following 2 lines are identical: - //return map[string]interface{}{"message": msg} - return echo.Map{"message": msg} + // The following 2 lines are identical: + //return map[string]interface{}{"message": msg} + return echo.Map{"message": msg} } // Decodes and validates a json object in the response's body. func decodeJson(c echo.Context, object any) error { - decoder := json.NewDecoder(c.Request().Body) - decoder.DisallowUnknownFields() - return decoder.Decode(object) + decoder := json.NewDecoder(c.Request().Body) + decoder.DisallowUnknownFields() + return decoder.Decode(object) } func getLoggedUser(users *users.Users, c echo.Context) (*users.User, error) { - // Retrieves user email from context. - email, err := GetUserFromContext(c) - if err != nil { - log.Error("Failed retrieving user info from context: "+err.Error()) - return nil, err - } - // Retrieves the user structure - user, err := users.GetUserByEmail(email) - if err != nil { - log.Error("No user matching email "+email+": "+err.Error()) - return nil, err - } - return &user, nil + // Retrieves user email from context. + email, err := GetUserFromContext(c) + if err != nil { + log.Error("Failed retrieving user info from context: "+err.Error()) + return nil, err + } + // Retrieves the user structure + user, err := users.GetUserByEmail(email) + if err != nil { + log.Error("No user matching email "+email+": "+err.Error()) + return nil, err + } + return &user, nil } diff --git a/src/router/router.go b/src/router/router.go index c2b9f6e486d7fc2fa34e9257b9a7c650043aa4db..5402ec0cc4d2ddc89acd5172d64216ab35f523c3 100644 --- a/src/router/router.go +++ b/src/router/router.go @@ -1,143 +1,143 @@ package router import ( - "os" - "time" - "syscall" - "context" - "strconv" - "net/http" - "os/signal" - "path/filepath" - "nexus-server/paths" - "nexus-server/users" - "nexus-server/logger" - "nexus-server/consts" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "os" + "time" + "syscall" + "context" + "strconv" + "net/http" + "os/signal" + "path/filepath" + "nexus-server/paths" + "nexus-server/users" + "nexus-server/logger" + "nexus-server/consts" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" ) type Router struct { - echo *echo.Echo - version *RouterVersion - token *RouterToken - users *RouterUsers - vms *RouterVMs - tpl *RouterTemplates + echo *echo.Echo + version *RouterVersion + token *RouterToken + users *RouterUsers + vms *RouterVMs + tpl *RouterTemplates } var log = logger.GetInstance() func New() *Router { - return &Router{ - echo.New(), - NewRouterVersion(), - NewRouterToken(), - NewRouterUsers(), - NewRouterVMs(), - NewRouterTemplates()} + return &Router{ + echo.New(), + NewRouterVersion(), + NewRouterToken(), + NewRouterUsers(), + NewRouterVMs(), + NewRouterTemplates()} } func (router *Router)Start(port int) { - auth, err := NewAuth(users.GetUsersInstance()) - if err != nil { - log.Fatal("Server error: "+err.Error()) - } - - router.echo.HideBanner = true - router.echo.Use(middleware.BodyLimit(consts.MaxUploadSize)) - - // Login management. - router.echo.POST("/login", auth.Login) - - // Version management. - router.echo.GET("/version", router.version.GetVersion) - - // Token management. - tokenGroup := router.echo.Group("/token") - tokenGroup.Use(middleware.JWTWithConfig(auth.GetTokenAccess())) - tokenGroup.GET("/refresh", router.token.GetRefreshToken) - - // User management. - usersGroup := router.echo.Group("/users") - usersGroup.Use(middleware.JWTWithConfig(auth.GetTokenAccess())) + auth, err := NewAuth(users.GetUsersInstance()) + if err != nil { + log.Fatal("Server error: "+err.Error()) + } + + router.echo.HideBanner = true + router.echo.Use(middleware.BodyLimit(consts.MaxUploadSize)) + + // Login management. + router.echo.POST("/login", auth.Login) + + // Version management. + router.echo.GET("/version", router.version.GetVersion) + + // Token management. + tokenGroup := router.echo.Group("/token") + tokenGroup.Use(middleware.JWTWithConfig(auth.GetTokenAccess())) + tokenGroup.GET("/refresh", router.token.GetRefreshToken) + + // User management. + usersGroup := router.echo.Group("/users") + usersGroup.Use(middleware.JWTWithConfig(auth.GetTokenAccess())) usersGroup.GET("", router.users.GetUsers) - usersGroup.GET("/whoami", router.users.GetLoggedUser) - usersGroup.POST("", router.users.CreateUser) - usersGroup.DELETE("/:email", router.users.DeleteUserByEmail) - usersGroup.PUT("/:email/caps", router.users.SetUserCaps) - usersGroup.PUT("/pwd", router.users.SetUserPwd) - - // VM management. - vmsGroup := router.echo.Group("/vms") - vmsGroup.Use(middleware.JWTWithConfig(auth.GetTokenAccess())) - vmsGroup.GET("", router.vms.GetListableVMs) - vmsGroup.GET("/attach", router.vms.GetAttachableVMs) - 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) - vmsGroup.GET("/importfiles", router.vms.GetFilesImportableVMs) - - vmsGroup.POST("", router.vms.CreateVM) - 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.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) - vmsGroup.POST("/:id/importfiles", router.vms.ImportFilesToVM) - - // Template management. - templatesGroup := router.echo.Group("/templates") - templatesGroup.Use(middleware.JWTWithConfig(auth.GetTokenAccess())) - templatesGroup.GET("", router.tpl.GetTemplates) - templatesGroup.POST("/vm", router.tpl.CreateTemplateFromVM) - templatesGroup.POST("/qcow", router.tpl.CreateTemplateFromQCOW) - templatesGroup.GET("/:id/disk", router.tpl.ExportDisk) - templatesGroup.DELETE("/:id", router.tpl.DeleteTemplateByID) - templatesGroup.PUT("/:id", router.tpl.EditTemplateByID) - - // Starts server in a dedicated goroutine. - go func() { - pkiDir := paths.GetInstance().NexusPkiDir - if err := router.echo.StartTLS(":"+strconv.Itoa(port), filepath.Join(pkiDir, "/server-cert.pem"), filepath.Join(pkiDir, "/server-key.pem")); err != nil { - if err != http.ErrServerClosed { - log.Fatal("Server error: "+err.Error()) - } else { - log.Info("Shutting down server...") - } - } - }() - - // Wait on dedicated signals. - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) - - for { - sig := <-sigs // blocks on any of the above signals. - log.Info("Server caught signal ("+sig.String()+")") - // SIGHUP tells the server to reload its configuration. - // This is just a placeholder: it does nothing for now. - // TODO: reloads users and templates? (very tricky though) - if sig == syscall.SIGHUP { - log.Info("Reloading server configuration") - } else { - // Any other signal tells the server to gracefully shutdown. - break - } - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := router.echo.Shutdown(ctx); err != nil { - log.Fatal("Server shutdown failed: "+err.Error()) - } - log.Info("Server shutdown completed.") + usersGroup.GET("/whoami", router.users.GetLoggedUser) + usersGroup.POST("", router.users.CreateUser) + usersGroup.DELETE("/:email", router.users.DeleteUserByEmail) + usersGroup.PUT("/:email/caps", router.users.SetUserCaps) + usersGroup.PUT("/pwd", router.users.SetUserPwd) + + // VM management. + vmsGroup := router.echo.Group("/vms") + vmsGroup.Use(middleware.JWTWithConfig(auth.GetTokenAccess())) + vmsGroup.GET("", router.vms.GetListableVMs) + vmsGroup.GET("/attach", router.vms.GetAttachableVMs) + 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) + vmsGroup.GET("/importfiles", router.vms.GetFilesImportableVMs) + + vmsGroup.POST("", router.vms.CreateVM) + 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.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) + vmsGroup.POST("/:id/importfiles", router.vms.ImportFilesToVM) + + // Template management. + templatesGroup := router.echo.Group("/templates") + templatesGroup.Use(middleware.JWTWithConfig(auth.GetTokenAccess())) + templatesGroup.GET("", router.tpl.GetTemplates) + templatesGroup.POST("/vm", router.tpl.CreateTemplateFromVM) + templatesGroup.POST("/qcow", router.tpl.CreateTemplateFromQCOW) + templatesGroup.GET("/:id/disk", router.tpl.ExportDisk) + templatesGroup.DELETE("/:id", router.tpl.DeleteTemplateByID) + templatesGroup.PUT("/:id", router.tpl.EditTemplateByID) + + // Starts server in a dedicated goroutine. + go func() { + pkiDir := paths.GetInstance().NexusPkiDir + if err := router.echo.StartTLS(":"+strconv.Itoa(port), filepath.Join(pkiDir, "/server-cert.pem"), filepath.Join(pkiDir, "/server-key.pem")); err != nil { + if err != http.ErrServerClosed { + log.Fatal("Server error: "+err.Error()) + } else { + log.Info("Shutting down server...") + } + } + }() + + // Wait on dedicated signals. + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) + + for { + sig := <-sigs // blocks on any of the above signals. + log.Info("Server caught signal ("+sig.String()+")") + // SIGHUP tells the server to reload its configuration. + // This is just a placeholder: it does nothing for now. + // TODO: reloads users and templates? (very tricky though) + if sig == syscall.SIGHUP { + log.Info("Reloading server configuration") + } else { + // Any other signal tells the server to gracefully shutdown. + break + } + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := router.echo.Shutdown(ctx); err != nil { + log.Fatal("Server shutdown failed: "+err.Error()) + } + log.Info("Server shutdown completed.") } diff --git a/src/router/routerTemplates.go b/src/router/routerTemplates.go index 33c7d4c9f80e31eaaf9f5fc4679edc128dbed83a..ac751c6182ac98e137cdc758095e4babb8659b44 100644 --- a/src/router/routerTemplates.go +++ b/src/router/routerTemplates.go @@ -1,28 +1,28 @@ package router import ( - "io" - "os" - "net/http" - "path/filepath" - "nexus-server/vms" - "nexus-server/caps" - "nexus-server/paths" - "nexus-server/users" + "io" + "os" + "net/http" + "path/filepath" + "nexus-server/vms" + "nexus-server/caps" + "nexus-server/paths" + "nexus-server/users" "github.com/labstack/echo/v4" - "github.com/google/uuid" + "github.com/google/uuid" ) type ( - RouterTemplates struct { - users *users.Users - tpl *vms.Templates - vms *vms.VMs - } + RouterTemplates struct { + users *users.Users + tpl *vms.Templates + vms *vms.VMs + } ) func NewRouterTemplates() *RouterTemplates { - return &RouterTemplates{ users: users.GetUsersInstance(), tpl: vms.GetTemplatesInstance(), vms: vms.GetVMsInstance() } + return &RouterTemplates{ users: users.GetUsersInstance(), tpl: vms.GetTemplatesInstance(), vms: vms.GetVMsInstance() } } // Returns templates that can be listed. @@ -31,85 +31,85 @@ func NewRouterTemplates() *RouterTemplates { // CAP_TPL_LIST: only templates owned by the user as well as public templates can be listed. // curl --cacert ca.pem -X GET https://localhost:1077/templates -H "Authorization: Bearer <AccessToken>" func (r *RouterTemplates)GetTemplates(c echo.Context) error { - // Retrieves logged user from context. - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - - // If the logged user has CAP_TPL_LIST_ANY, lists all templates. - if user.HasCapability(caps.CAP_TPL_LIST_ANY) { - // Returns all templates - return c.JSONPretty(http.StatusOK, r.tpl.GetTemplates(func(t vms.Template) bool { return true }), " ") - } else if user.HasCapability(caps.CAP_TPL_LIST) { - // Returns templates owned by the logged user and public templates. - return c.JSONPretty(http.StatusOK, - r.tpl.GetTemplates(func(t vms.Template) bool { - return t.Owner == user.Email || t.IsPublic() - }), " ") - } else { - return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) - } + // Retrieves logged user from context. + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + + // If the logged user has CAP_TPL_LIST_ANY, lists all templates. + if user.HasCapability(caps.CAP_TPL_LIST_ANY) { + // Returns all templates + return c.JSONPretty(http.StatusOK, r.tpl.GetTemplates(func(t vms.Template) bool { return true }), " ") + } else if user.HasCapability(caps.CAP_TPL_LIST) { + // Returns templates owned by the logged user and public templates. + return c.JSONPretty(http.StatusOK, + r.tpl.GetTemplates(func(t vms.Template) bool { + return t.Owner == user.Email || t.IsPublic() + }), " ") + } else { + return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) + } } // Creates a template from a VM. // Requires CAP_TPL_CREATE. // curl --cacert ca.pem -X POST https://localhost:1077/templates/vm -H 'Content-Type: application/json' -d '{"vmID":"e41f3556-ca24-4658-bd79-8c85bd6bff59","name":"Xubuntu_22.04","access":"public"}' -H "Authorization: Bearer <AccessToken>" func (r *RouterTemplates)CreateTemplateFromVM(c echo.Context) error { - // Retrieves logged user from context. - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - - if !user.HasCapability(caps.CAP_TPL_CREATE) { - return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) - } - - // Deserializes and validates the client's parameters. - type Parameters struct { - VMID uuid.UUID `json:"vmID" validate:"required"` - Name string `json:"name" validate:"required,min=2"` - Access string `json:"access" validate:"required,min=4"` - } - p := new(Parameters) - if err := decodeJson(c, &p); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - // Retrieves the VM from its vmID and checks the logged user can access it. - vm, err := r.vms.GetVM(p.VMID) - if err != nil { - return echo.NewHTTPError(http.StatusNotFound, err.Error()) - } - - // If the user isn't the VM's owner, checks that the required capabilities are met: - // either VM_LIST_ANY or VM_LIST in the VM access - if !vm.IsOwner(user.Email) { - // Check the user has the required capabilities: either VM_LIST_ANY or VM_LIST in the VM access - if !user.HasCapability(caps.CAP_VM_LIST_ANY) { - userCaps, exists := vm.Access[user.Email] - if !exists { - return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) - } - - _, hasAccess := userCaps[caps.CAP_VM_LIST] - if !hasAccess { - return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) - } - } - } - - // Creates a new template from the client's parameters. - template, err := vms.NewTemplateFromVM(p.Name, user.Email, p.Access, &vm) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - // Adds the template. - if err = r.tpl.AddTemplate(template); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } + // Retrieves logged user from context. + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + + if !user.HasCapability(caps.CAP_TPL_CREATE) { + return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) + } + + // Deserializes and validates the client's parameters. + type Parameters struct { + VMID uuid.UUID `json:"vmID" validate:"required"` + Name string `json:"name" validate:"required,min=2"` + Access string `json:"access" validate:"required,min=4"` + } + p := new(Parameters) + if err := decodeJson(c, &p); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + // Retrieves the VM from its vmID and checks the logged user can access it. + vm, err := r.vms.GetVM(p.VMID) + if err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + + // If the user isn't the VM's owner, checks that the required capabilities are met: + // either VM_LIST_ANY or VM_LIST in the VM access + if !vm.IsOwner(user.Email) { + // Check the user has the required capabilities: either VM_LIST_ANY or VM_LIST in the VM access + if !user.HasCapability(caps.CAP_VM_LIST_ANY) { + userCaps, exists := vm.Access[user.Email] + if !exists { + return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) + } + + _, hasAccess := userCaps[caps.CAP_VM_LIST] + if !hasAccess { + return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) + } + } + } + + // Creates a new template from the client's parameters. + template, err := vms.NewTemplateFromVM(p.Name, user.Email, p.Access, &vm) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + // Adds the template. + if err = r.tpl.AddTemplate(template); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } return c.JSONPretty(http.StatusCreated, template, " ") } @@ -126,62 +126,62 @@ func (r *RouterTemplates)CreateTemplateFromVM(c echo.Context) error { // The upload uses exactly the code recipe desribed here: // https://echo.labstack.com/guide/#form-multipartform-data func (r *RouterTemplates)CreateTemplateFromQCOW(c echo.Context) error { - // Retrieves logged user from context. - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - - if !user.HasCapability(caps.CAP_TPL_CREATE) { - return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) - } - - name := c.FormValue("name") - access := c.FormValue("access") - if err = vms.ValidateTemplateAccess(access); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - qcowFile, err := c.FormFile("qcow") - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - src, err := qcowFile.Open() - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - defer src.Close() - - tmpDir := paths.GetInstance().TmpDir - uuid, err := uuid.NewRandom() - if err != nil { - log.Error("Failed creating random UUID: "+err.Error()) - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - uploadedFile := filepath.Join(tmpDir, "upload_"+uuid.String()+".qcow") - dst, err := os.Create(uploadedFile) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - defer dst.Close() - defer os.Remove(uploadedFile) - - if _, err = io.Copy(dst, src); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - - // Creates a new template - template, err := vms.NewTemplateFromQCOW(name, user.Email, access, uploadedFile) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - // Adds the template. - if err = r.tpl.AddTemplate(template); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - - return c.JSONPretty(http.StatusCreated, template, " ") + // Retrieves logged user from context. + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + + if !user.HasCapability(caps.CAP_TPL_CREATE) { + return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) + } + + name := c.FormValue("name") + access := c.FormValue("access") + if err = vms.ValidateTemplateAccess(access); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + qcowFile, err := c.FormFile("qcow") + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + src, err := qcowFile.Open() + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + defer src.Close() + + tmpDir := paths.GetInstance().TmpDir + uuid, err := uuid.NewRandom() + if err != nil { + log.Error("Failed creating random UUID: "+err.Error()) + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + uploadedFile := filepath.Join(tmpDir, "upload_"+uuid.String()+".qcow") + dst, err := os.Create(uploadedFile) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + defer dst.Close() + defer os.Remove(uploadedFile) + + if _, err = io.Copy(dst, src); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + // Creates a new template + template, err := vms.NewTemplateFromQCOW(name, user.Email, access, uploadedFile) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + // Adds the template. + if err = r.tpl.AddTemplate(template); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + return c.JSONPretty(http.StatusCreated, template, " ") } // Deletes a template based on its ID. @@ -191,38 +191,38 @@ func (r *RouterTemplates)CreateTemplateFromQCOW(c echo.Context) error { // Remark: a template can only be deleted if no VM references it! // curl --cacert ca.pem -X DELETE https://localhost:1077/templates/4913a2bb-edfe-4dfe-af53-38197a44523b -H "Authorization: Bearer <AccessToken>" func (r *RouterTemplates)DeleteTemplateByID(c echo.Context) error { - // Retrieves logged user from context. - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - - // Retrieves template ID. - tplID, err := uuid.Parse(c.Param("id")) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - if user.HasCapability(caps.CAP_TPL_DESTROY_ANY) { - if err := r.tpl.DeleteTemplate(tplID, r.vms); err != nil { - return echo.NewHTTPError(http.StatusNotFound, err.Error()) - } - return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") - } else if user.HasCapability(caps.CAP_TPL_DESTROY) { - template, err := r.tpl.GetTemplate(tplID) - if err != nil { - return echo.NewHTTPError(http.StatusNotFound, err.Error()) - } - - if template.Owner == user.Email { - if err := r.tpl.DeleteTemplate(tplID, r.vms); err != nil { - return echo.NewHTTPError(http.StatusNotFound, err.Error()) - } - return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") - } - } - - return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) + // Retrieves logged user from context. + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + + // Retrieves template ID. + tplID, err := uuid.Parse(c.Param("id")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + if user.HasCapability(caps.CAP_TPL_DESTROY_ANY) { + if err := r.tpl.DeleteTemplate(tplID, r.vms); err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") + } else if user.HasCapability(caps.CAP_TPL_DESTROY) { + template, err := r.tpl.GetTemplate(tplID) + if err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + + if template.Owner == user.Email { + if err := r.tpl.DeleteTemplate(tplID, r.vms); err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") + } + } + + return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) } // Edit a template based on its ID. @@ -231,52 +231,52 @@ func (r *RouterTemplates)DeleteTemplateByID(c echo.Context) error { // CAP_TPL_EDIT: only a template owned by the user can be edited. // curl --cacert ca.pem -X PUT https://localhost:1077/templates/4913a2bb-edfe-4dfe-af53-38197a44523b -H "Authorization: Bearer <AccessToken>" func (r *RouterTemplates)EditTemplateByID(c echo.Context) error { - // Retrieves logged user from context. - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - - // Retrieves template ID. - tplID, err := uuid.Parse(c.Param("id")) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - // Deserializes and validates the client's parameters. - // Given these parameters are optional, we can't use a validator on them. - // Validation is performed in templates.EditTemplate() instead. - type Parameters struct { - Name string `json:"name"` - Access string `json:"access"` - } - p := new(Parameters) - - if user.HasCapability(caps.CAP_TPL_EDIT_ANY) { - if err := decodeJson(c, &p); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - if err := r.tpl.EditTemplate(tplID, p.Name, p.Access); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") - } else if user.HasCapability(caps.CAP_TPL_EDIT) { - template, err := r.tpl.GetTemplate(tplID) - if err != nil { - return echo.NewHTTPError(http.StatusNotFound, err.Error()) - } - if template.Owner == user.Email { - if err := decodeJson(c, &p); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - if err := r.tpl.EditTemplate(tplID, p.Name, p.Access); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") - } - } - - return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) + // Retrieves logged user from context. + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + + // Retrieves template ID. + tplID, err := uuid.Parse(c.Param("id")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + // Deserializes and validates the client's parameters. + // Given these parameters are optional, we can't use a validator on them. + // Validation is performed in templates.EditTemplate() instead. + type Parameters struct { + Name string `json:"name"` + Access string `json:"access"` + } + p := new(Parameters) + + if user.HasCapability(caps.CAP_TPL_EDIT_ANY) { + if err := decodeJson(c, &p); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + if err := r.tpl.EditTemplate(tplID, p.Name, p.Access); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") + } else if user.HasCapability(caps.CAP_TPL_EDIT) { + template, err := r.tpl.GetTemplate(tplID) + if err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + if template.Owner == user.Email { + if err := decodeJson(c, &p); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + if err := r.tpl.EditTemplate(tplID, p.Name, p.Access); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") + } + } + + return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) } // Exports a template's disk image. @@ -285,34 +285,34 @@ func (r *RouterTemplates)EditTemplateByID(c echo.Context) error { // CAP_TPL_READFS: only a template owned by the user can have its disk exported. // curl --cacert ca.pem -X GET https://localhost:1077/templates/4913a2bb-edfe-4dfe-af53-38197a44523b/disk -H "Authorization: Bearer <AccessToken>" --output disk.qcow func (r *RouterTemplates)ExportDisk(c echo.Context) error { - // Retrieves logged user from context. - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - - // Retrieves template ID. - tplID, err := uuid.Parse(c.Param("id")) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - if user.HasCapability(caps.CAP_TPL_READFS_ANY) { - t, err := r.tpl.GetTemplate(tplID) - if err != nil { - return echo.NewHTTPError(http.StatusNotFound, err.Error()) - } - return c.File(t.GetTemplateDiskPath()) - - } else if user.HasCapability(caps.CAP_TPL_READFS) { - t, err := r.tpl.GetTemplate(tplID) - if err != nil { - return echo.NewHTTPError(http.StatusNotFound, err.Error()) - } - if t.IsPublic() || t.Owner == user.Email { - return c.File(t.GetTemplateDiskPath()) - } - } - - return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) + // Retrieves logged user from context. + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + + // Retrieves template ID. + tplID, err := uuid.Parse(c.Param("id")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + if user.HasCapability(caps.CAP_TPL_READFS_ANY) { + t, err := r.tpl.GetTemplate(tplID) + if err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + return c.File(t.GetTemplateDiskPath()) + + } else if user.HasCapability(caps.CAP_TPL_READFS) { + t, err := r.tpl.GetTemplate(tplID) + if err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + if t.IsPublic() || t.Owner == user.Email { + return c.File(t.GetTemplateDiskPath()) + } + } + + return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) } diff --git a/src/router/routerToken.go b/src/router/routerToken.go index 995ed86e5fb905812243ee267c02dd53d59955be..99d4398eda6c8d881bb0b45804ec0667687276e2 100644 --- a/src/router/routerToken.go +++ b/src/router/routerToken.go @@ -1,32 +1,32 @@ package router import ( - "net/http" - "nexus-server/users" + "net/http" + "nexus-server/users" "github.com/labstack/echo/v4" ) type RouterToken struct { - users *users.Users + users *users.Users } func NewRouterToken() *RouterToken { - return &RouterToken{ users.GetUsersInstance() } + return &RouterToken{ users.GetUsersInstance() } } // Obtains a new access token. // curl --cacert ca.pem -X GET http://localhost:1077/token/refresh -H "Authorization: Bearer <AccessToken>" func (r *RouterToken)GetRefreshToken(c echo.Context) error { - // Retrieves logged user from context. - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } + // Retrieves logged user from context. + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } - token, err := createToken(user.Email) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } + token, err := createToken(user.Email) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } - return c.JSON(http.StatusOK, echo.Map{"token": token}) + return c.JSON(http.StatusOK, echo.Map{"token": token}) } diff --git a/src/router/routerUsers.go b/src/router/routerUsers.go index ef618d14d186820ca75be62b412386bebe429fc4..cd713b57b9917812f010ab58f35f1ba2110689f8 100644 --- a/src/router/routerUsers.go +++ b/src/router/routerUsers.go @@ -1,197 +1,197 @@ package router import ( - "net/http" - "nexus-server/caps" - "nexus-server/users" - "nexus-server/utils" - "github.com/labstack/echo/v4" - "github.com/go-playground/validator/v10" + "net/http" + "nexus-server/caps" + "nexus-server/users" + "nexus-server/utils" + "github.com/labstack/echo/v4" + "github.com/go-playground/validator/v10" ) type RouterUsers struct { - users *users.Users + users *users.Users } func NewRouterUsers() *RouterUsers { - return &RouterUsers{users.GetUsersInstance()} + return &RouterUsers{users.GetUsersInstance()} } // Lists users. // Requires CAP_USER_LIST. // curl --cacert ca.pem -X GET http://localhost:1077/users -H "Authorization: Bearer <AccessToken>" func (r *RouterUsers) GetUsers(c echo.Context) error { - // Retrieves logged user from context. - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - - if !user.HasCapability(caps.CAP_USER_LIST) { - return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) - } - - list := []users.UserWithoutPwd{} - for _, u := range r.users.GetUsers() { - user := users.UserWithoutPwd{u.Email, u.FirstName, u.LastName, u.Caps} - list = append(list, user) - } - - return c.JSONPretty(http.StatusOK, list, " ") + // Retrieves logged user from context. + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + + if !user.HasCapability(caps.CAP_USER_LIST) { + return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) + } + + list := []users.UserWithoutPwd{} + for _, u := range r.users.GetUsers() { + user := users.UserWithoutPwd{u.Email, u.FirstName, u.LastName, u.Caps} + list = append(list, user) + } + + return c.JSONPretty(http.StatusOK, list, " ") } // Retrieves the logged (authententicated) user. // curl --cacert ca.pem -X GET http://localhost:1077/users/me -H "Authorization: Bearer <AccessToken>" func (r *RouterUsers) GetLoggedUser(c echo.Context) error { - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } - return c.JSONPretty(http.StatusOK, user, " ") + return c.JSONPretty(http.StatusOK, user, " ") } // Deletes a user by its ID. // Requires CAP_USER_DESTROY. // curl --cacert ca.pem -X DELETE http://localhost:1077/users/janedoes@nexus.org -H "Authorization: Bearer <AccessToken>" func (r *RouterUsers) DeleteUserByEmail(c echo.Context) error { - // Retrieves logged user from context. - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - - if !user.HasCapability(caps.CAP_USER_DESTROY) { - return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) - } - - userToDelete, err := r.users.GetUserByEmail(c.Param("email")) - if err != nil { - return echo.NewHTTPError(http.StatusNotFound, err.Error()) - } - - if err = r.users.DeleteUserByEmail(userToDelete.Email); err != nil { - return echo.NewHTTPError(http.StatusNotFound, err.Error()) - } - - return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") + // Retrieves logged user from context. + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + + if !user.HasCapability(caps.CAP_USER_DESTROY) { + return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) + } + + userToDelete, err := r.users.GetUserByEmail(c.Param("email")) + if err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + + if err = r.users.DeleteUserByEmail(userToDelete.Email); err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + + return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") } // Creates a user. // Requires CAP_USER_CREATE. // curl --cacert ca.pem -X POST http://localhost:1077/users -H 'Content-Type: application/json' -d '{"email":"johndoe@nexus.org","firstName":"John","lastName":"Doe","pwd":"pipomolo","caps":{"USER_LIST":1,"VM_CREATE":1, "VM_SET_ACCESS":1}}' -H "Authorization: Bearer <AccessToken>" func (r *RouterUsers) CreateUser(c echo.Context) error { - // Retrieves logged user from context. - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } + // Retrieves logged user from context. + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } - if !user.HasCapability(caps.CAP_USER_CREATE) { - return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) - } + if !user.HasCapability(caps.CAP_USER_CREATE) { + return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) + } - // The client's parameters match exactly the User structure, thus - // creates a new empty user to deserialize the client's parameters into. - newUser := users.NewEmptyUser() + // The client's parameters match exactly the User structure, thus + // creates a new empty user to deserialize the client's parameters into. + newUser := users.NewEmptyUser() - // Deserializes the JSON body and checks its validity. - if err := decodeJson(c, &newUser); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } + // Deserializes the JSON body and checks its validity. + if err := decodeJson(c, &newUser); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } - if err = newUser.Validate(); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } + if err = newUser.Validate(); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } - newUser.Pwd = utils.HashPassword(newUser.Pwd) + newUser.Pwd = utils.HashPassword(newUser.Pwd) - // Adds user and updates users file. - if err = r.users.AddUser(newUser); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } + // Adds user and updates users file. + if err = r.users.AddUser(newUser); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } - return c.JSONPretty(http.StatusCreated, newUser, " ") + return c.JSONPretty(http.StatusCreated, newUser, " ") } // Update a user's capabilities. // Requires CAP_USER_SET_CAPS. // curl --cacert ca.pem -X PUT http://localhost:1077/users/janedoes@nexus.org/caps -H 'Content-Type: application/json' -d '{"caps":{"USER_LIST":1,"VM_CREATE":1, "VM_SET_ACCESS":1}}' -H "Authorization: Bearer <AccessToken>" func (r *RouterUsers) SetUserCaps(c echo.Context) error { - // Retrieves logged user from context. - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - - if !user.HasCapability(caps.CAP_USER_SET_CAPS) { - return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) - } - - // Deserializes and validates the client's parameters. - type Parameters struct { - Caps caps.Capabilities - } - params := &Parameters{make(caps.Capabilities)} - - if err := decodeJson(c, ¶ms); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - // Checks capabilities are valid. - if err := caps.ValidateUserCaps(params.Caps); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - // Retrieves the user for which the capabilities must be udpated. - userToUpdate, err := r.users.GetUserByEmail(c.Param("email")) - if err != nil { - return echo.NewHTTPError(http.StatusNotFound, err.Error()) - } - - // Updates the user's capabilities. - userToUpdate.Caps = params.Caps - - // Updates user and saves the new user file. - if err = r.users.UpdateUser(&userToUpdate); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") + // Retrieves logged user from context. + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + + if !user.HasCapability(caps.CAP_USER_SET_CAPS) { + return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) + } + + // Deserializes and validates the client's parameters. + type Parameters struct { + Caps caps.Capabilities + } + params := &Parameters{make(caps.Capabilities)} + + if err := decodeJson(c, ¶ms); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + // Checks capabilities are valid. + if err := caps.ValidateUserCaps(params.Caps); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + // Retrieves the user for which the capabilities must be udpated. + userToUpdate, err := r.users.GetUserByEmail(c.Param("email")) + if err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + + // Updates the user's capabilities. + userToUpdate.Caps = params.Caps + + // Updates user and saves the new user file. + if err = r.users.UpdateUser(&userToUpdate); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") } // Update the logged user's password. // curl --cacert ca.pem -X PUT http://localhost:1077/users/pwd -H 'Content-Type: application/json' -d '{"pwd": "new_password"}' -H "Authorization: Bearer <AccessToken>" func (r *RouterUsers) SetUserPwd(c echo.Context) error { - // Deserializes the client's parameters. - type Parameters struct { - Pwd string `json:"pwd" validate:"required,min=8"` - } - params := &Parameters{} - - // Deserializes the JSON body and checks its validity. - if err := decodeJson(c, ¶ms); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - // Checks the new password is valid. - if err := validator.New().Struct(params); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - // Retrieves the logged user for which the password must be udpated. - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - - // Hashes and udpates the user's password. - user.Pwd = utils.HashPassword(params.Pwd) - - // Updates user and saves the new user file. - if err = r.users.UpdateUser(user); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") + // Deserializes the client's parameters. + type Parameters struct { + Pwd string `json:"pwd" validate:"required,min=8"` + } + params := &Parameters{} + + // Deserializes the JSON body and checks its validity. + if err := decodeJson(c, ¶ms); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + // Checks the new password is valid. + if err := validator.New().Struct(params); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + // Retrieves the logged user for which the password must be udpated. + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + + // Hashes and udpates the user's password. + user.Pwd = utils.HashPassword(params.Pwd) + + // Updates user and saves the new user file. + if err = r.users.UpdateUser(user); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") } diff --git a/src/router/routerVMs.go b/src/router/routerVMs.go index 0fb2cd992bf72aed64ec1f4d0eaa5f695de8a9b0..964d35a3151654a8c8be43ff3b8247e443058293 100644 --- a/src/router/routerVMs.go +++ b/src/router/routerVMs.go @@ -1,31 +1,31 @@ package router import ( - "io" - "os" - "net/http" - "path/filepath" - "nexus-server/vms" - "nexus-server/caps" - "nexus-server/users" - "nexus-server/paths" - "github.com/google/uuid" - "github.com/labstack/echo/v4" - "github.com/go-playground/validator/v10" + "io" + "os" + "net/http" + "path/filepath" + "nexus-server/vms" + "nexus-server/caps" + "nexus-server/users" + "nexus-server/paths" + "github.com/google/uuid" + "github.com/labstack/echo/v4" + "github.com/go-playground/validator/v10" ) type ( - RouterVMs struct { - users *users.Users - vms *vms.VMs - } + RouterVMs struct { + users *users.Users + vms *vms.VMs + } - vmActionFn func(c echo.Context, vm *vms.VM) error - vmsListableFn func(c echo.Context, vm *vms.VM) error + vmActionFn func(c echo.Context, vm *vms.VM) error + vmsListableFn func(c echo.Context, vm *vms.VM) error ) func NewRouterVMs() *RouterVMs { - return &RouterVMs{ users: users.GetUsersInstance(), vms: vms.GetVMsInstance() } + return &RouterVMs{ users: users.GetUsersInstance(), vms: vms.GetVMsInstance() } } // Returns VMs that can be listed. @@ -34,9 +34,9 @@ func NewRouterVMs() *RouterVMs { // VM access cap: CAP_VM_LIST: returns all VMs with this cap for the logged user. // curl --cacert ca.pem -X GET https://localhost:1077/vms -H "Authorization: Bearer <AccessToken>" func (r *RouterVMs)GetListableVMs(c echo.Context) error { - return r.performVMsList(c, caps.CAP_VM_LIST_ANY, caps.CAP_VM_LIST, func(vm vms.VM) bool { - return true - }) + return r.performVMsList(c, caps.CAP_VM_LIST_ANY, caps.CAP_VM_LIST, func(vm vms.VM) bool { + return true + }) } // Returns VMs that are running and that can be attached to. @@ -45,9 +45,9 @@ func (r *RouterVMs)GetListableVMs(c echo.Context) error { // VM access cap: CAP_VM_LIST: returns all running VMs with this cap for the logged user. // curl --cacert ca.pem -X GET https://localhost:1077/vms/attach -H "Authorization: Bearer <AccessToken>" func (r *RouterVMs)GetAttachableVMs(c echo.Context) error { - return r.performVMsList(c, caps.CAP_VM_LIST_ANY, caps.CAP_VM_LIST, func(vm vms.VM) bool { - return vm.IsRunning() - }) + return r.performVMsList(c, caps.CAP_VM_LIST_ANY, caps.CAP_VM_LIST, func(vm vms.VM) bool { + return vm.IsRunning() + }) } // Returns VMs that are stopped and that can be deleted. @@ -56,9 +56,9 @@ func (r *RouterVMs)GetAttachableVMs(c echo.Context) error { // VM access cap: CAP_VM_DESTROY: returns all VMs with this cap for the logged user. // curl --cacert ca.pem -X GET https://localhost:1077/vms/del -H "Authorization: Bearer <AccessToken>" func (r *RouterVMs)GetDeletableVMs(c echo.Context) error { - return r.performVMsList(c, caps.CAP_VM_DESTROY_ANY, caps.CAP_VM_DESTROY, func(vm vms.VM) bool { - return !vm.IsRunning() - }) + return r.performVMsList(c, caps.CAP_VM_DESTROY_ANY, caps.CAP_VM_DESTROY, func(vm vms.VM) bool { + return !vm.IsRunning() + }) } // Returns VMs that are stopped and that can be started. @@ -67,9 +67,9 @@ func (r *RouterVMs)GetDeletableVMs(c echo.Context) error { // VM access cap: CAP_VM_START: returns all VMs with this cap for the logged user. // curl --cacert ca.pem -X GET https://localhost:1077/vms/start -H "Authorization: Bearer <AccessToken>" func (r *RouterVMs)GetStartableVMs(c echo.Context) error { - return r.performVMsList(c, caps.CAP_VM_START_ANY, caps.CAP_VM_START, func(vm vms.VM) bool { - return !vm.IsRunning() - }) + return r.performVMsList(c, caps.CAP_VM_START_ANY, caps.CAP_VM_START, func(vm vms.VM) bool { + return !vm.IsRunning() + }) } // Returns VMs that are running and that can be stopped. @@ -78,9 +78,9 @@ func (r *RouterVMs)GetStartableVMs(c echo.Context) error { // VM access cap: CAP_VM_STOP: returns all VMs with this cap for the logged user. // curl --cacert ca.pem -X GET https://localhost:1077/vms/stop -H "Authorization: Bearer <AccessToken>" func (r *RouterVMs)GetStoppableVMs(c echo.Context) error { - return r.performVMsList(c, caps.CAP_VM_STOP_ANY, caps.CAP_VM_STOP, func(vm vms.VM) bool { - return vm.IsRunning() - }) + return r.performVMsList(c, caps.CAP_VM_STOP_ANY, caps.CAP_VM_STOP, func(vm vms.VM) bool { + return vm.IsRunning() + }) } // Returns VMs that are running and that can be rebooted. @@ -89,9 +89,9 @@ func (r *RouterVMs)GetStoppableVMs(c echo.Context) error { // 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() - }) + 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. @@ -100,9 +100,9 @@ func (r *RouterVMs)GetRebootableVMs(c echo.Context) error { // VM access cap: CAP_VM_EDIT: returns all VMs with this cap for the logged user. // curl --cacert ca.pem -X GET https://localhost:1077/vms/edit -H "Authorization: Bearer <AccessToken>" func (r *RouterVMs)GetEditableVMs(c echo.Context) error { - return r.performVMsList(c, caps.CAP_VM_EDIT_ANY, caps.CAP_VM_EDIT, func(vm vms.VM) bool { - return true - }) + return r.performVMsList(c, caps.CAP_VM_EDIT_ANY, caps.CAP_VM_EDIT, func(vm vms.VM) bool { + return true + }) } // Returns VMs that can have their access set or deleted. @@ -111,33 +111,33 @@ func (r *RouterVMs)GetEditableVMs(c echo.Context) error { // VM access cap: CAP_VM_SET_ACCESS // curl --cacert ca.pem -X GET https://localhost:1077/vms/editaccess -H "Authorization: Bearer <AccessToken>" func (r *RouterVMs)GetEditableVMAccessVMs(c echo.Context) error { - // Retrieves logged user from context. - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - - return c.JSONPretty(http.StatusOK, r.vms.GetNetworkSerializedVMs( - func(vm vms.VM) bool { - // First, checks if the user is the VM's owner - if vm.IsOwner(user.Email) { - return true - } else { - // Then, checks that user has CAP_VM_SET_ACCESS and also that - // VM access has CAP_VM_SET_ACCESS set for the user - if user.HasCapability(caps.CAP_VM_SET_ACCESS) { - capabilities, exists := vm.Access[user.Email] - if exists { - _, visible := capabilities[caps.CAP_VM_SET_ACCESS] - return visible - } else { - return false - } - } else { - return false - } - } - }), " ") + // Retrieves logged user from context. + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + + return c.JSONPretty(http.StatusOK, r.vms.GetNetworkSerializedVMs( + func(vm vms.VM) bool { + // First, checks if the user is the VM's owner + if vm.IsOwner(user.Email) { + return true + } else { + // Then, checks that user has CAP_VM_SET_ACCESS and also that + // VM access has CAP_VM_SET_ACCESS set for the user + if user.HasCapability(caps.CAP_VM_SET_ACCESS) { + capabilities, exists := vm.Access[user.Email] + if exists { + _, visible := capabilities[caps.CAP_VM_SET_ACCESS] + return visible + } else { + return false + } + } else { + return false + } + } + }), " ") } // Returns VMs that can have a directory exported. @@ -146,9 +146,9 @@ func (r *RouterVMs)GetEditableVMAccessVMs(c echo.Context) error { // VM access cap: VM_READFS: returns all VMs with this cap for the logged user. // curl --cacert ca.pem -X GET https://localhost:1077/vms/exportdir -H "Authorization: Bearer <AccessToken>" func (r *RouterVMs)GetDirExportableVMs(c echo.Context) error { - return r.performVMsList(c, caps.CAP_VM_READFS_ANY, caps.CAP_VM_READFS, func(vm vms.VM) bool { - return true - }) + return r.performVMsList(c, caps.CAP_VM_READFS_ANY, caps.CAP_VM_READFS, func(vm vms.VM) bool { + return true + }) } // Returns VMs that can have files imported (i.e. copied) into their filesystem. @@ -157,9 +157,9 @@ func (r *RouterVMs)GetDirExportableVMs(c echo.Context) error { // VM access cap: VM_WRITEFS: returns all VMs with this cap for the logged user. // curl --cacert ca.pem -X GET https://localhost:1077/vms/importfiles -H "Authorization: Bearer <AccessToken>" func (r *RouterVMs)GetFilesImportableVMs(c echo.Context) error { - return r.performVMsList(c, caps.CAP_VM_WRITEFS_ANY, caps.CAP_VM_WRITEFS, func(vm vms.VM) bool { - return true - }) + return r.performVMsList(c, caps.CAP_VM_WRITEFS_ANY, caps.CAP_VM_WRITEFS, func(vm vms.VM) bool { + return true + }) } // Creates a VM. @@ -167,40 +167,40 @@ func (r *RouterVMs)GetFilesImportableVMs(c echo.Context) error { // User cap: CAP_VM_CREATE. // curl --cacert ca.pem -X POST https://localhost:1077/vms -H 'Content-Type: application/json' -d '{"name":"Systems Programming 2022","cpus":2,"ram":4096,"nic":"none","template":"Xubuntu_22.04"}' -H "Authorization: Bearer <AccessToken>" func (r *RouterVMs)CreateVM(c echo.Context) error { - // Retrieves logged user from context. - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - - if !user.HasCapability(caps.CAP_VM_CREATE) { - return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) - } - - // Deserializes and validates client's parameters. - type Parameters struct { - Name string `json:"name" validate:"required,min=4,max=256"` - Cpus int `json:"cpus" validate:"required,gte=1,lte=16"` - Ram int `json:"ram" validate:"required,gte=512,lte=32768"` // in MB - Nic vms.NicType `json:"nic" validate:"required"` // "none" or "user" - TemplateID uuid.UUID `json:"templateID" validate:"required"` - } - p := new(Parameters) - if err := decodeJson(c, &p); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - // Creates a new VM from the client's parameters. - vm, err := vms.NewVM(user.Email, p.Name, p.Cpus, p.Ram, p.Nic, p.TemplateID, user.Email) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - if err = r.vms.AddVM(vm); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - return c.JSONPretty(http.StatusCreated, vm.SerializeToNetwork(), " ") + // Retrieves logged user from context. + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + + if !user.HasCapability(caps.CAP_VM_CREATE) { + return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) + } + + // Deserializes and validates client's parameters. + type Parameters struct { + Name string `json:"name" validate:"required,min=4,max=256"` + Cpus int `json:"cpus" validate:"required,gte=1,lte=16"` + Ram int `json:"ram" validate:"required,gte=512,lte=32768"` // in MB + Nic vms.NicType `json:"nic" validate:"required"` // "none" or "user" + TemplateID uuid.UUID `json:"templateID" validate:"required"` + } + p := new(Parameters) + if err := decodeJson(c, &p); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + // Creates a new VM from the client's parameters. + vm, err := vms.NewVM(user.Email, p.Name, p.Cpus, p.Ram, p.Nic, p.TemplateID, user.Email) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + if err = r.vms.AddVM(vm); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + return c.JSONPretty(http.StatusCreated, vm.SerializeToNetwork(), " ") } // Deletes a VM based on its ID. @@ -209,12 +209,12 @@ func (r *RouterVMs)CreateVM(c echo.Context) error { // VM access cap: CAP_VM_DESTROY: any of the VMs with this cap for the logged user can be deleted. // curl --cacert ca.pem -X DELETE https://localhost:1077/vms/e41f3556-ca24-4658-bd79-8c85bd6bff59 -H "Authorization: Bearer <AccessToken>" func (r *RouterVMs)DeleteVMByID(c echo.Context) error { - return r.performVMAction(c, caps.CAP_VM_DESTROY_ANY, caps.CAP_VM_DESTROY, func(c echo.Context, vm *vms.VM) error { - if err := r.vms.DeleteVM(vm.ID); err != nil { - return echo.NewHTTPError(http.StatusNotFound, err.Error()) - } - return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") - }) + return r.performVMAction(c, caps.CAP_VM_DESTROY_ANY, caps.CAP_VM_DESTROY, func(c echo.Context, vm *vms.VM) error { + if err := r.vms.DeleteVM(vm.ID); err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") + }) } // Starts a VM based on its ID. @@ -223,13 +223,13 @@ func (r *RouterVMs)DeleteVMByID(c echo.Context) error { // 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/start -H "Authorization: Bearer <AccessToken>" func (r *RouterVMs)StartVM(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 { - _, _, err := r.vms.StartVM(vm.ID) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") - }) + return r.performVMAction(c, caps.CAP_VM_START_ANY, caps.CAP_VM_START, func(c echo.Context, vm *vms.VM) error { + _, _, err := r.vms.StartVM(vm.ID) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") + }) } // Kills a VM based on its ID. @@ -238,12 +238,12 @@ func (r *RouterVMs)StartVM(c echo.Context) error { // 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)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.KillVM(vm.ID); err != nil { - return echo.NewHTTPError(http.StatusNotFound, err.Error()) - } - return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") - }) + 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.KillVM(vm.ID); err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") + }) } // Gracefully shutdown a VM based on its ID. @@ -252,12 +252,12 @@ func (r *RouterVMs)KillVM(c echo.Context) error { // 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 { - if err := r.vms.ShutdownVM(vm.ID); err != nil { - return echo.NewHTTPError(http.StatusNotFound, err.Error()) - } - return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") - }) + 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.ShutdownVM(vm.ID); err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") + }) } // Reboot a VM based on its ID. @@ -266,12 +266,12 @@ func (r *RouterVMs)ShutdownVM(c echo.Context) error { // 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"), " ") - }) + 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 @@ -280,25 +280,25 @@ func (r *RouterVMs)RebootVM(c echo.Context) error { // VM access cap: CAP_VM_EDIT: any of the VMs with this cap for the logged user can be edited. // curl --cacert ca.pem -X PUT https://localhost:1077/vms/e41f3556-ca24-4658-bd79-8c85bd6bff59 -H 'Content-Type: application/json' -d '{"name":"Edited VM","cpus":1,"ram":2048,"nic":"user"}' -H "Authorization: Bearer <AccessToken>" func (r *RouterVMs)EditVMByID(c echo.Context) error { - return r.performVMAction(c, caps.CAP_VM_EDIT_ANY, caps.CAP_VM_EDIT, func(c echo.Context, vm *vms.VM) error { - // Deserializes and validates the client's parameters. - // Given these parameters are optional, we can't use a validator on them. - // Validation is performed in vm.EditVM() instead. - type Parameters struct { - Name string `json:"name"` - Cpus int `json:"cpus"` - Ram int `json:"ram"` // in MB - Nic vms.NicType `json:"nic"` - } - p := new(Parameters) - if err := decodeJson(c, &p); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - if err := r.vms.EditVM(vm.ID, p.Name, p.Cpus, p.Ram, p.Nic); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") - }) + return r.performVMAction(c, caps.CAP_VM_EDIT_ANY, caps.CAP_VM_EDIT, func(c echo.Context, vm *vms.VM) error { + // Deserializes and validates the client's parameters. + // Given these parameters are optional, we can't use a validator on them. + // Validation is performed in vm.EditVM() instead. + type Parameters struct { + Name string `json:"name"` + Cpus int `json:"cpus"` + Ram int `json:"ram"` // in MB + Nic vms.NicType `json:"nic"` + } + p := new(Parameters) + if err := decodeJson(c, &p); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + if err := r.vms.EditVM(vm.ID, p.Name, p.Cpus, p.Ram, p.Nic); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") + }) } // Set a VM access for a given user. @@ -307,56 +307,56 @@ func (r *RouterVMs)EditVMByID(c echo.Context) error { // VM access cap: CAP_VM_SET_ACCESS // curl --cacert ca.pem -X PUT https://localhost:1077/vms/e41f3556-ca24-4658-bd79-8c85bd6bff59/caps/janedoes@nexus.org -H 'Content-Type: application/json' -d '{"caps":{"VM_LIST":1,"VM_START":1,"VM_STOP":1}}' -H "Authorization: Bearer <AccessToken>" func (r *RouterVMs)SetVMAccessForUser(c echo.Context) error { - // Retrieves logged user from context and checks she/he has sufficient capabilities. - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - - // Checks the user for which to set the VM Access actually exists. - email := c.Param("email") - _, err = r.users.GetUserByEmail(email) - if err != nil { - return echo.NewHTTPError(http.StatusNotFound, err.Error()) - } - - // Retrieves the vmID of the VM to modify. - vmID, err := uuid.Parse(c.Param("id")) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - // Retrieves the VM to modify. - vm, err := r.vms.GetVM(vmID) - if err != nil { - return echo.NewHTTPError(http.StatusNotFound, err.Error()) - } - - // First, check that the logged user is the VM's owner. - if !vm.IsOwner(user.Email) { - // Next, checks the logged user has the VM_SET_ACCESS capability. - if !user.HasCapability(caps.CAP_VM_SET_ACCESS) { - return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) - } - } - - // Deserializes and validates the client's parameters. - type Parameters struct { - Access caps.Capabilities `json:"access" validate:"required"` - } - params := new(Parameters) - if err := decodeJson(c, ¶ms); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - if err := validator.New().Struct(params); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - if err = r.vms.SetVMAccess(vmID, user.Email, email, params.Access); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") + // Retrieves logged user from context and checks she/he has sufficient capabilities. + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + + // Checks the user for which to set the VM Access actually exists. + email := c.Param("email") + _, err = r.users.GetUserByEmail(email) + if err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + + // Retrieves the vmID of the VM to modify. + vmID, err := uuid.Parse(c.Param("id")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + // Retrieves the VM to modify. + vm, err := r.vms.GetVM(vmID) + if err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + + // First, check that the logged user is the VM's owner. + if !vm.IsOwner(user.Email) { + // Next, checks the logged user has the VM_SET_ACCESS capability. + if !user.HasCapability(caps.CAP_VM_SET_ACCESS) { + return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) + } + } + + // Deserializes and validates the client's parameters. + type Parameters struct { + Access caps.Capabilities `json:"access" validate:"required"` + } + params := new(Parameters) + if err := decodeJson(c, ¶ms); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + if err := validator.New().Struct(params); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + if err = r.vms.SetVMAccess(vmID, user.Email, email, params.Access); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") } // Delete a VM Access for a given user. @@ -365,41 +365,41 @@ func (r *RouterVMs)SetVMAccessForUser(c echo.Context) error { // VM access cap: CAP_VM_SET_ACCESS // curl --cacert ca.pem -X DELETE https://localhost:1077/vms/e41f3556-ca24-4658-bd79-8c85bd6bff59/caps/janedoes@nexus.org -H 'Content-Type: application/json' -H "Authorization: Bearer <AccessToken>" func (r *RouterVMs)DeleteVMAccessForUser(c echo.Context) error { - // Retrieves logged user from context and checks she/he has sufficient capabilities. - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - - // Retrieves the vmID of the VM to modify. - vmID, err := uuid.Parse(c.Param("id")) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - // Retrieves the VM to modify. - vm, err := r.vms.GetVM(vmID) - if err != nil { - return echo.NewHTTPError(http.StatusNotFound, err.Error()) - } - - // Does not check that the user to remove the VM access for actually exists. - // Indeed, it might have been deleted. - email := c.Param("email") - - // First, check that the logged user is the VM's owner. - if !vm.IsOwner(user.Email) { - // Next, checks the logged user has the VM_SET_ACCESS capability. - if !user.HasCapability(caps.CAP_VM_SET_ACCESS) { - return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) - } - } - - if err = r.vms.DeleteVMAccess(vmID, user.Email, email); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") + // Retrieves logged user from context and checks she/he has sufficient capabilities. + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + + // Retrieves the vmID of the VM to modify. + vmID, err := uuid.Parse(c.Param("id")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + // Retrieves the VM to modify. + vm, err := r.vms.GetVM(vmID) + if err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + + // Does not check that the user to remove the VM access for actually exists. + // Indeed, it might have been deleted. + email := c.Param("email") + + // First, check that the logged user is the VM's owner. + if !vm.IsOwner(user.Email) { + // Next, checks the logged user has the VM_SET_ACCESS capability. + if !user.HasCapability(caps.CAP_VM_SET_ACCESS) { + return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) + } + } + + if err = r.vms.DeleteVMAccess(vmID, user.Email, email); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + return c.JSONPretty(http.StatusOK, jsonMsg("OK"), " ") } // Exports a VM's directory into a compressed archive. @@ -408,33 +408,33 @@ func (r *RouterVMs)DeleteVMAccessForUser(c echo.Context) error { // VM access cap: VM_READFS: any of the VMs with this cap for the logged user can have their filesystem read. // curl --cacert ca.pem -X GET https://localhost:1077/vms/e41f3556-ca24-4658-bd79-8c85bd6bff59/exportdir -H 'Content-Type: application/json' -d '{"dir":"absolute_path_to_dir"}' -H "Authorization: Bearer <AccessToken>" --output dir.tar func (r *RouterVMs)ExportVMDir(c echo.Context) error { - return r.performVMAction(c, caps.CAP_VM_READFS_ANY, caps.CAP_VM_READFS, func(c echo.Context, vm *vms.VM) error { - // Deserializes and validates the client's parameter. - type Parameters struct { - Dir string `json:"dir"` - } - p := new(Parameters) - if err := decodeJson(c, &p); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - // Creates a unique tar filename. - tmpDir := paths.GetInstance().TmpDir - tarGzFile := filepath.Join(tmpDir, "exportdir_"+vm.ID.String()+".tar.gz") - - // Extracts VM's p.Dir directory into tarGzFile on the host. - if err := r.vms.ExportVMFiles(vm, p.Dir, tarGzFile); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Failed extracting VM's dir") - } - - // Sends the archive back to the client and once completed, deletes it. - defer func(file string) { - if err := os.Remove(file); err != nil { - log.Error("Failed removing archive of extracted VM dir: "+err.Error()) - } - }(tarGzFile) - return c.File(tarGzFile) - }) + return r.performVMAction(c, caps.CAP_VM_READFS_ANY, caps.CAP_VM_READFS, func(c echo.Context, vm *vms.VM) error { + // Deserializes and validates the client's parameter. + type Parameters struct { + Dir string `json:"dir"` + } + p := new(Parameters) + if err := decodeJson(c, &p); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + // Creates a unique tar filename. + tmpDir := paths.GetInstance().TmpDir + tarGzFile := filepath.Join(tmpDir, "exportdir_"+vm.ID.String()+".tar.gz") + + // Extracts VM's p.Dir directory into tarGzFile on the host. + if err := r.vms.ExportVMFiles(vm, p.Dir, tarGzFile); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Failed extracting VM's dir") + } + + // Sends the archive back to the client and once completed, deletes it. + defer func(file string) { + if err := os.Remove(file); err != nil { + log.Error("Failed removing archive of extracted VM dir: "+err.Error()) + } + }(tarGzFile) + return c.File(tarGzFile) + }) } // Import files into a VM's filesystem, in a specified directory. The file tree is received in a tar.gz archive. @@ -443,48 +443,48 @@ func (r *RouterVMs)ExportVMDir(c echo.Context) error { // VM access cap: VM_WRITEFS: any of the VMs with this cap for the logged user can import the file tree. // curl --cacert ca.pem -X POST https://localhost:1077/vms/e41f3556-ca24-4658-bd79-8c85bd6bff59/importfiles -H 'Content-Type: multipart/form-data' -F dir="/home/nexus" -F file=@files.tar -H "Authorization: Bearer <AccessToken>" func (r *RouterVMs)ImportFilesToVM(c echo.Context) error { - return r.performVMAction(c, caps.CAP_VM_WRITEFS_ANY, caps.CAP_VM_WRITEFS, func(c echo.Context, vm *vms.VM) error { - // Retrieves the various client arguments. - vmDir := c.FormValue("vmDir") - - // Retrieves the tar.gz archive (uploadedtarGzFile). - tarGzFile, err := c.FormFile("file") - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - - src, err := tarGzFile.Open() - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - defer src.Close() - - tmpDir := paths.GetInstance().TmpDir - uuid, err := uuid.NewRandom() - if err != nil { - log.Error("Failed creating random UUID: "+err.Error()) - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - uploadedtarGzFile := filepath.Join(tmpDir, "upload_"+uuid.String()+".tar") - dst, err := os.Create(uploadedtarGzFile) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - defer dst.Close() - defer os.Remove(uploadedtarGzFile) - - if _, err = io.Copy(dst, src); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - - // Copy the archive's files into the VM - // IMPORTANT: check the VM is NOT running! - if err = r.vms.ImportFilesToVM(vm, uploadedtarGzFile, vmDir); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - return c.JSONPretty(http.StatusCreated, jsonMsg("OK"), " ") - }) + return r.performVMAction(c, caps.CAP_VM_WRITEFS_ANY, caps.CAP_VM_WRITEFS, func(c echo.Context, vm *vms.VM) error { + // Retrieves the various client arguments. + vmDir := c.FormValue("vmDir") + + // Retrieves the tar.gz archive (uploadedtarGzFile). + tarGzFile, err := c.FormFile("file") + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + src, err := tarGzFile.Open() + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + defer src.Close() + + tmpDir := paths.GetInstance().TmpDir + uuid, err := uuid.NewRandom() + if err != nil { + log.Error("Failed creating random UUID: "+err.Error()) + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + uploadedtarGzFile := filepath.Join(tmpDir, "upload_"+uuid.String()+".tar") + dst, err := os.Create(uploadedtarGzFile) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + defer dst.Close() + defer os.Remove(uploadedtarGzFile) + + if _, err = io.Copy(dst, src); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + // Copy the archive's files into the VM + // IMPORTANT: check the VM is NOT running! + if err = r.vms.ImportFilesToVM(vm, uploadedtarGzFile, vmDir); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + return c.JSONPretty(http.StatusCreated, jsonMsg("OK"), " ") + }) } // Helper function that returns a list of serialized VMs that match either: @@ -493,35 +493,35 @@ func (r *RouterVMs)ImportFilesToVM(c echo.Context) error { // - the VM access for the logged user matches the vmAccessCapability capability. // Also, VMs for which cond is false are filtered out. func (r *RouterVMs)performVMsList(c echo.Context, userCapabilityAny, vmAccessCapability string, cond vms.VMKeeperFn) error { - // Retrieves logged user from context. - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - - // If the logged user has the XX_ANY capability (userCapabilityAny), returns all VMs. - if user.HasCapability(userCapabilityAny) { - // Returns all VMs that pass the condition. - return c.JSONPretty(http.StatusOK, r.vms.GetNetworkSerializedVMs(cond), " ") - } else { - // Returns all VMs: - // - owned by the logged user - // - for which the logged user has the specified capability (vmAccessCapability) in the VM's access - return c.JSONPretty(http.StatusOK, r.vms.GetNetworkSerializedVMs( - func(vm vms.VM) bool { - if vm.IsOwner(user.Email) { - return true - } else { - capabilities, exists := vm.Access[user.Email] - if exists { - _, visible := capabilities[vmAccessCapability] - return visible && cond(vm) - } else { - return false - } - } - }), " ") - } + // Retrieves logged user from context. + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + + // If the logged user has the XX_ANY capability (userCapabilityAny), returns all VMs. + if user.HasCapability(userCapabilityAny) { + // Returns all VMs that pass the condition. + return c.JSONPretty(http.StatusOK, r.vms.GetNetworkSerializedVMs(cond), " ") + } else { + // Returns all VMs: + // - owned by the logged user + // - for which the logged user has the specified capability (vmAccessCapability) in the VM's access + return c.JSONPretty(http.StatusOK, r.vms.GetNetworkSerializedVMs( + func(vm vms.VM) bool { + if vm.IsOwner(user.Email) { + return true + } else { + capabilities, exists := vm.Access[user.Email] + if exists { + _, visible := capabilities[vmAccessCapability] + return visible && cond(vm) + } else { + return false + } + } + }), " ") + } } // Helper function that performs an action on a VM based either on: @@ -529,40 +529,40 @@ func (r *RouterVMs)performVMsList(c echo.Context, userCapabilityAny, vmAccessCap // - the logged user has the userCapabilityAny capability. // - the VM access for the logged user matches the vmAccessCapability capability. func (r *RouterVMs)performVMAction(c echo.Context, userCapabilityAny, vmAccessCapability string, action vmActionFn) error { - // Retrieves the VM on which to perform the action. - id, err := uuid.Parse(c.Param("id")) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - vm, err := r.vms.GetVM(id) - if err != nil { - return echo.NewHTTPError(http.StatusNotFound, err.Error()) - } - - // Retrieves the logged user from context. - user, err := getLoggedUser(r.users, c) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) - } - - // First, checks if the user is the VM's owner - if vm.IsOwner(user.Email) { - return action(c, &vm) - } else if user.HasCapability(userCapabilityAny) { - // Next, checks if the user has the XX_ANY capability - return action(c, &vm) - } else { - // Finally, check if the VM access for the logged user matches the required capability - userCaps, exists := vm.Access[user.Email] - if !exists { - return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) - } - - _, hasAccess := userCaps[vmAccessCapability] - if !hasAccess { - return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) - } - - return action(c, &vm) - } + // Retrieves the VM on which to perform the action. + id, err := uuid.Parse(c.Param("id")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + vm, err := r.vms.GetVM(id) + if err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + + // Retrieves the logged user from context. + user, err := getLoggedUser(r.users, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) + } + + // First, checks if the user is the VM's owner + if vm.IsOwner(user.Email) { + return action(c, &vm) + } else if user.HasCapability(userCapabilityAny) { + // Next, checks if the user has the XX_ANY capability + return action(c, &vm) + } else { + // Finally, check if the VM access for the logged user matches the required capability + userCaps, exists := vm.Access[user.Email] + if !exists { + return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) + } + + _, hasAccess := userCaps[vmAccessCapability] + if !hasAccess { + return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps) + } + + return action(c, &vm) + } } diff --git a/src/router/routerVersion.go b/src/router/routerVersion.go index bfad7e390ce8151102a26ef2ed12363f95b68f40..60e9644191b7d6d1a8bcba3b34ff90d90972b735 100644 --- a/src/router/routerVersion.go +++ b/src/router/routerVersion.go @@ -1,8 +1,8 @@ package router import ( - "net/http" - "nexus-server/version" + "net/http" + "nexus-server/version" "github.com/labstack/echo/v4" ) @@ -10,11 +10,11 @@ type RouterVersion struct { } func NewRouterVersion() *RouterVersion { - return &RouterVersion{} + return &RouterVersion{} } // Retrives the version number. // curl --cacert ca.pem -X GET http://localhost:1077/version func (r *RouterVersion)GetVersion(c echo.Context) error { - return c.JSON(http.StatusOK, echo.Map{"version": version.Get().String()}) + return c.JSON(http.StatusOK, echo.Map{"version": version.Get().String()}) } diff --git a/src/users/user.go b/src/users/user.go index a53511d38619884a370e54de87147febf3c1ca69..9f4b2ce85961e00eb4ab9c4026ba50fb85454425 100644 --- a/src/users/user.go +++ b/src/users/user.go @@ -1,56 +1,56 @@ package users import ( - "nexus-server/caps" - "github.com/go-playground/validator/v10" + "nexus-server/caps" + "github.com/go-playground/validator/v10" ) type User struct { - Email string `json:"email" validate:"required,email"` - FirstName string `json:"firstname" validate:"required,min=2,max=32"` - LastName string `json:"lastname" validate:"required,min=2,max=32"` - Pwd string `json:"pwd" validate:"required,min=8"` - Caps caps.Capabilities `json:"caps" validate:"required"` + Email string `json:"email" validate:"required,email"` + FirstName string `json:"firstname" validate:"required,min=2,max=32"` + LastName string `json:"lastname" validate:"required,min=2,max=32"` + Pwd string `json:"pwd" validate:"required,min=8"` + Caps caps.Capabilities `json:"caps" validate:"required"` } type UserWithoutPwd struct { - Email string `json:"email" validate:"required,email"` - FirstName string `json:"firstname" validate:"required,min=2,max=32"` - LastName string `json:"lastname" validate:"required,min=2,max=32"` - Caps caps.Capabilities `json:"caps" validate:"required"` + Email string `json:"email" validate:"required,email"` + FirstName string `json:"firstname" validate:"required,min=2,max=32"` + LastName string `json:"lastname" validate:"required,min=2,max=32"` + Caps caps.Capabilities `json:"caps" validate:"required"` } var dummyUser = User{} // Creates an empty user. func NewEmptyUser() *User { - return &User{Email: "", FirstName: "", LastName: "", Caps: make(caps.Capabilities), Pwd: ""} + return &User{Email: "", FirstName: "", LastName: "", Caps: make(caps.Capabilities), Pwd: ""} } // Checks that the User structure's fields are valid. func (user *User) Validate() error { - // Checks the capabilities are valid - if err := caps.ValidateUserCaps(user.Caps); err != nil { - return err - } + // Checks the capabilities are valid + if err := caps.ValidateUserCaps(user.Caps); err != nil { + return err + } - validate := validator.New() - return validate.Struct(user) + validate := validator.New() + return validate.Struct(user) } // Returns true if user has the specified capability. func (user *User) HasCapability(capability string) bool { - _, exists := user.Caps[capability] - return exists + _, exists := user.Caps[capability] + return exists } func (user *User) GetVMAccessCapabilities() []string { - capabilities := []string{} - for cap, _ := range caps.VMAccessCaps { - _, exists := user.Caps[cap] - if exists { - capabilities = append(capabilities, cap) - } - } - return capabilities + capabilities := []string{} + for cap, _ := range caps.VMAccessCaps { + _, exists := user.Caps[cap] + if exists { + capabilities = append(capabilities, cap) + } + } + return capabilities } diff --git a/src/users/users.go b/src/users/users.go index 6f431c427677942a73166ff8a4ecfd1b3cc4ac5a..396866e22a2e0e30ae520119af71038d96a24a22 100644 --- a/src/users/users.go +++ b/src/users/users.go @@ -1,19 +1,19 @@ package users import ( - "os" - "sort" - "sync" - "errors" - "encoding/json" - "nexus-server/paths" - "nexus-server/logger" + "os" + "sort" + "sync" + "errors" + "encoding/json" + "nexus-server/paths" + "nexus-server/logger" ) type Users struct { - m map[string]User - file string - rwlock *sync.RWMutex + m map[string]User + file string + rwlock *sync.RWMutex } var log = logger.GetInstance() @@ -22,152 +22,152 @@ var users *Users // Returns a Users "singleton". // IMPORTANT: the InitUsers function must have been previously called! func GetUsersInstance() *Users { - return users + return users } // Creates users by reading them from the config directory. func InitUsers() error { - usersFile := paths.GetInstance().UsersFile - users = &Users { m: make(map[string]User), file: usersFile, rwlock: new(sync.RWMutex) } - return users.createUsersFromFile() + usersFile := paths.GetInstance().UsersFile + users = &Users { m: make(map[string]User), file: usersFile, rwlock: new(sync.RWMutex) } + return users.createUsersFromFile() } // Returns all users. // Thread-safe fonction. func (users *Users)GetUsers() []User { - users.rwlock.RLock() - list := users.usersToList() - users.rwlock.RUnlock() - - // Sort users by names - sort.Slice(list, func(i, j int) bool { + users.rwlock.RLock() + list := users.usersToList() + users.rwlock.RUnlock() + + // Sort users by names + sort.Slice(list, func(i, j int) bool { return list[i].LastName < list[j].LastName }) - return list + return list } // Returns a user by her email (unique). // Thread-safe fonction. func (users *Users)GetUserByEmail(email string) (User, error) { - users.rwlock.RLock() - defer users.rwlock.RUnlock() + users.rwlock.RLock() + defer users.rwlock.RUnlock() - for _, u := range users.m { - if email == u.Email { - return u, nil - } - } + for _, u := range users.m { + if email == u.Email { + return u, nil + } + } - return dummyUser, errors.New(email+": user not found") + return dummyUser, errors.New(email+": user not found") } // Deletes a user by its email and upates the user file. // TODO: only deletes a user when no VMs and templates are owned by her? // Thread-safe fonction. func (users *Users)DeleteUserByEmail(email string) error { - users.rwlock.Lock() - defer users.rwlock.Unlock() - key := email - _, exists := users.m[key] - if exists { - delete(users.m, key) - err := users.writeUsersFile() - return err - } - return errors.New(email+": user not found") + users.rwlock.Lock() + defer users.rwlock.Unlock() + key := email + _, exists := users.m[key] + if exists { + delete(users.m, key) + err := users.writeUsersFile() + return err + } + return errors.New(email+": user not found") } // Adds a user and updates the users file. // Thread-safe fonction. func (users *Users)AddUser(user *User) error { - users.rwlock.Lock() - defer users.rwlock.Unlock() - - // Checks the username doesn't already exists. - for _, u := range users.m { - if user.Email == u.Email { - return errors.New("User already exists") - } - } - - key := user.Email - users.m[key] = *user - err := users.writeUsersFile() - return err + users.rwlock.Lock() + defer users.rwlock.Unlock() + + // Checks the username doesn't already exists. + for _, u := range users.m { + if user.Email == u.Email { + return errors.New("User already exists") + } + } + + key := user.Email + users.m[key] = *user + err := users.writeUsersFile() + return err } // Updates a user and updates the user file. // Thread-safe fonction. func (users *Users)UpdateUser(user *User) error { - users.rwlock.Lock() - defer users.rwlock.Unlock() - key := user.Email - _, exists := users.m[key] - if exists { - // Deletes previous user and adds new one - delete(users.m, key) - users.m[key] = *user - err := users.writeUsersFile() - return err - } - return errors.New(key+": user not found") + users.rwlock.Lock() + defer users.rwlock.Unlock() + key := user.Email + _, exists := users.m[key] + if exists { + // Deletes previous user and adds new one + delete(users.m, key) + users.m[key] = *user + err := users.writeUsersFile() + return err + } + return errors.New(key+": user not found") } // Creates users by reading them from the user file. func (users *Users)createUsersFromFile() error { - filein, err := os.OpenFile(users.file, os.O_RDONLY, 0) - if err != nil { + filein, err := os.OpenFile(users.file, os.O_RDONLY, 0) + if err != nil { return errors.New("Failed reading users file: "+err.Error()) - } - defer filein.Close() - - // Decodes the json file into users, then checks: - // - all fields are present - // - extra fields are not allowed - decoder := json.NewDecoder(filein) - decoder.DisallowUnknownFields() - usersAsList := []User{} - if err = decoder.Decode(&usersAsList); err != nil { - return errors.New("Failed decoding users file \""+users.file+"\": "+err.Error()) - } - - // Validates each user entry - for _, u := range usersAsList { - if err = u.Validate(); err != nil { - return errors.New("Failed validating users file \""+users.file+"\": "+err.Error()) - } - // Add the entry to the map of users - key := u.Email - users.m[key] = u - } - - return nil + } + defer filein.Close() + + // Decodes the json file into users, then checks: + // - all fields are present + // - extra fields are not allowed + decoder := json.NewDecoder(filein) + decoder.DisallowUnknownFields() + usersAsList := []User{} + if err = decoder.Decode(&usersAsList); err != nil { + return errors.New("Failed decoding users file \""+users.file+"\": "+err.Error()) + } + + // Validates each user entry + for _, u := range usersAsList { + if err = u.Validate(); err != nil { + return errors.New("Failed validating users file \""+users.file+"\": "+err.Error()) + } + // Add the entry to the map of users + key := u.Email + users.m[key] = u + } + + return nil } // Writes users to file. func (users *Users)writeUsersFile() error { - file, err := os.Create(users.file) - if err != nil { - log.Error("Failed writing users file: "+err.Error()) + file, err := os.Create(users.file) + if err != nil { + log.Error("Failed writing users file: "+err.Error()) return errors.New("Failed writing users file: "+err.Error()) - } - defer file.Close() - - encoder := json.NewEncoder(file) - encoder.SetIndent("", " ") - usersAsList := users.usersToList() - if err = encoder.Encode(&usersAsList); err != nil { - log.Error("Failed encoding users file \""+users.file+"\": "+err.Error()) - return errors.New("Failed encoding users file \""+users.file+"\": "+err.Error()) - } - return nil + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + usersAsList := users.usersToList() + if err = encoder.Encode(&usersAsList); err != nil { + log.Error("Failed encoding users file \""+users.file+"\": "+err.Error()) + return errors.New("Failed encoding users file \""+users.file+"\": "+err.Error()) + } + return nil } // Converts the map of users into a list. func (users *Users)usersToList() []User { - list := []User{} - for _, val := range users.m { - list = append(list, val) - } - return list + list := []User{} + for _, val := range users.m { + list = append(list, val) + } + return list } diff --git a/src/utils/memory.go b/src/utils/memory.go index 70b8c0e4d255f51e29dba54ba7013fad8d5dec46..7ad76396b551a77d3aeff4d0e6528f730902a6ec 100644 --- a/src/utils/memory.go +++ b/src/utils/memory.go @@ -1,10 +1,10 @@ package utils import ( - "os" - "bufio" - "strconv" - "strings" + "os" + "bufio" + "strconv" + "strings" ) // This function returns info about the system's RAM @@ -12,37 +12,37 @@ import ( // Beware: it is Linux SPECIFIC! // Returns total and avaiable memory in MB. func GetRAM() (int, int, error) { - f, err := os.Open("/proc/meminfo") + f, err := os.Open("/proc/meminfo") if err != nil { - return 0, 0, err + return 0, 0, err } - defer f.Close() + defer f.Close() scan := bufio.NewScanner(f) scan.Split(bufio.ScanLines) - var memTotal int = -1 - var memAvail int = -1 - const units = 1024 + var memTotal int = -1 + var memAvail int = -1 + const units = 1024 for scan.Scan() { - s := scan.Text() - fields := strings.Split(s, ":") - attr := fields[0] - vals := strings.Split(strings.TrimSpace(fields[1]), " ") - val := strings.TrimSpace(vals[0]) - attr = strings.TrimSpace(strings.ToLower(attr)) - switch attr { - case "memtotal": - memTotal, _ = strconv.Atoi(val) - memTotal = memTotal/units - case "memavailable": - memAvail, _ = strconv.Atoi(val) - memAvail = memAvail/units - } + s := scan.Text() + fields := strings.Split(s, ":") + attr := fields[0] + vals := strings.Split(strings.TrimSpace(fields[1]), " ") + val := strings.TrimSpace(vals[0]) + attr = strings.TrimSpace(strings.ToLower(attr)) + switch attr { + case "memtotal": + memTotal, _ = strconv.Atoi(val) + memTotal = memTotal/units + case "memavailable": + memAvail, _ = strconv.Atoi(val) + memAvail = memAvail/units + } } - return memTotal, memAvail, nil + return memTotal, memAvail, nil } /* @@ -50,18 +50,18 @@ func GetRAM() (int, int, error) { // Returns: total RAM, shared RAM, buffered RAM, free RAM and possibly an error. // RAM amounts are given in megabytes (MB). func GetRAM() (int, int, int, int, error) { - in := &syscall.Sysinfo_t{} - if err := syscall.Sysinfo(in); err != nil { - return 0, 0, 0, 0, err - } + in := &syscall.Sysinfo_t{} + if err := syscall.Sysinfo(in); err != nil { + return 0, 0, 0, 0, err + } - const MB = 1024*1024 - // On 32-bit systems these fields are uint32 instead of uint64. - // We always convert to uint64 to match signature. - totalRam := int(uint64(in.Totalram)*uint64(in.Unit)/MB) - sharedRam := int(uint64(in.Sharedram)*uint64(in.Unit)/MB) - bufferedRam := int(uint64(in.Bufferram)*uint64(in.Unit)/MB) - freeRam := int(uint64(in.Freeram)*uint64(in.Unit)/MB) - return totalRam, sharedRam, bufferedRam, freeRam, nil + const MB = 1024*1024 + // On 32-bit systems these fields are uint32 instead of uint64. + // We always convert to uint64 to match signature. + totalRam := int(uint64(in.Totalram)*uint64(in.Unit)/MB) + sharedRam := int(uint64(in.Sharedram)*uint64(in.Unit)/MB) + bufferedRam := int(uint64(in.Bufferram)*uint64(in.Unit)/MB) + freeRam := int(uint64(in.Freeram)*uint64(in.Unit)/MB) + return totalRam, sharedRam, bufferedRam, freeRam, nil } */ \ No newline at end of file diff --git a/src/utils/utils.go b/src/utils/utils.go index 32bf340e8e10b622dc58f9c070d74b4b76214663..45cdd1eaa7e377b60c89568db1b11e7311504868 100644 --- a/src/utils/utils.go +++ b/src/utils/utils.go @@ -1,177 +1,177 @@ package utils import ( - "os" - "io" - "net" - "time" - "strings" - "strconv" - "math/rand" - "archive/tar" - "path/filepath" - "nexus-server/logger" - "golang.org/x/crypto/bcrypt" + "os" + "io" + "net" + "time" + "strings" + "strconv" + "math/rand" + "archive/tar" + "path/filepath" + "nexus-server/logger" + "golang.org/x/crypto/bcrypt" ) var log = logger.GetInstance() // Returns the list of subdirectories present in dir. func GetSubDirs(dir string) ([]string, error) { - subDirs := []string{} - - currentDir, err := os.Open(dir) - if err != nil { - return nil, err - } - - // Retrieves all files entries in the directory (0 = all files in the directory). - files, err := currentDir.Readdir(0) - if err != nil { - currentDir.Close() - return nil, err - } - - currentDir.Close() - - // Loop over file entries - for _, f := range(files) { - if f.IsDir() { - subDirs = append(subDirs, filepath.Join(dir, f.Name())) - } - } - - return subDirs, nil + subDirs := []string{} + + currentDir, err := os.Open(dir) + if err != nil { + return nil, err + } + + // Retrieves all files entries in the directory (0 = all files in the directory). + files, err := currentDir.Readdir(0) + if err != nil { + currentDir.Close() + return nil, err + } + + currentDir.Close() + + // Loop over file entries + for _, f := range(files) { + if f.IsDir() { + subDirs = append(subDirs, filepath.Join(dir, f.Name())) + } + } + + return subDirs, nil } // Initializes the random number generator. func RandInit() { - rand.Seed(time.Now().UnixNano()) + rand.Seed(time.Now().UnixNano()) } // Returns an int in the range [min,max] (both inclusive). func Rand(min, max int) int { - return rand.Intn(max-min+1)+min + return rand.Intn(max-min+1)+min } func CopyFiles(source, dest string) error { - src, err := os.Open(source) - if err != nil { - return err - } - defer src.Close() - - dst, err := os.Create(dest) - if err != nil { - return err - } - defer dst.Close() - _, err = io.Copy(dst, src) - - return err + src, err := os.Open(source) + if err != nil { + return err + } + defer src.Close() + + dst, err := os.Create(dest) + if err != nil { + return err + } + defer dst.Close() + _, err = io.Copy(dst, src) + + return err } // Returns a hash of the "clear" password passed in argument. func HashPassword(pwd string) string { - // Uses bcrypt to hash the password. - hashedPwd, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost) - return string(hashedPwd) + // Uses bcrypt to hash the password. + hashedPwd, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost) + return string(hashedPwd) } // Returns a random MAC address as a string (e.g. "b3:30:49:f8:d0:4b"). func RandMacAddress() (string, error) { - buf := make([]byte, 6) - _, err := rand.Read(buf) - if err != nil { - return "", err - } - - // Sets unicast mode (bit 0 to 0) and locally administered addresses (bit 1 to 1) - // For more details, see: https://en.wikipedia.org/wiki/MAC_address#Universal_vs._local_(U/L_bit) - buf[0] &= 0xFE - buf[0] |= 2 - - var mac net.HardwareAddr - mac = append(mac, buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]) - return mac.String(), nil + buf := make([]byte, 6) + _, err := rand.Read(buf) + if err != nil { + return "", err + } + + // Sets unicast mode (bit 0 to 0) and locally administered addresses (bit 1 to 1) + // For more details, see: https://en.wikipedia.org/wiki/MAC_address#Universal_vs._local_(U/L_bit) + buf[0] &= 0xFE + buf[0] |= 2 + + var mac net.HardwareAddr + mac = append(mac, buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]) + return mac.String(), nil } // Returns true if the specified file exists, false otherwise. func FileExists(filename string) bool { - info, err := os.Stat(filename) - if os.IsNotExist(err) { - return false - } - return !info.IsDir() + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() } // Returns true if the specified TCP port is available, false otherwise. func IsPortAvailable(port int) bool { - ln, err := net.Listen("tcp", ":" + strconv.Itoa(port)) - if err != nil { - return false - } - ln.Close() - return true + ln, err := net.Listen("tcp", ":" + strconv.Itoa(port)) + if err != nil { + return false + } + ln.Close() + return true } // Creates the destTarball .tar archive of srcDir and all its files and subdirectories. // This implementation fails if a symlink points to an absolute path that doesn't exist on the host. // Source code slightly modified from: https://golangdocs.com/tar-gzip-in-golang func TarDir(srcDir, destTarball string) error { - tarFile, err := os.Create(destTarball) + tarFile, err := os.Create(destTarball) if err != nil { return err } defer tarFile.Close() - + tarball := tar.NewWriter(tarFile) defer tarball.Close() - + info, err := os.Stat(srcDir) if err != nil { return err } - + var baseDir string if info.IsDir() { baseDir = filepath.Base(srcDir) } - + return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err - } - - if (info.Mode() & os.ModeSymlink) != 0 { - log.Warn("TarDir: skipping symlink: ", path) - return nil - } - - header, err := tar.FileInfoHeader(info, info.Name()) + } + + if (info.Mode() & os.ModeSymlink) != 0 { + log.Warn("TarDir: skipping symlink: ", path) + return nil + } + + header, err := tar.FileInfoHeader(info, info.Name()) if err != nil { return err - } - + } + if baseDir != "" { header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, srcDir)) } - + if err := tarball.WriteHeader(header); err != nil { return err } - + if info.IsDir() { return nil } - + file, err := os.Open(path) if err != nil { return err } defer file.Close() - _, err = io.Copy(tarball, file) + _, err = io.Copy(tarball, file) return err }) } @@ -182,9 +182,9 @@ func Untar(srcTarball, destDir string) error { if err != nil { return err } - defer reader.Close() - - // First pass: reads and creates directories first. + defer reader.Close() + + // First pass: reads and creates directories first. tarDirReader := tar.NewReader(reader) for { header, err := tarDirReader.Next() @@ -192,7 +192,7 @@ func Untar(srcTarball, destDir string) error { break } else if err != nil { return err - } + } path := filepath.Join(destDir, header.Name) info := header.FileInfo() if info.IsDir() { @@ -201,34 +201,34 @@ func Untar(srcTarball, destDir string) error { } continue } - } - - // Seeks to the beginning of the file. - whence := 0 - reader.Seek(0, whence) - - // Second pass: reads and creates files. - tarFileReader := tar.NewReader(reader) + } + + // Seeks to the beginning of the file. + whence := 0 + reader.Seek(0, whence) + + // Second pass: reads and creates files. + tarFileReader := tar.NewReader(reader) for { header, err := tarFileReader.Next() if err == io.EOF { break } else if err != nil { return err - } + } path := filepath.Join(destDir, header.Name) - info := header.FileInfo() - if !info.IsDir() { - file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode()) - if err != nil { - return err - } - defer file.Close() - _, err = io.Copy(file, tarFileReader) - if err != nil { - return err - } - } + info := header.FileInfo() + if !info.IsDir() { + file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode()) + if err != nil { + return err + } + defer file.Close() + _, err = io.Copy(file, tarFileReader) + if err != nil { + return err + } + } } return nil diff --git a/src/version/version.go b/src/version/version.go index 804b0773863e7d8dc074c99231acaa433a71bffd..45abd3ec7d26b8f1c8299766d0dad2dc7bbaf0ae 100644 --- a/src/version/version.go +++ b/src/version/version.go @@ -1,31 +1,31 @@ package version import ( - "strconv" + "strconv" ) const ( - major = 1 - minor = 5 - bugfix = 0 + major = 1 + minor = 5 + bugfix = 0 ) type Version struct { - major int - minor int - bugfix int + major int + minor int + bugfix int } var version Version = FromInt(major, minor, bugfix) func FromInt(major, minor, bugfix int) Version { - return Version{major, minor, bugfix} + return Version{major, minor, bugfix} } func Get() Version { - return version + return version } func (v Version) String() string { - return strconv.Itoa(v.major)+"."+strconv.Itoa(v.minor)+"."+strconv.Itoa(v.bugfix) + return strconv.Itoa(v.major)+"."+strconv.Itoa(v.minor)+"."+strconv.Itoa(v.bugfix) } diff --git a/src/vms/template.go b/src/vms/template.go index 52b4584d632551c2975f030abe717d03a42681e7..6a445b0ceeb59621c06a3af8670d1c482d69541a 100644 --- a/src/vms/template.go +++ b/src/vms/template.go @@ -1,218 +1,218 @@ package vms import ( - "os" - "time" - "errors" - "path/filepath" - "encoding/json" - "nexus-server/exec" - "nexus-server/utils" - "github.com/google/uuid" - "github.com/go-playground/validator/v10" + "os" + "time" + "errors" + "path/filepath" + "encoding/json" + "nexus-server/exec" + "nexus-server/utils" + "github.com/google/uuid" + "github.com/go-playground/validator/v10" ) type Template struct { - ID uuid.UUID `json:"id" validate:"required"` - Name string `json:"name" validate:"required,min=2,max=256"` - Owner string `json:"owner" validate:"required,email"` - Access string `json:"access" validate:"required,min=4,max=16"` // private or public - CreationTime time.Time `json:"creationTime" validate:"required"` + ID uuid.UUID `json:"id" validate:"required"` + Name string `json:"name" validate:"required,min=2,max=256"` + Owner string `json:"owner" validate:"required,email"` + Access string `json:"access" validate:"required,min=4,max=16"` // private or public + CreationTime time.Time `json:"creationTime" validate:"required"` } const ( - templateConfFile = "template.json" - templateDiskFile = "disk.qcow" + templateConfFile = "template.json" + templateDiskFile = "disk.qcow" - templatePublic = "public" - templatePrivate = "private" + templatePublic = "public" + templatePrivate = "private" ) var dummyTemplate = Template{} // Creates a template from a VM's disk. func NewTemplateFromVM(name, owner, access string, vm *VM) (*Template, error) { - // Marks the VM to copy from as being busy. - if err := vms.setDiskBusy(vm); err != nil { - return nil, errors.New("Failed setting disk busy flag during template creation: " + err.Error()) - } - // Clears the VM from being busy. - defer vms.clearDiskBusy(vm) - - // Creates the template. - template, err := newTemplate(name, owner, access) - if err != nil { - return nil, err - } - - // Creates the template directory. - templateDir := template.getTemplateDir() - if err := os.Mkdir(templateDir, 0750); err != nil { - msg := "Failed creating template dir: " + err.Error() - log.Error(msg) - return nil, errors.New(msg) - } - - // 1) Copies VM's overlay file into a new one (in the VM's directory!): - // cp disk.qcow disk.tpl.qcow - vmDiskFile, _ := filepath.Abs(filepath.Join(vm.dir, vmDiskFile)) - tplDiskFile, _ := filepath.Abs(filepath.Join(vm.dir, "/disk.tpl.qcow")) - if err := utils.CopyFiles(vmDiskFile, tplDiskFile); err != nil { - template.delete() - GetVMsInstance().updateVM(vm) - log.Error("Failed copying VM overlay disk: " + err.Error()) - return nil, errors.New("Failed copying VM overlay disk: " + err.Error()) - } - - // 2) Rebases the new template overlay in order to become a standalone disk file: - // qemu-img rebase -b "" disk.tpl.qcow - if err := exec.QemuImgRebase(tplDiskFile); err != nil { - template.delete() - os.Remove(tplDiskFile) - GetVMsInstance().updateVM(vm) - return nil, err - } - - // 3) Moves the template disk file into the template directory: - // mv disk.tpl.qcow new_template_dir/ - destFile, _ := filepath.Abs(filepath.Join(templateDir, templateDiskFile)) - if err := os.Rename(tplDiskFile, destFile); err != nil { - template.delete() - os.Remove(tplDiskFile) - GetVMsInstance().updateVM(vm) - log.Error("Failed moving template disk image to final location: " + err.Error()) - return nil, errors.New("Failed moving template disk image to final location: " + err.Error()) - } - - return template, nil + // Marks the VM to copy from as being busy. + if err := vms.setDiskBusy(vm); err != nil { + return nil, errors.New("Failed setting disk busy flag during template creation: " + err.Error()) + } + // Clears the VM from being busy. + defer vms.clearDiskBusy(vm) + + // Creates the template. + template, err := newTemplate(name, owner, access) + if err != nil { + return nil, err + } + + // Creates the template directory. + templateDir := template.getTemplateDir() + if err := os.Mkdir(templateDir, 0750); err != nil { + msg := "Failed creating template dir: " + err.Error() + log.Error(msg) + return nil, errors.New(msg) + } + + // 1) Copies VM's overlay file into a new one (in the VM's directory!): + // cp disk.qcow disk.tpl.qcow + vmDiskFile, _ := filepath.Abs(filepath.Join(vm.dir, vmDiskFile)) + tplDiskFile, _ := filepath.Abs(filepath.Join(vm.dir, "/disk.tpl.qcow")) + if err := utils.CopyFiles(vmDiskFile, tplDiskFile); err != nil { + template.delete() + GetVMsInstance().updateVM(vm) + log.Error("Failed copying VM overlay disk: " + err.Error()) + return nil, errors.New("Failed copying VM overlay disk: " + err.Error()) + } + + // 2) Rebases the new template overlay in order to become a standalone disk file: + // qemu-img rebase -b "" disk.tpl.qcow + if err := exec.QemuImgRebase(tplDiskFile); err != nil { + template.delete() + os.Remove(tplDiskFile) + GetVMsInstance().updateVM(vm) + return nil, err + } + + // 3) Moves the template disk file into the template directory: + // mv disk.tpl.qcow new_template_dir/ + destFile, _ := filepath.Abs(filepath.Join(templateDir, templateDiskFile)) + if err := os.Rename(tplDiskFile, destFile); err != nil { + template.delete() + os.Remove(tplDiskFile) + GetVMsInstance().updateVM(vm) + log.Error("Failed moving template disk image to final location: " + err.Error()) + return nil, errors.New("Failed moving template disk image to final location: " + err.Error()) + } + + return template, nil } // Creates a template from a qcow file. func NewTemplateFromQCOW(name, owner, access, qcowFile string) (*Template, error) { - // Creates the template. - template, err := newTemplate(name, owner, access) - if err != nil { - return nil, err - } - - // Creates the template directory. - templateDir := template.getTemplateDir() - if err := os.Mkdir(templateDir, 0750); err != nil { - log.Error("Failed creating template dir: " + err.Error()) - return nil, errors.New("Failed creating template dir: " + err.Error()) - } - - // Moves the qcow file from its location to the template directory. - if err := os.Rename(qcowFile, filepath.Join(templateDir, templateDiskFile)); err != nil { - log.Error("Failed moving uploaded qcow to template dir: " + err.Error()) - return nil, errors.New("Failed creating template: " + err.Error()) - } - - return template, nil + // Creates the template. + template, err := newTemplate(name, owner, access) + if err != nil { + return nil, err + } + + // Creates the template directory. + templateDir := template.getTemplateDir() + if err := os.Mkdir(templateDir, 0750); err != nil { + log.Error("Failed creating template dir: " + err.Error()) + return nil, errors.New("Failed creating template dir: " + err.Error()) + } + + // Moves the qcow file from its location to the template directory. + if err := os.Rename(qcowFile, filepath.Join(templateDir, templateDiskFile)); err != nil { + log.Error("Failed moving uploaded qcow to template dir: " + err.Error()) + return nil, errors.New("Failed creating template: " + err.Error()) + } + + return template, nil } func ValidateTemplateAccess(access string) error { - if access != templatePrivate && access != templatePublic { - return errors.New("Wrong template access type") - } - return nil + if access != templatePrivate && access != templatePublic { + return errors.New("Wrong template access type") + } + return nil } func (template *Template) IsPublic() bool { - return template.Access == templatePublic + return template.Access == templatePublic } func (template *Template) GetTemplateDiskPath() string { - return filepath.Join(template.getTemplateDir(), templateDiskFile) + return filepath.Join(template.getTemplateDir(), templateDiskFile) } // Creates a template. func newTemplate(name, owner, access string) (*Template, error) { - id, err := uuid.NewRandom() - if err != nil { - log.Error("Failed creating template: " + err.Error()) - return nil, err - } + id, err := uuid.NewRandom() + if err != nil { + log.Error("Failed creating template: " + err.Error()) + return nil, err + } - template := &Template{id, name, owner, access, time.Now()} + template := &Template{id, name, owner, access, time.Now()} - if err := template.validate(); err != nil { - return nil, errors.New("Failed validating template: " + err.Error()) - } + if err := template.validate(); err != nil { + return nil, errors.New("Failed validating template: " + err.Error()) + } - return template, nil + return template, nil } // Creates a template from a templateConfFile and returns it. func newTemplateFromFile(file string) (*Template, error) { - filein, err := os.OpenFile(file, os.O_RDONLY, 0) - if err != nil { - return nil, errors.New("Failed reading template config file: " + err.Error()) - } - defer filein.Close() - - // Decodes the json file into users, then checks: - // - all fields are present - // - extra fields are not allowed - decoder := json.NewDecoder(filein) - decoder.DisallowUnknownFields() - template := &Template{} - if err = decoder.Decode(&template); err != nil { - return nil, errors.New("Failed decoding template config file: " + err.Error()) - } - - if err = template.validate(); err != nil { - return nil, errors.New("Failed validating template config file: " + err.Error()) - } - - return template, nil + filein, err := os.OpenFile(file, os.O_RDONLY, 0) + if err != nil { + return nil, errors.New("Failed reading template config file: " + err.Error()) + } + defer filein.Close() + + // Decodes the json file into users, then checks: + // - all fields are present + // - extra fields are not allowed + decoder := json.NewDecoder(filein) + decoder.DisallowUnknownFields() + template := &Template{} + if err = decoder.Decode(&template); err != nil { + return nil, errors.New("Failed decoding template config file: " + err.Error()) + } + + if err = template.validate(); err != nil { + return nil, errors.New("Failed validating template config file: " + err.Error()) + } + + return template, nil } // Writes a template's config file. func (template *Template) writeConfig() error { - templateDir := template.getTemplateDir() - templateConfigFile := filepath.Join(templateDir, templateConfFile) - - file, err := os.Create(templateConfigFile) - if err != nil { - template.delete() - log.Error("Failed writing template config file: " + err.Error()) - return errors.New("Failed writing template config file: " + err.Error()) - } - defer file.Close() - encoder := json.NewEncoder(file) - encoder.SetIndent("", " ") - if err = encoder.Encode(template); err != nil { - template.delete() - log.Error("Failed encoding template config file: " + err.Error()) - return errors.New("Failed encoding template config file: " + err.Error()) - } - - return nil + templateDir := template.getTemplateDir() + templateConfigFile := filepath.Join(templateDir, templateConfFile) + + file, err := os.Create(templateConfigFile) + if err != nil { + template.delete() + log.Error("Failed writing template config file: " + err.Error()) + return errors.New("Failed writing template config file: " + err.Error()) + } + defer file.Close() + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + if err = encoder.Encode(template); err != nil { + template.delete() + log.Error("Failed encoding template config file: " + err.Error()) + return errors.New("Failed encoding template config file: " + err.Error()) + } + + return nil } // Checks the template's fields are valid. func (template *Template) validate() error { - validate := validator.New() - if err := validate.Struct(template); err != nil { - return err - } + validate := validator.New() + if err := validate.Struct(template); err != nil { + return err + } - return ValidateTemplateAccess(template.Access) + return ValidateTemplateAccess(template.Access) } // Deletes a template's directory and its content. func (template *Template) delete() error { - templateDir := template.getTemplateDir() - if err := os.RemoveAll(templateDir); err != nil { - log.Error("Failed deleting template files: " + err.Error()) - return errors.New("Failed deleting template files: " + err.Error()) - } + templateDir := template.getTemplateDir() + if err := os.RemoveAll(templateDir); err != nil { + log.Error("Failed deleting template files: " + err.Error()) + return errors.New("Failed deleting template files: " + err.Error()) + } - return nil + return nil } func (template *Template) getTemplateDir() string { - templatesDir := GetTemplatesInstance().getDir() - return filepath.Join(templatesDir, template.ID.String()) + templatesDir := GetTemplatesInstance().getDir() + return filepath.Join(templatesDir, template.ID.String()) } diff --git a/src/vms/templates.go b/src/vms/templates.go index ad1002711cabea53693f82212e7968d10882471f..ba92387f6760dd47024e6004d11aa7500b69c552 100644 --- a/src/vms/templates.go +++ b/src/vms/templates.go @@ -1,25 +1,25 @@ package vms import( - "os" - "sort" - "path" - "sync" - "errors" - "path/filepath" - "nexus-server/paths" - "nexus-server/utils" - "github.com/google/uuid" + "os" + "sort" + "path" + "sync" + "errors" + "path/filepath" + "nexus-server/paths" + "nexus-server/utils" + "github.com/google/uuid" ) type ( - TemplateKeeperFn func(tpl Template) bool + TemplateKeeperFn func(tpl Template) bool - Templates struct { - m map[string]Template - dir string // Base directory where templates reside - rwlock *sync.RWMutex - } + Templates struct { + m map[string]Template + dir string // Base directory where templates reside + rwlock *sync.RWMutex + } ) var templates *Templates @@ -27,76 +27,76 @@ var templates *Templates // Returns a Templates "singleton". // IMPORTANT: the InitTemplates function must have been previously called! func GetTemplatesInstance() *Templates { - return templates + return templates } // Create all templates from on-disk files. // NOTE: path is the root directory where templates reside. func InitTemplates() error { - templatesDir := paths.GetInstance().TemplatesDir - templates = &Templates { m: make(map[string]Template), dir: templatesDir, rwlock: new(sync.RWMutex) } - - dirs, err := utils.GetSubDirs(templatesDir) - if err != nil { - return errors.New("Failed reading templates directory: "+err.Error()) - } - - for _, dir := range(dirs) { - // Checks the template directory is valid. - templateDir := path.Base(dir) - _, err := uuid.Parse(templateDir) - if err != nil { - log.Warn("Skipping template "+templateDir+": invalid directory name") - continue - } - - // Checks that a templateDiskFile is present - if _, err := os.Stat(filepath.Join(dir, templateDiskFile)); err != nil { - log.Warn("Skipping template "+templateDir+": missing "+templateDiskFile) - continue - } - - // Read and validate the template's content - template, err := newTemplateFromFile(filepath.Join(dir, templateConfFile)) - if err != nil { - log.Warn("Skipping template "+templateDir+": "+err.Error()) - continue - } - - templates.m[templateDir] = *template - } - - return nil + templatesDir := paths.GetInstance().TemplatesDir + templates = &Templates { m: make(map[string]Template), dir: templatesDir, rwlock: new(sync.RWMutex) } + + dirs, err := utils.GetSubDirs(templatesDir) + if err != nil { + return errors.New("Failed reading templates directory: "+err.Error()) + } + + for _, dir := range(dirs) { + // Checks the template directory is valid. + templateDir := path.Base(dir) + _, err := uuid.Parse(templateDir) + if err != nil { + log.Warn("Skipping template "+templateDir+": invalid directory name") + continue + } + + // Checks that a templateDiskFile is present + if _, err := os.Stat(filepath.Join(dir, templateDiskFile)); err != nil { + log.Warn("Skipping template "+templateDir+": missing "+templateDiskFile) + continue + } + + // Read and validate the template's content + template, err := newTemplateFromFile(filepath.Join(dir, templateConfFile)) + if err != nil { + log.Warn("Skipping template "+templateDir+": "+err.Error()) + continue + } + + templates.m[templateDir] = *template + } + + return nil } // Returns the list of templates for which TemplateKeeperFn returns true. func (templates *Templates)GetTemplates(keep TemplateKeeperFn) []Template { - templates.rwlock.RLock() - list := []Template{} - for _, t := range templates.m { - if keep(t) { - list = append(list, t) - } - } - templates.rwlock.RUnlock() - - // Sort templates by names - sort.Slice(list, func(i, j int) bool { + templates.rwlock.RLock() + list := []Template{} + for _, t := range templates.m { + if keep(t) { + list = append(list, t) + } + } + templates.rwlock.RUnlock() + + // Sort templates by names + sort.Slice(list, func(i, j int) bool { return list[i].Name < list[j].Name }) - return list + return list } // Returns a template by its ID. func (templates *Templates)GetTemplate(tplID uuid.UUID) (Template, error) { - templates.rwlock.RLock() - defer templates.rwlock.RUnlock() - - template, exists := templates.m[tplID.String()] - if !exists { - return dummyTemplate, errors.New("Template not found") - } - return template, nil + templates.rwlock.RLock() + defer templates.rwlock.RUnlock() + + template, exists := templates.m[tplID.String()] + if !exists { + return dummyTemplate, errors.New("Template not found") + } + return template, nil } // Deletes a template by its ID. @@ -104,73 +104,73 @@ func (templates *Templates)GetTemplate(tplID uuid.UUID) (Template, error) { // TODO: prevents deletion if the template's disk image is // being exported by RouterTemplates.ExportDisk func (templates *Templates)DeleteTemplate(tplID uuid.UUID, vms *VMs) error { - id := tplID.String() - templates.rwlock.Lock() - defer templates.rwlock.Unlock() - template, exists := templates.m[id] - if exists { - if vms.IsTemplateUsed(id) { - return errors.New("Failed deleting template: still used by one or more VMs") - } - // Removes the template's files (and directory). - if err := template.delete(); err != nil { - return err - } - // Removes the template from the map. - delete(templates.m, id) - return nil - } - - return errors.New("Template not found") + id := tplID.String() + templates.rwlock.Lock() + defer templates.rwlock.Unlock() + template, exists := templates.m[id] + if exists { + if vms.IsTemplateUsed(id) { + return errors.New("Failed deleting template: still used by one or more VMs") + } + // Removes the template's files (and directory). + if err := template.delete(); err != nil { + return err + } + // Removes the template from the map. + delete(templates.m, id) + return nil + } + + return errors.New("Template not found") } // Adds a template and writes its config file. func (templates *Templates)AddTemplate(template *Template) error { - templates.rwlock.Lock() - defer templates.rwlock.Unlock() + templates.rwlock.Lock() + defer templates.rwlock.Unlock() - err := template.writeConfig() - if err != nil { - return err - } + err := template.writeConfig() + if err != nil { + return err + } - // Adds template to the map of templates. - templates.m[template.ID.String()] = *template + // Adds template to the map of templates. + templates.m[template.ID.String()] = *template - return nil + return nil } func (templates *Templates)getDir() string { - return templates.dir + return templates.dir } // Edit a template' specs: name, access func (templates *Templates)EditTemplate(tplID uuid.UUID, name, access string) error { - tpl, err := templates.GetTemplate(tplID) - if err != nil { - return err - } - - // Only updates fields that have changed. - if name != "" { - tpl.Name = name - } - if access != "" { - tpl.Access = access - } - - if err = tpl.validate(); err != nil { - return err - } - - err = tpl.writeConfig() - if err != nil { - return err - } - - key := tpl.ID.String() - delete(templates.m, key) - templates.m[key] = tpl - - return nil + tpl, err := templates.GetTemplate(tplID) + if err != nil { + return err + } + + // Only updates fields that have changed. + if name != "" { + tpl.Name = name + } + if access != "" { + tpl.Access = access + } + + if err = tpl.validate(); err != nil { + return err + } + + err = tpl.writeConfig() + if err != nil { + return err + } + + key := tpl.ID.String() + delete(templates.m, key) + templates.m[key] = tpl + + return nil } diff --git a/src/vms/vm.go b/src/vms/vm.go index f8aaec0d4f17fc84c10fa3134d620b4c28144c6f..ac6ccf0d5a034f6990de0538122954511074e0fc 100644 --- a/src/vms/vm.go +++ b/src/vms/vm.go @@ -1,253 +1,253 @@ package vms import ( - "os" - "sync" - "path" - "errors" - "syscall" - "net/mail" - "io/ioutil" - "path/filepath" - "encoding/json" - "encoding/base64" - "nexus-server/qga" - "nexus-server/caps" - "nexus-server/paths" - "nexus-server/exec" - "github.com/google/uuid" - "github.com/go-playground/validator/v10" - "github.com/sethvargo/go-password/password" + "os" + "sync" + "path" + "errors" + "syscall" + "net/mail" + "io/ioutil" + "path/filepath" + "encoding/json" + "encoding/base64" + "nexus-server/qga" + "nexus-server/caps" + "nexus-server/paths" + "nexus-server/exec" + "github.com/google/uuid" + "github.com/go-playground/validator/v10" + "github.com/sethvargo/go-password/password" ) type ( - // Internal VM structure. - VM struct { - ID uuid.UUID `json:"id" validate:"required"` - Owner string `json:"owner" validate:"required,email"` - Name string `json:"name" validate:"required,min=2,max=256"` - Cpus int `json:"cpus" validate:"required,gte=1,lte=16"` - Ram int `json:"ram" validate:"required,gte=512,lte=32768"` // in MB - Nic NicType `json:"nic" validate:"required"` // "none" or "user" - TemplateID uuid.UUID `json:"templateID" validate:"required"` - Access map[string]caps.Capabilities `json:"access" validate:"required"` - - // None of the fields below are serialized to disk. - dir string // VM directory - qgaSock string // QEMU Guest Agent (QGA) UNIX socket - Run runStates - DiskBusy bool // If true the VM's disk is busy (cannot be modified or deleted) - mutex *sync.Mutex - } - - runStates struct { - State VMState - Pid int - Port int - Pwd string - } - - VMState string - NicType string - - // VM fields to be serialized to disk. - VMDiskSerialized struct { - ID uuid.UUID `json:"id"` - Owner string `json:"owner"` - Name string `json:"name"` - Cpus int `json:"cpus"` - Ram int `json:"ram"` // in MB - Nic NicType `json:"nic"` - TemplateID uuid.UUID `json:"templateID"` - Access map[string]caps.Capabilities `json:"access"` - } - - // VM fields to be serialized over the network (sent to the client). - VMNetworkSerialized struct { - ID uuid.UUID - Owner string - Name string - Cpus int - Ram int - Nic NicType - TemplateID uuid.UUID - Access map[string]caps.Capabilities - State VMState - Port int - Pwd string - DiskBusy bool // If true the VM's disk is busy (cannot be modified or deleted) - } - - endOfExecCallback func(vm *VM) + // Internal VM structure. + VM struct { + ID uuid.UUID `json:"id" validate:"required"` + Owner string `json:"owner" validate:"required,email"` + Name string `json:"name" validate:"required,min=2,max=256"` + Cpus int `json:"cpus" validate:"required,gte=1,lte=16"` + Ram int `json:"ram" validate:"required,gte=512,lte=32768"` // in MB + Nic NicType `json:"nic" validate:"required"` // "none" or "user" + TemplateID uuid.UUID `json:"templateID" validate:"required"` + Access map[string]caps.Capabilities `json:"access" validate:"required"` + + // None of the fields below are serialized to disk. + dir string // VM directory + qgaSock string // QEMU Guest Agent (QGA) UNIX socket + Run runStates + DiskBusy bool // If true the VM's disk is busy (cannot be modified or deleted) + mutex *sync.Mutex + } + + runStates struct { + State VMState + Pid int + Port int + Pwd string + } + + VMState string + NicType string + + // VM fields to be serialized to disk. + VMDiskSerialized struct { + ID uuid.UUID `json:"id"` + Owner string `json:"owner"` + Name string `json:"name"` + Cpus int `json:"cpus"` + Ram int `json:"ram"` // in MB + Nic NicType `json:"nic"` + TemplateID uuid.UUID `json:"templateID"` + Access map[string]caps.Capabilities `json:"access"` + } + + // VM fields to be serialized over the network (sent to the client). + VMNetworkSerialized struct { + ID uuid.UUID + Owner string + Name string + Cpus int + Ram int + Nic NicType + TemplateID uuid.UUID + Access map[string]caps.Capabilities + State VMState + Port int + Pwd string + DiskBusy bool // If true the VM's disk is busy (cannot be modified or deleted) + } + + endOfExecCallback func(vm *VM) ) const ( - vmDiskFile = "disk.qcow" - vmConfFile = "vm.json" - vmSecretFile = "secret" - vmQGASockFile = "qga.sock" + vmDiskFile = "disk.qcow" + vmConfFile = "vm.json" + vmSecretFile = "secret" + vmQGASockFile = "qga.sock" - nicUser NicType = "user" - nicNone NicType = "none" + nicUser NicType = "user" + nicNone NicType = "none" - stateStopped VMState = "STOPPED" - stateRunning = "RUNNING" + stateStopped VMState = "STOPPED" + stateRunning = "RUNNING" ) var dummyVM = VM{} // Custom password generator with the following characters removed: 0, O, l, I var passwordGen, _ = password.NewGenerator(&password.GeneratorInput{ - LowerLetters: "abcdefghijkmnopqrstuvwxyz", - UpperLetters: "ABCDEFGHJKLMNPQRSTUVWXYZ", - Digits: "123456789", + LowerLetters: "abcdefghijkmnopqrstuvwxyz", + UpperLetters: "ABCDEFGHJKLMNPQRSTUVWXYZ", + Digits: "123456789", }) // Creates a VM. func NewVM(creatorEmail string, name string, cpus, ram int, nic NicType, templateID uuid.UUID, owner string) (*VM, error) { - vmID, err := uuid.NewRandom() - - if err != nil { - log.Error("Failed creating VM: "+err.Error()) - return nil, err - } - vm := newEmptyVM() - vm.ID = vmID - vm.Owner = owner - vm.Name = name - vm.Cpus = cpus - vm.Ram = ram - vm.Nic = nic - vm.TemplateID = templateID - id := vmID.String() - vm.dir = filepath.Join(vms.dir, id[0:3], id[3:6], id) - vm.qgaSock = filepath.Join(vm.dir, vmQGASockFile) - vm.Access = make(map[string]caps.Capabilities) - - if err = vm.validate(); err != nil { - return nil, errors.New("Failed validating VM: "+err.Error()) - } - - return vm, nil + vmID, err := uuid.NewRandom() + + if err != nil { + log.Error("Failed creating VM: "+err.Error()) + return nil, err + } + vm := newEmptyVM() + vm.ID = vmID + vm.Owner = owner + vm.Name = name + vm.Cpus = cpus + vm.Ram = ram + vm.Nic = nic + vm.TemplateID = templateID + id := vmID.String() + vm.dir = filepath.Join(vms.dir, id[0:3], id[3:6], id) + vm.qgaSock = filepath.Join(vm.dir, vmQGASockFile) + vm.Access = make(map[string]caps.Capabilities) + + if err = vm.validate(); err != nil { + return nil, errors.New("Failed validating VM: "+err.Error()) + } + + return vm, nil } func (vm *VM)SerializeToDisk() VMDiskSerialized { - return VMDiskSerialized { - ID: vm.ID, - Owner: vm.Owner, - Name: vm.Name, - Cpus: vm.Cpus, - Ram: vm.Ram, - Nic: vm.Nic, - TemplateID: vm.TemplateID, - Access: vm.Access, - } + return VMDiskSerialized { + ID: vm.ID, + Owner: vm.Owner, + Name: vm.Name, + Cpus: vm.Cpus, + Ram: vm.Ram, + Nic: vm.Nic, + TemplateID: vm.TemplateID, + Access: vm.Access, + } } func (vm *VM)SerializeToNetwork() VMNetworkSerialized { - return VMNetworkSerialized { - ID: vm.ID, - Owner: vm.Owner, - Name: vm.Name, - Cpus: vm.Cpus, - Ram: vm.Ram, - Nic: vm.Nic, - TemplateID: vm.TemplateID, - Access: vm.Access, - State: vm.Run.State, - Port: vm.Run.Port, - Pwd: vm.Run.Pwd, - DiskBusy: vm.DiskBusy, - } + return VMNetworkSerialized { + ID: vm.ID, + Owner: vm.Owner, + Name: vm.Name, + Cpus: vm.Cpus, + Ram: vm.Ram, + Nic: vm.Nic, + TemplateID: vm.TemplateID, + Access: vm.Access, + State: vm.Run.State, + Port: vm.Run.Port, + Pwd: vm.Run.Pwd, + DiskBusy: vm.DiskBusy, + } } func (vm *VM)getDiskPath() string { - vmDiskFile, _ := filepath.Abs(filepath.Join(vm.dir, vmDiskFile)) - return vmDiskFile + vmDiskFile, _ := filepath.Abs(filepath.Join(vm.dir, vmDiskFile)) + return vmDiskFile } // Returns true if the specified email is the VM's owner. func (vm *VM)IsOwner(email string) bool { - return email == vm.Owner + return email == vm.Owner } // Creates an empty VM. func newEmptyVM() *VM { - return &VM { - ID: uuid.Nil, - Owner: "", - Name: "", - Cpus: 0, - Ram: 0, - Nic: "", - TemplateID: uuid.Nil, - Access: make(map[string]caps.Capabilities), - - dir: "", - qgaSock: "", - Run: runStates { State: stateStopped, Pid: 0, Port: 0, Pwd: "" }, - DiskBusy: false, - mutex: new(sync.Mutex), - } + return &VM { + ID: uuid.Nil, + Owner: "", + Name: "", + Cpus: 0, + Ram: 0, + Nic: "", + TemplateID: uuid.Nil, + Access: make(map[string]caps.Capabilities), + + dir: "", + qgaSock: "", + Run: runStates { State: stateStopped, Pid: 0, Port: 0, Pwd: "" }, + DiskBusy: false, + mutex: new(sync.Mutex), + } } // Creates a VM from a vmConfFile and returns it. func newVMFromFile(vmFile string) (*VM, error) { - filein, err := os.OpenFile(vmFile, os.O_RDONLY, 0) - if err != nil { + filein, err := os.OpenFile(vmFile, os.O_RDONLY, 0) + if err != nil { return nil, errors.New("Failed reading VM config file: "+err.Error()) - } - defer filein.Close() - - // Decodes and validates the json file - decoder := json.NewDecoder(filein) - decoder.DisallowUnknownFields() - vm := newEmptyVM() - if err = decoder.Decode(&vm); err != nil { - return nil, errors.New("Failed decoding VM config file: "+err.Error()) - } - - if err = vm.validate(); err != nil { - return nil, errors.New("Failed validating VM: "+err.Error()) - } - - vm.dir = path.Dir(vmFile) - vm.qgaSock = filepath.Join(vm.dir, vmQGASockFile) - return vm, nil + } + defer filein.Close() + + // Decodes and validates the json file + decoder := json.NewDecoder(filein) + decoder.DisallowUnknownFields() + vm := newEmptyVM() + if err = decoder.Decode(&vm); err != nil { + return nil, errors.New("Failed decoding VM config file: "+err.Error()) + } + + if err = vm.validate(); err != nil { + return nil, errors.New("Failed validating VM: "+err.Error()) + } + + vm.dir = path.Dir(vmFile) + vm.qgaSock = filepath.Join(vm.dir, vmQGASockFile) + return vm, nil } func (vm *VM)IsRunning() bool { - return vm.Run.State == stateRunning + return vm.Run.State == stateRunning } // Checks that the VM structure's fields are valid. func (vm *VM)validate() error { - // Checks the capabilities are valid - for email, accessCaps := range vm.Access { - _, err := mail.ParseAddress(email) - if err != nil { - return errors.New("Invalid email") - } - if err := caps.ValidateVMAccessCaps(accessCaps); err != nil { - return err - } - } - - if (vm.Nic != nicNone && vm.Nic != nicUser) { - return errors.New("Invalid nic value: "+string(vm.Nic)) - } - - if err := validator.New().Struct(vm); err != nil { - return err - } - - // Checks that the template referenced by the VM actually exists. - _, err := GetTemplatesInstance().GetTemplate(vm.TemplateID) - if err != nil { - return err - } - - return nil + // Checks the capabilities are valid + for email, accessCaps := range vm.Access { + _, err := mail.ParseAddress(email) + if err != nil { + return errors.New("Invalid email") + } + if err := caps.ValidateVMAccessCaps(accessCaps); err != nil { + return err + } + } + + if (vm.Nic != nicNone && vm.Nic != nicUser) { + return errors.New("Invalid nic value: "+string(vm.Nic)) + } + + if err := validator.New().Struct(vm); err != nil { + return err + } + + // Checks that the template referenced by the VM actually exists. + _, err := GetTemplatesInstance().GetTemplate(vm.TemplateID) + if err != nil { + return err + } + + return nil } // Writes a VM's files: @@ -255,251 +255,251 @@ func (vm *VM)validate() error { // 2) Writes vmConfFile. // 3) Creates vmDiskFile as an overlay on top of the template disk. func (vm *VM)writeFiles() error { - // Checks the template referenced by the VM exists. - template, err := GetTemplatesInstance().GetTemplate(vm.TemplateID) - if err != nil { - return err - } - - // Creates the 3-directory structure. - if err = os.MkdirAll(vm.dir, 0750); err != nil { - log.Error("Failed creating VM dirs: "+err.Error()) - return errors.New("Failed creating VM dirs: "+err.Error()) - } - - // Writes vmConfFile. - if err = vm.writeConfig(); err != nil { - return err - } - - // Creates vmDiskFile as an overlay on top of the template disk. - // NOTE: template and output file must be both specified as absolute paths. - templateDiskFile, _ := filepath.Abs(filepath.Join(GetTemplatesInstance().getDir(), template.ID.String(), templateDiskFile)) - - if err := exec.QemuImgCreate(templateDiskFile, vm.getDiskPath()); err != nil { - vm.delete() - return err - } - - return nil + // Checks the template referenced by the VM exists. + template, err := GetTemplatesInstance().GetTemplate(vm.TemplateID) + if err != nil { + return err + } + + // Creates the 3-directory structure. + if err = os.MkdirAll(vm.dir, 0750); err != nil { + log.Error("Failed creating VM dirs: "+err.Error()) + return errors.New("Failed creating VM dirs: "+err.Error()) + } + + // Writes vmConfFile. + if err = vm.writeConfig(); err != nil { + return err + } + + // Creates vmDiskFile as an overlay on top of the template disk. + // NOTE: template and output file must be both specified as absolute paths. + templateDiskFile, _ := filepath.Abs(filepath.Join(GetTemplatesInstance().getDir(), template.ID.String(), templateDiskFile)) + + if err := exec.QemuImgCreate(templateDiskFile, vm.getDiskPath()); err != nil { + vm.delete() + return err + } + + return nil } // Writes the VM config file (vmConfFile). // NOTE: does not check the template's validity! func (vm *VM)writeConfig() error { - vmFile := filepath.Join(vm.dir, vmConfFile) - file, err := os.Create(vmFile) - if err != nil { - log.Error("Failed writing VM config file: "+err.Error()) + vmFile := filepath.Join(vm.dir, vmConfFile) + file, err := os.Create(vmFile) + if err != nil { + log.Error("Failed writing VM config file: "+err.Error()) return errors.New("Failed writing VM config file: "+err.Error()) - } - defer file.Close() - encoder := json.NewEncoder(file) - encoder.SetIndent("", " ") + } + defer file.Close() + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") - if err = encoder.Encode(vm.SerializeToDisk()); err != nil { - log.Error("Failed encoding VM config file: "+err.Error()) - return errors.New("Failed encoding VM config file: "+err.Error()) - } + if err = encoder.Encode(vm.SerializeToDisk()); err != nil { + log.Error("Failed encoding VM config file: "+err.Error()) + return errors.New("Failed encoding VM config file: "+err.Error()) + } - return nil + return nil } // Deletes the files associated to a VM. // The VM must be stopped before being deleted, otherwise an error is returned. func (vm *VM)delete() error { - if vm.IsRunning() { - return errors.New("Failed deleting VM: VM must be stopped") - } + if vm.IsRunning() { + return errors.New("Failed deleting VM: VM must be stopped") + } - if vm.DiskBusy { - return errors.New("Failed deleting VM: disk is busy") - } + if vm.DiskBusy { + return errors.New("Failed deleting VM: disk is busy") + } - // Deletes the VM's directory and its content. - if err := os.RemoveAll(vm.dir); err != nil { - log.Error("Failed deleting VM files: "+err.Error()) + // Deletes the VM's directory and its content. + if err := os.RemoveAll(vm.dir); err != nil { + log.Error("Failed deleting VM files: "+err.Error()) return errors.New("Failed deleting VM files: "+err.Error()) - } - - // Deletes parents directories if they are empty. - // Directories are only deleted if empty (otherwise an error is triggered which we ignore). - parentDir := path.Dir(vm.dir) - parentParentDir := path.Dir(parentDir) - os.Remove(parentDir) - os.Remove(parentParentDir) - return nil + } + + // Deletes parents directories if they are empty. + // Directories are only deleted if empty (otherwise an error is triggered which we ignore). + parentDir := path.Dir(vm.dir) + parentParentDir := path.Dir(parentDir) + os.Remove(parentDir) + os.Remove(parentParentDir) + return nil } // Starts a VM and returns the access password. // Password is randomly generated. func (vm *VM)start(port int, endofExecFn endOfExecCallback) (string, error) { - if vm.IsRunning() { - return "", errors.New("Failed starting VM: VM is already running") - } - - 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()) - } - - // 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()) - } - - 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 pwd, nil + if vm.IsRunning() { + return "", errors.New("Failed starting VM: VM is already running") + } + + 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()) + } + + // 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()) + } + + 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 pwd, nil } // Writes the specified password in Base64 in a "secret" file inside the VM directory. // Returns the file that was written if success. func (vm *VM)writeSecretFile(pwd string) (string, error) { - // Write the password in a "secret" file inside the VM directory. - pwdBase64 := base64.StdEncoding.EncodeToString([]byte(pwd)) - content := []byte(pwdBase64) - pwdFile := filepath.Join(vm.dir, vmSecretFile) - if err := ioutil.WriteFile(pwdFile, content, 0600); err != nil { - return "", err - } - return pwdFile, nil + // Write the password in a "secret" file inside the VM directory. + pwdBase64 := base64.StdEncoding.EncodeToString([]byte(pwd)) + content := []byte(pwdBase64) + pwdFile := filepath.Join(vm.dir, vmSecretFile) + if err := ioutil.WriteFile(pwdFile, content, 0600); err != nil { + return "", err + } + return pwdFile, nil } func (vm *VM)removeSecretFile() { - pwdFile := filepath.Join(vm.dir, vmSecretFile) - os.Remove(pwdFile) + pwdFile := filepath.Join(vm.dir, vmSecretFile) + os.Remove(pwdFile) } // Kills by force a running VM. func (vm *VM)kill() error { - if !vm.IsRunning() { - return errors.New("Failed stopping VM: VM is not running") - } - - // Sends a SIGINT signal to terminate the QEMU process. - // Note that QEMU terminates with status code 0 in this case (i.e. no error). - if err := syscall.Kill(vm.Run.Pid, syscall.SIGINT); err != nil { - log.Error("Failed stopping VM: "+err.Error()) - return errors.New("Failed stopping VM: "+err.Error()) - } - - return nil + if !vm.IsRunning() { + return errors.New("Failed stopping VM: VM is not running") + } + + // Sends a SIGINT signal to terminate the QEMU process. + // Note that QEMU terminates with status code 0 in this case (i.e. no error). + if err := syscall.Kill(vm.Run.Pid, syscall.SIGINT); err != nil { + log.Error("Failed stopping VM: "+err.Error()) + return errors.New("Failed stopping VM: "+err.Error()) + } + + return nil } // 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(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.SendShutdown(); 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(prefix+"(close): "+err.Error()) - return errors.New(prefix+"(close): "+err.Error()) - } - - return nil + 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.SendShutdown(); 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(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(prefix+"(close): "+err.Error()) - return errors.New(prefix+"(close): "+err.Error()) - } - - return nil + 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(prefix+"(close): "+err.Error()) + return errors.New(prefix+"(close): "+err.Error()) + } + + return nil } // Executes the VM in QEMU using the specified spice port and password. func (vm *VM)runQEMU(port int, pwd, pwdFile string, endofExecFn endOfExecCallback) error { - pkiDir := paths.GetInstance().NexusPkiDir - cmd, err := exec.NewQemuSystem(vm.qgaSock, vm.Cpus, vm.Ram, string(vm.Nic), filepath.Join(vm.dir, vmDiskFile), port, pwdFile, pkiDir) - if err != nil { - return err - } - - if err := cmd.Start(); err != nil { - log.Error("Failed executing VM "+vm.ID.String()+": exec.Start error: "+err.Error()) - log.Error("Failed cmd: "+cmd.String()) - return err - } - - vm.Run = runStates { State: stateRunning, Pid: cmd.Process.Pid, Port: port, Pwd: pwd } - - // Execute cmd.Wait() (which is a blocking call) inside a go-routine to avoid blocking. - // From here on, there are 2 flows of execution! - go func() { - if err := cmd.Wait(); err != nil { - log.Error("Failed executing VM "+vm.ID.String()+": exec.Wait error: "+err.Error()) - log.Error("Failed cmd: "+cmd.String()) - } - endofExecFn(vm) - } () - - return nil + pkiDir := paths.GetInstance().NexusPkiDir + cmd, err := exec.NewQemuSystem(vm.qgaSock, vm.Cpus, vm.Ram, string(vm.Nic), filepath.Join(vm.dir, vmDiskFile), port, pwdFile, pkiDir) + if err != nil { + return err + } + + if err := cmd.Start(); err != nil { + log.Error("Failed executing VM "+vm.ID.String()+": exec.Start error: "+err.Error()) + log.Error("Failed cmd: "+cmd.String()) + return err + } + + vm.Run = runStates { State: stateRunning, Pid: cmd.Process.Pid, Port: port, Pwd: pwd } + + // Execute cmd.Wait() (which is a blocking call) inside a go-routine to avoid blocking. + // From here on, there are 2 flows of execution! + go func() { + if err := cmd.Wait(); err != nil { + log.Error("Failed executing VM "+vm.ID.String()+": exec.Wait error: "+err.Error()) + log.Error("Failed cmd: "+cmd.String()) + } + endofExecFn(vm) + } () + + return nil } // Resets a VM's states. func (vm *VM)resetStates() { - vm.Run = runStates { State: stateStopped, Pid: 0, Port: 0, Pwd: "" } + vm.Run = runStates { State: stateStopped, Pid: 0, Port: 0, Pwd: "" } } diff --git a/src/vms/vms.go b/src/vms/vms.go index 03a001611e03afa9a38fba360c423acb894cba0f..f16b70b36997aabc079dd037c38d444ac8cd8e31 100644 --- a/src/vms/vms.go +++ b/src/vms/vms.go @@ -1,31 +1,31 @@ package vms import ( - "os" - "sort" - "sync" - "math" - "errors" - "path/filepath" - "nexus-server/exec" - "nexus-server/caps" - "nexus-server/paths" - "nexus-server/utils" - "nexus-server/logger" - "nexus-server/consts" - "github.com/google/uuid" + "os" + "sort" + "sync" + "math" + "errors" + "path/filepath" + "nexus-server/exec" + "nexus-server/caps" + "nexus-server/paths" + "nexus-server/utils" + "nexus-server/logger" + "nexus-server/consts" + "github.com/google/uuid" ) type ( - VMKeeperFn func(vm VM) bool - - VMs struct { - m map[string]VM - dir string // Base directory where VMs are stored - rwlock *sync.RWMutex // RWlock to ensure the coherency of the map (m) of VMs - usedPorts [65536]bool // Indicates which ports are used by the VMs - usedRAM int // Used RAM by running VMs (in MB) - } + VMKeeperFn func(vm VM) bool + + VMs struct { + m map[string]VM + dir string // Base directory where VMs are stored + rwlock *sync.RWMutex // RWlock to ensure the coherency of the map (m) of VMs + usedPorts [65536]bool // Indicates which ports are used by the VMs + usedRAM int // Used RAM by running VMs (in MB) + } ) var log = logger.GetInstance() @@ -34,441 +34,441 @@ var vms *VMs // Returns a VMs "singleton". // IMPORTANT: the InitVMs function must have been previously called! func GetVMsInstance() *VMs { - return vms + return vms } // Creates all VMs from their files on disk. // NOTE: path is the root directory where VMs reside. func InitVMs() error { - vmsDir := paths.GetInstance().VMsDir - vms = &VMs { m: make(map[string]VM), dir: vmsDir, rwlock: new(sync.RWMutex), usedRAM: 0 } - - errMsg := "Failed reading VMs directory: " - dirs1, err := utils.GetSubDirs(vmsDir) - if err != nil { - return errors.New(errMsg+err.Error()) - } - - for d1 := range(dirs1) { - dirs2, err := utils.GetSubDirs(dirs1[d1]) - if err != nil { - return errors.New(errMsg+err.Error()) - } - - for d2 := range(dirs2) { - dirs3, err := utils.GetSubDirs(dirs2[d2]) - if err != nil { - return errors.New(errMsg+err.Error()) - } - - for i := range(dirs3) { - vmDir := dirs3[i] - filename := filepath.Join(vmDir, vmConfFile) - vm, err := newVMFromFile(filename) - if err != nil { - log.Warn("Skipping VM: failed reading \""+filename+"\" "+err.Error()) - continue - } - - filename = filepath.Join(vmDir, vmDiskFile) - f, err := os.OpenFile(filename, os.O_RDONLY, 0) - if err != nil { - log.Warn("Skipping VM: failed reading config file:"+err.Error()) - continue - } - f.Close() - - vms.m[vm.ID.String()] = *vm - } - } - } - - return nil + vmsDir := paths.GetInstance().VMsDir + vms = &VMs { m: make(map[string]VM), dir: vmsDir, rwlock: new(sync.RWMutex), usedRAM: 0 } + + errMsg := "Failed reading VMs directory: " + dirs1, err := utils.GetSubDirs(vmsDir) + if err != nil { + return errors.New(errMsg+err.Error()) + } + + for d1 := range(dirs1) { + dirs2, err := utils.GetSubDirs(dirs1[d1]) + if err != nil { + return errors.New(errMsg+err.Error()) + } + + for d2 := range(dirs2) { + dirs3, err := utils.GetSubDirs(dirs2[d2]) + if err != nil { + return errors.New(errMsg+err.Error()) + } + + for i := range(dirs3) { + vmDir := dirs3[i] + filename := filepath.Join(vmDir, vmConfFile) + vm, err := newVMFromFile(filename) + if err != nil { + log.Warn("Skipping VM: failed reading \""+filename+"\" "+err.Error()) + continue + } + + filename = filepath.Join(vmDir, vmDiskFile) + f, err := os.OpenFile(filename, os.O_RDONLY, 0) + if err != nil { + log.Warn("Skipping VM: failed reading config file:"+err.Error()) + continue + } + f.Close() + + vms.m[vm.ID.String()] = *vm + } + } + } + + return nil } // Returns the list of serialized VMs for which VMKeeperFn returns true. func (vms *VMs)GetNetworkSerializedVMs(keepFn VMKeeperFn) []VMNetworkSerialized { - vms.rwlock.RLock() - list := []VMNetworkSerialized{} - for _, vm := range vms.m { - if keepFn(vm) { - list = append(list, vm.SerializeToNetwork()) - } - } - vms.rwlock.RUnlock() - - // Sort VMs by names - sort.Slice(list, func(i, j int) bool { + vms.rwlock.RLock() + list := []VMNetworkSerialized{} + for _, vm := range vms.m { + if keepFn(vm) { + list = append(list, vm.SerializeToNetwork()) + } + } + vms.rwlock.RUnlock() + + // Sort VMs by names + sort.Slice(list, func(i, j int) bool { return list[i].Name < list[j].Name }) - return list + return list } // Returns a VM by its ID. func (vms *VMs)GetVM(vmID uuid.UUID) (VM, error) { - vms.rwlock.RLock() - defer vms.rwlock.RUnlock() - return vms.getVMUnsafe(vmID) + vms.rwlock.RLock() + defer vms.rwlock.RUnlock() + return vms.getVMUnsafe(vmID) } func (vms *VMs)getVMUnsafe(vmID uuid.UUID) (VM, error) { - vm, exists := vms.m[vmID.String()] - if !exists { - return dummyVM, errors.New("VM not found") - } - return vm, nil + vm, exists := vms.m[vmID.String()] + if !exists { + return dummyVM, errors.New("VM not found") + } + return vm, nil } // Deletes a VM by its ID and deletes its files. func (vms *VMs)DeleteVM(vmID uuid.UUID) error { - vms.rwlock.Lock() - defer vms.rwlock.Unlock() - - vm, err := vms.getVMUnsafe(vmID) - if err != nil { - return err - } - - // Deletes the VM's files (and directories). - vm.mutex.Lock() - if err := vm.delete(); err != nil { - vm.mutex.Unlock() - return err - } - vm.mutex.Unlock() - // Removes the VM from the map. - delete(vms.m, vmID.String()) - return nil + vms.rwlock.Lock() + defer vms.rwlock.Unlock() + + vm, err := vms.getVMUnsafe(vmID) + if err != nil { + return err + } + + // Deletes the VM's files (and directories). + vm.mutex.Lock() + if err := vm.delete(); err != nil { + vm.mutex.Unlock() + return err + } + vm.mutex.Unlock() + // Removes the VM from the map. + delete(vms.m, vmID.String()) + return nil } // Adds a VM and writes its files. func (vms *VMs)AddVM(vm *VM) error { - vm.mutex.Lock() - // First, writes VM files since it can fail. - err := vm.writeFiles() - if err != nil { - vm.mutex.Unlock() - return err - } - vm.mutex.Unlock() - - // Adds VM to the map of VMs. - vms.rwlock.Lock() - key := vm.ID.String() - vms.m[key] = *vm - vms.rwlock.Unlock() - - return nil + vm.mutex.Lock() + // First, writes VM files since it can fail. + err := vm.writeFiles() + if err != nil { + vm.mutex.Unlock() + return err + } + vm.mutex.Unlock() + + // Adds VM to the map of VMs. + vms.rwlock.Lock() + key := vm.ID.String() + vms.m[key] = *vm + vms.rwlock.Unlock() + + 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) { - vms.rwlock.Lock() - defer vms.rwlock.Unlock() - - vm, err := vms.getVMUnsafe(vmID) - if err != nil { - return 0, "", err - } - - totalRAM, availRAM, err := utils.GetRAM() - if err != nil { - return -1, "", errors.New("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.Ram)*(1.-consts.KsmRamSaving))) - - vms.usedRAM += estimatedVmRAM - - // Checks that at least 15% of the available RAM is 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))) { - vms.usedRAM -= estimatedVmRAM - return -1, "", errors.New("Insufficient free RAM to start VM") - } - - // Locates a free port randomly chosen between VMSpiceMinPort and VMSpiceMaxPort (inclusive). - var port int - for { - port = utils.Rand(consts.VMSpiceMinPort, consts.VMSpiceMaxPort) - if !vms.usedPorts[port] { - if utils.IsPortAvailable(port) { - vms.usedPorts[port] = true - break - } - } - } - - // 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.updateVMMap(vm) - vms.rwlock.Unlock() - } - - vm.mutex.Lock() - pwd, err := vm.start(port, endofExecFn) - vms.updateVMMap(&vm) - vm.mutex.Unlock() - - return port, pwd, err + vms.rwlock.Lock() + defer vms.rwlock.Unlock() + + vm, err := vms.getVMUnsafe(vmID) + if err != nil { + return 0, "", err + } + + totalRAM, availRAM, err := utils.GetRAM() + if err != nil { + return -1, "", errors.New("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.Ram)*(1.-consts.KsmRamSaving))) + + vms.usedRAM += estimatedVmRAM + + // Checks that at least 15% of the available RAM is 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))) { + vms.usedRAM -= estimatedVmRAM + return -1, "", errors.New("Insufficient free RAM to start VM") + } + + // Locates a free port randomly chosen between VMSpiceMinPort and VMSpiceMaxPort (inclusive). + var port int + for { + port = utils.Rand(consts.VMSpiceMinPort, consts.VMSpiceMaxPort) + if !vms.usedPorts[port] { + if utils.IsPortAvailable(port) { + vms.usedPorts[port] = true + break + } + } + } + + // 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.updateVMMap(vm) + vms.rwlock.Unlock() + } + + vm.mutex.Lock() + pwd, err := vm.start(port, endofExecFn) + vms.updateVMMap(&vm) + vm.mutex.Unlock() + + return port, pwd, err } // Kills a VM by its ID. func (vms *VMs)KillVM(vmID uuid.UUID) error { - vms.rwlock.RLock() - defer vms.rwlock.RUnlock() - - vm, err := vms.getVMUnsafe(vmID) - if err != nil { - return err - } - - vm.mutex.Lock() - err = vm.kill() - vm.mutex.Unlock() - return err + vms.rwlock.RLock() + defer vms.rwlock.RUnlock() + + vm, err := vms.getVMUnsafe(vmID) + if err != nil { + return err + } + + vm.mutex.Lock() + err = vm.kill() + vm.mutex.Unlock() + return err } // Gracefully stops a VM by its ID. func (vms *VMs)ShutdownVM(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.shutdown() - vm.mutex.Unlock() - return err + vms.rwlock.Lock() + defer vms.rwlock.Unlock() + + vm, err := vms.getVMUnsafe(vmID) + if err != nil { + return err + } + + vm.mutex.Lock() + err = vm.shutdown() + vm.mutex.Unlock() + 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 + 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() - defer vms.rwlock.RUnlock() - - for _, vm := range vms.m { - if vm.TemplateID.String() == templateID { - return true - } - } - return false + vms.rwlock.RLock() + defer vms.rwlock.RUnlock() + + for _, vm := range vms.m { + if vm.TemplateID.String() == templateID { + return true + } + } + return false } // Edit a VM' specs: name, cpus, ram, nic func (vms *VMs)EditVM(vmID uuid.UUID, name string, cpus, ram int, nic NicType) error { - vm, err := vms.getVMUnsafe(vmID) - if err != nil { - return err - } - - // Only updates fields that have changed. - if name != "" { - vm.Name = name - } - if cpus > 0 { - vm.Cpus = cpus - } - if ram > 0 { - vm.Ram = ram - } - if nic != "" { - vm.Nic = nic - } - - if err = vm.validate(); err != nil { - return err - } - - vm.mutex.Lock() - defer vm.mutex.Unlock() - vms.rwlock.Lock() - defer vms.rwlock.Unlock() - - if err = vms.updateVM(&vm); err != nil { - return err - } - - return nil + vm, err := vms.getVMUnsafe(vmID) + if err != nil { + return err + } + + // Only updates fields that have changed. + if name != "" { + vm.Name = name + } + if cpus > 0 { + vm.Cpus = cpus + } + if ram > 0 { + vm.Ram = ram + } + if nic != "" { + vm.Nic = nic + } + + if err = vm.validate(); err != nil { + return err + } + + vm.mutex.Lock() + defer vm.mutex.Unlock() + vms.rwlock.Lock() + defer vms.rwlock.Unlock() + + if err = vms.updateVM(&vm); err != nil { + return err + } + + return nil } // Set a VM's Access for a given user (email). // loggedUserEmail is the email of the currently logged user // userMail is the email of the user for which to modify the access func (vms *VMs)SetVMAccess(vmID uuid.UUID, loggedUserEmail, userEmail string, newAccess caps.Capabilities) error { - if err := caps.ValidateVMAccessCaps(newAccess); err != nil { - return err - } - - vms.rwlock.Lock() - defer vms.rwlock.Unlock() - - // Retrieves the VM for which the access caps must be changed. - vm, err := vms.getVMUnsafe(vmID) - if err != nil { - return err - } - - vm.mutex.Lock() - defer vm.mutex.Unlock() - - // First, check that the logged user is the VM's owner. - if !vm.IsOwner(loggedUserEmail) { - // Next, checks the logged user has VM_SET_ACCESS set in her/his VM access. - userCaps := vm.Access[loggedUserEmail] - _, exists := userCaps[caps.CAP_VM_SET_ACCESS] - if !exists { - return errors.New("Insufficient capability") - } - } - - vm.Access[userEmail] = newAccess - - if err = vms.updateVM(&vm); err != nil { - return err - } - - return nil + if err := caps.ValidateVMAccessCaps(newAccess); err != nil { + return err + } + + vms.rwlock.Lock() + defer vms.rwlock.Unlock() + + // Retrieves the VM for which the access caps must be changed. + vm, err := vms.getVMUnsafe(vmID) + if err != nil { + return err + } + + vm.mutex.Lock() + defer vm.mutex.Unlock() + + // First, check that the logged user is the VM's owner. + if !vm.IsOwner(loggedUserEmail) { + // Next, checks the logged user has VM_SET_ACCESS set in her/his VM access. + userCaps := vm.Access[loggedUserEmail] + _, exists := userCaps[caps.CAP_VM_SET_ACCESS] + if !exists { + return errors.New("Insufficient capability") + } + } + + vm.Access[userEmail] = newAccess + + if err = vms.updateVM(&vm); err != nil { + return err + } + + return nil } // Remove a VM's Access for a given user (email). // loggedUserEmail is the email of the currently logged user // userMail is the email of the user for which to remove the access func (vms *VMs)DeleteVMAccess(vmID uuid.UUID, loggedUserEmail, userEmail string) error { - vms.rwlock.Lock() - defer vms.rwlock.Unlock() - - // Retrieves the VM for which the access caps must be changed. - vm, err := vms.getVMUnsafe(vmID) - if err != nil { - return err - } - - vm.mutex.Lock() - defer vm.mutex.Unlock() - - // First, check that the logged user is the VM's owner. - if !vm.IsOwner(loggedUserEmail) { - // Next, checks the logged user has VM_SET_ACCESS set in her/his VM access. - userCaps := vm.Access[loggedUserEmail] - _, exists := userCaps[caps.CAP_VM_SET_ACCESS] - if !exists { - return errors.New("Insufficient capability") - } - } - - // Only removes the user from the Access map if it actually had an access. - if _, exists := vm.Access[userEmail]; exists { - delete(vm.Access, userEmail) - } else { - return errors.New("User "+userEmail+" has no VM access") - } - - if err = vms.updateVM(&vm); err != nil { - return err - } - - return nil + vms.rwlock.Lock() + defer vms.rwlock.Unlock() + + // Retrieves the VM for which the access caps must be changed. + vm, err := vms.getVMUnsafe(vmID) + if err != nil { + return err + } + + vm.mutex.Lock() + defer vm.mutex.Unlock() + + // First, check that the logged user is the VM's owner. + if !vm.IsOwner(loggedUserEmail) { + // Next, checks the logged user has VM_SET_ACCESS set in her/his VM access. + userCaps := vm.Access[loggedUserEmail] + _, exists := userCaps[caps.CAP_VM_SET_ACCESS] + if !exists { + return errors.New("Insufficient capability") + } + } + + // Only removes the user from the Access map if it actually had an access. + if _, exists := vm.Access[userEmail]; exists { + delete(vm.Access, userEmail) + } else { + return errors.New("User "+userEmail+" has no VM access") + } + + if err = vms.updateVM(&vm); err != nil { + return err + } + + return nil } // Exports a VM's directory and its subdirectories into a tar.gz archive on the host. func (vms *VMs)ExportVMFiles(vm *VM, vmDir, tarGzFile string) error { - vmDisk := vm.getDiskPath() - return exec.CopyFromVM(vmDisk, vmDir, tarGzFile) + vmDisk := vm.getDiskPath() + return exec.CopyFromVM(vmDisk, vmDir, tarGzFile) } // Imports files from a tar.gz archive into a VM's filesystem, in a specified directory. func (vms *VMs)ImportFilesToVM(vm *VM, tarGzFile, vmDir string) error { - // Marks the VM to copy from as being busy. - if err := vms.setDiskBusy(vm); err != nil { - return errors.New("Failed setting disk busy flag during VM files import: "+err.Error()) - } - // Clears the VM from being busy. - defer vms.clearDiskBusy(vm) - - vmDisk := vm.getDiskPath() - return exec.CopyToVM(vmDisk, tarGzFile, vmDir) + // Marks the VM to copy from as being busy. + if err := vms.setDiskBusy(vm); err != nil { + return errors.New("Failed setting disk busy flag during VM files import: "+err.Error()) + } + // Clears the VM from being busy. + defer vms.clearDiskBusy(vm) + + vmDisk := vm.getDiskPath() + return exec.CopyToVM(vmDisk, tarGzFile, vmDir) } // Marks a VM as "busy", meaning its disk file is being accessed for a possibly long time. func (vms *VMs)setDiskBusy(vm *VM) error { - vm.mutex.Lock() - defer vm.mutex.Unlock() + vm.mutex.Lock() + defer vm.mutex.Unlock() - if vm.IsRunning() { - return errors.New("Only a non-running VM can be set to busy") - } + if vm.IsRunning() { + return errors.New("Only a non-running VM can be set to busy") + } - vm.DiskBusy = true + vm.DiskBusy = true - vms.rwlock.Lock() - defer vms.rwlock.Unlock() + vms.rwlock.Lock() + defer vms.rwlock.Unlock() - vms.updateVMMap(vm) + vms.updateVMMap(vm) - return nil + return nil } func (vms *VMs)clearDiskBusy(vm *VM) error { - vm.mutex.Lock() - defer vm.mutex.Unlock() + vm.mutex.Lock() + defer vm.mutex.Unlock() - if vm.IsRunning() { - return errors.New("Only a non-running VM can have its busy flag cleared") - } + if vm.IsRunning() { + return errors.New("Only a non-running VM can have its busy flag cleared") + } - vm.DiskBusy = false + vm.DiskBusy = false - vms.rwlock.Lock() - defer vms.rwlock.Unlock() + vms.rwlock.Lock() + defer vms.rwlock.Unlock() - vms.updateVMMap(vm) + vms.updateVMMap(vm) - return nil + return nil } // Updates a VM in the map of VMs and writes its updated config file. func (vms *VMs)updateVM(vm *VM) error { - err := vm.writeConfig() - if err != nil { - return err - } + err := vm.writeConfig() + if err != nil { + return err + } - vms.updateVMMap(vm) - return nil + vms.updateVMMap(vm) + return nil } // Updates a VM in the map of VMs. func (vms *VMs)updateVMMap(vm *VM) { - key := vm.ID.String() - delete(vms.m, key) - vms.m[key] = *vm + key := vm.ID.String() + delete(vms.m, key) + vms.m[key] = *vm } diff --git a/tools/genpwd/src/genpwd.go b/tools/genpwd/src/genpwd.go index 218e7b9c622313427596e0f36e5b842147b69346..041ec85550dea9c7d417a6b73b37df6ab515578a 100644 --- a/tools/genpwd/src/genpwd.go +++ b/tools/genpwd/src/genpwd.go @@ -1,32 +1,32 @@ package main import ( - "os" - "fmt" - "syscall" - "golang.org/x/term" - "golang.org/x/crypto/bcrypt" + "os" + "fmt" + "syscall" + "golang.org/x/term" + "golang.org/x/crypto/bcrypt" ) func main() { - fmt.Printf("Enter password: ") + fmt.Printf("Enter password: ") - bytePwd, err := term.ReadPassword(int(syscall.Stdin)) + bytePwd, err := term.ReadPassword(int(syscall.Stdin)) if err != nil { - fmt.Println(err.Error()) + fmt.Println(err.Error()) os.Exit(1) } - pwd := string(bytePwd) + pwd := string(bytePwd) - hashedPwd, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost) - stringPwd := string(hashedPwd) + hashedPwd, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost) + stringPwd := string(hashedPwd) - fmt.Println("") - fmt.Println(stringPwd) + fmt.Println("") + fmt.Println(stringPwd) - // if err := bcrypt.CompareHashAndPassword(hashedPwd, []byte("pipo")); err != nil { - // fmt.Println("don't match!") - // } else { - // fmt.Println("OK") - // } + // if err := bcrypt.CompareHashAndPassword(hashedPwd, []byte("pipo")); err != nil { + // fmt.Println("don't match!") + // } else { + // fmt.Println("OK") + // } }