diff --git a/dbclean/dbclean.py b/dbclean/dbclean.py index a4ea2ac..1e1c6e7 100755 --- a/dbclean/dbclean.py +++ b/dbclean/dbclean.py @@ -50,7 +50,7 @@ def err(msg, *args): if args.section == 'DEFAULT': parser.error("--section must not be 'DEFAULT'.") -config = configparser.SafeConfigParser({ +config = configparser.ConfigParser({ 'format': '%%Y-%%m-%%d_%%H:%%M:%%S', 'hourly': '24', 'daily': '31', 'monthly': '12', 'yearly': '3', diff --git a/dbdump/dbdump.conf.example b/dbdump/dbdump.conf.example index 7cf032c..25fd734 100644 --- a/dbdump/dbdump.conf.example +++ b/dbdump/dbdump.conf.example @@ -24,6 +24,14 @@ # unchanged (optional): remote = user@backup.example.com +# Store dumps with borgbackup. Specify the common directory, each db will have +# its own repository. +#borg = user@backup.example.com + +# Encrypt borg backups, store the keyfile on the server and encrypt it with the +# password specified in borg-key, if this parameter is given. +#borg-key = add-a-secret-password-here + # Override the default ssh connect-timeout of 10 seconds: #ssh-timeout = 10 @@ -70,6 +78,7 @@ remote = user@backup.example.com # POSTGRESQL OPTIONS: # Additional command-line parameters for psql and pgdump (optional): +#postgresql-connectstring = host=localhost port=5432 #postgresql-psql-opts = --someopt #postgresql-pgdump-opts = --otheropt diff --git a/dbdump/dbdump.py b/dbdump/dbdump.py index f37c8cb..a787a07 100755 --- a/dbdump/dbdump.py +++ b/dbdump/dbdump.py @@ -71,7 +71,7 @@ def err(msg, *args): section = config[args.section] -if 'remote' not in section: +if 'remote' not in section and 'borg' not in section: # Note that if we dump to a remote location, there is no real way to check to check if datadir # exists and is writeable. We have to rely on the competence of the admin in that case. datadir = section['datadir'] diff --git a/dbdump/libdump/backend.py b/dbdump/libdump/backend.py index bdb7dae..b5a3e4d 100644 --- a/dbdump/libdump/backend.py +++ b/dbdump/libdump/backend.py @@ -71,7 +71,52 @@ def dump(self, db, timestamp): sha = ['sha256sum'] sed = ['sed', 's/-$/%s/' % os.path.basename(path)] - if 'remote' in self.section: + if 'borg' in self.section: + borg_check = ['borg', 'info'] + borg_init = ['borg', 'init', '--umask', '0077', '--make-parent-dirs'] + borg_create = ['borg', 'create', '--umask', '0077', '--noctime', '--nobirthtime', '--compression', 'zstd', '--files-cache', 'disabled', '--content-from-command', '--'] + borg_repo = self.section['borg'] + + borg_env = os.environ.copy() + borg_env['BORG_RELOCATED_REPO_ACCESS_IS_OK'] = 'yes' + + if 'borg-key' in self.section: + borg_env['BORG_PASSPHRASE'] = self.section['borg-key'] + borg_init += ['--encryption', 'repokey-blake2'] + else: + borg_env['BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK'] = 'yes' + borg_init += ['--encryption', 'none'] + + borg_check += [f"{borg_repo}/{db}"] + borg_init += [f"{borg_repo}/{db}"] + borg_create += [f"{borg_repo}/{db}" + "::" + f"{timestamp}"] + borg_create += cmd + + if self.args.verbose: + str_cmds = [' '.join(c) for c in cmd] + print('# Dump databases:') + print(' | '.join(str_cmds)) + + # check if repo already exists + p_check = Popen(borg_check, env=borg_env, stdout=PIPE) + stdout, stderr = p_check.communicate() + if p_check.returncode != 0: + # repo is missing, create it + p_init = Popen(borg_init, env=borg_env, stdout=PIPE) + stdout, stderr = p_init.communicate() + if p_init.returncode != 0: + raise RuntimeError(f"{borg_init} returned with exit code {p_init.returncode}. (stderr: {stderr})") + + # create backup + p_create = Popen(borg_create, env=borg_env, stdout=PIPE) + stdout, stderr = p_create.communicate() + if self.args.verbose: + print("# borg_create:") + print(' | '.join) + if p_create.returncode != 0: + raise RuntimeError(f"{borg_create} returned with exit code {p_create.returncode}. (stderr: {stderr})") + + elif 'remote' in self.section: ssh = self.get_ssh(path, [tee, sha, sed]) cmds = [cmd, gzip, ] # just for output diff --git a/dbdump/libdump/postgresql.py b/dbdump/libdump/postgresql.py index af2d08d..2ccdee0 100644 --- a/dbdump/libdump/postgresql.py +++ b/dbdump/libdump/postgresql.py @@ -19,14 +19,18 @@ class postgresql(backend.backend): def get_db_list(self): - cmd = ['psql', '-Aqt', '-c', '"select datname from pg_database"'] + cmd = ['psql', '-Aqt', '-c', 'select datname from pg_database'] if 'postgresql-psql-opts' in self.section: cmd += self.section['postgresql-psql-opts'].split(' ') + if 'postgresql-connectstring' in self.section: + cmd.append(self.section['postgresql-connectstring']) + if 'su' in self.section: + quoted_args = [f"\"{arg}\"" if ' ' in arg else arg for arg in cmd ] cmd = ['su', self.section['su'], '-s', '/bin/bash', '-c', - ' '.join(cmd)] + ' '.join(quoted_args)] p_list = Popen(cmd, stdout=PIPE, stderr=PIPE) stdout, stderr = p_list.communicate() @@ -44,5 +48,12 @@ def get_command(self, database): cmd = ['pg_dump', '-c'] if 'postgresql-pgdump-opts' in self.section: cmd += self.section['postgresql-pgdump-opts'].split(' ') - cmd.append(database) + + connectstring_options = [] + if 'postgresql-connectstring' in self.section: + connectstring_options = self.section['postgresql-connectstring'].split(' ') + + connectstring_options.append(f"dbname={database}") + cmd.append(' '.join(connectstring_options)) + return cmd