]>
Commit | Line | Data |
---|---|---|
1 | #!/usr/bin/env python3 | |
2 | import os | |
3 | import sys | |
4 | from subprocess import check_call, CalledProcessError | |
5 | import shlex | |
6 | import datetime | |
7 | import configparser | |
8 | ||
9 | class Host(): | |
10 | def __init__(self, name, config): | |
11 | self.name = name | |
12 | self.config = config | |
13 | if not self.config.has_section('host'): | |
14 | self.config.add_section('host') | |
15 | self.store_dir = self.config['host']['store_dir'] | |
16 | self.host_dir = os.path.join(self.store_dir, name) | |
17 | self.subvol_dir = os.path.join(self.host_dir, "latest") | |
18 | self.keep = int(self.config.get("host", "keep", fallback=-1)) | |
19 | ||
20 | def backup(self): | |
21 | if not os.path.exists(self.host_dir): | |
22 | print("New host",self.name,".") | |
23 | os.makedirs(self.host_dir) | |
24 | if not os.path.exists(self.subvol_dir): | |
25 | try: | |
26 | check_call(shlex.split("btrfs subvol create %s"% self.subvol_dir)) | |
27 | except CalledProcessError as ex: | |
28 | print("Failed to create subvol! Aborting backup.") | |
29 | return() | |
30 | ||
31 | command = ("rsync -a --acls --xattrs --whole-file --numeric-ids --delete --delete-excluded --human-readable --inplace ") | |
32 | excludes = " --exclude " + " --exclude ".join(self.config.get("host", "exclude").split(',')) #FIXME | |
33 | try: | |
34 | print(command + excludes + " root@%s:/ "%(self.name) + self.subvol_dir) | |
35 | check_call(shlex.split(command + excludes + " root@%s:/ "%(self.name) + self.subvol_dir)) | |
36 | except CalledProcessError as ex: | |
37 | if ex.returncode not in (12, 30): | |
38 | print("Rsync did not transfer anything from %s, skipping snapshot."%self.name) | |
39 | return() | |
40 | todays_date = datetime.datetime.now().date().strftime("%F") | |
41 | if os.path.exists(os.path.join(self.host_dir, todays_date)): | |
42 | #There is a snapshot for today, removing it and creating a new one | |
43 | try: | |
44 | check_call(shlex.split("btrfs subvol delete %s"%(os.path.join(self.host_dir, todays_date)))) | |
45 | except CalledProcessError as ex: | |
46 | pass | |
47 | try: | |
48 | check_call(shlex.split("btrfs subvol snapshot -r %s %s"%(self.subvol_dir,os.path.join(self.host_dir, todays_date)))) | |
49 | except CalledProcessError as ex: | |
50 | pass | |
51 | ||
52 | def prune_snapshots(self): | |
53 | if self.keep == -1: | |
54 | print("No keep specified for %s, keeping all"%self.name) | |
55 | return | |
56 | if not os.path.exists(self.host_dir): | |
57 | print("New host, no pruning needed") | |
58 | return | |
59 | snaps = sorted([snap for snap in os.listdir(self.host_dir) if not snap == "latest" ], reverse=True) | |
60 | while len(snaps) > self.keep: | |
61 | snap = snaps.pop() | |
62 | try: | |
63 | check_call(shlex.split("btrfs subvol delete %s"%(os.path.join(self.host_dir, snap)))) | |
64 | except CalledProcessError as ex: | |
65 | pass | |
66 | ||
67 | class BackupRunner(): | |
68 | def __init__(self, config_dir): | |
69 | self.config_dir = config_dir | |
70 | if not os.path.exists(self.config_dir): | |
71 | print("No config found", self.config_dir) | |
72 | sys-exit(-1) | |
73 | ||
74 | def run(self, hostlist=None): | |
75 | self.hosts = hostlist or os.listdir(self.config_dir) | |
76 | ||
77 | for host in self.hosts: | |
78 | if host == 'default.cfg': | |
79 | continue | |
80 | try: | |
81 | configfile = os.path.join(self.config_dir, host) | |
82 | ||
83 | if not os.path.exists(configfile): | |
84 | # Trigger logging in the except clause | |
85 | raise BaseException() | |
86 | ||
87 | config = configparser.ConfigParser(strict=False) | |
88 | config.read_file(open(os.path.join(self.config_dir, 'default.cfg'),'r')) | |
89 | config.read(configfile) | |
90 | except BaseException as ex: | |
91 | print("Config error for %s. Skipping host."%host) | |
92 | continue | |
93 | h = Host(host, config) | |
94 | h.prune_snapshots() | |
95 | h.backup() | |
96 | ||
97 | if __name__ == "__main__": | |
98 | if os.geteuid() != 0: | |
99 | print("You need to be root. Otherwise all permissions will be lost.") | |
100 | sys.exit(-1) | |
101 | br = BackupRunner("/etc/butterbackup") | |
102 | ||
103 | hostlist = sys.argv[1:] | |
104 | br.run(hostlist=hostlist) | |
105 | ||
106 | sys.exit(0) | |
107 |