mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-22 10:13:00 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdbfe1c1e9 | ||
|
|
4c8ba5c5ab | ||
|
|
091e9fa9ee | ||
|
|
0f4e93c9c8 | ||
|
|
43bd42fb31 | ||
|
|
bb54068152 | ||
|
|
6c5eecac20 | ||
|
|
3f92b0b6ed | ||
|
|
30d2108399 | ||
|
|
d36e5f24ce | ||
|
|
76c50555d3 | ||
|
|
1cde65b4eb | ||
|
|
4e05539825 | ||
|
|
d17aa3e7cc | ||
|
|
3eff794f44 | ||
|
|
1ac51e34bc | ||
|
|
345b4cfb09 | ||
|
|
27a2bbc231 | ||
|
|
dbf1d0946a | ||
|
|
2078527539 | ||
|
|
620bf52e01 | ||
|
|
fdb08b6c56 |
@@ -0,0 +1,8 @@
|
||||
## Hold up!
|
||||
|
||||
Thanks for your contribution. Unfortunately, we don't use GitHub pull requests to manage code contributions to this repository (and GitHub doesn't have any way to disable pull requests entirely). Instead, please see:
|
||||
|
||||
https://torsion.org/borgmatic/#contributing
|
||||
|
||||
... which provides full instructions on how to submit pull requests. You can even use your GitHub account to login.
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
2.0.5
|
||||
* #1033: Add a "password_transport" option to the MariaDB and MySQL database hooks for customizing
|
||||
how borgmatic transmits passwords to the database client.
|
||||
* #1078: Add "keep_3monthly" and "keep_13weekly" options for customizing "prune" action archive
|
||||
retention.
|
||||
* #1078: Add a "use_chunks_archive" option for controlling whether Borg uses its chunks cache
|
||||
directory.
|
||||
* #1078: For the "compact" action, pass "--dry-run" through to Borg.
|
||||
* #1085: Fix a regression in which the default monitoring verbosity is 0 (warnings only) instead of
|
||||
1 (info about steps borgmatic is taking). This prevented logs from showing up in monitoring
|
||||
services like Healthchecks unless you had an explicit monitoring verbosity set.
|
||||
* Move Mastodon social hosting from Fosstodon to FLOSS.social: https://floss.social/@borgmatic
|
||||
* The borgmatic project no longer accepts pull requests on GitHub. But see
|
||||
https://torsion.org/borgmatic/#contributing for how you can still submit pull requests. You can
|
||||
even use your GitHub account to login.
|
||||
|
||||
2.0.4
|
||||
* #1072: Fix path rewriting for non-root patterns in the ZFS, Btrfs, and LVM hooks.
|
||||
* #1073: Clarify the documentation about when an "after: error" command hook runs and how it
|
||||
@@ -62,7 +78,7 @@
|
||||
* #837: Add custom command options for the MongoDB hook.
|
||||
* #1010: When using Borg 2, don't pass the "--stats" flag to "borg prune".
|
||||
* #1020: Document a database use case involving a temporary database client container:
|
||||
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#containers
|
||||
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#database-containers
|
||||
* #1037: Fix an error with the "extract" action when both a remote repository and a
|
||||
"working_directory" are used.
|
||||
* #1044: Fix an error in the systemd credential hook when the credential name contains a "."
|
||||
@@ -312,7 +328,7 @@
|
||||
paths when a "working_directory" is set.
|
||||
* #906: Add documentation details for how to run custom database dump commands using binaries from
|
||||
running containers:
|
||||
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#containers
|
||||
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#database-containers
|
||||
* Fix a regression in which the "color" option had no effect.
|
||||
* Add a recent contributors section to the documentation, because credit where credit's due! See:
|
||||
https://torsion.org/borgmatic/#recent-contributors
|
||||
@@ -368,7 +384,7 @@
|
||||
* Fix handling of the NO_COLOR environment variable to ignore an empty value.
|
||||
* Add documentation about backing up containerized databases by configuring borgmatic to exec into
|
||||
a container to run a dump command:
|
||||
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#containers
|
||||
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#database-containers
|
||||
|
||||
1.8.9
|
||||
* #311: Add custom dump/restore command options for MySQL and MariaDB.
|
||||
@@ -638,7 +654,7 @@
|
||||
at the command-line. See the configuration reference for more information:
|
||||
https://torsion.org/borgmatic/docs/reference/configuration/
|
||||
* #649: Add documentation on backing up a database running in a container:
|
||||
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#containers
|
||||
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#database-containers
|
||||
* #655: Fix error when databases are configured and a source directory doesn't exist.
|
||||
* Add code style plugins to enforce use of Python f-strings and prevent single-letter variables.
|
||||
To join in the pedantry, refresh your test environment with "tox --recreate".
|
||||
|
||||
@@ -138,7 +138,8 @@ issues.
|
||||
|
||||
### Social
|
||||
|
||||
Follow [borgmatic on Mastodon](https://fosstodon.org/@borgmatic).
|
||||
Follow <a rel="me" href="https://floss.social/@borgmatic">borgmatic on
|
||||
Mastodon</a>.
|
||||
|
||||
|
||||
### Chat
|
||||
@@ -159,21 +160,21 @@ Other questions or comments? Contact
|
||||
### Contributing
|
||||
|
||||
borgmatic [source code is
|
||||
available](https://projects.torsion.org/borgmatic-collective/borgmatic) and is also mirrored
|
||||
on [GitHub](https://github.com/borgmatic-collective/borgmatic) for convenience.
|
||||
available](https://projects.torsion.org/borgmatic-collective/borgmatic) and also
|
||||
has a read-only mirror on
|
||||
[GitHub](https://github.com/borgmatic-collective/borgmatic) for convenience.
|
||||
|
||||
borgmatic is licensed under the GNU General Public License version 3 or any
|
||||
later version.
|
||||
|
||||
If you'd like to contribute to borgmatic development, please feel free to
|
||||
submit a [Pull
|
||||
Request](https://projects.torsion.org/borgmatic-collective/borgmatic/pulls) or
|
||||
If you'd like to contribute to borgmatic development, please feel free to submit
|
||||
a [pull
|
||||
request](https://projects.torsion.org/borgmatic-collective/borgmatic/pulls) or
|
||||
open an
|
||||
[issue](https://projects.torsion.org/borgmatic-collective/borgmatic/issues) to
|
||||
discuss your idea. Note that you'll need to
|
||||
[register](https://projects.torsion.org/user/sign_up?invite_code=borgmatic)
|
||||
first. We also accept Pull Requests on GitHub, if that's more your thing. In
|
||||
general, contributions are very welcome. We don't bite!
|
||||
first. In general, contributions are very welcome. We don't bite!
|
||||
|
||||
Also, please check out the [borgmatic development
|
||||
how-to](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/) for
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
|
||||
import borgmatic.config.paths
|
||||
from borgmatic.borg import environment, flags
|
||||
from borgmatic.borg import environment, feature, flags
|
||||
from borgmatic.execute import execute_command
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -37,11 +37,16 @@ def compact_segments(
|
||||
+ (('--threshold', str(threshold)) if threshold else ())
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (
|
||||
('--dry-run',)
|
||||
if dry_run and feature.available(feature.Feature.DRY_RUN_COMPACT, local_borg_version)
|
||||
else ()
|
||||
)
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
if dry_run and not feature.available(feature.Feature.DRY_RUN_COMPACT, local_borg_version):
|
||||
logging.info('Skipping compact (dry run)')
|
||||
return
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ OPTION_TO_ENVIRONMENT_VARIABLE = {
|
||||
DEFAULT_BOOL_OPTION_TO_DOWNCASE_ENVIRONMENT_VARIABLE = {
|
||||
'relocated_repo_access_is_ok': 'BORG_RELOCATED_REPO_ACCESS_IS_OK',
|
||||
'unknown_unencrypted_repo_access_is_ok': 'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK',
|
||||
'use_chunks_archive': 'BORG_USE_CHUNKS_ARCHIVE',
|
||||
}
|
||||
|
||||
DEFAULT_BOOL_OPTION_TO_UPPERCASE_ENVIRONMENT_VARIABLE = {
|
||||
|
||||
@@ -18,6 +18,7 @@ class Feature(Enum):
|
||||
EXCLUDED_FILES_MINUS = 12
|
||||
ARCHIVE_SERIES = 13
|
||||
NO_PRUNE_STATS = 14
|
||||
DRY_RUN_COMPACT = 15
|
||||
|
||||
|
||||
FEATURE_TO_MINIMUM_BORG_VERSION = {
|
||||
@@ -35,6 +36,7 @@ FEATURE_TO_MINIMUM_BORG_VERSION = {
|
||||
Feature.EXCLUDED_FILES_MINUS: parse('2.0.0b5'), # --list --filter uses "-" for excludes
|
||||
Feature.ARCHIVE_SERIES: parse('2.0.0b11'), # identically named archives form a series
|
||||
Feature.NO_PRUNE_STATS: parse('2.0.0b10'), # prune --stats is not available
|
||||
Feature.DRY_RUN_COMPACT: parse('1.4.1'), # borg compact --dry-run support
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -95,7 +95,9 @@ class Monitoring_hooks:
|
||||
self.config_filename = config_filename
|
||||
self.config = config
|
||||
self.dry_run = global_arguments.dry_run
|
||||
self.monitoring_log_level = verbosity_to_log_level(config.get('monitoring_verbosity'))
|
||||
self.monitoring_log_level = verbosity_to_log_level(
|
||||
get_verbosity({config_filename: config}, 'monitoring_verbosity')
|
||||
)
|
||||
self.monitoring_hooks_are_activated = (
|
||||
using_primary_action and self.monitoring_log_level != DISABLED
|
||||
)
|
||||
|
||||
@@ -389,6 +389,13 @@ properties:
|
||||
Path for Borg cache files. Defaults to
|
||||
$borg_base_directory/.cache/borg
|
||||
example: /path/to/base/cache
|
||||
use_chunks_archive:
|
||||
type: boolean
|
||||
description: |
|
||||
Enables or disables the use of chunks.archive.d for faster cache
|
||||
resyncs in Borg. If true, value is set to "yes" (default) else
|
||||
it's set to "no", reducing disk usage but slowing resyncs.
|
||||
example: true
|
||||
borg_files_cache_ttl:
|
||||
type: integer
|
||||
description: |
|
||||
@@ -558,6 +565,14 @@ properties:
|
||||
type: integer
|
||||
description: Number of yearly archives to keep.
|
||||
example: 1
|
||||
keep_13weekly:
|
||||
type: integer
|
||||
description: Number of quarterly archives to keep (13 week strategy).
|
||||
example: 13
|
||||
keep_3monthly:
|
||||
type: integer
|
||||
description: Number of quarterly archives to keep (3 month strategy).
|
||||
example: 3
|
||||
prefix:
|
||||
type: string
|
||||
description: |
|
||||
@@ -1545,6 +1560,23 @@ properties:
|
||||
Defaults to the "password" option. Supports the
|
||||
"{credential ...}" syntax.
|
||||
example: trustsome1
|
||||
password_transport:
|
||||
type: string
|
||||
enum:
|
||||
- pipe
|
||||
- environment
|
||||
description: |
|
||||
How to transmit database passwords from borgmatic to the
|
||||
MariaDB client, one of:
|
||||
* "pipe": Securely transmit passwords via anonymous
|
||||
pipe. Only works if the database client is on the
|
||||
same host as borgmatic. (The server can be
|
||||
somewhere else.) This is the default value.
|
||||
* "environment": Transmit passwords via environment
|
||||
variable. Potentially less secure than a pipe, but
|
||||
necessary when the database client is elsewhere, e.g.
|
||||
when "mariadb_dump_command" is configured to "exec"
|
||||
into a container and run a client there.
|
||||
tls:
|
||||
type: boolean
|
||||
description: |
|
||||
@@ -1693,6 +1725,23 @@ properties:
|
||||
Defaults to the "password" option. Supports the
|
||||
"{credential ...}" syntax.
|
||||
example: trustsome1
|
||||
password_transport:
|
||||
type: string
|
||||
enum:
|
||||
- pipe
|
||||
- environment
|
||||
description: |
|
||||
How to transmit database passwords from borgmatic to the
|
||||
MySQL client, one of:
|
||||
* "pipe": Securely transmit passwords via anonymous
|
||||
pipe. Only works if the database client is on the
|
||||
same host as borgmatic. (The server can be
|
||||
somewhere else.) This is the default value.
|
||||
* "environment": Transmit passwords via environment
|
||||
variable. Potentially less secure than a pipe, but
|
||||
necessary when the database client is elsewhere, e.g.
|
||||
when "mysql_dump_command" is configured to "exec"
|
||||
into a container and run a client there.
|
||||
tls:
|
||||
type: boolean
|
||||
description: |
|
||||
|
||||
@@ -19,6 +19,7 @@ BORG_PLACEHOLDERS = {
|
||||
'{reverse-fqdn}',
|
||||
'{now}',
|
||||
'{utcnow}',
|
||||
'{unixtime}',
|
||||
'{user}',
|
||||
'{pid}',
|
||||
'{borgversion}',
|
||||
|
||||
@@ -120,13 +120,19 @@ def database_names_to_dump(database, config, username, password, environment, dr
|
||||
shlex.quote(part) for part in shlex.split(database.get('mariadb_command') or 'mariadb')
|
||||
)
|
||||
extra_options, defaults_extra_filename = parse_extra_options(database.get('list_options'))
|
||||
password_transport = database.get('password_transport', 'pipe')
|
||||
show_command = (
|
||||
mariadb_show_command
|
||||
+ make_defaults_file_options(username, password, defaults_extra_filename)
|
||||
+ (
|
||||
make_defaults_file_options(username, password, defaults_extra_filename)
|
||||
if password_transport == 'pipe'
|
||||
else ()
|
||||
)
|
||||
+ extra_options
|
||||
+ (('--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', 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 ())
|
||||
+ ('--skip-column-names', '--batch')
|
||||
@@ -184,14 +190,20 @@ def execute_dump_command(
|
||||
for part in shlex.split(database.get('mariadb_dump_command') or 'mariadb-dump')
|
||||
)
|
||||
extra_options, defaults_extra_filename = parse_extra_options(database.get('options'))
|
||||
password_transport = database.get('password_transport', 'pipe')
|
||||
dump_command = (
|
||||
mariadb_dump_command
|
||||
+ make_defaults_file_options(username, password, defaults_extra_filename)
|
||||
+ (
|
||||
make_defaults_file_options(username, password, defaults_extra_filename)
|
||||
if password_transport == 'pipe'
|
||||
else ()
|
||||
)
|
||||
+ extra_options
|
||||
+ (('--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', username) if username and password_transport == 'environment' else ())
|
||||
+ (('--ssl',) if database.get('tls') is True else ())
|
||||
+ (('--skip-ssl',) if database.get('tls') is False else ())
|
||||
+ ('--databases',)
|
||||
@@ -255,7 +267,14 @@ def dump_data_sources(
|
||||
password = borgmatic.hooks.credential.parse.resolve_credential(
|
||||
database.get('password'), config
|
||||
)
|
||||
environment = dict(os.environ)
|
||||
environment = dict(
|
||||
os.environ,
|
||||
**(
|
||||
{'MYSQL_PWD': password}
|
||||
if password and database.get('password_transport') == 'environment'
|
||||
else {}
|
||||
),
|
||||
)
|
||||
dump_database_names = database_names_to_dump(
|
||||
database, config, username, password, environment, dry_run
|
||||
)
|
||||
@@ -383,18 +402,27 @@ def restore_data_source_dump(
|
||||
shlex.quote(part) for part in shlex.split(data_source.get('mariadb_command') or 'mariadb')
|
||||
)
|
||||
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
|
||||
+ make_defaults_file_options(username, password, defaults_extra_filename)
|
||||
+ (
|
||||
make_defaults_file_options(username, password, defaults_extra_filename)
|
||||
if password_transport == 'pipe'
|
||||
else ()
|
||||
)
|
||||
+ extra_options
|
||||
+ ('--batch',)
|
||||
+ (('--host', hostname) if hostname else ())
|
||||
+ (('--port', str(port)) if port else ())
|
||||
+ (('--protocol', 'tcp') if hostname or port else ())
|
||||
+ (('--user', username) if username and password_transport == 'environment' else ())
|
||||
+ (('--ssl',) if tls is True else ())
|
||||
+ (('--skip-ssl',) if tls is False else ())
|
||||
)
|
||||
environment = dict(os.environ)
|
||||
environment = dict(
|
||||
os.environ,
|
||||
**({'MYSQL_PWD': password} if password and password_transport == 'environment' else {}),
|
||||
)
|
||||
|
||||
logger.debug(f"Restoring MariaDB database {data_source['name']}{dry_run_label}")
|
||||
if dry_run:
|
||||
|
||||
@@ -45,15 +45,21 @@ def database_names_to_dump(database, config, username, password, environment, dr
|
||||
extra_options, defaults_extra_filename = (
|
||||
borgmatic.hooks.data_source.mariadb.parse_extra_options(database.get('list_options'))
|
||||
)
|
||||
password_transport = database.get('password_transport', 'pipe')
|
||||
show_command = (
|
||||
mysql_show_command
|
||||
+ borgmatic.hooks.data_source.mariadb.make_defaults_file_options(
|
||||
username, password, defaults_extra_filename
|
||||
+ (
|
||||
borgmatic.hooks.data_source.mariadb.make_defaults_file_options(
|
||||
username, password, defaults_extra_filename
|
||||
)
|
||||
if password_transport == 'pipe'
|
||||
else ()
|
||||
)
|
||||
+ extra_options
|
||||
+ (('--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', 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 ())
|
||||
+ ('--skip-column-names', '--batch')
|
||||
@@ -109,16 +115,22 @@ def execute_dump_command(
|
||||
extra_options, defaults_extra_filename = (
|
||||
borgmatic.hooks.data_source.mariadb.parse_extra_options(database.get('options'))
|
||||
)
|
||||
password_transport = database.get('password_transport', 'pipe')
|
||||
dump_command = (
|
||||
mysql_dump_command
|
||||
+ borgmatic.hooks.data_source.mariadb.make_defaults_file_options(
|
||||
username, password, defaults_extra_filename
|
||||
+ (
|
||||
borgmatic.hooks.data_source.mariadb.make_defaults_file_options(
|
||||
username, password, defaults_extra_filename
|
||||
)
|
||||
if password_transport == 'pipe'
|
||||
else ()
|
||||
)
|
||||
+ extra_options
|
||||
+ (('--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', username) if username and password_transport == 'environment' else ())
|
||||
+ (('--ssl',) if database.get('tls') is True else ())
|
||||
+ (('--skip-ssl',) if database.get('tls') is False else ())
|
||||
+ ('--databases',)
|
||||
@@ -182,7 +194,14 @@ def dump_data_sources(
|
||||
password = borgmatic.hooks.credential.parse.resolve_credential(
|
||||
database.get('password'), config
|
||||
)
|
||||
environment = dict(os.environ)
|
||||
environment = dict(
|
||||
os.environ,
|
||||
**(
|
||||
{'MYSQL_PWD': password}
|
||||
if password and database.get('password_transport') == 'environment'
|
||||
else {}
|
||||
),
|
||||
)
|
||||
dump_database_names = database_names_to_dump(
|
||||
database, config, username, password, environment, dry_run
|
||||
)
|
||||
@@ -312,20 +331,29 @@ def restore_data_source_dump(
|
||||
extra_options, defaults_extra_filename = (
|
||||
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
|
||||
+ borgmatic.hooks.data_source.mariadb.make_defaults_file_options(
|
||||
username, password, defaults_extra_filename
|
||||
+ (
|
||||
borgmatic.hooks.data_source.mariadb.make_defaults_file_options(
|
||||
username, password, defaults_extra_filename
|
||||
)
|
||||
if password_transport == 'pipe'
|
||||
else ()
|
||||
)
|
||||
+ extra_options
|
||||
+ ('--batch',)
|
||||
+ (('--host', hostname) if hostname else ())
|
||||
+ (('--port', str(port)) if port else ())
|
||||
+ (('--protocol', 'tcp') if hostname or port else ())
|
||||
+ (('--user', username) if username and password_transport == 'environment' else ())
|
||||
+ (('--ssl',) if tls is True else ())
|
||||
+ (('--skip-ssl',) if tls is False else ())
|
||||
)
|
||||
environment = dict(os.environ)
|
||||
environment = dict(
|
||||
os.environ,
|
||||
**({'MYSQL_PWD': password} if password and password_transport == 'environment' else {}),
|
||||
)
|
||||
|
||||
logger.debug(f"Restoring MySQL database {data_source['name']}{dry_run_label}")
|
||||
if dry_run:
|
||||
|
||||
@@ -191,7 +191,7 @@ mysql_databases:
|
||||
format: sql
|
||||
```
|
||||
|
||||
### Containers
|
||||
### Database containers
|
||||
|
||||
If your database server is running within a container and borgmatic is too, no
|
||||
problem—configure borgmatic to connect to the container's name on its exposed
|
||||
@@ -209,6 +209,9 @@ postgresql_databases:
|
||||
<span class="minilink minilink-addedin">Prior to version 1.8.0</span> Put
|
||||
these options in the `hooks:` section of your configuration.
|
||||
|
||||
|
||||
#### Database client on the host
|
||||
|
||||
But what if borgmatic is running on the host? You can still connect to a
|
||||
database server container if its ports are properly exposed to the host. For
|
||||
instance, when running the database container, you can specify `--publish
|
||||
@@ -228,17 +231,19 @@ And then you can configure borgmatic running on the host to connect to the
|
||||
database:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
hostname: 127.0.0.1
|
||||
port: 5433
|
||||
username: postgres
|
||||
password: trustsome1
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
hostname: 127.0.0.1
|
||||
port: 5433
|
||||
username: postgres
|
||||
password: trustsome1
|
||||
```
|
||||
|
||||
Alter the ports in these examples to suit your particular database system.
|
||||
|
||||
|
||||
#### Database client in a running container
|
||||
|
||||
Normally, borgmatic dumps a database by running a database dump command (e.g.
|
||||
`pg_dump`) on the host or wherever borgmatic is running, and this command
|
||||
connects to your containerized database via the given `hostname` and `port`. But
|
||||
@@ -250,14 +255,13 @@ to run the dump command.
|
||||
For instance, if using Docker and PostgreSQL, something like this might work:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
hostname: 127.0.0.1
|
||||
port: 5433
|
||||
username: postgres
|
||||
password: trustsome1
|
||||
pg_dump_command: docker exec my_pg_container pg_dump
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
hostname: 127.0.0.1
|
||||
port: 5433
|
||||
username: postgres
|
||||
password: trustsome1
|
||||
pg_dump_command: docker exec my_pg_container pg_dump
|
||||
```
|
||||
|
||||
... where `my_pg_container` is the name of your running database container.
|
||||
@@ -282,19 +286,43 @@ services:
|
||||
- /run/user/1000:/run/user/1000
|
||||
```
|
||||
|
||||
And here's an example of using a MariaDB database client within a running Docker
|
||||
container:
|
||||
|
||||
```yaml
|
||||
mariadb_databases:
|
||||
- name: users
|
||||
hostname: 127.0.0.1
|
||||
username: example
|
||||
password: trustsome1
|
||||
password_transport: environment
|
||||
mariadb_dump_command: docker exec --env MYSQL_PWD my_mariadb_container mariadb-dump
|
||||
```
|
||||
|
||||
The `password_transport: environment` option tells borgmatic to transmit the
|
||||
password via environment variable instead of the default behavior of using an
|
||||
anonymous pipe. The environment variable transport is potentially less secure
|
||||
than the pipe, but it's necessary when the database client is running in a
|
||||
container separate from borgmatic.
|
||||
|
||||
A similar approach can work with MySQL, using `mysql_dump_command` instead of
|
||||
`mariadb_dump_command` to run `mysqldump` in a container.
|
||||
|
||||
|
||||
#### Database client in a temporary container
|
||||
|
||||
Another variation: If you're running borgmatic on the host but want to spin up a
|
||||
temporary `pg_dump` container whenever borgmatic dumps a database, for
|
||||
instance to make use of a `pg_dump` version not present on the host, try
|
||||
something like this:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
hostname: your-database-hostname
|
||||
username: postgres
|
||||
password: trustsome1
|
||||
pg_dump_command: docker run --rm --env PGPASSWORD postgres:17-alpine pg_dump
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
hostname: your-database-hostname
|
||||
username: postgres
|
||||
password: trustsome1
|
||||
pg_dump_command: docker run --rm --env PGPASSWORD postgres:17-alpine pg_dump
|
||||
```
|
||||
|
||||
The `--env PGPASSWORD` is necessary here for borgmatic to provide your database
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "borgmatic"
|
||||
version = "2.0.4"
|
||||
version = "2.0.5"
|
||||
authors = [
|
||||
{ name="Dan Helfman", email="witten@torsion.org" },
|
||||
]
|
||||
|
||||
@@ -68,8 +68,15 @@ def test_compact_segments_with_log_debug_calls_borg_with_debug_flag():
|
||||
)
|
||||
|
||||
|
||||
def test_compact_segments_with_dry_run_skips_borg_call():
|
||||
def test_compact_segments_with_dry_run_skips_borg_call_when_feature_unavailable():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.DRY_RUN_COMPACT, '1.2.3'
|
||||
).and_return(False)
|
||||
flexmock(module.environment).should_receive('make_environment').never()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
flexmock(logging).should_receive('info').with_args('Skipping compact (dry run)').once()
|
||||
|
||||
module.compact_segments(
|
||||
repository_path='repo',
|
||||
@@ -80,6 +87,24 @@ def test_compact_segments_with_dry_run_skips_borg_call():
|
||||
)
|
||||
|
||||
|
||||
def test_compact_segments_with_dry_run_executes_borg_call_when_feature_available():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.feature).should_receive('available').with_args(
|
||||
module.feature.Feature.DRY_RUN_COMPACT, '1.4.1'
|
||||
).and_return(True)
|
||||
flexmock(module.environment).should_receive('make_environment').once()
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').once()
|
||||
flexmock(module).should_receive('execute_command').once()
|
||||
|
||||
module.compact_segments(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.4.1',
|
||||
global_arguments=flexmock(),
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
|
||||
def test_compact_segments_with_local_path_calls_borg_via_local_path():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
insert_execute_command_mock(('borg1',) + COMPACT_COMMAND[1:] + ('repo',), logging.INFO)
|
||||
|
||||
@@ -92,6 +92,7 @@ def test_make_environment_without_configuration_sets_certain_environment_variabl
|
||||
'BORG_EXIT_CODES': 'modern',
|
||||
'BORG_RELOCATED_REPO_ACCESS_IS_OK': 'no',
|
||||
'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK': 'no',
|
||||
'BORG_USE_CHUNKS_ARCHIVE': 'no',
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +102,7 @@ def test_make_environment_without_configuration_passes_through_default_environme
|
||||
'USER': 'root',
|
||||
'BORG_RELOCATED_REPO_ACCESS_IS_OK': 'yup',
|
||||
'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK': 'nah',
|
||||
'BORG_USE_CHUNKS_ARCHIVE': 'yup',
|
||||
}
|
||||
)
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
@@ -113,6 +115,7 @@ def test_make_environment_without_configuration_passes_through_default_environme
|
||||
'USER': 'root',
|
||||
'BORG_RELOCATED_REPO_ACCESS_IS_OK': 'yup',
|
||||
'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK': 'nah',
|
||||
'BORG_USE_CHUNKS_ARCHIVE': 'yup',
|
||||
'BORG_EXIT_CODES': 'modern',
|
||||
}
|
||||
|
||||
@@ -170,3 +173,17 @@ def test_make_environment_with_integer_variable_value():
|
||||
environment = module.make_environment({'borg_files_cache_ttl': 40})
|
||||
|
||||
assert environment.get('BORG_FILES_CACHE_TTL') == '40'
|
||||
|
||||
|
||||
def test_make_environment_with_use_chunks_archive_should_set_correct_environment_value():
|
||||
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).and_return(None)
|
||||
flexmock(module.os).should_receive('pipe').never()
|
||||
|
||||
environment = module.make_environment({'use_chunks_archive': True})
|
||||
assert environment.get('BORG_USE_CHUNKS_ARCHIVE') == 'yes'
|
||||
|
||||
environment = module.make_environment({'use_chunks_archive': False})
|
||||
assert environment.get('BORG_USE_CHUNKS_ARCHIVE') == 'no'
|
||||
|
||||
@@ -43,6 +43,28 @@ def test_make_prune_flags_returns_flags_from_config():
|
||||
assert result == BASE_PRUNE_FLAGS
|
||||
|
||||
|
||||
def test_make_prune_flags_with_keep_13weekly_and_keep_3monthly():
|
||||
config = {
|
||||
'keep_13weekly': 4,
|
||||
'keep_3monthly': 5,
|
||||
}
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
|
||||
result = module.make_prune_flags(
|
||||
config, flexmock(match_archives=None), local_borg_version='1.2.3'
|
||||
)
|
||||
|
||||
expected = (
|
||||
'--keep-13weekly',
|
||||
'4',
|
||||
'--keep-3monthly',
|
||||
'5',
|
||||
)
|
||||
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_make_prune_flags_accepts_prefix_with_placeholders():
|
||||
config = {
|
||||
'keep_daily': 1,
|
||||
|
||||
@@ -28,6 +28,7 @@ def test_get_skip_actions_uses_config_and_arguments(config, arguments, expected_
|
||||
|
||||
|
||||
def test_monitoring_hooks_with_monioring_disabled_bails():
|
||||
flexmock(module).should_receive('get_verbosity').and_return(module.logging.INFO)
|
||||
flexmock(module).should_receive('verbosity_to_log_level').and_return(module.DISABLED)
|
||||
flexmock(module.dispatch).should_receive('call_hooks').never()
|
||||
|
||||
@@ -41,6 +42,7 @@ def test_monitoring_hooks_with_monioring_disabled_bails():
|
||||
|
||||
|
||||
def test_monitoring_hooks_with_non_primary_action_bails():
|
||||
flexmock(module).should_receive('get_verbosity').and_return(module.logging.INFO)
|
||||
flexmock(module).should_receive('verbosity_to_log_level').and_return(flexmock())
|
||||
flexmock(module.dispatch).should_receive('call_hooks').never()
|
||||
|
||||
@@ -54,6 +56,7 @@ def test_monitoring_hooks_with_non_primary_action_bails():
|
||||
|
||||
|
||||
def test_monitoring_hooks_pings_monitors():
|
||||
flexmock(module).should_receive('get_verbosity').and_return(module.logging.INFO)
|
||||
flexmock(module).should_receive('verbosity_to_log_level').and_return(flexmock())
|
||||
flexmock(module.dispatch).should_receive('call_hooks').with_args(
|
||||
'initialize_monitor',
|
||||
@@ -117,6 +120,7 @@ def test_monitoring_hooks_pings_monitors():
|
||||
|
||||
|
||||
def test_monitoring_hooks_with_start_ping_error_raises():
|
||||
flexmock(module).should_receive('get_verbosity').and_return(module.logging.INFO)
|
||||
flexmock(module).should_receive('verbosity_to_log_level').and_return(flexmock())
|
||||
flexmock(module.dispatch).should_receive('call_hooks').with_args(
|
||||
'initialize_monitor',
|
||||
@@ -172,6 +176,7 @@ def test_monitoring_hooks_with_start_ping_error_raises():
|
||||
|
||||
|
||||
def test_monitoring_hooks_with_log_ping_error_raises():
|
||||
flexmock(module).should_receive('get_verbosity').and_return(module.logging.INFO)
|
||||
flexmock(module).should_receive('verbosity_to_log_level').and_return(flexmock())
|
||||
flexmock(module.dispatch).should_receive('call_hooks').with_args(
|
||||
'initialize_monitor',
|
||||
@@ -227,6 +232,7 @@ def test_monitoring_hooks_with_log_ping_error_raises():
|
||||
|
||||
|
||||
def test_monitoring_hooks_with_finish_ping_error_raises():
|
||||
flexmock(module).should_receive('get_verbosity').and_return(module.logging.INFO)
|
||||
flexmock(module).should_receive('verbosity_to_log_level').and_return(flexmock())
|
||||
flexmock(module.dispatch).should_receive('call_hooks').with_args(
|
||||
'initialize_monitor',
|
||||
@@ -281,7 +287,8 @@ def test_monitoring_hooks_with_finish_ping_error_raises():
|
||||
pass
|
||||
|
||||
|
||||
def test_monitoring_with_wrapped_code_error_pings_fail():
|
||||
def test_monitoring_hooks_with_wrapped_code_error_pings_fail():
|
||||
flexmock(module).should_receive('get_verbosity').and_return(module.logging.INFO)
|
||||
flexmock(module).should_receive('verbosity_to_log_level').and_return(flexmock())
|
||||
flexmock(module.dispatch).should_receive('call_hooks').with_args(
|
||||
'initialize_monitor',
|
||||
@@ -345,7 +352,8 @@ def test_monitoring_with_wrapped_code_error_pings_fail():
|
||||
raise OSError()
|
||||
|
||||
|
||||
def test_monitoring_with_fail_ping_error_raise_original_error():
|
||||
def test_monitoring_hooks_with_fail_ping_error_raise_original_error():
|
||||
flexmock(module).should_receive('get_verbosity').and_return(module.logging.INFO)
|
||||
flexmock(module).should_receive('verbosity_to_log_level').and_return(flexmock())
|
||||
flexmock(module.dispatch).should_receive('call_hooks').with_args(
|
||||
'initialize_monitor',
|
||||
|
||||
@@ -167,6 +167,38 @@ def test_database_names_to_dump_queries_mariadb_for_database_names():
|
||||
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).should_receive('make_defaults_file_options').never()
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
(
|
||||
'mariadb',
|
||||
'--user',
|
||||
'root',
|
||||
'--skip-column-names',
|
||||
'--batch',
|
||||
'--execute',
|
||||
'show schemas',
|
||||
),
|
||||
environment=environment,
|
||||
).and_return('foo\nbar\nmysql\n').once()
|
||||
|
||||
names = module.database_names_to_dump(
|
||||
{'name': 'all', 'password_transport': 'environment'},
|
||||
{},
|
||||
'root',
|
||||
'trustsome1',
|
||||
environment,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
assert names == ('foo', 'bar')
|
||||
|
||||
|
||||
def test_database_names_to_dump_runs_mariadb_with_tls():
|
||||
environment = flexmock()
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
@@ -247,9 +279,22 @@ def test_dump_data_sources_dumps_each_database():
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
|
||||
('bar',)
|
||||
)
|
||||
flexmock(module).should_receive('database_names_to_dump').with_args(
|
||||
database=databases[0],
|
||||
config={},
|
||||
username=None,
|
||||
password=None,
|
||||
environment={'USER': 'root'},
|
||||
dry_run=False,
|
||||
).and_return(('foo',))
|
||||
flexmock(module).should_receive('database_names_to_dump').with_args(
|
||||
database=databases[1],
|
||||
config={},
|
||||
username=None,
|
||||
password=None,
|
||||
environment={'USER': 'root'},
|
||||
dry_run=False,
|
||||
).and_return(('bar',))
|
||||
|
||||
for name, process in zip(('foo', 'bar'), processes):
|
||||
flexmock(module).should_receive('execute_dump_command').with_args(
|
||||
@@ -285,9 +330,14 @@ def test_dump_data_sources_dumps_with_password():
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
|
||||
('bar',)
|
||||
)
|
||||
flexmock(module).should_receive('database_names_to_dump').with_args(
|
||||
database=database,
|
||||
config={},
|
||||
username='root',
|
||||
password='trustsome1',
|
||||
environment={'USER': 'root'},
|
||||
dry_run=False,
|
||||
).and_return(('foo',))
|
||||
|
||||
flexmock(module).should_receive('execute_dump_command').with_args(
|
||||
database=database,
|
||||
@@ -311,6 +361,50 @@ def test_dump_data_sources_dumps_with_password():
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_dump_data_sources_dumps_with_environment_password_transport_passes_password_environment_variable():
|
||||
database = {
|
||||
'name': 'foo',
|
||||
'username': 'root',
|
||||
'password': 'trustsome1',
|
||||
'password_transport': 'environment',
|
||||
}
|
||||
process = flexmock()
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
|
||||
flexmock(module).should_receive('database_names_to_dump').with_args(
|
||||
database=database,
|
||||
config={},
|
||||
username='root',
|
||||
password='trustsome1',
|
||||
environment={'USER': 'root', 'MYSQL_PWD': 'trustsome1'},
|
||||
dry_run=False,
|
||||
).and_return(('foo',))
|
||||
|
||||
flexmock(module).should_receive('execute_dump_command').with_args(
|
||||
database=database,
|
||||
config={},
|
||||
username='root',
|
||||
password='trustsome1',
|
||||
dump_path=object,
|
||||
database_names=('foo',),
|
||||
environment={'USER': 'root', 'MYSQL_PWD': 'trustsome1'},
|
||||
dry_run=object,
|
||||
dry_run_label=object,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(
|
||||
[database],
|
||||
{},
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
patterns=[],
|
||||
dry_run=False,
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_dump_data_sources_dumps_all_databases_at_once():
|
||||
databases = [{'name': 'all'}]
|
||||
process = flexmock()
|
||||
@@ -378,6 +472,54 @@ def test_dump_data_sources_dumps_all_databases_separately_when_format_configured
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_errors_for_missing_all_databases():
|
||||
databases = [{'name': 'all'}]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/all'
|
||||
)
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
patterns=[],
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
|
||||
databases = [{'name': 'all'}]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/all'
|
||||
)
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
patterns=[],
|
||||
dry_run=True,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
def test_database_names_to_dump_runs_mariadb_with_list_options():
|
||||
database = {'name': 'all', 'list_options': '--defaults-extra-file=mariadb.cnf --skip-ssl'}
|
||||
flexmock(module).should_receive('parse_extra_options').and_return(
|
||||
@@ -479,6 +621,48 @@ def test_execute_dump_command_runs_mariadb_dump():
|
||||
)
|
||||
|
||||
|
||||
def test_execute_dump_command_with_environment_password_transport_skips_defaults_file_and_passes_user_flag():
|
||||
process = flexmock()
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module).should_receive('parse_extra_options').and_return((), None)
|
||||
flexmock(module).should_receive('make_defaults_file_options').never()
|
||||
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',
|
||||
),
|
||||
environment=None,
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert (
|
||||
module.execute_dump_command(
|
||||
database={'name': 'foo', 'password_transport': 'environment'},
|
||||
config={},
|
||||
username='root',
|
||||
password='trustsome1',
|
||||
dump_path=flexmock(),
|
||||
database_names=('foo',),
|
||||
environment=None,
|
||||
dry_run=False,
|
||||
dry_run_label='',
|
||||
)
|
||||
== process
|
||||
)
|
||||
|
||||
|
||||
def test_execute_dump_command_runs_mariadb_dump_without_add_drop_database():
|
||||
process = flexmock()
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
|
||||
@@ -849,54 +1033,6 @@ def test_execute_dump_command_with_dry_run_skips_mariadb_dump():
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_errors_for_missing_all_databases():
|
||||
databases = [{'name': 'all'}]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/all'
|
||||
)
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
patterns=[],
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
|
||||
databases = [{'name': 'all'}]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/all'
|
||||
)
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
patterns=[],
|
||||
dry_run=True,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
def test_restore_data_source_dump_runs_mariadb_to_restore():
|
||||
hook_config = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
@@ -1168,6 +1304,49 @@ def test_restore_data_source_dump_runs_mariadb_with_username_and_password():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_data_source_with_environment_password_transport_skips_defaults_file_and_passes_user_flag():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
'username': 'root',
|
||||
'password': 'trustsome1',
|
||||
'password_transport': 'environment',
|
||||
}
|
||||
]
|
||||
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).should_receive('make_defaults_file_options').never()
|
||||
flexmock(module.os).should_receive('environ').and_return(
|
||||
{'USER': 'root', 'MYSQL_PWD': 'trustsome1'}
|
||||
)
|
||||
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,
|
||||
environment={'USER': 'root', 'MYSQL_PWD': 'trustsome1'},
|
||||
).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_with_connection_params_uses_connection_params_for_restore():
|
||||
hook_config = [
|
||||
{
|
||||
|
||||
@@ -60,6 +60,42 @@ def test_database_names_to_dump_queries_mysql_for_database_names():
|
||||
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.borgmatic.hooks.data_source.mariadb).should_receive(
|
||||
'parse_extra_options'
|
||||
).and_return((), None)
|
||||
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
|
||||
'make_defaults_file_options'
|
||||
).never()
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
(
|
||||
'mysql',
|
||||
'--user',
|
||||
'root',
|
||||
'--skip-column-names',
|
||||
'--batch',
|
||||
'--execute',
|
||||
'show schemas',
|
||||
),
|
||||
environment=environment,
|
||||
).and_return('foo\nbar\nmysql\n').once()
|
||||
|
||||
names = module.database_names_to_dump(
|
||||
{'name': 'all', 'password_transport': 'environment'},
|
||||
{},
|
||||
'root',
|
||||
'trustsome1',
|
||||
environment,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
assert names == ('foo', 'bar')
|
||||
|
||||
|
||||
def test_database_names_to_dump_runs_mysql_with_tls():
|
||||
environment = flexmock()
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
@@ -141,9 +177,22 @@ def test_dump_data_sources_dumps_each_database():
|
||||
'resolve_credential'
|
||||
).and_return(None)
|
||||
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
|
||||
('bar',)
|
||||
)
|
||||
flexmock(module).should_receive('database_names_to_dump').with_args(
|
||||
database=databases[0],
|
||||
config={},
|
||||
username=None,
|
||||
password=None,
|
||||
environment={'USER': 'root'},
|
||||
dry_run=False,
|
||||
).and_return(('foo',))
|
||||
flexmock(module).should_receive('database_names_to_dump').with_args(
|
||||
database=databases[1],
|
||||
config={},
|
||||
username=None,
|
||||
password=None,
|
||||
environment={'USER': 'root'},
|
||||
dry_run=False,
|
||||
).and_return(('bar',))
|
||||
|
||||
for name, process in zip(('foo', 'bar'), processes):
|
||||
flexmock(module).should_receive('execute_dump_command').with_args(
|
||||
@@ -179,9 +228,14 @@ def test_dump_data_sources_dumps_with_password():
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
|
||||
('bar',)
|
||||
)
|
||||
flexmock(module).should_receive('database_names_to_dump').with_args(
|
||||
database=database,
|
||||
config={},
|
||||
username='root',
|
||||
password='trustsome1',
|
||||
environment={'USER': 'root'},
|
||||
dry_run=False,
|
||||
).and_return(('foo',))
|
||||
|
||||
flexmock(module).should_receive('execute_dump_command').with_args(
|
||||
database=database,
|
||||
@@ -205,6 +259,50 @@ def test_dump_data_sources_dumps_with_password():
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_dump_data_sources_dumps_with_environment_password_transport_passes_password_environment_variable():
|
||||
database = {
|
||||
'name': 'foo',
|
||||
'username': 'root',
|
||||
'password': 'trustsome1',
|
||||
'password_transport': 'environment',
|
||||
}
|
||||
process = flexmock()
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
|
||||
flexmock(module).should_receive('database_names_to_dump').with_args(
|
||||
database=database,
|
||||
config={},
|
||||
username='root',
|
||||
password='trustsome1',
|
||||
environment={'USER': 'root', 'MYSQL_PWD': 'trustsome1'},
|
||||
dry_run=False,
|
||||
).and_return(('foo',))
|
||||
|
||||
flexmock(module).should_receive('execute_dump_command').with_args(
|
||||
database=database,
|
||||
config={},
|
||||
username='root',
|
||||
password='trustsome1',
|
||||
dump_path=object,
|
||||
database_names=('foo',),
|
||||
environment={'USER': 'root', 'MYSQL_PWD': 'trustsome1'},
|
||||
dry_run=object,
|
||||
dry_run_label=object,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(
|
||||
[database],
|
||||
{},
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
patterns=[],
|
||||
dry_run=False,
|
||||
) == [process]
|
||||
|
||||
|
||||
def test_dump_data_sources_dumps_all_databases_at_once():
|
||||
databases = [{'name': 'all'}]
|
||||
process = flexmock()
|
||||
@@ -272,6 +370,54 @@ def test_dump_data_sources_dumps_all_databases_separately_when_format_configured
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_errors_for_missing_all_databases():
|
||||
databases = [{'name': 'all'}]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/all'
|
||||
)
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
patterns=[],
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
|
||||
databases = [{'name': 'all'}]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/all'
|
||||
)
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
patterns=[],
|
||||
dry_run=True,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
def test_database_names_to_dump_runs_mysql_with_list_options():
|
||||
database = {'name': 'all', 'list_options': '--defaults-extra-file=my.cnf --skip-ssl'}
|
||||
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
|
||||
@@ -375,6 +521,52 @@ def test_execute_dump_command_runs_mysqldump():
|
||||
)
|
||||
|
||||
|
||||
def test_execute_dump_command_with_environment_password_transport_skips_defaults_file_and_passes_user_flag():
|
||||
process = flexmock()
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
|
||||
'parse_extra_options'
|
||||
).and_return((), None)
|
||||
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
|
||||
'make_defaults_file_options'
|
||||
).never()
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'mysqldump',
|
||||
'--add-drop-database',
|
||||
'--user',
|
||||
'root',
|
||||
'--databases',
|
||||
'foo',
|
||||
'--result-file',
|
||||
'dump',
|
||||
),
|
||||
environment=None,
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert (
|
||||
module.execute_dump_command(
|
||||
database={'name': 'foo', 'password_transport': 'environment'},
|
||||
config={},
|
||||
username='root',
|
||||
password='trustsome1',
|
||||
dump_path=flexmock(),
|
||||
database_names=('foo',),
|
||||
environment=None,
|
||||
dry_run=False,
|
||||
dry_run_label='',
|
||||
)
|
||||
== process
|
||||
)
|
||||
|
||||
|
||||
def test_execute_dump_command_runs_mysqldump_without_add_drop_database():
|
||||
process = flexmock()
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
|
||||
@@ -761,54 +953,6 @@ def test_execute_dump_command_with_dry_run_skips_mysqldump():
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_errors_for_missing_all_databases():
|
||||
databases = [{'name': 'all'}]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/all'
|
||||
)
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
assert module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
patterns=[],
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
|
||||
databases = [{'name': 'all'}]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.os).should_receive('environ').and_return({'USER': 'root'})
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/all'
|
||||
)
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
assert (
|
||||
module.dump_data_sources(
|
||||
databases,
|
||||
{},
|
||||
config_paths=('test.yaml',),
|
||||
borgmatic_runtime_directory='/run/borgmatic',
|
||||
patterns=[],
|
||||
dry_run=True,
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
def test_restore_data_source_dump_runs_mysql_to_restore():
|
||||
hook_config = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
@@ -1092,6 +1236,53 @@ def test_restore_data_source_dump_runs_mysql_with_username_and_password():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_data_source_with_environment_password_transport_skips_defaults_file_and_passes_user_flag():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
'username': 'root',
|
||||
'password': 'trustsome1',
|
||||
'password_transport': 'environment',
|
||||
}
|
||||
]
|
||||
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.borgmatic.hooks.data_source.mariadb).should_receive(
|
||||
'make_defaults_file_options'
|
||||
).never()
|
||||
flexmock(module.os).should_receive('environ').and_return(
|
||||
{'USER': 'root', 'MYSQL_PWD': 'trustsome1'}
|
||||
)
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
('mysql', '--batch', '--user', 'root'),
|
||||
processes=[extract_process],
|
||||
output_log_level=logging.DEBUG,
|
||||
input_file=extract_process.stdout,
|
||||
environment={'USER': 'root', 'MYSQL_PWD': 'trustsome1'},
|
||||
).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_with_connection_params_uses_connection_params_for_restore():
|
||||
hook_config = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user