From 07d5a0129caa38c4999ce8e5b24f14998bfbcb27 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Wed, 17 Jun 2026 16:26:01 -0700 Subject: [PATCH] For the MariaDB and MySQL hooks, add "events", "routines", and "tablespaces" options for disabling dumping of scheduled events, stored routines, and tablespaces, respectively (#1324). --- NEWS | 2 + borgmatic/config/schema.yaml | 42 +++++ borgmatic/hooks/data_source/mariadb.py | 5 +- borgmatic/hooks/data_source/mysql.py | 5 +- tests/unit/hooks/data_source/test_mariadb.py | 173 +++++++++++++++++-- tests/unit/hooks/data_source/test_mysql.py | 171 +++++++++++++++++- 6 files changed, 377 insertions(+), 21 deletions(-) diff --git a/NEWS b/NEWS index e464f264..81877952 100644 --- a/NEWS +++ b/NEWS @@ -8,6 +8,8 @@ "/". * #1322: Fix for the "restore" action sometimes failing to find a database dump that was dumped with a default port. + * #1324: For the MariaDB and MySQL hooks, add "events", "routines", and "tablespaces" options for + disabling dumping of scheduled events, stored routines, and tablespaces, respectively. * Fix a bug in which the "compact" action does not pass a compact threshold of zero to Borg. 2.1.6 diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 494d5944..24947951 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -1849,6 +1849,27 @@ properties: client and restore server. The default varies based on the MariaDB version. example: false + events: + type: boolean + description: | + Whether to include scheduled events within the dump. + Disable if your database user doesn't have the + permissions to dump events. Defaults to true. + example: false + routines: + type: boolean + description: | + Whether to include stored routines within the dump. + Disable if your user database doesn't have the + permissions to dump routines. Defaults to true. + example: false + tablespaces: + type: boolean + description: | + Whether to include tablespaces within the dump. Disable + if your database user doesn't have the permissions to + dump tablespaces. Defaults to true. + example: false mariadb_dump_command: type: string description: | @@ -2057,6 +2078,27 @@ properties: client and restore server. The default varies based on the MySQL installation. example: false + events: + type: boolean + description: | + Whether to include scheduled events within the dump. + Disable if your database user doesn't have the + permissions to dump events. Defaults to true. + example: false + routines: + type: boolean + description: | + Whether to include stored routines within the dump. + Disable if your database user doesn't have the + permissions to dump routines. Defaults to true. + example: false + tablespaces: + type: boolean + description: | + Whether to include tablespaces within the dump. Disable + if your database user doesn't have the permissions to + dump tablespaces. Defaults to true. + example: false mysql_dump_command: type: string description: | diff --git a/borgmatic/hooks/data_source/mariadb.py b/borgmatic/hooks/data_source/mariadb.py index a2c36e8d..17afc096 100644 --- a/borgmatic/hooks/data_source/mariadb.py +++ b/borgmatic/hooks/data_source/mariadb.py @@ -241,8 +241,11 @@ def execute_dump_command( + (('--user', username) if username and password_transport == 'environment' else ()) + (('--ssl',) if database.get('tls') is True else ()) + (('--skip-ssl',) if database.get('tls') is False else ()) - + ('--databases', '--events', '--routines', '--all-tablespaces') + + (('--events',) if database.get('events', True) else ()) + + (('--routines',) if database.get('routines', True) else ()) + + (('--all-tablespaces',) if database.get('tablespaces', True) else ()) + (('--system=users,udfs,servers',) if SYSTEM_DATABASE_NAME in database_names else ()) + + ('--databases',) + tuple(name for name in database_names if name != SYSTEM_DATABASE_NAME) + ('--result-file', dump_filename) ) diff --git a/borgmatic/hooks/data_source/mysql.py b/borgmatic/hooks/data_source/mysql.py index 1a81c4c8..8ea19715 100644 --- a/borgmatic/hooks/data_source/mysql.py +++ b/borgmatic/hooks/data_source/mysql.py @@ -165,7 +165,10 @@ def execute_dump_command( + (('--user', username) if username and password_transport == 'environment' else ()) + (('--ssl',) if database.get('tls') is True else ()) + (('--skip-ssl',) if database.get('tls') is False else ()) - + ('--databases', '--events', '--routines', '--all-tablespaces') + + (('--events',) if database.get('events', True) else ()) + + (('--routines',) if database.get('routines', True) else ()) + + (('--all-tablespaces',) if database.get('tablespaces', True) else ()) + + ('--databases',) + database_names + ('--result-file', dump_filename) ) diff --git a/tests/unit/hooks/data_source/test_mariadb.py b/tests/unit/hooks/data_source/test_mariadb.py index ec04bd0c..0ddbc1e2 100644 --- a/tests/unit/hooks/data_source/test_mariadb.py +++ b/tests/unit/hooks/data_source/test_mariadb.py @@ -855,10 +855,10 @@ def test_execute_dump_command_runs_mariadb_dump(): '--defaults-extra-file=/dev/fd/99', '--add-drop-database', '--single-transaction', - '--databases', '--events', '--routines', '--all-tablespaces', + '--databases', 'foo', '--result-file', 'dump', @@ -907,11 +907,11 @@ def test_execute_dump_command_substitutes_system_flag_for_system_database_name() '--defaults-extra-file=/dev/fd/99', '--add-drop-database', '--single-transaction', - '--databases', '--events', '--routines', '--all-tablespaces', '--system=users,udfs,servers', + '--databases', '--result-file', 'dump', ), @@ -956,10 +956,10 @@ def test_execute_dump_command_with_environment_password_transport_skips_defaults '--single-transaction', '--user', 'root', - '--databases', '--events', '--routines', '--all-tablespaces', + '--databases', 'foo', '--result-file', 'dump', @@ -1007,10 +1007,10 @@ def test_execute_dump_command_runs_mariadb_dump_without_add_drop_database(): 'mariadb-dump', '--defaults-extra-file=/dev/fd/99', '--single-transaction', - '--databases', '--events', '--routines', '--all-tablespaces', + '--databases', 'foo', '--result-file', 'dump', @@ -1067,10 +1067,10 @@ def test_execute_dump_command_runs_mariadb_dump_with_hostname_and_port(): '5433', '--protocol', 'tcp', - '--databases', '--events', '--routines', '--all-tablespaces', + '--databases', 'foo', '--result-file', 'dump', @@ -1120,10 +1120,10 @@ def test_execute_dump_command_runs_mariadb_dump_with_tls(): '--add-drop-database', '--single-transaction', '--ssl', - '--databases', '--events', '--routines', '--all-tablespaces', + '--databases', 'foo', '--result-file', 'dump', @@ -1173,10 +1173,10 @@ def test_execute_dump_command_runs_mariadb_dump_without_tls(): '--add-drop-database', '--single-transaction', '--skip-ssl', - '--databases', '--events', '--routines', '--all-tablespaces', + '--databases', 'foo', '--result-file', 'dump', @@ -1202,6 +1202,159 @@ def test_execute_dump_command_runs_mariadb_dump_without_tls(): ) +def test_execute_dump_command_runs_mariadb_dump_without_events(): + process = flexmock() + flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump') + flexmock(module.os.path).should_receive('exists').and_return(False) + flexmock(module.borgmatic.hooks.credential.parse).should_receive( + 'resolve_credential', + ).replace_with(lambda value, config: value) + flexmock(module).should_receive('parse_extra_options').and_return((), None) + flexmock(module.database_config).should_receive('resolve_database_option').and_return(None) + flexmock(module).should_receive('make_defaults_file_options').with_args( + 'root', + 'trustsome1', + None, + ).and_return(('--defaults-extra-file=/dev/fd/99',)) + flexmock(module.dump).should_receive('create_named_pipe_for_dump') + flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) + + flexmock(module).should_receive('execute_command').with_args( + ( + 'mariadb-dump', + '--defaults-extra-file=/dev/fd/99', + '--add-drop-database', + '--single-transaction', + '--routines', + '--all-tablespaces', + '--databases', + 'foo', + '--result-file', + 'dump', + ), + environment=None, + run_to_completion=False, + working_directory=None, + ).and_return(process).once() + + assert ( + module.execute_dump_command( + database={'name': 'foo', 'events': False}, + config={}, + username='root', + password='trustsome1', + dump_path=flexmock(), + database_names=('foo',), + environment=None, + dry_run=False, + dry_run_label='', + ) + == process + ) + + +def test_execute_dump_command_runs_mariadb_dump_without_routines(): + process = flexmock() + flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump') + flexmock(module.os.path).should_receive('exists').and_return(False) + flexmock(module.borgmatic.hooks.credential.parse).should_receive( + 'resolve_credential', + ).replace_with(lambda value, config: value) + flexmock(module).should_receive('parse_extra_options').and_return((), None) + flexmock(module.database_config).should_receive('resolve_database_option').and_return(None) + flexmock(module).should_receive('make_defaults_file_options').with_args( + 'root', + 'trustsome1', + None, + ).and_return(('--defaults-extra-file=/dev/fd/99',)) + flexmock(module.dump).should_receive('create_named_pipe_for_dump') + flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) + + flexmock(module).should_receive('execute_command').with_args( + ( + 'mariadb-dump', + '--defaults-extra-file=/dev/fd/99', + '--add-drop-database', + '--single-transaction', + '--events', + '--all-tablespaces', + '--databases', + 'foo', + '--result-file', + 'dump', + ), + environment=None, + run_to_completion=False, + working_directory=None, + ).and_return(process).once() + + assert ( + module.execute_dump_command( + database={'name': 'foo', 'routines': False}, + config={}, + username='root', + password='trustsome1', + dump_path=flexmock(), + database_names=('foo',), + environment=None, + dry_run=False, + dry_run_label='', + ) + == process + ) + + +def test_execute_dump_command_runs_mariadb_dump_without_tablespaces(): + process = flexmock() + flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump') + flexmock(module.os.path).should_receive('exists').and_return(False) + flexmock(module.borgmatic.hooks.credential.parse).should_receive( + 'resolve_credential', + ).replace_with(lambda value, config: value) + flexmock(module).should_receive('parse_extra_options').and_return((), None) + flexmock(module.database_config).should_receive('resolve_database_option').and_return(None) + flexmock(module).should_receive('make_defaults_file_options').with_args( + 'root', + 'trustsome1', + None, + ).and_return(('--defaults-extra-file=/dev/fd/99',)) + flexmock(module.dump).should_receive('create_named_pipe_for_dump') + flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) + + flexmock(module).should_receive('execute_command').with_args( + ( + 'mariadb-dump', + '--defaults-extra-file=/dev/fd/99', + '--add-drop-database', + '--single-transaction', + '--events', + '--routines', + '--databases', + 'foo', + '--result-file', + 'dump', + ), + environment=None, + run_to_completion=False, + working_directory=None, + ).and_return(process).once() + + assert ( + module.execute_dump_command( + database={'name': 'foo', 'tablespaces': False}, + config={}, + username='root', + password='trustsome1', + dump_path=flexmock(), + database_names=('foo',), + environment=None, + dry_run=False, + dry_run_label='', + ) + == process + ) + + def test_execute_dump_command_runs_mariadb_dump_with_username_and_password(): process = flexmock() flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump') @@ -1225,10 +1378,10 @@ def test_execute_dump_command_runs_mariadb_dump_with_username_and_password(): '--defaults-extra-file=/dev/fd/99', '--add-drop-database', '--single-transaction', - '--databases', '--events', '--routines', '--all-tablespaces', + '--databases', 'foo', '--result-file', 'dump', @@ -1278,10 +1431,10 @@ def test_execute_dump_command_runs_mariadb_dump_with_options(): '--stuff=such', '--add-drop-database', '--single-transaction', - '--databases', '--events', '--routines', '--all-tablespaces', + '--databases', 'foo', '--result-file', 'dump', @@ -1331,10 +1484,10 @@ def test_execute_dump_command_runs_non_default_mariadb_dump_with_options(): '--stuff=such', '--add-drop-database', '--single-transaction', - '--databases', '--events', '--routines', '--all-tablespaces', + '--databases', 'foo', '--result-file', 'dump', diff --git a/tests/unit/hooks/data_source/test_mysql.py b/tests/unit/hooks/data_source/test_mysql.py index b2f9a639..3cf19fda 100644 --- a/tests/unit/hooks/data_source/test_mysql.py +++ b/tests/unit/hooks/data_source/test_mysql.py @@ -718,10 +718,10 @@ def test_execute_dump_command_runs_mysqldump(): '--defaults-extra-file=/dev/fd/99', '--add-drop-database', '--single-transaction', - '--databases', '--events', '--routines', '--all-tablespaces', + '--databases', 'foo', '--result-file', 'dump', @@ -771,10 +771,10 @@ def test_execute_dump_command_with_environment_password_transport_skips_defaults '--single-transaction', '--user', 'root', - '--databases', '--events', '--routines', '--all-tablespaces', + '--databases', 'foo', '--result-file', 'dump', @@ -822,10 +822,10 @@ def test_execute_dump_command_runs_mysqldump_without_add_drop_database(): 'mysqldump', '--defaults-extra-file=/dev/fd/99', '--single-transaction', - '--databases', '--events', '--routines', '--all-tablespaces', + '--databases', 'foo', '--result-file', 'dump', @@ -882,10 +882,10 @@ def test_execute_dump_command_runs_mysqldump_with_hostname_and_port(): '5433', '--protocol', 'tcp', - '--databases', '--events', '--routines', '--all-tablespaces', + '--databases', 'foo', '--result-file', 'dump', @@ -935,10 +935,10 @@ def test_execute_dump_command_runs_mysqldump_with_tls(): '--add-drop-database', '--single-transaction', '--ssl', - '--databases', '--events', '--routines', '--all-tablespaces', + '--databases', 'foo', '--result-file', 'dump', @@ -988,10 +988,10 @@ def test_execute_dump_command_runs_mysqldump_without_tls(): '--add-drop-database', '--single-transaction', '--skip-ssl', - '--databases', '--events', '--routines', '--all-tablespaces', + '--databases', 'foo', '--result-file', 'dump', @@ -1017,6 +1017,159 @@ def test_execute_dump_command_runs_mysqldump_without_tls(): ) +def test_execute_dump_command_runs_mysqldump_without_events(): + process = flexmock() + flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump') + flexmock(module.os.path).should_receive('exists').and_return(False) + flexmock(module.borgmatic.hooks.credential.parse).should_receive( + 'resolve_credential', + ).replace_with(lambda value, config: value) + flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive( + 'parse_extra_options', + ).and_return((), None) + flexmock(module.database_config).should_receive('resolve_database_option').and_return(None) + flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive( + 'make_defaults_file_options', + ).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',)) + flexmock(module.dump).should_receive('create_named_pipe_for_dump') + flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) + + flexmock(module).should_receive('execute_command').with_args( + ( + 'mysqldump', + '--defaults-extra-file=/dev/fd/99', + '--add-drop-database', + '--single-transaction', + '--routines', + '--all-tablespaces', + '--databases', + 'foo', + '--result-file', + 'dump', + ), + environment=None, + run_to_completion=False, + working_directory=None, + ).and_return(process).once() + + assert ( + module.execute_dump_command( + database={'name': 'foo', 'events': False}, + config={}, + username='root', + password='trustsome1', + dump_path=flexmock(), + database_names=('foo',), + environment=None, + dry_run=False, + dry_run_label='', + ) + == process + ) + + +def test_execute_dump_command_runs_mysqldump_without_routines(): + process = flexmock() + flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump') + flexmock(module.os.path).should_receive('exists').and_return(False) + flexmock(module.borgmatic.hooks.credential.parse).should_receive( + 'resolve_credential', + ).replace_with(lambda value, config: value) + flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive( + 'parse_extra_options', + ).and_return((), None) + flexmock(module.database_config).should_receive('resolve_database_option').and_return(None) + flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive( + 'make_defaults_file_options', + ).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',)) + flexmock(module.dump).should_receive('create_named_pipe_for_dump') + flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) + + flexmock(module).should_receive('execute_command').with_args( + ( + 'mysqldump', + '--defaults-extra-file=/dev/fd/99', + '--add-drop-database', + '--single-transaction', + '--events', + '--all-tablespaces', + '--databases', + 'foo', + '--result-file', + 'dump', + ), + environment=None, + run_to_completion=False, + working_directory=None, + ).and_return(process).once() + + assert ( + module.execute_dump_command( + database={'name': 'foo', 'routines': False}, + config={}, + username='root', + password='trustsome1', + dump_path=flexmock(), + database_names=('foo',), + environment=None, + dry_run=False, + dry_run_label='', + ) + == process + ) + + +def test_execute_dump_command_runs_mysqldump_without_tablespaces(): + process = flexmock() + flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump') + flexmock(module.os.path).should_receive('exists').and_return(False) + flexmock(module.borgmatic.hooks.credential.parse).should_receive( + 'resolve_credential', + ).replace_with(lambda value, config: value) + flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive( + 'parse_extra_options', + ).and_return((), None) + flexmock(module.database_config).should_receive('resolve_database_option').and_return(None) + flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive( + 'make_defaults_file_options', + ).with_args('root', 'trustsome1', None).and_return(('--defaults-extra-file=/dev/fd/99',)) + flexmock(module.dump).should_receive('create_named_pipe_for_dump') + flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) + + flexmock(module).should_receive('execute_command').with_args( + ( + 'mysqldump', + '--defaults-extra-file=/dev/fd/99', + '--add-drop-database', + '--single-transaction', + '--events', + '--routines', + '--databases', + 'foo', + '--result-file', + 'dump', + ), + environment=None, + run_to_completion=False, + working_directory=None, + ).and_return(process).once() + + assert ( + module.execute_dump_command( + database={'name': 'foo', 'tablespaces': False}, + config={}, + username='root', + password='trustsome1', + dump_path=flexmock(), + database_names=('foo',), + environment=None, + dry_run=False, + dry_run_label='', + ) + == process + ) + + def test_execute_dump_command_runs_mysqldump_with_username_and_password(): process = flexmock() flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump') @@ -1040,10 +1193,10 @@ def test_execute_dump_command_runs_mysqldump_with_username_and_password(): '--defaults-extra-file=/dev/fd/99', '--add-drop-database', '--single-transaction', - '--databases', '--events', '--routines', '--all-tablespaces', + '--databases', 'foo', '--result-file', 'dump', @@ -1093,10 +1246,10 @@ def test_execute_dump_command_runs_mysqldump_with_options(): '--stuff=such', '--add-drop-database', '--single-transaction', - '--databases', '--events', '--routines', '--all-tablespaces', + '--databases', 'foo', '--result-file', 'dump', @@ -1145,10 +1298,10 @@ def test_execute_dump_command_runs_non_default_mysqldump(): '--defaults-extra-file=/dev/fd/99', '--add-drop-database', '--single-transaction', - '--databases', '--events', '--routines', '--all-tablespaces', + '--databases', 'foo', '--result-file', 'dump',