summaryrefslogtreecommitdiff
path: root/snap.py
blob: 80487292a3945ef41cedaed08fc7b761462275ce (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/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


children = set()

def waitchildren():
    while children:
        waitpid, waitret = os.wait()
        children.remove(waitpid)


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-?',
                               '--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:
                children.add(child)
                waitchildren()
    finally:
        waitchildren()
        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())