From 5414a17271f57bf472ee049cdc8f97ec8017ec4a Mon Sep 17 00:00:00 2001 From: Tim Vaillancourt Date: Thu, 14 Sep 2017 21:44:27 +0200 Subject: [PATCH 01/13] Basic rotation logic working --- mongodb_consistent_backup/Common/Config.py | 2 ++ mongodb_consistent_backup/Main.py | 15 +++++++++---- mongodb_consistent_backup/State.py | 26 ++++++++++------------ 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/mongodb_consistent_backup/Common/Config.py b/mongodb_consistent_backup/Common/Config.py index fc04991b..66a6dee0 100644 --- a/mongodb_consistent_backup/Common/Config.py +++ b/mongodb_consistent_backup/Common/Config.py @@ -73,6 +73,8 @@ def makeParser(self): parser.add_argument("--ssl.client_cert_file", dest="ssl.client_cert_file", help="Path to Client SSL Certificate file in PEM format (for optional client ssl auth)", default=None, type=str) parser.add_argument("-L", "--log-dir", dest="log_dir", help="Path to write log files to (default: disabled)", default='', type=str) parser.add_argument("--lock-file", dest="lock_file", help="Location of lock file (default: /tmp/mongodb-consistent-backup.lock)", default='/tmp/mongodb-consistent-backup.lock', type=str) + parser.add_argument("--rotate.max_backups", dest="rotate.max_backups", help="Maximum number of backups to keep in backup directory (default: disabled)", default=0, type=int) + parser.add_argument("--rotate.max_backup_days", dest="rotate.max_backup_days", help="Maximum age in days for backups in backup directory (default: disabled)", default=0, type=int) parser.add_argument("--sharding.balancer.wait_secs", dest="sharding.balancer.wait_secs", help="Maximum time to wait for balancer to stop, in seconds (default: 300)", default=300, type=int) parser.add_argument("--sharding.balancer.ping_secs", dest="sharding.balancer.ping_secs", help="Interval to check balancer state, in seconds (default: 3)", default=3, type=int) return self.makeParserLoadSubmodules(parser) diff --git a/mongodb_consistent_backup/Main.py b/mongodb_consistent_backup/Main.py index 9dd4975a..c1808450 100644 --- a/mongodb_consistent_backup/Main.py +++ b/mongodb_consistent_backup/Main.py @@ -14,8 +14,9 @@ from Notify import Notify from Oplog import Tailer, Resolver from Replication import Replset, ReplsetSharded +from Rotate import Rotate from Sharding import Sharding -from State import StateRoot, StateBackup, StateBackupReplset, StateDoneStamp, StateOplog +from State import StateRoot, StateBackup, StateBackupReplset, StateOplog from Upload import Upload @@ -97,8 +98,9 @@ def set_backup_dirs(self): self.backup_directory = os.path.join(self.config.backup.location, self.backup_root_subdirectory) def setup_state(self): - StateRoot(self.backup_root_directory, self.config).write(True) - self.state = StateBackup(self.backup_directory, self.config, self.backup_time, self.uri, sys.argv) + self.state_root = StateRoot(self.backup_root_directory, self.config) + self.state = StateBackup(self.backup_directory, self.config, self.backup_time, self.uri, sys.argv) + self.state_root.write(True) self.state.write() def setup_notifier(self): @@ -167,6 +169,10 @@ def update_symlinks(self): logging.info("Updating %s latest symlink to: %s" % (self.config.backup.name, self.backup_directory)) return os.symlink(self.backup_directory, self.backup_latest_symlink) + def rotate_backups(self): + rotater = Rotate(self.config, self.state_root) + rotater.run() + # TODO Rename class to be more exact as this assumes something went wrong # noinspection PyUnusedLocal def cleanup_and_exit(self, code, frame): @@ -451,6 +457,7 @@ def run(self): # stop timer self.stop_timer() + self.state.set("completed", True) # send notifications of backup state try: @@ -466,8 +473,8 @@ def run(self): self.notify.close() self.exception("Problem running Notifier! Error: %s" % e, e) - StateDoneStamp(self.backup_directory, self.config).write() self.update_symlinks() + self.rotate_backups() self.logger.rotate() logging.info("Completed %s in %.2f sec" % (self.program_name, self.timer.duration(self.timer_name))) diff --git a/mongodb_consistent_backup/State.py b/mongodb_consistent_backup/State.py index 7a21bd9b..4c3bbe08 100644 --- a/mongodb_consistent_backup/State.py +++ b/mongodb_consistent_backup/State.py @@ -37,10 +37,12 @@ def merge(self, new, old): merged.update(new) return merged - def load(self, load_one=False): + def load(self, load_one=False, filename=None): f = None + if not filename: + filename = self.state_file try: - f = open(self.state_file, "r") + f = open(filename, "r") data = decode_all(f.read()) if load_one and len(data) > 0: return data[0] @@ -94,6 +96,7 @@ def __init__(self, base_dir, config, backup_time, seed_uri, argv=None): StateBase.__init__(self, base_dir, config) self.base_dir = base_dir self.state['backup'] = True + self.state['completed'] = False self.state['name'] = backup_time self.state['method'] = config.backup.method self.state['path'] = base_dir @@ -128,6 +131,8 @@ def __init__(self, base_dir, config): StateBase.__init__(self, base_dir, config) self.base_dir = base_dir self.state['root'] = True + self.backups = {} + self.completed_backups = 0 self.init() @@ -136,7 +141,6 @@ def init(self): self.load_backups() def load_backups(self): - backups = [] if os.path.isdir(self.base_dir): for subdir in os.listdir(self.base_dir): try: @@ -145,16 +149,10 @@ def load_backups(self): continue state_path = os.path.join(bkp_path, self.meta_name) state_file = os.path.join(state_path, "meta.bson") - done_path = os.path.join(state_path, "done.bson") - if os.path.isdir(state_path) and os.path.isfile(state_file) and os.path.isfile(done_path): - backups.append(state_file) + self.backups[subdir] = self.load(True, state_file) + if self.backups[subdir]["completed"]: + self.completed_backups += 1 except: continue - logging.info("Found %i existing completed backups for set" % len(backups)) - return backups - - -class StateDoneStamp(StateBase): - def __init__(self, base_dir, config): - StateBase.__init__(self, base_dir, config, "done.bson") - self.state = {'done': True} + logging.info("Found %i existing completed backups for set" % self.completed_backups) + return self.backups From 7329ee36e87e6ebfc6336353159ab33d54de168b Mon Sep 17 00:00:00 2001 From: Tim Vaillancourt Date: Thu, 14 Sep 2017 23:16:21 +0200 Subject: [PATCH 02/13] Support rotation of backups --- README.rst | 1 + conf/mongodb-consistent-backup.example.conf | 3 + mongodb_consistent_backup/Main.py | 24 +----- mongodb_consistent_backup/Rotate.py | 92 +++++++++++++++++++++ mongodb_consistent_backup/State.py | 12 ++- 5 files changed, 107 insertions(+), 25 deletions(-) create mode 100644 mongodb_consistent_backup/Rotate.py diff --git a/README.rst b/README.rst index f87a7a1e..b0997a1a 100644 --- a/README.rst +++ b/README.rst @@ -34,6 +34,7 @@ Features - `Nagios NSCA `__ push notification support (*optional*) - Modular backup, archiving, upload and notification components +- Rotation of backups by time or count - Multi-threaded, single executable - Auto-scales to number of available CPUs by default diff --git a/conf/mongodb-consistent-backup.example.conf b/conf/mongodb-consistent-backup.example.conf index dc51bde1..82bf74ae 100644 --- a/conf/mongodb-consistent-backup.example.conf +++ b/conf/mongodb-consistent-backup.example.conf @@ -19,6 +19,9 @@ production: # binary: [path] (default: /usr/bin/mongodump) # compression: [auto|none|gzip] (default: auto - enable gzip if supported) # threads: [1-16] (default: auto-generated - shards/cpu) + #rotate: + # max_backups: [1+] + # max_backup_days: [0.1+] #replication: # max_lag_secs: [1+] (default: 10) # min_priority: [0-999] (default: 0) diff --git a/mongodb_consistent_backup/Main.py b/mongodb_consistent_backup/Main.py index c1808450..036b186d 100644 --- a/mongodb_consistent_backup/Main.py +++ b/mongodb_consistent_backup/Main.py @@ -92,8 +92,6 @@ def setup_signal_handlers(self): def set_backup_dirs(self): self.backup_root_directory = os.path.join(self.config.backup.location, self.config.backup.name) - self.backup_latest_symlink = os.path.join(self.backup_root_directory, "latest") - self.backup_previous_symlink = os.path.join(self.backup_root_directory, "previous") self.backup_root_subdirectory = os.path.join(self.config.backup.name, self.backup_time) self.backup_directory = os.path.join(self.config.backup.location, self.backup_root_subdirectory) @@ -153,25 +151,10 @@ def stop_timer(self): self.timer.stop(self.timer_name) self.state.set('timers', self.timer.dump()) - def read_symlink_latest(self): - if os.path.islink(self.backup_latest_symlink): - return os.readlink(self.backup_latest_symlink) - - def update_symlinks(self): - latest = self.read_symlink_latest() - if latest: - logging.info("Updating %s previous symlink to: %s" % (self.config.backup.name, latest)) - if os.path.islink(self.backup_previous_symlink): - os.remove(self.backup_previous_symlink) - os.symlink(latest, self.backup_previous_symlink) - if os.path.islink(self.backup_latest_symlink): - os.remove(self.backup_latest_symlink) - logging.info("Updating %s latest symlink to: %s" % (self.config.backup.name, self.backup_directory)) - return os.symlink(self.backup_directory, self.backup_latest_symlink) - def rotate_backups(self): - rotater = Rotate(self.config, self.state_root) - rotater.run() + rotater = Rotate(self.config, self.state_root, self.state) + rotater.rotate() + rotater.symlink() # TODO Rename class to be more exact as this assumes something went wrong # noinspection PyUnusedLocal @@ -473,7 +456,6 @@ def run(self): self.notify.close() self.exception("Problem running Notifier! Error: %s" % e, e) - self.update_symlinks() self.rotate_backups() self.logger.rotate() diff --git a/mongodb_consistent_backup/Rotate.py b/mongodb_consistent_backup/Rotate.py new file mode 100644 index 00000000..6fa972f3 --- /dev/null +++ b/mongodb_consistent_backup/Rotate.py @@ -0,0 +1,92 @@ +import logging +import os + +from math import ceil +from shutil import rmtree +from time import time + +from mongodb_consistent_backup.Errors import OperationError + + +class Rotate(object): + def __init__(self, config, state_root, state_bkp): + self.config = config + self.state_root = state_root + self.state_bkp = state_bkp + self.backup_name = self.config.backup.name + self.max_days = self.config.rotate.max_backup_days + self.max_backups = self.config.rotate.max_backups + + self.latest = state_bkp.get("name") + self.previous = None + self.backups = self.backups_by_unixts() + + self.base_dir = os.path.join(self.config.backup.location, self.config.backup.name) + self.latest_symlink = os.path.join(self.base_dir, "latest") + self.previous_symlink = os.path.join(self.base_dir, "previous") + + self.max_secs = 0 + if self.max_days > 0: + seconds = float(self.max_days) * 86400.00 + self.max_secs = int(ceil(seconds)) + + def backups_by_unixts(self): + backups = {} + for name in self.state_root.backups: + backup = self.state_root.backups[name] + backup_time = backup["updated_at"] + backups[backup_time] = backup + return backups + + def remove(self, ts): + if ts in self.backups: + backup = self.backups[ts] + path = os.path.join(self.base_dir, backup["name"]) + if os.path.isdir(path): + logging.debug("Removing backup path: %s" % path) + rmtree(path) + else: + raise OperationError("Backup path %s does not exist!" % path) + if self.previous == backup["name"]: + self.previous = None + del self.backups[ts] + + def rotate(self): + if self.max_days == 0 and self.max_backups == 0: + logging.info("Backup rotation is disabled, skipping") + return + logging.info("Rotating backups (max_num=%i, max_days=%.2f)" % (self.max_backups, self.max_days)) + kept_backups = 1 + now = int(time()) + for ts in sorted(self.backups.iterkeys(), reverse=True): + backup = self.backups[ts] + if not self.previous: + self.previous = backup["name"] + if self.max_backups == 0 or kept_backups < self.max_backups: + if self.max_secs > 0 and (now - ts) > self.max_secs: + logging.info("Backup %s exceeds max age %.2f days, removing backup" % (backup["name"], self.max_days)) + self.remove(ts) + continue + logging.info("Keeping backup %s" % backup["name"]) + kept_backups += 1 + else: + logging.info("Backup %s exceeds max backup count %i, removing backup" % (backup["name"], self.max_backups)) + self.remove(ts) + + def symlink(self): + try: + if os.path.islink(self.latest_symlink): + os.remove(self.latest_symlink) + latest = os.path.join(self.base_dir, self.latest) + logging.info("Updating %s latest symlink to current backup path: %s" % (self.backup_name, latest)) + os.symlink(latest, self.latest_symlink) + + if os.path.islink(self.previous_symlink): + os.remove(self.previous_symlink) + if self.previous: + previous = os.path.join(self.base_dir, self.previous) + logging.info("Updating %s previous symlink to: %s" % (self.backup_name, previous)) + os.symlink(previous, self.previous_symlink) + except Exception, e: + logging.error("Error creating backup symlinks: %s" % e) + raise OperationError(e) diff --git a/mongodb_consistent_backup/State.py b/mongodb_consistent_backup/State.py index 4c3bbe08..29def21e 100644 --- a/mongodb_consistent_backup/State.py +++ b/mongodb_consistent_backup/State.py @@ -53,6 +53,14 @@ def load(self, load_one=False, filename=None): if f: f.close() + def get(self, key): + if key in self.state: + return self.state[key] + + def set(self, name, summary): + self.state[name] = summary + self.write(True) + def write(self, do_merge=False): f = None try: @@ -121,10 +129,6 @@ def __init__(self, base_dir, config, backup_time, seed_uri, argv=None): def init(self): logging.info("Initializing backup state directory: %s" % self.base_dir) - def set(self, name, summary): - self.state[name] = summary - self.write(True) - class StateRoot(StateBase): def __init__(self, base_dir, config): From b39639de0bc31d5f292e2992364054a3e1a4adc7 Mon Sep 17 00:00:00 2001 From: Tim Vaillancourt Date: Thu, 14 Sep 2017 23:45:22 +0200 Subject: [PATCH 03/13] Take float for cmdline flag --- mongodb_consistent_backup/Common/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodb_consistent_backup/Common/Config.py b/mongodb_consistent_backup/Common/Config.py index 66a6dee0..01812782 100644 --- a/mongodb_consistent_backup/Common/Config.py +++ b/mongodb_consistent_backup/Common/Config.py @@ -74,7 +74,7 @@ def makeParser(self): parser.add_argument("-L", "--log-dir", dest="log_dir", help="Path to write log files to (default: disabled)", default='', type=str) parser.add_argument("--lock-file", dest="lock_file", help="Location of lock file (default: /tmp/mongodb-consistent-backup.lock)", default='/tmp/mongodb-consistent-backup.lock', type=str) parser.add_argument("--rotate.max_backups", dest="rotate.max_backups", help="Maximum number of backups to keep in backup directory (default: disabled)", default=0, type=int) - parser.add_argument("--rotate.max_backup_days", dest="rotate.max_backup_days", help="Maximum age in days for backups in backup directory (default: disabled)", default=0, type=int) + parser.add_argument("--rotate.max_backup_days", dest="rotate.max_backup_days", help="Maximum age in days for backups in backup directory (default: disabled)", default=0, type=float) parser.add_argument("--sharding.balancer.wait_secs", dest="sharding.balancer.wait_secs", help="Maximum time to wait for balancer to stop, in seconds (default: 300)", default=300, type=int) parser.add_argument("--sharding.balancer.ping_secs", dest="sharding.balancer.ping_secs", help="Interval to check balancer state, in seconds (default: 3)", default=3, type=int) return self.makeParserLoadSubmodules(parser) From e211dc0f038d5d8be3bd7957ca429ae8aa8cea6c Mon Sep 17 00:00:00 2001 From: Tim Vaillancourt Date: Fri, 15 Sep 2017 00:00:28 +0200 Subject: [PATCH 04/13] shorten var name --- conf/mongodb-consistent-backup.example.conf | 2 +- mongodb_consistent_backup/Common/Config.py | 2 +- mongodb_consistent_backup/Rotate.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/conf/mongodb-consistent-backup.example.conf b/conf/mongodb-consistent-backup.example.conf index 82bf74ae..47bc55dc 100644 --- a/conf/mongodb-consistent-backup.example.conf +++ b/conf/mongodb-consistent-backup.example.conf @@ -21,7 +21,7 @@ production: # threads: [1-16] (default: auto-generated - shards/cpu) #rotate: # max_backups: [1+] - # max_backup_days: [0.1+] + # max_days: [0.1+] #replication: # max_lag_secs: [1+] (default: 10) # min_priority: [0-999] (default: 0) diff --git a/mongodb_consistent_backup/Common/Config.py b/mongodb_consistent_backup/Common/Config.py index 01812782..6372ec52 100644 --- a/mongodb_consistent_backup/Common/Config.py +++ b/mongodb_consistent_backup/Common/Config.py @@ -74,7 +74,7 @@ def makeParser(self): parser.add_argument("-L", "--log-dir", dest="log_dir", help="Path to write log files to (default: disabled)", default='', type=str) parser.add_argument("--lock-file", dest="lock_file", help="Location of lock file (default: /tmp/mongodb-consistent-backup.lock)", default='/tmp/mongodb-consistent-backup.lock', type=str) parser.add_argument("--rotate.max_backups", dest="rotate.max_backups", help="Maximum number of backups to keep in backup directory (default: disabled)", default=0, type=int) - parser.add_argument("--rotate.max_backup_days", dest="rotate.max_backup_days", help="Maximum age in days for backups in backup directory (default: disabled)", default=0, type=float) + parser.add_argument("--rotate.max_days", dest="rotate.max_days", help="Maximum age in days for backups in backup directory (default: disabled)", default=0, type=float) parser.add_argument("--sharding.balancer.wait_secs", dest="sharding.balancer.wait_secs", help="Maximum time to wait for balancer to stop, in seconds (default: 300)", default=300, type=int) parser.add_argument("--sharding.balancer.ping_secs", dest="sharding.balancer.ping_secs", help="Interval to check balancer state, in seconds (default: 3)", default=3, type=int) return self.makeParserLoadSubmodules(parser) diff --git a/mongodb_consistent_backup/Rotate.py b/mongodb_consistent_backup/Rotate.py index 6fa972f3..53df647d 100644 --- a/mongodb_consistent_backup/Rotate.py +++ b/mongodb_consistent_backup/Rotate.py @@ -14,8 +14,8 @@ def __init__(self, config, state_root, state_bkp): self.state_root = state_root self.state_bkp = state_bkp self.backup_name = self.config.backup.name - self.max_days = self.config.rotate.max_backup_days self.max_backups = self.config.rotate.max_backups + self.max_days = self.config.rotate.max_days self.latest = state_bkp.get("name") self.previous = None From 06aada56fbd06b3aab549e0852c90368fa61711f Mon Sep 17 00:00:00 2001 From: Tim Vaillancourt Date: Fri, 15 Sep 2017 00:03:29 +0200 Subject: [PATCH 05/13] logging typo --- mongodb_consistent_backup/Rotate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodb_consistent_backup/Rotate.py b/mongodb_consistent_backup/Rotate.py index 53df647d..d17d490e 100644 --- a/mongodb_consistent_backup/Rotate.py +++ b/mongodb_consistent_backup/Rotate.py @@ -55,7 +55,7 @@ def rotate(self): if self.max_days == 0 and self.max_backups == 0: logging.info("Backup rotation is disabled, skipping") return - logging.info("Rotating backups (max_num=%i, max_days=%.2f)" % (self.max_backups, self.max_days)) + logging.info("Rotating backups (max_backups=%i, max_days=%.2f)" % (self.max_backups, self.max_days)) kept_backups = 1 now = int(time()) for ts in sorted(self.backups.iterkeys(), reverse=True): From b67541b4e20b3ebfbd3e8d781584402b48ec4a6e Mon Sep 17 00:00:00 2001 From: Tim Vaillancourt Date: Fri, 15 Sep 2017 00:11:48 +0200 Subject: [PATCH 06/13] logging typo #21 --- mongodb_consistent_backup/Rotate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodb_consistent_backup/Rotate.py b/mongodb_consistent_backup/Rotate.py index d17d490e..a151f9ce 100644 --- a/mongodb_consistent_backup/Rotate.py +++ b/mongodb_consistent_backup/Rotate.py @@ -67,7 +67,7 @@ def rotate(self): logging.info("Backup %s exceeds max age %.2f days, removing backup" % (backup["name"], self.max_days)) self.remove(ts) continue - logging.info("Keeping backup %s" % backup["name"]) + logging.info("Keeping previous backup %s" % backup["name"]) kept_backups += 1 else: logging.info("Backup %s exceeds max backup count %i, removing backup" % (backup["name"], self.max_backups)) From 0f431bf72842d22fa109cfee43b0fb1524cc3eb7 Mon Sep 17 00:00:00 2001 From: Tim Vaillancourt Date: Fri, 15 Sep 2017 00:26:17 +0200 Subject: [PATCH 07/13] use the 'path' field in the State data instead of guessing --- mongodb_consistent_backup/Rotate.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/mongodb_consistent_backup/Rotate.py b/mongodb_consistent_backup/Rotate.py index a151f9ce..bdda0f25 100644 --- a/mongodb_consistent_backup/Rotate.py +++ b/mongodb_consistent_backup/Rotate.py @@ -17,7 +17,7 @@ def __init__(self, config, state_root, state_bkp): self.max_backups = self.config.rotate.max_backups self.max_days = self.config.rotate.max_days - self.latest = state_bkp.get("name") + self.latest = state_bkp.state self.previous = None self.backups = self.backups_by_unixts() @@ -41,13 +41,12 @@ def backups_by_unixts(self): def remove(self, ts): if ts in self.backups: backup = self.backups[ts] - path = os.path.join(self.base_dir, backup["name"]) - if os.path.isdir(path): - logging.debug("Removing backup path: %s" % path) - rmtree(path) + if os.path.isdir(backup["path"]): + logging.debug("Removing backup path: %s" % backup["path"]) + rmtree(backup["path"]) else: - raise OperationError("Backup path %s does not exist!" % path) - if self.previous == backup["name"]: + raise OperationError("Backup path %s does not exist!" % backup["path"]) + if self.previous == backup: self.previous = None del self.backups[ts] @@ -61,7 +60,7 @@ def rotate(self): for ts in sorted(self.backups.iterkeys(), reverse=True): backup = self.backups[ts] if not self.previous: - self.previous = backup["name"] + self.previous = backup if self.max_backups == 0 or kept_backups < self.max_backups: if self.max_secs > 0 and (now - ts) > self.max_secs: logging.info("Backup %s exceeds max age %.2f days, removing backup" % (backup["name"], self.max_days)) @@ -77,16 +76,14 @@ def symlink(self): try: if os.path.islink(self.latest_symlink): os.remove(self.latest_symlink) - latest = os.path.join(self.base_dir, self.latest) - logging.info("Updating %s latest symlink to current backup path: %s" % (self.backup_name, latest)) - os.symlink(latest, self.latest_symlink) + logging.info("Updating %s latest symlink to current backup path: %s" % (self.backup_name, self.latest["path"])) + os.symlink(self.latest["path"], self.latest_symlink) if os.path.islink(self.previous_symlink): os.remove(self.previous_symlink) if self.previous: - previous = os.path.join(self.base_dir, self.previous) - logging.info("Updating %s previous symlink to: %s" % (self.backup_name, previous)) - os.symlink(previous, self.previous_symlink) + logging.info("Updating %s previous symlink to: %s" % (self.backup_name, self.previous["path"])) + os.symlink(self.previous["path"], self.previous_symlink) except Exception, e: logging.error("Error creating backup symlinks: %s" % e) raise OperationError(e) From d1def11b60f017f0085579d56573a6213cc83f7a Mon Sep 17 00:00:00 2001 From: Tim Vaillancourt Date: Fri, 15 Sep 2017 00:30:06 +0200 Subject: [PATCH 08/13] make State.py get return all state if no key provided --- mongodb_consistent_backup/Rotate.py | 2 +- mongodb_consistent_backup/State.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mongodb_consistent_backup/Rotate.py b/mongodb_consistent_backup/Rotate.py index bdda0f25..0b62c64b 100644 --- a/mongodb_consistent_backup/Rotate.py +++ b/mongodb_consistent_backup/Rotate.py @@ -17,7 +17,7 @@ def __init__(self, config, state_root, state_bkp): self.max_backups = self.config.rotate.max_backups self.max_days = self.config.rotate.max_days - self.latest = state_bkp.state + self.latest = state_bkp.get() self.previous = None self.backups = self.backups_by_unixts() diff --git a/mongodb_consistent_backup/State.py b/mongodb_consistent_backup/State.py index 29def21e..3ed1fa95 100644 --- a/mongodb_consistent_backup/State.py +++ b/mongodb_consistent_backup/State.py @@ -53,9 +53,10 @@ def load(self, load_one=False, filename=None): if f: f.close() - def get(self, key): + def get(self, key=None): if key in self.state: return self.state[key] + return self.state def set(self, name, summary): self.state[name] = summary From 000a9864454858a6d4b438a5a7e7d3e5e2b282d9 Mon Sep 17 00:00:00 2001 From: Tim Vaillancourt Date: Fri, 15 Sep 2017 02:55:33 +0200 Subject: [PATCH 09/13] move setting of self.previous to method at init-time so .symlink() can create previous symlink if rotate disabled --- mongodb_consistent_backup/Rotate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mongodb_consistent_backup/Rotate.py b/mongodb_consistent_backup/Rotate.py index 0b62c64b..abf049ed 100644 --- a/mongodb_consistent_backup/Rotate.py +++ b/mongodb_consistent_backup/Rotate.py @@ -17,9 +17,9 @@ def __init__(self, config, state_root, state_bkp): self.max_backups = self.config.rotate.max_backups self.max_days = self.config.rotate.max_days - self.latest = state_bkp.get() self.previous = None self.backups = self.backups_by_unixts() + self.latest = state_bkp.get() self.base_dir = os.path.join(self.config.backup.location, self.config.backup.name) self.latest_symlink = os.path.join(self.base_dir, "latest") @@ -36,6 +36,8 @@ def backups_by_unixts(self): backup = self.state_root.backups[name] backup_time = backup["updated_at"] backups[backup_time] = backup + if not self.previous or backup_time > self.previous["updated_at"]: + self.previous = backup return backups def remove(self, ts): @@ -59,8 +61,6 @@ def rotate(self): now = int(time()) for ts in sorted(self.backups.iterkeys(), reverse=True): backup = self.backups[ts] - if not self.previous: - self.previous = backup if self.max_backups == 0 or kept_backups < self.max_backups: if self.max_secs > 0 and (now - ts) > self.max_secs: logging.info("Backup %s exceeds max age %.2f days, removing backup" % (backup["name"], self.max_days)) From 6b5d5ed831f5cbef03cdadd8fe2cfddbf8105e3b Mon Sep 17 00:00:00 2001 From: Tim Vaillancourt Date: Fri, 15 Sep 2017 03:07:34 +0200 Subject: [PATCH 10/13] Make .run() --- mongodb_consistent_backup/Main.py | 3 +-- mongodb_consistent_backup/Rotate.py | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/mongodb_consistent_backup/Main.py b/mongodb_consistent_backup/Main.py index 036b186d..dc0c1fb0 100644 --- a/mongodb_consistent_backup/Main.py +++ b/mongodb_consistent_backup/Main.py @@ -153,8 +153,7 @@ def stop_timer(self): def rotate_backups(self): rotater = Rotate(self.config, self.state_root, self.state) - rotater.rotate() - rotater.symlink() + rotater.run() # TODO Rename class to be more exact as this assumes something went wrong # noinspection PyUnusedLocal diff --git a/mongodb_consistent_backup/Rotate.py b/mongodb_consistent_backup/Rotate.py index abf049ed..3df28d2c 100644 --- a/mongodb_consistent_backup/Rotate.py +++ b/mongodb_consistent_backup/Rotate.py @@ -87,3 +87,7 @@ def symlink(self): except Exception, e: logging.error("Error creating backup symlinks: %s" % e) raise OperationError(e) + + def run(self): + self.rotate() + self.symlink() From f35953dabdd8f8eacf5ff3eea1ee91922eba7007 Mon Sep 17 00:00:00 2001 From: Tim Vaillancourt Date: Fri, 15 Sep 2017 03:28:51 +0200 Subject: [PATCH 11/13] log less noise --- mongodb_consistent_backup/Rotate.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/mongodb_consistent_backup/Rotate.py b/mongodb_consistent_backup/Rotate.py index 3df28d2c..63b2bafe 100644 --- a/mongodb_consistent_backup/Rotate.py +++ b/mongodb_consistent_backup/Rotate.py @@ -59,18 +59,22 @@ def rotate(self): logging.info("Rotating backups (max_backups=%i, max_days=%.2f)" % (self.max_backups, self.max_days)) kept_backups = 1 now = int(time()) + remove_backups = {} for ts in sorted(self.backups.iterkeys(), reverse=True): backup = self.backups[ts] + name = backup["name"].encode("ascii", "ignore") if self.max_backups == 0 or kept_backups < self.max_backups: if self.max_secs > 0 and (now - ts) > self.max_secs: - logging.info("Backup %s exceeds max age %.2f days, removing backup" % (backup["name"], self.max_days)) - self.remove(ts) + remove_backups[name] = ts continue - logging.info("Keeping previous backup %s" % backup["name"]) + logging.debug("Keeping previous backup %s" % name) kept_backups += 1 else: - logging.info("Backup %s exceeds max backup count %i, removing backup" % (backup["name"], self.max_backups)) - self.remove(ts) + remove_backups[name] = ts + if len(remove_backups) > 0: + logging.info("Backup(s) exceeds max backup count or age, removing: %s" % remove_backups.keys()) + for name in remove_backups: + self.remove(remove_backups[name]) def symlink(self): try: From 18e942198757e9d3568629beb70ea41dea2e9d71 Mon Sep 17 00:00:00 2001 From: Tim Vaillancourt Date: Fri, 15 Sep 2017 03:33:53 +0200 Subject: [PATCH 12/13] disabled -> unlimited help text --- mongodb_consistent_backup/Common/Config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongodb_consistent_backup/Common/Config.py b/mongodb_consistent_backup/Common/Config.py index 6372ec52..83305c3e 100644 --- a/mongodb_consistent_backup/Common/Config.py +++ b/mongodb_consistent_backup/Common/Config.py @@ -73,8 +73,8 @@ def makeParser(self): parser.add_argument("--ssl.client_cert_file", dest="ssl.client_cert_file", help="Path to Client SSL Certificate file in PEM format (for optional client ssl auth)", default=None, type=str) parser.add_argument("-L", "--log-dir", dest="log_dir", help="Path to write log files to (default: disabled)", default='', type=str) parser.add_argument("--lock-file", dest="lock_file", help="Location of lock file (default: /tmp/mongodb-consistent-backup.lock)", default='/tmp/mongodb-consistent-backup.lock', type=str) - parser.add_argument("--rotate.max_backups", dest="rotate.max_backups", help="Maximum number of backups to keep in backup directory (default: disabled)", default=0, type=int) - parser.add_argument("--rotate.max_days", dest="rotate.max_days", help="Maximum age in days for backups in backup directory (default: disabled)", default=0, type=float) + parser.add_argument("--rotate.max_backups", dest="rotate.max_backups", help="Maximum number of backups to keep in backup directory (default: unlimited)", default=0, type=int) + parser.add_argument("--rotate.max_days", dest="rotate.max_days", help="Maximum age in days for backups in backup directory (default: unlimited)", default=0, type=float) parser.add_argument("--sharding.balancer.wait_secs", dest="sharding.balancer.wait_secs", help="Maximum time to wait for balancer to stop, in seconds (default: 300)", default=300, type=int) parser.add_argument("--sharding.balancer.ping_secs", dest="sharding.balancer.ping_secs", help="Interval to check balancer state, in seconds (default: 3)", default=3, type=int) return self.makeParserLoadSubmodules(parser) From 65b49c69f26131be560df28d852a5d42a8609f09 Mon Sep 17 00:00:00 2001 From: Tim Vaillancourt Date: Fri, 15 Sep 2017 03:40:34 +0200 Subject: [PATCH 13/13] Sort remove list --- mongodb_consistent_backup/Rotate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongodb_consistent_backup/Rotate.py b/mongodb_consistent_backup/Rotate.py index 63b2bafe..c71ae7c4 100644 --- a/mongodb_consistent_backup/Rotate.py +++ b/mongodb_consistent_backup/Rotate.py @@ -72,7 +72,7 @@ def rotate(self): else: remove_backups[name] = ts if len(remove_backups) > 0: - logging.info("Backup(s) exceeds max backup count or age, removing: %s" % remove_backups.keys()) + logging.info("Backup(s) exceeds max backup count or age, removing: %s" % sorted(remove_backups.keys())) for name in remove_backups: self.remove(remove_backups[name])