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

Implemented new vmimportdir command which allow users to copy a local directory into a VM

parent 5c13b77a
No related branches found
No related tags found
No related merge requests found
......@@ -148,6 +148,9 @@ vmexportdir Exports one or more VMs' directory into one or more tar arch
Creates one archive per VM (regex).
Requires VM_READFS VM access capability or VM_READFS_ANY user capability.
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
vmimportdir Copy a local directory and all its content to one or more VMs (regex).
Requires VM_WRITEFS VM access capability or VM_WRITEFS_ANY user capability.
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
tpllist Lists available templates (regex).
Requires TPL_LIST or TPL_LIST_ANY user capabilities.
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
......@@ -542,11 +545,21 @@ First, you need to create the VM that will be used by the students during the ex
![](img/vmattach.jpg)
1. Configure the VM to suit your needs, by:
- upgrading the system with `sudo apt-get update && apt-get upgrade`
- upgrading the system with `sudo apt-get update && apt-get upgrade` (not required)
- installing applications required for the exam, e.g. compilers, tools, editors, etc.
Once done, you must shutdown the VM.
<!--
- installing the QEMU Guest Agent with `sudo apt-get install qemu-guest-agent` (so you will be able to use the `vmshutdown` command)
- installing the applications, compilers, tools, editors, etc. that are required for the exam
- copying to the desktop the file describing the exam's objectives
Once done, you can shutdown the VM.
-->
1. Add the exam' specific material. All materials (questions, code, etc.) can be copied from your local machine to the VM with `vmimportdir`. Here, we copy the "exam" directory into the VM's Desktop (`/home/nexus/Desktop`):
```
vmimportdir "exam progsys" exam /home/nexus/Desktop
```
The command displays the directory was copied successfully:
```
Successfully copied "exam" into "/home/nexus/Desktop" in VM "Exam ProgSys Oct2022"
```
1. Now that the VM is ready for the exam, create a template from it. The template must be `private` as we don't want anyone else to access it. Let's choose "Exam ProgSys Oct2022" as the template name (template creation takes several minutes, the larger the VM, the longer). The first argument is the VM ID (as displayed when the VM was created earlier):
```
......@@ -609,9 +622,9 @@ First, you need to create the VM that will be used by the students during the ex
1. You can now print the PDF above, cut each line and give each student the strip of paper for their VM. Once a student has completed their exam, they shutdown their VM and give you the strip of paper back. Note that you can also shutdown the VM yourself with `nexus-cli vmshutdown` (provided the guest OS is running `qemu-guest-agent`). You can also force a VM to be stopped with `nexus-cli vmstop` although it's not recommended as it might corrupt the VM's filesystem.
1. To correct the students' exams, simply download the relevant files to your computer using `vmexportdir`. As an example, the command below extracts and downloads the `/home` directory of all VMs matching "Live Exam ProgSys" (each directory is saved in a `.tar` archive named after the VM's name):
1. To correct the students' exams, simply download the relevant files to your computer using `vmexportdir`. As an example, the command below extracts and downloads the `/home/nexus` directory of all VMs matching "Live Exam ProgSys" (each directory is saved in a `.tar` archive named after the VM's name):
```
vmexportdir "live exam progsys" /home
vmexportdir "live exam progsys" /home/nexus
```
This command displays each exported file tree:
```
......
package cmdVM
import (
"os"
u "nexus-client/utils"
g "nexus-client/globals"
)
type ImportDir struct {
Name string
}
func (cmd *ImportDir)GetName() string {
return cmd.Name
}
func (cmd *ImportDir)GetDesc() []string {
return []string{
"Copy a local directory and all its content to one or more VMs (regex).",
"Requires VM_WRITEFS VM access capability or VM_WRITEFS_ANY user capability."}
}
func (cmd *ImportDir)PrintUsage() {
for _, desc := range cmd.GetDesc() {
u.PrintlnErr(desc)
}
u.PrintlnErr("―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――")
u.PrintlnErr("USAGE: ",cmd.GetName()," [ID ...] [regex ...] localDir vmDir")
u.PrintlnErr("―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――")
u.PrintlnErr("\"localDir\" is the directory on the local filesystem to be copied into the VM.")
u.PrintlnErr("\"vmDir\" is the directory in the VM where localDir will be copied.")
printRegexUsageDetails()
}
func (cmd *ImportDir)Run(args []string) int {
client := g.GetInstance().Client
host := g.GetInstance().Host
argc := len(args)
if argc < 3 {
cmd.PrintUsage()
return 1
}
vmDir := args[argc-1]
localDir := args[argc-2]
vms, err := getFilteredVMs("/vms/importfiles", args[:argc-2])
if err != nil {
u.PrintlnErr("Error: "+err.Error())
return 1
}
if len(vms) == 0 {
u.PrintlnErr("No match.")
return 1
}
statusCode := 0
tmpTarFile, err := u.GetRandomTempFilename()
if err != nil {
u.PrintlnErr(err.Error())
return 1
}
tmpTarFile += ".tar"
defer os.Remove(tmpTarFile)
if err := u.TarDir(localDir, tmpTarFile); err != nil {
u.PrintlnErr(err.Error())
return 1
}
for _, vm := range(vms) {
uuid := vm.ID.String()
resp, err := client.R().SetFile("file", tmpTarFile).
SetFormData(map[string]string {
"vmDir": vmDir,
"localDir": localDir,
}).Post(host+"/vms/"+uuid+"/importfiles")
if err != nil {
u.PrintlnErr("Failed copying \""+localDir+"\" into \""+vmDir+"\" in VM \""+vm.Name+"\": "+err.Error())
statusCode = 1
} else {
if resp.IsSuccess() {
u.Println("Successfully copied \""+localDir+"\" into \""+vmDir+"\" in VM \""+vm.Name+"\"")
} else {
u.PrintlnErr("Failed copying \""+localDir+"\" into \""+vmDir+"\" in VM \""+vm.Name+"\": "+resp.Status()+": "+resp.String())
statusCode = 1
}
}
}
return statusCode
}
......@@ -48,6 +48,7 @@ var cmdList = []cmd.Command {
&cmdVM.SetAccess{"vmsetaccess"},
&cmdVM.DelAccess{"vmdelaccess"},
&cmdVM.ExportDir{"vmexportdir"},
&cmdVM.ImportDir{"vmimportdir"},
&cmdTemplate.List{"tpllist"},
&cmdTemplate.Create{"tplcreate"},
......
......@@ -53,6 +53,7 @@ var cmdList = []cmd.Command {
&cmdVM.SetAccess{"vmsetaccess"},
&cmdVM.DelAccess{"vmdelaccess"},
&cmdVM.ExportDir{"vmexportdir"},
&cmdVM.ImportDir{"vmimportdir"},
&cmdTemplate.List{"tpllist"},
&cmdTemplate.Create{"tplcreate"},
......
package utils
import (
"fmt"
"os"
"io"
"fmt"
"errors"
"strings"
"archive/tar"
"path/filepath"
"github.com/google/uuid"
)
func Print(a ...any) {
......@@ -31,3 +37,74 @@ func FileExists(filename string) bool {
}
return !info.IsDir()
}
// 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)
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 {
PrintlnErr("warning: tar archive creation: 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)
return err
})
}
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
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment