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

Factorized quite a bit of code.

Reworked README.md's tutorial for creating live exams and added screenshots.
parent 8007c7f1
No related branches found
No related tags found
No related merge requests found
Showing
with 208 additions and 99 deletions
......@@ -431,9 +431,9 @@ Create a new `public` template, named "Xubuntu 22.04 + golang toolchain" based o
tpllist 89649fe3-4940-4b77-929e-50903789cd87 "Xubuntu 22.04 + golang toolchain" public
```
Delete template `8ae56a30-3195-4aea-960d-abb45c47f99e`:
Delete template `d164992b-741e-46db-b71b-27087f2123f5`:
```
tpldel 8ae56a30-3195-4aea-960d-abb45c47f99e
tpldel d164992b-741e-46db-b71b-27087f2123f5
```
Delete all templates matching the "test" pattern:
......@@ -496,7 +496,7 @@ export NEXUS_CERT=/home/janedoe/nexus-ca-cert.pem
Below is a screenshot of `nexus-exam`'s graphical interface:
![](nexus-exam.png)
![](img/nexus-exam.png)
## Tutorial: creating a live exam with nexush
......@@ -512,61 +512,137 @@ First, you need to create the VM that will be used by the students during the ex
./nexush janedoe@nexus.org
```
After entering your password, you should see:
```
janedoe@nexus.org's password:
Welcome to nexush, the nexus shell.
Type: "help" for help on commands
"ls" to list files in current directory
"ls dir" to list files in dir
"quit" or "exit" to quit nexush
nexush>
```
1. List available templates and choose the one you wish to use for your exam VM:
```
tpllist .
```
Let's assume template `8ae56a30-3195-4aea-960d-abb45c47f99e` (Xubuntu_22.04), is the one you would like to use.
This command displays the templates you can use:
```
Debian 11 xfce | 29d9dd54-e001-4920-a9a8-4446d6c7d877 | public
Fedora 36 gnome | 4913a2bb-edfe-4dfe-af53-38197a44523b | public
Manjaro 21 xfce | a7867c1e-9369-41f7-a09d-af8c1401538f | public
Ubuntu 22.04 | 79964f07-42e6-41ae-b62b-2a23d18192fd | public
Xubuntu 22.04 | d164992b-741e-46db-b71b-27087f2123f5 | public
```
Let's assume template `d164992b-741e-46db-b71b-27087f2123f5` (Xubuntu_22.04), is the one you would like to use.
1. Create the VM based on the chosen template. Let's say you want the VM to be named "Exam Oct2022 ISC_433_PCO" and you want it to have 2 CPUs, 4GB of RAM and access to the Internet (for now):
1. Create the VM based on the chosen template. Let's say you want the VM to be named "Exam ProgSys Oct2022" and you want it to have 2 CPUs, 3GB RAM and access to the Internet (for now):
```
vmcreate "Exam ProgSys Oct2022" 2 3000 user d164992b-741e-46db-b71b-27087f2123f5
```
This command displays the name and ID of the created VM(s):
```
vmcreate "Exam Oct2022 ISC_433_PCO" 2 4096 user 8ae56a30-3195-4aea-960d-abb45c47f99e
Created VM "Exam ProgSys Oct2022" | e81f5481-f2e3-491d-9d22-a0247092ce9e
```
1. Now that the VM is created, you need to start it, connect to it, and configure it to fit your needs. First, start it with (you can also start it by specifying its VM ID):
1. Now that the VM is created, you need to start it, connect to it, and configure it to fit your needs. First, start it by starting any VM mathing the following expression (you can also start it by specifying its VM ID):
```
vmstart "Exam Oct2022 ISC_433_PCO"
vmstart "exam progsys"
```
This command displays the name of the started VM(s):
```
Started VM "Exam ProgSys Oct2022"
```
1. Attach to the VM in order to configure it to your needs (you can also attach to it by specifying its VM ID):
```
vmattach "Exam Oct2022 ISC_433_PCO"
vmattach "exam progsys"
```
This command opens a window showing the VM(s)'s desktop:
![](img/vmattach.jpg)
1. Configure the VM to your needs, by:
1. Configure the VM to suit your needs, by:
- upgrading the system with `sudo apt-get update && apt-get upgrade`
- 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. Now that the VM is ready for the exam (its ID is `62856385-4797-4f0f-b840-2e050c05a0a8`), you must create a template from it. This template must be `private` as we don't want anyone else to access it. Let's choose "Exam Oct2022 ISC_433_PCO" as the template name (template creation takes several minutes, the larger the VM, the longer):
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):
```
tplcreate e81f5481-f2e3-491d-9d22-a0247092ce9e "Exam ProgSys Oct2022" private
```
tplcreate 62856385-4797-4f0f-b840-2e050c05a0a8 "Exam Oct2022 ISC_433_PCO" private
This command displays details about the newly created template:
```
{
"id": "bc8b32e9-dab1-41e6-9de1-deda5e3444c6",
"name": "Exam ProgSys Oct2022",
"owner": "janedoe@nexus.org",
"access": "private"
}
```
Once done, the new template ID is displayed. Let's say its ID is `540a3f8b-daa9-4ea7-9d33-e9fcaa7c9c3a`
1. You can now create the VMs for your 50 students using the freshly created template. Let's say the base name for the 50 VMs to create is "Live Exam Oct2022 ISC_433_PCO". You want the VMs to have: 2 CPUs, 4GB of RAM and no network interface to prevent any fraud. Create a CSV file, say `students.csv` with the names of your 50 students, one per line. The 50 VMs can be created with:
1. You can now create the VMs for your 30 students using your new template (ID `bc8b32e9-dab1-41e6-9de1-deda5e3444c6`). Let's say the base name for the 30 VMs to create is "Live Exam ProgSys Oct2022". You want the VMs to have: 2 CPUs, 3GB RAM and no network interface to prevent any fraud. Create a CSV file, say `students.csv` with the names of your 30 students, one per line. Then, create the 30 VMs with:
```
vmcreate "Live Exam ProgSys Oct2022" 2 3000 none bc8b32e9-dab1-41e6-9de1-deda5e3444c6 students.csv
```
This command displays each VM created:
```
vmcreate "Live Exam Oct2022 ISC_433_PCO" 2 4096 none 540a3f8b-daa9-4ea7-9d33-e9fcaa7c9c3a students.csv
Created VM "Live Exam ProgSys Oct2022 [Alia Friedman]" | 74d8b83d-f59e-4129-bf68-af574968cf48
Created VM "Live Exam ProgSys Oct2022 [Aria Doyle]" | f3047faa-2f15-4f47-b79f-9acc19751b6c
Created VM "Live Exam ProgSys Oct2022 [Avah Coffey]" | 3ebd56a2-2c1e-416c-9847-f80ee3efa1c1
Created VM "Live Exam ProgSys Oct2022 [Briley Brady]" | 245fc5b2-b192-4b41-80be-2d39b5a2cef2
Created VM "Live Exam ProgSys Oct2022 [Brooklyn Sweeney]" | a9bafd7e-28f0-4f37-8b90-5e3c82d4bbc5
Created VM "Live Exam ProgSys Oct2022 [Cornelius Simmons]" | 40edb2b1-b4e9-4928-9dea-316ed834bf07
Created VM "Live Exam ProgSys Oct2022 [Donovan Heath]" | 8ae5c9cd-16f3-4e02-a940-e4209a6d7010
Created VM "Live Exam ProgSys Oct2022 [Ella Webster]" | 7d16f88f-afb1-4633-a646-57a9c87411d5
...
```
It should take a few seconds to generate these 50 VMs.
1. The day of the exam, you'll have to start the 50 VMs and generate a PDF with the credentials required to connect to each VM. To start the 50 VMs, run:
1. The day of the exam, you'll have to start the 30 VMs and generate a PDF with the credentials required to connect to each VM. To start the 30 VMs, run:
```
vmstart "live exam progsys"
```
This command displays each VM started:
```
vmstart "Live Exam Oct2022"
Started VM "Live Exam ProgSys Oct2022 [Alia Friedman]"
Started VM "Live Exam ProgSys Oct2022 [Aria Doyle]"
Started VM "Live Exam ProgSys Oct2022 [Avah Coffey]"
Started VM "Live Exam ProgSys Oct2022 [Briley Brady]"
Started VM "Live Exam ProgSys Oct2022 [Brooklyn Sweeney]"
Started VM "Live Exam ProgSys Oct2022 [Cornelius Simmons]"
Started VM "Live Exam ProgSys Oct2022 [Donovan Heath]"
Started VM "Live Exam ProgSys Oct2022 [Ella Webster]"
...
```
1. Finally, to produce the PDF containing all the credentials required to attach to each VM, run:
1. Finally, to produce the PDF file, `creds.pdf`, with all credentials required to attach to each VM, run:
```
vmcred2pdf "live exam progsys" creds.pdf
```
This command displays that file was written successfully:
```
vmcred2pdf "Live Exam Oct2022" creds.pdf
creds.pdf written successfully.
```
`creds.pdf` contains a table, where each line provides access to a VM: the VM name, the port, the password and a blank cell that should be filled by the student using this VM during the exam. The students must use `nexus-exam` to connect to their VM using the VM's credentials (port and password).
Here is what the PDF looks like:
![](img/vmcred2pdf.png)
1. You can now print the PDF above, cut each line and give each student the strip of paper for a VM's credentials. Once a student has completed the exam, they should shutdown the VM and give you the strip of paper back. Note that you can shutdown a VM with `nexus-cli vmshutdown` (if the guest OS runs `qemu-guest-agent`). You can also force a VM to be stopped with `nexus-cli vmstop` although it's not clean and might corrupt the VM's filesystem.
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, you can either start each VM and attach to it and inspect the files. However, an easier way is to download the relevant files to your local computer with the `vmlistexportdir` command. For instance, the command below extracts and downloads the `/home` directory of all VMs matching "Live Exam Oct2022" (each directory is saved in a `.tar` archive named after the VM's ID):
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):
```
vmlistexportdir "Live Exam Oct2022" /home
vmexportdir "live exam progsys" /home
```
This command displays each exported file tree:
```
Successfully exported /home from VM "Live Exam ProgSys Oct2022 [Alia Friedman]" into Live Exam ProgSys Oct2022 [Alia Friedman].tar
Successfully exported /home from VM "Live Exam ProgSys Oct2022 [Aria Doyle]" into Live Exam ProgSys Oct2022 [Aria Doyle].tar
Successfully exported /home from VM "Live Exam ProgSys Oct2022 [Avah Coffey]" into Live Exam ProgSys Oct2022 [Avah Coffey].tar
Successfully exported /home from VM "Live Exam ProgSys Oct2022 [Briley Brady]" into Live Exam ProgSys Oct2022 [Briley Brady].tar
Successfully exported /home from VM "Live Exam ProgSys Oct2022 [Brooklyn Sweeney]" into Live Exam ProgSys Oct2022 [Brooklyn Sweeney].tar
Successfully exported /home from VM "Live Exam ProgSys Oct2022 [Cornelius Simmons]" into Live Exam ProgSys Oct2022 [Cornelius Simmons].tar
Successfully exported /home from VM "Live Exam ProgSys Oct2022 [Donovan Heath]" into Live Exam ProgSys Oct2022 [Donovan Heath].tar
Successfully exported /home from VM "Live Exam ProgSys Oct2022 [Ella Webster]" into Live Exam ProgSys Oct2022 [Ella Webster].tar
...
```
img/nexus-exam.png

