Enhanced the repository sync

This commit is contained in:
Johannes Erwerle 2026-04-06 20:43:17 +02:00
parent 8fb0c1393a
commit 0117713934
4 changed files with 55 additions and 157 deletions

View file

@ -1,122 +1,31 @@
from pathlib import Path
from subprocess import run
from collections import defaultdict from collections import defaultdict
from typing import Any
from logging import getLogger from logging import getLogger
import shutil import shutil
from django.contrib.auth.models import User
from .models import BorgRepository from django.conf import settings
logger = getLogger("deployments") logger = getLogger("deployments")
# sync users # sync users
def get_users(user_prefix: str = "u-") -> dict[str, dict[str, Any]]: def sync_repos(dry_run=False):
"""Synchronize the repos"""
from .models import BorgRepository
passwd = Path("/etc/passwd").read_text() repos = BorgRepository.objects.all()
output = dict() print(repos)
for line in passwd.splitlines():
name, _, _, _, _, homedir, _ = line.split(":")
if not name.startswith(user_prefix):
continue
output[name] = {"homedir": Path(homedir)}
return output
def cleanup_users(
usernames: set[str],
system_users: dict[str, dict[str, Any]],
dry_run=False,
):
"""Cleans up all unused user accounts and directories on the system.
usernames is the set of users that should exist.
system_users contains all the users that exist (with the correct prefix)"""
base_dir = Path("/home/")
# cleanup users
for user in system_users.keys():
if user not in usernames:
logger.info("Deleting user '%(user)s'.")
print(f"Deleting user '{user}'.")
if not dry_run:
run(["userdel", user])
pass
# cleanup directories
for dir in base_dir.iterdir():
if dir.is_dir() and dir.name.startswith("u-") and dir.name not in usernames:
logger.info("Deleting home directory '%(dir)s'")
print(f"Deleting home directory '{dir}'")
if not dry_run:
shutil.rmtree(dir)
pass
def create_users(
usernames: set[str],
system_users: dict[str, dict[str, Any]],
dry_run=False,
):
for username in usernames:
if username not in system_users.keys():
logger.info("Adding user '%(username)s'")
print(f"Adding user '{username}'")
if not dry_run:
add_user(username)
def add_user(username: str):
users = get_users()
assert username not in users.keys(), "User already exists"
run(["useradd", "--no-user-group", "--create-home", username], check=True)
def sync_users(dry_run=False):
# gather data
users = User.objects.all()
usernames = {f"u-{user.pk}" for user in users}
system_users = get_users()
# clean up old users
cleanup_users(usernames=usernames, system_users=system_users, dry_run=dry_run)
# create the new users
create_users(usernames=usernames, system_users=system_users, dry_run=dry_run)
for user in users:
username = f"u-{user.pk}"
sync_repos(
username=username,
user=user,
system_user=system_users[username],
dry_run=dry_run,
)
# sync authorized_keys
def sync_repos(username: str, user: User, system_user: dict[str, Any], dry_run=False):
repos = BorgRepository.objects.filter(user=user)
repos_by_key = defaultdict(list) repos_by_key = defaultdict(list)
for repo in repos: for repo in repos:
repos_by_key[repo.key].append(repo) repos_by_key[repo.key].append(repo)
homedir = system_user["homedir"] print(repos)
# create .ssh directory # create .ssh directory
ssh_dir = homedir / ".ssh" ssh_dir = settings.BACKUP_AUTHORIZED_KEYS.parent
if not dry_run:
ssh_dir.mkdir( ssh_dir.mkdir(
mode=0o750, mode=0o750,
exist_ok=True, exist_ok=True,
@ -124,34 +33,34 @@ def sync_repos(username: str, user: User, system_user: dict[str, Any], dry_run=F
) )
# create authorized_keys file # create authorized_keys file
authorized_keys = ssh_dir / "authorized_keys" authorized_keys = settings.BACKUP_AUTHORIZED_KEYS
commands = [] commands = []
for key, repos in repos_by_key.items(): for key, repositories in repos_by_key.items():
repo_paths = [ repo_paths = [f"--restrict-to-repository {repo.path}" for repo in repositories]
f"--restrict-to-repository {str(system_user['homedir'] / repo.name)}"
for repo in repos
]
commands.append( commands.append(
f"""command="borg serve {" ".join(repo_paths)} --quota=500G",restrict ssh-rsa {key}""" f"""command="cd {str(settings.BACKUP_BORG_DIR)}; borg serve {" ".join(repo_paths)} --storage-quota=500G",restrict {key}"""
) )
print("\n".join(commands)) if not dry_run:
authorized_keys.write_text("\n".join(commands) + "\n") authorized_keys.write_text("\n".join(commands) + "\n")
print(repos)
# remove repositories that do no longer exist # remove repositories that do no longer exist
repo_names = {repo.name for repo in repos} repo_paths = {repo.path for repo in repos}
print(repo_names) print(repo_paths)
for dir in homedir.iterdir(): for user_dir in settings.BACKUP_BORG_DIR.iterdir():
print(dir.name) print(user_dir)
for dir in user_dir.iterdir():
if dir.is_dir() and not dir.name.startswith("."): if dir.is_dir() and not dir.name.startswith("."):
if dir.name not in repo_names: if dir not in repo_paths:
print(f"removing unused repo '{dir}'") print(f"removing unused repo '{dir}'")
if not dry_run:
shutil.rmtree(dir) shutil.rmtree(dir)
# create the repositories # create the repositories
for repo in repos: for repo in repos:
path = system_user["homedir"] / repo.name print(f"creating repo {repo}")
path.mkdir(mode=0o750, exist_ok=True, parents=True) if not dry_run:
shutil.chown(path, user=username) repo.path.mkdir(mode=0o750, exist_ok=True, parents=True)
shutil.chown(repo.path, user=settings.BACKUP_USER)

View file

@ -1,17 +1,9 @@
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand
from ...models import BorgRepository from ...deployments import sync_repos
from ...deployments import sync_users
class Command(BaseCommand): class Command(BaseCommand):
help = "Synchronized users on the host with the database." help = "Synchronized users on the host with the database."
def handle(self, *args, **options): def handle(self, *args, **options):
sync_repos(dry_run=False)
repos = BorgRepository.objects.all()
for repo in repos:
self.stdout.write(f"{repo}")
self.stdout.write(self.style.SUCCESS("This is a test"))
sync_users(dry_run=False)

View file

@ -1,6 +1,11 @@
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.conf import settings
from pathlib import Path
from .tasks import update_user
# Create your models here. # Create your models here.
@ -34,25 +39,22 @@ class BorgRepository(models.Model):
return f"BorgRepository '{self.user.username}' - '{self.name}' ({self.pk})" return f"BorgRepository '{self.user.username}' - '{self.name}' ({self.pk})"
@property @property
def username(self) -> str: def path(self) -> Path:
"""Returns the username of the linux user account for this user.""" return settings.BACKUP_BORG_DIR / str(self.user.pk) / str(self.pk)
return f"u-{self.pk}"
@property @property
def repo_url(self) -> str: def repo_url(self) -> str:
return f"{settings.BACKUP_USER}@{settings.BACKUP_REPO_HOST}:{self.user.pk}/{self.pk}" return f"{settings.BACKUP_USER}@{settings.BACKUP_REPO_HOST}:{self.user.pk}/{self.pk}"
def save(self): def save(self):
from .tasks import update_user
update_user.enqueue(user_pk=self.user.pk)
super().save() super().save()
update_user.enqueue()
def delete(self): def delete(self):
from .tasks import update_user
super().delete() super().delete()
update_user.enqueue(user_pk=self.user.pk) update_user.enqueue()
class Voucher(models.Model): class Voucher(models.Model):

View file

@ -1,12 +1,7 @@
from django.tasks import task from django.tasks import task
from .deployments import sync_repos, get_users from .deployments import sync_repos
from django.contrib.auth.models import User
@task @task
def update_user(user_pk): def update_user():
user = User.objects.get(pk=user_pk) sync_repos()
username = f"u-{user.pk}"
system_user = get_users()[username]
sync_repos(username=username, user=user, system_user=system_user)