diff --git a/HOWTO_build.md b/HOWTO_build.md deleted file mode 100644 index 57b0b7fd7480514c4f7b02ac6dc7da0de0a34e93..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..946a0d2e3ef27f13b7df0c3c867ffbfea8c625a0 --- /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 8ffb91b60758c5889b013635e619f62a8a586723..fb07345f2bb092daf908c6274ed85d5d0159f5fc 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 0000000000000000000000000000000000000000..1e06c5e8c1170f748561bb1fe7764016f5ba90a5 --- /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 0000000000000000000000000000000000000000..090d001363b36397d6752ebbb6f8e441ab463cfe --- /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: + + + + +## 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: +  + +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: +  + +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 a8da6b31f68c689fad9d2353d2c5ad63d3c0e44a..785258f0c3582329ed96390ab63b582860be5da6 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 3dc7365cbf319a22207dc6743e32eee0731cc3cd..5adf5cd7be34a789faef6826b5e4346f3ea82d56 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 44569d2222067945b433afa34731081d033bd009..5266b0dd2d204f79906b614313a1ae66f3cbbacc 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 f085cb530d16600f9292f576abde12fb7317ec7f..053916ad7d44a84cf68b85aa18214bbfa489983c 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 b54daf2931586fa9abd5745768a0c3cd068adfe9..0000000000000000000000000000000000000000 --- 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 d56af7a4d88ff0eacd56168b5544ffab4000e3dc..0fc0fefd0afdae9a873e757eca8e5590b18df211 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 dc72b9c9360258156a7ac974fd0ec35c44e70fa6..eefb0c4bfc7f595e2092e3ab75432f79c66520a1 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 4d6248b7fb64370882d2d2bb62c30305d92c41bc..759a3b7d98a2c45bdaab9a01ab46ca974dbf63b1 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 b2e07b2d801f1d817093fb56e38410587a198f0d..5ac187f11a20ad3a45a1093ec2663c704d638d1b 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 d5a92aa213fc11132c03a47fc951e1959837dc3f..89ae454c5bdd5dcd4084e77962c26af82c5757f1 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 a1a1b1301343aee67c665106a91f9681f855d1e1..d70477531767aa72d2b40b0790f0c63aa0720cac 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 9cc7956513220f357722115d8163d2dbbddba17c..03a957ec93512f72d7652cd8f13188d04c2a6762 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 a6c4df667c66a60184319a4ded995ec0a0459206..350be8af3c0be8c06029ee3efa23fbd0ec3a5d0b 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 dcc7797ee07203eea5aef2ddc9d9059d001f0b21..3d5245b58c7112380582f3c033c05100faf3df09 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 b460f99dc49e146e2a5d4b37ac76b920cbd675fd..2e81f76b12f43593a59853307caf87ab7229890b 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 6bb659551781df8707da9f33b0d115c85fa2a215..e156e7c4319de70e7d323cfb13106c1db76f6aef 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 bd335b33a553e3187ef1005204adc59129d892d0..093ff5b23fba1bb4f28a48237ce66b5408e300ce 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 bdebfdef1e49ce187711dc5b1a5c7f542e50326f..0c9b978676783b48b328370bc86d79984395ed9e 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 0000000000000000000000000000000000000000..4055a1ff7b64dd0d8431a0d875d047124addeee5 --- /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 0000000000000000000000000000000000000000..2c24cf7eb257a2b904209afe45a7bc933e694d96 --- /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 0000000000000000000000000000000000000000..3dfe1c9f26d3953e435514c804a71a01721639fe --- /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 ae5611811a981cfe509251ffe7b9e4588ae0baac..230f1ce8bd324ea9c13eda90da4c563f4b3aa23c 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 0000000000000000000000000000000000000000..25b5552dcc76ea3790dc6a107295721e6c1fbf1a --- /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 0000000000000000000000000000000000000000..b607ad28db86b3794ef2df4ef702fc993d83d885 --- /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 0000000000000000000000000000000000000000..a3e6b52c8ac9a1c7c59d650f587c0a6aed5728e3 --- /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 0000000000000000000000000000000000000000..3dfe1c9f26d3953e435514c804a71a01721639fe --- /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 0000000000000000000000000000000000000000..e748d643864aa9085fd51e24da3542646e298787 --- /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 95c9608639c59ce09cc3c77d7c9a2529fcf3bea6..0000000000000000000000000000000000000000 --- 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 8d3bde2aa692f6c0ce27565dc2cbbb2428e9e32c..2f7d0dd7035f6ca90250f7bb28bd9022c8ab8c20 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 eb8cd55973f0875e43da19a1d226165d22abdbf1..b7baa9dbcc89c88ab2a048330c733fc9246d0672 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 b13d60641407322d302fb68c05a3cb7adac6e4c2..78042919222bd19ee0a776b4b949e31571385c2c 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 cd713b57b9917812f010ab58f35f1ba2110689f8..e68af79c85f969b49f25d0350c2eb2799cf4b5ad 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 cd9e4b775c456d7b66d516225f867da46fa0ea78..4bd70fba2f343bd0452e54a61ea387e5c639be63 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 9f4b2ce85961e00eb4ab9c4026ba50fb85454425..b06d6a7f7a24fb62f7ef5d216852d1e15155eae0 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 99fa405b6ffe33fa77f0ee53f5c443e7df26cf24..5e8d45bcb6f11bc046706a20f830f66b8ef376ac 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 42e2c10a1b0542c5fa70e144e938c3c7342da09d..cdc59ec1d8f3a6e64caad26ef848964a345395d5 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 774f5847304b47b479ad6ac659bd7e058b728b08..6be0637135a4bdc7b8d927b074aefd9bce232448 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 ba92387f6760dd47024e6004d11aa7500b69c552..6a267c8ed62eafdf813ca9a672ca3a6dc4f6292b 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 e5f0c87d2f5cce915cec921441b7ff179a0eef21..203ab32e6dca0cf53cb1ff1639e29f0952c6e42a 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 56ed2ad7f963a0ab3048158a9f9f3a87dcd8ed1a..43dcaa9cf488e86aa7eff198453b44afd2957824 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