diff --git a/scripts/validate-backup-config.py b/scripts/validate-backup-config.py index a0d841e..c0a44ac 100644 --- a/scripts/validate-backup-config.py +++ b/scripts/validate-backup-config.py @@ -1,139 +1,148 @@ #!/usr/bin/env python3 import os, sys import itertools import collections import subprocess import json import re def get_backup_commands(host_bare): """ Returns a set of features used by the backup script in this host. Possible return values are: - 'lftp': uses lftp to copy backups to a Hetzner backup box - 'rsync': uses rsync to copy backups to a server we own - 'dar': uses the dar command to create incremental backups - 'borg': uses borg directly into a Hetzner backup box """ backup_script = 'roles/kde-backup/templates/backup-%s.sh' % host_bare detected_commands = set() with open(backup_script, "r") as f: for line in f: if line.startswith('rsync '): detected_commands.add('rsync') elif re.match('lftp( -q)? -f ~/bin/backup-options', line): detected_commands.add('lftp') elif re.match('\s*dar -c ', line): detected_commands.add('dar') elif re.match('borg ', line): detected_commands.add('borg') if 'rsync' not in detected_commands and 'lftp' not in detected_commands: raise RuntimeError("Can't figure out what the backup script is doing on %s" % host_bare) return detected_commands class Validator: def __init__(self): self.inventory = self.hosts = self.hostvars = None self.problems = [] def report_problem(self, problem, host=None): self.problems.append((host, problem)) def load_inventory(self): proc = subprocess.Popen(['ansible-inventory', '--list'], stdout=subprocess.PIPE) inventory = json.load(proc.stdout) proc.wait() self.inventory = inventory self.hosts = set(itertools.chain.from_iterable(inventory[group]['hosts'] for group in inventory['all']['children'])) self.hostvars = inventory['_meta']['hostvars'] def check_backup_vars(self): """ Checks if hetzner_backup_host and gohma_backup_* variables are set in hostvars for hosts where they are required. Also checks if backup_apt_dependencies is complete. """ assert self.inventory is not None for host in sorted(self.hosts): assert host.endswith('.kde.org') host_bare = host[:-len(".kde.org")] try: backup_cmds = get_backup_commands(host_bare) except FileNotFoundError: continue + # Check if needed variables are set depending on the tool the backup script uses. if 'lftp' in backup_cmds: if 'hetzner_backup_host' not in self.hostvars[host]: self.report_problem("Uses lftp but doesn't have hetzner_backup_host set", host) elif 'rsync' in backup_cmds: if ('gohma_backup_user' not in self.hostvars[host] or 'gohma_backup_home' not in self.hostvars[host]): self.report_problem("Uses rsync but doesn't have gohma_backup_* vars set", host) + # Check if backup dependencies are set if 'dar' in backup_cmds: if 'dar' not in self.hostvars[host].get('backup_apt_dependencies',[]): self.report_problem("Uses dar but doesn't include it in backup_apt_dependencies", host) if 'borg' in backup_cmds: if 'borgbackup' not in self.hostvars[host].get('backup_apt_dependencies',[]): self.report_problem("Uses borg but doesn't include it in backup_apt_dependencies", host) + # Borg variable checks + if 'borg' in backup_cmds: if 'backup_borg_passphrase' in self.hostvars[host]: if not self.hostvars[host]['backup_borg_passphrase'].startswith('{{vault_'): self.report_problem("Sets backup_borg_passphrase but it doesn't seem to be vault-protected", host) else: self.report_problem("Uses borg but doesn't set backup_borg_passphrase", host) # Check if there are unnecessary dependencies if 'dar' in self.hostvars[host].get('backup_apt_dependencies',[]) and 'dar' not in backup_cmds: self.report_problem("Includes dar in backup_apt_dependencies but doesn't seem to use it", host) if 'borgbackup' in self.hostvars[host].get('backup_apt_dependencies',[]) and 'borg' not in backup_cmds: self.report_problem("Includes borgbackup in backup_apt_dependencies but doesn't seem to use it", host) + # Ensure backup_size_logging is enabled only for Hetzner-borg setups + if self.hostvars[host].get('backup_size_logging', False): + if 'hetzner_backup_host' not in self.hostvars[host]: + self.report_problem("Enables backup_size_logging but doesn't have a Hetzner backup host set", host) + def check_gohma_vars_duplicate(self): """ Checks for duplicates in gohma_backup_* variables: two hosts using the same home directory or user account in gohma. """ # gather a dict of user:[host1,host2] and homedir:[host1,host2] backup_users = collections.defaultdict(lambda: []) backup_homes = collections.defaultdict(lambda: []) for host in self.hosts: if 'gohma_backup_user' in self.hostvars[host]: backup_users[self.hostvars[host]['gohma_backup_user']].append(host) if 'gohma_backup_home' in self.hostvars[host]: backup_homes[self.hostvars[host]['gohma_backup_home']].append(host) # Now give an error for any username or homedir used on more than one host for user, hosts in backup_users.items(): if len(hosts) > 1: self.report_problem("There's multiple hosts using the same Gohma backup user '%s': %s" % (user, ', '.join(hosts))) for home, hosts in backup_homes.items(): if len(hosts) > 1: self.report_problem("There's multiple hosts using the same Gohma backup home directory '%s': %s" % (user, ', '.join(hosts))) validator = Validator() validator.load_inventory() validator.check_backup_vars() validator.check_gohma_vars_duplicate() if len(validator.problems): for host, problem in validator.problems: if host is None: print(problem) else: print("[%s] %s" % (host, problem)) sys.exit(1)