1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00

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.
This commit is contained in:
catborise 2026-10-17 22:38:33 +03:00
parent fbef55fe2d
commit 837c5468ec
13 changed files with 129 additions and 343 deletions

117
README.md
View file

@ -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 `<graphics>` device, a `<console type='pty'>`) 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 `<graphics>` device, a `<console type='pty'>`) 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 `<graphics>` device, a `<console type='pty'>`) 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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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" " ")\"

View file

@ -1,2 +0,0 @@
WEBVIRT_PATH="/srv/webvirtcloud"
PATH="$WEBVIRT_PATH/venv/bin:/usr/bin"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 |

View file

@ -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 <<SVCEOF
[Unit]
Description=Process Monitoring and Control Daemon
After=network.target
[Service]
Type=forking
ExecStart=${supervisord_bin} -c /etc/supervisord.conf
ExecStop=${supervisorctl_bin} shutdown
ExecReload=${supervisorctl_bin} reload
KillMode=process
Restart=on-failure
RestartSec=42s
[Install]
WantedBy=multi-user.target
SVCEOF
systemctl daemon-reload >/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

View file

@ -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())