Skip to content

Commit 7329ee3

Browse files
Support rotation of backups
1 parent 5414a17 commit 7329ee3

File tree

5 files changed

+107
-25
lines changed

5 files changed

+107
-25
lines changed

README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Features
3434
- `Nagios NSCA <https://sourceforge.net/p/nagios/nsca>`__ push
3535
notification support (*optional*)
3636
- Modular backup, archiving, upload and notification components
37+
- Rotation of backups by time or count
3738
- Multi-threaded, single executable
3839
- Auto-scales to number of available CPUs by default
3940

conf/mongodb-consistent-backup.example.conf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ production:
1919
# binary: [path] (default: /usr/bin/mongodump)
2020
# compression: [auto|none|gzip] (default: auto - enable gzip if supported)
2121
# threads: [1-16] (default: auto-generated - shards/cpu)
22+
#rotate:
23+
# max_backups: [1+]
24+
# max_backup_days: [0.1+]
2225
#replication:
2326
# max_lag_secs: [1+] (default: 10)
2427
# min_priority: [0-999] (default: 0)

mongodb_consistent_backup/Main.py

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,6 @@ def setup_signal_handlers(self):
9292

9393
def set_backup_dirs(self):
9494
self.backup_root_directory = os.path.join(self.config.backup.location, self.config.backup.name)
95-
self.backup_latest_symlink = os.path.join(self.backup_root_directory, "latest")
96-
self.backup_previous_symlink = os.path.join(self.backup_root_directory, "previous")
9795
self.backup_root_subdirectory = os.path.join(self.config.backup.name, self.backup_time)
9896
self.backup_directory = os.path.join(self.config.backup.location, self.backup_root_subdirectory)
9997

@@ -153,25 +151,10 @@ def stop_timer(self):
153151
self.timer.stop(self.timer_name)
154152
self.state.set('timers', self.timer.dump())
155153

156-
def read_symlink_latest(self):
157-
if os.path.islink(self.backup_latest_symlink):
158-
return os.readlink(self.backup_latest_symlink)
159-
160-
def update_symlinks(self):
161-
latest = self.read_symlink_latest()
162-
if latest:
163-
logging.info("Updating %s previous symlink to: %s" % (self.config.backup.name, latest))
164-
if os.path.islink(self.backup_previous_symlink):
165-
os.remove(self.backup_previous_symlink)
166-
os.symlink(latest, self.backup_previous_symlink)
167-
if os.path.islink(self.backup_latest_symlink):
168-
os.remove(self.backup_latest_symlink)
169-
logging.info("Updating %s latest symlink to: %s" % (self.config.backup.name, self.backup_directory))
170-
return os.symlink(self.backup_directory, self.backup_latest_symlink)
171-
172154
def rotate_backups(self):
173-
rotater = Rotate(self.config, self.state_root)
174-
rotater.run()
155+
rotater = Rotate(self.config, self.state_root, self.state)
156+
rotater.rotate()
157+
rotater.symlink()
175158

176159
# TODO Rename class to be more exact as this assumes something went wrong
177160
# noinspection PyUnusedLocal
@@ -473,7 +456,6 @@ def run(self):
473456
self.notify.close()
474457
self.exception("Problem running Notifier! Error: %s" % e, e)
475458

476-
self.update_symlinks()
477459
self.rotate_backups()
478460

