From cbc8fa4a2ecc8a5b8d7e00e6343072f81e48045f Mon Sep 17 00:00:00 2001
From: Florent Gluck <florent.gluck@hesge.ch>
Date: Thu, 11 May 2023 15:30:14 +0200
Subject: [PATCH] ongoing work on merging/reworking server and client trees

---
 HOWTO_build.md                                |  48 --
 Makefile.server.dev                           |  42 +
 Makefile => Makefile.server.prod              |  28 +-
 {conf => config/server}/nexus-server          |   0
 {conf => config/server}/nexus-server.service  |   0
 {pki => config/server/pki}/.gitignore         |   0
 {pki => config/server/pki}/README.md          |   0
 {pki => config/server/pki}/ca.info            |   0
 {pki => config/server/pki}/gen-cert.sh        |   0
 .../server/pki}/server-generic.info           |   0
 {pki => config/server/pki}/server-nexus.info  |   0
 {conf => config/server}/users.json            |   0
 docs/HOWTO_build.md                           |  36 +
 docs/README_client.md                         | 804 ++++++++++++++++++
 README.md => docs/README_server.md            |   0
 {tools => docs}/distribs_install.md           |   4 +
 {img => docs/img}/nexus-exam.png              | Bin
 {img => docs/img}/vmattach.jpg                | Bin
 {img => docs/img}/vmcred2pdf.png              | Bin
 src/client/cmdTemplate/helper.go              |  25 +-
 src/client/cmdTemplate/templateCreate.go      |  12 +-
 src/client/cmdVM/helper.go                    |  30 +-
 src/client/cmdVM/vm.go                        |  34 -
 src/client/cmdVM/vmAttach.go                  |   9 +-
 src/client/cmdVM/vmCreate.go                  |  11 +-
 src/client/cmdVM/vmEdit.go                    |   5 +-
 src/client/cmdVM/vmExportDir.go               |   2 +-
 src/client/nexus-cli/go.mod                   |  12 +
 src/client/nexus-cli/nexus-cli.go             |   3 +-
 src/client/nexus-cli/validate                 |   2 +-
 src/client/nexus-exam/go.mod                  |   3 +
 src/client/nexus-exam/nexus-exam.go           |   3 +-
 src/client/nexush/go.mod                      |  14 +-
 src/client/nexush/nexush.go                   |   3 +-
 src/client/utils/utils.go                     |   9 -
 src/client/version/version.go                 |   4 +-
 src/{server => common}/caps/caps.go           |   0
 src/common/caps/go.mod                        |   3 +
 src/common/template/go.mod                    |   5 +
 src/common/template/go.sum                    |   2 +
 .../template}/template.go                     |  16 +-
 src/common/utils/go.mod                       |   3 +
 src/common/utils/utils.go                     |  14 +
 src/common/vm/go.mod                          |   5 +
 src/common/vm/go.sum                          |   2 +
 src/common/vm/vm.go                           |  60 ++
 src/server/caps/go.mod                        |   3 -
 src/server/exec/QemuSystem.go                 |   3 +-
 src/server/go.mod                             |  12 +-
 src/server/router/routerTemplates.go          |  20 +-
 src/server/router/routerUsers.go              |   2 +-
 src/server/router/routerVMs.go                |  27 +-
 src/server/users/user.go                      |   2 +-
 src/server/utils/utils.go                     |   9 -
 src/server/version/version.go                 |   4 +-
 src/server/vms/template.go                    |  47 +-
 src/server/vms/templates.go                   |  19 +-
 src/server/vms/vm.go                          | 191 ++---
 src/server/vms/vms.go                         |  37 +-
 tools/genpwd/{src => }/genpwd.go              |   0
 tools/genpwd/{src => }/go.mod                 |   0
 tools/genpwd/{src => }/go.sum                 |   0
 62 files changed, 1261 insertions(+), 368 deletions(-)
 delete mode 100644 HOWTO_build.md
 create mode 100644 Makefile.server.dev
 rename Makefile => Makefile.server.prod (83%)
 rename {conf => config/server}/nexus-server (100%)
 rename {conf => config/server}/nexus-server.service (100%)
 rename {pki => config/server/pki}/.gitignore (100%)
 rename {pki => config/server/pki}/README.md (100%)
 rename {pki => config/server/pki}/ca.info (100%)
 rename {pki => config/server/pki}/gen-cert.sh (100%)
 rename {pki => config/server/pki}/server-generic.info (100%)
 rename {pki => config/server/pki}/server-nexus.info (100%)
 rename {conf => config/server}/users.json (100%)
 create mode 100644 docs/HOWTO_build.md
 create mode 100644 docs/README_client.md
 rename README.md => docs/README_server.md (100%)
 rename {tools => docs}/distribs_install.md (96%)
 rename {img => docs/img}/nexus-exam.png (100%)
 rename {img => docs/img}/vmattach.jpg (100%)
 rename {img => docs/img}/vmcred2pdf.png (100%)
 delete mode 100644 src/client/cmdVM/vm.go
 rename src/{server => common}/caps/caps.go (100%)
 create mode 100644 src/common/caps/go.mod
 create mode 100644 src/common/template/go.mod
 create mode 100644 src/common/template/go.sum
 rename src/{client/cmdTemplate => common/template}/template.go (56%)
 create mode 100644 src/common/utils/go.mod
 create mode 100644 src/common/utils/utils.go
 create mode 100644 src/common/vm/go.mod
 create mode 100644 src/common/vm/go.sum
 create mode 100644 src/common/vm/vm.go
 delete mode 100644 src/server/caps/go.mod
 rename tools/genpwd/{src => }/genpwd.go (100%)
 rename tools/genpwd/{src => }/go.mod (100%)
 rename tools/genpwd/{src => }/go.sum (100%)

diff --git a/HOWTO_build.md b/HOWTO_build.md
deleted file mode 100644
index 57b0b7f..0000000
--- a/HOWTO_build.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# HOWTO build
-
-## Introduction
-
-This document describes how to obtain the source codes and build the following client applications:
-
-- `nexush`
-- `nexus-cli`
-- `nexus-exam`
-
-## Getting the sources
-
-`nexush`, `nexus-cli` and `nexus-exam`' source codes reside in the same git repository:
-```{.shell}
-git clone ssh://git@ssh.hesge.ch:10572/flg_projects/nexus_vdi/nexus-client.git nexus-client.git
-```
-
-If you didn't set up a public key in your gitlab account, use https instead:
-```{.shell}
-git clone https://gitedu.hesge.ch/flg_projects/nexus_vdi/nexus-client.git nexus-client.git
-```
-
-### Building nexush and nexus-cli
-
-To build `nexush` or `nexus-cli` binaries, go into `nexus-client.git/src/nexush` or `nexus-client.git/src/nexus-cli` and run:
-```
-make build
-```
-This builds statically linked binaries for all supported combinations of operating systems and architectures into the `build` directory.
-
-To removes all generated files, run:
-```
-make clean
-```
-
-## Building nexus-exam
-
-To build the `nexus-exam` binary, go to `nexus-client.git/src/nexus-exam` and run:
-```
-go build
-```
-
-For now, the only supported combination of operating system and architecture is Linux amd64. Furthermore, `nexus-exam` cannot be built statically (on Ubuntu 20.04 and 22.04 at least) as some static libraries are missing.
-
-Finally, run the following to strip symbols and reduce the binary size:
-```
-strip -s nexus-exam
-```
diff --git a/Makefile.server.dev b/Makefile.server.dev
new file mode 100644
index 0000000..946a0d2
--- /dev/null
+++ b/Makefile.server.dev
@@ -0,0 +1,42 @@
+BASEDIR=src
+
+help:
+	@echo "nexus-server installation script for development."
+	@echo ""
+	@echo "Available targets:"
+	@echo "install     create initial config (initial user and certificates for localhost)"
+	@echo "            and initial data files"
+	@echo "uninstall   remove config and data files"
+
+install: create_files copy_config create_certificates
+	@echo "Installing development environment"
+	@echo ""
+	@echo "Successfully installed development environment"
+
+uninstall: $(BASEDIR)/config $(BASEDIR)/data
+	@echo "Uninstalling development environment"
+	@rm -rf $^
+	@echo "OK"
+	@echo "Successfully uninstalled development environment"
+
+create_files:
+	@echo "Creating required file hierarchy in $(BASEDIR)"
+	@mkdir -p $(BASEDIR)/config/pki
+	@mkdir -p $(BASEDIR)/data/vms
+	@mkdir -p $(BASEDIR)/data/templates
+	@mkdir -p $(BASEDIR)/data/tmp
+	@echo "OK"
+
+copy_config:
+	@echo "Copying initial users config"
+	@cp -n config/server/users.json $(BASEDIR)/config/
+	@echo "OK"
+
+create_certificates:
+	@echo "Generating certificates"
+	@cd config/server/pki && ./gen-cert.sh
+	@echo "OK"
+	@cd ../../..
+	@echo "Moving certificates"
+	@mv -n config/server/pki/server-nexus $(BASEDIR)/config/pki/
+	@echo "OK"
diff --git a/Makefile b/Makefile.server.prod
similarity index 83%
rename from Makefile
rename to Makefile.server.prod
index 8ffb91b..fb07345 100644
--- a/Makefile
+++ b/Makefile.server.prod
@@ -14,7 +14,7 @@ __check_defined = \
       $(error Environment variable $1$(if $2, ($2)) must be set))
 
 help:
-	@echo "nexus-server installation script."
+	@echo "nexus-server installation script for production."
 	@echo ""
 	@echo "Available targets:"
 	@echo "build       build the nexus-server binaries [run as non-root]"
@@ -34,13 +34,13 @@ check_prefix:
 
 build:
 	@echo "Building nexus-server"
-	@cd src && go build -a .
+	@cd src/server && go build -a .
 	@echo "OK"
 	@echo "Building genpwd"
-	@cd tools/genpwd/src && go build -a .
+	@cd tools/genpwd && go build -a .
 	@echo "OK"
 
-install: check_prefix prepare_install src/nexus-server tools/genpwd/src/genpwd create_user create_files copy_config copy_binaries create_certificates set_permissions install_service
+install: check_prefix prepare_install src/server/nexus-server tools/genpwd/genpwd create_user create_files copy_config copy_binaries create_certificates set_permissions install_service
 	@echo ""
 	@echo "Successfully installed nexus-server into $(BASEDIR)"
 
@@ -87,10 +87,10 @@ remove_files:
 
 clean_build:
 	@echo "Cleaning up nexus-server"
-	@cd src && go clean .
+	@cd src/server && go clean .
 	@echo "OK"
 	@echo "Cleaning up genpwd"
-	@cd tools/genpwd/src && go clean .
+	@cd tools/genpwd && go clean .
 	@echo "OK"
 
 remove_service:
@@ -116,24 +116,24 @@ create_files:
 
 copy_config:
 	@echo "Copying initial users config"
-	@cp -n conf/users.json $(BASEDIR)/config/
+	@cp -n config/server/users.json $(BASEDIR)/config/
 	@echo "OK"
 
 copy_binaries:
 	@echo "Copying binaries"
-	@cp src/nexus-server $(BASEDIR)/bin
-	@cp tools/genpwd/src/genpwd $(BASEDIR)/bin
+	@cp src/server/nexus-server $(BASEDIR)/bin
+	@cp tools/genpwd/genpwd $(BASEDIR)/bin
 	@cp tools/template_creator $(BASEDIR)/bin
 	@cp tools/vm_run $(BASEDIR)/bin
 	@echo "OK"
 
 create_certificates:
 	@echo "Generating certificates"
-	@cd pki && ./gen-cert.sh
+	@cd config/server/pki && ./gen-cert.sh
 	@echo "OK"
-	@cd ..
+	@cd ../../..
 	@echo "Moving certificates"
-	@mv -n pki/server-nexus $(BASEDIR)/config/pki/
+	@mv -n config/server/pki/server-nexus $(BASEDIR)/config/pki/
 	@echo "OK"
 
 set_permissions:
@@ -147,10 +147,10 @@ set_permissions:
 
 install_service:
 	@echo "Copying nexus-server crond task"
-	@cp conf/nexus-server /etc/cron.d/
+	@cp config/server/nexus-server /etc/cron.d/
 	@echo "OK"
 	@echo "Copying nexus-server service file"
-	@sed s,_PREFIX_,$(PREFIX),g conf/nexus-server.service > /etc/systemd/system/nexus-server.service
+	@sed s,_PREFIX_,$(PREFIX),g config/server/nexus-server.service > /etc/systemd/system/nexus-server.service
 	@echo "OK"
 	@echo "Reloading systemd configuration"
 	@systemctl daemon-reload
