Browse Source

Fork a MariaDB database hook from the MySQL database hook (#727).

Dan Helfman 1 năm trước cách đây
mục cha
commit
193dd93de2

+ 8 - 8
.drone.yml

@@ -16,16 +16,16 @@ services:
       POSTGRES_USER: postgres2
       POSTGRES_USER: postgres2
     commands:
     commands:
       - docker-entrypoint.sh -p 5433
       - docker-entrypoint.sh -p 5433
-  - name: mysql
-    image: docker.io/mariadb:10.5
+  - name: mariadb
+    image: docker.io/mariadb:10.11.4
     environment:
     environment:
-      MYSQL_ROOT_PASSWORD: test
-      MYSQL_DATABASE: test
-  - name: mysql2
-    image: docker.io/mariadb:10.5
+      MARIADB_ROOT_PASSWORD: test
+      MARIADB_DATABASE: test
+  - name: mariadb2
+    image: docker.io/mariadb:10.11.4
     environment:
     environment:
-      MYSQL_ROOT_PASSWORD: test2
-      MYSQL_DATABASE: test
+      MARIADB_ROOT_PASSWORD: test2
+      MARIADB_DATABASE: test
     commands:
     commands:
       - docker-entrypoint.sh --port=3307
       - docker-entrypoint.sh --port=3307
   - name: mongodb
   - name: mongodb

+ 5 - 0
NEWS

@@ -1,3 +1,8 @@
+1.8.2.dev0
+ * #727: Add a MariaDB database hook that uses native MariaDB commands instead of the deprecated
+   MySQL ones. Be aware though that any existing backups made with the "mysql_databases:" hook are
+   only restorable with a "mysql_databases:" configuration.
+
 1.8.1
 1.8.1
  * #326: Add documentation for restoring a database to an alternate host:
  * #326: Add documentation for restoring a database to an alternate host:
    https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#restore-to-an-alternate-host
    https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#restore-to-an-alternate-host

+ 131 - 20
borgmatic/config/schema.yaml

@@ -841,10 +841,121 @@ properties:
         description: |
         description: |
             List of one or more PostgreSQL databases to dump before creating a
             List of one or more PostgreSQL databases to dump before creating a
             backup, run once per configuration file. The database dumps are
             backup, run once per configuration file. The database dumps are
-            added to your source directories at runtime, backed up, and removed
-            afterwards. Requires pg_dump/pg_dumpall/pg_restore commands. See
+            added to your source directories at runtime and streamed directly
+            to Borg. Requires pg_dump/pg_dumpall/pg_restore commands. See
             https://www.postgresql.org/docs/current/app-pgdump.html and
             https://www.postgresql.org/docs/current/app-pgdump.html and
-            https://www.postgresql.org/docs/current/libpq-ssl.html for details.
+            https://www.postgresql.org/docs/current/libpq-ssl.html for
+            details.
+    mariadb_databases:
+        type: array
+        items:
+            type: object
+            required: ['name']
+            additionalProperties: false
+            properties:
+                name:
+                    type: string
+                    description: |
+                        Database name (required if using this hook). Or "all" to
+                        dump all databases on the host. Note that using this
+                        database hook implicitly enables both read_special and
+                        one_file_system (see above) to support dump and restore
+                        streaming.
+                    example: users
+                hostname:
+                    type: string
+                    description: |
+                        Database hostname to connect to. Defaults to connecting
+                        via local Unix socket.
+                    example: database.example.org
+                restore_hostname:
+                    type: string
+                    description: |
+                        Database hostname to restore to. Defaults to the
+                        "hostname" option.
+                    example: database.example.org
+                port:
+                    type: integer
+                    description: Port to connect to. Defaults to 3306.
+                    example: 3307
+                restore_port:
+                    type: integer
+                    description: |
+                        Port to restore to. Defaults to the "port" option.
+                    example: 5433
+                username:
+                    type: string
+                    description: |
+                        Username with which to connect to the database. Defaults
+                        to the username of the current user.
+                    example: dbuser
+                restore_username:
+                    type: string
+                    description: |
+                        Username with which to restore the database. Defaults to
+                        the "username" option.
+                    example: dbuser
+                password:
+                    type: string
+                    description: |
+                        Password with which to connect to the database. Omitting
+                        a password will only work if MariaDB is configured to
+                        trust the configured username without a password.
+                    example: trustsome1
+                restore_password:
+                    type: string
+                    description: |
+                        Password with which to connect to the restore database.
+                        Defaults to the "password" option.
+                    example: trustsome1
+                format:
+                    type: string
+                    enum: ['sql']
+                    description: |
+                        Database dump output format. Currently only "sql" is
+                        supported. Defaults to "sql" for a single database. Or,
+                        when database name is "all" and format is blank, dumps
+                        all databases to a single file. But if a format is
+                        specified with an "all" database name, dumps each
+                        database to a separate file of that format, allowing
+                        more convenient restores of individual databases.
+                    example: directory
+                add_drop_database:
+                    type: boolean
+                    description: |
+                        Use the "--add-drop-database" flag with mariadb-dump,
+                        causing the database to be dropped right before restore.
+                        Defaults to true.
+                    example: false
+                options:
+                    type: string
+                    description: |
+                        Additional mariadb-dump options to pass directly to the
+                        dump command, without performing any validation on them.
+                        See mariadb-dump documentation for details.
+                    example: --skip-comments
+                list_options:
+                    type: string
+                    description: |
+                        Additional options to pass directly to the mariadb
+                        command that lists available databases, without
+                        performing any validation on them. See mariadb command
+                        documentation for details.
+                    example: --defaults-extra-file=mariadb.cnf
+                restore_options:
+                    type: string
+                    description: |
+                        Additional options to pass directly to the mariadb
+                        command that restores database dumps, without
+                        performing any validation on them. See mariadb command
+                        documentation for details.
+                    example: --defaults-extra-file=mariadb.cnf
+        description: |
+            List of one or more MariaDB databases to dump before creating a
+            backup, run once per configuration file. The database dumps are
+            added to your source directories at runtime and streamed directly
+            to Borg. Requires mariadb-dump/mariadb commands. See
+            https://mariadb.com/kb/en/library/mysqldump/ for details.
     mysql_databases:
     mysql_databases:
         type: array
         type: array
         items:
         items:
@@ -893,7 +1004,7 @@ properties:
                     description: |
                     description: |
                         Username with which to restore the database. Defaults to
                         Username with which to restore the database. Defaults to
                         the "username" option.
                         the "username" option.
-                    example: dbuser                        
+                    example: dbuser
                 password:
                 password:
                     type: string
                     type: string
                     description: |
                     description: |
@@ -906,7 +1017,7 @@ properties:
                     description: |
                     description: |
                         Password with which to connect to the restore database.
                         Password with which to connect to the restore database.
                         Defaults to the "password" option.
                         Defaults to the "password" option.
-                    example: trustsome1                        
+                    example: trustsome1
                 format:
                 format:
                     type: string
                     type: string
                     enum: ['sql']
                     enum: ['sql']
@@ -936,26 +1047,26 @@ properties:
                 list_options:
                 list_options:
                     type: string
                     type: string
                     description: |
                     description: |
-                        Additional mysql options to pass directly to the mysql
+                        Additional options to pass directly to the mysql
                         command that lists available databases, without
                         command that lists available databases, without
-                        performing any validation on them. See mysql
+                        performing any validation on them. See mysql command
                         documentation for details.
                         documentation for details.
                     example: --defaults-extra-file=my.cnf
                     example: --defaults-extra-file=my.cnf
                 restore_options:
                 restore_options:
                     type: string
                     type: string
                     description: |
                     description: |
-                        Additional mysql options to pass directly to the mysql
-                        command that restores database dumps, without performing
-                        any validation on them. See mysql documentation for
-                        details.
+                        Additional options to pass directly to the mysql
+                        command that restores database dumps, without
+                        performing any validation on them. See mysql command
+                        documentation for details.
                     example: --defaults-extra-file=my.cnf
                     example: --defaults-extra-file=my.cnf
         description: |
         description: |
-            List of one or more MySQL/MariaDB databases to dump before creating
-            a backup, run once per configuration file. The database dumps are
-            added to your source directories at runtime, backed up, and removed
-            afterwards. Requires mysqldump/mysql commands (from either MySQL or
-            MariaDB). See https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html
-            or https://mariadb.com/kb/en/library/mysqldump/ for details.
+            List of one or more MySQL databases to dump before creating a
+            backup, run once per configuration file. The database dumps are
+            added to your source directories at runtime and streamed directly
+            to Borg. Requires mysqldump/mysql commands. See
+            https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html for
+            details.
     sqlite_databases:
     sqlite_databases:
         type: array
         type: array
         items:
         items:
@@ -1033,7 +1144,7 @@ properties:
                     description: |
                     description: |
                         Username with which to restore the database. Defaults to
                         Username with which to restore the database. Defaults to
                         the "username" option.
                         the "username" option.
-                    example: dbuser                        
+                    example: dbuser
                 password:
                 password:
                     type: string
                     type: string
                     description: |
                     description: |
@@ -1080,8 +1191,8 @@ properties:
         description: |
         description: |
             List of one or more MongoDB databases to dump before creating a
             List of one or more MongoDB databases to dump before creating a
             backup, run once per configuration file. The database dumps are
             backup, run once per configuration file. The database dumps are
-            added to your source directories at runtime, backed up, and removed
-            afterwards. Requires mongodump/mongorestore commands. See
+            added to your source directories at runtime and streamed directly
+            to Borg. Requires mongodump/mongorestore commands. See
             https://docs.mongodb.com/database-tools/mongodump/ and
             https://docs.mongodb.com/database-tools/mongodump/ and
             https://docs.mongodb.com/database-tools/mongorestore/ for details.
             https://docs.mongodb.com/database-tools/mongorestore/ for details.
     ntfy:
     ntfy:

+ 2 - 0
borgmatic/hooks/dispatch.py

@@ -4,6 +4,7 @@ from borgmatic.hooks import (
     cronhub,
     cronhub,
     cronitor,
     cronitor,
     healthchecks,
     healthchecks,
+    mariadb,
     mongodb,
     mongodb,
     mysql,
     mysql,
     ntfy,
     ntfy,
@@ -18,6 +19,7 @@ HOOK_NAME_TO_MODULE = {
     'cronhub': cronhub,
     'cronhub': cronhub,
     'cronitor': cronitor,
     'cronitor': cronitor,
     'healthchecks': healthchecks,
     'healthchecks': healthchecks,
+    'mariadb_databases': mariadb,
     'mongodb_databases': mongodb,
     'mongodb_databases': mongodb,
     'mysql_databases': mysql,
     'mysql_databases': mysql,
     'ntfy': ntfy,
     'ntfy': ntfy,

+ 2 - 1
borgmatic/hooks/dump.py

@@ -7,9 +7,10 @@ from borgmatic.borg.state import DEFAULT_BORGMATIC_SOURCE_DIRECTORY
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 DATABASE_HOOK_NAMES = (
 DATABASE_HOOK_NAMES = (
-    'postgresql_databases',
+    'mariadb_databases',
     'mysql_databases',
     'mysql_databases',
     'mongodb_databases',
     'mongodb_databases',
+    'postgresql_databases',
     'sqlite_databases',
     'sqlite_databases',
 )
 )
 
 

+ 242 - 0
borgmatic/hooks/mariadb.py

@@ -0,0 +1,242 @@
+import copy
+import logging
+import os
+
+from borgmatic.execute import (
+    execute_command,
+    execute_command_and_capture_output,
+    execute_command_with_processes,
+)
+from borgmatic.hooks import dump
+
+logger = logging.getLogger(__name__)
+
+
+def make_dump_path(config):  # pragma: no cover
+    '''
+    Make the dump path from the given configuration dict and the name of this hook.
+    '''
+    return dump.make_database_dump_path(
+        config.get('borgmatic_source_directory'), 'mariadb_databases'
+    )
+
+
+SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
+
+
+def database_names_to_dump(database, extra_environment, log_prefix, dry_run):
+    '''
+    Given a requested database config, return the corresponding sequence of database 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.
+    '''
+    if database['name'] != 'all':
+        return (database['name'],)
+    if dry_run:
+        return ()
+
+    show_command = (
+        ('mariadb',)
+        + (tuple(database['list_options'].split(' ')) if 'list_options' in database else ())
+        + (('--host', database['hostname']) if 'hostname' in database else ())
+        + (('--port', str(database['port'])) if 'port' in database else ())
+        + (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+        + (('--user', database['username']) if 'username' in database else ())
+        + ('--skip-column-names', '--batch')
+        + ('--execute', 'show schemas')
+    )
+    logger.debug(f'{log_prefix}: Querying for "all" MariaDB databases to dump')
+    show_output = execute_command_and_capture_output(
+        show_command, extra_environment=extra_environment
+    )
+
+    return tuple(
+        show_name
+        for show_name in show_output.strip().splitlines()
+        if show_name not in SYSTEM_DATABASE_NAMES
+    )
+
+
+def execute_dump_command(
+    database, log_prefix, dump_path, database_names, extra_environment, dry_run, dry_run_label
+):
+    '''
+    Kick off a dump for the given MariaDB database (provided as a configuration dict) to a named
+    pipe constructed from the given dump path and database names. Use the given log prefix in any
+    log entries.
+
+    Return a subprocess.Popen instance for the dump process ready to spew to a named pipe. But if
+    this is a dry run, then don't actually dump anything and return None.
+    '''
+    database_name = database['name']
+    dump_filename = dump.make_database_dump_filename(
+        dump_path, database['name'], database.get('hostname')
+    )
+    if os.path.exists(dump_filename):
+        logger.warning(
+            f'{log_prefix}: Skipping duplicate dump of MariaDB database "{database_name}" to {dump_filename}'
+        )
+        return None
+
+    dump_command = (
+        ('mariadb-dump',)
+        + (tuple(database['options'].split(' ')) if 'options' in database else ())
+        + (('--add-drop-database',) if database.get('add_drop_database', True) else ())
+        + (('--host', database['hostname']) if 'hostname' in database else ())
+        + (('--port', str(database['port'])) if 'port' in database else ())
+        + (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+        + (('--user', database['username']) if 'username' in database else ())
+        + ('--databases',)
+        + database_names
+        + ('--result-file', dump_filename)
+    )
+
+    logger.debug(
+        f'{log_prefix}: Dumping MariaDB database "{database_name}" to {dump_filename}{dry_run_label}'
+    )
+    if dry_run:
+        return None
+
+    dump.create_named_pipe_for_dump(dump_filename)
+
+    return execute_command(
+        dump_command,
+        extra_environment=extra_environment,
+        run_to_completion=False,
+    )
+
+
+def dump_databases(databases, config, log_prefix, dry_run):
+    '''
+    Dump the given MariaDB databases to a named pipe. The databases are supplied as a sequence of
+    dicts, one dict describing each database as per the configuration schema. Use the given
+    configuration dict to construct the destination path and the given log prefix in any log
+    entries.
+
+    Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named
+    pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence.
+    '''
+    dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
+    processes = []
+
+    logger.info(f'{log_prefix}: Dumping MariaDB databases{dry_run_label}')
+
+    for database in databases:
+        dump_path = make_dump_path(config)
+        extra_environment = {'MYSQL_PWD': database['password']} if 'password' in database else None
+        dump_database_names = database_names_to_dump(
+            database, extra_environment, log_prefix, dry_run
+        )
+
+        if not dump_database_names:
+            if dry_run:
+                continue
+
+            raise ValueError('Cannot find any MariaDB databases to dump.')
+
+        if database['name'] == 'all' and database.get('format'):
+            for dump_name in dump_database_names:
+                renamed_database = copy.copy(database)
+                renamed_database['name'] = dump_name
+                processes.append(
+                    execute_dump_command(
+                        renamed_database,
+                        log_prefix,
+                        dump_path,
+                        (dump_name,),
+                        extra_environment,
+                        dry_run,
+                        dry_run_label,
+                    )
+                )
+        else:
+            processes.append(
+                execute_dump_command(
+                    database,
+                    log_prefix,
+                    dump_path,
+                    dump_database_names,
+                    extra_environment,
+                    dry_run,
+                    dry_run_label,
+                )
+            )
+
+    return [process for process in processes if process]
+
+
+def remove_database_dumps(databases, config, log_prefix, dry_run):  # pragma: no cover
+    '''
+    Remove all database dump files for this hook regardless of the given databases. Use the given
+    configuration dict to construct the destination path and the log prefix in any log entries. If
+    this is a dry run, then don't actually remove anything.
+    '''
+    dump.remove_database_dumps(make_dump_path(config), 'MariaDB', log_prefix, dry_run)
+
+
+def make_database_dump_pattern(databases, config, log_prefix, name=None):  # pragma: no cover
+    '''
+    Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a
+    database name to match, return the corresponding glob patterns to match the database dump in an
+    archive.
+    '''
+    return dump.make_database_dump_filename(make_dump_path(config), name, hostname='*')
+
+
+def restore_database_dump(
+    databases_config, config, log_prefix, database_name, dry_run, extract_process, connection_params
+):
+    '''
+    Restore the given MariaDB database from an extract stream. The databases are supplied as a
+    sequence containing one dict describing each database (as per the configuration schema), but
+    only the database corresponding to the given database name is restored. Use the given log prefix
+    in any log entries. If this is a dry run, then don't actually restore anything. Trigger the
+    given active extract process (an instance of subprocess.Popen) to produce output to consume.
+    '''
+    dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
+
+    try:
+        database = next(
+            database_config
+            for database_config in databases_config
+            if database_config.get('name') == database_name
+        )
+    except StopIteration:
+        raise ValueError(
+            f'A database named "{database_name}" could not be found in the configuration'
+        )
+
+    hostname = connection_params['hostname'] or database.get(
+        'restore_hostname', database.get('hostname')
+    )
+    port = str(connection_params['port'] or database.get('restore_port', database.get('port', '')))
+    username = connection_params['username'] or database.get(
+        'restore_username', database.get('username')
+    )
+    password = connection_params['password'] or database.get(
+        'restore_password', database.get('password')
+    )
+
+    restore_command = (
+        ('mariadb', '--batch')
+        + (tuple(database['restore_options'].split(' ')) if 'restore_options' in database else ())
+        + (('--host', hostname) if hostname else ())
+        + (('--port', str(port)) if port else ())
+        + (('--protocol', 'tcp') if hostname or port else ())
+        + (('--user', username) if username else ())
+    )
+    extra_environment = {'MYSQL_PWD': password} if password else None
+
+    logger.debug(f"{log_prefix}: Restoring MariaDB database {database['name']}{dry_run_label}")
+    if dry_run:
+        return
+
+    # Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
+    # if the restore paths don't exist in the archive.
+    execute_command_with_processes(
+        restore_command,
+        [extract_process],
+        output_log_level=logging.DEBUG,
+        input_file=extract_process.stdout,
+        extra_environment=extra_environment,
+    )

+ 17 - 20
borgmatic/hooks/mongodb.py

@@ -59,26 +59,23 @@ def build_dump_command(database, dump_filename, dump_format):
     Return the mongodump command from a single database configuration.
     Return the mongodump command from a single database configuration.
     '''
     '''
     all_databases = database['name'] == 'all'
     all_databases = database['name'] == 'all'
-    command = ['mongodump']
-    if dump_format == 'directory':
-        command.extend(('--out', dump_filename))
-    if 'hostname' in database:
-        command.extend(('--host', database['hostname']))
-    if 'port' in database:
-        command.extend(('--port', str(database['port'])))
-    if 'username' in database:
-        command.extend(('--username', database['username']))
-    if 'password' in database:
-        command.extend(('--password', database['password']))
-    if 'authentication_database' in database:
-        command.extend(('--authenticationDatabase', database['authentication_database']))
-    if not all_databases:
-        command.extend(('--db', database['name']))
-    if 'options' in database:
-        command.extend(database['options'].split(' '))
-    if dump_format != 'directory':
-        command.extend(('--archive', '>', dump_filename))
-    return command
+
+    return (
+        ('mongodump',)
+        + (('--out', dump_filename) if dump_format == 'directory' else ())
+        + (('--host', database['hostname']) if 'hostname' in database else ())
+        + (('--port', str(database['port'])) if 'port' in database else ())
+        + (('--username', database['username']) if 'username' in database else ())
+        + (('--password', database['password']) if 'password' in database else ())
+        + (
+            ('--authenticationDatabase', database['authentication_database'])
+            if 'authentication_database' in database
+            else ()
+        )
+        + (('--db', database['name']) if not all_databases else ())
+        + (tuple(database['options'].split(' ')) if 'options' in database else ())
+        + (('--archive', '>', dump_filename) if dump_format != 'directory' else ())
+    )
 
 
 
 
 def remove_database_dumps(databases, config, log_prefix, dry_run):  # pragma: no cover
 def remove_database_dumps(databases, config, log_prefix, dry_run):  # pragma: no cover

+ 50 - 21
docs/how-to/backup-your-databases.md

@@ -15,7 +15,7 @@ consistent snapshot that is more suited for backups.
 
 
 Fortunately, borgmatic includes built-in support for creating database dumps
 Fortunately, borgmatic includes built-in support for creating database dumps
 prior to running backups. For example, here is everything you need to dump and
 prior to running backups. For example, here is everything you need to dump and
-backup a couple of local PostgreSQL databases and a MySQL/MariaDB database.
+backup a couple of local PostgreSQL databases and a MySQL database.
 
 
 ```yaml
 ```yaml
 postgresql_databases:
 postgresql_databases:
@@ -46,6 +46,16 @@ sqlite_databases:
       path: /var/lib/sqlite3/mydb.sqlite
       path: /var/lib/sqlite3/mydb.sqlite
 ```
 ```
 
 
+<span class="minilink minilink-addedin">New in version 1.8.2</span> If you're
+using MariaDB, use the MariaDB database hook instead of `mysql_databases:` as
+the MariaDB hook calls native MariaDB commands instead of the deprecated MySQL
+ones. For instance:
+
+```yaml
+mariadb_databases:
+    - name: comments
+```
+
 As part of each backup, borgmatic streams a database dump for each configured
 As part of each backup, borgmatic streams a database dump for each configured
 database directly to Borg, so it's included in the backup without consuming
 database directly to Borg, so it's included in the backup without consuming
 additional disk space. (The exceptions are the PostgreSQL/MongoDB "directory"
 additional disk space. (The exceptions are the PostgreSQL/MongoDB "directory"
@@ -75,16 +85,23 @@ postgresql_databases:
       password: trustsome1
       password: trustsome1
       format: tar
       format: tar
       options: "--role=someone"
       options: "--role=someone"
+mariadb_databases:
+    - name: photos
+      hostname: database3.example.org
+      port: 3307
+      username: root
+      password: trustsome1
+      options: "--skip-comments"
 mysql_databases:
 mysql_databases:
     - name: posts
     - name: posts
-      hostname: database3.example.org
+      hostname: database4.example.org
       port: 3307
       port: 3307
       username: root
       username: root
       password: trustsome1
       password: trustsome1
       options: "--skip-comments"
       options: "--skip-comments"
 mongodb_databases:
 mongodb_databases:
     - name: messages
     - name: messages
-      hostname: database4.example.org
+      hostname: database5.example.org
       port: 27018
       port: 27018
       username: dbuser
       username: dbuser
       password: trustsome1
       password: trustsome1
@@ -108,6 +125,8 @@ If you want to dump all databases on a host, use `all` for the database name:
 ```yaml
 ```yaml
 postgresql_databases:
 postgresql_databases:
     - name: all
     - name: all
+mariadb_databases:
+    - name: all
 mysql_databases:
 mysql_databases:
     - name: all
     - name: all
 mongodb_databases:
 mongodb_databases:
@@ -123,15 +142,18 @@ The SQLite hook in particular does not consider "all" a special database name.
 these options in the `hooks:` section of your configuration.
 these options in the `hooks:` section of your configuration.
 
 
 <span class="minilink minilink-addedin">New in version 1.7.6</span> With
 <span class="minilink minilink-addedin">New in version 1.7.6</span> With
-PostgreSQL and MySQL, you can optionally dump "all" databases to separate
-files instead of one combined dump file, allowing more convenient restores of
-individual databases. Enable this by specifying your desired database dump
-`format`:
+PostgreSQL, MariaDB, and MySQL, you can optionally dump "all" databases to
+separate files instead of one combined dump file, allowing more convenient
+restores of individual databases. Enable this by specifying your desired
+database dump `format`:
 
 
 ```yaml
 ```yaml
 postgresql_databases:
 postgresql_databases:
     - name: all
     - name: all
       format: custom
       format: custom
+mariadb_databases:
+    - name: all
+      format: sql
 mysql_databases:
 mysql_databases:
     - name: all
     - name: all
       format: sql
       format: sql
@@ -222,10 +244,16 @@ to prepare for this situation, it's a good idea to include borgmatic's own
 configuration files as part of your regular backups. That way, you can always
 configuration files as part of your regular backups. That way, you can always
 bring back any missing configuration files in order to restore a database.
 bring back any missing configuration files in order to restore a database.
 
 
+<span class="minilink minilink-addedin">New in version 1.7.15</span> borgmatic
+automatically includes configuration files in your backup. See [the
+documentation on the `config bootstrap`
+action](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/#extract-the-configuration-files-used-to-create-an-archive)
+for more information.
+
 
 
 ## Supported databases
 ## Supported databases
 
 
-As of now, borgmatic supports PostgreSQL, MySQL/MariaDB, MongoDB, and SQLite
+As of now, borgmatic supports PostgreSQL, MariaDB, MySQL, MongoDB, and SQLite
 databases directly. But see below about general-purpose preparation and
 databases directly. But see below about general-purpose preparation and
 cleanup hooks as a work-around with other database systems. Also, please [file
 cleanup hooks as a work-around with other database systems. Also, please [file
 a ticket](https://torsion.org/borgmatic/#issues) for additional database
 a ticket](https://torsion.org/borgmatic/#issues) for additional database
@@ -420,9 +448,9 @@ dumps with any database system.
 
 
 ## Troubleshooting
 ## Troubleshooting
 
 
-### PostgreSQL/MySQL authentication errors
+### Authentication errors
 
 
-With PostgreSQL and MySQL/MariaDB, if you're getting authentication errors
+With PostgreSQL, MariaDB, and MySQL, if you're getting authentication errors
 when borgmatic tries to connect to your database, a natural reaction is to
 when borgmatic tries to connect to your database, a natural reaction is to
 increase your borgmatic verbosity with `--verbosity 2` and go looking in the
 increase your borgmatic verbosity with `--verbosity 2` and go looking in the
 logs. You'll notice though that your database password does not show up in the
 logs. You'll notice though that your database password does not show up in the
@@ -436,23 +464,24 @@ authenticated. For instance, with PostgreSQL, check your
 [pg_hba.conf](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html)
 [pg_hba.conf](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html)
 file for that configuration.
 file for that configuration.
 
 
-Additionally, MySQL/MariaDB may be picking up some of your credentials from a
-defaults file like `~/.my.cnf`. If that's the case, then it's possible
-MySQL/MariaDB ends up using, say, a username from borgmatic's configuration
-and a password from `~/.my.cnf`. This may result in authentication errors if
-this combination of credentials is not what you intend.
+Additionally, MariaDB or MySQL may be picking up some of your credentials from
+a defaults file like `~/mariadb.cnf` or `~/.my.cnf`. If that's the case, then
+it's possible MariaDB or MySQL end up using, say, a username from borgmatic's
+configuration and a password from `~/mariadb.cnf` or `~/.my.cnf`. This may
+result in authentication errors if this combination of credentials is not what
+you intend.
 
 
 
 
-### MySQL table lock errors
+### MariaDB or MySQL table lock errors
 
 
-If you encounter table lock errors during a database dump with MySQL/MariaDB,
-you may need to [use a
-transaction](https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html#option_mysqldump_single-transaction).
+If you encounter table lock errors during a database dump with MariaDB or
+MySQL, you may need to [use a
+transaction](https://mariadb.com/docs/skysql-dbaas/ref/mdb/cli/mariadb-dump/single-transaction/).
 You can add any additional flags to the `options:` in your database
 You can add any additional flags to the `options:` in your database
-configuration. Here's an example:
+configuration. Here's an example with MariaDB:
 
 
 ```yaml
 ```yaml
-mysql_databases:
+mariadb_databases:
     - name: posts
     - name: posts
       options: "--single-transaction --quick"
       options: "--single-transaction --quick"
 ```
 ```

+ 1 - 1
setup.py

@@ -1,6 +1,6 @@
 from setuptools import find_packages, setup
 from setuptools import find_packages, setup
 
 
-VERSION = '1.8.1'
+VERSION = '1.8.2.dev0'
 
 
 
 
 setup(
 setup(

+ 10 - 10
tests/end-to-end/docker-compose.yaml

@@ -12,16 +12,16 @@ services:
       POSTGRES_DB: test
       POSTGRES_DB: test
       POSTGRES_USER: postgres2
       POSTGRES_USER: postgres2
     command: docker-entrypoint.sh -p 5433
     command: docker-entrypoint.sh -p 5433
-  mysql:
-    image: docker.io/mariadb:10.5
+  mariadb:
+    image: docker.io/mariadb:10.11.4
     environment:
     environment:
-      MYSQL_ROOT_PASSWORD: test
-      MYSQL_DATABASE: test
-  mysql2:
-    image: docker.io/mariadb:10.5
+      MARIADB_ROOT_PASSWORD: test
+      MARIADB_DATABASE: test
+  mariadb2:
+    image: docker.io/mariadb:10.11.4
     environment:
     environment:
-      MYSQL_ROOT_PASSWORD: test2
-      MYSQL_DATABASE: test
+      MARIADB_ROOT_PASSWORD: test2
+      MARIADB_DATABASE: test
     command: docker-entrypoint.sh --port=3307
     command: docker-entrypoint.sh --port=3307
   mongodb:
   mongodb:
     image: docker.io/mongo:5.0.5
     image: docker.io/mongo:5.0.5
@@ -50,7 +50,7 @@ services:
     depends_on:
     depends_on:
       - postgresql
       - postgresql
       - postgresql2
       - postgresql2
-      - mysql
-      - mysql2
+      - mariadb
+      - mariadb2
       - mongodb
       - mongodb
       - mongodb2
       - mongodb2

+ 28 - 5
tests/end-to-end/test_database.py

@@ -45,18 +45,32 @@ postgresql_databases:
       hostname: postgresql
       hostname: postgresql
       username: postgres
       username: postgres
       password: test
       password: test
+mariadb_databases:
+    - name: test
+      hostname: mariadb
+      username: root
+      password: test
+    - name: all
+      hostname: mariadb
+      username: root
+      password: test
+    - name: all
+      format: sql
+      hostname: mariadb
+      username: root
+      password: test
 mysql_databases:
 mysql_databases:
     - name: test
     - name: test
-      hostname: mysql
+      hostname: mariadb
       username: root
       username: root
       password: test
       password: test
     - name: all
     - name: all
-      hostname: mysql
+      hostname: mariadb
       username: root
       username: root
       password: test
       password: test
     - name: all
     - name: all
       format: sql
       format: sql
-      hostname: mysql
+      hostname: mariadb
       username: root
       username: root
       password: test
       password: test
 mongodb_databases:
 mongodb_databases:
@@ -111,12 +125,21 @@ postgresql_databases:
       restore_port: 5433
       restore_port: 5433
       restore_username: postgres2
       restore_username: postgres2
       restore_password: test2
       restore_password: test2
+mariadb_databases:
+    - name: test
+      hostname: mariadb
+      username: root
+      password: test
+      restore_hostname: mariadb2
+      restore_port: 3307
+      restore_username: root
+      restore_password: test2
 mysql_databases:
 mysql_databases:
     - name: test
     - name: test
-      hostname: mysql
+      hostname: mariadb
       username: root
       username: root
       password: test
       password: test
-      restore_hostname: mysql2
+      restore_hostname: mariadb2
       restore_port: 3307
       restore_port: 3307
       restore_username: root
       restore_username: root
       restore_password: test2
       restore_password: test2

+ 644 - 0
tests/unit/hooks/test_mariadb.py

@@ -0,0 +1,644 @@
+import logging
+
+import pytest
+from flexmock import flexmock
+
+from borgmatic.hooks import mariadb as module
+
+
+def test_database_names_to_dump_passes_through_name():
+    extra_environment = flexmock()
+    log_prefix = ''
+
+    names = module.database_names_to_dump(
+        {'name': 'foo'}, extra_environment, log_prefix, dry_run=False
+    )
+
+    assert names == ('foo',)
+
+
+def test_database_names_to_dump_bails_for_dry_run():
+    extra_environment = flexmock()
+    log_prefix = ''
+    flexmock(module).should_receive('execute_command_and_capture_output').never()
+
+    names = module.database_names_to_dump(
+        {'name': 'all'}, extra_environment, log_prefix, dry_run=True
+    )
+
+    assert names == ()
+
+
+def test_database_names_to_dump_queries_mariadb_for_database_names():
+    extra_environment = flexmock()
+    log_prefix = ''
+    flexmock(module).should_receive('execute_command_and_capture_output').with_args(
+        ('mariadb', '--skip-column-names', '--batch', '--execute', 'show schemas'),
+        extra_environment=extra_environment,
+    ).and_return('foo\nbar\nmysql\n').once()
+
+    names = module.database_names_to_dump(
+        {'name': 'all'}, extra_environment, log_prefix, dry_run=False
+    )
+
+    assert names == ('foo', 'bar')
+
+
+def test_dump_databases_dumps_each_database():
+    databases = [{'name': 'foo'}, {'name': 'bar'}]
+    processes = [flexmock(), flexmock()]
+    flexmock(module).should_receive('make_dump_path').and_return('')
+    flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
+        ('bar',)
+    )
+
+    for name, process in zip(('foo', 'bar'), processes):
+        flexmock(module).should_receive('execute_dump_command').with_args(
+            database={'name': name},
+            log_prefix=object,
+            dump_path=object,
+            database_names=(name,),
+            extra_environment=object,
+            dry_run=object,
+            dry_run_label=object,
+        ).and_return(process).once()
+
+    assert module.dump_databases(databases, {}, 'test.yaml', dry_run=False) == processes
+
+
+def test_dump_databases_dumps_with_password():
+    database = {'name': 'foo', 'username': 'root', 'password': 'trustsome1'}
+    process = flexmock()
+    flexmock(module).should_receive('make_dump_path').and_return('')
+    flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
+        ('bar',)
+    )
+
+    flexmock(module).should_receive('execute_dump_command').with_args(
+        database=database,
+        log_prefix=object,
+        dump_path=object,
+        database_names=('foo',),
+        extra_environment={'MYSQL_PWD': 'trustsome1'},
+        dry_run=object,
+        dry_run_label=object,
+    ).and_return(process).once()
+
+    assert module.dump_databases([database], {}, 'test.yaml', dry_run=False) == [process]
+
+
+def test_dump_databases_dumps_all_databases_at_once():
+    databases = [{'name': 'all'}]
+    process = flexmock()
+    flexmock(module).should_receive('make_dump_path').and_return('')
+    flexmock(module).should_receive('database_names_to_dump').and_return(('foo', 'bar'))
+    flexmock(module).should_receive('execute_dump_command').with_args(
+        database={'name': 'all'},
+        log_prefix=object,
+        dump_path=object,
+        database_names=('foo', 'bar'),
+        extra_environment=object,
+        dry_run=object,
+        dry_run_label=object,
+    ).and_return(process).once()
+
+    assert module.dump_databases(databases, {}, 'test.yaml', dry_run=False) == [process]
+
+
+def test_dump_databases_dumps_all_databases_separately_when_format_configured():
+    databases = [{'name': 'all', 'format': 'sql'}]
+    processes = [flexmock(), flexmock()]
+    flexmock(module).should_receive('make_dump_path').and_return('')
+    flexmock(module).should_receive('database_names_to_dump').and_return(('foo', 'bar'))
+
+    for name, process in zip(('foo', 'bar'), processes):
+        flexmock(module).should_receive('execute_dump_command').with_args(
+            database={'name': name, 'format': 'sql'},
+            log_prefix=object,
+            dump_path=object,
+            database_names=(name,),
+            extra_environment=object,
+            dry_run=object,
+            dry_run_label=object,
+        ).and_return(process).once()
+
+    assert module.dump_databases(databases, {}, 'test.yaml', dry_run=False) == processes
+
+
+def test_database_names_to_dump_runs_mariadb_with_list_options():
+    database = {'name': 'all', 'list_options': '--defaults-extra-file=mariadb.cnf'}
+    flexmock(module).should_receive('execute_command_and_capture_output').with_args(
+        (
+            'mariadb',
+            '--defaults-extra-file=mariadb.cnf',
+            '--skip-column-names',
+            '--batch',
+            '--execute',
+            'show schemas',
+        ),
+        extra_environment=None,
+    ).and_return(('foo\nbar')).once()
+
+    assert module.database_names_to_dump(database, None, 'test.yaml', '') == ('foo', 'bar')
+
+
+def test_execute_dump_command_runs_mariadb_dump():
+    process = flexmock()
+    flexmock(module.dump).should_receive('make_database_dump_filename').and_return('dump')
+    flexmock(module.os.path).should_receive('exists').and_return(False)
+    flexmock(module.dump).should_receive('create_named_pipe_for_dump')
+
+    flexmock(module).should_receive('execute_command').with_args(
+        (
+            'mariadb-dump',
+            '--add-drop-database',
+            '--databases',
+            'foo',
+            '--result-file',
+            'dump',
+        ),
+        extra_environment=None,
+        run_to_completion=False,
+    ).and_return(process).once()
+
+    assert (
+        module.execute_dump_command(
+            database={'name': 'foo'},
+            log_prefix='log',
+            dump_path=flexmock(),
+            database_names=('foo',),
+            extra_environment=None,
+            dry_run=False,
+            dry_run_label='',
+        )
+        == process
+    )
+
+
+def test_execute_dump_command_runs_mariadb_dump_without_add_drop_database():
+    process = flexmock()
+    flexmock(module.dump).should_receive('make_database_dump_filename').and_return('dump')
+    flexmock(module.os.path).should_receive('exists').and_return(False)
+    flexmock(module.dump).should_receive('create_named_pipe_for_dump')
+
+    flexmock(module).should_receive('execute_command').with_args(
+        (
+            'mariadb-dump',
+            '--databases',
+            'foo',
+            '--result-file',
+            'dump',
+        ),
+        extra_environment=None,
+        run_to_completion=False,
+    ).and_return(process).once()
+
+    assert (
+        module.execute_dump_command(
+            database={'name': 'foo', 'add_drop_database': False},
+            log_prefix='log',
+            dump_path=flexmock(),
+            database_names=('foo',),
+            extra_environment=None,
+            dry_run=False,
+            dry_run_label='',
+        )
+        == process
+    )
+
+
+def test_execute_dump_command_runs_mariadb_dump_with_hostname_and_port():
+    process = flexmock()
+    flexmock(module.dump).should_receive('make_database_dump_filename').and_return('dump')
+    flexmock(module.os.path).should_receive('exists').and_return(False)
+    flexmock(module.dump).should_receive('create_named_pipe_for_dump')
+
+    flexmock(module).should_receive('execute_command').with_args(
+        (
+            'mariadb-dump',
+            '--add-drop-database',
+            '--host',
+            'database.example.org',
+            '--port',
+            '5433',
+            '--protocol',
+            'tcp',
+            '--databases',
+            'foo',
+            '--result-file',
+            'dump',
+        ),
+        extra_environment=None,
+        run_to_completion=False,
+    ).and_return(process).once()
+
+    assert (
+        module.execute_dump_command(
+            database={'name': 'foo', 'hostname': 'database.example.org', 'port': 5433},
+            log_prefix='log',
+            dump_path=flexmock(),
+            database_names=('foo',),
+            extra_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_database_dump_filename').and_return('dump')
+    flexmock(module.os.path).should_receive('exists').and_return(False)
+    flexmock(module.dump).should_receive('create_named_pipe_for_dump')
+
+    flexmock(module).should_receive('execute_command').with_args(
+        (
+            'mariadb-dump',
+            '--add-drop-database',
+            '--user',
+            'root',
+            '--databases',
+            'foo',
+            '--result-file',
+            'dump',
+        ),
+        extra_environment={'MYSQL_PWD': 'trustsome1'},
+        run_to_completion=False,
+    ).and_return(process).once()
+
+    assert (
+        module.execute_dump_command(
+            database={'name': 'foo', 'username': 'root', 'password': 'trustsome1'},
+            log_prefix='log',
+            dump_path=flexmock(),
+            database_names=('foo',),
+            extra_environment={'MYSQL_PWD': 'trustsome1'},
+            dry_run=False,
+            dry_run_label='',
+        )
+        == process
+    )
+
+
+def test_execute_dump_command_runs_mariadb_dump_with_options():
+    process = flexmock()
+    flexmock(module.dump).should_receive('make_database_dump_filename').and_return('dump')
+    flexmock(module.os.path).should_receive('exists').and_return(False)
+    flexmock(module.dump).should_receive('create_named_pipe_for_dump')
+
+    flexmock(module).should_receive('execute_command').with_args(
+        (
+            'mariadb-dump',
+            '--stuff=such',
+            '--add-drop-database',
+            '--databases',
+            'foo',
+            '--result-file',
+            'dump',
+        ),
+        extra_environment=None,
+        run_to_completion=False,
+    ).and_return(process).once()
+
+    assert (
+        module.execute_dump_command(
+            database={'name': 'foo', 'options': '--stuff=such'},
+            log_prefix='log',
+            dump_path=flexmock(),
+            database_names=('foo',),
+            extra_environment=None,
+            dry_run=False,
+            dry_run_label='',
+        )
+        == process
+    )
+
+
+def test_execute_dump_command_with_duplicate_dump_skips_mariadb_dump():
+    flexmock(module.dump).should_receive('make_database_dump_filename').and_return('dump')
+    flexmock(module.os.path).should_receive('exists').and_return(True)
+    flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
+    flexmock(module).should_receive('execute_command').never()
+
+    assert (
+        module.execute_dump_command(
+            database={'name': 'foo'},
+            log_prefix='log',
+            dump_path=flexmock(),
+            database_names=('foo',),
+            extra_environment=None,
+            dry_run=True,
+            dry_run_label='SO DRY',
+        )
+        is None
+    )
+
+
+def test_execute_dump_command_with_dry_run_skips_mariadb_dump():
+    flexmock(module.dump).should_receive('make_database_dump_filename').and_return('dump')
+    flexmock(module.os.path).should_receive('exists').and_return(False)
+    flexmock(module.dump).should_receive('create_named_pipe_for_dump')
+
+    flexmock(module).should_receive('execute_command').never()
+
+    assert (
+        module.execute_dump_command(
+            database={'name': 'foo'},
+            log_prefix='log',
+            dump_path=flexmock(),
+            database_names=('foo',),
+            extra_environment=None,
+            dry_run=True,
+            dry_run_label='SO DRY',
+        )
+        is None
+    )
+
+
+def test_dump_databases_errors_for_missing_all_databases():
+    databases = [{'name': 'all'}]
+    flexmock(module).should_receive('make_dump_path').and_return('')
+    flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
+        'databases/localhost/all'
+    )
+    flexmock(module).should_receive('database_names_to_dump').and_return(())
+
+    with pytest.raises(ValueError):
+        assert module.dump_databases(databases, {}, 'test.yaml', dry_run=False)
+
+
+def test_dump_databases_does_not_error_for_missing_all_databases_with_dry_run():
+    databases = [{'name': 'all'}]
+    flexmock(module).should_receive('make_dump_path').and_return('')
+    flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
+        'databases/localhost/all'
+    )
+    flexmock(module).should_receive('database_names_to_dump').and_return(())
+
+    assert module.dump_databases(databases, {}, 'test.yaml', dry_run=True) == []
+
+
+def test_restore_database_dump_runs_mariadb_to_restore():
+    databases_config = [{'name': 'foo'}, {'name': 'bar'}]
+    extract_process = flexmock(stdout=flexmock())
+
+    flexmock(module).should_receive('execute_command_with_processes').with_args(
+        ('mariadb', '--batch'),
+        processes=[extract_process],
+        output_log_level=logging.DEBUG,
+        input_file=extract_process.stdout,
+        extra_environment=None,
+    ).once()
+
+    module.restore_database_dump(
+        databases_config,
+        {},
+        'test.yaml',
+        database_name='foo',
+        dry_run=False,
+        extract_process=extract_process,
+        connection_params={
+            'hostname': None,
+            'port': None,
+            'username': None,
+            'password': None,
+        },
+    )
+
+
+def test_restore_database_dump_errors_when_database_missing_from_configuration():
+    databases_config = [{'name': 'foo'}, {'name': 'bar'}]
+    extract_process = flexmock(stdout=flexmock())
+
+    flexmock(module).should_receive('execute_command_with_processes').never()
+
+    with pytest.raises(ValueError):
+        module.restore_database_dump(
+            databases_config,
+            {},
+            'test.yaml',
+            database_name='other',
+            dry_run=False,
+            extract_process=extract_process,
+            connection_params={
+                'hostname': None,
+                'port': None,
+                'username': None,
+                'password': None,
+            },
+        )
+
+
+def test_restore_database_dump_runs_mariadb_with_options():
+    databases_config = [{'name': 'foo', 'restore_options': '--harder'}]
+    extract_process = flexmock(stdout=flexmock())
+
+    flexmock(module).should_receive('execute_command_with_processes').with_args(
+        ('mariadb', '--batch', '--harder'),
+        processes=[extract_process],
+        output_log_level=logging.DEBUG,
+        input_file=extract_process.stdout,
+        extra_environment=None,
+    ).once()
+
+    module.restore_database_dump(
+        databases_config,
+        {},
+        'test.yaml',
+        database_name='foo',
+        dry_run=False,
+        extract_process=extract_process,
+        connection_params={
+            'hostname': None,
+            'port': None,
+            'username': None,
+            'password': None,
+        },
+    )
+
+
+def test_restore_database_dump_runs_mariadb_with_hostname_and_port():
+    databases_config = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
+    extract_process = flexmock(stdout=flexmock())
+
+    flexmock(module).should_receive('execute_command_with_processes').with_args(
+        (
+            'mariadb',
+            '--batch',
+            '--host',
+            'database.example.org',
+            '--port',
+            '5433',
+            '--protocol',
+            'tcp',
+        ),
+        processes=[extract_process],
+        output_log_level=logging.DEBUG,
+        input_file=extract_process.stdout,
+        extra_environment=None,
+    ).once()
+
+    module.restore_database_dump(
+        databases_config,
+        {},
+        'test.yaml',
+        database_name='foo',
+        dry_run=False,
+        extract_process=extract_process,
+        connection_params={
+            'hostname': None,
+            'port': None,
+            'username': None,
+            'password': None,
+        },
+    )
+
+
+def test_restore_database_dump_runs_mariadb_with_username_and_password():
+    databases_config = [{'name': 'foo', 'username': 'root', 'password': 'trustsome1'}]
+    extract_process = flexmock(stdout=flexmock())
+
+    flexmock(module).should_receive('execute_command_with_processes').with_args(
+        ('mariadb', '--batch', '--user', 'root'),
+        processes=[extract_process],
+        output_log_level=logging.DEBUG,
+        input_file=extract_process.stdout,
+        extra_environment={'MYSQL_PWD': 'trustsome1'},
+    ).once()
+
+    module.restore_database_dump(
+        databases_config,
+        {},
+        'test.yaml',
+        database_name='foo',
+        dry_run=False,
+        extract_process=extract_process,
+        connection_params={
+            'hostname': None,
+            'port': None,
+            'username': None,
+            'password': None,
+        },
+    )
+
+
+def test_restore_database_dump_with_connection_params_uses_connection_params_for_restore():
+    databases_config = [
+        {
+            'name': 'foo',
+            'username': 'root',
+            'password': 'trustsome1',
+            'restore_hostname': 'restorehost',
+            'restore_port': 'restoreport',
+            'restore_username': 'restoreusername',
+            'restore_password': 'restorepassword',
+        }
+    ]
+    extract_process = flexmock(stdout=flexmock())
+
+    flexmock(module).should_receive('execute_command_with_processes').with_args(
+        (
+            'mariadb',
+            '--batch',
+            '--host',
+            'clihost',
+            '--port',
+            'cliport',
+            '--protocol',
+            'tcp',
+            '--user',
+            'cliusername',
+        ),
+        processes=[extract_process],
+        output_log_level=logging.DEBUG,
+        input_file=extract_process.stdout,
+        extra_environment={'MYSQL_PWD': 'clipassword'},
+    ).once()
+
+    module.restore_database_dump(
+        databases_config,
+        {},
+        'test.yaml',
+        database_name='foo',
+        dry_run=False,
+        extract_process=extract_process,
+        connection_params={
+            'hostname': 'clihost',
+            'port': 'cliport',
+            'username': 'cliusername',
+            'password': 'clipassword',
+        },
+    )
+
+
+def test_restore_database_dump_without_connection_params_uses_restore_params_in_config_for_restore():
+    databases_config = [
+        {
+            'name': 'foo',
+            'username': 'root',
+            'password': 'trustsome1',
+            'hostname': 'dbhost',
+            'port': 'dbport',
+            'restore_username': 'restoreuser',
+            'restore_password': 'restorepass',
+            'restore_hostname': 'restorehost',
+            'restore_port': 'restoreport',
+        }
+    ]
+    extract_process = flexmock(stdout=flexmock())
+
+    flexmock(module).should_receive('execute_command_with_processes').with_args(
+        (
+            'mariadb',
+            '--batch',
+            '--host',
+            'restorehost',
+            '--port',
+            'restoreport',
+            '--protocol',
+            'tcp',
+            '--user',
+            'restoreuser',
+        ),
+        processes=[extract_process],
+        output_log_level=logging.DEBUG,
+        input_file=extract_process.stdout,
+        extra_environment={'MYSQL_PWD': 'restorepass'},
+    ).once()
+
+    module.restore_database_dump(
+        databases_config,
+        {},
+        'test.yaml',
+        database_name='foo',
+        dry_run=False,
+        extract_process=extract_process,
+        connection_params={
+            'hostname': None,
+            'port': None,
+            'username': None,
+            'password': None,
+        },
+    )
+
+
+def test_restore_database_dump_with_dry_run_skips_restore():
+    databases_config = [{'name': 'foo'}]
+
+    flexmock(module).should_receive('execute_command_with_processes').never()
+
+    module.restore_database_dump(
+        databases_config,
+        {},
+        'test.yaml',
+        database_name='foo',
+        dry_run=True,
+        extract_process=flexmock(),
+        connection_params={
+            'hostname': None,
+            'port': None,
+            'username': None,
+            'password': None,
+        },
+    )

+ 8 - 8
tests/unit/hooks/test_mongodb.py

@@ -17,7 +17,7 @@ def test_dump_databases_runs_mongodump_for_each_database():
 
 
     for name, process in zip(('foo', 'bar'), processes):
     for name, process in zip(('foo', 'bar'), processes):
         flexmock(module).should_receive('execute_command').with_args(
         flexmock(module).should_receive('execute_command').with_args(
-            ['mongodump', '--db', name, '--archive', '>', f'databases/localhost/{name}'],
+            ('mongodump', '--db', name, '--archive', '>', f'databases/localhost/{name}'),
             shell=True,
             shell=True,
             run_to_completion=False,
             run_to_completion=False,
         ).and_return(process).once()
         ).and_return(process).once()
@@ -47,7 +47,7 @@ def test_dump_databases_runs_mongodump_with_hostname_and_port():
     flexmock(module.dump).should_receive('create_named_pipe_for_dump')
     flexmock(module.dump).should_receive('create_named_pipe_for_dump')
 
 
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
-        [
+        (
             'mongodump',
             'mongodump',
             '--host',
             '--host',
             'database.example.org',
             'database.example.org',
@@ -58,7 +58,7 @@ def test_dump_databases_runs_mongodump_with_hostname_and_port():
             '--archive',
             '--archive',
             '>',
             '>',
             'databases/database.example.org/foo',
             'databases/database.example.org/foo',
-        ],
+        ),
         shell=True,
         shell=True,
         run_to_completion=False,
         run_to_completion=False,
     ).and_return(process).once()
     ).and_return(process).once()
@@ -83,7 +83,7 @@ def test_dump_databases_runs_mongodump_with_username_and_password():
     flexmock(module.dump).should_receive('create_named_pipe_for_dump')
     flexmock(module.dump).should_receive('create_named_pipe_for_dump')
 
 
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
-        [
+        (
             'mongodump',
             'mongodump',
             '--username',
             '--username',
             'mongo',
             'mongo',
@@ -96,7 +96,7 @@ def test_dump_databases_runs_mongodump_with_username_and_password():
             '--archive',
             '--archive',
             '>',
             '>',
             'databases/localhost/foo',
             'databases/localhost/foo',
-        ],
+        ),
         shell=True,
         shell=True,
         run_to_completion=False,
         run_to_completion=False,
     ).and_return(process).once()
     ).and_return(process).once()
@@ -114,7 +114,7 @@ def test_dump_databases_runs_mongodump_with_directory_format():
     flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
     flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
 
 
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
-        ['mongodump', '--out', 'databases/localhost/foo', '--db', 'foo'],
+        ('mongodump', '--out', 'databases/localhost/foo', '--db', 'foo'),
         shell=True,
         shell=True,
     ).and_return(flexmock()).once()
     ).and_return(flexmock()).once()
 
 
@@ -131,7 +131,7 @@ def test_dump_databases_runs_mongodump_with_options():
     flexmock(module.dump).should_receive('create_named_pipe_for_dump')
     flexmock(module.dump).should_receive('create_named_pipe_for_dump')
 
 
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
-        ['mongodump', '--db', 'foo', '--stuff=such', '--archive', '>', 'databases/localhost/foo'],
+        ('mongodump', '--db', 'foo', '--stuff=such', '--archive', '>', 'databases/localhost/foo'),
         shell=True,
         shell=True,
         run_to_completion=False,
         run_to_completion=False,
     ).and_return(process).once()
     ).and_return(process).once()
@@ -149,7 +149,7 @@ def test_dump_databases_runs_mongodumpall_for_all_databases():
     flexmock(module.dump).should_receive('create_named_pipe_for_dump')
     flexmock(module.dump).should_receive('create_named_pipe_for_dump')
 
 
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
-        ['mongodump', '--archive', '>', 'databases/localhost/all'],
+        ('mongodump', '--archive', '>', 'databases/localhost/all'),
         shell=True,
         shell=True,
         run_to_completion=False,
         run_to_completion=False,
     ).and_return(process).once()
     ).and_return(process).once()