From f01c77e10321dbaf45359da6087e029309880be3 Mon Sep 17 00:00:00 2001
From: Florent Gluck <florent.gluck@hesge.ch>
Date: Thu, 1 Sep 2022 10:32:11 +0200
Subject: [PATCH] Implemented 2 new routes which allow users to copy a local
 directory into a VM: GET  /vms/importfiles POST /vms/{id}/importfiles

---
 README.md               | 30 ++++++++++++++++--------------
 src/exec/VirtCopyIn.go  |  5 ++---
 src/nexus-server.go     |  6 +++---
 src/router/router.go    |  1 +
 src/router/routerVMs.go | 29 ++++++++++++++++++++---------
 src/utils/utils.go      |  4 ++--
 6 files changed, 44 insertions(+), 31 deletions(-)

diff --git a/README.md b/README.md
index 5a66712..bc76ef9 100644
--- a/README.md
+++ b/README.md
@@ -356,20 +356,20 @@ Legend for the "Done" column:
 |  x   | `/vms/editaccess`        | returns VMs that can have their access changed | GET    |            | `VM_SET_ACCESS`  | AND | `VM_SET_ACCESS`     |
 |  x   | `/vms/del`               | returns VMs that can be deleted                | GET    |            | `VM_DESTROY_ANY` | OR  | `VM_DESTROY`        |
 |  x   | `/vms/exportdir`         | returns VMs that can have a dir downloaded     | GET    |            | `VM_READFS_ANY`  | OR  | `VM_READFS`         |
-|      | `/vms/importdir`         | returns VMs allowing a fs upload               | GET    |            | `VM_WRITEFS_ANY` | OR  | `VM_WRITEFS`        |
-
-| Done | Route                      | Description                | Method | Parameters                 | Req. user cap.   | Op. | Req. VM access cap. |
-|---   |---                         |---                         |---     |---                         |---               |---  |---                  |
-|  x   | `/vms`                     | create a VM                | POST   | name,cpus,ram,nic,template | `VM_CREATE`      |     |                     |
-|  x   | `/vms/{id}`                | delete a VM                | DELETE |                            | `VM_DESTROY_ANY` | OR  | `VM_DESTROY`        |
-|  x   | `/vms/{id}`                | edit a VM                  | PUT    | name,cpus,ram,nic          | `VM_EDIT_ANY`    | OR  | `VM_EDIT`           |
-|  x   | `/vms/{id}/start`          | start a VM                 | PUT    |                            | `VM_START_ANY`   | OR  | `VM_START`          |
-|  x   | `/vms/{id}/stop`           | stop a VM (by force)       | PUT    |                            | `VM_STOP_ANY`    | OR  | `VM_STOP`           |
-|  x   | `/vms/{id}/shutdown`       | gracefully shutdown a VM   | PUT    |                            | `VM_STOP_ANY`    | OR  | `VM_STOP`           |
-|  x   | `/vms/{id}/access/{email}` | set VM access for a user   | PUT    | caps                       | `VM_SET_ACCESS`  | AND | `VM_SET_ACCESS`     |
-|  x   | `/vms/{id}/access/{email}` | del VM access for a user   | DELETE |                            | `VM_SET_ACCESS`  | AND | `VM_SET_ACCESS`     |
-|  x   | `/vms/{id}/exportdir`      | download a VM's directory  | GET    | dir                        | `VM_READFS_ANY`  | OR  | `VM_READFS`         |
-|      | `/vms/{id}/importfiles`    | upload files in a VM's dir | POST   | files (.tar archive),dir   | `VM_WRITEFS_ANY` | OR  | `VM_WRITEFS`        |
+|  x   | `/vms/importfiles`       | returns VMs allowing files upload              | GET    |            | `VM_WRITEFS_ANY` | OR  | `VM_WRITEFS`        |
+
+| Done | Route                      | Description                  | Method | Parameters                 | Req. user cap.   | Op. | Req. VM access cap. |
+|---   |---                         |---                           |---     |---                         |---               |---  |---                  |
+|  x   | `/vms`                     | create a VM                  | POST   | name,cpus,ram,nic,template | `VM_CREATE`      |     |                     |
+|  x   | `/vms/{id}`                | delete a VM                  | DELETE |                            | `VM_DESTROY_ANY` | OR  | `VM_DESTROY`        |
+|  x   | `/vms/{id}`                | edit a VM                    | PUT    | name,cpus,ram,nic          | `VM_EDIT_ANY`    | OR  | `VM_EDIT`           |
+|  x   | `/vms/{id}/start`          | start a VM                   | PUT    |                            | `VM_START_ANY`   | OR  | `VM_START`          |
+|  x   | `/vms/{id}/stop`           | stop a VM (by force)         | PUT    |                            | `VM_STOP_ANY`    | OR  | `VM_STOP`           |
+|  x   | `/vms/{id}/shutdown`       | gracefully shutdown a VM     | PUT    |                            | `VM_STOP_ANY`    | OR  | `VM_STOP`           |
+|  x   | `/vms/{id}/access/{email}` | set VM access for a user     | PUT    | caps                       | `VM_SET_ACCESS`  | AND | `VM_SET_ACCESS`     |
+|  x   | `/vms/{id}/access/{email}` | del VM access for a user     | DELETE |                            | `VM_SET_ACCESS`  | AND | `VM_SET_ACCESS`     |
+|  x   | `/vms/{id}/exportdir`      | download a VM's dir          | GET    | dir                        | `VM_READFS_ANY`  | OR  | `VM_READFS`         |
+|  x   | `/vms/{id}/importfiles`    | upload files into a VM's dir | POST   | files (.tar archive),dir   | `VM_WRITEFS_ANY` | OR  | `VM_WRITEFS`        |
 
 ### Template management
 