diff --git a/conf/nexus-server b/config/server/nexus-server
similarity index 100%
rename from conf/nexus-server
rename to config/server/nexus-server
diff --git a/conf/nexus-server.service b/config/server/nexus-server.service
similarity index 100%
rename from conf/nexus-server.service
rename to config/server/nexus-server.service
diff --git a/pki/.gitignore b/config/server/pki/.gitignore
similarity index 100%
rename from pki/.gitignore
rename to config/server/pki/.gitignore
diff --git a/pki/README.md b/config/server/pki/README.md
similarity index 100%
rename from pki/README.md
rename to config/server/pki/README.md
diff --git a/pki/ca.info b/config/server/pki/ca.info
similarity index 100%
rename from pki/ca.info
rename to config/server/pki/ca.info
diff --git a/pki/gen-cert.sh b/config/server/pki/gen-cert.sh
similarity index 100%
rename from pki/gen-cert.sh
rename to config/server/pki/gen-cert.sh
diff --git a/pki/server-generic.info b/config/server/pki/server-generic.info
similarity index 100%
rename from pki/server-generic.info
rename to config/server/pki/server-generic.info
diff --git a/pki/server-nexus.info b/config/server/pki/server-nexus.info
similarity index 100%
rename from pki/server-nexus.info
rename to config/server/pki/server-nexus.info
diff --git a/conf/users.json b/config/server/users.json
similarity index 100%
rename from conf/users.json
rename to config/server/users.json
diff --git a/docs/HOWTO_build.md b/docs/HOWTO_build.md
new file mode 100644
index 0000000..1e06c5e
--- /dev/null
+++ b/docs/HOWTO_build.md
@@ -0,0 +1,36 @@
+# Building nexus clients
+
+## Introduction
+
+This document describes how to build the following nexus clients:
+
+- `nexush`
+- `nexus-cli`
+- `nexus-exam`
+
+### Building nexush and nexus-cli
+
+To build `nexush` or `nexus-cli` binaries, go into `src/nexush` or `src/nexus-cli` and run:
+```
+make build
+```
+This command statically builds binaries for all supported combinations of operating systems and architectures into the `build` directory.
+
+To removes all generated files, run:
+```
+make clean
+```
+
+## Building nexus-exam
+
+To build the `nexus-exam` binary, go to `nexus-client.git/src/nexus-exam` and run:
+```
+go build
+```
+
+For now, the only supported combination of operating system and architecture is Linux amd64. Furthermore, `nexus-exam` cannot be built statically (on Ubuntu 20.04 and 22.04 at least) as some static libraries are missing.
+
+Finally, run the following to strip symbols and reduce the binary size:
+```
+strip -s nexus-exam
+```
diff --git a/docs/README_client.md b/docs/README_client.md
new file mode 100644
index 0000000..090d001
--- /dev/null
+++ b/docs/README_client.md
@@ -0,0 +1,804 @@
+# nexus-client
+
+## Introduction
+
+**nexus-client** is the client component of the nexus project, a VDI (Virtual Desktop Infrastructure) written from scratch in Go and based on Linux/KVM + QEMU.
+
+## Components
+
+The full project, **nexus_vdi**, is made of three software components:
+
+1. **nexus-server**: the server program (backend) that runs on a server.
+   - Exposes a REST API to manage VMs and users
+1. **nexus-client**: the client or end-user program to manage VMs, users and templates.
+   - The client uses REST messages to communicate with `nexus-server`
+   - The client can be run from anywhere (locally or remotely) as long as it can communicate with `nexus-server`
+   - Two clients have been developed so far:
+     - `nexush`: an interactive command line client, similar to a shell. It is the most user-friendly way to interact with `nexus-server`, but it is not suited for scripting.
+     - `nexus-cli`: a collection of commands particularly suited to scripting and automated operations.
+     - Both clients offer the same features, functionalities and syntax. A nice touch is that commands support regular expressions allowing many VMs to be created/started/stopped/destroyed/etc. at once.
+   - Both clients feature an "vmattach" command which lets users interact with their VM's desktop.
+     - this feature requires `remote-viewer` which is part of the [virt-viewer project](https://gitlab.com/virt-viewer/virt-viewer.git)
+1. **nexus-exam**: a minimalistic graphical application used during live exams. It doesn't feature any option and only allows a student to connect to her/his running VM.
+
+To obtain a nexus access, please contact Florent Gluck at florent.gluck@hesge.ch.
+
+## Building nexus-clients and nexus-exam
+
+To build `nexush`, `nexus-cli` and `nexus-exam` read the [following documentation](HOWTO_build.md).
+
+## Obtaining nexus-clients
+
+Precompiled nexus client binaries can be [found here](https://drive.switch.ch/index.php/s/d89cBJBbGvpc4nO).
+
+Two clients are available:
+
+- `nexush` is the most "user-friendly" client; it is the nexus shell
+- `nexus-cli` is another client more suited to scripting
+
+Both clients feature the same commands and they are available for multiples operating systems and processor architectures:
+
+| Operating System | Architecture | Clients directory location |
+|---               |---           |---                     |
+| Linux            | x86-64/AMD64 | binaries/amd64/linux   |
+| OSX              | x86-64/AMD64 | binaries/amd64/darwin  |
+| Windows          | x86-64/AMD64 | binaries/amd64/windows |
+| Linux            | ARM64        | binaries/arm64/linux   |
+| OSX              | ARM64        | binaries/arm64/darwin  |
+| Windows          | ARM64        | binaries/arm64/windows |
+
+## Installing nexus-clients' dependency
+
+Both clients require the `remote-viewer` software in order to display remote VM desktops.
+
+### Installing remote-viewer on Linux
+
+On Ubuntu/Debian, `remote-viewer` is part of the `virt-viewer` package which can be installed with:
+```
+sudo apt-get install virt-viewer
+```
+
+### Installing remote-viewer on Windows
+
+On Windows, `remote-viewer` is part of the `virt-viewer` MSI package that can be downloaded here: [https://virt-manager.org/download.html](https://virt-manager.org/download.html).
+
+Once installed, make sure to add the folder where `remote-viewer.exe` is installed to your user's PATH environment variable.
+
+### Installing remote-viewer on Mac OSX
+
+On Mac, `remote-viewer` is part of the `virt-viewer` package from MacPorts that can be installed from here [https://ports.macports.org/port/virt-viewer/](https://ports.macports.org/port/virt-viewer/).
+
+## Overview of nexus' basic concepts
+
+### Templates
+
+- Templates are immutable OS disk images used to create VMs: a VM is an instance of a specific template
+- Templates can be `private` or `public`: a private template is only visible to its owner whereas a public template is visible to everyone
+- **Important**: templates don't store any hardware configuration (number of CPUs, amount of RAM, network interface, etc.); they only store the disk image, nothing more
+
+### VMs
+
+- A VM is always created from a template
+- A VM's hardware (number of CPUs, amount of RAM, network interface, etc.) is completely independant of its disk content
+- Access rights to a VM can be finely controlled by its owner and anyone else allowed (see [Access control](#access-control))
+
+### Users
+
+- What users can or cannot do is defined by their capabilities: see [Access control](#access-control) for more information
+
+## Available template images
+
+The table below describes the basic public templates that are currently available. The numbers between parenthesis indicate the year and month of creation (also available in the detailed output of `tpllist -l <template ID>`).
+
+| Template name                                         | Description                                                     |
+|---                                                    |---                                                              |
+| Xubuntu 22.04 (2023.02) + dev env/unpriv user         | Xubuntu 22.04 system (Ubuntu with XFCE desktop environment)     |
+|                                                       | + C dev tools (gcc, make, etc.), VScode IDE compiler            |
+|                                                       | + QEMU system hypervisor                                        |
+|                                                       | `nexus` user (`/home/nexus`) with sudo privileges (pwd `nexus`) |
+|                                                       | `student` user (`/home/student`) without any privilege (no pwd) |
+| Xubuntu 22.04 (2023.02) + dev env/unpriv user/pi-hole | Xubuntu 22.04 system (Ubuntu with XFCE desktop environment)     |
+|                                                       | + C dev tools (gcc, make, etc.), VScode IDE compiler            |
+|                                                       | + QEMU system hypervisor                                        |
+|                                                       | + Pi-hole domain firewall (see [Domain firewall](#domain-firewall)) |
+|                                                       | `nexus` user (`/home/nexus`) with sudo privileges (pwd `nexus`) |
+|                                                       | `student` user (`/home/student`) without any privilege (no pwd) |
+| Manjaro 21 xfce (2022.08)                             | Vanilla Manjaro 21 system with XFCE desktop environment         |
+|                                                       | `nexus` user (`/home/nexus`) with sudo privileges (pwd `nexus`) |
+
+<!--
+| Xubuntu 22.04 (2022.08) + dev env                     | Xubuntu 22.04 system (Ubuntu with XFCE desktop environment)     |
+|                                                       | + C dev tools (gcc, make, etc.), VScode IDE compiler            |
+|                                                       | + QEMU system hypervisor                                        |
+|                                                       | `nexus` user (`/home/nexus`) with sudo privileges (pwd `nexus`) |
+| Debian 11 xfce (2022.08)                              | Vanilla Debian 11 system with XFCE desktop environment          |
+|                                                       | `nexus` user (`/home/nexus`) with sudo privileges (pwd `nexus`) |
+| Ubuntu 22.04 (2022.08)                                | Vanilla Ubuntu 22.04 system with the GNOME desktop environment  |
+|                                                       | `nexus` user (`/home/nexus`) with sudo privileges (pwd `nexus`) |
+-->
+
+## nexush
+
+`nexush` is a **nexus-client** in the form of a single native executable featuring the commands listed below.
+Most commands support regular expressions (regex) in order to perform actions on multiple VMs at once.
+
+Regular expressions must conform to the RE2 GO syntax described here [https://github.com/google/re2/wiki/Syntax](https://github.com/google/re2/wiki/Syntax).
+
+`nexush` features commands to manipulate:
+
+- VMs (commands starting with `vm`)
+- templates (commands starting with `tpl`)
+- users (commands starting with `user`)
+
+Example of execution showing all available commands:
+
+```
+Welcome to nexush, the nexus shell.
+Type: "help" for help on commands
+      "quit" or "exit" to quit nexush
+nexush> help
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+ls                  List files in the specified dir or in the current dir if no argument is specified.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+userlist            Lists users.
+                    Requires USER_LIST user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+usercreate          Creates a user.
+                    Requires USER_CREATE user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+userdel             Deletes one or more users.
+                    Requires USER_DESTROY user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+usersetcaps         Sets a user's capabilities.
+                    Requires USER_SET_CAPS user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+vmlist              Lists VMs.
+                    Requires VM_LIST VM access capability or VM_LIST_ANY user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+vmcred2pdf          Creates a PDF with the credentials required to attach to running VMs.
+                    Requires VM_LIST VM access capability or VM_LIST_ANY user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+vmcred2txt          Creates a text file with the credentials required to attach to running VMs.
+                    Requires VM_LIST VM access capability or VM_LIST_ANY user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+vmstart             Starts one or more VMs.
+                    Requires VM_START VM access capability or VM_START_ANY user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+vmkill              Kills one or more VMs.
+                    Requires VM_STOP VM access capability or VM_STOP_ANY user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+vmshutdown          Gracefully shutdowns one or more VMs.
+                    Requires VM_STOP VM access capability or VM_STOP_ANY user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+vmreboot            Gracefully reboots one or more VMs.
+                    Requires VM_REBOOT VM access capability or VM_REBOOT_ANY user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+vmattach            Attaches to one or more VMs in order to use their desktop environment.
+                    Requires VM_LIST VM access capability or VM_LIST_ANY user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+vmcreate            Creates one or more VMs.
+                    Requires VM_CREATE user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+vmedit              Edits one or more VMs' properties: name, cpus, ram or nic.
+                    Requires VM_EDIT VM access capability or VM_EDIT_ANY user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+vmdel               Deletes one or more VMs.
+                    Requires VM_DESTROY VM access capability or VM_DESTROY_ANY user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+vmaddaccess         Adds a user's VM access in one or more VMs.
+                    Requires VM_SET_ACCESS user capability and VM_SET_ACCESS VM access capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+vmdelaccess         Removes a user's VM access in one or more VMs.
+                    Requires VM_SET_ACCESS user capability and VM_SET_ACCESS VM access capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+vmexportdir         Exports one or more VMs' directory into one or more tar archives.
+                    Requires VM_READFS VM access capability or VM_READFS_ANY user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+vmimportdir         Copies a local directory (or file) and all its content into one or more VMs.
+                    Requires VM_WRITEFS VM access capability or VM_WRITEFS_ANY user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+tpllist             Lists available templates.
+                    Requires TPL_LIST or TPL_LIST_ANY user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+tplcreate           Creates a template from an existing VM.
+                    Requires TPL_CREATE user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+tpledit             Edits one or more template's properties: name, access.
+                    Requires TPL_EDIT or TPL_EDIT_ANY user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+tpldel              Deletes a template.
+                    Requires TPL_DESTROY or TPL_DESTROY_ANY user capability.
+―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
+tplexportdisk       Exports a template's disk.
+                    Requires TPL_READFS or TPL_READFS_ANY user capability.
+```
+
+Note that when attached to a VM's desktop (`vmattach` command), ctrl+F12 toggles between fullscreen/non-fullscreen modes.
+
+## nexus-cli
+
+`nexus-cli` is another **nexus-client** in the form of a single native executable featuring pretty much the exact same commands as `nexush`. Therefore we won't list them here.
+
+## USB redirection
+
+USB redirection allows USB devices plugged in the PC running nexus client to be accessible in the guest OS of the VM running on the server.
+
+When creating a new VM with `vmcreate`, you can specify USB devices that are accessible in the VM. A USB device is specified by two hexadecimal number, a vendor ID (`VID`) and a product ID (`PID`). For instance, the example below creates a VM and allows two USB devices, 067b:2303 (a USB/serial connector) and 0781:5567 (a SanDisk USB key) to be accessible in the VM:
+```
+vmcreate "usb test" 2 4000 none 067b:2303,0781:5567 fbccb584-9ea6-40f7-926d-dabf3970525e
+```
+
+If you wish to forbid all USB devices simply specify "none" instead:
+```
+vmcreate "usb test" 2 4000 none none fbccb584-9ea6-40f7-926d-dabf3970525e
+```
+
+To obtain the vendor ID and product ID of a USB device, run the `lsusb` command on Linux. The output should be similar to the one below, where the sixth column indicates vendor ID and product ID separated by a colon (`:`):
+```
+Bus 003 Device 026: ID 04d8:0b27 Microchip Technology, Inc. USB2734
+Bus 003 Device 003: ID 0b0e:245d GN Netcom Jabra Link 370
+Bus 003 Device 009: ID 058f:6362 Alcor Micro Corp. Flash Card Reader/Writer
+Bus 003 Device 006: ID 046d:08e3 Logitech, Inc. C505 HD Webcam
+```
+
+### Vendor ID and product ID of USB devices of interest
+
+| Device description                           | VID:PID   |
+|---                                           |---        |
+| Atmel at91sam SAMBA bootloader               | 03eb:6124 |
+| Prolific Technology USB/serial               | 067b:2303 |
+| MyLab2 serial port                           | 0403:6015 |
+| Digilent Analog Discovery 2                  | 0403:6014 |
+| NXP Semiconductors NXP CMSIS-DAP             | 1fc9:001d |
+| NXP Semiconductors LPC11U3x CMSIS-DAP v1.0.4 | 1fc9:0132 |
+| Saleae Logic Pro 16                          | 21a9:1006 |
+| Peak USB CAN                                 | 0c72:000c |
+| Segger J-Link                                | 1366:0101 |
+| Xilinx KV260 and KR260 JTAG and UART         | 0403:6011 |
+| Xilinx Nexys Video JTAG                      | 0403:6010 |
+| Xilinx Nexys Video UART                      | 0403:6001 |
+| USBest Technology USB key                    | 1307:0165 |
+| SanDisk Cruzer Blade USB key                 | 0781:5567 |
+
+## nexush usage examples
+
+Launch `nexush` and log in as user `janedoe@nexus.org` (note that you will be prompted for your password):
+```
+./nexush janedoe@nexus.org
+```
+
+Display available commands:
+```
+help
+```
+
+Display the help for the `vmcreate` command:
+```
+vmcreate
+```
+
+Display information about the currently logged-on user:
+```
+whoami
+```
+
+List all users:
+```
+userlist .
+```
+
+List users matching the "jane" pattern:
+```
+userlist jane
+```
+
+Add new user `lukeskywalker@force.net` with a list of capabilities:
+```
+useradd lukeskywalker@force.org Luke Skywalker maytheforcebewithyou USER_CREATE USER_DESTROY USER_LIST USER_SET_CAPS VM_CREATE
+```
+
+List all listable VMs:
+```
+vmlist .
+```
+
+List all listable VMs with more details ("long output"):
+```
+vmlist -l .
+```
+
+List listable VMs matching the "ubuntu" pattern:
+```
+vmlist ubuntu
+```
+
+List listable VMs matching the "ubuntu" pattern and also the VM with ID `6713ce26-941e-4d95-8e92-6b71d44bf75a`:
+```
+vmlist ubuntu 6713ce26-941e-4d95-8e92-6b71d44bf75a
+```
+
+Start VM `6713ce26-941e-4d95-8e92-6b71d44bf75a`:
+```
+vmstart 6713ce26-941e-4d95-8e92-6b71d44bf75a
+```
+
+Start VMs matching the "exam ISC_433 PCO" pattern:
+```
+vmstart "exam ISC_433 PCO"
+```
+
+Attach to VM `6713ce26-941e-4d95-8e92-6b71d44bf75a` and all VMs matching the pattern "zorglub":
+```
+vmattach 6713ce26-941e-4d95-8e92-6b71d44bf75a zorglub
+```
+
+Shutdown VMs matching the "exam ISC_433 PCO" pattern (for this to work, `qemu-guest-agent` must be running in the VM's guest OS!):
+```
+vmshutdown "exam ISC_433 PCO"
+```
+
+Kill VM `6713ce26-941e-4d95-8e92-6b71d44bf75a`:
+```
+vmkill 6713ce26-941e-4d95-8e92-6b71d44bf75a
+```
+
+Create a VM named "Doom", based on the `fbccb584-9ea6-40f7-926d-dabf3970525e` (Doom) template, with 4 CPUs, 4GB RAM, a network interface with NAT translation, and no USB devices:
+```
+vmcreate Doom 4 4096 user none fbccb584-9ea6-40f7-926d-dabf3970525e
+```
+
+Create 50 VMs with the base name "ISC_433 Exam" based on the `6713ce26-941e-4d95-8e92-6b71d44bf75a` template, with 2 CPUs, 2GB RAM, no network interface, and no USB devices:
+```
+vmcreate "ISC_433 Exam" 2 2048 none none 6713ce26-941e-4d95-8e92-6b71d44bf75a 50
+```
+It takes about 30 seconds and 11MB of disk space to create these 50 VMs.
+They will have the following names:
+```
+ISC_433 Exam <1>
+ISC_433 Exam <2>
+...
+ISC_433 Exam <50>
+```
+
+Edit VM `6713ce26-941e-4d95-8e92-6b71d44bf75a` by changing its name to "Tagada VM" and no network interface (`none`):
+```
+vmedit 6713ce26-941e-4d95-8e92-6b71d44bf75a name="Tagada VM" nic=none
+```
+
+Edit VMs matching the "PCO lab2" pattern by changing their CPU to 1 core, a network interface with NAT translation (`user`), and adding access to USB devices baba:0007 and 1234:abcd:
+```
+vmedit "PCO lab2" cpus=1 nic=user usb=baba:0007,1234:abcd
+```
+
+Delete VM `6713ce26-941e-4d95-8e92-6b71d44bf75a`:
+```
+vmdel 6713ce26-941e-4d95-8e92-6b71d44bf75a
+```
+
+Delete VMs matching the "exam ISC_433 PCO" pattern:
+```
+vmdel "exam ISC_433 PCO"
+```
+
+Set the VM access for VM `89649fe3-4940-4b77-929e-50903789cd87` with: `VM_LIST` and `VM_DESTROY` for user `student@nexus.org`:
+```
+vmsetaccess 89649fe3-4940-4b77-929e-50903789cd87 student@nexus.org VM_LIST VM_DESTROY
+```
+
+Set VM access for VMs matching the "alpine" pattern with: `VM_START` and `VM_STOP` for user `student@nexus.org`:
+```
+vmsetaccess alpine student@nexus.org VM_START VM_STOP
+```
+
+Remove VM access for `student@nexus.org` from VM `89649fe3-4940-4b77-929e-50903789cd87`:
+```
+vmdelaccess 89649fe3-4940-4b77-929e-50903789cd87 student@nexus.org
+```
+
+Remove VM access for `student@nexus.org` from VMs matching the "lab2" pattern:
+```
+vmdelaccess lab2 student@nexus.org
+```
+
+Generate `exam_vms.pdf` with the credentials required to connect to all running VMs matching "exam prog sys":
+```
+vmcred2pdf "exam prog sys" output.pdf
+```
+
+Extract and download the `/home` directory of all VMs matching "exam prog sys" (each directory is saved in a `.tar.gz` archive named after the VM's ID):
+```
+vmexportdir "exam prog sys" /home
+```
+
+List all available templates:
+```
+tpllist .
+```
+
+List templates matching the "ubuntu" pattern:
+```
+tpllist ubuntu
+```
+
+Create a new `public` template, named "Xubuntu 22.04 + golang toolchain" based on VM `89649fe3-4940-4b77-929e-50903789cd87` (`public` templates are accessible to everyone while `private` templates are only accessible to their creators):
+```
+tpllist 89649fe3-4940-4b77-929e-50903789cd87 "Xubuntu 22.04 + golang toolchain" public
+```
+
+Delete template `3d440a31-17da-423d-8d95-a96b4cecff8b`:
+```
+tpldel 3d440a31-17da-423d-8d95-a96b4cecff8b
+```
+
+Quit `nexush`:
+```
+quit
+```
+
+## nexus-cli usage examples
+
+Given `nexus-cli` offers the same VM, user and template commands as `nexush`, we only show a handful of examples here.
+
+List available commands:
+```
+./nexus-cli
+```
+
+Authentify user `janedoe@nexus.org` and obtain an access token:
+```
+export NEXUS_TOKEN=`./nexus-cli login janedoe@nexus.org pipomolo`
+```
+
+Start VM `6713ce26-941e-4d95-8e92-6b71d44bf75a`:
+```
+vmstart 6713ce26-941e-4d95-8e92-6b71d44bf75a
+```
+
+## nexus-exam
+
+### Running nexus-exam
+
+<!--
+`nexus-exam` requires two environment variables:
+
+- `NEXUS_SERVER`: specifies the nexus server to connect to.
+- `NEXUS_CERT`: specifies the path to the public certificate required for encrypted communication (TLS) with the nexus server.
+
+Example of variables initialization:
+```
+export NEXUS_SERVER=10.136.26.127:1077
+export NEXUS_CERT=$(pwd)/ca-cert.isc-nexus-prod.pem
+```
+-->
+
+The purpose of `nexus-exam` is to be used during live exam sessions. Students are provided with laptops configured to boot on dedicated USB keys. These USB keys feature a minimal Linux system that automatically run `nexus-exam` at boot time. 50 such USB keys have been created and are ready to be used.
+
+The only thing that's necessary to use `nexus-exam` is to boot on one of the USB keys mentionned above.
+
+To obtain these USB keys, please contact Florent Gluck at florent.gluck@hesge.ch.
+
+Below is a screenshot of `nexus-exam`'s graphical interface:
+
+![](img/nexus-exam.png)
+
+
+## Tutorial: creating a live exam with nexush
+
+First and foremost, templates for the most popular distributions are available on HEPIA ISC nexus server. They are described in [Available template images](#available-template-images).
+
+Let's say you want to create an exam for the class "ProgSys". Let's assume 30 students are enrolled in the class.
+
+First, you need to create the VM that will be used by the students during the exam. Typically, this VM will contain the exam environment (compilers, editors, tools, etc.) along the description of the exam, for instance as a PDF on the desktop.
+
+1. First, log-in with your user:
+   ```
+   ./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
+         "quit" or "exit" to quit nexush
+   nexush>
+   ```
+
+1. List available templates and choose the one you wish to use for your exam VM:
+   ```
+   tpllist .
+   ```
+   This command displays the templates you can use:
+   ```
+   Debian 11 xfce (2022.08)                              | 0accbad1-3865-416f-bac7-a2f80ba7f081 | public
+   Manjaro 21 xfce (2022.08)                             | 502c30cb-49ea-4a83-a405-8c9182d9970c | public
+   Ubuntu 22.04 (2022.08)                                | 77518795-31e4-4fba-a160-22aa5d603f3e | public
+   Xubuntu 22.04 (2022.08) + dev env                     | 0fc5c07a-bc36-4583-934f-0cacf030221b | public
+   Xubuntu 22.04 (2023.02) + dev env/unpriv user         | 8b746cf9-1a9b-4dec-8f3d-4f7479fafd86 | public
+   Xubuntu 22.04 (2023.02) + dev env/unpriv user/pi-hole | 3d440a31-17da-423d-8d95-a96b4cecff8b | public
+   ```
+   Let's assume template `0fc5c07a-bc36-4583-934f-0cacf030221b` (Xubuntu 22.04 (2022.08) + dev env), 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 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 none 0fc5c07a-bc36-4583-934f-0cacf030221b
+   ```
+   This command displays the name and ID of the created VM(s):
+   ```
+   Created VM "Exam ProgSys Oct2022" | 148ea20e-9c45-4bd1-a670-f18312d436b8
+   ```
+
+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 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 progsys"
+   ```
+   This command opens a window showing the VM(s)'s desktop:
+   ![](img/vmattach.jpg)
+
+1. Configure the VM to suit your needs, by:
+   - 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)
+   -->
+
+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):
+   ```
+   tplcreate 148ea20e-9c45-4bd1-a670-f18312d436b8 "Exam ProgSys Oct2022" private
+   ```
+   This command displays details about the newly created template:
+   ```
+   {
+       "id": "0fb0b1f2-c72c-416e-961a-6bb802da89bb",
+       "name": "Exam ProgSys Oct2022",
+       "owner": "florent.gluck@hesge.ch",
+       "access": "private"
+       "creationTime": "2022-10-25T16:42:17+01:00"
+   }
+   ```
+
+1. You can now create the VMs for your 30 students using your new template (ID `0fb0b1f2-c72c-416e-961a-6bb802da89bb`). 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 none 0fb0b1f2-c72c-416e-961a-6bb802da89bb students.csv
+   ```
+   This command displays each VM created:
+   ```
+   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
+   ...
+   ```
+
+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:
+   ```
+   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 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:
+   ```
+   creds.pdf written successfully.
+   ```
+   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 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 vmkill` 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/nexus/Desktop/exam` directory of all VMs matching "Live Exam ProgSys" (each directory is saved in a `.tar.gz` archive named after the VM's name):
+   ```
+   vmexportdir "live exam progsys" /home/nexus/Desktop/exam
+   ```
+   This command displays each exported file tree:
+   ```
+   Successfully exported /home/nexus/Desktop/exam from VM "Live Exam ProgSys Oct2022 <Alia Friedman>" into Live Exam ProgSys Oct2022 <Alia Friedman>.tar.gz
+   Successfully exported /home/nexus/Desktop/exam from VM "Live Exam ProgSys Oct2022 <Aria Doyle>" into Live Exam ProgSys Oct2022 <Aria Doyle>.tar.gz
+   Successfully exported /home/nexus/Desktop/exam from VM "Live Exam ProgSys Oct2022 <Avah Coffey>" into Live Exam ProgSys Oct2022 <Avah Coffey>.tar.gz
+   Successfully exported /home/nexus/Desktop/exam from VM "Live Exam ProgSys Oct2022 <Briley Brady>" into Live Exam ProgSys Oct2022 <Briley Brady>.tar.gz
+   Successfully exported /home/nexus/Desktop/exam from VM "Live Exam ProgSys Oct2022 <Brooklyn Sweeney>" into Live Exam ProgSys Oct2022 <Brooklyn Sweeney>.tar.gz
+   Successfully exported /home/nexus/Desktop/exam from VM "Live Exam ProgSys Oct2022 <Cornelius Simmons>" into Live Exam ProgSys Oct2022 <Cornelius Simmons>.tar.gz
+   Successfully exported /home/nexus/Desktop/exam from VM "Live Exam ProgSys Oct2022 <Donovan Heath>" into Live Exam ProgSys Oct2022 <Donovan Heath>.tar.gz
+   Successfully exported /home/nexus/Desktop/exam from VM "Live Exam ProgSys Oct2022 <Ella Webster>" into Live Exam ProgSys Oct2022 <Ella Webster>.tar.gz
+   ...
+   ```
+
+## Advanced use: configuring nexush and nexus-cli
+
+Because communication between nexus client and nexus server is encrypted, a public certificate is required. Both clients, `nexush` and `nexus-cli`, embed the necessary public certificate to communicate with the server. By default, they are also configured to communicate with the production nexus-server available at ISC.
+
+Both clients can be configured to communicate with any nexus server. ISC has two nexus serveurs, one for production use and one for development. The corresponding public certificates can be found in the `certificates` directory on this git repository.
+
+Here are the ip addresses, ports and certificates to use ISC's production and development nexus servers:
+
+| Server      | IP            | Port | Certificate                |
+|---          |---            |---   |---                         |
+| production  | 10.136.26.127 | 1077 | ca-cert.isc-nexus-prod.pem |
+| development | 10.136.26.125 | 1077 | ca-cert.isc-nexus-dev.pem  |
+
+Nexus clients use two environment variables to specify which nexus server to use and on which port, and which public certificate to use:
+
+| Variable     | Description                                      |
+|---           |---                                               |
+| NEXUS_SERVER | server to use, using the syntax: `ip:port`       | 
+| NEXUS_CERT   | location (path) to the public certificate to use |
+
+On Linux and Mac OSX, a simple and effective way is to store these variables in a `.env` file that is sourced before running the clients.
+
+Here is an example of `.env.nexus` file configured to use the production ISC server:
+
+```
+export NEXUS_SERVER=10.136.26.127:1077
+export NEXUS_CERT=$(pwd)/ca-cert.isc-nexus-prod.pem
+```
+
+Then, source it and run your client of choice:
+```
+source .env.nexus
+./nexush
+```
+
+## Access control
+
+Access control is implemented through capabilities.
+Capabilities define what users can or cannot do. They are divided into two categories:
+
+- **User capabilities**: define user access control; these are stored in the user metadata
+- **VM access capabilities**: define access to VMs; these are stored in the VM metadata
+
+### User capabilities
+
+The various operations a user can or cannot do are defined by her/his capabilities.
+
+The table below lists all potential capabilities associated to a user:
+
+| Capability      | Description                                       |
+|---              |---                                                |
+| USER_CREATE     | Can create a user                                 |
+| USER_DESTROY    | Can destroy a user                                |
+| USER_SET_CAPS   | Can change a user's capabilities                  |
+| USER_LIST       | Can list all users                                |
+| VM_CREATE       | Can create a VM                                   |
+| VM_DESTROY_ANY  | Can destoy **ANY** VM                             |
+| VM_EDIT_ANY     | Can edit **ANY** VM                               |
+| VM_START_ANY    | Can start **ANY** VM                              |
+| VM_STOP_ANY     | Can kill/shutdown **ANY** VM                      |
+| VM_REBOOT_ANY   | Can Reboot **ANY** VM                             |
+| VM_LIST_ANY     | Can list **ANY** VM                               |
+| VM_READFS_ANY   | Can export files from **ANY** VM                  |
+| VM_WRITEFS_ANY  | Can import files into **ANY** VM                  |
+| VM_SET_ACCESS   | Can change (edit or delete) a VM's access         |
+| TPL_CREATE      | Can create a template                             |
+| TPL_EDIT        | Can edit a template                               |
+| TPL_EDIT_ANY    | Can edit **ANY** template                         |
+| TPL_DESTROY     | Can destroy a template                            |
+| TPL_DESTROY_ANY | Can destroy **ANY** template                      |
+| TPL_LIST        | Can list public or owned templates                |
+| TPL_LIST_ANY    | Can list **ANY** template                         |
+| TPL_READFS      | Can export files from a public or owned templates |
+| TPL_READFS_ANY  | Can export files from **ANY** template            |
+
+### VM access capabilities
+
+What a user can or cannot do **with a VM** is defined by the capabilities stored in the VM for this very user.
+
+The table below lists all potential capabilities granted to a user for a given VM.
+These capabilities are called "VM access capabilities":
+
+| Capability    | Description                                                        |
+|---            |---                                                                 |
+| VM_SET_ACCESS | User can add/change access to the VM                               |
+|               | VM_SET_ACCESS **must also be present** in the user's capabilities! |
+| VM_DESTROY    | User can destroy the VM                                            |
+| VM_EDIT       | User can edit the VM                                               |
+| VM_START      | User can start the VM                                              |
+| VM_STOP       | User can kill/shutdown the VM                                      |
+| VM_REBOOT     | User can reboot the VM                                             |
+| VM_LIST       | User can list or attach to the VM                                  |
+| VM_READFS     | User can export files from the VM                                  |
+| VM_WRITEFS    | User can import files into the VM                                  |
+
+### IMPORTANT
+
+- When a user creates a VM, she/he is **automatically granted all VM access capabilities**.
+- No other users is granted any access to the created VM. However, the VM owner can add any access type to any users they like.
+
+## Domain firewall
+
+Some templates come with a domain firewall already installed: [Pi-hole](https://pi-hole.net/).
+
+Pi-hole is a tool that lets you configure which domains are accessible and which are not. It can use whitelists or blacklists to define what's allowed or not and it's highly configurable through either a web interface or the command line.
+
+To configure Pi-hole in the VM:
+
+1. Log on with the `nexus` user as you'll need root privileges
+1. Set the password to access the web interface by running the following in a terminal:
+   ```
+   pihole -a -p
+   ```
+1. Log on Pi-hole's interface by going to `http://pi.hole` in a browser
+1. In the configuration, all domains are blocked except for the ones that are whitelisted; consequently, you must configure the domains you want to allow (you can also disable domain filtering entirely)
+1. Once done, the settings will be persistent
+
+The Pi-hole installation in the template(s) already come with domain rules to allow/forbid the three scenarios below:
+
+- [Cyberlearn](https://cyberlearn.hes-so.ch)
+- [Gitedu](https://gitedu.hesge.ch)
+- Docker Hub (to allow `docker pull/run` from docker CLI)
+
+## FAQ
+
+### [1] What is QEMU Guest Agent?
+
+QEMU Guest Agent is a service (a *daemon* in the UNIX terminology) that is meant to run inside a VM's guest OS. Its purpose is to interact with the hypervisor so that the hypervisor can read the state of the guest OS, send commands (e.g. shutdown, reset, etc.), read files, etc. Communications between hypervisor and QEMU Guest Agent is done through the QEMU Guest Agent Protocol. 
+
+Certain nexus features require QEMU Guest Agent to run in the VM's guest OS. This is the case of the `vmshutdown` command which gracefully shutdowns a VM. This command won't work if QEMU Guest Agent is not running in the VM's guest OS.
+
+All base OS VM templates provided in nexus feature QEMU Guest Agent, so users don't need to worry about it.
+
+On a Ubuntu/Debian system, the `qemu-guest-agent` service can be installed with:
+```
+sudo apt-get install qemu-guest-agent
+```
+
+### [2] How to automatically change the display resolution when the window's dimensions change?
+
+Some desktop environments automatically take care of this out of the box. Gnome in Ubuntu does it automatically, but XFCE doesn't do it for instance.
+
+Below we desribe what to do to solve this issue. The idea is to execute a script that runs indefinitely. The scripts' purpose is to monitor the display output and whenever a change of resolution is detected, it adjusts the display to the newly detected resolution.
+
+1. As root, create the bash script below and save it into `/usr/local/bin/update-virt-display`:
+   ```bash
+   #!/bin/bash
+
+   # Indefinitely wait for a screen resolution change and when one happens, update the desktop size accordingly.
+   xev -root -event randr | \
+   grep --line-buffered 'subtype XRROutputChangeNotifyEvent' | \
+   while read vars ; do \
+      xrandr --output "$(xrandr | awk '/ connected/{print $1; exit; }')" --auto
+   done
+   ```
+1. As root, make the script executable with:
+   ```
+   chmod +x /usr/local/bin/update-virt-display
+   ```
+1. Then, for each user you want to support, create a `.xprofile` file in the user's home directory with the following content:
+   ```
+   /usr/local/bin/update-virt-display &
+   ```
diff --git a/README.md b/docs/README_server.md
similarity index 100%
rename from README.md
rename to docs/README_server.md
diff --git a/tools/distribs_install.md b/docs/distribs_install.md
similarity index 96%
rename from tools/distribs_install.md
rename to docs/distribs_install.md
index a8da6b3..785258f 100644
--- a/tools/distribs_install.md
+++ b/docs/distribs_install.md
@@ -1,3 +1,7 @@
+<!--
+This document describes how each VM OS image is created and the post-install steps that have been undertaken.
+-->
+
 # Common to all distribs
 
 - username: `nexus`
diff --git a/img/nexus-exam.png b/docs/img/nexus-exam.png
similarity index 100%
rename from img/nexus-exam.png
rename to docs/img/nexus-exam.png
diff --git a/img/vmattach.jpg b/docs/img/vmattach.jpg
similarity index 100%
rename from img/vmattach.jpg
rename to docs/img/vmattach.jpg
diff --git a/img/vmcred2pdf.png b/docs/img/vmcred2pdf.png
similarity index 100%
rename from img/vmcred2pdf.png
rename to docs/img/vmcred2pdf.png
diff --git a/src/client/cmdTemplate/helper.go b/src/client/cmdTemplate/helper.go
index 3dc7365..5adf5cd 100644
--- a/src/client/cmdTemplate/helper.go
+++ b/src/client/cmdTemplate/helper.go
@@ -7,21 +7,13 @@ import (
     "strings"
     "encoding/json"
     "nexus-client/cmd"
+    t "nexus-common/template"
     u "nexus-client/utils"
     g "nexus-client/globals"
     "github.com/google/uuid"
     "github.com/go-resty/resty/v2"
 )
 
-// Converts a Template structure into a pretty string.
-func (tpl *Template)String() string {
-    output, err := json.MarshalIndent(tpl, "", "    ")
-    if err != nil {
-        return err.Error()
-    }
-    return string(output)
-}
-
 func printUsage(c cmd.Command, action string) {
     for _, desc := range c.GetDesc() {
         u.PrintlnErr(desc)
@@ -81,7 +73,12 @@ func printFilteredTemplates(c cmd.Command, args []string, route string) int {
 
     if foundLongOutputFlag >= 0 {
         for _, template := range templates {
-            u.Println(template.String())
+            str, err := template.String()
+            if err != nil {
+                u.PrintlnErr("Failed decoding template "+template.ID.String()+" to string. Skipped.")
+            } else {
+                u.Println(str)
+            }
         }
     } else {
         // Compute the length of the longest name and access (used for padding columns)
@@ -122,7 +119,7 @@ func printFilteredTemplates(c cmd.Command, args []string, route string) int {
 // Regular expression examples:
 //   "."   -> matches everything
 //   "bla" -> matches any template name containing "bla"
-func getFilteredTemplates(route string, patterns []string) ([]Template, error) {
+func getFilteredTemplates(route string, patterns []string) ([]t.TemplateSerialized, error) {
     if len(patterns) < 1 {
         return nil, errors.New("At least one ID or regex must be specified")
     }
@@ -147,7 +144,7 @@ func getFilteredTemplates(route string, patterns []string) ([]Template, error) {
         return nil, err
     }
 
-    templatesList := []Template{}
+    templatesList := []t.TemplateSerialized{}
 
     if resp.IsSuccess() {
         templates, err := getTemplates(resp)
@@ -186,8 +183,8 @@ func getFilteredTemplates(route string, patterns []string) ([]Template, error) {
 }
 
 // Retrieves all templates (no filtering).
-func getTemplates(resp *resty.Response) ([]Template, error) {
-    templates := []Template{}
+func getTemplates(resp *resty.Response) ([]t.TemplateSerialized, error) {
+    templates := []t.TemplateSerialized{}
     if err := json.Unmarshal(resp.Body(), &templates); err != nil {
         return nil, err
     }
diff --git a/src/client/cmdTemplate/templateCreate.go b/src/client/cmdTemplate/templateCreate.go
index 44569d2..5266b0d 100644
--- a/src/client/cmdTemplate/templateCreate.go
+++ b/src/client/cmdTemplate/templateCreate.go
@@ -26,11 +26,11 @@ func (cmd *Create)PrintUsage() {
         u.PrintlnErr(desc)
     }
     u.PrintlnErr("―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――")
-    u.PrintlnErr("USAGE: "+cmd.GetName()+" ID name access")
+    u.PrintlnErr("USAGE: "+cmd.GetName()+" name access vmID")
     u.PrintlnErr("―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――")
-    u.PrintlnErr("vmID      ID of the VM used to create the template.")
     u.PrintlnErr("name      Name of the template to create.")
-    u.PrintlnErr("access    Access type, either \"public\" or \"private\"")
+    u.PrintlnErr("access    Access type, either \"public\" or \"private\".")
+    u.PrintlnErr("vmID      ID of the VM used to create the template.")
 }
 
 func (cmd *Create)Run(args []string) int {
@@ -40,12 +40,12 @@ func (cmd *Create)Run(args []string) int {
         return 1
     }
 
-    vmID, err := uuid.Parse(args[0])
+    name := args[0]
+    access := args[1]
+    vmID, err := uuid.Parse(args[2])
     if err != nil {
         u.PrintlnErr(err)
     }
-    name := args[1]
-    access := args[2]
 
     resp, err := cmd.makeRequestForID(vmID, name, access)
     if err != nil {
diff --git a/src/client/cmdVM/helper.go b/src/client/cmdVM/helper.go
index f085cb5..053916a 100644
--- a/src/client/cmdVM/helper.go
+++ b/src/client/cmdVM/helper.go
@@ -7,21 +7,13 @@ import (
     "strings"
     "encoding/json"
     "nexus-client/cmd"
+    "nexus-common/vm"
     u "nexus-client/utils"
     g "nexus-client/globals"
     "github.com/google/uuid"
     "github.com/go-resty/resty/v2"
 )
 
-// Converts a VM structure into a pretty string.
-func (vm *VMNetworkSerialized)String() string {
-    output, err := json.MarshalIndent(vm, "", "    ")
-    if err != nil {
-        return err.Error()
-    }
-    return string(output)
-}
-
 func printRegexUsage(c cmd.Command) {
     for _, desc := range c.GetDesc() {
         u.PrintlnErr(desc)
@@ -74,7 +66,13 @@ func printFilteredVMs(c cmd.Command, args []string, route string) int {
 
     if foundLongOutputFlag >= 0 {
         for _, vm := range vms {
-            u.Println(vm.String())
+            str, err := vm.String()
+            if err != nil {
+                u.PrintlnErr("Failed decoding VM "+vm.ID.String()+" to string. Skipped.")
+            } else {
+                u.Println(str)
+            }
+
         }
     } else {
         // Compute the length of the longest name and state (used for padding columns)
@@ -114,7 +112,7 @@ func printFilteredVMs(c cmd.Command, args []string, route string) int {
 // Regular expression examples:
 //   "."   -> matches everything
 //   "bla" -> matches any VM name containing "bla"
-func getFilteredVMs(route string, patterns []string) ([]VMNetworkSerialized, error) {
+func getFilteredVMs(route string, patterns []string) ([]vm.VMNetworkSerialized, error) {
     if len(patterns) < 1 {
         return nil, errors.New("At least one ID or regex must be specified")
     }
@@ -139,7 +137,7 @@ func getFilteredVMs(route string, patterns []string) ([]VMNetworkSerialized, err
         return nil, err
     }
 
-    vmsList := []VMNetworkSerialized{}
+    vmsList := []vm.VMNetworkSerialized{}
 
     if resp.IsSuccess() {
         vms, err := getVMs(resp)
@@ -178,8 +176,8 @@ func getFilteredVMs(route string, patterns []string) ([]VMNetworkSerialized, err
 }
 
 // Retrieves all VMs (no filtering).
-func getVMs(resp *resty.Response) ([]VMNetworkSerialized, error) {
-    vms := []VMNetworkSerialized{}
+func getVMs(resp *resty.Response) ([]vm.VMNetworkSerialized, error) {
+    vms := []vm.VMNetworkSerialized{}
     if err := json.Unmarshal(resp.Body(), &vms); err != nil {
         return nil, err
     }
@@ -187,8 +185,8 @@ func getVMs(resp *resty.Response) ([]VMNetworkSerialized, error) {
 }
 
 // Retrieve a single VM.
-func getVM(resp *resty.Response) (*VMNetworkSerialized, error) {
-    var vm *VMNetworkSerialized = &VMNetworkSerialized{}
+func getVM(resp *resty.Response) (*vm.VMNetworkSerialized, error) {
+    var vm *vm.VMNetworkSerialized = &vm.VMNetworkSerialized{}
     if err := json.Unmarshal(resp.Body(), vm); err != nil {
         return vm, err
     }
diff --git a/src/client/cmdVM/vm.go b/src/client/cmdVM/vm.go
deleted file mode 100644
index b54daf2..0000000
--- a/src/client/cmdVM/vm.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package cmdVM
-
-import (
-    "github.com/google/uuid"
-)
-
-// Make sure these types MATCH their counterparts in nexus-server codebase!
-type (
-    VMNetworkSerialized struct {
-        ID uuid.UUID           `json:"id"`
-        Owner string           `json:"owner"`
-        Name string            `json:"name"`
-        Cpus int               `json:"cpus"`
-        Ram int                `json:"ram"`
-        Nic NicType            `json:"nic"`
-        UsbDevs []string       `json:"usbDevs"`  // vendorID:productID
-        TemplateID uuid.UUID   `json:"templateID"`
-        Access map[string]Capabilities `json:"access"`
-        State VMState          `json:"state"`
-        Port int               `json:"port"`
-        Pwd string             `json:"pwd"`
-        DiskBusy bool          `json:"diskBusy"`
-    }
-
-    Capabilities map[string]int
-
-    VMState string
-    NicType string
-)
-
-const (
-    STOPPED VMState = "STOPPED"
-    RUNNING = "RUNNING"
-)
\ No newline at end of file
diff --git a/src/client/cmdVM/vmAttach.go b/src/client/cmdVM/vmAttach.go
index d56af7a..0fc0fef 100644
--- a/src/client/cmdVM/vmAttach.go
+++ b/src/client/cmdVM/vmAttach.go
@@ -3,6 +3,7 @@ package cmdVM
 import (
     // "sync"
     "nexus-client/exec"
+    "nexus-common/vm"
     u "nexus-client/utils"
     g "nexus-client/globals"
 )
@@ -51,11 +52,11 @@ func (cmd *Attach)Run(args []string) int {
     // var wg sync.WaitGroup
     // wg.Add(len(vms))
 
-    for _, vm := range(vms) {
-        go func(vm VMNetworkSerialized) {
-            exec.RunRemoteViewer(hostname, cert, vm.Name, vm.Port, vm.Pwd, false)
+    for _, v := range(vms) {
+        go func(v vm.VMNetworkSerialized) {
+            exec.RunRemoteViewer(hostname, cert, v.Name, v.Port, v.Pwd, false)
             // wg.Done()
-        } (vm)
+        } (v)
     }
 
     // wg.Wait()
diff --git a/src/client/cmdVM/vmCreate.go b/src/client/cmdVM/vmCreate.go
index dc72b9c..eefb0c4 100644
--- a/src/client/cmdVM/vmCreate.go
+++ b/src/client/cmdVM/vmCreate.go
@@ -5,6 +5,7 @@ import (
     "strconv"
     u "nexus-client/utils"
     g "nexus-client/globals"
+    "nexus-common/vm"
     "github.com/google/uuid"
 )
 
@@ -27,7 +28,7 @@ func (cmd *Create)PrintUsage() {
         u.PrintlnErr(desc)
     }
     u.PrintlnErr("―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――")
-    u.PrintlnErr("USAGE: "+cmd.GetName()+" name cpus ram nic usb template [count|file.csv]")
+    u.PrintlnErr("USAGE: "+cmd.GetName()+" name cpus ram nic usb templateID [count|file.csv]")
     u.PrintlnErr("―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――")
     const usage string = `name        Name of the VM to create.
 cpus        Number of CPUs, between 1 and 16.
@@ -66,7 +67,7 @@ func (cmd *Create)Run(args []string) int {
         u.PrintlnErr("Invalid amount of RAM")
         return 1
     }
-    nic := NicType(args[3])
+    nic := vm.NicType(args[3])
     usbDevs := u.Str2UsbDevices(args[4])
     templateID, err := uuid.Parse(args[5])
     if err != nil {
@@ -104,7 +105,7 @@ func (cmd *Create)Run(args []string) int {
         Name string
         Cpus int
         Ram int
-        Nic NicType
+        Nic vm.NicType
         UsbDevs []string
         TemplateID uuid.UUID
     }
@@ -125,10 +126,10 @@ func (cmd *Create)Run(args []string) int {
         if csvEntries == nil {
             if count > 1 {
                 numberPadded := fmt.Sprintf("%0"+strconv.Itoa(digits)+"d", i)
-                vmArgs.Name = name+" <"+numberPadded+">"
+                vmArgs.Name = name+" "+numberPadded
             }
         } else {
-            vmArgs.Name = name+" <"+csvEntries[i-1]+">"
+            vmArgs.Name = name+" "+csvEntries[i-1]
         }
 
         resp, err := client.R().SetBody(vmArgs).Post(host+"/vms")
diff --git a/src/client/cmdVM/vmEdit.go b/src/client/cmdVM/vmEdit.go
index 4d6248b..759a3b7 100644
--- a/src/client/cmdVM/vmEdit.go
+++ b/src/client/cmdVM/vmEdit.go
@@ -4,6 +4,7 @@ import (
     "strconv"
     "strings"
     "errors"
+    "nexus-common/vm"
     u "nexus-client/utils"
     g "nexus-client/globals"
 )
@@ -16,7 +17,7 @@ type vmEditParameters struct {
     Name string
     Cpus int
     Ram int
-    Nic NicType
+    Nic vm.NicType
     UsbDevs []string
 }
 
@@ -146,7 +147,7 @@ func (cmd *Edit)parseArgs(args []string) (*vmEditParameters, []string, error) {
         }
         s = getStringVal(arg, "nic=")
         if s != "" {
-            vmParams.Nic = NicType(s)
+            vmParams.Nic = vm.NicType(s)
             atLeastOneArg = true
             continue
         }
diff --git a/src/client/cmdVM/vmExportDir.go b/src/client/cmdVM/vmExportDir.go
index b2e07b2..5ac187f 100644
--- a/src/client/cmdVM/vmExportDir.go
+++ b/src/client/cmdVM/vmExportDir.go
@@ -68,7 +68,7 @@ func (cmd *ExportDir)Run(args []string) int {
 
     for _, vm := range(vms) {
         uuid := vm.ID.String()
-        outputFile := vm.Name+".tar.gz"
+        outputFile := vm.Name+"_"+vm.ID.String()+".tar.gz"
         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())
diff --git a/src/client/nexus-cli/go.mod b/src/client/nexus-cli/go.mod
index d5a92aa..89ae454 100644
--- a/src/client/nexus-cli/go.mod
+++ b/src/client/nexus-cli/go.mod
@@ -2,12 +2,20 @@ module nexus-cli
 
 go 1.18
 
+replace nexus-common/caps => ../../common/caps
+
+replace nexus-common/vm => ../../common/vm
+
+replace nexus-common/template => ../../common/template
+
 replace nexus-client/cmd => ../cmd
 
 replace nexus-client/globals => ../globals
 
 replace nexus-client/defaults => ../defaults
 
+replace nexus-common/utils => ../../common/utils
+
 replace nexus-client/utils => ../utils
 
 replace nexus-client/cmdLogin => ../cmdLogin
@@ -49,4 +57,8 @@ require (
 	nexus-client/defaults v0.0.0-00010101000000-000000000000 // indirect
 	nexus-client/exec v0.0.0-00010101000000-000000000000 // indirect
 	nexus-client/version v0.0.0-00010101000000-000000000000 // indirect
+	nexus-common/caps v0.0.0-00010101000000-000000000000 // indirect
+	nexus-common/template v0.0.0-00010101000000-000000000000 // indirect
+	nexus-common/utils v0.0.0-00010101000000-000000000000 // indirect
+	nexus-common/vm v0.0.0-00010101000000-000000000000 // indirect
 )
diff --git a/src/client/nexus-cli/nexus-cli.go b/src/client/nexus-cli/nexus-cli.go
index a1a1b13..d704775 100644
--- a/src/client/nexus-cli/nexus-cli.go
+++ b/src/client/nexus-cli/nexus-cli.go
@@ -6,6 +6,7 @@ import (
     "strings"
     "syscall"
     "os/signal"
+    "nexus-common/utils"
     u "nexus-client/utils"
     g "nexus-client/globals"
     "nexus-client/defaults"
@@ -106,7 +107,7 @@ func main() {
         // os.Exit(1)
     }
 
-    if !u.FileExists(certEnvVar) {
+    if !utils.FileExists(certEnvVar) {
         u.PrintlnErr("Failed reading certificate \""+certEnvVar+"\"!")
         return
     }
diff --git a/src/client/nexus-cli/validate b/src/client/nexus-cli/validate
index 9cc7956..03a957e 100755
--- a/src/client/nexus-cli/validate
+++ b/src/client/nexus-cli/validate
@@ -75,7 +75,7 @@ check "vmimportdir"
 OK
 
 echo "Create template from VM (can take a few minutes)..."
-templateID=`$nexus_cli tplcreate $vmID "$partial_name" private | grep \"id\" | awk -F'"' '{print $4}'`
+templateID=`$nexus_cli tplcreate "$partial_name" private $vmID | grep \"id\" | awk -F'"' '{print $4}'`
 checkVar $templateID "tplcreate"
 OK
 
diff --git a/src/client/nexus-exam/go.mod b/src/client/nexus-exam/go.mod
index a6c4df6..350be8a 100644
--- a/src/client/nexus-exam/go.mod
+++ b/src/client/nexus-exam/go.mod
@@ -2,6 +2,8 @@ module nexus-exam
 
 go 1.18
 
+replace nexus-common/utils => ../../common/utils
+
 replace nexus-client/utils => ../utils
 
 replace nexus-client/globals => ../globals
@@ -42,4 +44,5 @@ require (
 	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
+	nexus-common/utils v0.0.0-00010101000000-000000000000 // indirect
 )
diff --git a/src/client/nexus-exam/nexus-exam.go b/src/client/nexus-exam/nexus-exam.go
index dcc7797..3d5245b 100644
--- a/src/client/nexus-exam/nexus-exam.go
+++ b/src/client/nexus-exam/nexus-exam.go
@@ -4,6 +4,7 @@ import (
     "os"
     "errors"
     "strconv"
+    "nexus-common/utils"
     "nexus-client/exec"
     u "nexus-client/utils"
     g "nexus-client/globals"
@@ -39,7 +40,7 @@ func main() {
         os.Exit(1)
     }
 
-    if !u.FileExists(pubCert) {
+    if !utils.FileExists(pubCert) {
         u.PrintlnErr("Failed reading certificate \""+pubCert+"\"!")
         os.Exit(1)
     }
diff --git a/src/client/nexush/go.mod b/src/client/nexush/go.mod
index b460f99..2e81f76 100644
--- a/src/client/nexush/go.mod
+++ b/src/client/nexush/go.mod
@@ -2,12 +2,20 @@ module nexush
 
 go 1.18
 
+replace nexus-common/caps => ../../common/caps
+
+replace nexus-common/vm => ../../common/vm
+
+replace nexus-common/template => ../../common/template
+
 replace nexus-client/version => ../version
 
 replace nexus-client/globals => ../globals
 
 replace nexus-client/defaults => ../defaults
 
+replace nexus-common/utils => ../../common/utils
+
 replace nexus-client/utils => ../utils
 
 replace nexus-client/cmd => ../cmd
@@ -36,7 +44,6 @@ require (
 	nexus-client/cmdUser v0.0.0-00010101000000-000000000000
 	nexus-client/cmdVM v0.0.0-00010101000000-000000000000
 	nexus-client/globals v0.0.0-00010101000000-000000000000
-	nexus-client/utils v0.0.0-00010101000000-000000000000
 )
 
 require (
@@ -50,5 +57,10 @@ require (
 	nexus-client/cmdVersion v0.0.0-00010101000000-000000000000 // indirect
 	nexus-client/defaults v0.0.0-00010101000000-000000000000 // indirect
 	nexus-client/exec v0.0.0-00010101000000-000000000000 // indirect
+	nexus-client/utils v0.0.0-00010101000000-000000000000 // indirect
 	nexus-client/version v0.0.0-00010101000000-000000000000 // indirect
+	nexus-common/caps v0.0.0-00010101000000-000000000000 // indirect
+	nexus-common/template v0.0.0-00010101000000-000000000000 // indirect
+	nexus-common/utils v0.0.0-00010101000000-000000000000 // indirect
+	nexus-common/vm v0.0.0-00010101000000-000000000000 // indirect
 )
diff --git a/src/client/nexush/nexush.go b/src/client/nexush/nexush.go
index 6bb6595..e156e7c 100644
--- a/src/client/nexush/nexush.go
+++ b/src/client/nexush/nexush.go
@@ -10,6 +10,7 @@ import (
     "os/signal"
     "golang.org/x/term"
     u "nexus-client/utils"
+    "nexus-common/utils"
     g "nexus-client/globals"
     "nexus-client/defaults"
     "nexus-client/version"
@@ -116,7 +117,7 @@ func main() {
         // os.Exit(1)
     }
 
-    if !u.FileExists(certEnvVar) {
+    if !utils.FileExists(certEnvVar) {
         u.PrintlnErr("Failed reading certificate \""+certEnvVar+"\"!")
         return
     }
diff --git a/src/client/utils/utils.go b/src/client/utils/utils.go
index bd335b3..093ff5b 100644
--- a/src/client/utils/utils.go
+++ b/src/client/utils/utils.go
@@ -34,15 +34,6 @@ func PrintlnErr(a ...any) {
     PrintErr("\n")
 }
 
-// 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()
-}
-
 // Creates a tar.gz archive of dir and all its files and subdirectories.
 // Note: dir can also be a file.
 // Source code slightly modified from: https://gist.github.com/mimoo/25fc9716e0f1353791f5908f94d6e726
diff --git a/src/client/version/version.go b/src/client/version/version.go
index bdebfde..0c9b978 100644
--- a/src/client/version/version.go
+++ b/src/client/version/version.go
@@ -7,8 +7,8 @@ import (
 
 const (
     major  = 1
-    minor  = 6
-    bugfix = 7
+    minor  = 7
+    bugfix = 0
 )
 
 type Version struct {
diff --git a/src/server/caps/caps.go b/src/common/caps/caps.go
similarity index 100%
rename from src/server/caps/caps.go
rename to src/common/caps/caps.go
diff --git a/src/common/caps/go.mod b/src/common/caps/go.mod
new file mode 100644
index 0000000..4055a1f
--- /dev/null
+++ b/src/common/caps/go.mod
@@ -0,0 +1,3 @@
+module nexus-common/caps
+
+go 1.18
diff --git a/src/common/template/go.mod b/src/common/template/go.mod
new file mode 100644
index 0000000..2c24cf7
--- /dev/null
+++ b/src/common/template/go.mod
@@ -0,0 +1,5 @@
+module nexus-common/template
+
+go 1.18
+
+require github.com/google/uuid v1.3.0
diff --git a/src/common/template/go.sum b/src/common/template/go.sum
new file mode 100644
index 0000000..3dfe1c9
--- /dev/null
+++ b/src/common/template/go.sum
@@ -0,0 +1,2 @@
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
diff --git a/src/client/cmdTemplate/template.go b/src/common/template/template.go
similarity index 56%
rename from src/client/cmdTemplate/template.go
rename to src/common/template/template.go
index ae56118..230f1ce 100644
--- a/src/client/cmdTemplate/template.go
+++ b/src/common/template/template.go
@@ -1,13 +1,14 @@
-package cmdTemplate
+package template
 
 import (
     "time"
+    "encoding/json"
     "github.com/google/uuid"
 )
 
-// Make sure these types MATCH their counterparts in nexus-server codebase!
+// Template fields to be serialized over the network.
 type (
-    Template struct {
+    TemplateSerialized 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"`
@@ -15,3 +16,12 @@ type (
         CreationTime time.Time `json:"creationTime" validate:"required"`
     }
 )
+
+// Converts a TemplateSerialized structure into a pretty string.
+func (t *TemplateSerialized)String() (string, error) {
+    output, err := json.MarshalIndent(t, "", "    ")
+    if err != nil {
+        return "", err
+    }
+    return string(output), nil
+}
diff --git a/src/common/utils/go.mod b/src/common/utils/go.mod
new file mode 100644
index 0000000..25b5552
--- /dev/null
+++ b/src/common/utils/go.mod
@@ -0,0 +1,3 @@
+module nexus-common/utils
+
+go 1.18
diff --git a/src/common/utils/utils.go b/src/common/utils/utils.go
new file mode 100644
index 0000000..b607ad2
--- /dev/null
+++ b/src/common/utils/utils.go
@@ -0,0 +1,14 @@
+package utils
+
+import (
+    "os"
+)
+
+// 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()
+}
diff --git a/src/common/vm/go.mod b/src/common/vm/go.mod
new file mode 100644
index 0000000..a3e6b52
--- /dev/null
+++ b/src/common/vm/go.mod
@@ -0,0 +1,5 @@
+module nexus-common/vm
+
+go 1.18
+
+require github.com/google/uuid v1.3.0
diff --git a/src/common/vm/go.sum b/src/common/vm/go.sum
new file mode 100644
index 0000000..3dfe1c9
--- /dev/null
+++ b/src/common/vm/go.sum
@@ -0,0 +1,2 @@
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
diff --git a/src/common/vm/vm.go b/src/common/vm/vm.go
new file mode 100644
index 0000000..e748d64
--- /dev/null
+++ b/src/common/vm/vm.go
@@ -0,0 +1,60 @@
+package vm
+
+import (
+    "encoding/json"
+    "nexus-common/caps"
+    "github.com/google/uuid"
+)
+
+type (
+    // VM fields to be serialized to disk.
+    VMDiskSerialized 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"`
+        UsbDevs []string      `json:"usbDevs"    validate:"required"`
+        TemplateID uuid.UUID  `json:"templateID" validate:"required"`
+        Access map[string]caps.Capabilities `json:"access"`
+    }
+
+    // VM fields to be serialized over the network.
+    VMNetworkSerialized 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"`
+        UsbDevs []string      `json:"usbDevs"    validate:"required"`
+        TemplateID uuid.UUID  `json:"templateID" validate:"required"`
+        Access map[string]caps.Capabilities `json:"access"`
+
+        State VMState          `json:"state"`
+        Port int               `json:"port"`
+        Pwd string             `json:"pwd"`
+        DiskBusy bool          `json:"diskBusy"`
+    }
+
+    VMState string  // see stateXXX below
+    NicType string  // see nicXXX below
+)
+
+const (
+    StateStopped VMState = "STOPPED"
+    StateRunning VMState = "RUNNING"
+
+    NicUser NicType = "user"
+    NicNone NicType = "none"
+)
+
+// Converts a VMNetworkSerialized structure into a pretty string.
+func (v *VMNetworkSerialized)String() (string, error) {
+    output, err := json.MarshalIndent(v, "", "    ")
+    if err != nil {
+        return "", err
+    }
+    return string(output), nil
+}
diff --git a/src/server/caps/go.mod b/src/server/caps/go.mod
deleted file mode 100644
index 95c9608..0000000
--- a/src/server/caps/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module nexus-server/caps
-
-go 1.18
diff --git a/src/server/exec/QemuSystem.go b/src/server/exec/QemuSystem.go
index 8d3bde2..2f7d0dd 100644
--- a/src/server/exec/QemuSystem.go
+++ b/src/server/exec/QemuSystem.go
@@ -98,9 +98,8 @@ func NewQemuSystem(qgaSock string, cpus, ram int, nic string, usbDevs []string,
     args = append(args, "-device", "virtio-serial", "-device", "virtserialport,chardev=qga0,name=org.qemu.guest_agent.0", "-chardev", "socket,path="+qgaSock+",server=on,wait=off,id=qga0")
     // USB redirection
     args = append(args, usb...)
-
     // Sound support
-    // args = append(args, "-soundhw", "hda")
+    args = append(args, "-soundhw", "hda")
 
     // To share a folder with the host:
     // - Add QEMU args: virtfs local,path=/tmp/pipo,mount_tag=sharedfs,security_model=none
diff --git a/src/server/go.mod b/src/server/go.mod
index eb8cd55..b7baa9d 100644
--- a/src/server/go.mod
+++ b/src/server/go.mod
@@ -2,6 +2,12 @@ module nexus-server
 
 go 1.18
 
+replace nexus-common/template => ../common/template
+
+replace nexus-common/vm => ../common/vm
+
+replace nexus-common/caps => ../common/caps
+
 replace nexus-server/consts => ./consts
 
 replace nexus-server/users => ./users
@@ -14,8 +20,6 @@ replace nexus-server/vms => ./vms
 
 replace nexus-server/utils => ./utils
 
-replace nexus-server/caps => ./caps
-
 replace nexus-server/paths => ./paths
 
 replace nexus-server/cleaner => ./cleaner
@@ -57,7 +61,9 @@ require (
 	golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
 	golang.org/x/text v0.3.7 // indirect
 	golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
-	nexus-server/caps v0.0.0-00010101000000-000000000000 // indirect
+	nexus-common/caps v0.0.0-00010101000000-000000000000 // indirect
+	nexus-common/template v0.0.0-00010101000000-000000000000 // indirect
+	nexus-common/vm v0.0.0-00010101000000-000000000000 // indirect
 	nexus-server/exec v0.0.0-00010101000000-000000000000 // indirect
 	nexus-server/qga v0.0.0-00010101000000-000000000000 // indirect
 	nexus-server/version v0.0.0-00010101000000-000000000000 // indirect
diff --git a/src/server/router/routerTemplates.go b/src/server/router/routerTemplates.go
index b13d606..7804291 100644
--- a/src/server/router/routerTemplates.go
+++ b/src/server/router/routerTemplates.go
@@ -5,8 +5,8 @@ import (
     "os"
     "net/http"
     "path/filepath"
+    "nexus-common/caps"
     "nexus-server/vms"
-    "nexus-server/caps"
     "nexus-server/paths"
     "nexus-server/users"
     "github.com/labstack/echo/v4"
@@ -40,12 +40,12 @@ func (r *RouterTemplates)GetTemplates(c echo.Context) 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 }), "    ")
+        return c.JSONPretty(http.StatusOK, r.tpl.GetNetworkSerializedTemplates(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()
+            r.tpl.GetNetworkSerializedTemplates(func(t vms.Template) bool {
+                return t.GetOwner() == user.Email || t.IsPublic()
             }), "    ")
     } else {
         return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps)
@@ -88,7 +88,7 @@ func (r *RouterTemplates)CreateTemplateFromVM(c echo.Context) error {
     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]
+            userCaps, exists := vm.GetAccess()[user.Email]
             if !exists {
                 return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps)
             }
@@ -111,7 +111,7 @@ func (r *RouterTemplates)CreateTemplateFromVM(c echo.Context) error {
         return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
     }
 
-    return c.JSONPretty(http.StatusCreated, template, "    ")
+    return c.JSONPretty(http.StatusCreated, template.SerializeToNetwork(), "    ")
 }
 
 // Creates a template from an uploaded .qcow file.
@@ -181,7 +181,7 @@ func (r *RouterTemplates)CreateTemplateFromQCOW(c echo.Context) error {
         return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
     }
 
-    return c.JSONPretty(http.StatusCreated, template, "    ")
+    return c.JSONPretty(http.StatusCreated, template.SerializeToNetwork(), "    ")
 }
 
 // Deletes a template based on its ID.
@@ -214,7 +214,7 @@ func (r *RouterTemplates)DeleteTemplateByID(c echo.Context) error {
             return echo.NewHTTPError(http.StatusNotFound, err.Error())
         }
 
-        if template.Owner == user.Email {
+        if template.GetOwner() == user.Email {
             if err := r.tpl.DeleteTemplate(tplID, r.vms); err != nil {
                 return echo.NewHTTPError(http.StatusNotFound, err.Error())
             }
@@ -265,7 +265,7 @@ func (r *RouterTemplates)EditTemplateByID(c echo.Context) error {
         if err != nil {
             return echo.NewHTTPError(http.StatusNotFound, err.Error())
         }
-        if template.Owner == user.Email {
+        if template.GetOwner() == user.Email {
             if err := decodeJson(c, &p); err != nil {
                 return echo.NewHTTPError(http.StatusBadRequest, err.Error())
             }
@@ -309,7 +309,7 @@ func (r *RouterTemplates)ExportDisk(c echo.Context) error {
         if err != nil {
             return echo.NewHTTPError(http.StatusNotFound, err.Error())
         }
-        if t.IsPublic() || t.Owner == user.Email {
+        if t.IsPublic() || t.GetOwner() == user.Email {
             return c.File(t.GetTemplateDiskPath())
         }
     }
diff --git a/src/server/router/routerUsers.go b/src/server/router/routerUsers.go
index cd713b5..e68af79 100644
--- a/src/server/router/routerUsers.go
+++ b/src/server/router/routerUsers.go
@@ -2,7 +2,7 @@ package router
 
 import (
     "net/http"
-    "nexus-server/caps"
+    "nexus-common/caps"
     "nexus-server/users"
     "nexus-server/utils"
     "github.com/labstack/echo/v4"
diff --git a/src/server/router/routerVMs.go b/src/server/router/routerVMs.go
index cd9e4b7..4bd70fb 100644
--- a/src/server/router/routerVMs.go
+++ b/src/server/router/routerVMs.go
@@ -5,8 +5,9 @@ import (
     "os"
     "net/http"
     "path/filepath"
+    "nexus-common/caps"
+    vmc "nexus-common/vm"
     "nexus-server/vms"
-    "nexus-server/caps"
     "nexus-server/users"
     "nexus-server/paths"
     "github.com/google/uuid"
@@ -126,7 +127,7 @@ func (r *RouterVMs)GetEditableVMAccessVMs(c echo.Context) error {
                 // 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]
+                    capabilities, exists := vm.GetAccess()[user.Email]
                     if exists {
                         _, visible := capabilities[caps.CAP_VM_SET_ACCESS]
                         return visible && !vm.IsRunning()
@@ -182,7 +183,7 @@ func (r *RouterVMs)CreateVM(c echo.Context) error {
         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"`
-        Nic vms.NicType       `json:"nic"        validate:"required`
+        Nic vmc.NicType       `json:"nic"        validate:"required`
         UsbDevs []string      `json:"usbDevs"    validate:"required`
         TemplateID uuid.UUID  `json:"templateID" validate:"required"`
     }
@@ -211,7 +212,7 @@ func (r *RouterVMs)CreateVM(c echo.Context) error {
 // 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 {
+        if err := r.vms.DeleteVM(vm.GetID()); err != nil {
             return echo.NewHTTPError(http.StatusNotFound, err.Error())
         }
         return c.JSONPretty(http.StatusOK, jsonMsg("OK"), "    ")
@@ -225,7 +226,7 @@ func (r *RouterVMs)DeleteVMByID(c echo.Context) error {
 // 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)
+        _, _, err := r.vms.StartVM(vm.GetID())
         if err != nil {
             return echo.NewHTTPError(http.StatusBadRequest, err.Error())
         }
@@ -240,7 +241,7 @@ func (r *RouterVMs)StartVM(c echo.Context) error {
 // 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 {
+        if err := r.vms.KillVM(vm.GetID()); err != nil {
             return echo.NewHTTPError(http.StatusNotFound, err.Error())
         }
         return c.JSONPretty(http.StatusOK, jsonMsg("OK"), "    ")
@@ -254,7 +255,7 @@ func (r *RouterVMs)KillVM(c echo.Context) error {
 // 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 {
+        if err := r.vms.ShutdownVM(vm.GetID()); err != nil {
             return echo.NewHTTPError(http.StatusNotFound, err.Error())
         }
         return c.JSONPretty(http.StatusOK, jsonMsg("OK"), "    ")
@@ -268,7 +269,7 @@ func (r *RouterVMs)ShutdownVM(c echo.Context) error {
 // 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 {
+        if err := r.vms.RebootVM(vm.GetID()); err != nil {
             return echo.NewHTTPError(http.StatusNotFound, err.Error())
         }
         return c.JSONPretty(http.StatusOK, jsonMsg("OK"), "    ")
@@ -289,14 +290,14 @@ func (r *RouterVMs)EditVMByID(c echo.Context) error {
             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"`
-            Nic vms.NicType       `json:"nic"        validate:"required`
+            Nic vmc.NicType       `json:"nic"        validate:"required`
             UsbDevs []string      `json:"usbDevs"    validate:"required`
         }
         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, p.UsbDevs); err != nil {
+        if err := r.vms.EditVM(vm.GetID(), p.Name, p.Cpus, p.Ram, p.Nic, p.UsbDevs); err != nil {
             return echo.NewHTTPError(http.StatusBadRequest, err.Error())
         }
         return c.JSONPretty(http.StatusOK, vm.SerializeToNetwork(), "    ")
@@ -422,7 +423,7 @@ func (r *RouterVMs)ExportVMDir(c echo.Context) error {
 
         // Creates a unique tar filename.
         tmpDir := paths.GetInstance().TmpDir
-        tarGzFile := filepath.Join(tmpDir, "exportdir_"+vm.ID.String()+".tar.gz")
+        tarGzFile := filepath.Join(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 {
@@ -514,7 +515,7 @@ func (r *RouterVMs)performVMsList(c echo.Context, userCapabilityAny, vmAccessCap
                 if vm.IsOwner(user.Email) {
                     return cond(vm)
                 } else {
-                    capabilities, exists := vm.Access[user.Email]
+                    capabilities, exists := vm.GetAccess()[user.Email]
                     if exists {
                         _, visible := capabilities[vmAccessCapability]
                         return visible && cond(vm)
@@ -555,7 +556,7 @@ func (r *RouterVMs)performVMAction(c echo.Context, userCapabilityAny, vmAccessCa
         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]
+        userCaps, exists := vm.GetAccess()[user.Email]
         if !exists {
             return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps)
         }
diff --git a/src/server/users/user.go b/src/server/users/user.go
index 9f4b2ce..b06d6a7 100644
--- a/src/server/users/user.go
+++ b/src/server/users/user.go
@@ -1,7 +1,7 @@
 package users
 
 import (
-    "nexus-server/caps"
+    "nexus-common/caps"
     "github.com/go-playground/validator/v10"
 )
 
diff --git a/src/server/utils/utils.go b/src/server/utils/utils.go
index 99fa405..5e8d45b 100644
--- a/src/server/utils/utils.go
+++ b/src/server/utils/utils.go
@@ -94,15 +94,6 @@ func RandMacAddress() (string, error) {
     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()
-}
-
 // Returns true if the specified TCP port is available, false otherwise.
 func IsPortAvailable(port int) bool {
     ln, err := net.Listen("tcp", ":" + strconv.Itoa(port))
diff --git a/src/server/version/version.go b/src/server/version/version.go
index 42e2c10..cdc59ec 100644
--- a/src/server/version/version.go
+++ b/src/server/version/version.go
@@ -6,8 +6,8 @@ import (
 
 const (
     major  = 1
-    minor  = 6
-    bugfix = 2
+    minor  = 7
+    bugfix = 0
 )
 
 type Version struct {
diff --git a/src/server/vms/template.go b/src/server/vms/template.go
index 774f584..6be0637 100644
--- a/src/server/vms/template.go
+++ b/src/server/vms/template.go
@@ -6,19 +6,18 @@ import (
     "errors"
     "path/filepath"
     "encoding/json"
+    "nexus-common/template"
     "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"`
-}
+type (
+    Template struct {
+        t template.TemplateSerialized
+    }
+)
 
 const (
     templateConfFile = "template.json"
@@ -124,14 +123,18 @@ func ValidateTemplateAccess(access string) error {
     return nil
 }
 
-func (template *Template) IsPublic() bool {
-    return template.Access == templatePublic
+func (template *Template)IsPublic() bool {
+    return template.t.Access == templatePublic
 }
 
-func (template *Template) GetTemplateDiskPath() string {
+func (template *Template)GetTemplateDiskPath() string {
     return filepath.Join(template.getTemplateDir(), templateDiskFile)
 }
 
+func (template *Template)GetOwner() string {
+    return template.t.Owner
+}
+
 // Creates a template.
 func newTemplate(name, owner, access string) (*Template, error) {
     id, err := uuid.NewRandom()
@@ -140,7 +143,7 @@ func newTemplate(name, owner, access string) (*Template, error) {
         return nil, err
     }
 
-    template := &Template{id, name, owner, access, time.Now()}
+    template := &Template { template.TemplateSerialized{id, name, owner, access, time.Now()} }
 
     if err := template.validate(); err != nil {
         return nil, errors.New("Failed validating template: " + err.Error())
@@ -158,12 +161,12 @@ func newTemplateFromFile(file string) (*Template, error) {
     defer filein.Close()
 
     // Decodes the json file into users, then checks:
-    // - all fields are present
+    // - all fields are present:
     // - extra fields are not allowed
     decoder := json.NewDecoder(filein)
     decoder.DisallowUnknownFields()
     template := &Template{}
-    if err = decoder.Decode(&template); err != nil {
+    if err = decoder.Decode(&template.t); err != nil {
         return nil, errors.New("Failed decoding template config file: " + err.Error())
     }
 
@@ -175,7 +178,7 @@ func newTemplateFromFile(file string) (*Template, error) {
 }
 
 // Writes a template's config file.
-func (template *Template) writeConfig() error {
+func (template *Template)writeConfig() error {
     templateDir := template.getTemplateDir()
     templateConfigFile := filepath.Join(templateDir, templateConfFile)
 
@@ -188,7 +191,7 @@ func (template *Template) writeConfig() error {
     defer file.Close()
     encoder := json.NewEncoder(file)
     encoder.SetIndent("", "    ")
-    if err = encoder.Encode(template); err != nil {
+    if err = encoder.Encode(template.t); err != nil {
         template.delete()
         log.Error("Failed encoding template config file: " + err.Error())
         return errors.New("Failed encoding template config file: " + err.Error())
@@ -198,17 +201,21 @@ func (template *Template) writeConfig() error {
 }
 
 // Checks the template's fields are valid.
-func (template *Template) validate() error {
+func (template *Template)validate() error {
     validate := validator.New()
     if err := validate.Struct(template); err != nil {
         return err
     }
 
-    return ValidateTemplateAccess(template.Access)
+    return ValidateTemplateAccess(template.t.Access)
+}
+
+func (template *Template)SerializeToNetwork() template.TemplateSerialized {
+    return template.t
 }
 
 // Deletes a template's directory and its content.
-func (template *Template) delete() error {
+func (template *Template)delete() error {
     templateDir := template.getTemplateDir()
     if err := os.RemoveAll(templateDir); err != nil {
         log.Error("Failed deleting template files: " + err.Error())
@@ -218,7 +225,7 @@ func (template *Template) delete() error {
     return nil
 }
 
-func (template *Template) getTemplateDir() string {
+func (template *Template)getTemplateDir() string {
     templatesDir := GetTemplatesInstance().getDir()
-    return filepath.Join(templatesDir, template.ID.String())
+    return filepath.Join(templatesDir, template.t.ID.String())
 }
diff --git a/src/server/vms/templates.go b/src/server/vms/templates.go
index ba92387..6a267c8 100644
--- a/src/server/vms/templates.go
+++ b/src/server/vms/templates.go
@@ -7,6 +7,7 @@ import(
     "sync"
     "errors"
     "path/filepath"
+    t "nexus-common/template"
     "nexus-server/paths"
     "nexus-server/utils"
     "github.com/google/uuid"
@@ -70,12 +71,12 @@ func InitTemplates() error {
 }
 
 // Returns the list of templates for which TemplateKeeperFn returns true.
-func (templates *Templates)GetTemplates(keep TemplateKeeperFn) []Template {
+func (templates *Templates)GetNetworkSerializedTemplates(keep TemplateKeeperFn) []t.TemplateSerialized {
     templates.rwlock.RLock()
-    list := []Template{}
-    for _, t := range templates.m {
-        if keep(t) {
-            list = append(list, t)
+    list := []t.TemplateSerialized{}
+    for _, tpl := range templates.m {
+        if keep(tpl) {
+            list = append(list, tpl.SerializeToNetwork())
         }
     }
     templates.rwlock.RUnlock()
@@ -135,7 +136,7 @@ func (templates *Templates)AddTemplate(template *Template) error {
     }
 
     // Adds template to the map of templates.
-    templates.m[template.ID.String()] = *template
+    templates.m[template.t.ID.String()] = *template
 
     return nil
 }
@@ -153,10 +154,10 @@ func (templates *Templates)EditTemplate(tplID uuid.UUID, name, access string) er
 
     // Only updates fields that have changed.
     if name != "" {
-        tpl.Name = name
+        tpl.t.Name = name
     }
     if access != "" {
-        tpl.Access = access
+        tpl.t.Access = access
     }
 
     if err = tpl.validate(); err != nil {
@@ -168,7 +169,7 @@ func (templates *Templates)EditTemplate(tplID uuid.UUID, name, access string) er
         return err
     }
 
-    key := tpl.ID.String()
+    key := tpl.t.ID.String()
     delete(templates.m, key)
     templates.m[key] = tpl
 
diff --git a/src/server/vms/vm.go b/src/server/vms/vm.go
index e5f0c87..203ab32 100644
--- a/src/server/vms/vm.go
+++ b/src/server/vms/vm.go
@@ -12,8 +12,9 @@ import (
     "path/filepath"
     "encoding/json"
     "encoding/base64"
+    vmc "nexus-common/vm"
+    "nexus-common/caps"
     "nexus-server/qga"
-    "nexus-server/caps"
     "nexus-server/exec"
     "nexus-server/paths"
     "github.com/google/uuid"
@@ -24,64 +25,23 @@ import (
 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"`
-        UsbDevs []string      `json:"usbDevs"    validate:"required"`
-        TemplateID uuid.UUID  `json:"templateID" validate:"required"`
-        Access map[string]caps.Capabilities `json:"access"`
+        v vmc.VMDiskSerialized
 
         // None of the fields below are serialized to disk.
-        dir string             // VM directory
-        qgaSock string         // QEMU Guest Agent (QGA) UNIX socket
+        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)
+        DiskBusy bool      // If true the VM's disk is busy (cannot be modified or deleted)
         mutex *sync.Mutex
     }
 
-    // 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"`
-        Nic NicType            `json:"nic"`
-        UsbDevs []string       `json:"usbDevs"`
-        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           `json:"id"`
-        Owner string           `json:"owner"`
-        Name string            `json:"name"`
-        Cpus int               `json:"cpus"`
-        Ram int                `json:"ram"`
-        Nic NicType            `json:"nic"`
-        UsbDevs []string       `json:"usbDevs"`
-        TemplateID uuid.UUID   `json:"templateID"`
-        Access map[string]caps.Capabilities `json:"access"`
-        State VMState          `json:"state"`
-        Port int               `json:"port"`
-        Pwd string             `json:"pwd"`
-        DiskBusy bool          `json:"diskBusy"`
-    }
-
     runStates struct {
-        State VMState
+        State vmc.VMState
         Pid int
         Port int
         Pwd string
     }
 
-    VMState string  // see stateXXX defined below
-    NicType string  // see nicXXX defined below
-
     endOfExecCallback func(vm *VM)
 )
 
@@ -90,12 +50,6 @@ const (
     vmConfFile     = "vm.json"
     vmSecretFile   = "secret"
     vmQGASockFile  = "qga.sock"
-
-    nicUser NicType = "user"
-    nicNone NicType = "none"
-
-    stateStopped VMState  = "STOPPED"
-    stateRunning          = "RUNNING"
 )
 
 var dummyVM = VM{}
@@ -108,7 +62,7 @@ var passwordGen, _ = password.NewGenerator(&password.GeneratorInput{
 })
 
 // Creates a VM.
-func NewVM(creatorEmail string, name string, cpus, ram int, nic NicType, usbDevs []string, templateID uuid.UUID, owner string) (*VM, error) {
+func NewVM(creatorEmail string, name string, cpus, ram int, nic vmc.NicType, usbDevs []string, templateID uuid.UUID, owner string) (*VM, error) {
     vmID, err := uuid.NewRandom()
 
     if err != nil {
@@ -116,18 +70,18 @@ func NewVM(creatorEmail string, name string, cpus, ram int, nic NicType, usbDevs
         return nil, err
     }
     vm := newEmptyVM()
-    vm.ID = vmID
-    vm.Owner = owner
-    vm.Name = name
-    vm.Cpus = cpus
-    vm.Ram = ram
-    vm.Nic = nic
-    vm.UsbDevs = usbDevs
-    vm.TemplateID = templateID
+    vm.v.ID = vmID
+    vm.v.Owner = owner
+    vm.v.Name = name
+    vm.v.Cpus = cpus
+    vm.v.Ram = ram
+    vm.v.Nic = nic
+    vm.v.UsbDevs = usbDevs
+    vm.v.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)
+    vm.v.Access = make(map[string]caps.Capabilities)
 
     if err = vm.validate(); err != nil {
         return nil, errors.New("Failed validating VM: "+err.Error())
@@ -136,31 +90,31 @@ func NewVM(creatorEmail string, name string, cpus, ram int, nic NicType, usbDevs
     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,
-        UsbDevs: vm.UsbDevs,
-        TemplateID: vm.TemplateID,
-        Access: vm.Access,
+func (vm *VM)SerializeToDisk() vmc.VMDiskSerialized {
+    return vmc.VMDiskSerialized {
+        ID: vm.v.ID,
+        Owner: vm.v.Owner,
+        Name: vm.v.Name,
+        Cpus: vm.v.Cpus,
+        Ram: vm.v.Ram,
+        Nic: vm.v.Nic,
+        UsbDevs: vm.v.UsbDevs,
+        TemplateID: vm.v.TemplateID,
+        Access: vm.v.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,
-        UsbDevs: vm.UsbDevs,
-        TemplateID: vm.TemplateID,
-        Access: vm.Access,
+func (vm *VM)SerializeToNetwork() vmc.VMNetworkSerialized {
+    return vmc.VMNetworkSerialized {
+        ID: vm.v.ID,
+        Owner: vm.v.Owner,
+        Name: vm.v.Name,
+        Cpus: vm.v.Cpus,
+        Ram: vm.v.Ram,
+        Nic: vm.v.Nic,
+        UsbDevs: vm.v.UsbDevs,
+        TemplateID: vm.v.TemplateID,
+        Access: vm.v.Access,
         State: vm.Run.State,
         Port: vm.Run.Port,
         Pwd: vm.Run.Pwd,
@@ -168,32 +122,41 @@ func (vm *VM)SerializeToNetwork() VMNetworkSerialized {
     }
 }
 
-func (vm *VM)getDiskPath() string {
-    vmDiskFile, _ := filepath.Abs(filepath.Join(vm.dir, vmDiskFile))
-    return vmDiskFile
+func (vm *VM)GetID() uuid.UUID {
+    return vm.v.ID
+}
+
+func (vm *VM)GetAccess() map[string]caps.Capabilities {
+    return vm.v.Access
 }
 
 // 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.v.Owner
+}
+
+func (vm *VM)getDiskPath() string {
+    vmDiskFile, _ := filepath.Abs(filepath.Join(vm.dir, vmDiskFile))
+    return vmDiskFile
 }
 
 // Creates an empty VM.
 func newEmptyVM() *VM {
     return &VM {
-        ID: uuid.Nil,
-        Owner: "",
-        Name: "",
-        Cpus: 0,
-        Ram: 0,
-        Nic: "",
-        UsbDevs: []string{},
-        TemplateID: uuid.Nil,
-        Access: make(map[string]caps.Capabilities),
-
+        v: vmc.VMDiskSerialized {
+            ID: uuid.Nil,
+            Owner: "",
+            Name: "",
+            Cpus: 0,
+            Ram: 0,
+            Nic: "",
+            UsbDevs: []string{},
+            TemplateID: uuid.Nil,
+            Access: make(map[string]caps.Capabilities),
+        },
         dir: "",
         qgaSock: "",
-        Run: runStates { State: stateStopped, Pid: 0, Port: 0, Pwd: "" },
+        Run: runStates { State: vmc.StateStopped, Pid: 0, Port: 0, Pwd: "" },
         DiskBusy: false,
         mutex: new(sync.Mutex),
     }
@@ -211,7 +174,7 @@ func newVMFromFile(vmFile string) (*VM, error) {
     decoder := json.NewDecoder(filein)
     decoder.DisallowUnknownFields()
     vm := newEmptyVM()
-    if err = decoder.Decode(&vm); err != nil {
+    if err = decoder.Decode(&vm.v); err != nil {
         return nil, errors.New("Failed decoding VM config file: "+err.Error())
     }
 
@@ -225,13 +188,13 @@ func newVMFromFile(vmFile string) (*VM, error) {
 }
 
 func (vm *VM)IsRunning() bool {
-    return vm.Run.State == stateRunning
+    return vm.Run.State == vmc.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 {
+    for email, accessCaps := range vm.v.Access {
         _, err := mail.ParseAddress(email)
         if err != nil {
             return errors.New("Invalid email")
@@ -241,11 +204,11 @@ func (vm *VM)validate() error {
         }
     }
 
-    if (vm.Nic != nicNone && vm.Nic != nicUser) {
-        return errors.New("Invalid nic value: "+string(vm.Nic))
+    if (vm.v.Nic != vmc.NicNone && vm.v.Nic != vmc.NicUser) {
+        return errors.New("Invalid nic value: "+string(vm.v.Nic))
     }
 
-    if err := validateUsbDevs(vm.UsbDevs); err != nil {
+    if err := validateUsbDevs(vm.v.UsbDevs); err != nil {
         return err
     }
 
@@ -254,7 +217,7 @@ func (vm *VM)validate() error {
     }
 
     // Checks that the template referenced by the VM actually exists.
-    _, err := GetTemplatesInstance().GetTemplate(vm.TemplateID)
+    _, err := GetTemplatesInstance().GetTemplate(vm.v.TemplateID)
     if err != nil {
         return err
     }
@@ -268,7 +231,7 @@ func (vm *VM)validate() error {
 // 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)
+    template, err := GetTemplatesInstance().GetTemplate(vm.v.TemplateID)
     if err != nil {
         return err
     }
@@ -286,7 +249,7 @@ func (vm *VM)writeFiles() error {
 
     // 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))
+    templateDiskFile, _ := filepath.Abs(filepath.Join(GetTemplatesInstance().getDir(), template.t.ID.String(), templateDiskFile))
 
     if err := exec.QemuImgCreate(templateDiskFile, vm.getDiskPath()); err != nil {
         vm.delete()
@@ -481,24 +444,24 @@ func (vm *VM)reboot() error {
 // 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), vm.UsbDevs, filepath.Join(vm.dir, vmDiskFile), port, pwdFile, pkiDir)
+    cmd, err := exec.NewQemuSystem(vm.qgaSock, vm.v.Cpus, vm.v.Ram, string(vm.v.Nic), vm.v.UsbDevs, 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 executing VM "+vm.v.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 }
+    vm.Run = runStates { State: vmc.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 executing VM "+vm.v.ID.String()+": exec.Wait error: "+err.Error())
             log.Error("Failed cmd: "+cmd.String())
         }
         endofExecFn(vm)
@@ -509,7 +472,7 @@ func (vm *VM)runQEMU(port int, pwd, pwdFile string, endofExecFn endOfExecCallbac
 
 // Resets a VM's states.
 func (vm *VM)resetStates() {
-    vm.Run = runStates { State: stateStopped, Pid: 0, Port: 0, Pwd: "" }
+    vm.Run = runStates { State: vmc.StateStopped, Pid: 0, Port: 0, Pwd: "" }
 }
 
 // Validates and convert a string of USB devices of the form "1fc9:001d,067b:2303"
diff --git a/src/server/vms/vms.go b/src/server/vms/vms.go
index 56ed2ad..43dcaa9 100644
--- a/src/server/vms/vms.go
+++ b/src/server/vms/vms.go
@@ -7,8 +7,9 @@ import (
     "math"
     "errors"
     "path/filepath"
+    "nexus-common/vm"
+    "nexus-common/caps"
     "nexus-server/exec"
-    "nexus-server/caps"
     "nexus-server/paths"
     "nexus-server/utils"
     "nexus-server/logger"
@@ -78,7 +79,7 @@ func InitVMs() error {
                 }
                 f.Close()
 
-                vms.m[vm.ID.String()] = vm
+                vms.m[vm.v.ID.String()] = vm
             }
         }
     }
@@ -87,9 +88,9 @@ func InitVMs() error {
 }
 
 // Returns the list of serialized VMs for which VMKeeperFn returns true.
-func (vms *VMs)GetNetworkSerializedVMs(keepFn VMKeeperFn) []VMNetworkSerialized {
+func (vms *VMs)GetNetworkSerializedVMs(keepFn VMKeeperFn) []vm.VMNetworkSerialized {
     vms.rwlock.RLock()
-    list := []VMNetworkSerialized{}
+    list := []vm.VMNetworkSerialized{}
     for _, vm := range vms.m {
         vm.mutex.Lock()
         if keepFn(vm) {
@@ -164,7 +165,7 @@ func (vms *VMs)AddVM(vm *VM) error {
 
     // Adds VM to the map of VMs.
     vms.rwlock.Lock()
-    key := vm.ID.String()
+    key := vm.v.ID.String()
     vms.m[key] = vm
     vms.rwlock.Unlock()
 
@@ -195,7 +196,7 @@ func (vms *VMs)StartVM(vmID uuid.UUID) (int, string, 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)))
+    estimatedVmRAM := int(math.Round(float64(vm.v.Ram)*(1.-consts.KsmRamSaving)))
 
     vms.usedRAM += estimatedVmRAM
 
@@ -304,7 +305,7 @@ func (vms *VMs)IsTemplateUsed(templateID string) bool {
 
     for _, vm := range vms.m {
         vm.mutex.Lock()
-        if vm.TemplateID.String() == templateID {
+        if vm.v.TemplateID.String() == templateID {
             vm.mutex.Unlock()
             return true
         }
@@ -314,7 +315,7 @@ func (vms *VMs)IsTemplateUsed(templateID string) bool {
 }
 
 // Edit a VM' specs: name, cpus, ram, nic
-func (vms *VMs)EditVM(vmID uuid.UUID, name string, cpus, ram int, nic NicType, usbDevs []string) error {
+func (vms *VMs)EditVM(vmID uuid.UUID, name string, cpus, ram int, nic vm.NicType, usbDevs []string) error {
     vms.rwlock.Lock()
     defer vms.rwlock.Unlock()
 
@@ -332,19 +333,19 @@ func (vms *VMs)EditVM(vmID uuid.UUID, name string, cpus, ram int, nic NicType, u
 
     // Only updates fields that have changed.
     if name != "" {
-        vm.Name = name
+        vm.v.Name = name
     }
     if cpus > 0 {
-        vm.Cpus = cpus
+        vm.v.Cpus = cpus
     }
     if ram > 0 {
-        vm.Ram = ram
+        vm.v.Ram = ram
     }
     if nic != "" {
-        vm.Nic = nic
+        vm.v.Nic = nic
     }
     if usbDevs != nil {
-        vm.UsbDevs = usbDevs
+        vm.v.UsbDevs = usbDevs
     }
 
     if err = vm.validate(); err != nil {
@@ -385,14 +386,14 @@ func (vms *VMs)SetVMAccess(vmID uuid.UUID, loggedUserEmail, userEmail string, ne
     // 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]
+        userCaps := vm.v.Access[loggedUserEmail]
         _, exists := userCaps[caps.CAP_VM_SET_ACCESS]
         if !exists {
             return errors.New("Insufficient capability")
         }
     }
 
-    vm.Access[userEmail] = newAccess
+    vm.v.Access[userEmail] = newAccess
 
     if err = vm.writeConfig(); err != nil {
         return err
@@ -424,7 +425,7 @@ func (vms *VMs)DeleteVMAccess(vmID uuid.UUID, loggedUserEmail, userEmail string)
     // 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]
+        userCaps := vm.v.Access[loggedUserEmail]
         _, exists := userCaps[caps.CAP_VM_SET_ACCESS]
         if !exists {
             return errors.New("Insufficient capability")
@@ -432,8 +433,8 @@ func (vms *VMs)DeleteVMAccess(vmID uuid.UUID, loggedUserEmail, userEmail string)
     }
 
     // Only removes the user from the Access map if it actually had an access.
-    if _, exists := vm.Access[userEmail]; exists {
-        delete(vm.Access, userEmail)
+    if _, exists := vm.v.Access[userEmail]; exists {
+        delete(vm.v.Access, userEmail)
     } else {
         return errors.New("User "+userEmail+" has no VM access")
     }
diff --git a/tools/genpwd/src/genpwd.go b/tools/genpwd/genpwd.go
similarity index 100%
rename from tools/genpwd/src/genpwd.go
rename to tools/genpwd/genpwd.go
diff --git a/tools/genpwd/src/go.mod b/tools/genpwd/go.mod
similarity index 100%
rename from tools/genpwd/src/go.mod
rename to tools/genpwd/go.mod
diff --git a/tools/genpwd/src/go.sum b/tools/genpwd/go.sum
similarity index 100%
rename from tools/genpwd/src/go.sum
rename to tools/genpwd/go.sum
-- 
GitLab