479461
self.logger.rotate()
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import logging
2+
import os
3+
4+
from math import ceil
5+
from shutil import rmtree
6+
from time import time
7+
8+
from mongodb_consistent_backup.Errors import OperationError
9+
10+
11+
class Rotate(object):
12+
def __init__(self, config, state_root, state_bkp):
13+
self.config = config
14+
self.state_root = state_root
15+
self.state_bkp = state_bkp
16+
self.backup_name = self.config.backup.name
17+
self.max_days = self.config.rotate.max_backup_days
18+
self.max_backups = self.config.rotate.max_backups
19+
20+
self.latest = state_bkp.get("name")
21+
self.previous = None
22+
self.backups = self.backups_by_unixts()
23+
24+
self.base_dir = os.path.join(self.config.backup.location, self.config.backup.name)
25+
self.latest_symlink = os.path.join(self.base_dir, "latest")
26+
self.previous_symlink = os.path.join(self.base_dir, "previous")
27+
28+
self.max_secs = 0
29+
if self.max_days > 0:
30+
seconds = float(self.max_days) * 86400.00
31+
self.max_secs = int(ceil(seconds))
32+
33+
def backups_by_unixts(self):
34+
backups = {}
35+
for name in self.state_root.backups:
36+
backup = self.state_root.backups[name]
37+
backup_time = backup["updated_at"]
38+
backups[backup_time] = backup
39+
return backups
40+
41+
def remove(self, ts):
42+
if ts in self.backups:
43+
backup = self.backups[ts]
44+
path = os.path.join(self.base_dir, backup["name"])
45+
if os.path.isdir(path):
46+
logging.debug("Removing backup path: %s" % path)
47+
rmtree(path)
48+
else:
49+
raise OperationError("Backup path %s does not exist!" % path)
50+
if self.previous == backup["name"]:
51+
self.previous = None
52+
del self.backups[ts]
53+
54+
def rotate(self):
55+
if self.max_days == 0 and self.max_backups == 0:
56+
logging.info("Backup rotation is disabled, skipping")
57+
return
58+
logging.info("Rotating backups (max_num=%i, max_days=%.2f)" % (self.max_backups, self.max_days))
59+
kept_backups = 1
60+
now = int(time())
61+
for ts in sorted(self.backups.iterkeys(), reverse=True):
62+
backup = self.backups[ts]
63+
if not self.previous:
64+
self.previous = backup["name"]
65+
if self.max_backups == 0 or kept_backups < self.max_backups:
66+
if self.max_secs > 0 and (now - ts) > self.max_secs:
67+
logging.info("Backup %s exceeds max age %.2f days, removing backup" % (backup["name"], self.max_days))
68+
self.remove(ts)
69+
continue
70+
logging.info("Keeping backup %s" % backup["name"])
71+
kept_backups += 1
72+
else:
73+
logging.info("Backup %s exceeds max backup count %i, removing backup" % (backup["name"], self.max_backups))
74+
self.remove(ts)
75+
76+
def symlink(self):
77+
try:
78+
if os.path.islink(self.latest_symlink):
79+
os.remove(self.latest_symlink)
80+
latest = os.path.join(self.base_dir, self.latest)
81+
logging.info("Updating %s latest symlink to current backup path: %s" % (self.backup_name, latest))
82+
os.symlink(latest, self.latest_symlink)
83+
84+
if os.path.islink(self.previous_symlink):
85+
os.remove(self.previous_symlink)
86+
if self.previous:
87+
previous = os.path.join(self.base_dir, self.previous)
88+
logging.info("Updating %s previous symlink to: %s" % (self.backup_name, previous))
89+
os.symlink(previous, self.previous_symlink)
90+
except Exception, e:
91+
logging.error("Error creating backup symlinks: %s" % e)
92+
raise OperationError(e)

mongodb_consistent_backup/State.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ def load(self, load_one=False, filename=None):
5353
if f:
5454
f.close()
5555

56+
def get(self, key):
57+
if key in self.state:
58+
return self.state[key]
59+
60+
def set(self, name, summary):
61+
self.state[name] = summary
62+
self.write(True)
63+
5664
def write(self, do_merge=False):
5765
f = None
5866
try:
@@ -121,10 +129,6 @@ def __init__(self, base_dir, config, backup_time, seed_uri, argv=None):
121129
def init(self):
122130
logging.info("Initializing backup state directory: %s" % self.base_dir)
123131

124-
def set(self, name, summary):
125-
self.state[name] = summary
126-
self.write(True)
127-
128132

129133
class StateRoot(StateBase):
130134
def __init__(self, base_dir, config):

0 commit comments

Comments
 (0)