diff options
| -rw-r--r-- | snap.py | 104 |
1 files changed, 104 insertions, 0 deletions
diff --git a/snap.py b/snap.py new file mode 100644 index 00000000..cc75739b --- /dev/null +++ b/snap.py | |||
| @@ -0,0 +1,104 @@ | |||
| 1 | #! /usr/bin/env nix-shell | ||
| 2 | #! nix-shell -i python -p "python3.withPackages (p: with p; [dateutil python-unshare])" | ||
| 3 | |||
| 4 | import os | ||
| 5 | import argparse | ||
| 6 | import sys | ||
| 7 | from datetime import (datetime,timezone) | ||
| 8 | from dateutil.tz import (tzlocal) | ||
| 9 | import subprocess | ||
| 10 | import json | ||
| 11 | |||
| 12 | from tempfile import TemporaryDirectory | ||
| 13 | |||
| 14 | import pathlib | ||
| 15 | |||
| 16 | import unshare | ||
| 17 | |||
| 18 | |||
| 19 | def borg_lv(lv, size_percent, target, archive_prefix, **args): | ||
| 20 | vgp = lv.split('/') | ||
| 21 | lvn = vgp[-1] | ||
| 22 | snn = f'{lvn}_snap_{datetime.utcnow().strftime("%Y%m%dT%H%MZ")}' | ||
| 23 | sn = "/".join([*vgp[:-1], snn]) | ||
| 24 | |||
| 25 | try: | ||
| 26 | subprocess.run(['lvcreate', f'-l{size_percent}%ORIGIN', '-s', '-n', snn, lv], stdin=subprocess.DEVNULL, check=True) | ||
| 27 | |||
| 28 | creation_time = None | ||
| 29 | with subprocess.Popen(['lvs', '--reportformat=json', '-olv_time', sn], stdout=subprocess.PIPE) as proc: | ||
| 30 | res = json.load(proc.stdout) | ||
| 31 | creation_time = datetime.strptime(res['report'][0]['lv'][0]['lv_time'], '%Y-%m-%d %H:%M:%S %z').astimezone(timezone.utc) | ||
| 32 | |||
| 33 | with TemporaryDirectory(prefix=f'{snn}_') as tmpdir: | ||
| 34 | child = os.fork() | ||
| 35 | if child == 0: | ||
| 36 | unshare.unshare(unshare.CLONE_NEWNS) | ||
| 37 | subprocess.run(['mount', '--make-rprivate', '/'], check=True) | ||
| 38 | chroot = pathlib.Path(tmpdir) / 'chroot' | ||
| 39 | upper = pathlib.Path(tmpdir) / 'upper' | ||
| 40 | work = pathlib.Path(tmpdir) / 'work' | ||
| 41 | for path in [chroot,upper,work]: | ||
| 42 | path.mkdir() | ||
| 43 | subprocess.run(['mount', '-t', 'overlay', 'overlay', '-o', f'lowerdir=/,upperdir={upper},workdir={work}', chroot], check=True) | ||
| 44 | bindMounts = ['nix', 'run', 'proc', 'dev', 'sys', pathlib.Path(os.path.expanduser('~')).relative_to('/')] | ||
| 45 | if 'SSH_AUTH_SOCK' in os.environ: | ||
| 46 | bindMounts.append(pathlib.Path(os.environ['SSH_AUTH_SOCK']).parent.relative_to('/')) | ||
| 47 | for bindMount in bindMounts: | ||
| 48 | (chroot / bindMount).mkdir(parents=True,exist_ok=True) | ||
| 49 | subprocess.run(['mount', '--bind', pathlib.Path('/') / bindMount, chroot / bindMount], check=True) | ||
| 50 | os.chroot(chroot) | ||
| 51 | os.chdir('/') | ||
| 52 | dir = pathlib.Path('/borg') | ||
| 53 | dir.mkdir(parents=True,exist_ok=True) | ||
| 54 | subprocess.run(['mount', f'/dev/{sn}', dir], check=True) | ||
| 55 | |||
| 56 | archive_lvn=lvn | ||
| 57 | if "/".join(vgp[:-1]) != archive_prefix.split('.')[-2]: | ||
| 58 | archive_lvn=f'{"_".join(vgp[:-1])}_{archive_lvn}' | ||
| 59 | archive=f'{target}::{archive_prefix}{archive_lvn}-{{utcnow}}' | ||
| 60 | |||
| 61 | env = os.environ.copy() | ||
| 62 | create_args = ['borg', | ||
| 63 | 'create', | ||
| 64 | '--lock-wait=600', | ||
| 65 | '--one-file-system', | ||
| 66 | '--compression=auto,zstd,10', | ||
| 67 | '--chunker-params=10,23,16,4095', | ||
| 68 | f'--timestamp={creation_time.strftime("%Y-%m-%dT%H:%M:%S")}', | ||
| 69 | '--show-rc', | ||
| 70 | '--progress', | ||
| 71 | '--list', | ||
| 72 | '--filter=AMEi-x?', | ||
| 73 | '--stats', | ||
| 74 | '--patterns-from=.backup-vidhar', | ||
| 75 | '--exclude-caches', | ||
| 76 | '--keep-exclude-tags' | ||
| 77 | ] | ||
| 78 | env['BORG_FILES_CACHE_SUFFIX'] = "_".join(vgp) | ||
| 79 | create_args += [archive] | ||
| 80 | subprocess.run(create_args, cwd=dir, check=True, env=env) | ||
| 81 | |||
| 82 | os._exit(0) | ||
| 83 | else: | ||
| 84 | while True: | ||
| 85 | waitpid, waitret = os.wait() | ||
| 86 | if waitret != 0: | ||
| 87 | sys.exit(waitret) | ||
| 88 | if waitpid == child: | ||
| 89 | break | ||
| 90 | finally: | ||
| 91 | subprocess.run(['lvremove', '--force', sn], stdin=subprocess.DEVNULL, check=True) | ||
| 92 | |||
| 93 | def main(): | ||
| 94 | parser = argparse.ArgumentParser() | ||
| 95 | parser.add_argument('lv', metavar='LV') | ||
| 96 | parser.add_argument('--size-percent', metavar='PERCENT', type = float, default=10) | ||
| 97 | parser.add_argument('--target', metavar='REPO', default='borg.vidhar:.') | ||
| 98 | parser.add_argument('--archive-prefix', metavar='REPO', default='yggdrasil.niflheim.ymir.') | ||
| 99 | args = parser.parse_args() | ||
| 100 | |||
| 101 | return borg_lv(**vars(args)) | ||
| 102 | |||
| 103 | if __name__ == "__main__": | ||
| 104 | sys.exit(main()) | ||
