summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--snap.py104
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
4import os
5import argparse
6import sys
7from datetime import (datetime,timezone)
8from dateutil.tz import (tzlocal)
9import subprocess
10import json
11
12from tempfile import TemporaryDirectory
13
14import pathlib
15
16import unshare
17
18
19def 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
93def 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
103if __name__ == "__main__":
104 sys.exit(main())