class ListBackups(Command): """${cmd_usage} ${cmd_option_list} Lists available backups """ name = 'list-backups' aliases = ['lb'] description = 'List available backups' options = [ option('-v', '--verbose', action='store_true', help="Verbose output") ] def print_table(self, table): header = table[0] rest = table[1:] fmt = "%-28s %-9s %-16s %s" print fmt % tuple(header) print "-" * 80 for row in rest: print fmt % tuple(row) def run(self, cmd, opts): backup_list = [x for x in spool.list_backups()] if not backup_list: print "No backups" return 0 backupsets_seen = [] for backup in backup_list: if backup.backupset not in backupsets_seen: backupsets_seen.append(backup.backupset) print "Backupset[%s]:" % (backup.backupset) # Read the backup.conf backup.load_config() plugin_name = backup.config.get('holland:backup', {})['plugin'] if not plugin_name: print "Skipping broken backup: %s" % backup.name continue print "\t%s" % backup.name if opts.verbose: print "\t", backup.info() plugin = load_backup_plugin(plugin_name) plugin = plugin(backup.backupset, backup.config, backup.path) if hasattr(plugin, 'info'): plugin_info = plugin.info() import re rec = re.compile(r'^', re.M) print rec.sub('\t\t', plugin_info) return 0
class Restore(Command): """${cmd_usage} Restore data from an existing Holland backup The actual restore is delegated to the backup plugin that created the backup. Example: holland ${cmd_name} some-backup --help # Example restore for a mysqldump based backup holland ${cmd_name} some-backup --table mysql.proc ${cmd_option_list} """ name = 'restore' aliases = [ 're' ] options = [ option('--dry-run', '-n', action='store_true', help="Print what restore actually would do without actually running the restore") ] description = 'Restore data from an existing Holland Backup' def __init__(self): Command.__init__(self) self.optparser.disable_interspersed_args() def run(self, cmd, opts, backup_name, *restore_options): backup = spool.find_backup(backup_name) if not backup: logging.error("No backup found named %s", backup_name) return 1 config = backup.config plugin_name = config.get('holland:backup', {}).get('plugin') plugin = load_first_entrypoint('holland.restore', plugin_name)(backup) plugin.dispatch([plugin_name] + list(restore_options)) return 1
class Purge(Command): """${cmd_usage} Purge the requested job runs ${cmd_option_list} """ name = 'purge' aliases = ['pg'] options = [ option('--dry-run', '-n', action='store_true', dest='force', default=False, help="Print what would be purged without actually purging"), option('--all', '-a', action='store_true', default=False, help="When purging a backupset purge everything rather than " "using the retention count from the active configuration"), option( '--force', '-f', action='store_true', default=False, help="Execute the purge (disable dry-run). Alias for --execute"), option('--execute', action='store_true', dest='force', help="Execute the purge (disable dry-run)") ] description = 'Purge the requested job runs' def run(self, cmd, opts, *backups): error = 0 if not backups: LOG.info("No backupsets specified - using backupsets from %s", hollandcfg.filename) backups = hollandcfg.lookup('holland.backupsets') if not backups: LOG.warn("Nothing to purge") return 0 if not opts.force: LOG.warn( "Running in dry-run mode. Use --execute to do a real purge.") for name in backups: if '/' not in name: backupset = spool.find_backupset(name) if not backupset: LOG.error("Failed to find backupset '%s'", name) error = 1 continue purge_backupset(backupset, opts.force, opts.all) else: backup = spool.find_backup(name) if not backup: LOG.error("Failed to find single backup '%s'", name) error = 1 continue purge_backup(backup, opts.force) if opts.force: spool.find_backupset(backup.backupset).update_symlinks() return error
class MkConfig(Command): """${cmd_usage} Generate a config file for a backup plugin. ${cmd_option_list} """ name = 'mk-config' aliases = [ 'mc' ] options = [ option('--name', help='Name of the backupset'), option('--edit', action='store_true', help='Edit the generated config'), option('--provider', action='store_true', help='Generate a provider config'), option('--file', '-f', help='Save the final config to the specified file'), option('--minimal', '-m', action='store_true', default=False, help="Do not include comment from a backup " "plugin's configspec"), ] description = 'Generate a config file for a backup plugin' # After initial validation: # run through and flag required parameters with a 'REQUIRED' comment # run through and comment out default=None parameters def _cleanup_config(self, config, skip_comments=False): errors = config.validate(validator, preserve_errors=True,copy=True) # First flag any required parameters for entry in flatten_errors(config, errors): section_list, key, error = entry section_name, = section_list if error is False: config[section_name][key] = '' config[section_name].comments[key].append('REQUIRED') elif error: print >>sys.stderr, "Bad configspec generated error", error pending_comments = [] for section in list(config): if pending_comments: if skip_comments: comments = [] else: comments = config.comments.get(section, []) comments = pending_comments + comments config.comments[section] = comments del pending_comments[:] for idx, (key, value) in enumerate(config[section].items()): if value is None: if not skip_comments: pending_comments.extend(config[section].comments.get(key, [])) pending_comments.append('%s = "" # no default' % key) del config[section][key] else: if skip_comments: del config[section].comments[key][:] if pending_comments: if skip_comments: comments = [] else: comments = config[section].comments.get(key, []) comments = pending_comments + comments config[section].comments[key] = comments del pending_comments[:] if value is True or value is False: config[section][key] = ['no','yes'][value] if pending_comments: if skip_comments: config.final_comment = pending_comments else: config.final_comment = pending_comments + config.final_comment # drop initial whitespace config.initial_comment = [] # insert a blank between [holland:backup] and first section try: config.comments[config.sections[1]].insert(0, '') except IndexError: pass def run(self, cmd, opts, plugin_type): if opts.name and opts.provider: print >>sys.stderr, "Can't specify a name for a global provider config" return 1 try: plugin_cls = load_first_entrypoint('holland.backup', plugin_type) except PluginLoadError, exc: logging.info("Failed to load backup plugin %r: %s", plugin_type, exc) return 1 try: cfgspec = sys.modules[plugin_cls.__module__].CONFIGSPEC except: print >>sys.stderr, "Could not load config-spec from plugin %r" % plugin_type return 1 base_config = """ [holland:backup] plugin = "" backups-to-keep = 1 auto-purge-failures = yes purge-policy = after-backup """.lstrip().splitlines() cfg = ConfigObj(base_config, configspec=cfgspec, list_values=True,stringify=True) cfg['holland:backup']['plugin'] = plugin_type self._cleanup_config(cfg, skip_comments=opts.minimal) if opts.edit: done = False editor = _find_editor() if not editor: print >>sys.stderr, "Could not find a valid editor" return 1 tmpfileobj = tempfile.NamedTemporaryFile() cfg.filename = tmpfileobj.name cfg.write() while not done: status = subprocess.call([editor, cfg.filename]) if status != 0: if not confirm("Editor exited with non-zero status[%d]. " "Would you like to retry?" % status): print >>sys.stderr, "Aborting" return 1 else: continue try: cfg.reload() except ParseError, exc: print >>sys.stderr, "%s : %s" % \ (exc.msg, exc.line) else: errors = cfg.validate(validator,preserve_errors=True) if errors is True: done = True continue else: _report_errors(cfg, errors) if not confirm('There were configuration errors. Continue?'): print >>sys.stderr, "Aborting" return 1 tmpfileobj.close()
class Backup(Command): """${cmd_usage} Backup the specified backupsets or all active backupsets specified in holland.conf ${cmd_option_list} """ name = 'backup' aliases = [ 'bk' ] options = [ option('--abort-immediately', action='store_true', help="Abort on the first backupset that fails."), option('--dry-run', '-n', action='store_true', help="Print backup commands without executing them."), option('--no-lock', '-f', action='store_true', default=False, help="Run even if another copy of Holland is running.") ] description = 'Run backups for active backupsets' def run(self, cmd, opts, *backupsets): if not backupsets: backupsets = hollandcfg.lookup('holland.backupsets') # strip empty items from backupsets list backupsets = [name for name in backupsets if name] if not backupsets: LOG.info("Nothing to backup") return 1 runner = BackupRunner(spool) # dry-run implies no-lock if opts.dry_run: opts.no_lock = True # don't purge if doing a dry-run, or when simultaneous backups may be running if not opts.no_lock: purge_mgr = PurgeManager() runner.register_cb('before-backup', purge_mgr) runner.register_cb('after-backup', purge_mgr) runner.register_cb('failed-backup', purge_backup) runner.register_cb('after-backup', report_low_space) if not opts.dry_run: runner.register_cb('before-backup', call_hooks) runner.register_cb('after-backup', call_hooks) runner.register_cb('failed-backup', call_hooks) error = 1 LOG.info("--- Starting %s run ---", opts.dry_run and 'dry' or 'backup') for name in backupsets: try: config = hollandcfg.backupset(name) # ensure we have at least an empty holland:backup section config.setdefault('holland:backup', {}) except (SyntaxError, IOError), exc: LOG.error("Could not load backupset '%s': %s", name, exc) break if not opts.no_lock: lock = Lock(config.filename) try: lock.acquire() LOG.debug("Set advisory lock on %s", lock.path) except LockError: LOG.debug("Unable to acquire advisory lock on %s", lock.path) LOG.error("Another holland backup process is already " "running backupset '%s'. Aborting.", name) break try: try: runner.backup(name, config, opts.dry_run) except BackupError, exc: LOG.error("Backup failed: %s", exc.args[0]) break except ConfigError, exc: break