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