1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00
webvirtcloud/appsettings/management/commands/backup.py
catborise 482f5bc16d fix(backup): TLS client certificates persist in Docker and are backed up
libvirt reads a TLS compute's client certificate from the panel user's
~/.pki/libvirt. In Docker that was /var/www/.pki inside the container,
so `docker compose up -d --build` dropped it and every TLS compute
stopped connecting; no backup held it on either install type, although
"Moving to a new machine" says the backup carries what the computes
accept, and the README never said where the certificates go.

Docker keeps /var/www/.pki in a webvirtcloud-pki volume, owned by
www-data and private like ~/.ssh; the backup copies ~/.pki into pki/;
the moving steps put it back; and "Adding a Compute Node" says where the
files go.
2026-11-06 09:12:34 +03:00

83 lines
3.7 KiB
Python

import fcntl
import os
import shutil
import sqlite3
import subprocess
from contextlib import closing
from datetime import datetime
from pathlib import Path
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.db import connection
PREFIX = "webvirtcloud-backup-"
class Command(BaseCommand):
help = ("Copy the database, secret key, worker id, settings and the SSH keys and TLS certificates of the "
"panel's user into a new directory in backups/, then delete all but the newest --keep backups.")
def add_arguments(self, parser):
parser.add_argument("--keep", type=int, default=7, help="how many backups to keep (default 7)")
def handle(self, *args, keep, **options):
if keep < 1:
raise CommandError("--keep must be at least 1")
os.umask(0o077) # the database holds compute passwords and keys
backups = settings.BASE_DIR / "backups"
backups.mkdir(mode=0o700, exist_ok=True)
with open(backups / ".lock", "w") as lock:
try:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
raise CommandError("Another backup is running") from None
self.stdout.write(str(self.backup(backups, keep)))
def backup(self, backups, keep):
name = PREFIX + datetime.now().strftime("%Y%m%d-%H%M%S")
partial = backups / f".{name}"
partial.mkdir()
try:
self.copy(partial)
partial.rename(backups / name)
except BaseException:
shutil.rmtree(partial, ignore_errors=True)
raise
# only after a good backup: a failing one never costs an older one;
# the partial ones are what a killed run left behind
for old in [*sorted(backups.glob(PREFIX + "*"))[:-keep], *backups.glob(f".{PREFIX}*")]:
shutil.rmtree(old)
return backups / name
def copy(self, to):
database = connection.settings_dict
if database["ENGINE"] == "django.db.backends.sqlite3":
# the backup API copies a consistent state while the panel writes
read_only = Path(database["NAME"]).resolve().as_uri() + "?mode=ro"
with closing(sqlite3.connect(read_only, uri=True, timeout=30)) as source, \
closing(sqlite3.connect(to / "db.sqlite3")) as copy:
source.backup(copy)
result = copy.execute("PRAGMA integrity_check").fetchone()[0]
if result != "ok":
raise CommandError(f"The copy of the database is damaged: {result}")
else:
self.stderr.write("The database is not sqlite and is not in the backup: back it up with its own tools.")
data = settings.BASE_DIR / "data"
for source in (data / "secret_key", data / "node_id", settings.BASE_DIR / "webvirtcloud" / "settings.py"):
if source.is_file():
shutil.copyfile(source, to / source.name)
ssh = Path.home() / ".ssh"
if ssh.is_dir():
(to / "ssh").mkdir()
for source in ssh.iterdir():
if source.is_file() and not source.is_symlink():
shutil.copyfile(source, to / "ssh" / source.name)
# the TLS client certificates libvirt reads (~/.pki/libvirt); a link stays a link
pki = Path.home() / ".pki"
if pki.is_dir():
shutil.copytree(pki, to / "pki", symlinks=True)
if (settings.BASE_DIR / ".git").exists():
commit = subprocess.run(["git", "rev-parse", "HEAD"], cwd=settings.BASE_DIR, capture_output=True, text=True)
if commit.returncode == 0:
(to / "commit").write_text(commit.stdout)