diff --git a/config/server/nexus.conf b/config/server/nexus.conf index 8eab2815fb445638f7e39edf98c86075357a022f..7e1cd386f74710504544b70c8eb6f3ef473a4e71 100644 --- a/config/server/nexus.conf +++ b/config/server/nexus.conf @@ -27,7 +27,7 @@ locked_time_hours = 4.0 [limits] -max_upload_size = 16M +max_upload_size = 128M # We estimate that KVM allows for this amount of RAM saving in % (due to page sharing across VMs). # 30% seems to be a pretty conservative estimate. diff --git a/src/client/cmdVM/vmImportDir.go b/src/client/cmdVM/vmImportDir.go index 03cb894e34c99d51348a94d0020a24d2ec542275..f1a22388099342174073b97b501630a02975ff33 100644 --- a/src/client/cmdVM/vmImportDir.go +++ b/src/client/cmdVM/vmImportDir.go @@ -2,6 +2,7 @@ package cmdVM import ( u "nexus-client/utils" + "nexus-common/utils" libclient "nexus-libclient/vm" "os" @@ -59,7 +60,7 @@ func (cmd *ImportDir) Run(args []string) int { statusCode := 0 - tmpTarGzFile, err := u.GetRandomTempFilename() + tmpTarGzFile, err := utils.GetRandomTempFilename() if err != nil { u.PrintlnErr(err) return 1 diff --git a/src/client/utils/files.go b/src/client/utils/files.go index 0f0f7fd6488c2bacc319f0143262084455e157d0..c8dcf2d4c0164d984efb4a7873485b0eb61fa4be 100644 --- a/src/client/utils/files.go +++ b/src/client/utils/files.go @@ -10,8 +10,6 @@ import ( "os" "path/filepath" "strings" - - "github.com/google/uuid" ) // Creates a tar.gz archive of dir and all its files and subdirectories. @@ -72,13 +70,3 @@ func TarGzDir(dir, archive string) error { return nil } - -func GetRandomTempFilename() (string, error) { - tempDir := os.TempDir() - uuid, err := uuid.NewRandom() - if err != nil { - return "", errors.New("Failed creating random UUID: " + err.Error()) - } - randName := "temp_" + uuid.String() - return filepath.Join(tempDir, randName), nil -} diff --git a/src/common/utils/files.go b/src/common/utils/files.go index 729503afc05dc11609c0eb56374d0095a4e16865..600f5fb4a8901b27bffd29dc2ab5c3f952e05603 100644 --- a/src/common/utils/files.go +++ b/src/common/utils/files.go @@ -3,6 +3,9 @@ package utils import ( "errors" "os" + "path/filepath" + + "github.com/google/uuid" ) // Returns true if the specified file exists, false otherwise. @@ -10,3 +13,28 @@ func FileExists(filename string) bool { _, err := os.Stat(filename) return !errors.Is(err, os.ErrNotExist) } + +func GetRandomTempFilename() (string, error) { + tempDir := os.TempDir() + uuid, err := uuid.NewRandom() + if err != nil { + return "", errors.New("Failed creating random UUID: " + err.Error()) + } + randName := "temp_" + uuid.String() + return filepath.Join(tempDir, randName), nil +} + +func CreateRandomEmptyFile() (string, error) { + filePath, err := GetRandomTempFilename() + if err != nil { + return "", err + } + + file, err := os.Create(filePath) + if err != nil { + return "", err + } + file.Close() + + return filePath, nil +} diff --git a/src/server/exec/Guestfish.go b/src/server/exec/Guestfish.go index e30750c9ed5110d275b953ccc052db1bec89824c..f3c649c6847e7c506352f1e0a4f9403f89336286 100644 --- a/src/server/exec/Guestfish.go +++ b/src/server/exec/Guestfish.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os/exec" + "path/filepath" "strings" ) @@ -22,27 +23,55 @@ func CheckGuestfish() error { 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(conf.Tools.Guestfish, "--rw", "-i", "tar-in", "-a", vmDiskFile, tarGzFile, vmDir, "compress:gzip") +// Uploads and unarchives a local tar.gz archive into a directory (vmDir) inside the VM's filesystem. +func DearchiveToVM(vmDiskFile, localTarGzFile, vmDir string) error { + cmd := exec.Command(conf.Tools.Guestfish, "--rw", "-i", "tar-in", "-a", vmDiskFile, localTarGzFile, vmDir, "compress:gzip") stdoutStderr, err := cmd.CombinedOutput() if err != nil { output := fmt.Sprintf("[%s]", stdoutStderr) - msg := "Failed writing to \"" + vmDir + "\" in qcow (" + vmDiskFile + ")" + msg := "Failed dearchiving to \"" + vmDir + "\" in VM" 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(conf.Tools.Guestfish, "--ro", "-i", "tar-out", "-a", vmDiskFile, vmDir, tarGzFile, "compress:gzip") +// Uploads a local file into a directory (vmDir) inside the VM's filesystem. +func CopyToVM(vmDiskFile, localFile, vmDir string) error { + filename := filepath.Base(localFile) + cmd := exec.Command(conf.Tools.Guestfish, "--rw", "-i", "upload", "-a", vmDiskFile, localFile, filepath.Join(vmDir, filename)) stdoutStderr, err := cmd.CombinedOutput() if err != nil { output := fmt.Sprintf("[%s]", stdoutStderr) - msg := "Failed reading \"" + vmDir + "\" in qcow (" + vmDiskFile + "): " + output - log.Error(msg) + msg := "Failed copying file to \"" + vmDir + "\" in VM" + log.Error(msg + ": " + output) + return errors.New(msg) + } + return nil +} + +// Recursively downloads a directory from the VM's filesystem (vmDir) into a local tar.gz archive. +func ArchiveFromVM(vmDiskFile, vmDir, localTarGzFile string) error { + cmd := exec.Command(conf.Tools.Guestfish, "--ro", "-i", "tar-out", "-a", vmDiskFile, vmDir, localTarGzFile, "compress:gzip") + stdoutStderr, err := cmd.CombinedOutput() + if err != nil { + output := fmt.Sprintf("[%s]", stdoutStderr) + msg := "Failed archiving \"" + vmDir + "\" in VM" + log.Error(msg + ": " + output) + return errors.New(msg) + } + return nil +} + +// Delete a file from a VM's filesystem. +// filePath is the full path in the VM to the file to delete. +func DeleteFromVM(vmDiskFile, filePath string) error { + cmd := exec.Command(conf.Tools.Guestfish, "--rw", "-a", vmDiskFile, "-i", "rm", filePath) + stdoutStderr, err := cmd.CombinedOutput() + if err != nil { + output := fmt.Sprintf("[%s]", stdoutStderr) + msg := "Failed deleting \"" + filePath + "\" in VM" + log.Error(msg + ": " + output) return errors.New(msg) } return nil diff --git a/src/server/nexus-server.go b/src/server/nexus-server.go index ac9436a7541f072d21d314fe5423253f76263a2c..3158f816e59ddfa9a54c96d894b49f2d64e55886 100644 --- a/src/server/nexus-server.go +++ b/src/server/nexus-server.go @@ -47,8 +47,17 @@ func main() { cleaner.Start() - log.Info("Using port range [", conf.Core.VMSpiceMinPort, "-", conf.Core.VMSpiceMaxPort, "]") + log.Info("API port: ", conf.Core.APIDefaultPort) + log.Info("Spice port range [", conf.Core.VMSpiceMinPort, "-", conf.Core.VMSpiceMaxPort, "]") log.Info("Tmp directory: ", conf.Core.TmpDir) + log.Info("Max upload size: ", conf.Limits.MaxUploadSize) + log.Info("KSM RAM saving: ", conf.Limits.KsmRamSaving*100, "%") + log.Info("RAM usage limit: ", conf.Limits.RamUsageLimit*100, "%") + + log.Info("Max login attemps: ", conf.Auth.MaxLoginAttempts) + log.Info("Account lock time: ", conf.Auth.LockedTimeInHours, " hours") + log.Info("Access token expiration time: ", conf.Auth.AccessTokenExpirationTimeInHours, " hours") + router.New().Start(conf.Core.APIDefaultPort) } diff --git a/src/server/router/routerVMs.go b/src/server/router/routerVMs.go index 2ba356b8fb28ba73b81e996019c685cf34b262d8..0c88b380eaf8f2fc93e51522b163150cdd250f9c 100644 --- a/src/server/router/routerVMs.go +++ b/src/server/router/routerVMs.go @@ -6,8 +6,11 @@ import ( "net/http" "nexus-common/caps" "nexus-common/params" + "nexus-common/utils" + "nexus-server/exec" "nexus-server/users" "nexus-server/vms" + "os" "path/filepath" @@ -595,7 +598,7 @@ func (r *RouterVMs) ExportVMDir(c echo.Context) error { tarGzFile := filepath.Join(conf.Core.TmpDir, "exportdir_"+vm.GetID().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 { + if err := r.vms.ExportFilesFromVM(vm, p.Dir, tarGzFile); err != nil { msg := "Failed extracting VM " + vmID + "'s dir: " + err.Error() log.Error(msg) return echo.NewHTTPError(http.StatusBadRequest, msg) @@ -621,6 +624,31 @@ func (r *RouterVMs) ImportFilesToVM(c echo.Context) error { // Retrieves the various client arguments. vmDir := c.FormValue("vmDir") + // Checks if importing files into the VM would be successfull. + // The purpose of this function is to quickly checks if the import would succeed + // without having to wait for the archive to be uploaded from the client (which can take a while). + // It attempts to create an empty file inside the VM directory. + + // Create a local file on the host + emptyLocalFile, err := utils.CreateRandomEmptyFile() + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + // Remove the local file on the host when exiting the function + defer os.Remove(emptyLocalFile) + + // Copy the local file from the host to the VM in vmDir + if err := r.vms.ImportFilesToVM(vm, exec.CopyToVM, emptyLocalFile, vmDir); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + // Remove the file we just copied in the VM + filename := filepath.Base(emptyLocalFile) + if err := r.vms.DeleteFileFromVM(vm, filepath.Join(vmDir, filename)); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + // Retrieves the tar.gz archive (uploadedtarGzFile). tarGzFile, err := c.FormFile("file") if err != nil { @@ -652,7 +680,7 @@ func (r *RouterVMs) ImportFilesToVM(c echo.Context) error { // Copy the archive's files into the VM // IMPORTANT: the VM cannot have its disk busy - if err = r.vms.ImportFilesToVM(vm, uploadedtarGzFile, vmDir); err != nil { + if err = r.vms.ImportFilesToVM(vm, exec.DearchiveToVM, uploadedtarGzFile, vmDir); err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } diff --git a/src/server/vms/vms.go b/src/server/vms/vms.go index 657a22e2d402a76dd7d36d117502862cc519d76a..13a6df5a33db30af2efd6b8dbc9130e0103963de 100644 --- a/src/server/vms/vms.go +++ b/src/server/vms/vms.go @@ -777,7 +777,7 @@ func (vms *VMs) DeleteVMAccess(vmID uuid.UUID, user *users.User, destUserEmail s // Technically, extracting files from a running VM should work, but some files might be inconsistent. // In consequence, we forbid this action on a running VM. // Concurrency: safe -func (vms *VMs) ExportVMFiles(vm *VM, vmDir, tarGzFile string) error { +func (vms *VMs) ExportFilesFromVM(vm *VM, vmDir, localTarGzFile string) error { prefix := "Failed exporting files from VM: " vm.mutex.Lock() @@ -797,7 +797,7 @@ func (vms *VMs) ExportVMFiles(vm *VM, vmDir, tarGzFile string) error { vmDisk := vm.getDiskPath() - if err := exec.CopyFromVM(vmDisk, vmDir, tarGzFile); err != nil { + if err := exec.ArchiveFromVM(vmDisk, vmDir, localTarGzFile); err != nil { vm.mutex.Lock() vm.DiskBusy = false vm.mutex.Unlock() @@ -815,7 +815,11 @@ func (vms *VMs) ExportVMFiles(vm *VM, vmDir, tarGzFile string) error { // Imports files from a tar.gz archive into a VM disk image (guest's filesystem), in a specified directory. // Concurrency: safe -func (vms *VMs) ImportFilesToVM(vm *VM, tarGzFile, vmDir string) error { +//func (vms *VMs) ImportFilesToVM(vm *VM, localTarGzFile, vmDir string) error { + +type ImportFileFunc func(vmDiskFile, localFile, vmDir string) error + +func (vms *VMs) ImportFilesToVM(vm *VM, fn ImportFileFunc, localFile, vmDir string) error { prefix := "Failed importing files into VM: " vm.mutex.Lock() @@ -835,7 +839,7 @@ func (vms *VMs) ImportFilesToVM(vm *VM, tarGzFile, vmDir string) error { vmDisk := vm.getDiskPath() - if err := exec.CopyToVM(vmDisk, tarGzFile, vmDir); err != nil { + if err := fn(vmDisk, localFile, vmDir); err != nil { vm.mutex.Lock() vm.DiskBusy = false vm.mutex.Unlock() @@ -850,3 +854,32 @@ func (vms *VMs) ImportFilesToVM(vm *VM, tarGzFile, vmDir string) error { return nil } + +// Delete a file from a VM's filesystem. +// filePath is the full path in the VM to the file to delete. +func (vms *VMs) DeleteFileFromVM(vm *VM, filePath string) error { + prefix := "Failed deleting file from VM: " + + vm.mutex.Lock() + defer vm.mutex.Unlock() + + if vm.IsRunning() { + return errors.New(prefix + "VM must be stopped") + } + + if vm.IsDiskBusy() { + return errors.New(prefix + "disk in use (busy)") + } + + vm.DiskBusy = true + defer func(vm *VM) { + vm.DiskBusy = false + }(vm) + + if err := exec.DeleteFromVM(vm.getDiskPath(), filePath); err != nil { + msg := prefix + err.Error() + log.Error(msg) + return errors.New(msg) + } + return nil +}