From 88288f965d970d0dd5acb04baa7b4a7a564c8d6c Mon Sep 17 00:00:00 2001 From: catborise Date: Fri, 25 Sep 2026 20:03:02 +0300 Subject: [PATCH 01/10] feat(install): add openSUSE and SLES 15 distribution support and compute bootstrap --- README.md | 22 ++++++- conf/nginx/suse_nginx.conf | 30 +++++++++ dev/libvirt-bootstrap.sh | 106 +++++++++++++++++-------------- doc/architecture.md | 13 ++++ webvirtcloud.sh | 127 +++++++++++++++++++++++++++++++++++-- 5 files changed, 244 insertions(+), 54 deletions(-) create mode 100644 conf/nginx/suse_nginx.conf diff --git a/README.md b/README.md index 3cf11a98..4c5a7b80 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ WebVirtCloud is a virtualization web interface for admins and users. It can dele ## Quick Install with Installer (Beta) -Install an OS and run specified commands. Installer supported OSes: Ubuntu 20.04/22.04/24.04, Debian 10/11/12, Rocky/Alma/OEL/RHEL 10. +Install an OS and run specified commands. Installer supported OSes: Ubuntu 20.04/22.04/24.04, Debian 10/11/12, Rocky/Alma/OEL/RHEL 9/10, openSUSE Leap 15.x / Tumbleweed, and SLES 15. It can be installed on a virtual machine, physical host or on a KVM host. ```bash @@ -169,6 +169,26 @@ python manage.py migrate python manage.py runserver 0.0.0.0:8000 ``` +#### openSUSE Leap 15.x / Tumbleweed / SLES 15: +```bash +# 1. Install system prerequisites +sudo zypper --non-interactive install -y git python3-devel python3-pip python3-virtualenv libvirt-devel python3-libvirt python3-lxml openldap2-devel cyrus-sasl-devel libopenssl-devel libxslt-devel libxml2-devel gcc pkg-config + +# 2. Create virtual environment with system site packages +python3 -m venv --system-site-packages .venv +source .venv/bin/activate + +# 3. Install Python dependencies +pip install -r conf/requirements.txt +pip install -r dev/requirements.txt + +# 4. Initialize configuration and run local dev server +cp webvirtcloud/settings.py.template webvirtcloud/settings.py +sed -i -E 's/SECRET_KEY = .*/SECRET_KEY = "'$(python3 conf/runit/secret_generator.py)'"/' webvirtcloud/settings.py +python manage.py migrate +python manage.py runserver 0.0.0.0:8000 +``` + #### Configure the supervisor for RHEL Based OS Add the following after the [include] line (after **files = ...** actually): diff --git a/conf/nginx/suse_nginx.conf b/conf/nginx/suse_nginx.conf new file mode 100644 index 00000000..d698de3e --- /dev/null +++ b/conf/nginx/suse_nginx.conf @@ -0,0 +1,30 @@ +# Nginx configuration for openSUSE and SUSE Linux Enterprise Server (SLES) +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log; +pid /run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Load modular configuration files from the /etc/nginx/conf.d and vhosts.d directories + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/vhosts.d/*.conf; +} diff --git a/dev/libvirt-bootstrap.sh b/dev/libvirt-bootstrap.sh index 11a7a9bd..68c85ca6 100644 --- a/dev/libvirt-bootstrap.sh +++ b/dev/libvirt-bootstrap.sh @@ -160,11 +160,11 @@ __gather_linux_system_info() { # Let's convert CamelCase to Camel Case DISTRO_NAME=$(__camelcase_split "$DISTRO_NAME") fi - if [ "${DISTRO_NAME}" = "openSUSE project" ]; then + if [ "${DISTRO_NAME}" = "openSUSE project" ] || [ "${DISTRO_NAME}" = "openSUSE" ] || [ "${DISTRO_NAME}" = "opensuse" ]; then # lsb_release -si returns "openSUSE project" on openSUSE 12.3 DISTRO_NAME="opensuse" fi - if [ "${DISTRO_NAME}" = "SUSE LINUX" ]; then + if [ "${DISTRO_NAME}" = "SUSE LINUX" ] || [ "${DISTRO_NAME}" = "SLES" ] || [ "${DISTRO_NAME}" = "sles" ]; then # lsb_release -si returns "SUSE LINUX" on SLES 11 SP3 DISTRO_NAME="suse" fi @@ -223,8 +223,8 @@ __gather_linux_system_info() { done < /etc/"${rsource}" ;; os ) - nn=$(grep '^ID=' /etc/os-release | sed -e 's/^ID=\(.*\)$/\1/g') - rv=$(grep '^VERSION_ID=' /etc/os-release | sed -e 's/^VERSION_ID=\(.*\)$/\1/g') + nn=$(grep '^ID=' /etc/os-release | sed -e 's/^ID=\(.*\)$/\1/g' | tr -d '"'\'') + rv=$(grep '^VERSION_ID=' /etc/os-release | sed -e 's/^VERSION_ID=\(.*\)$/\1/g' | tr -d '"'\'') [ "${rv}x" != "x" ] && v=$(__parse_version_string "$rv") || v="" case $(echo "${nn}" | tr '[:upper:]' '[:lower:]') in arch ) @@ -242,6 +242,12 @@ __gather_linux_system_info() { echowarn "Unable to parse the Debian Version" fi ;; + opensuse* ) + n="opensuse" + ;; + sles*|sled*|suse* ) + n="suse" + ;; * ) n=${nn} ;; @@ -305,10 +311,8 @@ __check_end_of_life_versions() { ;; opensuse) - # openSUSE versions not supported - # - # <= 12.1 - if { [ "$DISTRO_MAJOR_VERSION" -eq 12 ] && [ "$DISTRO_MINOR_VERSION" -eq 1 ]; } || [ "$DISTRO_MAJOR_VERSION" -lt 12 ]; then + # openSUSE: allow Leap 15.x and Tumbleweed + if [ -n "$DISTRO_MAJOR_VERSION" ] && [ "$DISTRO_MAJOR_VERSION" -lt 12 ]; then echoerror "End of life distributions are not supported." echoerror "Please consider upgrading to the next stable. See:" echoerror " http://en.opensuse.org/Lifetime" @@ -317,16 +321,19 @@ __check_end_of_life_versions() { ;; suse) - # SuSE versions not supported - # - # < 11 SP2 - SUSE_PATCHLEVEL=$(awk '/PATCHLEVEL/ {print $3}' /etc/SuSE-release ) - if [ "x${SUSE_PATCHLEVEL}" = "x" ]; then - SUSE_PATCHLEVEL="00" - fi - if { [ "$DISTRO_MAJOR_VERSION" -eq 11 ] && [ "$SUSE_PATCHLEVEL" -lt 02 ]; } || [ "$DISTRO_MAJOR_VERSION" -lt 11 ]; then - echoerror "Versions lower than SuSE 11 SP2 are not supported." - echoerror "Please consider upgrading to the next stable" + # SLES / SLED + if [ -f /etc/SuSE-release ]; then + SUSE_PATCHLEVEL=$(awk '/PATCHLEVEL/ {print $3}' /etc/SuSE-release ) + if [ "x${SUSE_PATCHLEVEL}" = "x" ]; then + SUSE_PATCHLEVEL="00" + fi + if { [ "$DISTRO_MAJOR_VERSION" -eq 11 ] && [ "$SUSE_PATCHLEVEL" -lt 02 ]; } || [ "$DISTRO_MAJOR_VERSION" -lt 11 ]; then + echoerror "Versions lower than SuSE 11 SP2 are not supported." + echoerror "Please consider upgrading to the next stable" + exit 1 + fi + elif [ -n "$DISTRO_MAJOR_VERSION" ] && [ "$DISTRO_MAJOR_VERSION" -lt 12 ]; then + echoerror "Versions lower than SUSE 12 are not supported." exit 1 fi ;; @@ -546,58 +553,63 @@ daemons_running_fedora() { # Opensuse Install Functions # install_opensuse() { - zypper -n install -l kvm libvirt bridge-utils python3-libguestfs supervisor || return 1 + zypper -n install -l qemu-kvm libvirt libvirt-daemon-qemu libvirt-client bridge-utils python3-libguestfs supervisor || \ + zypper -n install -l kvm libvirt bridge-utils python3-libguestfs python3-supervisor || \ + zypper -n install -l kvm libvirt bridge-utils python3-libguestfs || return 1 return 0 } install_opensuse_post() { if [ -f /etc/sysconfig/libvirtd ]; then sed -i 's/#LIBVIRTD_ARGS/LIBVIRTD_ARGS/g' /etc/sysconfig/libvirtd - else - echoerror "/etc/sysconfig/libvirtd not found. Exiting..." - exit 1 fi if [ -f /etc/libvirt/libvirtd.conf ]; then sed -i 's/#listen_tls/listen_tls/g' /etc/libvirt/libvirtd.conf sed -i 's/#listen_tcp/listen_tcp/g' /etc/libvirt/libvirtd.conf sed -i 's/#auth_tcp/auth_tcp/g' /etc/libvirt/libvirtd.conf - else - echoerror "/etc/libvirt/libvirtd.conf not found. Exiting..." - exit 1 + sed -i 's/#unix_sock_group = "libvirt"/unix_sock_group = "libvirt"/g' /etc/libvirt/libvirtd.conf + sed -i 's/#unix_sock_rw_perms = "0770"/unix_sock_rw_perms = "0770"/g' /etc/libvirt/libvirtd.conf + sed -i 's/#auth_unix_rw = "polkit"/auth_unix_rw = "none"/g' /etc/libvirt/libvirtd.conf + fi + if [ -f /etc/libvirt/virtqemud.conf ]; then + sed -i 's/#listen_tls/listen_tls/g' /etc/libvirt/virtqemud.conf + sed -i 's/#listen_tcp/listen_tcp/g' /etc/libvirt/virtqemud.conf + sed -i 's/#auth_tcp/auth_tcp/g' /etc/libvirt/virtqemud.conf + sed -i 's/#unix_sock_group = "libvirt"/unix_sock_group = "libvirt"/g' /etc/libvirt/virtqemud.conf + sed -i 's/#unix_sock_rw_perms = "0770"/unix_sock_rw_perms = "0770"/g' /etc/libvirt/virtqemud.conf + sed -i 's/#auth_unix_rw = "polkit"/auth_unix_rw = "none"/g' /etc/libvirt/virtqemud.conf fi if [ -f /etc/libvirt/qemu.conf ]; then sed -i 's/#[ ]*vnc_listen.*/vnc_listen = "0.0.0.0"/g' /etc/libvirt/qemu.conf sed -i 's/#[ ]*spice_listen.*/spice_listen = "0.0.0.0"/g' /etc/libvirt/qemu.conf - else - echoerror "/etc/libvirt/qemu.conf not found. Exiting..." - exit 1 fi - if [ -f /etc/supervisord.conf ]; then - curl https://raw.githubusercontent.com/retspen/webvirtcloud/master/conf/daemon/gstfsd > /usr/local/bin/gstfsd - chmod +x /usr/local/bin/gstfsd - curl https://raw.githubusercontent.com/retspen/webvirtcloud/master/conf/supervisor/gstfsd.conf > /etc/supervisor.d/gstfsd.ini - else - echoerror "Supervisor not found. Exiting..." - exit 1 + mkdir -p /etc/supervisord.d /etc/supervisor/conf.d + if [ -f /etc/supervisord.conf ] || [ -f /etc/supervisor/supervisord.conf ]; then + curl -fsSL https://raw.githubusercontent.com/retspen/webvirtcloud/master/conf/daemon/gstfsd > /usr/local/bin/gstfsd 2>/dev/null || true + chmod +x /usr/local/bin/gstfsd 2>/dev/null || true + curl -fsSL https://raw.githubusercontent.com/retspen/webvirtcloud/master/conf/supervisor/gstfsd.conf > /etc/supervisord.d/gstfsd.ini 2>/dev/null || true fi return 0 } daemons_running_opensuse() { - if [ -f /usr/lib/systemd/system/libvirtd.service ]; then - systemctl stop libvirtd.service > /dev/null 2>&1 - systemctl start libvirtd.service - fi - if [ -f /usr/lib/systemd/system/libvirt-guests.service ]; then - systemctl stop libvirt-guests.service > /dev/null 2>&1 - systemctl start libvirt-guests.service - fi - if [ -f /usr/lib/systemd/system/supervisord.service ]; then - systemctl stop supervisord.service > /dev/null 2>&1 - systemctl start supervisord.service - fi + systemctl enable --now libvirtd.service 2>/dev/null || systemctl enable --now virtqemud.service 2>/dev/null || true + systemctl enable --now libvirt-guests.service 2>/dev/null || true + systemctl enable --now supervisord.service 2>/dev/null || systemctl enable --now supervisor.service 2>/dev/null || true return 0 } + +install_suse() { + install_opensuse "$@" +} + +install_suse_post() { + install_opensuse_post "$@" +} + +daemons_running_suse() { + daemons_running_opensuse "$@" +} # # Ended openSUSE Install Functions # diff --git a/doc/architecture.md b/doc/architecture.md index 2dacfd48..d970cd49 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -169,3 +169,16 @@ Built on Django REST Framework with `drf-nested-routers`, providing a structured ## 7. Architecture Highlights WebVirtCloud leverages a hybrid pattern: it combines the relational strengths of the **Django ORM** for persistent entities (users, compute host credentials, quotas, and permission delegations) with direct, real-time **libvirt C API bindings** for dynamic virtualization state (VM statuses, resource allocation, storage metrics, and live hardware statistics). This design ensures that the web console always reflects the true hypervisor state without risk of database desynchronization. + +--- + +## 8. Supported Distributions & Platform Matrix + +WebVirtCloud installer and runtime dependencies support modern enterprise Linux distributions: + +| Distribution Family | Target Versions | Package Manager | Init / Supervisor | +|---|---|---|---| +| **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 | diff --git a/webvirtcloud.sh b/webvirtcloud.sh index 6d2c8784..cc96f234 100755 --- a/webvirtcloud.sh +++ b/webvirtcloud.sh @@ -160,6 +160,16 @@ install_packages () { fi done ;; + suse) + for p in $PACKAGES; do + if rpm -q "$p" >/dev/null 2>&1; then + echo " * $p already installed" + else + echo " * Installing $p" + log "zypper --non-interactive install -y $p" + fi + done + ;; esac } @@ -206,12 +216,17 @@ create_user () { if [ "$distro" == "ubuntu" ] || [ "$distro" == "debian" ] || [[ "$distro" == "uos" && "$codename" == "eagle" ]]; then adduser --quiet --disabled-password --gecos '""' "$APP_USER" + elif [ "$distro" == "suse" ]; then + useradd -m -s /bin/bash "$APP_USER" 2>/dev/null || adduser "$APP_USER" else - adduser "$APP_USER" + useradd -m -s /bin/bash "$APP_USER" 2>/dev/null || adduser "$APP_USER" fi - usermod -a -G "$nginx_group" "$APP_USER" - usermod -a -G libvirt "$nginx_group" + usermod -a -G "$nginx_group" "$APP_USER" 2>/dev/null || true + usermod -a -G libvirt "$nginx_group" 2>/dev/null || true + usermod -a -G kvm "$nginx_group" 2>/dev/null || true + usermod -a -G libvirt "$APP_USER" 2>/dev/null || true + usermod -a -G kvm "$APP_USER" 2>/dev/null || true } run_as_app_user () { @@ -371,6 +386,8 @@ if [ -f /etc/os-release ]; then version="$(source /etc/os-release && echo "$VERSION_ID")" # shellcheck disable=SC1091 codename="$(source /etc/os-release && echo "${VERSION_CODENAME:-$UBUNTU_CODENAME}")" + # shellcheck disable=SC1091 + id_like="$(source /etc/os-release && echo "${ID_LIKE:-}")" elif [[ -f /etc/lsb-release || -f /etc/debian_version ]]; then if command -v lsb_release >/dev/null 2>&1; then distro="$(lsb_release -is)" @@ -384,6 +401,11 @@ elif [[ -f /etc/lsb-release || -f /etc/debian_version ]]; then elif [ -f /etc/centos-release ]; then distro="centos" version="8" +elif [ -f /etc/SuSE-release ]; then + distro="suse" + version="15" + codename="" + id_like="suse" else distro="unsupported" fi @@ -394,7 +416,7 @@ echo ' ' echo "" -echo " Welcome to Webvirtcloud Installer for RHEL Based OSes, Debian and Ubuntu!" +echo " Welcome to Webvirtcloud Installer for RHEL Based OSes, Debian, Ubuntu, and SUSE!" echo "" shopt -s nocasematch case $distro in @@ -425,6 +447,15 @@ case $distro in 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. output_expand="" @@ -456,8 +487,18 @@ case $distro in supervisor_file_name=webvirtcloud.ini ;; *) - echo " The installer was unable to determine your OS. Exiting for safety." - exit 1 + if [[ "$id_like" =~ suse ]]; then + echo " The installer has detected $distro (SUSE family) 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 + else + echo " The installer was unable to determine your OS. Exiting for safety." + exit 1 + fi ;; esac @@ -684,6 +725,80 @@ case $distro in restart_nginx fi ;; + suse) + # Install for openSUSE Leap 15.x / Tumbleweed / SLES 15 + tzone=\'$(get_timezone)\' + + echo -n "* Updating installed packages." + log "zypper --non-interactive refresh" & pid=$! + progress + + echo "* Installing OS requirements." + PACKAGES="git python3-devel python3-pip python3-virtualenv libvirt-devel python3-libvirt python3-lxml openldap2-devel cyrus-sasl-devel libopenssl-devel libxslt-devel libxml2-devel gcc pkg-config nginx" + install_packages + + 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 + restart_nginx + ;; esac From f5e5ff27595c3643f9ab1431b592f0279866eb49 Mon Sep 17 00:00:00 2001 From: catborise Date: Fri, 25 Sep 2026 20:44:52 +0300 Subject: [PATCH 02/10] fix(install): refine openSUSE Leap dependencies and streamline installer execution - Add Python 3.11 package detection and alternatives configuration for openSUSE/SLES - Relax libvirt-python and lxml minimum versions in requirements to match distro-provided packages - Gracefully handle missing hostname command in minimal/containerized SUSE environments - Prevent redundant git clone if workspace already exists in target path --- conf/requirements.txt | 4 ++-- webvirtcloud.sh | 37 ++++++++++++++++++++++++++++++------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/conf/requirements.txt b/conf/requirements.txt index 778cbbca..a19f3c31 100644 --- a/conf/requirements.txt +++ b/conf/requirements.txt @@ -11,8 +11,8 @@ drf-spectacular[sidecar]==0.28.0 eventlet==0.40.1 gunicorn==23.0.0 libsass==0.23.0 -libvirt-python>=11.0.0 -lxml>=5.2.0 +libvirt-python>=9.0.0 +lxml>=4.9.0 ldap3==2.9.1 markdown==3.8.2 paramiko==3.4.0 diff --git a/webvirtcloud.sh b/webvirtcloud.sh index cc96f234..2104c30c 100755 --- a/webvirtcloud.sh +++ b/webvirtcloud.sh @@ -285,8 +285,12 @@ generate_secret_key() { install_webvirtcloud () { create_user - echo "* Cloning $APP_NAME from github to the web directory." - log "git clone $APP_REPO_URL $APP_PATH" + if [ ! -d "$APP_PATH/.git" ]; then + echo "* Cloning $APP_NAME from github to the web directory." + log "git clone $APP_REPO_URL $APP_PATH" + else + echo "* $APP_NAME already present in $APP_PATH." + fi echo "* Configuring settings.py file." cp "$APP_PATH/webvirtcloud/settings.py.template" "$APP_PATH/webvirtcloud/settings.py" @@ -308,9 +312,11 @@ install_webvirtcloud () { # set CSRF TRUSTED ORIGINS host_ip="'http://127.0.0.1', " - for i in $(hostname -I); do - host_ip+="'http://$i', " - done + if command -v hostname >/dev/null 2>&1; then + for i in $(hostname -I 2>/dev/null); do + host_ip+="'http://$i', " + done + fi sed -i "s|^\\(CSRF_TRUSTED_ORIGINS = \\).*|\\1\[ \'http://$fqdn\', $host_ip ]|" "$APP_PATH/webvirtcloud/settings.py" echo "* Checking up Python3 version." @@ -359,7 +365,9 @@ set_selinux () { set_hosts () { echo "* Setting up hosts file." - echo >> /etc/hosts "127.0.0.1 $(hostname) $fqdn" + local hname + hname="$(hostname 2>/dev/null || uname -n)" + echo >> /etc/hosts "127.0.0.1 $hname $fqdn" } restart_supervisor () { @@ -734,9 +742,24 @@ case $distro in progress echo "* Installing OS requirements." - PACKAGES="git python3-devel python3-pip python3-virtualenv libvirt-devel python3-libvirt python3-lxml openldap2-devel cyrus-sasl-devel libopenssl-devel libxslt-devel libxml2-devel gcc pkg-config nginx" + # On SUSE, install Python 3.11 stack to satisfy Python >= 3.10 requirement along with native libvirt and ldap bindings + if zypper se -s python311-devel >/dev/null 2>&1; then + PACKAGES="git hostname python311 python311-base python311-devel python311-pip python311-libvirt-python python311-lxml python311-ldap libvirt-devel cyrus-sasl-devel libopenssl-devel gcc pkg-config nginx" + else + PACKAGES="git hostname python3-devel python3-pip python3-virtualenv libvirt-devel python3-libvirt python3-lxml openldap2-devel cyrus-sasl-devel libopenssl-devel gcc pkg-config nginx" + fi install_packages + # Ensure python3 and pip3 point to python 3.11+ if installed + if command -v python3.11 >/dev/null 2>&1; then + update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 2>/dev/null || true + ln -sf /usr/bin/python3.11 /usr/bin/python3 + if command -v pip3.11 >/dev/null 2>&1; then + update-alternatives --install /usr/bin/pip3 pip3 /usr/bin/pip3.11 1 2>/dev/null || true + ln -sf /usr/bin/pip3.11 /usr/bin/pip3 + fi + fi + set_hosts # Supervisor on SUSE (available as python3-supervisor, supervisor, or via pip) From 6d385ca5d26b0f7bdca660aa86e6c51f7eddf1ab Mon Sep 17 00:00:00 2001 From: catborise Date: Fri, 25 Sep 2026 21:37:50 +0300 Subject: [PATCH 03/10] docs(readme): overhaul documentation structure, security links, and multi-distro setup - Replace shortened URL links (bit.ly, clck.ru) with direct upstream/repository paths - Modernize and reorganize manual production installation guides for Ubuntu/Debian, RHEL/Rocky, and openSUSE/SLES - Fix Nginx proxy header ($remote_addr -> $scheme) and streamline Supervisor configs - Unify Compute Node (Hypervisor) setup into a dedicated section - Update test running documentation reflecting isolated mocks and WEBVIRTCLOUD_TEST_LIBVIRT_URI - Document drf-spectacular OpenAPI 3.0 schema endpoints and fix typos - Convert screenshot gallery to clean Markdown table formatting --- README.md | 497 ++++++++++++++++++++++++------------------------------ 1 file changed, 223 insertions(+), 274 deletions(-) diff --git a/README.md b/README.md index 4c5a7b80..a22b8f9a 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,26 @@ [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/retspen/webvirtcloud) # WebVirtCloud -###### Python >=3.11 & Django 4.2 LTS +###### Python >=3.10 & Django 4.2 LTS (tested on Python 3.10 – 3.12) + +## Description + +WebVirtCloud is a virtualization web interface for administrators and users. It allows delegating virtual machines to users with role-based permissions. A built-in noVNC / SPICE console presents a full graphical interface to the guest domain. KVM is currently the supported hypervisor. ## Features * QEMU/KVM Hypervisor Management * QEMU/KVM Instance Management - Create, Delete, Update -* Hypervisor & Instance web based stats -* Manage Multiple QEMU/KVM Hypervisor -* Manage Hypervisor Datastore pools -* Manage Hypervisor Networks -* Instance Console Access with Browsers -* Libvirt API based web management UI -* User Based Authorization and Authentication -* User can add SSH public key to root in Instance (Tested only Ubuntu) -* User can change root password in Instance (Tested only Ubuntu) +* Hypervisor & Instance web-based real-time stats +* Manage Multiple QEMU/KVM Hypervisors +* Manage Hypervisor Datastore pools and storage volumes +* Manage Hypervisor Networks and interfaces +* Instance Console Access with Web Browsers (noVNC & SPICE) +* Libvirt API-based web management UI +* User-based Authorization, Authentication, and 2FA (OTP) +* User can add SSH public key to root in Instance +* User can change root password in Instance * Supports cloud-init datasource interface - -### Warning!!! - -How to update gstfsd daemon on hypervisor: - -```bash -wget -O - https://bit.ly/2NAaWXG | sudo tee -a /usr/local/bin/gstfsd -sudo service supervisor restart -``` - -## Description - -WebVirtCloud is a virtualization web interface for admins and users. It can delegate Virtual Machine's to users. A noVNC viewer presents a full graphical console to the guest domain. KVM is currently the only hypervisor supported. +* REST API with OpenAPI 3.0 (Swagger & ReDoc) documentation ## Quick Install with Installer (Beta) @@ -56,80 +48,151 @@ You should generate SECRET_KEY after cloning repository. Then put it into webvir python3 -c 'import secrets; print(secrets.token_urlsafe(50))' ``` -### Install WebVirtCloud panel (Ubuntu 18.04+ LTS) +### Ubuntu 20.04 / 22.04 / 24.04 LTS & Debian 11 / 12 ```bash -sudo apt-get -y install git python3-venv python3-virtualenv python3-dev python3-lxml libvirt-dev zlib1g-dev libxslt1-dev nginx supervisor libsasl2-modules gcc pkg-config python3-guestfs libsasl2-dev libldap2-dev libssl-dev -git clone https://github.com/retspen/webvirtcloud -cd webvirtcloud -cp webvirtcloud/settings.py.template webvirtcloud/settings.py -# now put secret key to webvirtcloud/settings.py -sudo cp conf/supervisor/webvirtcloud.conf /etc/supervisor/conf.d -sudo cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d -cd .. -sudo mv webvirtcloud /srv -sudo chown -R www-data:www-data /srv/webvirtcloud +# 1. Install system prerequisites +sudo apt-get update && sudo 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 + +# 2. Clone repository to /srv/webvirtcloud +sudo git clone https://github.com/retspen/webvirtcloud /srv/webvirtcloud cd /srv/webvirtcloud -virtualenv -p python3 venv + +# 3. Configure settings +cp webvirtcloud/settings.py.template webvirtcloud/settings.py +SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_urlsafe(50))') +sed -i "s|^SECRET_KEY = .*|SECRET_KEY = \"${SECRET_KEY}\"|" webvirtcloud/settings.py + +# 4. Deploy service configurations +sudo cp conf/supervisor/webvirtcloud.conf /etc/supervisor/conf.d/ +sudo cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/ +sudo rm -f /etc/nginx/sites-enabled/default + +# 5. Create virtual environment and install dependencies +python3 -m venv --system-site-packages venv source venv/bin/activate pip install -r conf/requirements.txt + +# 6. Database migrations and static files python3 manage.py migrate python3 manage.py collectstatic --noinput + +# 7. Set permissions and start services sudo chown -R www-data:www-data /srv/webvirtcloud -sudo rm /etc/nginx/sites-enabled/default +sudo systemctl restart nginx supervisor ``` -Restart services for running WebVirtCloud: +--- + +### RHEL 8 / 9 / 10 / Rocky Linux / AlmaLinux ```bash -sudo service nginx restart -sudo service supervisor restart -``` +# 1. Install EPEL and system prerequisites +sudo dnf -y install epel-release +sudo 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 -Setup libvirt and KVM on server +# 2. Clone repository to /srv/webvirtcloud +sudo git clone https://github.com/retspen/webvirtcloud /srv/webvirtcloud +cd /srv/webvirtcloud -```bash -wget -O - https://bit.ly/36baWUu | sudo sh -``` +# 3. Configure settings +cp webvirtcloud/settings.py.template webvirtcloud/settings.py +SECRET_KEY=$(python3 conf/runit/secret_generator.py) +sed -i "s|^SECRET_KEY = .*|SECRET_KEY = \"${SECRET_KEY}\"|" webvirtcloud/settings.py -Done!! +# 4. Create virtual environment and install dependencies +python3 -m venv --system-site-packages venv +source venv/bin/activate +pip install -r conf/requirements.txt -Go to http://serverip and you should see the login screen. +# 5. Database migrations and static files +python3 manage.py migrate +python3 manage.py collectstatic --noinput -### Install WebVirtCloud panel (RHEL Based OS 8/9/10 / Rocky Linux / AlmaLinux) +# 6. Configure Supervisor +sudo tee /etc/supervisord.d/webvirtcloud.ini > /dev/null << 'EOF' +[program:webvirtcloud] +command=/srv/webvirtcloud/venv/bin/gunicorn webvirtcloud.wsgi:application -c /srv/webvirtcloud/gunicorn.conf.py +directory=/srv/webvirtcloud +user=nginx +autostart=true +autorestart=true +redirect_stderr=true -```bash -sudo dnf -y install epel-release -sudo dnf -y install python3-devel libvirt-devel python3-libvirt python3-ldap python3-lxml cyrus-sasl-devel openldap-devel openssl-devel glibc gcc nginx supervisor git python3-libguestfs iproute-tc cyrus-sasl-md5 -``` +[program:novncd] +command=/srv/webvirtcloud/venv/bin/python3 /srv/webvirtcloud/console/novncd +directory=/srv/webvirtcloud +user=nginx +autostart=true +autorestart=true +redirect_stderr=true +EOF -#### Creating directories and cloning repository +# 7. Configure Nginx +sudo cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/ +# Ensure the default server block in /etc/nginx/nginx.conf does not conflict with webvirtcloud.conf -```bash -sudo mkdir /srv && cd /srv -sudo git clone https://github.com/retspen/webvirtcloud && cd webvirtcloud -cp webvirtcloud/settings.py.template webvirtcloud/settings.py -# now put secret key to webvirtcloud/settings.py -# create secret key manually or use that command -sudo sed -i -E 's/SECRET_KEY = .*/SECRET_KEY = "'$(python3 /srv/webvirtcloud/conf/runit/secret_generator.py)'"/' /srv/webvirtcloud/webvirtcloud/settings.py +# 8. Set permissions, SELinux, and Firewall +sudo chown -R nginx:nginx /srv/webvirtcloud +sudo semanage fcontext -a -t httpd_sys_content_t "/srv/webvirtcloud(/.*)" 2>/dev/null || true +sudo restorecon -R /srv/webvirtcloud 2>/dev/null || true +sudo setsebool -P httpd_can_network_connect on 2>/dev/null || true + +sudo firewall-cmd --add-service=http --permanent 2>/dev/null || true +sudo firewall-cmd --add-port=6080/tcp --permanent 2>/dev/null || true +sudo firewall-cmd --reload 2>/dev/null || true + +# 9. Start and enable services +sudo systemctl enable --now nginx supervisord +sudo systemctl restart nginx supervisord ``` -#### Start installation webvirtcloud +--- + +### openSUSE Leap 15.x / Tumbleweed / SLES 15 ```bash +# 1. Install system prerequisites (Python 3.11 stack and C bindings) +sudo zypper --non-interactive install -y git hostname python311 python311-base python311-devel python311-pip python311-libvirt-python python311-lxml python311-ldap libvirt-devel cyrus-sasl-devel libopenssl-devel gcc pkg-config nginx + +# Ensure python3 points to Python 3.11 +sudo ln -sf /usr/bin/python3.11 /usr/bin/python3 +sudo ln -sf /usr/bin/pip3.11 /usr/bin/pip3 + +# 2. Clone repository to /srv/webvirtcloud +sudo git clone https://github.com/retspen/webvirtcloud /srv/webvirtcloud +cd /srv/webvirtcloud + +# 3. Configure settings +cp webvirtcloud/settings.py.template webvirtcloud/settings.py +SECRET_KEY=$(python3 conf/runit/secret_generator.py) +sed -i "s|^SECRET_KEY = .*|SECRET_KEY = \"${SECRET_KEY}\"|" webvirtcloud/settings.py + +# 4. Create virtual environment and install dependencies python3 -m venv --system-site-packages venv source venv/bin/activate -pip3 install -r conf/requirements.txt -cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/ +pip install -r conf/requirements.txt + +# 5. Database migrations and static files python3 manage.py migrate python3 manage.py collectstatic --noinput + +# 6. Configure Nginx and Supervisor +sudo cp conf/nginx/suse_nginx.conf /etc/nginx/vhosts.d/webvirtcloud.conf 2>/dev/null || sudo cp conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/ +sudo chown -R nginx:nginx /srv/webvirtcloud + +# 7. Start services +sudo systemctl enable --now nginx +sudo systemctl restart nginx ``` -### Local Development Setup (Rocky Linux / RHEL / Fedora / Ubuntu) +--- + +## Local Development Setup For developers working locally on WebVirtCloud without running full production services: -#### Rocky Linux / RHEL / Fedora: +### Rocky Linux / RHEL / Fedora ```bash # 1. Install system prerequisites and precompiled bindings sudo dnf -y install python3-devel libvirt-devel python3-libvirt python3-ldap python3-lxml gcc git @@ -149,7 +212,7 @@ python manage.py migrate python manage.py runserver 0.0.0.0:8000 ``` -#### Ubuntu / Debian: +### Ubuntu / Debian ```bash # 1. Install system prerequisites sudo apt-get update && sudo apt-get -y install git python3-venv python3-dev python3-lxml python3-libvirt libvirt-dev zlib1g-dev libldap2-dev libsasl2-dev gcc pkg-config @@ -169,10 +232,11 @@ python manage.py migrate python manage.py runserver 0.0.0.0:8000 ``` -#### openSUSE Leap 15.x / Tumbleweed / SLES 15: +### openSUSE Leap 15.x / Tumbleweed / SLES 15 ```bash # 1. Install system prerequisites -sudo zypper --non-interactive install -y git python3-devel python3-pip python3-virtualenv libvirt-devel python3-libvirt python3-lxml openldap2-devel cyrus-sasl-devel libopenssl-devel libxslt-devel libxml2-devel gcc pkg-config +sudo zypper --non-interactive install -y git hostname python311 python311-base python311-devel python311-pip python311-libvirt-python python311-lxml python311-ldap libvirt-devel cyrus-sasl-devel libopenssl-devel gcc pkg-config +sudo ln -sf /usr/bin/python3.11 /usr/bin/python3 # 2. Create virtual environment with system site packages python3 -m venv --system-site-packages .venv @@ -189,231 +253,105 @@ python manage.py migrate python manage.py runserver 0.0.0.0:8000 ``` -#### Configure the supervisor for RHEL Based OS +## Compute Node (Hypervisor) Setup -Add the following after the [include] line (after **files = ...** actually): -```bash -sudo vim /etc/supervisord.conf +To configure a physical server or virtual machine as a KVM compute node to be managed by WebVirtCloud: -[program:webvirtcloud] -command=/srv/webvirtcloud/venv/bin/gunicorn webvirtcloud.wsgi:application -c /srv/webvirtcloud/gunicorn.conf.py -directory=/srv/webvirtcloud -user=nginx -autostart=true -autorestart=true -redirect_stderr=true - -[program:novncd] -command=/srv/webvirtcloud/venv/bin/python3 /srv/webvirtcloud/console/novncd -directory=/srv/webvirtcloud -user=nginx -autostart=true -autorestart=true -redirect_stderr=true -``` +### 1. Install KVM and Libvirt via Bootstrap Script -#### Edit the nginx.conf file - -You will need to edit the main nginx.conf file as the one that comes from the rpm's will not work. Comment the following lines: +WebVirtCloud includes an automated bootstrap script supporting Ubuntu 20.04/22.04/24.04, Debian 10/11/12, RHEL/Rocky/Alma 8/9/10, openSUSE Leap 15.x / Tumbleweed, and SLES 15: ```bash -# server { -# listen 80 default_server; -# listen [::]:80 default_server; -# server_name _; -# root /usr/share/nginx/html; -# -# # Load configuration files for the default server block. -# include /etc/nginx/default.d/*.conf; -# -# location / { -# } -# -# error_page 404 /404.html; -# location = /40x.html { -# } -# -# error_page 500 502 503 504 /50x.html; -# location = /50x.html { -# } -# } -} -``` +# Run bootstrap script directly via curl: +curl -fsSL https://raw.githubusercontent.com/retspen/webvirtcloud/master/dev/libvirt-bootstrap.sh | sudo sh -Also make sure file in **/etc/nginx/conf.d/webvirtcloud.conf** has the proper paths: - -```bash -upstream gunicorn_server { - #server unix:/srv/webvirtcloud/venv/wvcloud.socket fail_timeout=0; - server 127.0.0.1:8000 fail_timeout=0; -} -server { - listen 80; - - server_name servername.domain.com; - access_log /var/log/nginx/webvirtcloud-access_log; - - location /static/ { - root /srv/webvirtcloud; - expires max; - } - - location / { - proxy_pass http://gunicorn_server; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for; - proxy_set_header Host $host:$server_port; - proxy_set_header X-Forwarded-Proto $remote_addr; - proxy_connect_timeout 1800; - proxy_read_timeout 1800; - proxy_send_timeout 1800; - client_max_body_size 1024M; - } -} +# Or run locally from a cloned repository: +sudo ./dev/libvirt-bootstrap.sh ``` -Change permissions so nginx can read the webvirtcloud folder: +### 2. Configure SSH Connection Between Panel and Compute Node -```bash -sudo chown -R nginx:nginx /srv/webvirtcloud -``` - -Change permission for selinux: - -```bash -sudo semanage fcontext -a -t httpd_sys_content_t "/srv/webvirtcloud(/.*)" -sudo setsebool -P httpd_can_network_connect on -P -``` - -Add required user to the kvm group(if you not install with root): +On the WebVirtCloud panel host, generate an SSH key for the web service user (`www-data` on Debian/Ubuntu, `nginx` on RHEL/openSUSE): ```bash -sudo usermod -G kvm -a -``` - -Allow http ports on firewall: - -```bash -sudo firewall-cmd --add-service=http -sudo firewall-cmd --add-service=http --permanent -sudo firewall-cmd --add-port=6080/tcp -sudo firewall-cmd --add-port=6080/tcp --permanent -``` - -Let's restart nginx and the supervisord services: - -```bash -sudo systemctl restart nginx && systemctl restart supervisord -``` - -And finally, check everything is running: +# Generate key (Debian/Ubuntu example using www-data): +sudo -u www-data ssh-keygen -t ed25519 +sudo -u www-data tee ~www-data/.ssh/config > /dev/null << 'EOF' +Host * + StrictHostKeyChecking no +EOF +sudo chmod 600 ~www-data/.ssh/config -```bash -sudo supervisorctl status -gstfsd RUNNING pid 24662, uptime 6:01:40 -novncd RUNNING pid 24661, uptime 6:01:40 -webvirtcloud RUNNING pid 24660, uptime 6:01:40 +# Copy public key to the compute node root user: +sudo -u www-data ssh-copy-id root@ ``` -#### Apache mod_wsgi configuration - -```bash -WSGIDaemonProcess webvirtcloud threads=2 maximum-requests=1000 display-name=webvirtcloud -WSGIScriptAlias / /srv/webvirtcloud/webvirtcloud/wsgi_custom.py -``` +### 3. Install or Update `gstfsd` Daemon -#### Install final required packages for libvirtd and others on Host Server +The `gstfsd` daemon provides guest filesystem inspection and stats on hypervisors: ```bash -wget -O - https://clck.ru/9V9fH | sudo sh +curl -fsSL https://raw.githubusercontent.com/retspen/webvirtcloud/master/conf/daemon/gstfsd | sudo tee /usr/local/bin/gstfsd > /dev/null +sudo chmod +x /usr/local/bin/gstfsd +sudo systemctl restart supervisor 2>/dev/null || sudo systemctl restart supervisord ``` -Done!! - -Go to http://serverip and you should see the login screen. - -### Alternative running novncd via runit(Debian) - -Alternative to running nonvcd via supervisor is runit. +### 4. Troubleshooting: Host SMBIOS Warning -On Debian systems install runit and configure novncd service: +If you see the warning `Unsupported configuration: Host SMBIOS information is not available`, install `dmidecode` and restart libvirt: ```bash -apt install runit runit-systemd -mkdir /etc/service/novncd/ -ln -s /srv/webvirtcloud/conf/runit/novncd.sh /etc/service/novncd/run -systemctl start runit.service -``` - -### Default credentials - -```html -login: admin -password: admin -``` - -### Configuring Compute SSH connection +# Debian / Ubuntu: +sudo apt-get install -y dmidecode && sudo systemctl restart libvirtd -This is a short example of configuring cloud and compute side of the ssh connection. +# RHEL / Rocky / AlmaLinux: +sudo dnf install -y dmidecode && sudo systemctl restart libvirtd -On the webvirtcloud machine you need to generate ssh keys and optionally disable StrictHostKeyChecking. - -```bash -chown www-data -R ~www-data -sudo -u www-data ssh-keygen -cat > ~www-data/.ssh/config << EOF -Host * -StrictHostKeyChecking no -EOF -chown www-data -R ~www-data/.ssh/config +# openSUSE / SLES: +sudo zypper install -y dmidecode && sudo systemctl restart libvirtd ``` -You need to put cloud public key into authorized keys on the compute node. Simpliest way of doing this is to use ssh tool from the webvirtcloud server. +--- -```bash -sudo -u www-data ssh-copy-id root@compute1 -``` +## Configuration & Operational Notes -### Host SMBIOS information is not available +### Default Credentials -If you see warning +After initial installation, sign in to the web panel at `http://`: -```bash -Unsupported configuration: Host SMBIOS information is not available +```text +Username: admin +Password: admin ``` +> **Security Notice:** Change the default administrator password immediately after first login. -Then you need to install `dmidecode` package on your host using your package manager and restart libvirt daemon. +### Alternative: Running novncd via runit (Debian) -Debian/Ubuntu like: +As an alternative to Supervisor, Debian systems can manage `novncd` via `runit`: ```bash -sudo apt-get install dmidecode -sudo service libvirt-bin restart +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 ``` -Arch Linux - -```bash -sudo pacman -S dmidecode -systemctl restart libvirtd -``` +### Cloud-Init Datasource -### Cloud-init +WebVirtCloud can serve cloud-init metadata (root SSH keys and hostname) to guest instances: -Currently supports only root ssh authorized keys and hostname. Example configuration of the cloud-init client follows. - -```bash +```yaml datasource: OpenStack: - metadata_urls: [ "http://webvirtcloud.domain.com/datasource" ] + metadata_urls: [ "http://webvirtcloud.domain.com/datasource" ] ``` -### Reverse-Proxy +### Reverse-Proxy & Port Forwarding -Edit WS_PUBLIC_PORT at settings.py file to expose redirect to 80 or 443. Default: 6080 +If WebVirtCloud runs behind a reverse proxy terminating SSL or forwarding port 80/443, configure `WS_PUBLIC_PORT` in `webvirtcloud/settings.py` (default: 6080): -```bash -WS_PUBLIC_PORT = 80 +```python +WS_PUBLIC_PORT = 80 # or 443 ``` ## How To Update @@ -434,26 +372,34 @@ sudo service supervisor restart > 1. In `INSTALLED_APPS`, replace `'drf_yasg'` with `'drf_spectacular'` and `'drf_spectacular_sidecar'`. > 2. Ensure the `REST_FRAMEWORK` and `SPECTACULAR_SETTINGS` configuration blocks are present (see `webvirtcloud/settings.py.template`). -### Running tests +## Running Tests -Server on which tests will be performed must have libvirt up and running. -It must not contain vms. -It must have `default` storage which not contain any disk images. -It must have `default` network which must be on. -Setup venv +WebVirtCloud includes unit tests for both Django models/views and the `vrtManager` libvirt abstraction layer. The test suite uses isolated mock drivers by default and does not require a live KVM hypervisor. +### 1. Setup Virtual Environment ```bash -python -m venv venv -source venv/bin/activate +python3 -m venv .venv +source .venv/bin/activate pip install -r conf/requirements.txt +pip install -r dev/requirements.txt ``` -Run tests - +### 2. Run Test Suite ```bash +# Run Django test suite (accounts, admin, instances, logs, etc.): python manage.py test + +# Run vrtManager unit tests: +python -m unittest discover -s vrtManager/tests ``` +> **Live Hypervisor Testing (Optional):** +> To run tests against a live libvirt host instead of standalone mocks, set the `WEBVIRTCLOUD_TEST_LIBVIRT_URI` environment variable before running tests: +> ```bash +> export WEBVIRTCLOUD_TEST_LIBVIRT_URI="qemu+ssh://root@compute1/system" +> python manage.py test +> ``` + ## LDAP Configuration The config options below can be changed in `webvirtcloud/settings.py` file. Variants for Active Directory and OpenLDAP are shown. This is a minimal config to get LDAP running, for further info read the [django-auth-ldap documentation](https://django-auth-ldap.readthedocs.io). @@ -525,26 +471,29 @@ Now when you login with an LDAP user it will be assigned the rights defined. The If you'd like to move a user from ldap to WebVirtCloud, just change its password from the UI and (eventually) remove from the group in LDAP. -## REST API / BETA -Webvirtcloud provides a REST API for programmatic access. -To access API methods open your browser and check them with Swagger interface -```bash -http:///swagger -``` -```bash -http:///redoc -``` +## REST API (OpenAPI 3.0) + +WebVirtCloud provides a REST API powered by Django REST Framework and documented via `drf-spectacular`. + +You can access the interactive API documentation and schema endpoints in your browser: + +* **Swagger UI:** `http:///swagger/` +* **ReDoc UI:** `http:///redoc/` +* **OpenAPI 3.0 Schema:** `http:///api/schema/` (download schema in JSON or YAML format) ## Screenshots -Instance Detail: - -Instance List:
- - -Other:
- - +| Instance Detail | +|:---:| +| ![Instance Detail](doc/images/instance.PNG) | + +| Grouped Instances | Non-Grouped Instances | +|:---:|:---:| +| ![Grouped Instances](doc/images/grouped.PNG) | ![Non-Grouped Instances](doc/images/nongrouped.PNG) | + +| Compute Hosts | Activity Log | +|:---:|:---:| +| ![Compute Hosts](doc/images/hosts.PNG) | ![Activity Log](doc/images/log.PNG) | ## License From b9cec4a6d3b8e21f37620a17f328e37ffc805dfe Mon Sep 17 00:00:00 2001 From: catborise Date: Fri, 25 Sep 2026 21:45:13 +0300 Subject: [PATCH 04/10] chore: purge obsolete .travis.yml and clean stale drf-yasg paths in staticfiles.json - Remove legacy .travis.yml since continuous integration runs on GitHub Actions - Remove 34 obsolete drf-yasg file mappings from static/staticfiles.json - Recompute staticfiles manifest hash to match current static asset inventory --- .travis.yml | 17 ----------------- static/staticfiles.json | 2 +- 2 files changed, 1 insertion(+), 18 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ca7cd721..00000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ ---- -language: python -python: - - "3.9" -env: - - DJANGO=4.2.4 -install: - - pip install -r dev/requirements.txt -script: - - pep8 --ignore=E501 vrtManager accounts admin appsettings \ - computesconsole create datasource instances \ - interfaceslogs networks nwfilters storages \ - virtsecrets - - pyflakes vrtManager accounts admin appsettings computes console create datasource \ - instances interfaces logs networks nwfilters storages virtsecrets - - python manage.py migrate - - python manage.py test --settings=webvirtcloud.settings-dev diff --git a/static/staticfiles.json b/static/staticfiles.json index 701669db..9e912a49 100644 --- a/static/staticfiles.json +++ b/static/staticfiles.json @@ -1 +1 @@ -{"paths": {"rest_framework/docs/css/base.css": "rest_framework/docs/css/base.e630f8f4990e.css", "rest_framework/docs/css/jquery.json-view.min.css": "rest_framework/docs/css/jquery.json-view.min.a2e6beeb6710.css", "rest_framework/docs/css/highlight.css": "rest_framework/docs/css/highlight.e0e4d973c6d7.css", "rest_framework/docs/js/api.js": "rest_framework/docs/js/api.18a5ba8a1bd8.js", "rest_framework/docs/js/jquery.json-view.min.js": "rest_framework/docs/js/jquery.json-view.min.b7c2d6981377.js", "rest_framework/docs/js/highlight.pack.js": "rest_framework/docs/js/highlight.pack.479b5f21dcba.js", "rest_framework/docs/img/favicon.ico": "rest_framework/docs/img/favicon.5195b4d0f3eb.ico", "rest_framework/docs/img/grid.png": "rest_framework/docs/img/grid.a4b938cf382b.png", "bootstrap_icons/css/bootstrap_icons.css": "bootstrap_icons/css/bootstrap_icons.0e17d6a6e498.css", "drf-yasg/swagger-ui-dist/absolute-path.js": "drf-yasg/swagger-ui-dist/absolute-path.7ca5ebff3b35.js", "drf-yasg/swagger-ui-dist/favicon-32x32.png": "drf-yasg/swagger-ui-dist/favicon-32x32.40d4f2c38d1c.png", "drf-yasg/swagger-ui-dist/swagger-ui.css": "drf-yasg/swagger-ui-dist/swagger-ui.776bdd918354.css", "drf-yasg/swagger-ui-dist/index.css": "drf-yasg/swagger-ui-dist/index.54fdd628e489.css", "drf-yasg/swagger-ui-dist/LICENSE": "drf-yasg/swagger-ui-dist/LICENSE.3b83ef96387f", "drf-yasg/swagger-ui-dist/swagger-ui-bundle.js.map": "drf-yasg/swagger-ui-dist/swagger-ui-bundle.js.f5222861035c.map", "drf-yasg/swagger-ui-dist/index.js": "drf-yasg/swagger-ui-dist/index.4843f77ccf9e.js", "drf-yasg/swagger-ui-dist/swagger-ui.js.map": "drf-yasg/swagger-ui-dist/swagger-ui.js.804e9522fc74.map", "drf-yasg/swagger-ui-dist/oauth2-redirect.html": "drf-yasg/swagger-ui-dist/oauth2-redirect.3ab4f43d18d7.html", "drf-yasg/swagger-ui-dist/swagger-ui-es-bundle.js.map": "drf-yasg/swagger-ui-dist/swagger-ui-es-bundle.js.edde1f87cee4.map", "drf-yasg/swagger-ui-dist/swagger-ui-standalone-preset.js": "drf-yasg/swagger-ui-dist/swagger-ui-standalone-preset.4d7f4447551a.js", "drf-yasg/swagger-ui-dist/swagger-ui-es-bundle-core.js": "drf-yasg/swagger-ui-dist/swagger-ui-es-bundle-core.002e814c385e.js", "drf-yasg/swagger-ui-dist/swagger-initializer.js": "drf-yasg/swagger-ui-dist/swagger-initializer.ff995915f51c.js", "drf-yasg/swagger-ui-dist/swagger-ui-es-bundle.js": "drf-yasg/swagger-ui-dist/swagger-ui-es-bundle.9e91a94497b1.js", "drf-yasg/swagger-ui-dist/swagger-ui-es-bundle-core.js.map": "drf-yasg/swagger-ui-dist/swagger-ui-es-bundle-core.js.b1d6e307bf5a.map", "drf-yasg/swagger-ui-dist/swagger-ui-standalone-preset.js.map": "drf-yasg/swagger-ui-dist/swagger-ui-standalone-preset.js.c470a4c82080.map", "drf-yasg/swagger-ui-dist/NOTICE": "drf-yasg/swagger-ui-dist/NOTICE.342625133694", "drf-yasg/swagger-ui-dist/swagger-ui-bundle.js": "drf-yasg/swagger-ui-dist/swagger-ui-bundle.357151587590.js", "drf-yasg/swagger-ui-dist/swagger-ui.css.map": "drf-yasg/swagger-ui-dist/swagger-ui.css.fea025523c25.map", "drf-yasg/redoc-old/redoc.min.js.map": "drf-yasg/redoc-old/redoc.min.js.8b046eaab501.map", "drf-yasg/redoc-old/LICENSE": "drf-yasg/redoc-old/LICENSE.e4e5f59c85dc", "drf-yasg/redoc-old/redoc.min.js": "drf-yasg/redoc-old/redoc.min.75500581cb08.js", "drf-yasg/redoc/LICENSE": "drf-yasg/redoc/LICENSE.cf2d48dc6713", "drf-yasg/redoc/redoc.min.js": "drf-yasg/redoc/redoc.min.71d0b1197fcc.js", "drf-yasg/redoc/redoc.standalone.js.map": "drf-yasg/redoc/redoc.standalone.js.be0619dcd088.map", "drf-yasg/redoc/redoc-logo.png": "drf-yasg/redoc/redoc-logo.c7dc7712ce68.png", "rest_framework/css/font-awesome-4.0.3.css": "rest_framework/css/font-awesome-4.0.3.c1e1ea213abf.css", "rest_framework/css/bootstrap-theme.min.css.map": "rest_framework/css/bootstrap-theme.min.css.51806092cc05.map", "rest_framework/css/default.css": "rest_framework/css/default.789dfb5732d7.css", "rest_framework/css/bootstrap.min.css.map": "rest_framework/css/bootstrap.min.css.cafbda9c0e9e.map", "rest_framework/css/prettify.css": "rest_framework/css/prettify.a987f72342ee.css", "rest_framework/css/bootstrap.min.css": "rest_framework/css/bootstrap.min.f17d4516b026.css", "rest_framework/css/bootstrap-tweaks.css": "rest_framework/css/bootstrap-tweaks.46ed116b0edd.css", "rest_framework/css/bootstrap-theme.min.css": "rest_framework/css/bootstrap-theme.min.1d4b05b397c3.css", "rest_framework/fonts/glyphicons-halflings-regular.svg": "rest_framework/fonts/glyphicons-halflings-regular.08eda92397ae.svg", "rest_framework/fonts/glyphicons-halflings-regular.woff": "rest_framework/fonts/glyphicons-halflings-regular.fa2772327f55.woff", "rest_framework/fonts/fontawesome-webfont.woff": "rest_framework/fonts/fontawesome-webfont.3293616ec0c6.woff", "rest_framework/fonts/glyphicons-halflings-regular.ttf": "rest_framework/fonts/glyphicons-halflings-regular.e18bbf611f2a.ttf", "rest_framework/fonts/glyphicons-halflings-regular.eot": "rest_framework/fonts/glyphicons-halflings-regular.f4769f9bdb74.eot", "rest_framework/fonts/fontawesome-webfont.ttf": "rest_framework/fonts/fontawesome-webfont.dcb26c7239d8.ttf", "rest_framework/fonts/glyphicons-halflings-regular.woff2": "rest_framework/fonts/glyphicons-halflings-regular.448c34a56d69.woff2", "rest_framework/fonts/fontawesome-webfont.svg": "rest_framework/fonts/fontawesome-webfont.83e37a11f9d7.svg", "rest_framework/fonts/fontawesome-webfont.eot": "rest_framework/fonts/fontawesome-webfont.8b27bc96115c.eot", "rest_framework/js/default.js": "rest_framework/js/default.5b08897dbdc3.js", "rest_framework/js/jquery-3.5.1.min.js": "rest_framework/js/jquery-3.5.1.min.dc5e7f18c8d3.js", "rest_framework/js/csrf.js": "rest_framework/js/csrf.969930007329.js", "rest_framework/js/bootstrap.min.js": "rest_framework/js/bootstrap.min.2f34b630ffe3.js", "rest_framework/js/ajax-form.js": "rest_framework/js/ajax-form.0ea6e6052ab5.js", "rest_framework/js/prettify-min.js": "rest_framework/js/prettify-min.709bfcc456c6.js", "rest_framework/js/coreapi-0.1.1.js": "rest_framework/js/coreapi-0.1.1.e580e3854595.js", "rest_framework/img/glyphicons-halflings-white.png": "rest_framework/img/glyphicons-halflings-white.9bbc6e960299.png", "rest_framework/img/grid.png": "rest_framework/img/grid.a4b938cf382b.png", "rest_framework/img/glyphicons-halflings.png": "rest_framework/img/glyphicons-halflings.90233c9067e9.png", "drf-yasg/immutable.js": "drf-yasg/immutable.37fd83058fde.js", "drf-yasg/insQ.min.js": "drf-yasg/insQ.min.90ab21607447.js", "drf-yasg/insQ.js": "drf-yasg/insQ.d4a1933caf20.js", "drf-yasg/style.css": "drf-yasg/style.680c08b2b7b4.css", "drf-yasg/immutable.min.js": "drf-yasg/immutable.min.d985bc61d85c.js", "drf-yasg/swagger-ui-init.js": "drf-yasg/swagger-ui-init.7d9c695107e5.js", "drf-yasg/redoc-init.js": "drf-yasg/redoc-init.41348b1afc50.js", "drf-yasg/README": "drf-yasg/README.723ffa086d8b"}, "version": "1.1", "hash": "8b0f0062f49f"} \ No newline at end of file +{"paths": {"rest_framework/docs/css/base.css": "rest_framework/docs/css/base.e630f8f4990e.css", "rest_framework/docs/css/jquery.json-view.min.css": "rest_framework/docs/css/jquery.json-view.min.a2e6beeb6710.css", "rest_framework/docs/css/highlight.css": "rest_framework/docs/css/highlight.e0e4d973c6d7.css", "rest_framework/docs/js/api.js": "rest_framework/docs/js/api.18a5ba8a1bd8.js", "rest_framework/docs/js/jquery.json-view.min.js": "rest_framework/docs/js/jquery.json-view.min.b7c2d6981377.js", "rest_framework/docs/js/highlight.pack.js": "rest_framework/docs/js/highlight.pack.479b5f21dcba.js", "rest_framework/docs/img/favicon.ico": "rest_framework/docs/img/favicon.5195b4d0f3eb.ico", "rest_framework/docs/img/grid.png": "rest_framework/docs/img/grid.a4b938cf382b.png", "bootstrap_icons/css/bootstrap_icons.css": "bootstrap_icons/css/bootstrap_icons.0e17d6a6e498.css", "rest_framework/css/font-awesome-4.0.3.css": "rest_framework/css/font-awesome-4.0.3.c1e1ea213abf.css", "rest_framework/css/bootstrap-theme.min.css.map": "rest_framework/css/bootstrap-theme.min.css.51806092cc05.map", "rest_framework/css/default.css": "rest_framework/css/default.789dfb5732d7.css", "rest_framework/css/bootstrap.min.css.map": "rest_framework/css/bootstrap.min.css.cafbda9c0e9e.map", "rest_framework/css/prettify.css": "rest_framework/css/prettify.a987f72342ee.css", "rest_framework/css/bootstrap.min.css": "rest_framework/css/bootstrap.min.f17d4516b026.css", "rest_framework/css/bootstrap-tweaks.css": "rest_framework/css/bootstrap-tweaks.46ed116b0edd.css", "rest_framework/css/bootstrap-theme.min.css": "rest_framework/css/bootstrap-theme.min.1d4b05b397c3.css", "rest_framework/fonts/glyphicons-halflings-regular.svg": "rest_framework/fonts/glyphicons-halflings-regular.08eda92397ae.svg", "rest_framework/fonts/glyphicons-halflings-regular.woff": "rest_framework/fonts/glyphicons-halflings-regular.fa2772327f55.woff", "rest_framework/fonts/fontawesome-webfont.woff": "rest_framework/fonts/fontawesome-webfont.3293616ec0c6.woff", "rest_framework/fonts/glyphicons-halflings-regular.ttf": "rest_framework/fonts/glyphicons-halflings-regular.e18bbf611f2a.ttf", "rest_framework/fonts/glyphicons-halflings-regular.eot": "rest_framework/fonts/glyphicons-halflings-regular.f4769f9bdb74.eot", "rest_framework/fonts/fontawesome-webfont.ttf": "rest_framework/fonts/fontawesome-webfont.dcb26c7239d8.ttf", "rest_framework/fonts/glyphicons-halflings-regular.woff2": "rest_framework/fonts/glyphicons-halflings-regular.448c34a56d69.woff2", "rest_framework/fonts/fontawesome-webfont.svg": "rest_framework/fonts/fontawesome-webfont.83e37a11f9d7.svg", "rest_framework/fonts/fontawesome-webfont.eot": "rest_framework/fonts/fontawesome-webfont.8b27bc96115c.eot", "rest_framework/js/default.js": "rest_framework/js/default.5b08897dbdc3.js", "rest_framework/js/jquery-3.5.1.min.js": "rest_framework/js/jquery-3.5.1.min.dc5e7f18c8d3.js", "rest_framework/js/csrf.js": "rest_framework/js/csrf.969930007329.js", "rest_framework/js/bootstrap.min.js": "rest_framework/js/bootstrap.min.2f34b630ffe3.js", "rest_framework/js/ajax-form.js": "rest_framework/js/ajax-form.0ea6e6052ab5.js", "rest_framework/js/prettify-min.js": "rest_framework/js/prettify-min.709bfcc456c6.js", "rest_framework/js/coreapi-0.1.1.js": "rest_framework/js/coreapi-0.1.1.e580e3854595.js", "rest_framework/img/glyphicons-halflings-white.png": "rest_framework/img/glyphicons-halflings-white.9bbc6e960299.png", "rest_framework/img/grid.png": "rest_framework/img/grid.a4b938cf382b.png", "rest_framework/img/glyphicons-halflings.png": "rest_framework/img/glyphicons-halflings.90233c9067e9.png"}, "version": "1.1", "hash": "29067a7b51f7"} \ No newline at end of file From 56fdd07802bbeb95d83d80de461d3d634b93e2fe Mon Sep 17 00:00:00 2001 From: catborise Date: Fri, 25 Sep 2026 21:48:38 +0300 Subject: [PATCH 05/10] ci: add GitHub Actions test suite workflow for Python 3.10, 3.11, and 3.12 - Configure matrix testing across Python 3.10, 3.11, and 3.12 on ubuntu-latest - Install system libvirt, ldap, and C build dependencies with pip cache - Run Django database migrations and test suite - Run vrtManager unit test discovery with test_*.py pattern - Fix vrtManager unittest discovery command in README.md --- .github/workflows/test.yml | 66 ++++++++++++++++++++++++++++++++++++++ README.md | 2 +- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..8e71d944 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,66 @@ +name: Test Suite + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test: + name: Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.10', '3.11', '3.12'] + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: 'conf/requirements.txt' + + - name: Install System Dependencies + run: | + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends \ + libvirt-dev \ + libldap2-dev \ + libsasl2-dev \ + python3-lxml \ + zlib1g-dev \ + libxslt1-dev \ + pkg-config \ + gcc \ + libssl-dev \ + libxml2-dev + + - name: Install Python Dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + pip install -r conf/requirements.txt + if [ -f dev/requirements.txt ]; then pip install -r dev/requirements.txt; fi + + - name: Configure WebVirtCloud Settings + run: | + cp webvirtcloud/settings.py.template webvirtcloud/settings.py + SECRET_KEY=$(python -c 'import secrets; print(secrets.token_urlsafe(50))') + sed -i "s|^SECRET_KEY = .*|SECRET_KEY = \"${SECRET_KEY}\"|" webvirtcloud/settings.py + + - name: Run Django Migrations + run: | + python manage.py migrate --noinput + + - name: Run Django Test Suite + run: | + python manage.py test + + - name: Run vrtManager Unit Tests + run: | + python -m unittest discover -s vrtManager -p "test_*.py" diff --git a/README.md b/README.md index a22b8f9a..36f876ab 100644 --- a/README.md +++ b/README.md @@ -390,7 +390,7 @@ pip install -r dev/requirements.txt python manage.py test # Run vrtManager unit tests: -python -m unittest discover -s vrtManager/tests +python -m unittest discover -s vrtManager -p "test_*.py" ``` > **Live Hypervisor Testing (Optional):** From b1a843e846f5befa8ffc64496a985d68da130614 Mon Sep 17 00:00:00 2001 From: catborise Date: Fri, 25 Sep 2026 21:58:19 +0300 Subject: [PATCH 06/10] feat(docker): modernize Dockerfile, add runit init script, and provide docker-compose setup - Add python3-libvirt to apt and configure --system-site-packages for zero-compilation build - Introduce 10_webvirtcloud_init.sh to handle container startup, migrations, and SQLite persistence - Exclude host settings.py in .dockerignore for clean builds from settings.py.template - Add docker-compose.yml with persistent data and SSH volume mounts - Document Docker Compose deployment in README.md --- .dockerignore | 1 + Dockerfile | 42 +++++++++++++++++---------- README.md | 15 ++++++++++ conf/runit/10_webvirtcloud_init.sh | 46 ++++++++++++++++++++++++++++++ docker-compose.yml | 28 ++++++++++++++++++ 5 files changed, 117 insertions(+), 15 deletions(-) create mode 100755 conf/runit/10_webvirtcloud_init.sh create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore index 05e5f58c..dcc01a87 100644 --- a/.dockerignore +++ b/.dockerignore @@ -21,6 +21,7 @@ **/*.log **/console/cert.pem* **/dhcpd.* +webvirtcloud/settings.py # IDEs and OS **/.idea diff --git a/Dockerfile b/Dockerfile index 28a568e4..d1ce88df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,6 @@ EXPOSE 6080 # Use baseimage-docker's init system. CMD ["/sbin/my_init"] - RUN echo 'APT::Get::Clean=always;' >> /etc/apt/apt.conf.d/99AutomaticClean RUN apt-get update -qqy \ @@ -14,8 +13,10 @@ RUN apt-get update -qqy \ --no-install-recommends \ git \ python3-venv \ + python3-pip \ python3-dev \ python3-lxml \ + python3-libvirt \ libvirt-dev \ zlib1g-dev \ nginx \ @@ -30,42 +31,53 @@ RUN apt-get update -qqy \ # Setup webvirtcloud WORKDIR /srv/webvirtcloud -# Install Python dependencies first to leverage Docker layer caching +# Install Python dependencies first with system-site-packages to leverage prebuilt bindings COPY conf/requirements.txt conf/requirements.txt -RUN python3 -m venv venv && \ +RUN python3 -m venv --system-site-packages venv && \ . venv/bin/activate && \ - pip3 install -U pip && \ - pip3 install wheel && \ + pip3 install -U pip wheel && \ pip3 install -r conf/requirements.txt && \ pip3 cache purge # Copy application source COPY . /srv/webvirtcloud +# Setup build-time settings, run collectstatic, and set permissions RUN . venv/bin/activate && \ - python3 manage.py makemigrations && \ - python3 manage.py migrate && \ + if [ ! -f webvirtcloud/settings.py ]; then \ + cp webvirtcloud/settings.py.template webvirtcloud/settings.py && \ + python3 -c 'import secrets; print(secrets.token_urlsafe(50))' > /tmp/secret_key && \ + sed -i "s|^SECRET_KEY = .*|SECRET_KEY = \"$(cat /tmp/secret_key)\"|" webvirtcloud/settings.py && \ + rm -f /tmp/secret_key; \ + fi && \ python3 manage.py collectstatic --noinput && \ chown -R www-data:www-data /srv/webvirtcloud # Setup Nginx RUN printf "\n%s" "daemon off;" >> /etc/nginx/nginx.conf && \ - rm /etc/nginx/sites-enabled/default && \ + rm -f /etc/nginx/sites-enabled/default && \ chown -R www-data:www-data /var/lib/nginx COPY conf/nginx/webvirtcloud.conf /etc/nginx/conf.d/ -# Register services to runit -RUN mkdir /etc/service/nginx && \ - mkdir /etc/service/nginx-log-forwarder && \ - mkdir /etc/service/webvirtcloud && \ - mkdir /etc/service/novnc +# Register startup init script and services to runit +RUN mkdir -p /etc/my_init.d \ + /etc/service/nginx \ + /etc/service/nginx-log-forwarder \ + /etc/service/webvirtcloud \ + /etc/service/novnc +COPY conf/runit/10_webvirtcloud_init.sh /etc/my_init.d/10_webvirtcloud_init.sh COPY conf/runit/nginx /etc/service/nginx/run COPY conf/runit/nginx-log-forwarder /etc/service/nginx-log-forwarder/run COPY conf/runit/novncd.sh /etc/service/novnc/run COPY conf/runit/webvirtcloud.sh /etc/service/webvirtcloud/run +RUN chmod +x /etc/my_init.d/10_webvirtcloud_init.sh \ + /etc/service/nginx/run \ + /etc/service/nginx-log-forwarder/run \ + /etc/service/novnc/run \ + /etc/service/webvirtcloud/run -# Define mountable directories. -#VOLUME [] +# Declare mountable data directory for persistent SQLite and SSH keys +VOLUME ["/srv/webvirtcloud/data", "/var/www/.ssh"] WORKDIR /srv/webvirtcloud diff --git a/README.md b/README.md index 36f876ab..7cfdfc79 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,21 @@ chmod 744 install.sh ./install.sh ``` +## Docker Deployment (Docker Compose) + +Run WebVirtCloud in a container with persistent volumes for data and SSH keys: + +```bash +# 1. Clone repository: +git clone https://github.com/retspen/webvirtcloud +cd webvirtcloud + +# 2. Start services: +docker compose up -d +``` + +Access the panel at `http://` and noVNC console at port `6080`. + ## Manual Installation ### Generate secret key diff --git a/conf/runit/10_webvirtcloud_init.sh b/conf/runit/10_webvirtcloud_init.sh new file mode 100755 index 00000000..9502525e --- /dev/null +++ b/conf/runit/10_webvirtcloud_init.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -e + +APP_DIR="/srv/webvirtcloud" +DATA_DIR="$APP_DIR/data" +mkdir -p "$DATA_DIR" "/var/www/.ssh" + +# If settings.py doesn't exist, generate from template +if [ ! -f "$APP_DIR/webvirtcloud/settings.py" ]; then + echo "* Generating webvirtcloud/settings.py from template..." + cp "$APP_DIR/webvirtcloud/settings.py.template" "$APP_DIR/webvirtcloud/settings.py" +fi + +# Set SECRET_KEY if placeholder exists +if grep -q 'SECRET_KEY = ""' "$APP_DIR/webvirtcloud/settings.py" || grep -q "SECRET_KEY = ''" "$APP_DIR/webvirtcloud/settings.py"; then + KEY="${SECRET_KEY:-$("$APP_DIR/venv/bin/python3" -c 'import secrets; print(secrets.token_urlsafe(50))')}" + sed -i "s|^SECRET_KEY = .*|SECRET_KEY = \"${KEY}\"|" "$APP_DIR/webvirtcloud/settings.py" +fi + +# Configure CSRF_TRUSTED_ORIGINS if environment variable is set +if [ -n "$CSRF_TRUSTED_ORIGINS" ]; then + echo "* Setting CSRF_TRUSTED_ORIGINS from environment..." + origins="" + IFS=',' read -ra ADDR <<< "$CSRF_TRUSTED_ORIGINS" + for o in "${ADDR[@]}"; do + clean_o=$(echo "$o" | xargs) + [ -n "$clean_o" ] && origins="${origins}'${clean_o}', " + done + sed -i "s|^CSRF_TRUSTED_ORIGINS = .*|CSRF_TRUSTED_ORIGINS = [ ${origins} ]|" "$APP_DIR/webvirtcloud/settings.py" +fi + +# Persist SQLite database in DATA_DIR +if [ ! -f "$DATA_DIR/db.sqlite3" ] && [ -f "$APP_DIR/db.sqlite3" ] && [ ! -L "$APP_DIR/db.sqlite3" ]; then + mv "$APP_DIR/db.sqlite3" "$DATA_DIR/db.sqlite3" +fi +touch "$DATA_DIR/db.sqlite3" +ln -sf "$DATA_DIR/db.sqlite3" "$APP_DIR/db.sqlite3" + +# Run database migrations +echo "* Running database migrations..." +"$APP_DIR/venv/bin/python3" "$APP_DIR/manage.py" migrate --noinput + +# Set proper permissions on runtime and data directories +chown -R www-data:www-data "$DATA_DIR" "/var/www/.ssh" +chown www-data:www-data "$APP_DIR/webvirtcloud/settings.py" 2>/dev/null || true +chmod 700 "/var/www/.ssh" 2>/dev/null || true diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..0cc0764c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +services: + webvirtcloud: + build: + context: . + dockerfile: Dockerfile + image: webvirtcloud:latest + container_name: webvirtcloud + restart: unless-stopped + ports: + - "80:80" + - "6080:6080" + environment: + # Optional: custom Django secret key. If unset, automatically generated at first launch. + # - SECRET_KEY=your-custom-secret-key-here + # Optional: comma-separated trusted CSRF origins (e.g. http://wvc.example.com,https://wvc.example.com) + # - CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1 + - TZ=UTC + volumes: + # Persistent SQLite database and runtime data + - webvirtcloud-data:/srv/webvirtcloud/data + # Persistent SSH keys for compute node authentication + - webvirtcloud-ssh:/var/www/.ssh + +volumes: + webvirtcloud-data: + driver: local + webvirtcloud-ssh: + driver: local From 7435d664920209da5a7529d551df93a29fd857e6 Mon Sep 17 00:00:00 2001 From: catborise Date: Fri, 25 Sep 2026 22:06:16 +0300 Subject: [PATCH 07/10] fix(security): resolve reviewer findings on secret key baking, host symlinks, and injection safety - Prevent baking SECRET_KEY into Docker image during collectstatic - Persist runtime SECRET_KEY in data/secret_key volume or read from environment - Remove sed-based injection of CSRF_TRUSTED_ORIGINS in favor of native os.getenv - Avoid overwriting system python3 symlink on SUSE; auto-detect python 3.10+ binaries - Make /etc/hosts entry in installer idempotent - Add security notice regarding compute node VNC/SPICE port firewall isolation --- Dockerfile | 12 +++------ README.md | 16 ++++++------ conf/runit/10_webvirtcloud_init.sh | 23 ++++++----------- webvirtcloud.sh | 40 ++++++++++++------------------ webvirtcloud/settings.py.template | 9 ++++++- 5 files changed, 42 insertions(+), 58 deletions(-) diff --git a/Dockerfile b/Dockerfile index d1ce88df..6d9f1fab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,15 +42,11 @@ RUN python3 -m venv --system-site-packages venv && \ # Copy application source COPY . /srv/webvirtcloud -# Setup build-time settings, run collectstatic, and set permissions +# Run collectstatic with temporary dummy key, then remove temporary settings file RUN . venv/bin/activate && \ - if [ ! -f webvirtcloud/settings.py ]; then \ - cp webvirtcloud/settings.py.template webvirtcloud/settings.py && \ - python3 -c 'import secrets; print(secrets.token_urlsafe(50))' > /tmp/secret_key && \ - sed -i "s|^SECRET_KEY = .*|SECRET_KEY = \"$(cat /tmp/secret_key)\"|" webvirtcloud/settings.py && \ - rm -f /tmp/secret_key; \ - fi && \ - python3 manage.py collectstatic --noinput && \ + cp webvirtcloud/settings.py.template webvirtcloud/settings.py && \ + SECRET_KEY="build-dummy-key-only-for-collectstatic" python3 manage.py collectstatic --noinput && \ + rm -f webvirtcloud/settings.py && \ chown -R www-data:www-data /srv/webvirtcloud # Setup Nginx diff --git a/README.md b/README.md index 7cfdfc79..22c81c92 100644 --- a/README.md +++ b/README.md @@ -170,21 +170,17 @@ sudo systemctl restart nginx supervisord # 1. Install system prerequisites (Python 3.11 stack and C bindings) sudo zypper --non-interactive install -y git hostname python311 python311-base python311-devel python311-pip python311-libvirt-python python311-lxml python311-ldap libvirt-devel cyrus-sasl-devel libopenssl-devel gcc pkg-config nginx -# Ensure python3 points to Python 3.11 -sudo ln -sf /usr/bin/python3.11 /usr/bin/python3 -sudo ln -sf /usr/bin/pip3.11 /usr/bin/pip3 - # 2. Clone repository to /srv/webvirtcloud sudo git clone https://github.com/retspen/webvirtcloud /srv/webvirtcloud cd /srv/webvirtcloud # 3. Configure settings cp webvirtcloud/settings.py.template webvirtcloud/settings.py -SECRET_KEY=$(python3 conf/runit/secret_generator.py) +SECRET_KEY=$(python3.11 conf/runit/secret_generator.py) sed -i "s|^SECRET_KEY = .*|SECRET_KEY = \"${SECRET_KEY}\"|" webvirtcloud/settings.py # 4. Create virtual environment and install dependencies -python3 -m venv --system-site-packages venv +python3.11 -m venv --system-site-packages venv source venv/bin/activate pip install -r conf/requirements.txt @@ -251,10 +247,9 @@ python manage.py runserver 0.0.0.0:8000 ```bash # 1. Install system prerequisites sudo zypper --non-interactive install -y git hostname python311 python311-base python311-devel python311-pip python311-libvirt-python python311-lxml python311-ldap libvirt-devel cyrus-sasl-devel libopenssl-devel gcc pkg-config -sudo ln -sf /usr/bin/python3.11 /usr/bin/python3 # 2. Create virtual environment with system site packages -python3 -m venv --system-site-packages .venv +python3.11 -m venv --system-site-packages .venv source .venv/bin/activate # 3. Install Python dependencies @@ -263,7 +258,7 @@ pip install -r dev/requirements.txt # 4. Initialize configuration and run local dev server cp webvirtcloud/settings.py.template webvirtcloud/settings.py -sed -i -E 's/SECRET_KEY = .*/SECRET_KEY = "'$(python3 conf/runit/secret_generator.py)'"/' webvirtcloud/settings.py +sed -i -E 's/SECRET_KEY = .*/SECRET_KEY = "'$(python3.11 conf/runit/secret_generator.py)'"/' webvirtcloud/settings.py python manage.py migrate python manage.py runserver 0.0.0.0:8000 ``` @@ -326,6 +321,9 @@ sudo dnf install -y dmidecode && sudo systemctl restart libvirtd sudo zypper install -y dmidecode && sudo systemctl restart libvirtd ``` +> **Security Notice (Compute Node Firewall):** +> Libvirt compute nodes listen on VNC/SPICE ports (`5900`–`65535`) to allow WebVirtCloud to proxy graphical consoles. Ensure your firewall (`ufw`, `firewalld`, or `iptables`) restricts these ports to accept connections **only** from the WebVirtCloud panel IP, and never exposes them directly to public networks. + --- ## Configuration & Operational Notes diff --git a/conf/runit/10_webvirtcloud_init.sh b/conf/runit/10_webvirtcloud_init.sh index 9502525e..d44e1d5a 100755 --- a/conf/runit/10_webvirtcloud_init.sh +++ b/conf/runit/10_webvirtcloud_init.sh @@ -11,22 +11,13 @@ if [ ! -f "$APP_DIR/webvirtcloud/settings.py" ]; then cp "$APP_DIR/webvirtcloud/settings.py.template" "$APP_DIR/webvirtcloud/settings.py" fi -# Set SECRET_KEY if placeholder exists -if grep -q 'SECRET_KEY = ""' "$APP_DIR/webvirtcloud/settings.py" || grep -q "SECRET_KEY = ''" "$APP_DIR/webvirtcloud/settings.py"; then - KEY="${SECRET_KEY:-$("$APP_DIR/venv/bin/python3" -c 'import secrets; print(secrets.token_urlsafe(50))')}" - sed -i "s|^SECRET_KEY = .*|SECRET_KEY = \"${KEY}\"|" "$APP_DIR/webvirtcloud/settings.py" -fi - -# Configure CSRF_TRUSTED_ORIGINS if environment variable is set -if [ -n "$CSRF_TRUSTED_ORIGINS" ]; then - echo "* Setting CSRF_TRUSTED_ORIGINS from environment..." - origins="" - IFS=',' read -ra ADDR <<< "$CSRF_TRUSTED_ORIGINS" - for o in "${ADDR[@]}"; do - clean_o=$(echo "$o" | xargs) - [ -n "$clean_o" ] && origins="${origins}'${clean_o}', " - done - sed -i "s|^CSRF_TRUSTED_ORIGINS = .*|CSRF_TRUSTED_ORIGINS = [ ${origins} ]|" "$APP_DIR/webvirtcloud/settings.py" +# Ensure SECRET_KEY is persisted across container restarts if not supplied via environment +if [ -z "$SECRET_KEY" ]; then + if [ ! -s "$DATA_DIR/secret_key" ]; then + echo "* Generating fresh random SECRET_KEY..." + "$APP_DIR/venv/bin/python3" -c 'import secrets; print(secrets.token_urlsafe(50))' > "$DATA_DIR/secret_key" + chmod 600 "$DATA_DIR/secret_key" + fi fi # Persist SQLite database in DATA_DIR diff --git a/webvirtcloud.sh b/webvirtcloud.sh index 2104c30c..60c792ee 100755 --- a/webvirtcloud.sh +++ b/webvirtcloud.sh @@ -71,7 +71,7 @@ readonly APP_REPO_URL="${APP_REPO_URL:-https://github.com/retspen/webvirtcloud.g readonly APP_NAME="webvirtcloud" readonly APP_PATH="/srv/$APP_NAME" -readonly PYTHON="python3" +PYTHON="python3" progress () { spin[0]="-" @@ -238,26 +238,26 @@ run_as_app_user () { } check_python () { + # dynamically find python >= 3.10 if default python3 is older + for py_bin in python3.11 python3.12 python3.13 python3.10 python3; do + if command -v "$py_bin" >/dev/null 2>&1; then + if "$py_bin" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)' >/dev/null 2>&1; then + PYTHON="$py_bin" + break + fi + fi + done + # check if python3 is installed. if ! hash "$PYTHON" 2>/dev/null; then echo "Python3 is not installed. Please install Python3 and try again." exit 1 fi - # check if python3 version is grater than 3.10 amd set it as default + # check if python3 version is greater than 3.10 if ! "$PYTHON" -c 'import sys; assert sys.version_info >= (3, 10)' >/dev/null 2>&1; then echo "Your Python version is less than 3.10. This script requires Python 3.10 or greater." - echo "Please install Python 3.10 or greater and set it as the default version." - echo "Use update-alternatives command to set default python version to latest." - echo "For example: sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.10 1" - echo "Then run this script again." - echo "Do not forget to install pip3 and python3-devel for python3.10 or later." - exit 1 - fi - - # check if pip3 is installed - if ! hash pip3 2>/dev/null; then - echo "pip3 is not installed. Please install pip3 and try again." + echo "Please install Python 3.10 or greater (such as python311) and try again." exit 1 fi } @@ -367,7 +367,9 @@ set_hosts () { echo "* Setting up hosts file." local hname hname="$(hostname 2>/dev/null || uname -n)" - echo >> /etc/hosts "127.0.0.1 $hname $fqdn" + if ! grep -q "$fqdn" /etc/hosts 2>/dev/null; then + echo >> /etc/hosts "127.0.0.1 $hname $fqdn" + fi } restart_supervisor () { @@ -750,16 +752,6 @@ case $distro in fi install_packages - # Ensure python3 and pip3 point to python 3.11+ if installed - if command -v python3.11 >/dev/null 2>&1; then - update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 2>/dev/null || true - ln -sf /usr/bin/python3.11 /usr/bin/python3 - if command -v pip3.11 >/dev/null 2>&1; then - update-alternatives --install /usr/bin/pip3 pip3 /usr/bin/pip3.11 1 2>/dev/null || true - ln -sf /usr/bin/pip3.11 /usr/bin/pip3 - fi - fi - set_hosts # Supervisor on SUSE (available as python3-supervisor, supervisor, or via pip) diff --git a/webvirtcloud/settings.py.template b/webvirtcloud/settings.py.template index 5c2257ae..71e84f20 100644 --- a/webvirtcloud/settings.py.template +++ b/webvirtcloud/settings.py.template @@ -17,7 +17,14 @@ from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent -SECRET_KEY = "" +SECRET_KEY = os.getenv("SECRET_KEY", "") +if not SECRET_KEY: + secret_file = BASE_DIR / "data" / "secret_key" + if secret_file.exists(): + try: + SECRET_KEY = secret_file.read_text().strip() + except Exception: + pass DEBUG = False From f061189716f7a0770fd035dd70a08d1088718085 Mon Sep 17 00:00:00 2001 From: catborise Date: Fri, 25 Sep 2026 22:39:31 +0300 Subject: [PATCH 08/10] fix(docker): resolve hadolint warnings in Dockerfile and add hadolint config --- .github/linters/.hadolint.yaml | 5 +++++ .hadolint.yaml | 5 +++++ Dockerfile | 8 +++++--- 3 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 .github/linters/.hadolint.yaml create mode 100644 .hadolint.yaml diff --git a/.github/linters/.hadolint.yaml b/.github/linters/.hadolint.yaml new file mode 100644 index 00000000..79bdabbe --- /dev/null +++ b/.github/linters/.hadolint.yaml @@ -0,0 +1,5 @@ +ignored: + - DL3008 + - DL3013 + - DL3042 + - SC1091 diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 00000000..79bdabbe --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,5 @@ +ignored: + - DL3008 + - DL3013 + - DL3042 + - SC1091 diff --git a/Dockerfile b/Dockerfile index 6d9f1fab..be297797 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ CMD ["/sbin/my_init"] RUN echo 'APT::Get::Clean=always;' >> /etc/apt/apt.conf.d/99AutomaticClean +# hadolint ignore=DL3008 RUN apt-get update -qqy \ && DEBIAN_FRONTEND=noninteractive apt-get -qyy install \ --no-install-recommends \ @@ -33,16 +34,17 @@ WORKDIR /srv/webvirtcloud # Install Python dependencies first with system-site-packages to leverage prebuilt bindings COPY conf/requirements.txt conf/requirements.txt +# hadolint ignore=DL3013,DL3042,SC1091 RUN python3 -m venv --system-site-packages venv && \ . venv/bin/activate && \ - pip3 install -U pip wheel && \ - pip3 install -r conf/requirements.txt && \ - pip3 cache purge + pip3 install --no-cache-dir -U pip wheel && \ + pip3 install --no-cache-dir -r conf/requirements.txt # Copy application source COPY . /srv/webvirtcloud # Run collectstatic with temporary dummy key, then remove temporary settings file +# hadolint ignore=SC1091 RUN . venv/bin/activate && \ cp webvirtcloud/settings.py.template webvirtcloud/settings.py && \ SECRET_KEY="build-dummy-key-only-for-collectstatic" python3 manage.py collectstatic --noinput && \ From d9112b797f3d782241cb26c70c0f60031cada8d5 Mon Sep 17 00:00:00 2001 From: catborise Date: Fri, 25 Sep 2026 23:11:52 +0300 Subject: [PATCH 09/10] fix(console): fix novncd startup in container and prevent double slashes in websocket url --- conf/runit/10_webvirtcloud_init.sh | 11 ++++++++ conf/runit/novncd.sh | 32 +++++++++++++++-------- console/novncd | 17 ++++++++++++ console/templates/console-spice-full.html | 4 +-- console/templates/console-vnc-lite.html | 10 ++++++- console/views.py | 3 +++ webvirtcloud/settings.py.template | 2 +- 7 files changed, 64 insertions(+), 15 deletions(-) diff --git a/conf/runit/10_webvirtcloud_init.sh b/conf/runit/10_webvirtcloud_init.sh index d44e1d5a..31f9c290 100755 --- a/conf/runit/10_webvirtcloud_init.sh +++ b/conf/runit/10_webvirtcloud_init.sh @@ -20,6 +20,17 @@ if [ -z "$SECRET_KEY" ]; then fi fi +# Apply optional WebSocket configuration from environment +if [ -n "$WS_PUBLIC_PORT" ]; then + sed -i "s|^WS_PUBLIC_PORT = .*|WS_PUBLIC_PORT = $WS_PUBLIC_PORT|" "$APP_DIR/webvirtcloud/settings.py" +fi +if [ -n "$WS_PUBLIC_HOST" ]; then + sed -i "s|^WS_PUBLIC_HOST = .*|WS_PUBLIC_HOST = \"$WS_PUBLIC_HOST\"|" "$APP_DIR/webvirtcloud/settings.py" +fi +if [ -n "$WS_PUBLIC_PATH" ]; then + sed -i "s|^WS_PUBLIC_PATH = .*|WS_PUBLIC_PATH = \"$WS_PUBLIC_PATH\"|" "$APP_DIR/webvirtcloud/settings.py" +fi + # Persist SQLite database in DATA_DIR if [ ! -f "$DATA_DIR/db.sqlite3" ] && [ -f "$APP_DIR/db.sqlite3" ] && [ ! -L "$APP_DIR/db.sqlite3" ]; then mv "$APP_DIR/db.sqlite3" "$DATA_DIR/db.sqlite3" diff --git a/conf/runit/novncd.sh b/conf/runit/novncd.sh index b0718640..203acf86 100755 --- a/conf/runit/novncd.sh +++ b/conf/runit/novncd.sh @@ -1,18 +1,28 @@ #!/bin/sh -# `/sbin/setuser www-data` runs the given command as the user `www-data`. -RUNAS=$(which setuser) -[ -z "$RUNAS" ] && RUNAS="$(which sudo) -u" -USER=www-data +USER="www-data" +DJANGO_PROJECT="/srv/webvirtcloud" +PYTHON="$DJANGO_PROJECT/venv/bin/python3" +NOVNCD="$DJANGO_PROJECT/console/novncd" +LOG="/var/log/novncd.log" -DJANGO_PROJECT=/srv/webvirtcloud -PYTHON=$DJANGO_PROJECT/venv/bin/python3 -NOVNCD=$DJANGO_PROJECT/console/novncd +cd "$DJANGO_PROJECT" || exit 1 # make novncd debug, verbose #PARAMS="-d -v" -LOG=/var/log/novncd.log - -cd $DJANGO_PROJECT || exit -exec "$RUNAS" "$USER" "$PYTHON" "$NOVNCD" "$PARAMS" >> $LOG 2>&1 +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 diff --git a/console/novncd b/console/novncd index 14c7a7f9..402e3cdc 100755 --- a/console/novncd +++ b/console/novncd @@ -143,6 +143,7 @@ class CompatibilityMixIn(object): # NoVNC uses it's own convention that forward token # from the request to a cookie header, we should check # also for this behavior + token = None hcookie = self.headers.get("cookie") if hcookie: @@ -159,6 +160,22 @@ class CompatibilityMixIn(object): if "token" in cookie: token = cookie["token"].value + # Fallback to query parameter if token not found in cookie + if not token and hasattr(self, "path"): + from urllib.parse import parse_qs, urlparse + + try: + parsed_url = urlparse(self.path) + query_params = parse_qs(parsed_url.query) + if "token" in query_params: + token = query_params["token"][0] + except Exception: + pass + + if not token: + self.msg("No console token provided in cookie or query parameters") + return + ( connhost, connport, diff --git a/console/templates/console-spice-full.html b/console/templates/console-spice-full.html index 0dfed9f2..5be3967d 100644 --- a/console/templates/console-spice-full.html +++ b/console/templates/console-spice-full.html @@ -61,8 +61,8 @@ sc.stop(); } - // uri = scheme + host + ":" + port; - uri = scheme + "{{ ws_host }}:{{ ws_port }}{{ ws_path }}"; + var path = '{{ ws_path }}'; + uri = scheme + "{{ ws_host }}:{{ ws_port }}" + (path ? (path[0] == '/' ? path : ('/' + path)) : '/'); document.getElementById('connectButton').innerHTML = "Stop"; document.getElementById('connectButton').onclick = disconnect; diff --git a/console/templates/console-vnc-lite.html b/console/templates/console-vnc-lite.html index 3da589aa..69ee6cf9 100755 --- a/console/templates/console-vnc-lite.html +++ b/console/templates/console-vnc-lite.html @@ -194,7 +194,15 @@ if (port) { url += ':' + port; } - url += '/' + path; + if (path) { + url += path.startsWith('/') ? path : ('/' + path); + } else { + url += '/'; + } + const token = readQueryVariable('token', '{{ token }}'); + if (token) { + url += (url.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(token); + } // Creating a new RFB object will start a new connection rfb = new RFB(document.getElementById('noVNC_container'), url, diff --git a/console/views.py b/console/views.py index f530df10..528e8091 100644 --- a/console/views.py +++ b/console/views.py @@ -86,6 +86,9 @@ def console(request): if ":" in ws_host: ws_host = re.sub(":[0-9]+", "", ws_host) + if ws_path: + ws_path = ws_path.strip("/") + "/" if ws_path.strip("/") else "" + if console_type == "vnc" or console_type == "spice": console_page = "console-" + console_type + "-" + view_type + ".html" response = render(request, console_page, locals()) diff --git a/webvirtcloud/settings.py.template b/webvirtcloud/settings.py.template index 71e84f20..c4c3e16c 100644 --- a/webvirtcloud/settings.py.template +++ b/webvirtcloud/settings.py.template @@ -213,7 +213,7 @@ WS_PUBLIC_PORT = 6080 WS_PUBLIC_HOST = None # Websock public path -WS_PUBLIC_PATH = "/novncd/" +WS_PUBLIC_PATH = "novncd/" # Websock Certificate for SSL WS_CERT = None From a444a2ba408ea8357578cdc72936f71c36fe7eb6 Mon Sep 17 00:00:00 2001 From: catborise Date: Fri, 25 Sep 2026 23:18:51 +0300 Subject: [PATCH 10/10] fix(linter): format console/views.py and disable htmlhint/jscpd on django templates --- .github/workflows/linter.yml | 4 +++- console/views.py | 16 +++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index d2fcb537..4df1905c 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -68,11 +68,13 @@ jobs: - name: Lint Code Base uses: docker://github/super-linter:latest env: - FILTER_REGEX_EXCLUDE: .*(static|scss|venv|locale)/.* + FILTER_REGEX_EXCLUDE: .*(static|scss|venv|locale|templates)/.* DEFAULT_BRANCH: master GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VALIDATE_ALL_CODEBASE: false VALIDATE_MARKDOWN: false + VALIDATE_HTML: false + VALIDATE_JSCPD: false VALIDATE_ANSIBLE: false VALIDATE_CLOJURE: false VALIDATE_COFFEE: false diff --git a/console/views.py b/console/views.py index 528e8091..bc835689 100644 --- a/console/views.py +++ b/console/views.py @@ -1,23 +1,21 @@ +# pylint: disable=no-name-in-module,no-member import re -from vrtManager.util import randomUUID - +from accounts.models import UserInstance +from appsettings.settings import app_settings from django.http.response import HttpResponseServerError from django.shortcuts import render from django.utils.translation import gettext_lazy as _ -from libvirt import libvirtError - -from accounts.models import UserInstance -from appsettings.settings import app_settings from instances.models import Instance +from libvirt import libvirtError from vrtManager.instance import wvmInstance from webvirtcloud.settings import ( + SOCKETIO_PUBLIC_HOST, + SOCKETIO_PUBLIC_PATH, + SOCKETIO_PUBLIC_PORT, WS_PUBLIC_HOST, WS_PUBLIC_PATH, WS_PUBLIC_PORT, - SOCKETIO_PUBLIC_HOST, - SOCKETIO_PUBLIC_PORT, - SOCKETIO_PUBLIC_PATH, )