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