@@ -414,6 +414,8 @@ Note that package `guestfish` is required, notably the `virt-copy-out` and `virt
 sudo chmod 0644 /boot/vmlinuz*
 ```
 
+**BEWARE**: whenever a new kernel is installed, its permission **must be changed again**, otherwise importing or exporting files from VMs won't work!
+
 ### Prerequisites
 
 The host machine on which **nexus-server** will be running requires the following software:
diff --git a/src/exec/VirtCopyIn.go b/src/exec/VirtCopyIn.go
index 35c1fb4..c10dd7c 100644
--- a/src/exec/VirtCopyIn.go
+++ b/src/exec/VirtCopyIn.go
@@ -34,13 +34,12 @@ func CheckVirtCopyIn() error {
 // into a directory on the VM's filesystem (vmDir).
 // Note: both directories must exist, otherwise the command will fail.
 func CopyToVM(vmDiskFile, localDir, vmDir string) error {
-	log.Info("localDir = ",localDir)
 	cmd := exec.Command(virtcopyinBinary, "-a", vmDiskFile, localDir, vmDir)
 	stdoutStderr, err := cmd.CombinedOutput()
 	if err != nil {
 		output := fmt.Sprintf("[%s]", stdoutStderr)
-		msg := "Failed writing to \""+vmDir+"\" in qcow ("+vmDiskFile+"): "+output
-		log.Error(msg)
+		msg := "Failed writing to \""+vmDir+"\" in qcow ("+vmDiskFile+")"
+		log.Error(msg+": "+output)
 		return errors.New(msg)
 	}
 	return nil
diff --git a/src/nexus-server.go b/src/nexus-server.go
index 7634593..e40c13a 100644
--- a/src/nexus-server.go
+++ b/src/nexus-server.go
@@ -37,9 +37,9 @@ func main() {
 	if err := exec.CheckVirtCopyOut(); err != nil {
 		log.Fatal(err)
 	}
-	if err := exec.CheckVirtCopyIn(); err != nil {
-		log.Fatal(err)
-	}
+	// if err := exec.CheckVirtCopyIn(); err != nil {
+	// 	log.Fatal(err)
+	// }
 
 	usage := func() {
 		fmt.Println("Usage of "+path.Base(os.Args[0])+":")
diff --git a/src/router/router.go b/src/router/router.go
index edf6844..1682d9f 100644
--- a/src/router/router.go
+++ b/src/router/router.go
@@ -70,6 +70,7 @@ func (router *Router)Start(port int) {
 	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)
diff --git a/src/router/routerVMs.go b/src/router/routerVMs.go
index f8fa7e4..8cb4557 100644
--- a/src/router/routerVMs.go
+++ b/src/router/routerVMs.go
@@ -135,6 +135,17 @@ func (r *RouterVMs)GetDirExportableVMs(c echo.Context) error {
 	})
 }
 
+// Returns VMs that can have files imported (i.e. copied) into their filesystem.
+// Requires either capability:
+// User cap: VM_WRITEFS_ANY: returns all VMs.
+// 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
+	})
+}
+
 // Creates a VM.
 // Requires this capability:
 // User cap: CAP_VM_CREATE.
