From 837c5468ec1bdae3833232ec2ef451243ec616ef Mon Sep 17 00:00:00 2001 From: catborise Date: Sat, 17 Oct 2026 22:38:33 +0300 Subject: [PATCH] build: systemd runs the panel on hosts instead of supervisor The installer and the manual installation install four systemd units (webvirt-cloud, webvirt-novncd, webvirt-socketiod, webvirt-worker) that run as the web server's user with absolute paths and read /etc/default/webvirtcloud. gunicorn, socketiod and the worker stop with KillMode=mixed, so they wind down their own children. supervisor, its installer code and the contributed systemd files go; the README gets the update, rollback and a one-time move from supervisor, and the installer refuses to run over a supervisor install. Docker keeps runit; novncd's runit script is the one-line form of the others. --- README.md | 117 +++++++---------- conf/runit/novncd.sh | 28 +--- conf/supervisor/webvirtcloud.conf | 37 ------ conf/systemd/README.md | 16 --- conf/systemd/install.sh | 7 - conf/systemd/webvirt | 2 - conf/systemd/webvirt-cloud.service | 22 ++-- conf/systemd/webvirt-novncd.service | 19 +-- conf/systemd/webvirt-socketiod.service | 21 ++- conf/systemd/webvirt-worker.service | 15 ++- doc/architecture.md | 14 +- webvirtcloud.sh | 153 +++++----------------- webvirtcloud/tests/test_worker_install.py | 21 +-- 13 files changed, 129 insertions(+), 343 deletions(-) delete mode 100644 conf/supervisor/webvirtcloud.conf delete mode 100644 conf/systemd/README.md delete mode 100755 conf/systemd/install.sh delete mode 100644 conf/systemd/webvirt diff --git a/README.md b/README.md index 03f762a0..63bf2e4a 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ chmod 744 install.sh sudo ./install.sh ``` -The installer writes the server's names and IPs to `ALLOWED_HOSTS`. Running it again keeps `settings.py`, the database, the secret key and the main nginx config, but replaces `/etc/nginx/conf.d/webvirtcloud.conf` and the supervisor config; back up your own lines in those two (TLS, `environment=`) first. +The installer writes the server's names and IPs to `ALLOWED_HOSTS`. Running it again keeps `settings.py`, the database, the secret key and the main nginx config, but replaces `/etc/nginx/conf.d/webvirtcloud.conf` and the systemd units; back up your own lines in the nginx config (TLS) first. Put your own service settings in `/etc/default/webvirtcloud` or in drop-ins (`systemctl edit webvirt-cloud`), which a re-run keeps. ### Docker @@ -101,7 +101,7 @@ The virtualenv is created with `--system-site-packages`: `libvirt-python` and `p ```bash # 1. System packages -apt-get update && apt-get -y install git python3-venv python3-dev python3-lxml python3-libvirt libvirt-dev zlib1g-dev libxslt1-dev nginx supervisor libsasl2-modules gcc pkg-config python3-guestfs libsasl2-dev libldap2-dev libssl-dev +apt-get update && apt-get -y install git python3-venv python3-dev python3-lxml python3-libvirt libvirt-dev zlib1g-dev libxslt1-dev nginx libsasl2-modules gcc pkg-config python3-guestfs libsasl2-dev libldap2-dev libssl-dev # 2. Code git clone https://github.com/retspen/webvirtcloud /srv/webvirtcloud @@ -121,15 +121,16 @@ pip install -r conf/requirements.txt python3 manage.py migrate python3 manage.py collectstatic --noinput -# 6. Supervisor (gunicorn, novncd and the job worker) and nginx -cp conf/supervisor/webvirtcloud.conf /etc/supervisor/conf.d/ +# 6. systemd units (gunicorn, novncd, socketiod and the job worker) and nginx +cp conf/systemd/webvirt-{cloud,novncd,socketiod,worker}.service /etc/systemd/system/ cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/ rm -f /etc/nginx/sites-enabled/default # 7. Ownership, private files, start chown -R www-data:www-data /srv/webvirtcloud chmod 700 data && chmod 600 webvirtcloud/settings.py db.sqlite3 -systemctl restart nginx supervisor +systemctl daemon-reload && systemctl enable --now webvirt-cloud webvirt-novncd webvirt-socketiod webvirt-worker +nginx -t && systemctl restart nginx ``` #### RHEL / Rocky Linux / AlmaLinux / Oracle Linux 10 @@ -137,7 +138,7 @@ systemctl restart nginx supervisor ```bash # 1. System packages dnf -y install epel-release -dnf -y install git python3-devel libvirt-devel python3-libvirt python3-ldap python3-lxml cyrus-sasl-devel cyrus-sasl-md5 openldap-devel openssl-devel glibc gcc nginx supervisor python3-libguestfs iproute-tc +dnf -y install git python3-devel libvirt-devel python3-libvirt python3-ldap python3-lxml cyrus-sasl-devel cyrus-sasl-md5 openldap-devel openssl-devel glibc gcc nginx python3-libguestfs iproute-tc # 2. Code git clone https://github.com/retspen/webvirtcloud /srv/webvirtcloud @@ -157,8 +158,8 @@ pip install -r conf/requirements.txt python3 manage.py migrate python3 manage.py collectstatic --noinput -# 6. Supervisor (gunicorn, novncd and the job worker, as nginx) and nginx -sed 's/^user=.*/user=nginx/' conf/supervisor/webvirtcloud.conf > /etc/supervisord.d/webvirtcloud.ini +# 6. systemd units (gunicorn, novncd, socketiod and the job worker, as nginx) and nginx +for u in cloud novncd socketiod worker; do sed 's/^\(User\|Group\)=.*/\1=nginx/' conf/systemd/webvirt-$u.service > /etc/systemd/system/webvirt-$u.service; done cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/ # make sure the default server block in /etc/nginx/nginx.conf does not also listen on port 80 @@ -171,8 +172,8 @@ setsebool -P httpd_can_network_connect on 2>/dev/null || true firewall-cmd --add-service=http --permanent 2>/dev/null || true firewall-cmd --add-port=6080/tcp --permanent 2>/dev/null || true firewall-cmd --reload 2>/dev/null || true -systemctl enable nginx supervisord -nginx -t && systemctl restart nginx supervisord +systemctl daemon-reload && systemctl enable --now webvirt-cloud webvirt-novncd webvirt-socketiod webvirt-worker +systemctl enable nginx && nginx -t && systemctl restart nginx ``` #### openSUSE Leap 15.x / Tumbleweed, SLES 15 @@ -180,7 +181,6 @@ nginx -t && systemctl restart nginx supervisord ```bash # 1. System packages (Python 3.11 stack) zypper --non-interactive install -y git hostname python311 python311-base python311-devel python311-pip python311-libvirt-python python311-lxml python311-ldap libvirt-devel openldap2-devel cyrus-sasl-devel libopenssl-devel gcc pkg-config nginx -zypper --non-interactive install -y python3-supervisor || zypper --non-interactive install -y supervisor # 2. Code git clone https://github.com/retspen/webvirtcloud /srv/webvirtcloud @@ -200,17 +200,15 @@ pip install -r conf/requirements.txt python3 manage.py migrate python3 manage.py collectstatic --noinput -# 6. Supervisor (gunicorn, novncd and the job worker, as nginx) and nginx -mkdir -p /etc/supervisord.d -sed 's/^user=.*/user=nginx/' conf/supervisor/webvirtcloud.conf > /etc/supervisord.d/webvirtcloud.ini -# make sure /etc/supervisord.conf has: [include] files = /etc/supervisord.d/*.ini +# 6. systemd units (gunicorn, novncd, socketiod and the job worker, as nginx) and nginx +for u in cloud novncd socketiod worker; do sed 's/^\(User\|Group\)=.*/\1=nginx/' conf/systemd/webvirt-$u.service > /etc/systemd/system/webvirt-$u.service; done cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/ # 7. Ownership, private files, start chown -R nginx:nginx /srv/webvirtcloud chmod 700 data && chmod 600 webvirtcloud/settings.py db.sqlite3 -systemctl enable nginx supervisord -nginx -t && systemctl restart nginx supervisord +systemctl daemon-reload && systemctl enable --now webvirt-cloud webvirt-novncd webvirt-socketiod webvirt-worker +systemctl enable nginx && nginx -t && systemctl restart nginx ``` ### First login @@ -296,15 +294,15 @@ If libvirt warns `Host SMBIOS information is not available`, install `dmidecode` Where to set them: - **Docker:** the `environment:` list of `docker-compose.override.yml`; every process in the container gets it. -- **Installer and manual installation:** edit `webvirtcloud/settings.py`, or put the same `environment=` line in `[program:webvirtcloud]`, `[program:novncd]` and `[program:webvirt-worker]` of the supervisor config (novncd and the worker read the database too; supervisor takes one `environment=` per program, so merge the worker's into its `WEBVIRTCLOUD_LOG_FILE` line) and export the variables before running `manage.py`. -- **gunicorn** runs at most 8 workers (fewer on hosts with under 4 CPUs); to change it, set `GUNICORN_CMD_ARGS="--workers N"` in the `[program:webvirtcloud]` environment or in `docker-compose.override.yml`. +- **Installer and manual installation:** edit `webvirtcloud/settings.py`, or put them in `/etc/default/webvirtcloud` (root, mode 600), one `NAME='value'` per line (single quotes: systemd and the shell then read the same value); all four services read it. Then `systemctl restart webvirt-cloud webvirt-novncd webvirt-socketiod webvirt-worker`, and load the file before running `manage.py`: `set -a; . /etc/default/webvirtcloud; set +a`. +- **gunicorn** runs at most 8 workers (fewer on hosts with under 4 CPUs); to change it, set `GUNICORN_CMD_ARGS=--workers N` in a drop-in of `webvirt-cloud` (`systemctl edit webvirt-cloud`: `[Service]` and `Environment="GUNICORN_CMD_ARGS=--workers N"`) or in `docker-compose.override.yml`. ### HTTPS and reverse proxy For a panel served over HTTPS: - set `WEBVIRTCLOUD_HTTPS=1`: session and CSRF cookies become HTTPS-only, HTTP is redirected to HTTPS (except `/datasource/`, which VMs fetch over HTTP), and HSTS is sent for one year; -- add the HTTPS origin to `CSRF_TRUSTED_ORIGINS`, e.g. `environment=CSRF_TRUSTED_ORIGINS="https://wvc.example.com",WEBVIRTCLOUD_HTTPS="1"` in supervisor; +- add the HTTPS origin to `CSRF_TRUSTED_ORIGINS`, e.g. `CSRF_TRUSTED_ORIGINS=https://wvc.example.com` and `WEBVIRTCLOUD_HTTPS=1` in `/etc/default/webvirtcloud`; - set the public noVNC port to the proxy's port (the bundled nginx config already proxies `/novncd/`): `WS_PUBLIC_PORT = 443` in `webvirtcloud/settings.py`, or `WS_PUBLIC_PORT=443` in `docker-compose.override.yml` for Docker; TLS can end at the bundled nginx or at a proxy in front of it; such a proxy must send `X-Forwarded-Proto: https`. Failed logins are counted per client address, which the bundled nginx passes in `X-Real-IP`. A proxy that replaces the bundled nginx must set `X-Real-IP` itself, and its address must be in `LOGIN_TRUSTED_PROXIES` in `settings.py`. With a proxy in front of the bundled nginx, configure nginx's `set_real_ip_from` / `real_ip_header` for that proxy, or all clients share its address. @@ -393,7 +391,7 @@ Set `OTP_ENABLED = True` in `settings.py` to ask for a one-time code at login. I ### Serial console -A VM without graphics (no `` device, a ``) opens a serial console (xterm.js) instead of VNC. It is off by default. To turn it on, set `WEBVIRTCLOUD_SERIAL_CONSOLE=1` (Docker: in `docker-compose.override.yml`, then `docker compose -p webvirtcloud up -d`) or `SERIAL_CONSOLE_ENABLED = True` in `settings.py` and restart socketiod. +A VM without graphics (no `` device, a ``) opens a serial console (xterm.js) instead of VNC. It is off by default. To turn it on, set `WEBVIRTCLOUD_SERIAL_CONSOLE=1` (Docker: in `docker-compose.override.yml`, then `docker compose -p webvirtcloud up -d`) or `SERIAL_CONSOLE_ENABLED = True` in `settings.py` and `systemctl restart webvirt-cloud webvirt-socketiod`. - The VM page's Settings → Console switches a shut-off VM between VNC and serial (Console type). A new VNC console takes the form's other fields; on a TCP or TLS compute it needs Listen on: All interfaces (only SSH computes reach a localhost console). - The guest must use its serial port: a login prompt needs `systemctl enable --now serial-getty@ttyS0`, boot messages need the kernel parameter `console=ttyS0,115200`. The console shows only what the guest writes after it opens; press Enter for a new prompt. @@ -401,17 +399,6 @@ A VM without graphics (no `` device, a ``) opens a - It accepts the console page's signed token, as noVNC does: whoever gets that token within 120 s gets the console, and an open console stays open after its user loses access to the VM. - One console per VM, at most 20 at a time; a second one shows "refused". -### Running novncd with runit (Debian) - -Instead of Supervisor, `novncd` can run under `runit`. First set `autostart=false` for `[program:novncd]` in the supervisor config and run `sudo supervisorctl update`, so the two do not compete for the port: - -```bash -sudo apt install -y runit runit-systemd -sudo mkdir -p /etc/service/novncd/ -sudo ln -s /srv/webvirtcloud/conf/runit/novncd.sh /etc/service/novncd/run -sudo systemctl start runit.service -``` - ## Updating Every update starts with a backup in its own directory (mode 700, named by date) that holds the database, the commit it was taken from and, for installer and manual installations, `settings.py` and the nginx config. The database is copied with SQLite's backup API, which also finishes a write interrupted by a crash. This covers sqlite only; with PostgreSQL (`WEBVIRTCLOUD_DB_HOST`), back up that database yourself before updating. @@ -434,6 +421,19 @@ docker compose run --rm --no-deps -T --entrypoint /srv/webvirtcloud/venv/bin/pyt docker compose up -d ``` +### Moving from supervisor to systemd + +Versions up to v3.0.0 ran the panel under supervisor. Once, before the update steps below, stop it there, keep its config and remove it: + +```bash +supervisorctl stop webvirtcloud novncd socketiod webvirt-worker +mv /etc/supervisor/conf.d/webvirtcloud.conf /root/webvirtcloud-supervisor.conf # Debian / Ubuntu +mv /etc/supervisord.d/webvirtcloud.ini /root/webvirtcloud-supervisor.conf # RHEL / openSUSE +supervisorctl update +``` + +Move the `environment=` values of that config into `/etc/default/webvirtcloud` (root, mode 600), one `NAME='value'` per line; leave out the worker's `WEBVIRTCLOUD_LOG_FILE`. Step 4 installs and starts the systemd units. Remove supervisor once nothing else uses it (on a compute node, gstfsd may). + ### Installer and manual installation `git pull` never overwrites `webvirtcloud/settings.py` or `/etc/nginx/conf.d/webvirtcloud.conf`; steps 2 and 4 bring them up to date. Run the steps from a root shell; they use `$b`, so in a new shell set it again to the backup directory. @@ -441,31 +441,22 @@ docker compose up -d 1. Stop the panel, back up and pull: ```bash - supervisorctl stop webvirtcloud novncd socketiod webvirt-worker + systemctl stop webvirt-cloud webvirt-novncd webvirt-socketiod webvirt-worker # once: root may use the repo, which belongs to the service user git config --global --add safe.directory /srv/webvirtcloud b=/root/webvirtcloud-backup-$(date +%Y%m%d-%H%M%S) cd /srv/webvirtcloud && - ! supervisorctl status webvirtcloud novncd socketiod webvirt-worker | grep RUNNING && + ! systemctl is-active --quiet webvirt-cloud webvirt-novncd webvirt-socketiod webvirt-worker && mkdir -m 700 "$b" && venv/bin/python3 -c "import sqlite3,sys; sqlite3.connect('db.sqlite3').backup(sqlite3.connect(sys.argv[1]))" "$b/db.sqlite3" && cp -p webvirtcloud/settings.py /etc/nginx/conf.d/webvirtcloud.conf "$b"/ && + { [ ! -f /etc/default/webvirtcloud ] || cp -p /etc/default/webvirtcloud "$b"/; } && git rev-parse HEAD > "$b/commit" && git ls-files -z --others --exclude-standard -- '*/migrations/*.py' | tar --null -T - --remove-files -cf "$b/local-migrations.tar" && git checkout -- static/css/wvc-main.min.css dev/scss/wvc-main.scss && git checkout master && git pull ``` - Until step 4 installs the worker, `supervisorctl` reports "no such process" for `webvirt-worker` on a first update from a version without it. Then keep the supervisor config for step 4 and the restore (`supervisor.conf` in the backup directory): - - ```bash - cp -p /etc/supervisor/conf.d/webvirtcloud.conf "$b/supervisor.conf" # Debian / Ubuntu - ``` - - ```bash - cp -p /etc/supervisord.d/webvirtcloud.ini "$b/supervisor.conf" # RHEL / openSUSE - ``` - 2. Rebuild `settings.py` from the template (the file keeps its owner and mode), then put back your own values; the `diff` lists them: ```bash @@ -491,31 +482,29 @@ docker compose up -d Installers from before September 2025 generated migrations of their own; step 1 moved them into `local-migrations.tar`. One of them has the name of a tracked migration, which `migrate` would skip; the second line marks it unapplied so that the tracked one runs. -4. Update the nginx config the same way as `settings.py`, and the supervisor config (it gains the `webvirt-worker` program): +4. Update the nginx config the same way as `settings.py`: ```bash cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/webvirtcloud.conf diff "$b/webvirtcloud.conf" /etc/nginx/conf.d/webvirtcloud.conf ``` - Debian / Ubuntu: + Put back your own lines (`server_name`, TLS, the noVNC port). Install the systemd units; your own service settings stay in `/etc/default/webvirtcloud` and in drop-ins. Debian / Ubuntu: ```bash - cp conf/supervisor/webvirtcloud.conf /etc/supervisor/conf.d/ - diff "$b/supervisor.conf" /etc/supervisor/conf.d/webvirtcloud.conf + cp conf/systemd/webvirt-{cloud,novncd,socketiod,worker}.service /etc/systemd/system/ ``` RHEL / openSUSE: ```bash - sed 's/^user=.*/user=nginx/' conf/supervisor/webvirtcloud.conf > /etc/supervisord.d/webvirtcloud.ini - diff "$b/supervisor.conf" /etc/supervisord.d/webvirtcloud.ini + for u in cloud novncd socketiod worker; do sed 's/^\(User\|Group\)=.*/\1=nginx/' conf/systemd/webvirt-$u.service > /etc/systemd/system/webvirt-$u.service; done ``` - Put back your own lines (`server_name`, TLS, the noVNC port, supervisor `environment=` settings), then reload nginx and start the panel: + Then reload nginx and start the panel: ```bash - nginx -t && systemctl reload nginx && supervisorctl update && supervisorctl start webvirtcloud novncd socketiod webvirt-worker + nginx -t && systemctl reload nginx && systemctl daemon-reload && systemctl enable --now webvirt-cloud webvirt-novncd webvirt-socketiod webvirt-worker ``` Older versions collected static files into `static/`; `git clean -n static/` lists the leftovers, which can be deleted. @@ -541,9 +530,9 @@ Installer and manual installation, from a root shell (`nginx:nginx` on RHEL / op ```bash b=/root/webvirtcloud-backup-YYYYMMDD-HHMMSS -supervisorctl stop webvirtcloud novncd socketiod webvirt-worker +systemctl stop webvirt-cloud webvirt-novncd webvirt-socketiod webvirt-worker cd /srv/webvirtcloud && -! supervisorctl status webvirtcloud novncd socketiod webvirt-worker | grep RUNNING && +! systemctl is-active --quiet webvirt-cloud webvirt-novncd webvirt-socketiod webvirt-worker && git checkout -- static/css/wvc-main.min.css dev/scss/wvc-main.scss && git checkout "$(cat "$b/commit")" && tar -xf "$b/local-migrations.tar" && @@ -556,26 +545,13 @@ chown -R www-data:www-data /srv/webvirtcloud && chmod 600 db.sqlite3 webvirtcloud/settings.py ``` -Restore the supervisor config, Debian / Ubuntu: +Put back `/etc/default/webvirtcloud` if the backup has it (`cp -p "$b/webvirtcloud" /etc/default/`), then reload nginx and start the panel: ```bash -cp "$b/supervisor.conf" /etc/supervisor/conf.d/webvirtcloud.conf +nginx -t && systemctl reload nginx && systemctl start webvirt-cloud webvirt-novncd webvirt-socketiod webvirt-worker ``` -RHEL / openSUSE: - -```bash -cp "$b/supervisor.conf" /etc/supervisord.d/webvirtcloud.ini -``` - -Then reload nginx and start the panel: - -```bash -nginx -t && systemctl reload nginx && -supervisorctl update && supervisorctl start webvirtcloud novncd socketiod webvirt-worker -``` - -When the restored version predates the worker, `supervisorctl` reports that `webvirt-worker` does not exist; the other programs start. A rollback leaves the clone on a detached HEAD at the restored commit; the update steps record that commit before they switch back to `master`. +A version from before v3.0.0 has no worker: `systemctl disable --now webvirt-worker`. A rollback keeps the systemd units. A rollback leaves the clone on a detached HEAD at the restored commit; the update steps record that commit before they switch back to `master`. ### Behavior changes @@ -624,6 +600,7 @@ When the restored version predates the worker, `supervisorctl` reports that `web - Manual installs that keep their own `settings.py` copy the SQLite `OPTIONS` and `TEST` entries from `settings.py.template` and add `"operations"` to `INSTALLED_APPS`. - The SQLite database must be on a local file system (WAL). - Outside compose, stop the Docker container with `docker stop -t 95` so the worker can finish running jobs. +- Installer and manual installations run the panel as systemd units (`webvirt-cloud`, `webvirt-novncd`, `webvirt-socketiod`, `webvirt-worker`) instead of supervisor; see [Moving from supervisor to systemd](#moving-from-supervisor-to-systemd). Service settings go into `/etc/default/webvirtcloud`, service output into the journal (`journalctl -u webvirt-cloud`); the app log stays in `data/`. ## Usage diff --git a/conf/runit/novncd.sh b/conf/runit/novncd.sh index 203acf86..38c02628 100755 --- a/conf/runit/novncd.sh +++ b/conf/runit/novncd.sh @@ -1,28 +1,4 @@ #!/bin/sh -USER="www-data" -DJANGO_PROJECT="/srv/webvirtcloud" -PYTHON="$DJANGO_PROJECT/venv/bin/python3" -NOVNCD="$DJANGO_PROJECT/console/novncd" -LOG="/var/log/novncd.log" - -cd "$DJANGO_PROJECT" || exit 1 - -# make novncd debug, verbose -#PARAMS="-d -v" - -if [ -x /sbin/setuser ]; then - if [ -n "$PARAMS" ]; then - # shellcheck disable=SC2086 - exec /sbin/setuser "$USER" "$PYTHON" "$NOVNCD" $PARAMS >> "$LOG" 2>&1 - else - exec /sbin/setuser "$USER" "$PYTHON" "$NOVNCD" >> "$LOG" 2>&1 - fi -elif command -v su >/dev/null 2>&1; then - if [ -n "$PARAMS" ]; then - # shellcheck disable=SC2086 - exec su -s /bin/sh "$USER" -c "exec \"$PYTHON\" \"$NOVNCD\" $PARAMS" >> "$LOG" 2>&1 - else - exec su -s /bin/sh "$USER" -c "exec \"$PYTHON\" \"$NOVNCD\"" >> "$LOG" 2>&1 - fi -fi +cd /srv/webvirtcloud || exit 1 +exec /sbin/setuser www-data /srv/webvirtcloud/venv/bin/python3 /srv/webvirtcloud/console/novncd >> /var/log/novncd.log 2>&1 diff --git a/conf/supervisor/webvirtcloud.conf b/conf/supervisor/webvirtcloud.conf deleted file mode 100644 index 7d04a357..00000000 --- a/conf/supervisor/webvirtcloud.conf +++ /dev/null @@ -1,37 +0,0 @@ -[program:webvirtcloud] -command=/srv/webvirtcloud/venv/bin/gunicorn webvirtcloud.wsgi:application -c /srv/webvirtcloud/gunicorn.conf.py -directory=/srv/webvirtcloud -user=www-data -autostart=true -autorestart=true -redirect_stderr=true - -[program:novncd] -command=/srv/webvirtcloud/venv/bin/python3 /srv/webvirtcloud/console/novncd -directory=/srv/webvirtcloud -user=www-data -autostart=true -autorestart=true -redirect_stderr=true - -# Serial console daemon; it refuses connections while SERIAL_CONSOLE_ENABLED -# is off in settings.py. -[program:socketiod] -command=/srv/webvirtcloud/venv/bin/python3 /srv/webvirtcloud/console/socketiod -directory=/srv/webvirtcloud -user=www-data -autostart=true -autorestart=true -redirect_stderr=true - -# The job worker: one per installation. It drains running jobs for 60 s on -# stop; supervisor waits a little longer before killing it. -[program:webvirt-worker] -command=/srv/webvirtcloud/venv/bin/python3 /srv/webvirtcloud/manage.py run_worker -directory=/srv/webvirtcloud -user=www-data -environment=WEBVIRTCLOUD_LOG_FILE="/srv/webvirtcloud/data/webvirtcloud-worker.log" -autostart=true -autorestart=true -stopwaitsecs=90 -redirect_stderr=true diff --git a/conf/systemd/README.md b/conf/systemd/README.md deleted file mode 100644 index b41990c7..00000000 --- a/conf/systemd/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Ubuntu Service Files - -I choose to install this service on Ubuntu 22.04 - -The process is not fully documented but I wanted to contribute the service files - -The file webvirt is beeing installed into /etc/default/webvirt and allows you to easily change the location of the webvirtcloud installation path - -The PATH variable being set for these services includes /usr/bin which contains the ssh executable. That is being used by webvirt-cloud and webvirt-novncd - -Should the executable be located in another location please adjust the path - -The error you should be seeing is "ssh executable not found" - - -Use one of the two service managers: the installer sets up supervisor (`conf/supervisor/webvirtcloud.conf` runs the worker); these units are for manual installs without supervisor. Enabling both starts two workers; the second waits for the lease and does nothing. diff --git a/conf/systemd/install.sh b/conf/systemd/install.sh deleted file mode 100755 index 5fc9d07a..00000000 --- a/conf/systemd/install.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -FILEPATH=$(readlink -f "$0"); -SCRIPTPATH=$(dirname "$FILEPATH"); -cd "$SCRIPTPATH" -cp webvirt-* /lib/systemd/system/ -cp webvirt /etc/default/ -echo Run to start services \"systemctl daemon-reload\; systemctl enable --now $(ls webvirt-* | tr "\n" " ")\" diff --git a/conf/systemd/webvirt b/conf/systemd/webvirt deleted file mode 100644 index c2060626..00000000 --- a/conf/systemd/webvirt +++ /dev/null @@ -1,2 +0,0 @@ -WEBVIRT_PATH="/srv/webvirtcloud" -PATH="$WEBVIRT_PATH/venv/bin:/usr/bin" diff --git a/conf/systemd/webvirt-cloud.service b/conf/systemd/webvirt-cloud.service index 9fe33077..590da320 100644 --- a/conf/systemd/webvirt-cloud.service +++ b/conf/systemd/webvirt-cloud.service @@ -1,23 +1,19 @@ [Unit] -Description=Webvirt Cloud Webinterface +Description=WebVirtCloud panel (gunicorn) Wants=network-online.target After=network-online.target [Service] -Type=simple -Restart=on-failure -RestartSec=10 User=www-data Group=www-data -LimitNOFILE=64000 -AmbientCapabilities=CAP_NET_BIND_SERVICE -EnvironmentFile=/etc/default/webvirt - -ExecStart=/bin/bash -c "cd ${WEBVIRT_PATH};${WEBVIRT_PATH}/venv/bin/gunicorn webvirtcloud.wsgi:application -c ${WEBVIRT_PATH}/gunicorn.conf.py" - -# Make sure stderr/stdout is captured in the systemd journal. -StandardOutput=journal -StandardError=journal +WorkingDirectory=/srv/webvirtcloud +EnvironmentFile=-/etc/default/webvirtcloud +ExecStart=/srv/webvirtcloud/venv/bin/gunicorn webvirtcloud.wsgi:application -c /srv/webvirtcloud/gunicorn.conf.py +# gunicorn stops its workers itself; the rest of the group is killed after the timeout +KillMode=mixed +TimeoutStopSec=90 +Restart=on-failure +RestartSec=10 [Install] WantedBy=multi-user.target diff --git a/conf/systemd/webvirt-novncd.service b/conf/systemd/webvirt-novncd.service index 0cac27d9..f9c3d075 100644 --- a/conf/systemd/webvirt-novncd.service +++ b/conf/systemd/webvirt-novncd.service @@ -1,23 +1,16 @@ [Unit] -Description=Webvirt Cloud NOVNCD +Description=WebVirtCloud VNC console proxy (novncd) Wants=network-online.target After=network-online.target [Service] -Type=simple -Restart=on-failure -RestartSec=10 User=www-data Group=www-data -LimitNOFILE=64000 -AmbientCapabilities=CAP_NET_BIND_SERVICE -EnvironmentFile=/etc/default/webvirt - -ExecStart=/bin/bash -c "cd ${WEBVIRT_PATH};${WEBVIRT_PATH}/venv/bin/python3 ${WEBVIRT_PATH}/console/novncd" - -# Make sure stderr/stdout is captured in the systemd journal. -StandardOutput=journal -StandardError=journal +WorkingDirectory=/srv/webvirtcloud +EnvironmentFile=-/etc/default/webvirtcloud +ExecStart=/srv/webvirtcloud/venv/bin/python3 /srv/webvirtcloud/console/novncd +Restart=on-failure +RestartSec=10 [Install] WantedBy=multi-user.target diff --git a/conf/systemd/webvirt-socketiod.service b/conf/systemd/webvirt-socketiod.service index eb097bc5..6d28d0c6 100644 --- a/conf/systemd/webvirt-socketiod.service +++ b/conf/systemd/webvirt-socketiod.service @@ -1,23 +1,18 @@ [Unit] -Description=Webvirt Cloud serial console (socketiod; refuses connections unless SERIAL_CONSOLE_ENABLED = True) +Description=WebVirtCloud serial console (socketiod; refuses connections unless SERIAL_CONSOLE_ENABLED) Wants=network-online.target After=network-online.target [Service] -Type=simple -Restart=on-failure -RestartSec=10 User=www-data Group=www-data -LimitNOFILE=64000 -AmbientCapabilities=CAP_NET_BIND_SERVICE -EnvironmentFile=/etc/default/webvirt - -ExecStart=/bin/bash -c "cd ${WEBVIRT_PATH};${WEBVIRT_PATH}/venv/bin/python3 ${WEBVIRT_PATH}/console/socketiod" - -# Make sure stderr/stdout is captured in the systemd journal. -StandardOutput=journal -StandardError=journal +WorkingDirectory=/srv/webvirtcloud +EnvironmentFile=-/etc/default/webvirtcloud +ExecStart=/srv/webvirtcloud/venv/bin/python3 /srv/webvirtcloud/console/socketiod +# gunicorn stops its worker itself; the consoles end with their ptys +KillMode=mixed +Restart=on-failure +RestartSec=10 [Install] WantedBy=multi-user.target diff --git a/conf/systemd/webvirt-worker.service b/conf/systemd/webvirt-worker.service index 687f19bb..044b8288 100644 --- a/conf/systemd/webvirt-worker.service +++ b/conf/systemd/webvirt-worker.service @@ -4,16 +4,17 @@ Wants=network-online.target After=network-online.target [Service] -Type=simple -Restart=on-failure -RestartSec=10 User=www-data Group=www-data -EnvironmentFile=/etc/default/webvirt -ExecStart=/bin/bash -c "cd ${WEBVIRT_PATH}; WEBVIRTCLOUD_LOG_FILE=${WEBVIRT_PATH}/data/webvirtcloud-worker.log ${WEBVIRT_PATH}/venv/bin/python3 manage.py run_worker" +WorkingDirectory=/srv/webvirtcloud +EnvironmentFile=-/etc/default/webvirtcloud +Environment=WEBVIRTCLOUD_LOG_FILE=/srv/webvirtcloud/data/webvirtcloud-worker.log +ExecStart=/srv/webvirtcloud/venv/bin/python3 /srv/webvirtcloud/manage.py run_worker +# the worker finishes its jobs for up to 60 s; its libvirt connections stay meanwhile +KillMode=mixed TimeoutStopSec=90 -StandardOutput=journal -StandardError=journal +Restart=on-failure +RestartSec=10 [Install] WantedBy=multi-user.target diff --git a/doc/architecture.md b/doc/architecture.md index 6c05b5e6..48b79972 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -26,7 +26,7 @@ The platform provides: | **Frontend** | Django Templates, Bootstrap 5, Bootstrap Icons, jQuery | Server-rendered responsive and dynamic user interface | | **Identity & Security** | Django Auth, `django-otp` (2FA/TOTP), `django-auth-ldap` | Role-based authorization, two-factor authentication, and optional LDAP/Active Directory support | | **Web / WSGI Server** | Nginx + Gunicorn + WhiteNoise | Static file delivery and reverse proxy architecture | -| **Process Manager** | Supervisor / Runit / Systemd | Background service management for Gunicorn, novncd, and socketiod | +| **Process Manager** | systemd (hosts), runit (Docker) | Service management for Gunicorn, novncd, socketiod and the job worker | --- @@ -162,7 +162,7 @@ Built on Django REST Framework with `drf-nested-routers`, providing a structured 1. **Gunicorn**: WSGI application server running `webvirtcloud.wsgi:application` (binds to a Unix socket or port 8000). 2. **novncd**: Background WebSockify daemon translating VNC TCP ports to WebSockets on port 6080. 3. **Nginx**: Front-facing reverse proxy handling ports 80/443, serving static assets directly, proxying dynamic web requests to Gunicorn, and routing WebSocket console traffic to `novncd`. -4. **Supervisor / Runit / Systemd**: Process supervision managing Gunicorn and novncd daemon lifecycles with automatic restarts. +4. **systemd / runit**: systemd units on hosts (`conf/systemd/`) and runit services in the Docker image (`conf/runit/`) run Gunicorn, novncd, socketiod and the job worker with automatic restarts. --- @@ -176,9 +176,9 @@ WebVirtCloud leverages a hybrid pattern: it combines the relational strengths of WebVirtCloud installer and runtime dependencies support modern enterprise Linux distributions: -| Distribution Family | Target Versions | Package Manager | Init / Supervisor | +| Distribution Family | Target Versions | Package Manager | Services | |---|---|---|---| -| **Ubuntu** | 20.04, 22.04 LTS, 24.04 LTS | `apt` | Systemd / Supervisor | -| **Debian** | 10, 11, 12 (Bookworm) | `apt` | Systemd / Supervisor / Runit | -| **RHEL / Rocky / Alma** | 9.x, 10.x | `dnf` | Systemd / Supervisord | -| **openSUSE / SLES** | openSUSE Leap 15.x, Tumbleweed, SLES 15 | `zypper` | Systemd / Supervisord | +| **Ubuntu** | 20.04, 22.04 LTS, 24.04 LTS | `apt` | systemd | +| **Debian** | 10, 11, 12 (Bookworm) | `apt` | systemd | +| **RHEL / Rocky / Alma** | 9.x, 10.x | `dnf` | systemd | +| **openSUSE / SLES** | openSUSE Leap 15.x, Tumbleweed, SLES 15 | `zypper` | systemd | diff --git a/webvirtcloud.sh b/webvirtcloud.sh index 9b9aa1a8..fdc1eeaf 100755 --- a/webvirtcloud.sh +++ b/webvirtcloud.sh @@ -64,6 +64,11 @@ if [ "$(id -u)" != "0" ]; then fi fi +if [ -f /etc/supervisor/conf.d/webvirtcloud.conf ] || [ -f /etc/supervisord.d/webvirtcloud.ini ]; then + echo "WebVirtCloud runs under supervisor here: move it to systemd first (README: Updating, Moving from supervisor to systemd)." >&2 + exit 1 +fi + clear readonly APP_USER="wvcuser" @@ -202,13 +207,14 @@ configure_nginx () { } -configure_supervisor () { - # Copy template supervisor service for gunicorn and novnc - echo " * Copying supervisor configuration" - mkdir -p "$supervisor_conf_path" > /dev/null 2>&1 - cp "$APP_PATH"/conf/supervisor/webvirtcloud.conf "$supervisor_conf_path"/"$supervisor_file_name" - nginx_group_escape="$(echo -n "$nginx_group"|sed -e 's/[](){}<>=:\!\?\+\|\/\&$*.^[]/\\&/g')" - sed -i "s|^\\(user=\\).*|\\1$nginx_group_escape|" "$supervisor_conf_path/$supervisor_file_name" +configure_systemd () { + # The panel's services run as the web server's user; a re-run restarts them. + units="webvirt-cloud webvirt-novncd webvirt-socketiod webvirt-worker" + for unit in $units; do + sed "s/^\\(User\\|Group\\)=.*/\\1=$nginx_group/" "$APP_PATH/conf/systemd/$unit.service" > "/etc/systemd/system/$unit.service" || exit 1 + done + # shellcheck disable=SC2086 + systemctl daemon-reload && systemctl enable $units && systemctl restart $units || exit 1 } create_user () { @@ -386,14 +392,6 @@ set_hosts () { fi } -restart_supervisor () { - echo "* Setting Supervisor to start on boot and restart." - log "systemctl enable $supervisor_service" - #systemctl enable $supervisor_service - log "systemctl restart $supervisor_service" - #systemctl restart $supervisor_service -} - restart_nginx () { echo "* Setting Nginx to start on boot and starting Nginx." log "systemctl enable nginx.service" @@ -449,36 +447,24 @@ case $distro in distro=ubuntu nginx_group=www-data nginxfile=/etc/nginx/conf.d/$APP_NAME.conf - supervisor_service=supervisor - supervisor_conf_path=/etc/supervisor/conf.d - supervisor_file_name=webvirtcloud.conf ;; *debian*) echo " The installer has detected $distro version $version codename $codename." distro=debian nginx_group=www-data nginxfile=/etc/nginx/conf.d/$APP_NAME.conf - supervisor_service=supervisor - supervisor_conf_path=/etc/supervisor/conf.d - supervisor_file_name=webvirtcloud.conf ;; *centos*|*redhat*|*ol*|*rhel*|*rocky*|*Rocky*|*alma*) echo " The installer has detected $distro version $version." distro=centos nginx_group=nginx nginxfile=/etc/nginx/conf.d/$APP_NAME.conf - supervisor_service=supervisord - supervisor_conf_path=/etc/supervisord.d - supervisor_file_name=webvirtcloud.ini ;; *opensuse*|*sles*|*sled*|*suse*) echo " The installer has detected $distro version $version." distro=suse nginx_group=nginx nginxfile=/etc/nginx/conf.d/$APP_NAME.conf - supervisor_service=supervisord - supervisor_conf_path=/etc/supervisord.d - supervisor_file_name=webvirtcloud.ini ;; *Uos*|*uos*) # codename may be fuyu, kongzi, eagle or empty string. @@ -491,14 +477,8 @@ case $distro in nginxfile=/etc/nginx/conf.d/$APP_NAME.conf if test "${codename}" == "eagle"; then nginx_group=www-data - supervisor_service=supervisor - supervisor_conf_path=/etc/supervisor/conf.d - supervisor_file_name=webvirtcloud.conf else nginx_group=nginx - supervisor_service=supervisord - supervisor_conf_path=/etc/supervisord.d - supervisor_file_name=webvirtcloud.ini fi ;; *OpenEuler*|*openEuler*) @@ -506,9 +486,6 @@ case $distro in distro=openEuler nginx_group=nginx nginxfile=/etc/nginx/conf.d/$APP_NAME.conf - supervisor_service=supervisord - supervisor_conf_path=/etc/supervisord.d - supervisor_file_name=webvirtcloud.ini ;; *) if [[ "$id_like" =~ suse ]]; then @@ -516,9 +493,6 @@ case $distro in distro=suse nginx_group=nginx nginxfile=/etc/nginx/conf.d/$APP_NAME.conf - supervisor_service=supervisord - supervisor_conf_path=/etc/supervisord.d - supervisor_file_name=webvirtcloud.ini else echo " The installer was unable to determine your OS. Exiting for safety." exit 1 @@ -593,7 +567,7 @@ case $distro in progress echo "* Installing OS requirements." - PACKAGES="git python3-venv python3-virtualenv python3-pip python3-dev python3-lxml libvirt-dev zlib1g-dev libxslt1-dev libsasl2-dev libldap2-dev nginx supervisor smbios-utils libsasl2-modules gcc pkg-config python3-guestfs uuid" + PACKAGES="git python3-venv python3-virtualenv python3-pip python3-dev python3-lxml libvirt-dev zlib1g-dev libxslt1-dev libsasl2-dev libldap2-dev nginx smbios-utils libsasl2-modules gcc pkg-config python3-guestfs uuid" install_packages @@ -604,10 +578,9 @@ case $distro in echo "* Configuring Nginx." configure_nginx - echo "* Configuring Supervisor." - configure_supervisor + echo "* Configuring systemd." + configure_systemd - restart_supervisor restart_nginx else echo "Unsupported Debian version. Version found: $version" @@ -624,7 +597,7 @@ case $distro in progress echo "* Installing OS requirements." - PACKAGES="git python3-venv python3-virtualenv python3-pip python3-dev python3-lxml libvirt-dev zlib1g-dev libxslt1-dev libsasl2-dev libldap2-dev nginx supervisor libsasl2-modules gcc pkg-config python3-guestfs" + PACKAGES="git python3-venv python3-virtualenv python3-pip python3-dev python3-lxml libvirt-dev zlib1g-dev libxslt1-dev libsasl2-dev libldap2-dev nginx libsasl2-modules gcc pkg-config python3-guestfs" install_packages set_hosts @@ -634,10 +607,9 @@ case $distro in echo "* Configuring Nginx." configure_nginx - echo "* Configuring Supervisor." - configure_supervisor + echo "* Configuring systemd." + configure_systemd - restart_supervisor restart_nginx else echo "Unsupported Ubuntu version. Version found: $version" @@ -657,8 +629,8 @@ case $distro in log "dnf config-manager --set-enabled crb" fi - echo "* Adding wget & epel-release & supervisor repository." - log "yum -y install wget epel-release supervisor" + echo "* Adding wget & epel-release repository." + log "yum -y install wget epel-release" echo "* Installing OS requirements." PACKAGES="git python3-virtualenv python3-devel libvirt-devel python3-libvirt python3-ldap glibc gcc nginx python3-lxml python3-libguestfs iproute-tc cyrus-sasl-md5 openldap-devel cyrus-sasl-devel openssl-devel" @@ -673,17 +645,14 @@ case $distro in echo "* Configuring Nginx." configure_nginx - echo "* Configuring Supervisor." - log "pip3 install supervisor " - configure_supervisor - - set_firewall - + set_firewall + set_selinux - restart_supervisor + echo "* Configuring systemd." + configure_systemd + restart_nginx - else echo "Unsupported RHEL Based OS($distro) version. Version found: $version" exit 1 @@ -709,15 +678,13 @@ case $distro in echo "* Configuring Nginx." configure_nginx - echo "* Configuring Supervisor." - log "pip install supervisor " - configure_supervisor - set_firewall set_selinux - restart_supervisor + echo "* Configuring systemd." + configure_systemd + restart_nginx fi ;; @@ -737,15 +704,13 @@ case $distro in echo "* Configuring Nginx." configure_nginx - echo "* Configuring Supervisor." - log "pip install supervisor " - configure_supervisor - set_firewall set_selinux - restart_supervisor + echo "* Configuring systemd." + configure_systemd + restart_nginx fi ;; @@ -768,64 +733,16 @@ case $distro in set_hosts - # Supervisor on SUSE (available as python3-supervisor, supervisor, or via pip) - if ! command -v supervisord >/dev/null 2>&1; then - if zypper --non-interactive install -y python3-supervisor >/dev/null 2>&1; then - echo " * python3-supervisor installed via zypper" - elif zypper --non-interactive install -y supervisor >/dev/null 2>&1; then - echo " * supervisor installed via zypper" - else - echo " * Installing supervisor via pip3" - log "pip3 install supervisor" - fi - fi - - # Ensure /etc/supervisord.d directory exists and is included in supervisord.conf - mkdir -p /etc/supervisord.d - if [ ! -f /etc/supervisord.conf ] && [ ! -f /etc/supervisor/supervisord.conf ]; then - if command -v echo_supervisord_conf >/dev/null 2>&1; then - echo_supervisord_conf > /etc/supervisord.conf - echo -e "\n[include]\nfiles = /etc/supervisord.d/*.ini\n" >> /etc/supervisord.conf - fi - elif [ -f /etc/supervisord.conf ] && ! grep -q "/etc/supervisord.d" /etc/supervisord.conf; then - echo -e "\n[include]\nfiles = /etc/supervisord.d/*.ini\n" >> /etc/supervisord.conf - fi - - # Ensure systemd service for supervisord exists if installed via pip - if [ ! -f /usr/lib/systemd/system/supervisord.service ] && [ ! -f /etc/systemd/system/supervisord.service ]; then - supervisord_bin="$(command -v supervisord 2>/dev/null || echo "/usr/local/bin/supervisord")" - supervisorctl_bin="$(command -v supervisorctl 2>/dev/null || echo "/usr/local/bin/supervisorctl")" - cat > /etc/systemd/system/supervisord.service </dev/null 2>&1 || true - fi - install_webvirtcloud echo "* Configuring Nginx." configure_nginx - echo "* Configuring Supervisor." - configure_supervisor - set_firewall - restart_supervisor + echo "* Configuring systemd." + configure_systemd + restart_nginx ;; esac diff --git a/webvirtcloud/tests/test_worker_install.py b/webvirtcloud/tests/test_worker_install.py index 8887b16a..8685bf15 100644 --- a/webvirtcloud/tests/test_worker_install.py +++ b/webvirtcloud/tests/test_worker_install.py @@ -1,7 +1,7 @@ -"""Every install type runs the worker as www-data, with its own log file and -a stop timeout above the worker's 60 s grace.""" +"""Every install type runs the worker as the web server's user (www-data, or +nginx where the installer and README substitute it), with its own log file +and a stop timeout above the worker's 60 s grace.""" -import re from pathlib import Path from django.conf import settings @@ -26,16 +26,9 @@ class WorkerInstallTestCase(SimpleTestCase): self.assertIn("webvirtcloud-data:/srv/webvirtcloud/data", compose) self.assertNotIn("hostname:", compose) - def test_supervisor(self): - conf = (BASE / "conf/supervisor/webvirtcloud.conf").read_text() - block = re.search(r"\[program:webvirt-worker\](.*?)(\n\[|\Z)", conf, re.S).group(1) - for text in ("manage.py run_worker", "user=www-data", "webvirtcloud-worker.log", "autorestart=true"): - self.assertIn(text, block) - self.assertRegex(block, r"stopwaitsecs=9\d") - - def test_systemd_is_an_alternative(self): + def test_systemd(self): unit = (BASE / "conf/systemd/webvirt-worker.service").read_text() - self.assertIn("run_worker", unit) - self.assertIn("Restart=on-failure", unit) + for text in ("manage.py run_worker", "User=www-data", "webvirtcloud-worker.log", "KillMode=mixed", + "EnvironmentFile=-/etc/default/webvirtcloud", "Restart=on-failure"): + self.assertIn(text, unit) self.assertRegex(unit, r"TimeoutStopSec=9\d") - self.assertIn("one of the two", (BASE / "conf/systemd/README.md").read_text())