#!/usr/bin/env python3
import os
import sys
-from subprocess import Popen, PIPE
+from subprocess import check_call, CalledProcessError
import shlex
import datetime
+import configparser
-def backup_host(host, base_dir, fp):
- dest_dir = os.path.join(base_dir, host, "latest")
- if not os.path.exists(base_dir):
- print("New host",host,".")
- run("btrfs subvol create %s"% os.path.join(base_dir, host))
- os.makedirs(dest_dir)
- command = ("rsync -a --numeric-ids --delete --delete-excluded --human-readable --inplace ")
- excludes = fp.readline()[:-1]
- (stdout,stderr) = run(command + excludes + " root@%s:/ "%(host) + dest_dir)
- if stdout:
- print(stdout)
- if stderr:
- print(stdout)
- run("btrfs subvol snapshot %s %s"%(os.path.join(base_dir, host),os.path.join(base_dir, host, datetime.datetime.now().date().strftime("%F"))))
-
-def run(cmd):
- (stdout, stderr) = Popen(shlex.split(cmd), stdout=PIPE).communicate()
- if stdout:
- stdout = stdout.decode('utf-8')
- if stderr:
- stderr = stderr.decode('utf-8')
- return(stdout, stderr)
+class Host():
+ def __init__(self, name, config):
+ self.name = name
+ self.config = config
+ if not self.config.has_section('host'):
+ self.config.add_section('host')
+ self.store_dir = self.config['host']['store_dir']
+ self.host_dir = os.path.join(self.store_dir, name)
+ self.subvol_dir = os.path.join(self.host_dir, "latest")
+ self.keep = int(self.config.get("host", "keep", fallback=-1))
+
+ def backup(self):
+ if not os.path.exists(self.host_dir):
+ print("New host",self.name,".")
+ os.makedirs(self.host_dir)
+ if not os.path.exists(self.subvol_dir):
+ try:
+ check_call(shlex.split("btrfs subvol create %s"% self.subvol_dir))
+ except CalledProcessError as ex:
+ print("Failed to create subvol! Aborting backup.")
+ return()
+
+ command = ("rsync -a --acls --xattrs --whole-file --numeric-ids --delete --delete-excluded --human-readable --inplace ")
+ excludes = " --exclude " + " --exclude ".join(self.config.get("host", "exclude").split(',')) #FIXME
+ try:
+ print(command + excludes + " root@%s:/ "%(self.name) + self.subvol_dir)
+ check_call(shlex.split(command + excludes + " root@%s:/ "%(self.name) + self.subvol_dir))
+ except CalledProcessError as ex:
+ if ex.returncode in (24,):
+ pass
+ else:
+ print("Rsync error from %s, skipping snapshot. Rsync exit value=%s"%(self.name, ex.returncode))
+ return()
+ todays_date = datetime.datetime.now().date().strftime("%F")
+ if os.path.exists(os.path.join(self.host_dir, todays_date)):
+ #There is a snapshot for today, removing it and creating a new one
+ try:
+ check_call(shlex.split("btrfs subvol delete %s"%(os.path.join(self.host_dir, todays_date))))
+ except CalledProcessError as ex:
+ pass
+ try:
+ check_call(shlex.split("btrfs subvol snapshot -r %s %s"%(self.subvol_dir,os.path.join(self.host_dir, todays_date))))
+ except CalledProcessError as ex:
+ pass
+
+ def prune_snapshots(self):
+ if self.keep == -1:
+ print("No keep specified for %s, keeping all"%self.name)
+ return
+ if not os.path.exists(self.host_dir):
+ print("New host, no pruning needed")
+ return
+ snaps = sorted([snap for snap in os.listdir(self.host_dir) if not snap == "latest" ], reverse=True)
+ while len(snaps) > self.keep:
+ snap = snaps.pop()
+ try:
+ check_call(shlex.split("btrfs subvol delete %s"%(os.path.join(self.host_dir, snap))))
+ except CalledProcessError as ex:
+ pass
+class BackupRunner():
+ def __init__(self, config_dir):
+ self.config_dir = config_dir
+ if not os.path.exists(self.config_dir):
+ print("No config found", self.config_dir)
+ sys-exit(-1)
+
+ def run(self):
+ self.hosts = os.listdir(self.config_dir)
+
+ for host in self.hosts:
+ if host == 'default.cfg':
+ continue
+ try:
+ config = configparser.ConfigParser(strict=False)
+ config.read_file(open(os.path.join(self.config_dir, 'default.cfg'),'r'))
+ config.read(os.path.join(self.config_dir, host))
+ except BaseException as ex:
+ print("Config error for %s. Skipping host."%host)
+ continue
+ h = Host(host, config)
+ h.prune_snapshots()
+ h.backup()
+
if __name__ == "__main__":
if os.geteuid() != 0:
print("You need to be root. Otherwise all permissions will be lost.")
sys.exit(-1)
- base_path="/etc/butterbackup"
- dest_dir="/mnt/data2"
- if not os.path.exists(base_path):
- print("No hosts to backup, please place them in",base_path)
- sys-exit(-1)
- hosts = os.listdir(base_path)
- for host in hosts:
- fp = open(os.path.join(base_path, host),"r")
- backup_host(host, dest_dir, fp)
+ br = BackupRunner("/etc/butterbackup")
+ br.run()
sys.exit(0)