201 lines
6.1 KiB
Python
201 lines
6.1 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import os
|
||
|
|
import shutil
|
||
|
|
import subprocess
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Literal
|
||
|
|
|
||
|
|
from django.conf import settings
|
||
|
|
from django.db import connection
|
||
|
|
|
||
|
|
from .models import GlobalConfig
|
||
|
|
|
||
|
|
|
||
|
|
CheckStatus = Literal["ok", "warning", "failed", "skipped"]
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass(frozen=True)
|
||
|
|
class SelfCheck:
|
||
|
|
name: str
|
||
|
|
status: CheckStatus
|
||
|
|
message: str
|
||
|
|
detail: str = ""
|
||
|
|
|
||
|
|
|
||
|
|
def collect_self_checks() -> list[SelfCheck]:
|
||
|
|
checks: list[SelfCheck] = []
|
||
|
|
checks.extend(_django_checks())
|
||
|
|
checks.extend(_path_checks())
|
||
|
|
checks.extend(_binary_checks())
|
||
|
|
checks.extend(_database_checks())
|
||
|
|
checks.extend(_config_checks())
|
||
|
|
checks.extend(_systemd_checks())
|
||
|
|
return checks
|
||
|
|
|
||
|
|
|
||
|
|
def summarize_self_checks(checks: list[SelfCheck]) -> dict[str, int]:
|
||
|
|
return {
|
||
|
|
"ok": sum(1 for check in checks if check.status == "ok"),
|
||
|
|
"warning": sum(1 for check in checks if check.status == "warning"),
|
||
|
|
"failed": sum(1 for check in checks if check.status == "failed"),
|
||
|
|
"skipped": sum(1 for check in checks if check.status == "skipped"),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def _django_checks() -> list[SelfCheck]:
|
||
|
|
checks = [
|
||
|
|
SelfCheck(
|
||
|
|
"Django debug",
|
||
|
|
"warning" if settings.DEBUG else "ok",
|
||
|
|
"DEBUG is enabled." if settings.DEBUG else "DEBUG is disabled.",
|
||
|
|
),
|
||
|
|
SelfCheck(
|
||
|
|
"Django secret key",
|
||
|
|
"failed" if settings.SECRET_KEY == "dev-only-change-me" else "ok",
|
||
|
|
"Default development secret key is still active."
|
||
|
|
if settings.SECRET_KEY == "dev-only-change-me"
|
||
|
|
else "Secret key is configured.",
|
||
|
|
),
|
||
|
|
SelfCheck(
|
||
|
|
"Allowed hosts",
|
||
|
|
"ok" if settings.ALLOWED_HOSTS else "failed",
|
||
|
|
", ".join(settings.ALLOWED_HOSTS) if settings.ALLOWED_HOSTS else "No allowed hosts configured.",
|
||
|
|
),
|
||
|
|
]
|
||
|
|
return checks
|
||
|
|
|
||
|
|
|
||
|
|
def _path_checks() -> list[SelfCheck]:
|
||
|
|
checks = []
|
||
|
|
checks.append(_path_check("POBSYNC_HOME", Path(settings.POBSYNC_HOME), must_be_absolute=True, must_be_writable=True))
|
||
|
|
checks.append(
|
||
|
|
_path_check(
|
||
|
|
"POBSYNC_BACKUP_ROOT",
|
||
|
|
Path(settings.POBSYNC_BACKUP_ROOT),
|
||
|
|
must_be_absolute=True,
|
||
|
|
must_exist=True,
|
||
|
|
must_be_writable=True,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
checks.append(
|
||
|
|
_path_check(
|
||
|
|
"Static root",
|
||
|
|
Path(settings.STATIC_ROOT),
|
||
|
|
must_be_absolute=True,
|
||
|
|
must_exist=False,
|
||
|
|
must_be_writable=True,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
db_settings = settings.DATABASES["default"]
|
||
|
|
if db_settings["ENGINE"] == "django.db.backends.sqlite3":
|
||
|
|
checks.append(
|
||
|
|
_path_check(
|
||
|
|
"SQLite directory",
|
||
|
|
Path(str(db_settings["NAME"])).parent,
|
||
|
|
must_be_absolute=True,
|
||
|
|
must_exist=True,
|
||
|
|
must_be_writable=True,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
return checks
|
||
|
|
|
||
|
|
|
||
|
|
def _path_check(
|
||
|
|
name: str,
|
||
|
|
path: Path,
|
||
|
|
*,
|
||
|
|
must_be_absolute: bool,
|
||
|
|
must_exist: bool = False,
|
||
|
|
must_be_writable: bool,
|
||
|
|
) -> SelfCheck:
|
||
|
|
if must_be_absolute and not path.is_absolute():
|
||
|
|
return SelfCheck(name, "failed", f"{path} is not absolute.")
|
||
|
|
if must_exist and not path.exists():
|
||
|
|
return SelfCheck(name, "failed", f"{path} does not exist.")
|
||
|
|
|
||
|
|
target = path if path.exists() else path.parent
|
||
|
|
if not target.exists():
|
||
|
|
return SelfCheck(name, "failed", f"{target} does not exist.")
|
||
|
|
if must_be_writable and not os.access(target, os.W_OK):
|
||
|
|
return SelfCheck(name, "failed", f"{target} is not writable by this process.")
|
||
|
|
return SelfCheck(name, "ok", str(path))
|
||
|
|
|
||
|
|
|
||
|
|
def _binary_checks() -> list[SelfCheck]:
|
||
|
|
checks = []
|
||
|
|
for binary in ("rsync", "ssh", "ssh-keygen", "gunicorn"):
|
||
|
|
path = shutil.which(binary)
|
||
|
|
checks.append(
|
||
|
|
SelfCheck(
|
||
|
|
f"Binary: {binary}",
|
||
|
|
"ok" if path else "failed",
|
||
|
|
path or f"{binary} was not found in PATH.",
|
||
|
|
)
|
||
|
|
)
|
||
|
|
return checks
|
||
|
|
|
||
|
|
|
||
|
|
def _database_checks() -> list[SelfCheck]:
|
||
|
|
try:
|
||
|
|
with connection.cursor() as cursor:
|
||
|
|
cursor.execute("SELECT 1")
|
||
|
|
cursor.fetchone()
|
||
|
|
except Exception as exc:
|
||
|
|
return [SelfCheck("Database connection", "failed", f"{type(exc).__name__}: {exc}")]
|
||
|
|
return [SelfCheck("Database connection", "ok", settings.DATABASES["default"]["ENGINE"])]
|
||
|
|
|
||
|
|
|
||
|
|
def _config_checks() -> list[SelfCheck]:
|
||
|
|
try:
|
||
|
|
global_config = GlobalConfig.objects.get(name="default")
|
||
|
|
except GlobalConfig.DoesNotExist:
|
||
|
|
return [SelfCheck("Global config", "warning", "Default global config has not been created yet.")]
|
||
|
|
|
||
|
|
status: CheckStatus = "ok"
|
||
|
|
message = "Default global config exists."
|
||
|
|
if global_config.backup_root != settings.POBSYNC_BACKUP_ROOT:
|
||
|
|
status = "warning"
|
||
|
|
message = "Global config backup root differs from runtime POBSYNC_BACKUP_ROOT."
|
||
|
|
return [
|
||
|
|
SelfCheck(
|
||
|
|
"Global config",
|
||
|
|
status,
|
||
|
|
message,
|
||
|
|
f"database={global_config.backup_root} runtime={settings.POBSYNC_BACKUP_ROOT}",
|
||
|
|
)
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def _systemd_checks() -> list[SelfCheck]:
|
||
|
|
if not Path("/run/systemd/system").exists() or shutil.which("systemctl") is None:
|
||
|
|
return [
|
||
|
|
SelfCheck(
|
||
|
|
"Systemd services",
|
||
|
|
"skipped",
|
||
|
|
"systemd is not available in this runtime.",
|
||
|
|
"This is expected inside Docker.",
|
||
|
|
)
|
||
|
|
]
|
||
|
|
|
||
|
|
checks = []
|
||
|
|
for service in ("pobsync-web.service", "pobsync-worker.service", "pobsync-scheduler.service"):
|
||
|
|
result = subprocess.run(
|
||
|
|
["systemctl", "is-active", service],
|
||
|
|
check=False,
|
||
|
|
stdout=subprocess.PIPE,
|
||
|
|
stderr=subprocess.PIPE,
|
||
|
|
text=True,
|
||
|
|
timeout=5,
|
||
|
|
)
|
||
|
|
active_state = result.stdout.strip() or result.stderr.strip()
|
||
|
|
checks.append(
|
||
|
|
SelfCheck(
|
||
|
|
service,
|
||
|
|
"ok" if result.returncode == 0 else "failed",
|
||
|
|
active_state,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
return checks
|