Browse Source

For the MariaDB and MySQL database hooks, add a "socket_path" option for Unix socket database connections (#1193).

Dan Helfman 4 days ago
parent
commit
a31f913f54

+ 2 - 0
NEWS

@@ -8,6 +8,8 @@
    systemd service file and the ZFS, LVM, and Btrfs hooks.
  * #1193: In the documentation for the MariaDB/MySQL database hooks, clarify how to set custom
    command-line flags for database commands.
+ * #1193: For the MariaDB and MySQL database hooks, add a "socket_path" option for Unix socket
+   database connections.
  * #1194: Fix for an incorrect diff command shown when running the "generate config" action with a
    source configuration file. 
  * #1195: Fix a regression in the ZFS, LVM, and Btrfs hooks in which snapshotted paths ignored

+ 28 - 0
borgmatic/config/schema.yaml

@@ -1717,6 +1717,20 @@ properties:
                     description: |
                         Port to restore to. Defaults to the "port" option.
                     example: 5433
+                socket_path:
+                    type: string
+                    description: |
+                        Path of a Unix socket to connect to instead of a remote
+                        hostname. Ignored when "hostname" is set and not
+                        "localhost". Defaults to "/run/mysqld/mysqld.sock".
+                    example: database.example.org
+                restore_socket_path:
+                    type: string
+                    description: |
+                        Path of a Unix socket to connect to instead of a remote
+                        hostname. Ignored when "hostname" is set and not
+                        "localhost". Defaults to the "socket_path" option.
+                    example: database.example.org
                 username:
                     type: string
                     description: |
@@ -1911,6 +1925,20 @@ properties:
                     description: |
                         Port to restore to. Defaults to the "port" option.
                     example: 5433
+                socket_path:
+                    type: string
+                    description: |
+                        Path of a Unix socket to connect to instead of a remote
+                        hostname. Ignored when "hostname" is set and not
+                        "localhost". Defaults to "/run/mysqld/mysqld.sock".
+                    example: database.example.org
+                restore_socket_path:
+                    type: string
+                    description: |
+                        Path of a Unix socket to connect to instead of a remote
+                        hostname. Ignored when "hostname" is set and not
+                        "localhost". Defaults to the "socket_path" option.
+                    example: database.example.org
                 username:
                     type: string
                     description: |

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

@@ -132,6 +132,8 @@ def database_names_to_dump(database, config, username, password, environment, dr
     extra_options, defaults_extra_filename = parse_extra_options(database.get('list_options'))
     password_transport = database.get('password_transport', 'pipe')
     hostname = database_config.resolve_database_option('hostname', database)
+    socket_path = database.get('socket_path')
+
     show_command = (
         mariadb_show_command
         + (
@@ -143,6 +145,7 @@ def database_names_to_dump(database, config, username, password, environment, dr
         + (('--host', hostname) if hostname else ())
         + (('--port', str(database['port'])) if 'port' in database else ())
         + (('--protocol', 'tcp') if hostname or 'port' in database else ())
+        + (('--socket', socket_path) if socket_path else ())
         + (('--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 ())
@@ -213,6 +216,8 @@ def execute_dump_command(
     extra_options, defaults_extra_filename = parse_extra_options(database.get('options'))
     password_transport = database.get('password_transport', 'pipe')
     hostname = database_config.resolve_database_option('hostname', database)
+    socket_path = database.get('socket_path')
+
     dump_command = (
         mariadb_dump_command
         + (
@@ -225,6 +230,7 @@ def execute_dump_command(
         + (('--host', hostname) if hostname else ())
         + (('--port', str(database['port'])) if 'port' in database else ())
         + (('--protocol', 'tcp') if hostname or 'port' in database else ())
+        + (('--socket', socket_path) if socket_path else ())
         + (('--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 ())
@@ -448,6 +454,7 @@ def restore_data_source_dump(
     port = database_config.resolve_database_option(
         'port', data_source, connection_params, restore=True
     )
+    socket_path = database_config.resolve_database_option('socket_path', data_source, restore=True)
     tls = database_config.resolve_database_option('tls', data_source, restore=True)
     username = borgmatic.hooks.credential.parse.resolve_credential(
         database_config.resolve_database_option(
@@ -467,6 +474,7 @@ def restore_data_source_dump(
     )
     extra_options, defaults_extra_filename = parse_extra_options(data_source.get('restore_options'))
     password_transport = data_source.get('password_transport', 'pipe')
+
     restore_command = (
         mariadb_restore_command
         + (
@@ -479,6 +487,7 @@ def restore_data_source_dump(
         + (('--host', hostname) if hostname else ())
         + (('--port', str(port)) if port else ())
         + (('--protocol', 'tcp') if hostname or port else ())
+        + (('--socket', socket_path) if socket_path else ())
         + (('--user', username) if username and password_transport == 'environment' else ())
         + (('--ssl',) if tls is True else ())
         + (('--skip-ssl',) if tls is False else ())

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

@@ -57,6 +57,8 @@ def database_names_to_dump(database, config, username, password, environment, dr
     )
     password_transport = database.get('password_transport', 'pipe')
     hostname = database_config.resolve_database_option('hostname', database)
+    socket_path = database.get('socket_path')
+
     show_command = (
         mysql_show_command
         + (
@@ -72,6 +74,7 @@ def database_names_to_dump(database, config, username, password, environment, dr
         + (('--host', hostname) if hostname else ())
         + (('--port', str(database['port'])) if 'port' in database else ())
         + (('--protocol', 'tcp') if hostname or 'port' in database else ())
+        + (('--socket', socket_path) if socket_path else ())
         + (('--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 ())
@@ -140,6 +143,8 @@ def execute_dump_command(
     )
     password_transport = database.get('password_transport', 'pipe')
     hostname = database_config.resolve_database_option('hostname', database)
+    socket_path = database.get('socket_path')
+
     dump_command = (
         mysql_dump_command
         + (
@@ -156,6 +161,7 @@ def execute_dump_command(
         + (('--host', hostname) if hostname else ())
         + (('--port', str(database['port'])) if 'port' in database else ())
         + (('--protocol', 'tcp') if hostname or 'port' in database else ())
+        + (('--socket', socket_path) if socket_path else ())
         + (('--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 ())
@@ -379,6 +385,7 @@ def restore_data_source_dump(
     port = database_config.resolve_database_option(
         'port', data_source, connection_params, restore=True
     )
+    socket_path = database_config.resolve_database_option('socket_path', data_source, restore=True)
     tls = database_config.resolve_database_option('tls', data_source, restore=True)
     username = borgmatic.hooks.credential.parse.resolve_credential(
         database_config.resolve_database_option(
@@ -400,6 +407,7 @@ def restore_data_source_dump(
         borgmatic.hooks.data_source.mariadb.parse_extra_options(data_source.get('restore_options'))
     )
     password_transport = data_source.get('password_transport', 'pipe')
+
     restore_command = (
         mysql_restore_command
         + (
@@ -416,6 +424,7 @@ def restore_data_source_dump(
         + (('--host', hostname) if hostname else ())
         + (('--port', str(port)) if port else ())
         + (('--protocol', 'tcp') if hostname or port else ())
+        + (('--socket', socket_path) if socket_path else ())
         + (('--user', username) if username and password_transport == 'environment' else ())
         + (('--ssl',) if tls is True else ())
         + (('--skip-ssl',) if tls is False else ())

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

@@ -181,6 +181,7 @@ def test_database_names_to_dump_queries_mariadb_for_database_names():
         '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',
@@ -220,6 +221,7 @@ def test_database_names_to_dump_with_database_name_all_and_skip_names_filters_ou
         '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',
@@ -251,12 +253,53 @@ def test_database_names_to_dump_with_database_name_all_and_skip_names_filters_ou
     assert names == ('baz',)
 
 
+def test_database_names_to_dump_runs_mariadb_with_socket_path():
+    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.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.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
+    flexmock(module).should_receive('execute_command_and_capture_output').with_args(
+        (
+            'mariadb',
+            '--defaults-extra-file=/dev/fd/99',
+            '--socket',
+            '/socket',
+            '--skip-column-names',
+            '--batch',
+            '--execute',
+            'show schemas',
+        ),
+        environment=environment,
+        working_directory=None,
+    ).and_return('foo\nbar\nmysql\n').once()
+
+    names = module.database_names_to_dump(
+        {'name': 'all', 'socket_path': '/socket'},
+        {},
+        'root',
+        'trustsome1',
+        environment,
+        dry_run=False,
+    )
+
+    assert names == ('foo', 'bar')
+
+
 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(
         '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').never()
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
@@ -291,6 +334,7 @@ def test_database_names_to_dump_runs_mariadb_with_tls():
         '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',
@@ -329,6 +373,7 @@ def test_database_names_to_dump_runs_mariadb_without_tls():
         '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',
@@ -706,6 +751,7 @@ def test_database_names_to_dump_runs_mariadb_with_list_options():
         ('--skip-ssl',),
         'mariadb.cnf',
     )
+    flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
     flexmock(module).should_receive('make_defaults_file_options').with_args(
         'root',
         'trustsome1',
@@ -742,6 +788,7 @@ def test_database_names_to_dump_runs_non_default_mariadb_with_list_options():
         ('--skip-ssl',),
         'mariadb.cnf',
     )
+    flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
     flexmock(module).should_receive('make_defaults_file_options').with_args(
         'root',
         'trustsome1',
@@ -776,6 +823,7 @@ def test_execute_dump_command_runs_mariadb_dump():
         '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',
@@ -823,6 +871,7 @@ def test_execute_dump_command_with_environment_password_transport_skips_defaults
         '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').never()
     flexmock(module.dump).should_receive('create_named_pipe_for_dump')
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
@@ -867,6 +916,7 @@ def test_execute_dump_command_runs_mariadb_dump_without_add_drop_database():
         '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',
@@ -913,6 +963,9 @@ def test_execute_dump_command_runs_mariadb_dump_with_hostname_and_port():
         '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(
+        'database.example.org'
+    )
     flexmock(module).should_receive('make_defaults_file_options').with_args(
         'root',
         'trustsome1',
@@ -966,6 +1019,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_tls():
         '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',
@@ -1014,6 +1068,7 @@ def test_execute_dump_command_runs_mariadb_dump_without_tls():
         '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',
@@ -1062,6 +1117,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_username_and_password():
         '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',
@@ -1109,6 +1165,7 @@ def test_execute_dump_command_runs_mariadb_dump_with_options():
         'resolve_credential',
     ).replace_with(lambda value, config: value)
     flexmock(module).should_receive('parse_extra_options').and_return(('--stuff=such',), 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',
@@ -1157,6 +1214,7 @@ def test_execute_dump_command_runs_non_default_mariadb_dump_with_options():
         'resolve_credential',
     ).replace_with(lambda value, config: value)
     flexmock(module).should_receive('parse_extra_options').and_return(('--stuff=such',), 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',
@@ -1205,6 +1263,7 @@ def test_execute_dump_command_with_duplicate_dump_skips_mariadb_dump():
     flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
     flexmock(module.os.path).should_receive('exists').and_return(True)
     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',
@@ -1236,6 +1295,7 @@ def test_execute_dump_command_with_dry_run_skips_mariadb_dump():
         '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',
@@ -1269,6 +1329,7 @@ def test_restore_data_source_dump_runs_mariadb_to_restore():
         '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(
         None,
         None,
@@ -1309,6 +1370,7 @@ def test_restore_data_source_dump_runs_mariadb_with_options():
         'resolve_credential',
     ).replace_with(lambda value, config: value)
     flexmock(module).should_receive('parse_extra_options').and_return(('--harder',), None)
+    flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
     flexmock(module).should_receive('make_defaults_file_options').with_args(
         None,
         None,
@@ -1351,6 +1413,7 @@ def test_restore_data_source_dump_runs_non_default_mariadb_with_options():
         'resolve_credential',
     ).replace_with(lambda value, config: value)
     flexmock(module).should_receive('parse_extra_options').and_return(('--harder',), None)
+    flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
     flexmock(module).should_receive('make_defaults_file_options').with_args(
         None,
         None,
@@ -1391,6 +1454,9 @@ def test_restore_data_source_dump_runs_mariadb_with_hostname_and_port():
         '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').replace_with(
+        lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
+    )
     flexmock(module).should_receive('make_defaults_file_options').with_args(
         None,
         None,
@@ -1432,6 +1498,54 @@ def test_restore_data_source_dump_runs_mariadb_with_hostname_and_port():
     )
 
 
+def test_restore_data_source_dump_runs_mariadb_with_socket_path():
+    hook_config = [{'name': 'foo', 'socket_path': '/socket'}]
+    extract_process = flexmock(stdout=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.database_config).should_receive('resolve_database_option').replace_with(
+        lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
+    )
+    flexmock(module).should_receive('make_defaults_file_options').with_args(
+        None,
+        None,
+        None,
+    ).and_return(())
+    flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
+    flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
+    flexmock(module).should_receive('execute_command_with_processes').with_args(
+        (
+            'mariadb',
+            '--batch',
+            '--socket',
+            '/socket',
+        ),
+        processes=[extract_process],
+        output_log_level=logging.DEBUG,
+        input_file=extract_process.stdout,
+        environment={'USER': 'root'},
+        working_directory=None,
+    ).once()
+
+    module.restore_data_source_dump(
+        hook_config,
+        {},
+        data_source=hook_config[0],
+        dry_run=False,
+        extract_process=extract_process,
+        connection_params={
+            'hostname': None,
+            'port': None,
+            'username': None,
+            'password': None,
+        },
+        borgmatic_runtime_directory='/run/borgmatic',
+    )
+
+
 def test_restore_data_source_dump_runs_mariadb_with_tls():
     hook_config = [{'name': 'foo', 'tls': True}]
     extract_process = flexmock(stdout=flexmock())
@@ -1440,6 +1554,9 @@ def test_restore_data_source_dump_runs_mariadb_with_tls():
         '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').replace_with(
+        lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
+    )
     flexmock(module).should_receive('make_defaults_file_options').with_args(
         None,
         None,
@@ -1484,6 +1601,9 @@ def test_restore_data_source_dump_runs_mariadb_without_tls():
         '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').replace_with(
+        lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
+    )
     flexmock(module).should_receive('make_defaults_file_options').with_args(
         None,
         None,
@@ -1528,6 +1648,9 @@ def test_restore_data_source_dump_runs_mariadb_with_username_and_password():
         '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').replace_with(
+        lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
+    )
     flexmock(module).should_receive('make_defaults_file_options').with_args(
         'root',
         'trustsome1',
@@ -1575,6 +1698,9 @@ def test_restore_data_source_with_environment_password_transport_skips_defaults_
         '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').replace_with(
+        lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
+    )
     flexmock(module).should_receive('make_defaults_file_options').never()
     flexmock(module.os).should_receive('environ').and_return(
         {'USER': 'root', 'MYSQL_PWD': 'trustsome1'},
@@ -1623,6 +1749,11 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
         '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').replace_with(
+        lambda option, data_source, connection_params=None, restore=False: (
+            connection_params or {}
+        ).get(option)
+    )
     flexmock(module).should_receive('make_defaults_file_options').with_args(
         'cliusername',
         'clipassword',
@@ -1687,6 +1818,11 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
         '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').replace_with(
+        lambda option, data_source, connection_params=None, restore=False: data_source.get(
+            f'restore_{option}'
+        )
+    )
     flexmock(module).should_receive('make_defaults_file_options').with_args(
         'restoreuser',
         'restorepass',
@@ -1737,6 +1873,7 @@ def test_restore_data_source_dump_with_dry_run_skips_restore():
         '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(
         None,
         None,

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

@@ -65,6 +65,7 @@ def test_database_names_to_dump_queries_mysql_for_database_names():
     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',))
@@ -102,6 +103,7 @@ def test_database_names_to_dump_with_database_name_all_and_skip_names_filters_ou
     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(
@@ -135,6 +137,46 @@ def test_database_names_to_dump_with_database_name_all_and_skip_names_filters_ou
     assert names == ('baz',)
 
 
+def test_database_names_to_dump_runs_mysql_with_socket_path():
+    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.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.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
+    flexmock(module).should_receive('execute_command_and_capture_output').with_args(
+        (
+            'mysql',
+            '--defaults-extra-file=/dev/fd/99',
+            '--socket',
+            '/socket',
+            '--skip-column-names',
+            '--batch',
+            '--execute',
+            'show schemas',
+        ),
+        environment=environment,
+        working_directory=None,
+    ).and_return('foo\nbar\nmysql\n').once()
+
+    names = module.database_names_to_dump(
+        {'name': 'all', 'socket_path': '/socket'},
+        {},
+        'root',
+        'trustsome1',
+        environment,
+        dry_run=False,
+    )
+
+    assert names == ('foo', 'bar')
+
+
 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(
@@ -143,6 +185,7 @@ def test_database_names_to_dump_with_environment_password_transport_skips_defaul
     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',
     ).never()
@@ -181,6 +224,7 @@ def test_database_names_to_dump_runs_mysql_with_tls():
     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',))
@@ -219,6 +263,7 @@ def test_database_names_to_dump_runs_mysql_without_tls():
     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',))
@@ -591,6 +636,7 @@ def test_database_names_to_dump_runs_mysql_with_list_options():
     flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
         'parse_extra_options',
     ).and_return(('--skip-ssl',), 'my.cnf')
+    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', 'my.cnf').and_return(('--defaults-extra-file=/dev/fd/99',))
@@ -624,6 +670,7 @@ def test_database_names_to_dump_runs_non_default_mysql_with_list_options():
     flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
         'parse_extra_options',
     ).and_return(('--skip-ssl',), 'my.cnf')
+    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', 'my.cnf').and_return(('--defaults-extra-file=/dev/fd/99',))
@@ -658,6 +705,7 @@ def test_execute_dump_command_runs_mysqldump():
     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',))
@@ -705,6 +753,7 @@ def test_execute_dump_command_with_environment_password_transport_skips_defaults
     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',
     ).never()
@@ -753,6 +802,7 @@ def test_execute_dump_command_runs_mysqldump_without_add_drop_database():
     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',))
@@ -799,6 +849,9 @@ def test_execute_dump_command_runs_mysqldump_with_hostname_and_port():
     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(
+        'database.example.org'
+    )
     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',))
@@ -852,6 +905,7 @@ def test_execute_dump_command_runs_mysqldump_with_tls():
     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',))
@@ -900,6 +954,7 @@ def test_execute_dump_command_runs_mysqldump_without_tls():
     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',))
@@ -948,6 +1003,7 @@ def test_execute_dump_command_runs_mysqldump_with_username_and_password():
     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',))
@@ -995,6 +1051,7 @@ def test_execute_dump_command_runs_mysqldump_with_options():
     flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
         'parse_extra_options',
     ).and_return(('--stuff=such',), 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',))
@@ -1043,6 +1100,7 @@ def test_execute_dump_command_runs_non_default_mysqldump():
     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',))
@@ -1089,6 +1147,7 @@ def test_execute_dump_command_with_duplicate_dump_skips_mysqldump():
     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',))
@@ -1120,6 +1179,7 @@ def test_execute_dump_command_with_dry_run_skips_mysqldump():
     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',))
@@ -1153,6 +1213,7 @@ def test_restore_data_source_dump_runs_mysql_to_restore():
     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(None, None, None).and_return(())
@@ -1193,6 +1254,7 @@ def test_restore_data_source_dump_runs_mysql_with_options():
     flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
         'parse_extra_options',
     ).and_return(('--harder',), 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(None, None, None).and_return(())
@@ -1233,6 +1295,7 @@ def test_restore_data_source_dump_runs_non_default_mysql_with_options():
     flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
         'parse_extra_options',
     ).and_return(('--harder',), 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(None, None, None).and_return(())
@@ -1273,6 +1336,9 @@ def test_restore_data_source_dump_runs_mysql_with_hostname_and_port():
     flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
         'parse_extra_options',
     ).and_return((), None)
+    flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
+        lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
+    )
     flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
         'make_defaults_file_options',
     ).with_args(None, None, None).and_return(())
@@ -1312,6 +1378,58 @@ def test_restore_data_source_dump_runs_mysql_with_hostname_and_port():
     )
 
 
+def test_restore_data_source_dump_runs_mysql_with_socket_path():
+    hook_config = [{'name': 'foo', 'socket_path': '/socket'}]
+    extract_process = flexmock(stdout=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.database_config).should_receive('resolve_database_option').replace_with(
+        lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
+    )
+    flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
+        'make_defaults_file_options'
+    ).with_args(
+        None,
+        None,
+        None,
+    ).and_return(())
+    flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
+    flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
+    flexmock(module).should_receive('execute_command_with_processes').with_args(
+        (
+            'mysql',
+            '--batch',
+            '--socket',
+            '/socket',
+        ),
+        processes=[extract_process],
+        output_log_level=logging.DEBUG,
+        input_file=extract_process.stdout,
+        environment={'USER': 'root'},
+        working_directory=None,
+    ).once()
+
+    module.restore_data_source_dump(
+        hook_config,
+        {},
+        data_source=hook_config[0],
+        dry_run=False,
+        extract_process=extract_process,
+        connection_params={
+            'hostname': None,
+            'port': None,
+            'username': None,
+            'password': None,
+        },
+        borgmatic_runtime_directory='/run/borgmatic',
+    )
+
+
 def test_restore_data_source_dump_runs_mysql_with_tls():
     hook_config = [{'name': 'foo', 'tls': True}]
     extract_process = flexmock(stdout=flexmock())
@@ -1322,6 +1440,9 @@ def test_restore_data_source_dump_runs_mysql_with_tls():
     flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
         'parse_extra_options',
     ).and_return((), None)
+    flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
+        lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
+    )
     flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
         'make_defaults_file_options',
     ).with_args(None, None, None).and_return(())
@@ -1363,6 +1484,9 @@ def test_restore_data_source_dump_runs_mysql_without_tls():
     flexmock(module.borgmatic.hooks.credential.parse).should_receive(
         'resolve_credential',
     ).replace_with(lambda value, config: value)
+    flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
+        lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
+    )
     flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
         'parse_extra_options',
     ).and_return((), None)
@@ -1410,6 +1534,9 @@ def test_restore_data_source_dump_runs_mysql_with_username_and_password():
     flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
         'parse_extra_options',
     ).and_return((), None)
+    flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
+        lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
+    )
     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',))
@@ -1457,6 +1584,9 @@ def test_restore_data_source_with_environment_password_transport_skips_defaults_
     flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
         'parse_extra_options',
     ).and_return((), None)
+    flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
+        lambda option, data_source, connection_params=None, restore=False: data_source.get(option)
+    )
     flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
         'make_defaults_file_options',
     ).never()
@@ -1509,6 +1639,11 @@ def test_restore_data_source_dump_with_connection_params_uses_connection_params_
     flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
         'parse_extra_options',
     ).and_return((), None)
+    flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
+        lambda option, data_source, connection_params=None, restore=False: (
+            connection_params or {}
+        ).get(option)
+    )
     flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
         'make_defaults_file_options',
     ).with_args('cliusername', 'clipassword', None).and_return(
@@ -1575,6 +1710,11 @@ def test_restore_data_source_dump_without_connection_params_uses_restore_params_
     flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
         'parse_extra_options',
     ).and_return((), None)
+    flexmock(module.database_config).should_receive('resolve_database_option').replace_with(
+        lambda option, data_source, connection_params=None, restore=False: data_source.get(
+            f'restore_{option}'
+        )
+    )
     flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
         'make_defaults_file_options',
     ).with_args('restoreuser', 'restorepass', None).and_return(
@@ -1627,6 +1767,7 @@ def test_restore_data_source_dump_with_dry_run_skips_restore():
     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(None, None, None).and_return(())