17.7 KiB

img/vmattach.jpg

84.6 KiB

img/vmcred2pdf.png

42.4 KiB

......@@ -2,10 +2,7 @@ package cmdVM
import (
"sync"
"errors"
"strings"
"strconv"
"os/exec"
"nexus-client/exec"
u "nexus-client/utils"
g "nexus-client/globals"
)
......@@ -14,10 +11,6 @@ type Attach struct {
Name string
}
const (
REMOTE_VIEWER_BINARY = "remote-viewer"
)
func (cmd *Attach)GetName() string {
return cmd.Name
}
......@@ -43,11 +36,6 @@ func (cmd *Attach)Run(args []string) int {
return 1
}
if err := cmd.checkRemoteViewer(); err != nil {
u.PrintlnErr(err.Error())
return 1
}
vms, err := getFilteredVMs("/vms/attach", args)
if err != nil {
u.PrintlnErr(err.Error())
......@@ -65,7 +53,7 @@ func (cmd *Attach)Run(args []string) int {
for _, vm := range(vms) {
go func(vm VM) {
cmd.runViewer(hostname, cert, vm)
exec.RunRemoteViewer(hostname, cert, vm.Name, vm.Run.Port, vm.Run.Pwd, false)
wg.Done()
} (vm)
}
......@@ -74,37 +62,3 @@ func (cmd *Attach)Run(args []string) int {
return 0
}
func (cmd *Attach)runViewer(hostname string, cert string, vm VM) {
port := strconv.Itoa(vm.Run.Port)
spice := "spice://"+hostname+"?tls-port="+port+"&password="+vm.Run.Pwd
spiceCert := "--spice-ca-file="+cert
spiceSecure := "--spice-secure-channels=all"
title := vm.Name
// To exit fullscreen mode, either:
// - move the mouse to the middle of the top of the screen.
// - use ctrl+F12 as specified below
// To activate kiosk mode, add these options: "-k --kiosk-quit=on-disconnect"
c := exec.Command("remote-viewer", spice, spiceCert, spiceSecure, "-t", title, "--hotkeys=toggle-fullscreen=ctrl+f12")
if err := c.Run(); err != nil {
u.PrintlnErr("Failed attaching to VM: "+err.Error())
}
}
func (cmd *Attach)checkRemoteViewer() error {
output, err := exec.Command(REMOTE_VIEWER_BINARY, "--version").Output()
if err != nil {
return errors.New(REMOTE_VIEWER_BINARY+" is missing. Please install it.")
}
out := string(output)
lines := strings.Split(out, "\n")
fields := strings.Split(lines[0], " ")
if len(fields) < 3 {
return errors.New("Failed extracting "+REMOTE_VIEWER_BINARY+" version number!")
}
// This is what we should get if remote-viewer is working properly.
if fields[0] == REMOTE_VIEWER_BINARY && fields[1] == "version" {
return nil
}
return errors.New("Failed extracting "+REMOTE_VIEWER_BINARY+" version number!")
}
......@@ -83,5 +83,6 @@ func (cmd *Cred2pdf)Run(args []string) int {
return 1
}
u.Println(pdfFile+" written successfully.")
return 0
}
......@@ -66,7 +66,7 @@ func (cmd *ExportDir)Run(args []string) int {
for _, vm := range(vms) {
uuid := vm.ID.String()
outputFile := "exportdir_"+uuid+".tar"
outputFile := vm.Name+".tar"
resp, err := client.R().SetOutput(outputFile).SetBody(params).Get(host+"/vms/"+uuid+"/exportdir")
if err != nil {
u.PrintlnErr("Failed exporting "+dir+" from VM \""+vm.Name+"\": "+err.Error())
......
package exec
import (
"fmt"
"errors"
"os/exec"
"strings"
"strconv"
u "nexus-client/utils"
)
const (
remoteViewerBinary = "remote-viewer"
)
func CheckRemoteViewer() error {
output, err := exec.Command(remoteViewerBinary, "--version").Output()
if err != nil {
return errors.New(remoteViewerBinary+" is required but not found. On Ubuntu/Debian, it can be installed with \"sudo apt-get install virt-viewer\".")
}
out := string(output)
lines := strings.Split(out, "\n")
fields := strings.Split(lines[0], " ")
if len(fields) < 3 {
return errors.New("Failed extracting "+remoteViewerBinary+" version number!")
}
// This is what we should get if remote-viewer is working properly.
if fields[0] == remoteViewerBinary && fields[1] == "version" {
return nil
}
return errors.New("Failed extracting "+remoteViewerBinary+" version number!")
}
// Run remote-viewer.
// name is the VM's name.
// port is the port the VM spice server is listening to.
// pwd is the spice password to connect to the running VM.
func RunRemoteViewer(hostname string, cert string, name string, port int, pwd string, fullscreen bool) {
portStr := strconv.Itoa(port)
spice := "spice://"+hostname+"?tls-port="+portStr+"&password="+pwd
spiceCert := "--spice-ca-file="+cert
spiceSecure := "--spice-secure-channels=all"
// To exit fullscreen mode, either:
// - move the mouse to the middle of the top of the screen.
// - use ctrl+F12 as specified below
// To activate kiosk mode, add these options: "-k --kiosk-quit=on-disconnect"
var c *exec.Cmd
if fullscreen {
c = exec.Command("remote-viewer", spice, spiceCert, spiceSecure, "-t", name, "--hotkeys=toggle-fullscreen=ctrl+f12", "-f")
} else {
c = exec.Command("remote-viewer", spice, spiceCert, spiceSecure, "-t", name, "--hotkeys=toggle-fullscreen=ctrl+f12")
}
stdoutStderr, err := c.CombinedOutput()
if err != nil {
u.PrintlnErr("Failed attaching to VM!")
output := fmt.Sprintf("[%s]", stdoutStderr)
u.PrintlnErr(output)
}
}
module nexus-client/exec
go 1.18
......@@ -18,6 +18,8 @@ replace nexus-client/cmdVM => ../cmdVM
replace nexus-client/cmdTemplate => ../cmdTemplate
replace nexus-client/exec => ../exec
require (
github.com/go-resty/resty/v2 v2.7.0
nexus-client/cmd v0.0.0-00010101000000-000000000000
......@@ -37,4 +39,5 @@ require (
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
nexus-client/cmdToken v0.0.0-00010101000000-000000000000 // indirect
nexus-client/exec v0.0.0-00010101000000-000000000000 // indirect
)
......@@ -6,6 +6,7 @@ import (
"strings"
u "nexus-client/utils"
g "nexus-client/globals"
"nexus-client/exec"
"nexus-client/cmd"
"nexus-client/cmdLogin"
"nexus-client/cmdToken"
......@@ -27,13 +28,13 @@ var cmdList = []cmd.Command {
&cmdUser.SetCaps{"usersetcaps"},
&cmdVM.List{"vmlist"},
&cmdVM.ListStart{"vmliststart"},
&cmdVM.ListAttach{"vmlistattach"},
&cmdVM.ListStop{"vmliststop"},
&cmdVM.ListEdit{"vmlistedit"},
&cmdVM.ListEditAccess{"vmlisteditaccess"},
&cmdVM.ListDel{"vmlistdel"},
&cmdVM.ListExportDir{"vmlistexportdir"},
// &cmdVM.ListStart{"vmliststart"},
// &cmdVM.ListAttach{"vmlistattach"},
// &cmdVM.ListStop{"vmliststop"},
// &cmdVM.ListEdit{"vmlistedit"},
// &cmdVM.ListEditAccess{"vmlisteditaccess"},
// &cmdVM.ListDel{"vmlistdel"},
// &cmdVM.ListExportDir{"vmlistexportdir"},
&cmdVM.Cred2pdf{"vmcred2pdf"},
......@@ -54,6 +55,11 @@ var cmdList = []cmd.Command {
}
func main() {
if err := exec.CheckRemoteViewer(); err != nil {
u.PrintlnErr(err.Error())
os.Exit(1)
}
server, found := os.LookupEnv(g.ENV_NEXUS_SERVER)
if !found {
u.PrintlnErr("Environment variable \""+g.ENV_NEXUS_SERVER+"\" must be set!")
......
......@@ -4,6 +4,10 @@ go 1.18
replace nexus-client/utils => ../utils
replace nexus-client/globals => ../globals
replace nexus-client/exec => ../exec
require fyne.io/fyne/v2 v2.2.1
require (
......@@ -16,6 +20,7 @@ require (
github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec // indirect
github.com/go-resty/resty/v2 v2.7.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
......@@ -28,10 +33,12 @@ require (
github.com/yuin/goldmark v1.4.0 // indirect
golang.org/x/image v0.0.0-20220601225756-64ec528b34cd // indirect
golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee // indirect
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect
golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect
nexus-client/exec v0.0.0-00010101000000-000000000000 // indirect
nexus-client/globals v0.0.0-00010101000000-000000000000 // indirect
nexus-client/utils v0.0.0-00010101000000-000000000000 // indirect
)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment