mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
- A Devices section of each host lists its PCI devices by IOMMU group and its USB devices: what each is (vendor, product, IDs, class, driver), what the host itself has on it (its network interfaces, disks and graphics card, anywhere below it) and the VMs whose definitions name it. - A VM names a PCI device by its address, a USB device by its address or by its model only; a VM that names a model only is shown on every device of that model, which one it takes is known only when it starts. - A host without IOMMU groups says that no PCI device can be given to a VM and how to turn the IOMMU on. - One listing of the host's node devices, one XML read each (a device gone meanwhile is left out), and the VMs' definitions read once; nothing is shown when they cannot be read, as a device would look free. - Read only: giving devices to VMs comes in the next layers.
420 lines
13 KiB
Text
420 lines
13 KiB
Text
"""
|
|
Django settings for webvirtcloud project.
|
|
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
try:
|
|
import ldap
|
|
from django_auth_ldap.config import LDAPSearch, NestedActiveDirectoryGroupType
|
|
HAS_LDAP = True
|
|
except ImportError:
|
|
HAS_LDAP = False
|
|
from django.utils.translation import gettext_lazy as _
|
|
from pathlib import Path
|
|
|
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
|
|
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
|
|
|
|
MAC_OUI = '52:54:10'
|
|
|
|
# Host names the panel answers to. The installer writes the server's; add more
|
|
# (comma-separated, "*" for any) through the environment.
|
|
ALLOWED_HOSTS = ["localhost", "127.0.0.1", "[::1]"]
|
|
if extra_hosts := os.getenv("ALLOWED_HOSTS"):
|
|
ALLOWED_HOSTS.extend([host.strip() for host in extra_hosts.split(",") if host.strip()])
|
|
|
|
CSRF_TRUSTED_ORIGINS = ["http://localhost", "http://127.0.0.1", "http://localhost:8000", "http://127.0.0.1:8000"]
|
|
if extra_origins := os.getenv("CSRF_TRUSTED_ORIGINS"):
|
|
CSRF_TRUSTED_ORIGINS.extend([origin.strip() for origin in extra_origins.split(",") if origin.strip()])
|
|
|
|
# Honor X-Forwarded-Proto for request.is_secure(). gunicorn listens on
|
|
# 127.0.0.1 only, so the bundled nginx is the one client that sets it.
|
|
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
|
|
|
# Production profile for a panel served over HTTPS: secure cookies, HTTP to
|
|
# HTTPS redirect and HSTS. Off by default (plain HTTP).
|
|
if os.getenv("WEBVIRTCLOUD_HTTPS") == "1":
|
|
SESSION_COOKIE_SECURE = True
|
|
CSRF_COOKIE_SECURE = True
|
|
SECURE_SSL_REDIRECT = True
|
|
# VMs fetch cloud-init metadata over plain HTTP
|
|
SECURE_REDIRECT_EXEMPT = [r"^datasource/"]
|
|
SECURE_HSTS_SECONDS = 31536000
|
|
|
|
# Application definition
|
|
INSTALLED_APPS = [
|
|
"django.contrib.auth",
|
|
"django.contrib.contenttypes",
|
|
"django.contrib.sessions",
|
|
"django.contrib.messages",
|
|
"django.contrib.staticfiles",
|
|
"django_bootstrap5",
|
|
"django_bootstrap_icons",
|
|
"django_otp",
|
|
"django_otp.plugins.otp_totp",
|
|
"drf_spectacular",
|
|
"drf_spectacular_sidecar",
|
|
"accounts",
|
|
"admin",
|
|
"appsettings",
|
|
"computes",
|
|
"console",
|
|
"datasource",
|
|
"networks",
|
|
"instances",
|
|
"interfaces",
|
|
"nwfilters",
|
|
"nodedevs",
|
|
"storages",
|
|
"virtsecrets",
|
|
"logs",
|
|
"operations",
|
|
"rest_framework",
|
|
"knox",
|
|
"axes",
|
|
]
|
|
|
|
MIDDLEWARE = [
|
|
"django.middleware.security.SecurityMiddleware",
|
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
"django.middleware.locale.LocaleMiddleware",
|
|
"django.middleware.common.CommonMiddleware",
|
|
"django.middleware.csrf.CsrfViewMiddleware",
|
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
"django_otp.middleware.OTPMiddleware",
|
|
"django.contrib.auth.middleware.LoginRequiredMiddleware",
|
|
"accounts.middleware.ForcePasswordChangeMiddleware",
|
|
"django.contrib.auth.middleware.RemoteUserMiddleware",
|
|
"django.contrib.messages.middleware.MessageMiddleware",
|
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
"webvirtcloud.middleware.ContentSecurityPolicyMiddleware",
|
|
"appsettings.middleware.AppSettingsMiddleware",
|
|
"webvirtcloud.middleware.ExceptionMiddleware",
|
|
# last, as django-axes requires
|
|
"axes.middleware.AxesMiddleware",
|
|
]
|
|
|
|
ROOT_URLCONF = "webvirtcloud.urls"
|
|
|
|
TEMPLATES = [
|
|
{
|
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
"DIRS": [
|
|
Path.joinpath(BASE_DIR, "templates"),
|
|
],
|
|
"APP_DIRS": True,
|
|
"OPTIONS": {
|
|
"context_processors": [
|
|
"django.template.context_processors.debug",
|
|
"django.template.context_processors.request",
|
|
"django.contrib.auth.context_processors.auth",
|
|
"django.contrib.messages.context_processors.messages",
|
|
"appsettings.context_processors.app_settings",
|
|
],
|
|
"libraries": {
|
|
"common_tags": "webvirtcloud.common_tags",
|
|
},
|
|
},
|
|
},
|
|
]
|
|
|
|
WSGI_APPLICATION = "webvirtcloud.wsgi.application"
|
|
|
|
|
|
# Database
|
|
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
|
|
|
|
# sqlite by default; PostgreSQL when WEBVIRTCLOUD_DB_HOST is set (a host name,
|
|
# an IP, or a unix socket directory).
|
|
if db_host := os.getenv("WEBVIRTCLOUD_DB_HOST"):
|
|
DATABASES = {
|
|
"default": {
|
|
"ENGINE": "django.db.backends.postgresql",
|
|
"HOST": db_host,
|
|
"PORT": os.getenv("WEBVIRTCLOUD_DB_PORT", "5432"),
|
|
"NAME": os.getenv("WEBVIRTCLOUD_DB_NAME", "webvirtcloud"),
|
|
"USER": os.getenv("WEBVIRTCLOUD_DB_USER", "webvirtcloud"),
|
|
"PASSWORD": os.getenv("WEBVIRTCLOUD_DB_PASSWORD", ""),
|
|
}
|
|
}
|
|
else:
|
|
DATABASES = {
|
|
"default": {
|
|
"ENGINE": "django.db.backends.sqlite3",
|
|
"NAME": Path.joinpath(BASE_DIR, "db.sqlite3"),
|
|
# IMMEDIATE takes the write lock when a transaction starts, so a
|
|
# check-then-write cannot interleave with another process; timeout
|
|
# is how long a writer waits for that lock (seconds). WAL is set
|
|
# per connection by the operations app.
|
|
"OPTIONS": {"transaction_mode": "IMMEDIATE", "timeout": 20},
|
|
# A file, not shared-cache memory: test threads and worker processes
|
|
# open the same database, and shared-cache locks ignore the timeout.
|
|
"TEST": {"NAME": os.getenv("WEBVIRTCLOUD_TEST_DB") or Path.joinpath(BASE_DIR, "test_db.sqlite3")},
|
|
}
|
|
}
|
|
|
|
AUTHENTICATION_BACKENDS = [
|
|
# first: refuses logins that are locked out
|
|
"axes.backends.AxesStandaloneBackend",
|
|
"django.contrib.auth.backends.ModelBackend",
|
|
#"django_auth_ldap.backend.LDAPBackend",
|
|
]
|
|
|
|
LOGIN_URL = "/accounts/login/"
|
|
|
|
# Tests create users by the thousand: a fast hash for them (manage.py test only)
|
|
if sys.argv[1:2] == ["test"]:
|
|
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
|
|
|
|
# manage.py test --parallel on SQLite (WAL)
|
|
TEST_RUNNER = "webvirtcloud.test_runner.TestRunner"
|
|
|
|
# Failed logins (django-axes): a user is locked for the address the logins
|
|
# come from. The limit and the lock time are on the Settings page
|
|
# (LOGIN_FAILURE_LIMIT, LOGIN_LOCKOUT_MINUTES); unlock with
|
|
# "manage.py axes_reset_username <username>".
|
|
AXES_FAILURE_LIMIT = "accounts.lockout.failure_limit"
|
|
AXES_COOLOFF_TIME = "accounts.lockout.cooloff_time"
|
|
AXES_LOCKOUT_PARAMETERS = [["username", "ip_address"]]
|
|
AXES_RESET_ON_SUCCESS = True
|
|
AXES_CLIENT_IP_CALLABLE = "accounts.lockout.client_ip"
|
|
# Reverse proxies other than a local one whose X-Real-IP header gives the
|
|
# client address, e.g. ["10.0.0.5", "172.18.0.0/16"]
|
|
LOGIN_TRUSTED_PROXIES = []
|
|
|
|
LOGOUT_REDIRECT_URL = "/accounts/login/"
|
|
|
|
LOGIN_REDIRECT_URL="/instances/"
|
|
|
|
LANGUAGE_CODE = "en-us"
|
|
|
|
TIME_ZONE = "UTC"
|
|
|
|
USE_I18N = True
|
|
|
|
|
|
USE_TZ = True
|
|
|
|
STATIC_URL = "/static/"
|
|
|
|
# Source assets live in static/; collectstatic gathers them with the apps'
|
|
# assets into staticfiles/ (gitignored), which WhiteNoise and nginx serve.
|
|
STATICFILES_DIRS = [BASE_DIR / "static"]
|
|
STATIC_ROOT = BASE_DIR / "staticfiles"
|
|
|
|
|
|
BS_ICONS_CACHE = Path.joinpath(BASE_DIR, 'static/icon_cache')
|
|
|
|
# Default primary key field type
|
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
|
|
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|
|
|
|
|
LOCALE_PATHS = [
|
|
"locale/",
|
|
]
|
|
|
|
# Resolve the application log path and make sure its directory exists, so a
|
|
# clean checkout, CI, or a Docker build (none of which create data/) does not
|
|
# fail when logging is configured.
|
|
WEBVIRTCLOUD_LOG_FILE = os.getenv("WEBVIRTCLOUD_LOG_FILE", str(BASE_DIR / "data" / "webvirtcloud.log"))
|
|
os.makedirs(os.path.dirname(WEBVIRTCLOUD_LOG_FILE), exist_ok=True)
|
|
|
|
LOGGING = {
|
|
'version': 1,
|
|
'disable_existing_loggers': False,
|
|
'formatters': {
|
|
'standard': {
|
|
'format': '[%(asctime)s] %(levelname)s: %(message)s'
|
|
},
|
|
},
|
|
'handlers': {
|
|
'mail_admins': {
|
|
'level': 'ERROR',
|
|
'class': 'django.utils.log.AdminEmailHandler'
|
|
},
|
|
'default': {
|
|
'level': 'INFO',
|
|
'class': 'logging.handlers.RotatingFileHandler',
|
|
'filename': WEBVIRTCLOUD_LOG_FILE,
|
|
'maxBytes': 10 * 1024 * 1024,
|
|
'backupCount': 5,
|
|
'formatter': 'standard',
|
|
},
|
|
},
|
|
'loggers': {
|
|
'': {
|
|
'handlers': ['default'],
|
|
'level': 'INFO',
|
|
'propagate': True
|
|
},
|
|
'django.request': {
|
|
"handlers": ["mail_admins"],
|
|
"level": "ERROR",
|
|
"propagate": True,
|
|
}
|
|
},
|
|
}
|
|
|
|
|
|
#
|
|
# WebVirtCloud settings
|
|
#
|
|
|
|
# The VNC console proxy (novncd). Browsers reach it through nginx's /novncd/
|
|
# on the panel's own address, so it listens on the loopback only.
|
|
WS_PORT = 6080
|
|
WS_HOST = "127.0.0.1"
|
|
|
|
# Serial console (xterm.js over socketiod) for VMs without graphics. socketiod
|
|
# accepts only the console page's signed token (as noVNC does); while this is
|
|
# False it refuses every connection. Restart socketiod after changing it.
|
|
SERIAL_CONSOLE_ENABLED = os.getenv("WEBVIRTCLOUD_SERIAL_CONSOLE") == "1"
|
|
|
|
SOCKETIO_PORT = 6081
|
|
# Listen on localhost only; the console page reaches socketiod through the
|
|
# nginx /socket.io/ location.
|
|
SOCKETIO_HOST = "127.0.0.1"
|
|
|
|
# List of console listen addresses
|
|
QEMU_CONSOLE_LISTENER_ADDRESSES = (
|
|
("127.0.0.1", _("Localhost")),
|
|
("0.0.0.0", _("All interfaces")),
|
|
)
|
|
|
|
# List taken from http://qemu.weilnetz.de/qemu-doc.html#sec_005finvocation
|
|
QEMU_KEYMAPS = [
|
|
"ar",
|
|
"da",
|
|
"de",
|
|
"de-ch",
|
|
"en-gb",
|
|
"en-us",
|
|
"es",
|
|
"et",
|
|
"fi",
|
|
"fo",
|
|
"fr",
|
|
"fr-be",
|
|
"fr-ca",
|
|
"fr-ch",
|
|
"hr",
|
|
"hu",
|
|
"is",
|
|
"it",
|
|
"ja",
|
|
"lt",
|
|
"lv",
|
|
"mk",
|
|
"nl",
|
|
"nl-be",
|
|
"no",
|
|
"pl",
|
|
"pt",
|
|
"pt-br",
|
|
"ru",
|
|
"sl",
|
|
"sv",
|
|
"th",
|
|
"tr",
|
|
]
|
|
|
|
# Keepalive interval and count for libvirt connections
|
|
LIBVIRT_KEEPALIVE_INTERVAL = 5
|
|
LIBVIRT_KEEPALIVE_COUNT = 5
|
|
|
|
ALLOW_EMPTY_PASSWORD = False
|
|
NEW_USER_DEFAULT_INSTANCES = []
|
|
# Days the owners of a VM that vanished from its compute are kept, in case it comes back
|
|
INSTANCE_OWNERSHIP_RETENTION_DAYS = 30
|
|
|
|
|
|
# OTP Config
|
|
OTP_ENABLED = False
|
|
|
|
|
|
# REST Framework & OpenAPI (drf-spectacular)
|
|
REST_FRAMEWORK = {
|
|
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
|
# An API token, or a session: a password alone (HTTP Basic) must not reach
|
|
# the API, as it would skip the OTP second factor the login form enforces;
|
|
# tokens are made in a session. The token class comes first, so a refused
|
|
# request gets 401 with its challenge, not 403.
|
|
"DEFAULT_AUTHENTICATION_CLASSES": [
|
|
"accounts.auth.TokenAuthentication",
|
|
"rest_framework.authentication.SessionAuthentication",
|
|
],
|
|
# A view that forgets its own permission_classes is not public.
|
|
"DEFAULT_PERMISSION_CLASSES": [
|
|
"rest_framework.permissions.IsAuthenticated",
|
|
],
|
|
}
|
|
|
|
# API tokens (django-rest-knox): kept as SHA-512 digests, sent as
|
|
# "Authorization: Token wvc_..."; made and deleted on the profile page
|
|
KNOX_TOKEN_MODEL = "accounts.ApiToken"
|
|
REST_KNOX = {"TOKEN_PREFIX": "wvc_"}
|
|
# knox stays installed for its token code, but its migrations would create a
|
|
# knox_authtoken table that accounts.ApiToken replaces
|
|
MIGRATION_MODULES = {"knox": None}
|
|
|
|
SPECTACULAR_SETTINGS = {
|
|
"TITLE": "Webvirtcloud REST-API",
|
|
"DESCRIPTION": "Webvirtcloud REST API",
|
|
"VERSION": "v1",
|
|
"SERVE_INCLUDE_SCHEMA": False,
|
|
"SERVE_PERMISSIONS": ["rest_framework.permissions.IsAuthenticated"],
|
|
"SWAGGER_UI_DIST": "SIDECAR",
|
|
"SWAGGER_UI_FAVICON_HREF": "SIDECAR",
|
|
"REDOC_DIST": "SIDECAR",
|
|
}
|
|
|
|
# EMAIL Config (SMTP; override through the environment)
|
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
|
EMAIL_HOST = os.getenv("EMAIL_HOST", "smtp.gmail.com")
|
|
EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "1") == "1"
|
|
EMAIL_PORT = int(os.getenv("EMAIL_PORT", "587"))
|
|
## sender's email-id
|
|
EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "")
|
|
## password associated with above email-id
|
|
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "")
|
|
|
|
# LDAP Config
|
|
#
|
|
|
|
if HAS_LDAP:
|
|
AUTH_LDAP_SERVER_URI = "ldap://example.com"
|
|
AUTH_LDAP_BIND_DN = "username@example.com"
|
|
AUTH_LDAP_BIND_PASSWORD = "password"
|
|
AUTH_LDAP_USER_SEARCH = LDAPSearch(
|
|
"CN=Users,DC=example,DC=com", ldap.SCOPE_SUBTREE, "(sAMAccountName=%(user)s)"
|
|
)
|
|
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
|
|
"CN=Users,DC=example,DC=com", ldap.SCOPE_SUBTREE, "(objectClass=group)"
|
|
)
|
|
AUTH_LDAP_GROUP_TYPE = NestedActiveDirectoryGroupType()
|
|
AUTH_LDAP_REQUIRE_GROUP = "CN=WebVirtCloud Access,CN=Users,DC=example,DC=com"
|
|
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
|
|
"is_staff": "CN=WebVirtCloud Staff,CN=Users,DC=example,DC=com",
|
|
"is_superuser": "CN=WebVirtCloud Admins,CN=Users,DC=example,DC=com",
|
|
}
|
|
AUTH_LDAP_USER_ATTR_MAP = {
|
|
"first_name": "givenName",
|
|
"last_name": "sn",
|
|
"email": "mail",
|
|
}
|