#!/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 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 main(): 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())