@@ -476,10 +487,11 @@ func (r *RouterVMs)ExportVMDir(c echo.Context) error {
 // 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 {
-		// Retrieve the "dir" argument
-		vmDir := c.FormValue("dir")
+		// Retrieves the various client arguments.
+		vmDir := c.FormValue("vmDir")
+		localDir := c.FormValue("localDir")
 
-		// Retrieve the tar archive (uploadedTarFile)
+		// Retrieves the tar archive (uploadedTarFile).
 		tarFile, err := c.FormFile("file")
 		if err != nil {
 			return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
@@ -503,19 +515,20 @@ func (r *RouterVMs)ImportFilesToVM(c echo.Context) error {
 			return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
 		}
 		defer dst.Close()
-		//defer os.Remove(uploadedTarFile)
+		defer os.Remove(uploadedTarFile)
 	
 		if _, err = io.Copy(dst, src); err != nil {
 			return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
 		}
 
-		// Extract the archive into tmpExtractDir
-		tmpExtractDir := filepath.Join(tmpDir, "extract_"+uuid.String())
+		// Extracts the archive into tmpExtractDir.
+		tmpExtractDir := filepath.Join(tmpDir, "upload_"+uuid.String())
 		if err := os.Mkdir(tmpExtractDir, 0750); err != nil {
 			msg := "ImportFilesToVM: failed creating dir: "+err.Error()
 			log.Error(msg)
 			return echo.NewHTTPError(http.StatusInternalServerError, msg)
 		}
+		defer os.RemoveAll(tmpExtractDir)
 
 		if err := utils.Untar(uploadedTarFile, tmpExtractDir); err != nil {
 			msg := "ImportFilesToVM: failed unarchiving tarball: "+err.Error()
@@ -525,11 +538,9 @@ func (r *RouterVMs)ImportFilesToVM(c echo.Context) error {
 		
 		// Copy the archive's files into the VM
 		// IMPORTANT: check the VM is NOT running!
-		if err = r.vms.ImportFilesToVM(vm, tmpExtractDir, vmDir); err != nil {
+		if err = r.vms.ImportFilesToVM(vm, filepath.Join(tmpExtractDir, localDir), vmDir); err != nil {
 			return echo.NewHTTPError(http.StatusBadRequest, err.Error())
 		}
-
-		// Remove the files and the archive
 	
 		return c.JSONPretty(http.StatusCreated, jsonMsg("OK"), "    ")
 	})
diff --git a/src/utils/utils.go b/src/utils/utils.go
index 24392a5..32bf340 100644
--- a/src/utils/utils.go
+++ b/src/utils/utils.go
@@ -117,7 +117,7 @@ func IsPortAvailable(port int) bool {
 
 // 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 taken from: https://golangdocs.com/tar-gzip-in-golang
+// Source code slightly modified from: https://golangdocs.com/tar-gzip-in-golang
 func TarDir(srcDir, destTarball string) error {
 	tarFile, err := os.Create(destTarball)
     if err != nil {
@@ -130,7 +130,7 @@ func TarDir(srcDir, destTarball string) error {
  
     info, err := os.Stat(srcDir)
     if err != nil {
-        return nil
+        return err
     }
  
     var baseDir string
-- 
GitLab