diff --git a/NEWS b/NEWS index 4c0e4f3e..639b9186 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,5 @@ 2.0.8.dev0 + * #1116: Add support for database backup labels. * #1114: Document systemd configuration changes for the ZFS filesystem hook. * #1118: Fix a bug in which Borg hangs during database backup when different filesystems are in use. diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 0a49416a..5562c428 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -1323,6 +1323,11 @@ properties: implicitly enables read_special (see above) to support dump and restore streaming. example: users + label: + type: string + description: | + Label to identify the database dump in the backup. + example: my_backup_label hostname: type: string description: | @@ -1524,6 +1529,11 @@ properties: database hook implicitly enables read_special (see above) to support dump and restore streaming. example: users + label: + type: string + description: | + Label to identify the database dump in the backup. + example: my_backup_label hostname: type: string description: | @@ -1689,6 +1699,11 @@ properties: database hook implicitly enables read_special (see above) to support dump and restore streaming. example: users + label: + type: string + description: | + Label to identify the database dump in the backup. + example: my_backup_label hostname: type: string description: | @@ -1862,6 +1877,11 @@ properties: read_special (see above) to support dump and restore streaming. example: /var/lib/sqlite/users.db + label: + type: string + description: | + Label to identify the database dump in the backup. + example: my_backup_label restore_path: type: string description: | @@ -1910,6 +1930,11 @@ properties: database hook implicitly enables read_special (see above) to support dump and restore streaming. example: users + label: + type: string + description: | + Label to identify the database dump in the backup. + example: my_backup_label hostname: type: string description: | diff --git a/borgmatic/hooks/data_source/dump.py b/borgmatic/hooks/data_source/dump.py index 3b746f7f..af64e0fc 100644 --- a/borgmatic/hooks/data_source/dump.py +++ b/borgmatic/hooks/data_source/dump.py @@ -19,7 +19,7 @@ def make_data_source_dump_path(borgmatic_runtime_directory, data_source_hook_nam return os.path.join(borgmatic_runtime_directory, data_source_hook_name) -def make_data_source_dump_filename(dump_path, name, hostname=None, port=None): +def make_data_source_dump_filename(dump_path, name, hostname=None, port=None, label=None): ''' Based on the given dump directory path, data source name, hostname, and port, return a filename to use for the data source dump. The hostname defaults to localhost. @@ -29,12 +29,12 @@ def make_data_source_dump_filename(dump_path, name, hostname=None, port=None): if os.path.sep in name: raise ValueError(f'Invalid data source name {name}') - return os.path.join( - dump_path, - (hostname or 'localhost') + ('' if port is None else f':{port}'), - name, + identifier = ( + label if label else (hostname or 'localhost') + ('' if port is None else f':{port}') ) + return os.path.join(dump_path, identifier, name) + def write_data_source_dumps_metadata(borgmatic_runtime_directory, hook_name, dumps_metadata): ''' diff --git a/borgmatic/hooks/data_source/mariadb.py b/borgmatic/hooks/data_source/mariadb.py index 50d66ea6..4bd10bab 100644 --- a/borgmatic/hooks/data_source/mariadb.py +++ b/borgmatic/hooks/data_source/mariadb.py @@ -178,6 +178,7 @@ def execute_dump_command( database['name'], database.get('hostname'), database.get('port'), + database.get('label'), ) if os.path.exists(dump_filename): @@ -384,16 +385,16 @@ def make_data_source_dump_patterns( borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config) return ( - dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'), + dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, label='*'), dump.make_data_source_dump_filename( make_dump_path(borgmatic_runtime_directory), name, - hostname='*', + label='*', ), dump.make_data_source_dump_filename( make_dump_path(borgmatic_source_directory), name, - hostname='*', + label='*', ), ) diff --git a/borgmatic/hooks/data_source/mongodb.py b/borgmatic/hooks/data_source/mongodb.py index 3a249aaa..528ad607 100644 --- a/borgmatic/hooks/data_source/mongodb.py +++ b/borgmatic/hooks/data_source/mongodb.py @@ -71,6 +71,7 @@ def dump_data_sources( name, database.get('hostname'), database.get('port'), + database.get('label'), ) dump_format = database.get('format', 'archive') @@ -200,16 +201,16 @@ def make_data_source_dump_patterns( borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config) return ( - dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'), + dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, label='*'), dump.make_data_source_dump_filename( make_dump_path(borgmatic_runtime_directory), name, - hostname='*', + label='*', ), dump.make_data_source_dump_filename( make_dump_path(borgmatic_source_directory), name, - hostname='*', + label='*', ), ) @@ -238,6 +239,7 @@ def restore_data_source_dump( make_dump_path(borgmatic_runtime_directory), data_source['name'], data_source.get('hostname'), + data_source.get('label'), ) restore_command = build_restore_command( extract_process, diff --git a/borgmatic/hooks/data_source/mysql.py b/borgmatic/hooks/data_source/mysql.py index ea4aae29..48991902 100644 --- a/borgmatic/hooks/data_source/mysql.py +++ b/borgmatic/hooks/data_source/mysql.py @@ -104,6 +104,7 @@ def execute_dump_command( database['name'], database.get('hostname'), database.get('port'), + database.get('label'), ) if os.path.exists(dump_filename): @@ -315,16 +316,16 @@ def make_data_source_dump_patterns( borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config) return ( - dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'), + dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, label='*'), dump.make_data_source_dump_filename( make_dump_path(borgmatic_runtime_directory), name, - hostname='*', + label='*', ), dump.make_data_source_dump_filename( make_dump_path(borgmatic_source_directory), name, - hostname='*', + label='*', ), ) diff --git a/borgmatic/hooks/data_source/postgresql.py b/borgmatic/hooks/data_source/postgresql.py index f7eac3a2..695d79d9 100644 --- a/borgmatic/hooks/data_source/postgresql.py +++ b/borgmatic/hooks/data_source/postgresql.py @@ -186,6 +186,7 @@ def dump_data_sources( database_name, database.get('hostname'), database.get('port'), + database.get('label'), ) if os.path.exists(dump_filename): @@ -302,16 +303,16 @@ def make_data_source_dump_patterns( borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config) return ( - dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'), + dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, label='*'), dump.make_data_source_dump_filename( make_dump_path(borgmatic_runtime_directory), name, - hostname='*', + label='*', ), dump.make_data_source_dump_filename( make_dump_path(borgmatic_source_directory), name, - hostname='*', + label='*', ), ) @@ -359,6 +360,7 @@ def restore_data_source_dump( make_dump_path(borgmatic_runtime_directory), data_source['name'], data_source.get('hostname'), + data_source.get('label'), ) psql_command = tuple( shlex.quote(part) for part in shlex.split(data_source.get('psql_command') or 'psql') diff --git a/borgmatic/hooks/data_source/sqlite.py b/borgmatic/hooks/data_source/sqlite.py index 28555e5b..72fe059b 100644 --- a/borgmatic/hooks/data_source/sqlite.py +++ b/borgmatic/hooks/data_source/sqlite.py @@ -71,7 +71,9 @@ def dump_data_sources( ) dump_path = make_dump_path(borgmatic_runtime_directory) - dump_filename = dump.make_data_source_dump_filename(dump_path, database['name']) + dump_filename = dump.make_data_source_dump_filename( + dump_path, database['name'], label=database.get('label') + ) if os.path.exists(dump_filename): logger.warning( @@ -143,16 +145,16 @@ def make_data_source_dump_patterns( borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config) return ( - dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, hostname='*'), + dump.make_data_source_dump_filename(make_dump_path('borgmatic'), name, label='*'), dump.make_data_source_dump_filename( make_dump_path(borgmatic_runtime_directory), name, - hostname='*', + label='*', ), dump.make_data_source_dump_filename( make_dump_path(borgmatic_source_directory), name, - hostname='*', + label='*', ), ) diff --git a/docs/how-to/backup-your-databases.md b/docs/how-to/backup-your-databases.md index 1239c1ec..4b3dc742 100644 --- a/docs/how-to/backup-your-databases.md +++ b/docs/how-to/backup-your-databases.md @@ -72,8 +72,10 @@ Here's a more involved example that connects to remote databases: ```yaml postgresql_databases: - name: users + label: database_server1 hostname: database1.example.org - name: orders + label: database_server2 hostname: database2.example.org port: 5433 username: postgres diff --git a/tests/unit/hooks/data_source/test_dump.py b/tests/unit/hooks/data_source/test_dump.py index 9aa35884..0acc3f5d 100644 --- a/tests/unit/hooks/data_source/test_dump.py +++ b/tests/unit/hooks/data_source/test_dump.py @@ -25,6 +25,13 @@ def test_make_data_source_dump_filename_uses_name_and_hostname_and_port(): ) +def test_make_data_source_dump_filename_users_label(): + assert ( + module.make_data_source_dump_filename('databases', 'test', 'hostname', 1234, 'custom_label') + == 'databases/custom_label/test' + ) + + def test_make_data_source_dump_filename_without_hostname_defaults_to_localhost(): assert module.make_data_source_dump_filename('databases', 'test') == 'databases/localhost/test'