Browse Source

For the MariaDB and MySQL database hooks, add a "skip_names" option to ignore particular databases when dumping "all" (#973).

Dan Helfman 3 tuần trước cách đây
mục cha
commit
4017897b1a

+ 2 - 0
NEWS

@@ -1,6 +1,8 @@
 2.0.10.dev0
  * #942: Factor reference material out of the documentation how-to guides. This means there's now a
    whole reference section in the docs! Check it out: https://torsion.org/borgmatic/
+ * #973: For the MariaDB and MySQL database hooks, add a "skip_names" option to ignore particular
+   databases when dumping "all".
  * #1150: Fix for a runtime directory error when the "create" action is used with the "--log-json"
    flag.
  * #1161: Fix a traceback (TypeError) in the "check" action with Python 3.14.

+ 18 - 0
borgmatic/config/schema.yaml

@@ -1546,6 +1546,15 @@ properties:
                         database hook implicitly enables read_special (see
                         above) to support dump and restore streaming.
                     example: users
+                skip_names:
+                    type: array
+                    items:
+                        type: string
+                    description: |
+                        Database names to skip when dumping "all" databases.
+                        Ignored when the database name is not "all".
+                    example:
+                        - cache
                 label:
                     type: string
                     description: |
@@ -1728,6 +1737,15 @@ properties:
                         database hook implicitly enables read_special (see
                         above) to support dump and restore streaming.
                     example: users
+                skip_names:
+                    type: array
+                    items:
+                        type: string
+                    description: |
+                        Database names to skip when dumping "all" databases.
+                        Ignored when the database name is not "all".
+                    example:
+                        - cache
                 label:
                     type: string
                     description: |

+ 11 - 0
borgmatic/hooks/data_source/mariadb.py

@@ -112,7 +112,14 @@ def database_names_to_dump(database, config, username, password, environment, dr
     names to dump. In the case of "all", query for the names of databases on the configured host and
     return them, excluding any system databases that will cause problems during restore.
     '''
+    skip_names = database.get('skip_names')
+
     if database['name'] != 'all':
+        if skip_names:
+            logger.warning(
+                f'For MariaDB database {database["name"]}, ignoring the "skip_names" option, which is only supported for database "all"'
+            )
+
         return (database['name'],)
 
     if dry_run:
@@ -144,12 +151,16 @@ def database_names_to_dump(database, config, username, password, environment, dr
 
     logger.debug('Querying for "all" MariaDB databases to dump')
 
+    if skip_names:
+        logger.debug(f'Skipping database names: {", ".join(skip_names)}')
+
     show_output = execute_command_and_capture_output(show_command, environment=environment)
 
     return tuple(
         show_name
         for show_name in show_output.strip().splitlines()
         if show_name not in SYSTEM_DATABASE_NAMES
+        if not skip_names or show_name not in skip_names
     )
 
 

+ 11 - 0
borgmatic/hooks/data_source/mysql.py

@@ -35,7 +35,14 @@ def database_names_to_dump(database, config, username, password, environment, dr
     names to dump. In the case of "all", query for the names of databases on the configured host and
     return them, excluding any system databases that will cause problems during restore.
     '''
+    skip_names = database.get('skip_names')
+
     if database['name'] != 'all':
+        if skip_names:
+            logger.warning(
+                f'For MySQL database {database["name"]}, ignoring the "skip_names" option, which is only supported for database "all"'
+            )
+
         return (database['name'],)
 
     if dry_run:
@@ -73,12 +80,16 @@ def database_names_to_dump(database, config, username, password, environment, dr
 
     logger.debug('Querying for "all" MySQL databases to dump')
 
+    if skip_names:
+        logger.debug(f'Skipping database names: {", ".join(skip_names)}')
+
     show_output = execute_command_and_capture_output(show_command, environment=environment)
 
     return tuple(
         show_name
         for show_name in show_output.strip().splitlines()
         if show_name not in SYSTEM_DATABASE_NAMES
+        if not skip_names or show_name not in skip_names
     )
 
 

+ 52 - 0
tests/unit/hooks/data_source/test_mariadb.py

@@ -142,6 +142,23 @@ def test_database_names_to_dump_passes_through_name():
     assert names == ('foo',)
 
 
+def test_database_names_to_dump_with_non_all_name_and_skip_names_warns():
+    environment = flexmock()
+
+    flexmock(module.logger).should_receive('warning').once()
+
+    names = module.database_names_to_dump(
+        {'name': 'foo', 'skip_names': ('foo', 'bar')},
+        {},
+        'root',
+        'trustsome1',
+        environment,
+        dry_run=False,
+    )
+
+    assert names == ('foo',)
+
+
 def test_database_names_to_dump_bails_for_dry_run():
     environment = flexmock()
     flexmock(module).should_receive('execute_command_and_capture_output').never()
@@ -193,6 +210,41 @@ def test_database_names_to_dump_queries_mariadb_for_database_names():
     assert names == ('foo', 'bar')
 
 
+def test_database_names_to_dump_with_database_name_all_and_skip_names_filters_out_unwanted_databases():
+    environment = flexmock()
+    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).should_receive('make_defaults_file_options').with_args(
+        'root',
+        'trustsome1',
+        None,
+    ).and_return(('--defaults-extra-file=/dev/fd/99',))
+    flexmock(module).should_receive('execute_command_and_capture_output').with_args(
+        (
+            'mariadb',
+            '--defaults-extra-file=/dev/fd/99',
+            '--skip-column-names',
+            '--batch',
+            '--execute',
+            'show schemas',
+        ),
+        environment=environment,
+    ).and_return('foo\nbar\nbaz\nmysql\n').once()
+
+    names = module.database_names_to_dump(
+        {'name': 'all', 'skip_names': ('foo', 'bar')},
+        {},
+        'root',
+        'trustsome1',
+        environment,
+        dry_run=False,
+    )
+
+    assert names == ('baz',)
+
+
 def test_database_names_to_dump_with_environment_password_transport_skips_defaults_file_and_passes_user_flag():
     environment = flexmock()
     flexmock(module.borgmatic.hooks.credential.parse).should_receive(

+ 56 - 0
tests/unit/hooks/data_source/test_mysql.py

@@ -21,6 +21,23 @@ def test_database_names_to_dump_passes_through_name():
     assert names == ('foo',)
 
 
+def test_database_names_to_dump_with_non_all_name_and_skip_names_warns():
+    environment = flexmock()
+
+    flexmock(module.logger).should_receive('warning').once()
+
+    names = module.database_names_to_dump(
+        {'name': 'foo', 'skip_names': ('foo', 'bar')},
+        {},
+        'root',
+        'trustsome1',
+        environment,
+        dry_run=False,
+    )
+
+    assert names == ('foo',)
+
+
 def test_database_names_to_dump_bails_for_dry_run():
     environment = flexmock()
     flexmock(module.borgmatic.hooks.credential.parse).should_receive(
@@ -75,6 +92,45 @@ def test_database_names_to_dump_queries_mysql_for_database_names():
     assert names == ('foo', 'bar')
 
 
+def test_database_names_to_dump_with_database_name_all_and_skip_names_filters_out_unwanted_databases():
+    environment = flexmock()
+    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.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).should_receive('execute_command_and_capture_output').with_args(
+        (
+            'mysql',
+            '--defaults-extra-file=/dev/fd/99',
+            '--skip-column-names',
+            '--batch',
+            '--execute',
+            'show schemas',
+        ),
+        environment=environment,
+    ).and_return('foo\nbar\nbaz\nmysql\n').once()
+
+    names = module.database_names_to_dump(
+        {'name': 'all', 'skip_names': ('foo', 'bar')},
+        {},
+        'root',
+        'trustsome1',
+        environment,
+        dry_run=False,
+    )
+
+    assert names == ('baz',)
+
+
 def test_database_names_to_dump_with_environment_password_transport_skips_defaults_file_and_passes_user_flag():
     environment = flexmock()
     flexmock(module.borgmatic.hooks.credential.parse).should_receive(