#!/usr/bin/env python import os import argparse import sys from datetime import (datetime,timezone) from dateutil.tz import (tzlocal) import subprocess import json from tempfile import TemporaryDirectory import pathlib import unshare import signal def borg_lv(lv, size_percent, target, archive_prefix, dry_run, **args): vgp = lv.split('/') lvn = vgp[-1] snn = f'{lvn}_snap_{datetime.utcnow().strftime("%Y%m%dT%H%MZ")}' sn = "/".join([*vgp[:-1], snn]) try: subprocess.run(['lvcreate', f'-l{size_percent}%ORIGIN', '-s', '-n', snn, lv], stdin=subprocess.DEVNULL, check=True) creation_time = None with subprocess.Popen(['lvs', '--reportformat=json', '-olv_time', sn], stdout=subprocess.PIPE) as proc: res = json.load(proc.stdout) creation_time = datetime.strptime(res['report'][0]['lv'][0]['lv_time'], '%Y-%m-%d %H:%M:%S %z').astimezone(timezone.utc) with TemporaryDirectory(prefix=f'{snn}_') as tmpdir: child = os.fork() if child == 0: unshare.unshare(unshare.CLONE_NEWNS) subprocess.run(['mount', '--make-rprivate', '/'], check=True) chroot = pathlib.Path(tmpdir) / 'chroot' upper = pathlib.Path(tmpdir) / 'upper' work = pathlib.Path(tmpdir) / 'work' for path in [chroot,upper,work]: path.mkdir() subprocess.run(['mount', '-t', 'overlay', 'overlay', '-o', f'lowerdir=/,upperdir={upper},workdir={work}', chroot], check=True) bindMounts = ['nix', 'run', 'proc', 'dev', 'sys', pathlib.Path(os.path.expanduser('~')).relative_to('/')] if 'SSH_AUTH_SOCK' in os.environ: bindMounts.append(pathlib.Path(os.environ['SSH_AUTH_SOCK']).parent.relative_to('/')) for bindMount in bindMounts: (chroot / bindMount).mkdir(parents=True,exist_ok=True) subprocess.run(['mount', '--bind', pathlib.Path('/') / bindMount, chroot / bindMount], check=True) os.chroot(chroot) os.chdir('/') dir = pathlib.Path('/borg') dir.mkdir(parents=True,exist_ok=True) subprocess.run(['mount', f'/dev/{sn}', dir], check=True) archive_lvn=lvn if "/".join(vgp[:-1]) != archive_prefix.split('.')[-2]: archive_lvn=f'{"_".join(vgp[:-1])}_{archive_lvn}' archive=f'{target}::{archive_prefix}{archive_lvn}-{{utcnow}}' env = os.environ.copy() create_args = ['borg', 'create', '--lock-wait=600', '--one-file-system', '--compression=auto,zstd,10', '--chunker-params=10,23,16,4095', f'--timestamp={creation_time.strftime("%Y-%m-%dT%H:%M:%S")}', '--show-rc', # '--progress', '--list', '--filter=AMEi-x?', '--stats', '--patterns-from=.backup-vidhar', '--exclude-caches', '--keep-exclude-tags' ] env['BORG_FILES_CACHE_SUFFIX'] = "_".join(vgp) if dry_run: create_args += ['--dry-run'] create_args += [archive] subprocess.run(create_args, cwd=dir, check=True, env=env) os._exit(0) else: while True: waitpid, waitret = os.wait() if waitret != 0: sys.exit(waitret) if waitpid == child: break finally: subprocess.run(['lvremove', '--force', sn], stdin=subprocess.DEVNULL, check=True) def sigterm(signum, frame): raise SystemExit(128 + signum) def main(): signal.signal(signal.SIGTERM, sigterm) parser = argparse.ArgumentParser() parser.add_argument('lv', metavar='LV') parser.add_argument('--size-percent', metavar='PERCENT', type = float, default=10) parser.add_argument('--target', metavar='REPO', default='borg.vidhar:.') parser.add_argument('--archive-prefix', metavar='REPO', default='yggdrasil.niflheim.ymir.') parser.add_argument('--dry-run', action='store_true') args = parser.parse_args() return borg_lv(**vars(args)) if __name__ == "__main__": sys.exit(main())