infra/roles/backup/templates/sdfbackup.py.j2

214 lines
6.1 KiB
Django/Jinja

#!/usr/bin/env python3
import os
import socket
import subprocess
import json
from time import sleep
import sys
#from influxdb import InfluxDBClient
from loguru import logger
#import sudoisinflux
### https://ruderich.org/simon/notes/encrypted-remote-backups
class SDF(object):
def __exit__(self, exc_type, exc_value, traceback):
self.unmount()
def __enter__(self):
return self.mount()
def assert_nonempty_dir(self, path):
assert os.path.isdir(path)
assert len(os.listdir(path)) > 0
def assert_unmounted(self):
assert os.stat(self.mountpoint)
assert os.path.isdir(self.mountpoint)
assert not os.path.isfile(self.check_file)
assert len(os.listdir(self.mountpoint)) == 0
def assert_mounted(self):
assert os.path.isfile(self.check_file)
self.assert_nonempty_dir(self.mountpoint)
def do_sync(self):
sleep(10.0)
logger.debug("starting 'sync'")
subprocess.run(["sync"], check=True)
sleep(3.0)
class SSHMount(SDF):
def __init__(self, destination, mountpoint):
self.destination = destination
self.mountpoint = mountpoint
self.check_file = os.path.join(self.mountpoint, ".sdf_ssh")
def mount(self):
#self.assert_unmounted()
p = subprocess.run(
["sshfs", "-o", "idmap=user,gid=0", self.destination, self.mountpoint],
check=True
)
#self.assert_mounted()
c = len(os.listdir(self.mountpoint))
logger.debug(f"mounted '{self.destination}' as '{self.mountpoint}': {c} files")
return self
def unmount(self):
#self.assert_mounted()
#self.do_sync()
umount_cmds = [["umount"], ["umount", "-f"], ["fusermount", "-u"]]
for umount in umount_cmds:
try:
subprocess.run(umount + [self.mountpoint], check=True)
break
except subprocess.CalledProcessError as e:
logger.error(e)
continue
else:
logger.error(f"giving up on unmounting '{self.mountpoint}'")
raise SystemExit(1)
#self.assert_unmounted()
logger.debug(f"unmounted '{self.mountpoint}'")
class LUKSMount(SDF):
def __init__(self, path, mountpoint, key_path):
self.path = path
self.mountpoint = mountpoint
self.key_path = key_path
self.name = os.path.splitext(os.path.basename(self.path))[0]
self.check_file = os.path.join(self.mountpoint, ".sdf_luks")
def mount(self):
#self.assert_unmounted()
key_param = f"--key-file={self.key_path}"
crypts = "/usr/sbin/cryptsetup"
subprocess.run(
[crypts, "luksOpen", self.path, self.name, key_param],
check=True
)
subprocess.run(
['/usr/bin/mount', f'/dev/mapper/{self.name}', self.mountpoint],
check=True
)
#self.assert_mounted()
logger.debug(f"luksOpen '{self.path}' on '{self.mountpoint}'..")
return self
def unmount(self):
#self.assert_mounted()
self.do_sync()
logger.debug(os.listdir(self.mountpoint))
subprocess.run(["umount", self.mountpoint], check=True)
self.assert_unmounted()
subprocess.run(
["/usr/sbin/cryptsetup", "luksClose", self.name],
check=True
)
logger.debug(f"closed LUKS '{self.name}' and unmounted '{self.mountpoint}'")
def rsync(self, src_root, paths):
self.assert_nonempty_dir(src_root)
for path in paths:
assert not path.endswith("/")
self._rsync(os.path.join(src_root, path))
def _rsync(self, path):
rsync_cmd = [
"/usr/bin/rsync", "-rah", "--numeric-ids",
"--exclude", "ruv-dl", # move to config
"--delete-after", path, self.mountpoint
]
logger.debug(" ".join(rsync_cmd))
subprocess.run(rsync_cmd, check=True)
@logger.catch
def main():
with open('/usr/local/etc/sdfbackup.json') as f:
config = json.load(f)
# where the LUKS volume gets mounted when its open
luks_mountpoint = config['luks_args']['mountpoint']
key_path = config['luks_args']['key_path']
if '-v' not in sys.argv:
logger.remove()
logger.add(sys.stderr, level="WARNING")
logger.add("/var/log/sdfbackup.log")
if "--open" in sys.argv:
# mount the sshfs and luks, exit and leave it open
ssh = SSHMount(**config['ssh_args'])
ssh.mount()
path = os.path.join(config['ssh_args']['mountpoint'], config['luks_args']['luks_img'])
luks = LUKSMount(path, luks_mountpoint, key_path)
luks.mount()
logger.warning(f"mounted '{luks.name}' on '{luks_mountpoint}', please close manually")
sys.exit(0)
logger.debug("starting sdfbackup.py..")
with SSHMount(**config['ssh_args']) as ssh:
# the path the the LUKS file image, in the sshfs mountpoint
path = os.path.join(ssh.mountpoint, config['luks_args']['luks_img'])
with LUKSMount(path, luks_mountpoint, key_path) as luks:
df0 = subprocess.run(["df", "-h", luks_mountpoint], capture_output=True).stdout.splitlines()[1]
logger.debug(df0)
luks.rsync(config['src_root'], config['paths'])
df1 = subprocess.run(["df", "-h", luks_mountpoint], capture_output=True).stdout.decode().splitlines()[1]
used_percent = df1.split()[-2][:-1]
avail_gb = df1.split()[-3][:-1]
used_gb = df1.split()[-4][:-1]
logger.debug(df1)
with open("/root/.sdfbackup.json", 'w') as f:
f.write(json.dumps({
'used_percent': int(used_percent),
'avail_gb': int(avail_gb),
'used_gb': int(used_gb)
}))
logger.debug("tree: start")
treecmd = [
"tree", luks_mountpoint, "-L", "8", "-o", "/root/sdftree.txt"
]
subprocess.run(treecmd, check=True)
logger.debug("tree finished")
logger.debug("exiting sdfbackup.py..")
if __name__ == "__main__":
main()