214 lines
6.1 KiB
Django/Jinja
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()
|