Compare commits

..
Author SHA1 Message Date
Dan Helfman 6ac2c86d2d Merge branch 'main' into parallel-borgmatic 2026-05-09 11:06:36 -07:00
Dan Helfman 9839b3dada Fix end-to-end database tests (#1303). 2026-05-09 11:05:28 -07:00
Dan Helfman b7ecc9c6c3 Fix repo-create action (#1267). 2026-05-09 10:30:48 -07:00
Dan Helfman 4f728b1cd4 Get rest of tests passing (#1267). 2026-05-08 22:16:41 -07:00
Dan Helfman 78db924b70 Get tests passing (#1267). 2026-05-08 18:14:15 -07:00
Dan Helfman b55834c1ca Initial support for multiple borgmatic instances run in parallel—as long as they're operating on different Borg repositories (#1267). 2026-05-08 12:44:16 -07:00
Dan Helfman 411685280c For MariaDB/MySQL, dump events, routines, and tablespaces. For MariaDB, dump a subset of system data when dumping "mysql"/"all" (#1303). 2026-05-08 09:38:13 -07:00
Dan Helfman cce679248f Clarify "umask" option semantics in regards to integer and octal (#1305). 2026-05-06 14:57:43 -07:00
Dan Helfman 68c9516424 Upgrade Apprise dependency. 2026-05-02 19:08:24 -07:00
Dan Helfman a508cabe3f Update the KeePassXC credential hook to support KeePassXC's secret service integration. 2026-04-29 12:52:27 -07:00
Dan Helfman 0e1659bd73 Expand the "patterns_from" and "exclude_from" options to support paths containing tildes and globs (#1301). 2026-04-27 10:13:21 -07:00
Dan Helfman ad61ad356e Update the documentation to use Pagefind's component-based search UI. 2026-04-26 09:10:07 -07:00
Dan Helfman 5c7d03910b Fix the "source_directories_must_exist" option to support source directories relative to a "working_directory" (#1300). 2026-04-25 13:49:55 -07:00
Dan Helfman 468af1de0b Fix the PostgreSQL database hook to properly parse "*options" values containing quoted spaces. 2026-04-24 19:24:44 -07:00
Dan Helfman 524d8263a7 Upgrade dependencies. 2026-04-24 17:04:29 -07:00
Dan Helfman 1d2aea0951 Add a development script for upgrading pinned dependencies. 2026-04-24 11:54:37 -07:00
Dan Helfman 961ff7c724 For the MariaDB and MySQL database hooks, escape quotes in passwords when the "password_transport" option is "pipe". 2026-04-24 10:24:45 -07:00
Dan Helfman 3e80056956 Add projects.torsion.org reply by email to NEWS. 2026-04-20 21:52:22 -07:00
Dan Helfman d8f558ce0d Add the NEWS changelog file to release tarball (#1298). 2026-04-20 15:42:17 -07:00
Dan Helfman 9da75fdc58 Use pytest tmp_path fixture instead of hardcoded /tmp paths (#1296).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/1296
2026-04-20 18:10:02 +00:00
Dan Helfman ac9c8bb644 Fix build of standalone binary. 2026-04-20 10:28:57 -07:00
Sirio Balmelli 0b34ef0e1a tests: use pytest tmp_path fixture instead of hardcoded /tmp paths
tmp_path is a built-in pytest fixture providing a unique per-test tempdir.

Hardcoded /tmp paths break when multiple builders run tests concurrently
(e.g. Nix parallel builds): leftover or colliding files cause spurious failures.

Signed-off-by: Sirio Balmelli <sirio@b-ad.ch>
2026-04-15 16:35:41 +02:00
53 changed files with 913 additions and 369 deletions
+1
View File
@@ -1,4 +1,5 @@
# This file only applies to the source dist tarball, not the built wheel.
include NEWS
include borgmatic/config/schema.yaml
graft docs
graft sample
+27
View File
@@ -1,3 +1,30 @@
2.1.6.dev0
* #1267: Support multiple borgmatic instances run in parallel—as long as they're operating on
different Borg repositories. This required an internal change to the runtime directory path,
which may cause a one-time performance hit (Borg cache misses) for Borg 1.x users of database and
filesystem hooks. See the documentation for more information:
https://torsion.org/borgmatic/how-to/make-per-application-backups/#parallelism
* #1300: Fix the "source_directories_must_exist" option to support source directories relative to a
"working_directory".
* #1301: Expand the "patterns_from" and "exclude_from" options to support paths containing tildes
and globs.
* #1303: For the MariaDB and MySQL database hooks, include events, routines, and tablespaces when
dumping a database.
* #1303: For the MariaDB hook, include only a subset of system data when dumping the "mysql" system
database (or "all" databases), so the dump is actually restorable. See the documentation for more
information: https://torsion.org/borgmatic/reference/configuration/data-sources/mariadb/
* Update the KeePassXC credential hook to support KeePassXC's secret service integration. See the
documentation for more information:
https://torsion.org/borgmatic/reference/configuration/credentials/keepassxc/
* Add the NEWS changelog file to release tarball (#1298).
* Enable reply by email on projects.torsion.org, so replies to notification emails get posted as
comments on tickets.
* For the MariaDB and MySQL database hooks, escape quotes in passwords when the
"password_transport" option is "pipe".
* Add a development script for upgrading pinned dependencies.
* Fix the PostgreSQL database hook to properly parse "*options" values containing quoted spaces.
* Update the documentation to use Pagefind's component-based search UI.
2.1.5
* #1229: Document the permissions needed for the PostgreSQL database hook:
https://torsion.org/borgmatic/reference/configuration/data-sources/postgresql/
+19
View File
@@ -0,0 +1,19 @@
.
apprise
attrs
certifi
charset-normalizer
click
idna
jsonschema
jsonschema-specifications
markdown
oauthlib
packaging
pyyaml
referencing
requests
requests-oauthlib
rpds-py
ruamel-yaml
urllib3
+21 -19
View File
@@ -1,19 +1,21 @@
.
apprise==1.9.9
attrs==26.1.0
certifi==2026.2.25
charset-normalizer==3.4.6
click==8.3.1
idna==3.11
jsonschema==4.26.0
jsonschema-specifications==2025.9.1
markdown==3.10.2
oauthlib==3.3.1
packaging==26.0
pyyaml==6.0.3
referencing==0.37.0
requests==2.33.1
requests-oauthlib==2.0.0
rpds-py==0.30.0
ruamel-yaml==0.19.1
urllib3==2.6.3
# This file was autogenerated by uv via the following command:
# uv pip compile --annotation-style line binary_requirements.in -o binary_requirements.txt
apprise==1.10.0 # via -r binary_requirements.in
attrs==26.1.0 # via jsonschema, referencing, -r binary_requirements.in
. # via -r binary_requirements.in
certifi==2026.4.22 # via apprise, requests, -r binary_requirements.in
charset-normalizer==3.4.7 # via requests, -r binary_requirements.in
click==8.3.3 # via apprise, -r binary_requirements.in
idna==3.13 # via requests, -r binary_requirements.in
jsonschema==4.26.0 # via borgmatic, -r binary_requirements.in
jsonschema-specifications==2025.9.1 # via jsonschema, -r binary_requirements.in
markdown==3.10.2 # via apprise, -r binary_requirements.in
oauthlib==3.3.1 # via requests-oauthlib, -r binary_requirements.in
packaging==26.2 # via borgmatic, -r binary_requirements.in
pyyaml==6.0.3 # via apprise, -r binary_requirements.in
referencing==0.37.0 # via jsonschema, jsonschema-specifications, -r binary_requirements.in
requests==2.33.1 # via apprise, borgmatic, requests-oauthlib, -r binary_requirements.in
requests-oauthlib==2.0.0 # via apprise, -r binary_requirements.in
rpds-py==0.30.0 # via jsonschema, referencing, -r binary_requirements.in
ruamel-yaml==0.19.1 # via borgmatic, -r binary_requirements.in
urllib3==2.6.3 # via requests, -r binary_requirements.in
+5 -3
View File
@@ -383,7 +383,7 @@ def collect_spot_check_source_paths(
# Omit "progress" because it interferes with "list_details".
config=dict(config, progress=False, list_details=True),
patterns=borgmatic.actions.pattern.process_patterns(
borgmatic.actions.pattern.collect_patterns(config)
borgmatic.actions.pattern.collect_patterns(config, working_directory)
+ tuple(
borgmatic.borg.pattern.Pattern(
config_path,
@@ -788,7 +788,7 @@ def run_check(
'''
logger.info('Running consistency checks')
repository_id = borgmatic.borg.check.get_repository_id(
repository_id = borgmatic.borg.repo_info.get_repository_id(
repository['path'],
config,
local_borg_version,
@@ -844,7 +844,9 @@ def run_check(
if 'spot' in checks:
logger.info('Running spot check')
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
with borgmatic.config.paths.Runtime_directory(
config, repository_id
) as borgmatic_runtime_directory:
spot_check(
repository,
config,
+12 -1
View File
@@ -3,6 +3,7 @@ import logging
import os
import borgmatic.borg.extract
import borgmatic.borg.repo_info
import borgmatic.borg.repo_list
import borgmatic.config.paths
@@ -109,8 +110,18 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
local_path=bootstrap_arguments.local_path,
remote_path=bootstrap_arguments.remote_path,
)
repository_id = borgmatic.borg.repo_info.get_repository_id(
bootstrap_arguments.repository,
config,
local_borg_version,
global_arguments,
local_path=bootstrap_arguments.local_path,
remote_path=bootstrap_arguments.remote_path,
)
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
with borgmatic.config.paths.Runtime_directory(
config, repository_id
) as borgmatic_runtime_directory:
manifest_config_paths = load_config_paths_from_archive(
bootstrap_arguments.repository,
archive_name,
+12 -2
View File
@@ -42,10 +42,20 @@ def run_create(
logger.info(f'Creating archive{dry_run_label}')
working_directory = borgmatic.config.paths.get_working_directory(config)
repository_id = borgmatic.borg.repo_info.get_repository_id(
repository['path'],
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
with borgmatic.config.paths.Runtime_directory(
config, repository_id
) as borgmatic_runtime_directory:
patterns = pattern.process_patterns(
pattern.collect_patterns(config),
pattern.collect_patterns(config, working_directory),
config,
working_directory,
borgmatic_runtime_directory,
+3 -2
View File
@@ -21,10 +21,11 @@ def run_diff(
# Only process patterns if only_patterns flag is set
if diff_arguments.only_patterns:
working_directory = borgmatic.config.paths.get_working_directory(config)
processed_patterns = borgmatic.actions.pattern.process_patterns(
(*borgmatic.actions.pattern.collect_patterns(config),),
(*borgmatic.actions.pattern.collect_patterns(config, working_directory),),
config,
borgmatic.config.paths.get_working_directory(config),
working_directory,
)
else:
processed_patterns = None
+8 -5
View File
@@ -34,10 +34,11 @@ def parse_pattern(pattern_line, default_style=borgmatic.borg.pattern.Pattern_sty
)
def collect_patterns(config):
def collect_patterns(config, working_directory):
'''
Given a configuration dict, produce a single sequence of patterns comprised of the configured
source directories, patterns, excludes, pattern files, and exclude files.
Given a configuration dict and the working directory, produce a single sequence of patterns
comprised of the configured source directories, patterns, excludes, pattern files, and exclude
files.
The idea is that Borg has all these different ways of specifying includes, excludes, source
directories, etc., but we'd like to collapse them all down to one common format (patterns) for
@@ -68,7 +69,8 @@ def collect_patterns(config):
+ tuple(
parse_pattern(pattern_line.strip())
for filename in config.get('patterns_from', ())
for pattern_line in open(filename, encoding='utf-8')
for expanded_path in expand_directory(filename, working_directory)
for pattern_line in open(expanded_path, encoding='utf-8')
if not pattern_line.lstrip().startswith('#')
if pattern_line.strip()
)
@@ -78,7 +80,8 @@ def collect_patterns(config):
borgmatic.borg.pattern.Pattern_style.FNMATCH,
)
for filename in config.get('exclude_from', ())
for exclude_line in open(filename, encoding='utf-8')
for expanded_path in expand_directory(filename, working_directory)
for exclude_line in open(expanded_path, encoding='utf-8')
if not exclude_line.lstrip().startswith('#')
if exclude_line.strip()
)
+4 -2
View File
@@ -30,10 +30,12 @@ def run_recreate(
else:
logger.answer(f'Recreating repository{dry_run_label}')
working_directory = borgmatic.config.paths.get_working_directory(config)
# Collect and process patterns.
processed_patterns = borgmatic.actions.pattern.process_patterns(
(
*borgmatic.actions.pattern.collect_patterns(config),
*borgmatic.actions.pattern.collect_patterns(config, working_directory),
# Also add borgmatic-specific paths, so they don't get excluded from the recreated
# archive. Note that this doesn't currently work for archives created with Borg 1.2 or
# below.
@@ -42,7 +44,7 @@ def run_recreate(
),
),
config,
borgmatic.config.paths.get_working_directory(config),
working_directory,
)
archive = borgmatic.borg.repo_list.resolve_archive_name(
+12 -2
View File
@@ -536,10 +536,20 @@ def run_restore(
'''
logger.info(f'Restoring data sources from archive {restore_arguments.archive}')
working_directory = borgmatic.config.paths.get_working_directory(config)
repository_id = borgmatic.borg.repo_info.get_repository_id(
repository['path'],
config,
local_borg_version,
global_arguments,
local_path=local_path,
remote_path=remote_path,
)
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
with borgmatic.config.paths.Runtime_directory(
config, repository_id
) as borgmatic_runtime_directory:
patterns = borgmatic.actions.pattern.process_patterns(
borgmatic.actions.pattern.collect_patterns(config),
borgmatic.actions.pattern.collect_patterns(config, working_directory),
config,
working_directory,
)
+1 -33
View File
@@ -1,10 +1,8 @@
import argparse
import json
import logging
import shlex
import borgmatic.config.paths
from borgmatic.borg import environment, feature, flags, repo_info
from borgmatic.borg import environment, feature, flags
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
@@ -82,36 +80,6 @@ def make_check_name_flags(checks, archive_filter_flags):
)
def get_repository_id(
repository_path,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
):
'''
Given a local or remote repository path, a configuration dict, the local Borg version, global
arguments, and local/remote commands to run, return the corresponding Borg repository ID.
Raise ValueError if the Borg repository ID cannot be determined.
'''
try:
return json.loads(
repo_info.display_repository_info(
repository_path,
config,
local_borg_version,
argparse.Namespace(json=True),
global_arguments,
local_path,
remote_path,
),
)['repository']['id']
except (json.JSONDecodeError, KeyError):
raise ValueError(f'Cannot determine Borg repository ID for {repository_path}')
def check_archives(
repository_path,
config,
+3 -1
View File
@@ -180,7 +180,9 @@ def make_base_create_command( # noqa: PLR0912
open pattern file handle).
'''
if config.get('source_directories_must_exist', True):
borgmatic.borg.pattern.check_all_root_patterns_exist(patterns)
borgmatic.borg.pattern.check_all_root_patterns_exist(
patterns, borgmatic.config.paths.get_working_directory(config)
)
patterns_file = borgmatic.borg.pattern.write_patterns_file(
patterns,
+4 -4
View File
@@ -88,16 +88,16 @@ def write_patterns_file(patterns, borgmatic_runtime_directory, patterns_file=Non
return patterns_file
def check_all_root_patterns_exist(patterns):
def check_all_root_patterns_exist(patterns, working_directory):
'''
Given a sequence of Pattern instances, check that all root pattern paths exist. If any don't,
raise an exception.
Given a sequence of Pattern instances and the current working directory, check that all root
pattern paths exist. If any don't, raise an exception.
'''
missing_paths = [
pattern.path
for pattern in patterns
if pattern.type == Pattern_type.ROOT
if not os.path.exists(pattern.path)
if not os.path.exists(os.path.join(working_directory or '', pattern.path))
]
if missing_paths:
+32
View File
@@ -1,3 +1,5 @@
import argparse
import json
import logging
import shlex
@@ -81,3 +83,33 @@ def display_repository_info(
)
return None
def get_repository_id(
repository_path,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
):
'''
Given a local or remote repository path, a configuration dict, the local Borg version, global
arguments, and local/remote commands to run, return the corresponding Borg repository ID.
Raise ValueError if the Borg repository ID cannot be determined.
'''
try:
return json.loads(
display_repository_info(
repository_path,
config,
local_borg_version,
argparse.Namespace(json=True),
global_arguments,
local_path,
remote_path,
),
)['repository']['id']
except (json.JSONDecodeError, KeyError):
raise ValueError(f'Cannot determine Borg repository ID for {repository_path}')
+39 -20
View File
@@ -1,6 +1,7 @@
import contextlib
import logging
import os
import shutil
import tempfile
from enum import Enum
@@ -84,6 +85,25 @@ def replace_temporary_subdirectory_with_glob(
)
class Fixed_name_temporary_directory:
'''
A class whose instances can stand-in for tempfile.TemporaryDirectory's, except the temporary
filename path is fixed rather than randomly generated.
'''
def __init__(self, path):
'''
Given a temporary directory path, save it off for later.
'''
self.name = path
def cleanup(self):
'''
Remove the temporary directory path.
'''
shutil.rmtree(self.name, ignore_errors=True)
class Runtime_directory:
'''
A Python context manager for creating and cleaning up the borgmatic runtime directory used for
@@ -98,16 +118,17 @@ class Runtime_directory:
automatically gets cleaned up as necessary.
'''
def __init__(self, config):
def __init__(self, config, repository_id):
'''
Given a configuration dict determine the borgmatic runtime directory, creating a secure,
temporary directory within it if necessary. Defaults to $XDG_RUNTIME_DIR/./borgmatic or
$RUNTIME_DIRECTORY/./borgmatic or $TMPDIR/borgmatic-[random]/./borgmatic or
$TEMP/borgmatic-[random]/./borgmatic or /tmp/borgmatic-[random]/./borgmatic where "[random]"
is a randomly generated string intended to avoid path collisions.
If XDG_RUNTIME_DIR or RUNTIME_DIRECTORY is set and already ends in "/borgmatic", then don't
tack on a second "/borgmatic" path component.
Given a configuration dict and the Borg ID for a repository, determine the borgmatic runtime
directory, creating a secure, temporary directory within it if necessary. Defaults to
$XDG_RUNTIME_DIR/borgmatic-[repository_id]/./borgmatic or
$RUNTIME_DIRECTORY/borgmatic-[repository_id]/./borgmatic or
$TMPDIR/borgmatic-[random]/./borgmatic or $TEMP/borgmatic-[random]/./borgmatic or
/tmp/borgmatic-[random]/./borgmatic where "[random]" is a randomly generated string and
"[repository_id]" is the Borg repository ID being operated on. Both strings are intended to
avoid path collisions, and the random string helps avoid predictable temporary path attacks
in shared temporary directories.
The "/./" is taking advantage of a Borg feature such that the part of the path before the "/./"
does not get stored in the file path within an archive. That way, the path of the runtime
@@ -125,7 +146,8 @@ class Runtime_directory:
if not runtime_directory.startswith(os.path.sep):
raise ValueError('The runtime directory must be an absolute path')
self.temporary_directory = None
runtime_directory = os.path.join(runtime_directory, f'borgmatic-{repository_id}')
self.temporary_directory = Fixed_name_temporary_directory(runtime_directory)
else:
base_directory = (
os.environ.get('TMPDIR') or os.environ.get('TEMP') or '/tmp' # noqa: S108
@@ -141,11 +163,9 @@ class Runtime_directory:
)
runtime_directory = self.temporary_directory.name
(base_path, final_directory) = os.path.split(runtime_directory.rstrip(os.path.sep))
self.runtime_path = expand_user_in_path(
os.path.join(
base_path if final_directory == 'borgmatic' else runtime_directory,
runtime_directory,
'.', # Borg 1.4+ "slashdot" hack.
'borgmatic',
),
@@ -162,14 +182,13 @@ class Runtime_directory:
def __exit__(self, exception_type, exception, traceback):
'''
Delete any temporary directory that was created as part of initialization.
Delete the temporary directory that was created as part of initialization.
'''
if self.temporary_directory:
# The cleanup() call errors if, for instance, there's still a
# mounted filesystem within the temporary directory. There's
# nothing we can do about that here, so swallow the error.
with contextlib.suppress(OSError):
self.temporary_directory.cleanup()
# The cleanup() call errors if, for instance, there's still a
# mounted filesystem within the temporary directory. There's
# nothing we can do about that here, so swallow the error.
with contextlib.suppress(OSError):
self.temporary_directory.cleanup()
def make_runtime_directory_glob(borgmatic_runtime_directory):
+13 -2
View File
@@ -489,6 +489,9 @@ properties:
description: |
Umask used for when executing Borg or calling hooks. Defaults to
0077 for Borg or the umask that borgmatic is run with for hooks.
Even though this value is a YAML integer, borgmatic interprets it as
octal. YAML's "0o"-prefixed octal notation is not currently
supported.
example: 0077
lock_wait:
type: integer
@@ -3309,24 +3312,32 @@ properties:
description: |
Command to use instead of "keepassxc-cli".
example: /usr/local/bin/keepassxc-cli
secret_tool_command:
type: string
description: |
Command to use instead of "secret-tool".
example: /usr/local/bin/secret-tool
ask_for_password:
type: boolean
description: |
Whether keepassxc-cli should prompt the user for a password.
Disabling this is only really useful if you're unlocking
your KeePassXC database with a key file instead of a
password. Defaults to true.
password. Ignored when using KeePassXC's secret service
integration. Defaults to true.
example: false
key_file:
type: string
description: |
Path to a key file for unlocking the KeePassXC database.
Ignored when using KeePassXC's secret service integration.
example: /path/to/keyfile
yubikey:
type: string
description: |
YubiKey slot and optional serial number used to access the
KeePassXC database. The format is "<slot[:serial]>", where:
KeePassXC database. Ignored when using KeePassXC's secret
service integration. The format is "<slot[:serial]>", where:
* <slot> is the YubiKey slot number (e.g., `1` or `2`).
* <serial> (optional) is the YubiKey's serial number (e.g.,
`7370001`).
+17 -2
View File
@@ -7,19 +7,34 @@ import borgmatic.execute
logger = logging.getLogger(__name__)
SECRET_SERVICE_DATABASE_PATH = 'secret-service'
def load_credential(hook_config, config, credential_parameters):
'''
Given the hook configuration dict, the configuration dict, and a credential parameters tuple
containing a KeePassXC database path and an attribute name to load, run keepassxc-cli to fetch
the corresponding KeePassXC credential and return it.
the corresponding KeePassXC credential and return it. Or use secret-tool if the database path
is "secret-service", indicating that KeePassXC's secret service integration should be used
instead.
Raise ValueError if keepassxc-cli can't retrieve the credential.
Raise ValueError if keepassxc-cli or secret-tool can't retrieve the credential.
'''
try:
(database_path, attribute_name) = credential_parameters
except ValueError:
raise ValueError(f'Invalid KeePassXC credential parameters: {credential_parameters}')
if database_path == SECRET_SERVICE_DATABASE_PATH:
command = (
*shlex.split((hook_config or {}).get('secret_tool_command', 'secret-tool')),
*('lookup', 'Path', attribute_name),
)
return '\n'.join(borgmatic.execute.execute_command_and_capture_output(command)).rstrip(
os.linesep
)
expanded_database_path = os.path.expanduser(database_path)
if not os.path.exists(expanded_database_path):
+18 -10
View File
@@ -1,4 +1,3 @@
import copy
import logging
import os
import re
@@ -67,7 +66,9 @@ def make_defaults_file_options(username=None, password=None, defaults_extra_file
Do not use the returned value for multiple different command invocations. That will not work
because each pipe is "used up" once read.
'''
escaped_password = None if password is None else password.replace('\\', '\\\\')
escaped_password = (
None if password is None else password.replace('\\', '\\\\').replace('"', '\\"')
)
values = '\n'.join(
(
@@ -106,6 +107,10 @@ def make_defaults_file_options(username=None, password=None, defaults_extra_file
return (f'--defaults-extra-file=/dev/fd/{read_file_descriptor}',)
EXCLUDED_SYSTEM_DATABASE_NAMES = ('information_schema', 'performance_schema', 'sys')
SYSTEM_DATABASE_NAME = 'mysql'
def database_names_to_dump(database, config, username, password, environment, dry_run):
'''
Given a requested database config, a configuration dict, a database username and password, an
@@ -164,17 +169,18 @@ def database_names_to_dump(database, config, username, password, environment, dr
working_directory=borgmatic.config.paths.get_working_directory(config),
)
# Dumping system databases directly doesn't work; too much gets dumped (including virtual tables
# that MariaDB recreates on startup) and so the dump isn't restorable. Therefore several system
# databases are excluded here. But also see below where select system tables from the "mysql"
# system table are dumped via the "--system" flag.
return tuple(
show_name.strip()
for show_name in show_lines
if show_name not in SYSTEM_DATABASE_NAMES
if show_name not in EXCLUDED_SYSTEM_DATABASE_NAMES
if not skip_names or show_name not in skip_names
)
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
def execute_dump_command(
database,
config,
@@ -235,8 +241,9 @@ def execute_dump_command(
+ (('--user', username) if username and password_transport == 'environment' else ())
+ (('--ssl',) if database.get('tls') is True else ())
+ (('--skip-ssl',) if database.get('tls') is False else ())
+ ('--databases',)
+ database_names
+ ('--databases', '--events', '--routines', '--all-tablespaces')
+ (('--system=users,udfs,servers',) if SYSTEM_DATABASE_NAME in database_names else ())
+ tuple(name for name in database_names if name != SYSTEM_DATABASE_NAME)
+ ('--result-file', dump_filename)
)
@@ -323,6 +330,7 @@ def dump_data_sources(
raise ValueError('Cannot find any MariaDB databases to dump.')
# Database dumps to individual files.
if database['name'] == 'all' and database.get('format'):
for database_name in dump_database_names:
dumps_metadata.append(
@@ -335,8 +343,7 @@ def dump_data_sources(
database.get('container'),
)
)
renamed_database = copy.copy(database)
renamed_database['name'] = database_name
renamed_database = dict(database, name=database_name)
processes.append(
execute_dump_command(
renamed_database,
@@ -350,6 +357,7 @@ def dump_data_sources(
dry_run_label,
),
)
# Database dumps all to one file.
else:
dumps_metadata.append(
borgmatic.actions.restore.Dump(
+2 -4
View File
@@ -1,4 +1,3 @@
import copy
import logging
import os
import shlex
@@ -166,7 +165,7 @@ def execute_dump_command(
+ (('--user', username) if username and password_transport == 'environment' else ())
+ (('--ssl',) if database.get('tls') is True else ())
+ (('--skip-ssl',) if database.get('tls') is False else ())
+ ('--databases',)
+ ('--databases', '--events', '--routines', '--all-tablespaces')
+ database_names
+ ('--result-file', dump_filename)
)
@@ -266,8 +265,7 @@ def dump_data_sources(
database.get('container'),
)
)
renamed_database = copy.copy(database)
renamed_database['name'] = database_name
renamed_database = dict(database, name=database_name)
processes.append(
execute_dump_command(
renamed_database,
+8 -4
View File
@@ -100,7 +100,11 @@ def database_names_to_dump(database, config, environment, dry_run):
if 'username' in database
else ()
)
+ (tuple(database['list_options'].split(' ')) if 'list_options' in database else ())
+ (
tuple(shlex.quote(part) for part in shlex.split(database['list_options']))
if 'list_options' in database
else ()
)
)
logger.debug('Querying for "all" PostgreSQL databases to dump')
list_lines = execute_command_and_capture_output(
@@ -226,7 +230,7 @@ def dump_data_sources(
+ (('--compress', shlex.quote(str(compression))) if compression is not None else ())
+ (('--file', shlex.quote(dump_filename)) if dump_format == 'directory' else ())
+ (
tuple(shlex.quote(option) for option in database['options'].split(' '))
tuple(shlex.quote(part) for part in shlex.split(database['options']))
if 'options' in database
else ()
)
@@ -393,7 +397,7 @@ def restore_data_source_dump(
+ (('--username', username) if username else ())
+ (('--dbname', data_source['name']) if not all_databases else ())
+ (
tuple(data_source['analyze_options'].split(' '))
tuple(shlex.quote(part) for part in shlex.split(data_source['analyze_options']))
if 'analyze_options' in data_source
else ()
)
@@ -414,7 +418,7 @@ def restore_data_source_dump(
+ (('--username', username) if username else ())
+ (('--no-owner',) if data_source.get('no_owner', False) else ())
+ (
tuple(data_source['restore_options'].split(' '))
tuple(shlex.quote(part) for part in shlex.split(data_source['restore_options']))
if 'restore_options' in data_source
else ()
)
+1
View File
@@ -23,6 +23,7 @@
{% if feedTitle and feedUrl %}
<link rel="alternate" href="{{ feedUrl }}" title="{{ feedTitle }}" type="application/atom+xml">
{% endif %}
{{ head_additions | safe }}
</head>
<body>
+16 -10
View File
@@ -58,19 +58,25 @@ choice](https://torsion.org/borgmatic/how-to/set-up-backups/#autopilot), each
entry using borgmatic's `--config` flag instead of relying on
`/etc/borgmatic.d`.
<a id="limitations"></a>
## Limitations
## Parallelism
borgmatic does not currently support its own parallelism—being run multiple
times on the same machine simultaneously. In particular, many of the [data
source
hooks](https://torsion.org/borgmatic/reference/configuration/data-sources/) rely
on global borgmatic runtime files which can't be shared across processes, and
therefore multiple borgmatic instances on the same machine would interfere with
each other.
<span class="minilink minilink-addedin">New in version 2.1.6</span> borgmatic
supports multiple borgmatic instances run on the same machine in parallel—as
long as they're operating on different Borg repositories.
A single borgmatic instance also doesn't currently support running multiple Borg
instances in parallel on the same machine.
However, note that a single borgmatic instance doesn't currently support running
multiple Borg instances in parallel on the same machine.
<span class="minilink minilink-addedin">Prior to version 2.1.6</span> borgmatic
did not support parallel borgmatic runs on the same machine simultaneously. In
particular, many of the [data source
hooks](https://torsion.org/borgmatic/reference/configuration/data-sources/)
relied on global borgmatic runtime files which couldn't be shared across
processes, and therefore multiple borgmatic instances on the same machine
interfered with each other.
<a id="archive-naming"></a>
@@ -43,14 +43,15 @@ postgresql_databases:
```
### Custom command
### Custom commands
You can also optionally override the `keepassxc-cli` command that borgmatic calls to load
passwords:
You can also optionally override the `keepassxc-cli` or `secret-tool` commands
that borgmatic call to load passwords:
```yaml
keepassxc:
keepassxc_cli_command: /usr/local/bin/keepassxc-cli
secret_tool_command: /usr/local/bin/secret-tool
```
Another example:
@@ -98,3 +99,30 @@ keepassxc:
The value here is the YubiKey slot number (e.g., `1` or `2`) and optional serial
number (e.g., `7370001`) used to access the KeePassXC database. Join the two
values with a colon, but omit the colon if you're leaving out the serial number.
### Secret service integration
<span class="minilink minilink-addedin">New in version 2.1.6</span> borgmatic
supports [KeePassXC's secret service
integration](https://keepassxc.org/docs/KeePassXC_UserGuide#_secret_service_integration)
that integrates with the [freedesktop secret service
API](https://specifications.freedesktop.org/secret-service/latest/) and allows
clients like borgmatic to access your passwords.
To use this feature from borgmatic, specify `secret-service` instead of a
KeePassXC database path when calling this credential hook. For instance:
```yaml
encryption_passphrase: "{credential keepassxc secret-service borgmatic}"
```
With this in place, borgmatic runs libsecret's `secret-tool` command to retrieve
the password titled "borgmatic" on demand. KeePassXC may then prompt you to
approve the password access, depending on how you've configured it.
One benefit of using the KeePassXC's secret service integration like this is
that you don't have to type a KeePassXC database passhprase (or use a keyfile)
whenever borgmatic accesses your passwords. Instead, you can configure
KeePassXC to prompt you to approve or deny each access. Or you can even
pre-approve password access to support automated borgmatic runs.
@@ -16,10 +16,46 @@ mariadb_databases:
```
### Full configuration
## System databases
<span class="minilink minilink-addedin">New in version 2.1.6</span> When dumping
["all"
databases](https://torsion.org/borgmatic/how-to/backup-your-databases/#all-databases),
borgmatic excludes most data coming from [MariaDB system
databases](https://mariadb.com/docs/server/reference/system-tables),
because much of it is populated on MariaDB startup and thus not restorable (or
just unnecessary to backup). The system data that borgmatic does include in
these dumps are: users, roles, grants, user-defined functions, and remote
servers.
Within a Borg archive, you can find this data stored in a dump named `mysql`—the
name of the system table this data comes from. And if you'd like to dump this
data without having to dump "all" databases, then you can configure a database
named `mysql` in your borgmatic configuration. For example:
```yaml
mariadb_databases:
- name: mysql
```
Even in this case though, only the subset of system data described above is
included in the dump.
<span class="minilink minilink-addedin">Prior to version 2.1.6</span> Dumps of
"all" databases excluded system databases and all of their data. Additionally,
explicitly dumping `mysql` was treated like any other database—and thus wasn't
easily restorable.
## Full configuration
{% include snippet/configuration/sample.md %}
```yaml
{% include borgmatic/mariadb_databases.yaml %}
```
## Related documentation
* [How to backup your databases](https://torsion.org/borgmatic/how-to/backup-your-databases/)
@@ -14,10 +14,15 @@ mongodb_databases:
```
### Full configuration
## Full configuration
{% include snippet/configuration/sample.md %}
```yaml
{% include borgmatic/mongodb_databases.yaml %}
```
## Related documentation
* [How to backup your databases](https://torsion.org/borgmatic/how-to/backup-your-databases/)
@@ -21,3 +21,8 @@ mysql_databases:
```yaml
{% include borgmatic/mysql_databases.yaml %}
```
## Related documentation
* [How to backup your databases](https://torsion.org/borgmatic/how-to/backup-your-databases/)
@@ -76,3 +76,8 @@ roles](https://www.postgresql.org/docs/current/predefined-roles.html) and
```yaml
{% include borgmatic/postgresql_databases.yaml %}
```
## Related documentation
* [How to backup your databases](https://torsion.org/borgmatic/how-to/backup-your-databases/)
@@ -22,3 +22,8 @@ sqlite_databases:
```yaml
{% include borgmatic/sqlite_databases.yaml %}
```
## Related documentation
* [How to backup your databases](https://torsion.org/borgmatic/how-to/backup-your-databases/)
@@ -29,11 +29,18 @@ probes the following values:
You can see the runtime directory path that borgmatic selects by running with
`--verbosity 2` and looking for `Using runtime directory` in the output.
Regardless of the runtime directory selected, borgmatic stores its files
within a `borgmatic` subdirectory of the runtime directory. Additionally, in
the case of `TMPDIR`, `TEMP`, and the hard-coded `/tmp`, borgmatic creates a
randomly named subdirectory in an effort to reduce path collisions in shared
system temporary directories.
Regardless of the runtime directory selected, borgmatic stores its files within
a `borgmatic` subdirectory of the runtime directory. Additionally, in the case
of `TMPDIR`, `TEMP`, and the hard-coded `/tmp`, borgmatic creates a randomly
named subdirectory in an effort to reduce path collisions and predictable
temporary path attacks in shared temporary directories.
<span class="minilink minilink-addedin">New in version 2.1.6</span> When
constructing the runtime directory, borgmatic now creates a subdirectory named
after the Borg ID of the current repository. This means that borgmatic supports
multiple borgmatic instances run [in
parallel](https://torsion.org/borgmatic/how-to/make-per-application-backups/#parallelism)—as
long as they're operating on different Borg repositories.
<span class="minilink minilink-addedin">Prior to version 1.9.0</span>
borgmatic created temporary streaming database dumps within the `~/.borgmatic`
+40 -6
View File
@@ -1,14 +1,48 @@
---
title: Search the documentation
eleventyExcludeFromCollections: true
templateEngineOverride: md
head_additions: >
<link href="/borgmatic/pagefind/pagefind-component-ui.css" rel="stylesheet">
<script src="/borgmatic/pagefind/pagefind-component-ui.js" type="module"></script>
<style>
@media (prefers-color-scheme: light) {
:root {
--pf-summary-font-size: 14px;
--pf-result-title-font-size: 16px;
--pf-result-excerpt-font-size: 15px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--pf-summary-font-size: 14px;
--pf-result-title-font-size: 16px;
--pf-result-excerpt-font-size: 15px;
}
}
.pf-result .pf-result-title, .pf-heading-link {
font-weight: 600 !important;
}
.pf-result .pf-result-excerpt mark, .pf-heading-excerpt mark {
background-color: yellow !important;
}
</style>
---
<link href="/borgmatic/pagefind/pagefind-ui.css" rel="stylesheet">
<script src="/borgmatic/pagefind/pagefind-ui.js"></script>
<div id="search"></div>
<script>
<pagefind-config bundle-path="/borgmatic/pagefind/" base-url="/borgmatic/"></pagefind-config>
<p><pagefind-input placeholder="Search"></pagefind-input></p>
<p><pagefind-summary></pagefind-summary></p>
<p><pagefind-results></pagefind-results></p>
<script type="module">
const manager = window.PagefindComponents.getInstanceManager();
const instance = manager.getInstance('default');
window.addEventListener('DOMContentLoaded', (event) => {
let search = new PagefindUI({ element: '#search', showSubResults: true, autofocus: true });
let url_parameters = new URLSearchParams(window.location.search);
search.triggerSearch(url_parameters.get('query'));
instance.triggerSearch(url_parameters.get('query'));
});
</script>
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "borgmatic"
version = "2.1.5"
version = "2.1.6.dev0"
authors = [
{ name="Dan Helfman", email="witten@torsion.org" },
]
+2 -2
View File
@@ -44,9 +44,9 @@ scripts/export-docs-from-image
docs_path=dist/borgmatic-docs.tar.gz
# Build stand-alone binary.
uv venv --clear binary
uv venv --python 3.13 --clear binary
source binary/bin/activate
uv pip install -r binary_requirements.txt
uv pip install -r binary_requirements.txt nuitka[onefile]
nuitka --mode=onefile --enable-plugin=upx --include-package-data=borgmatic --include-data-dir=borgmatic.egg-info=borgmatic.egg-info --include-package=borgmatic.hooks --include-package=apprise --no-deployment-flag=self-execution borgmatic/commands/borgmatic.py
deactivate
rm -fr binary borgmatic.build borgmatic.dist
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
set -eou pipefail
uv pip compile --quiet --upgrade --annotation-style line test_requirements.in -o test_requirements.txt
uv pip compile --quiet --upgrade --annotation-style line binary_requirements.in -o binary_requirements.txt
# Carry forward certain packages with version ranges instead of absolute pins. This is because these
# packages are a pain to compile, and pre-built wheels available on pypi may not be available for
# pinned versions on particular platforms.
while IFS= read -r package_line; do
package_name=$(echo "$package_line" | cut -d ">" -f 1)
sed -i "s/^$package_name==.*/$package_line/" test_requirements.txt
done <<< $(grep ">" test_requirements.in)
sed -i "s/was autogenerated/was (mostly) autogenerated/" test_requirements.txt
+28
View File
@@ -0,0 +1,28 @@
apprise
attrs
certifi
charset-normalizer
click>=8.1.8
codespell
coverage
flexmock
idna
iniconfig
jsonschema
jsonschema-specifications
markdown
oauthlib
packaging
pluggy
pygments
pytest
pytest-cov
pytest-timeout
pyyaml>5.0.0
referencing
requests
requests-oauthlib
rpds-py
ruamel-yaml>0.15.0
typing-extensions
urllib3
+27 -25
View File
@@ -1,28 +1,30 @@
apprise==1.9.9
attrs==26.1.0
certifi==2026.2.25
charset-normalizer==3.4.6
# This file was (mostly) autogenerated by uv via the following command:
# uv pip compile --annotation-style line test_requirements.in -o test_requirements.txt
apprise==1.10.0 # via -r test_requirements.in
attrs==26.1.0 # via jsonschema, referencing, -r test_requirements.in
certifi==2026.4.22 # via apprise, requests, -r test_requirements.in
charset-normalizer==3.4.7 # via requests, -r test_requirements.in
click>=8.1.8
codespell==2.4.2
coverage==7.13.5
flexmock==0.13.0
idna==3.11
iniconfig==2.3.0
jsonschema==4.26.0
jsonschema-specifications==2025.9.1
markdown==3.10.2
oauthlib==3.3.1
packaging==26.0
pluggy==1.6.0
pygments==2.20.0
pytest==9.0.2
pytest-cov==7.1.0
pytest-timeout==2.4.0
codespell==2.4.2 # via -r test_requirements.in
coverage==7.13.5 # via pytest-cov, -r test_requirements.in
flexmock==0.13.0 # via -r test_requirements.in
idna==3.13 # via requests, -r test_requirements.in
iniconfig==2.3.0 # via pytest, -r test_requirements.in
jsonschema==4.26.0 # via -r test_requirements.in
jsonschema-specifications==2025.9.1 # via jsonschema, -r test_requirements.in
markdown==3.10.2 # via apprise, -r test_requirements.in
oauthlib==3.3.1 # via requests-oauthlib, -r test_requirements.in
packaging==26.2 # via pytest, -r test_requirements.in
pluggy==1.6.0 # via pytest, pytest-cov, -r test_requirements.in
pygments==2.20.0 # via pytest, -r test_requirements.in
pytest==9.0.3 # via pytest-cov, pytest-timeout, -r test_requirements.in
pytest-cov==7.1.0 # via -r test_requirements.in
pytest-timeout==2.4.0 # via -r test_requirements.in
pyyaml>5.0.0
referencing==0.37.0
requests==2.33.1
requests-oauthlib==2.0.0
rpds-py==0.30.0
referencing==0.37.0 # via jsonschema, jsonschema-specifications, -r test_requirements.in
requests==2.33.1 # via apprise, requests-oauthlib, -r test_requirements.in
requests-oauthlib==2.0.0 # via apprise, -r test_requirements.in
rpds-py==0.30.0 # via jsonschema, referencing, -r test_requirements.in
ruamel-yaml>0.15.0
typing-extensions==4.15.0
urllib3==2.6.3
typing-extensions==4.15.0 # via -r test_requirements.in
urllib3==2.6.3 # via requests, -r test_requirements.in
@@ -57,11 +57,15 @@ mariadb_databases:
hostname: mariadb
username: root
password: test
# Needed for restoring users to the system database, because we're not actually dropping
# users first. (Dropping root and mariadb.sys would cause problems.)
restore_options: --force
- name: all
{mariadb_mysql_dump_format_option}
hostname: mariadb
username: root
password: test
restore_options: --force
mysql_databases:
- name: test
hostname: not-actually-mysql
@@ -132,6 +136,7 @@ mariadb_databases:
restore_port: 3307
restore_username: root
restore_password: test2
restore_options: --force
mysql_databases:
- name: test
container: not-actually-mysql
@@ -201,6 +206,7 @@ mariadb_databases:
restore_port: 3307
restore_username: root
restore_password: test2
restore_options: --force
mysql_databases:
- name: test
hostname: not-actually-mysql
+2 -2
View File
@@ -315,10 +315,10 @@ def test_write_configuration_with_already_existing_file_raises():
module.write_configuration('config.yaml', 'config: yaml')
def test_write_configuration_with_already_existing_file_and_overwrite_does_not_raise():
def test_write_configuration_with_already_existing_file_and_overwrite_does_not_raise(tmp_path):
flexmock(os.path).should_receive('exists').and_return(True)
module.write_configuration('/tmp/config.yaml', 'config: yaml', overwrite=True)
module.write_configuration(str(tmp_path / 'config.yaml'), 'config: yaml', overwrite=True)
def test_write_configuration_with_already_existing_directory_does_not_raise():
+3 -3
View File
@@ -222,11 +222,11 @@ def test_parse_configuration_merges_include():
assert logs == []
def test_parse_configuration_raises_for_missing_config_file():
def test_parse_configuration_raises_for_missing_config_file(tmp_path):
with pytest.raises(FileNotFoundError):
module.parse_configuration(
'/tmp/config.yaml',
'/tmp/schema.yaml',
str(tmp_path / 'nonexistent' / 'config.yaml'),
str(tmp_path / 'nonexistent' / 'schema.yaml'),
arguments={'global': flexmock()},
)
@@ -171,6 +171,7 @@ def test_run_bootstrap_does_not_raise():
flexmock(module.borgmatic.borg.repo_list).should_receive('resolve_archive_name').and_return(
'archive',
)
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('repo')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
@@ -222,6 +223,7 @@ def test_run_bootstrap_translates_ssh_command_argument_to_config():
local_path='borg7',
remote_path='borg8',
).and_return('archive')
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('repo')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
+4 -4
View File
@@ -1838,7 +1838,7 @@ def test_spot_check_without_any_source_paths_errors():
def test_run_check_checks_archives_for_configured_repository():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module).should_receive('upgrade_check_times')
flexmock(module).should_receive('parse_checks')
flexmock(module.borgmatic.borg.check).should_receive('make_archive_filter_flags').and_return(())
@@ -1873,7 +1873,7 @@ def test_run_check_checks_archives_for_configured_repository():
def test_run_check_runs_configured_extract_check():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module).should_receive('upgrade_check_times')
flexmock(module).should_receive('parse_checks')
flexmock(module.borgmatic.borg.check).should_receive('make_archive_filter_flags').and_return(())
@@ -1906,7 +1906,7 @@ def test_run_check_runs_configured_extract_check():
def test_run_check_runs_configured_spot_check():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module).should_receive('upgrade_check_times')
flexmock(module).should_receive('parse_checks')
flexmock(module.borgmatic.borg.check).should_receive('make_archive_filter_flags').and_return(())
@@ -1942,7 +1942,7 @@ def test_run_check_runs_configured_spot_check():
def test_run_check_without_checks_runs_nothing_except_hooks():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.check).should_receive('get_repository_id').and_return(flexmock())
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module).should_receive('upgrade_check_times')
flexmock(module).should_receive('parse_checks')
flexmock(module.borgmatic.borg.check).should_receive('make_archive_filter_flags').and_return(())
+7
View File
@@ -11,6 +11,7 @@ from borgmatic.actions import create as module
def test_run_create_executes_and_calls_hooks_for_configured_repository():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
@@ -49,6 +50,7 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
def test_run_create_with_both_list_and_json_errors():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').never()
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').never()
flexmock(module.borgmatic.borg.create).should_receive('create_archive').never()
create_arguments = flexmock(
@@ -80,6 +82,7 @@ def test_run_create_with_both_list_and_json_errors():
def test_run_create_with_both_list_and_progress_errors():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').never()
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').never()
flexmock(module.borgmatic.borg.create).should_receive('create_archive').never()
create_arguments = flexmock(
@@ -111,6 +114,7 @@ def test_run_create_with_both_list_and_progress_errors():
def test_run_create_produces_json():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
@@ -156,6 +160,7 @@ def test_run_create_with_active_dumps_roundtrips_via_checkpoint_archive():
mock_dump_process.should_receive('poll').and_return(None).and_return(0)
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
@@ -234,6 +239,7 @@ def test_run_create_with_active_dumps_json_updates_archive_info():
}
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
@@ -316,6 +322,7 @@ def mock_dump_cleanup(config, borgmatic_runtime_directory, patterns, dry_run):
def test_run_create_with_active_dumps_removes_data_source_dumps_with_original_patterns():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
flexmock(),
)
+29 -7
View File
@@ -35,7 +35,7 @@ def test_parse_pattern_with_invalid_pattern_line_errors():
def test_collect_patterns_converts_source_directories():
assert module.collect_patterns({'source_directories': ['/foo', '/bar']}) == (
assert module.collect_patterns({'source_directories': ['/foo', '/bar']}, '/working') == (
Pattern('/foo', source=Pattern_source.CONFIG),
Pattern('/bar', source=Pattern_source.CONFIG),
)
@@ -48,14 +48,18 @@ def test_collect_patterns_parses_config_patterns():
flexmock(module).should_receive('parse_pattern').with_args(' ').never()
flexmock(module).should_receive('parse_pattern').with_args('R /bar').and_return(Pattern('/bar'))
assert module.collect_patterns({'patterns': ['R /foo', '# comment', '', ' ', 'R /bar']}) == (
assert module.collect_patterns(
{'patterns': ['R /foo', '# comment', '', ' ', 'R /bar']}, '/working'
) == (
Pattern('/foo'),
Pattern('/bar'),
)
def test_collect_patterns_converts_exclude_patterns():
assert module.collect_patterns({'exclude_patterns': ['/foo', '/bar', 'sh:**/baz']}) == (
assert module.collect_patterns(
{'exclude_patterns': ['/foo', '/bar', 'sh:**/baz']}, '/working'
) == (
Pattern(
'/foo',
Pattern_type.NO_RECURSE,
@@ -78,6 +82,12 @@ def test_collect_patterns_converts_exclude_patterns():
def test_collect_patterns_reads_config_patterns_from_file():
flexmock(module).should_receive('expand_directory').with_args(
'file1.txt', '/working'
).and_return(['file1.txt'])
flexmock(module).should_receive('expand_directory').with_args(
'file2.txt', '/working'
).and_return(['file2.txt'])
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('file1.txt', encoding='utf-8').and_return(
io.StringIO('R /foo')
@@ -92,7 +102,7 @@ def test_collect_patterns_reads_config_patterns_from_file():
flexmock(module).should_receive('parse_pattern').with_args('R /bar').and_return(Pattern('/bar'))
flexmock(module).should_receive('parse_pattern').with_args('R /baz').and_return(Pattern('/baz'))
assert module.collect_patterns({'patterns_from': ['file1.txt', 'file2.txt']}) == (
assert module.collect_patterns({'patterns_from': ['file1.txt', 'file2.txt']}, '/working') == (
Pattern('/foo'),
Pattern('/bar'),
Pattern('/baz'),
@@ -101,16 +111,25 @@ def test_collect_patterns_reads_config_patterns_from_file():
def test_collect_patterns_errors_on_missing_config_patterns_from_file():
builtins = flexmock(sys.modules['builtins'])
flexmock(module).should_receive('expand_directory').with_args(
'file1.txt', '/working'
).and_return(['file1.txt'])
builtins.should_receive('open').with_args('file1.txt', encoding='utf-8').and_raise(
FileNotFoundError
)
flexmock(module).should_receive('parse_pattern').never()
with pytest.raises(ValueError):
module.collect_patterns({'patterns_from': ['file1.txt', 'file2.txt']})
module.collect_patterns({'patterns_from': ['file1.txt', 'file2.txt']}, '/working')
def test_collect_patterns_reads_config_exclude_from_file():
flexmock(module).should_receive('expand_directory').with_args(
'file1.txt', '/working'
).and_return(['file1.txt'])
flexmock(module).should_receive('expand_directory').with_args(
'file2.txt', '/working'
).and_return(['file2.txt'])
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('file1.txt', encoding='utf-8').and_return(
io.StringIO('/foo')
@@ -134,7 +153,7 @@ def test_collect_patterns_reads_config_exclude_from_file():
default_style=Pattern_style.FNMATCH,
).and_return(Pattern('/baz', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH))
assert module.collect_patterns({'exclude_from': ['file1.txt', 'file2.txt']}) == (
assert module.collect_patterns({'exclude_from': ['file1.txt', 'file2.txt']}, '/working') == (
Pattern('/foo', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH),
Pattern('/bar', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH),
Pattern('/baz', Pattern_type.NO_RECURSE, Pattern_style.FNMATCH),
@@ -142,12 +161,15 @@ def test_collect_patterns_reads_config_exclude_from_file():
def test_collect_patterns_errors_on_missing_config_exclude_from_file():
flexmock(module).should_receive('expand_directory').with_args(
'file1.txt', '/working'
).and_return(['file1.txt'])
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('file1.txt', encoding='utf-8').and_raise(OSError)
flexmock(module).should_receive('parse_pattern').never()
with pytest.raises(ValueError):
module.collect_patterns({'exclude_from': ['file1.txt', 'file2.txt']})
module.collect_patterns({'exclude_from': ['file1.txt', 'file2.txt']}, '/working')
def test_expand_directory_with_basic_path_passes_it_through():
+5
View File
@@ -1175,6 +1175,7 @@ def test_run_restore_restores_each_data_source():
}
borgmatic_runtime_directory = flexmock()
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
borgmatic_runtime_directory,
)
@@ -1249,6 +1250,7 @@ def test_run_restore_restores_data_source_by_falling_back_to_all_name():
}
borgmatic_runtime_directory = flexmock()
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
borgmatic_runtime_directory,
)
@@ -1311,6 +1313,7 @@ def test_run_restore_restores_data_source_configured_with_all_name():
}
borgmatic_runtime_directory = flexmock()
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
borgmatic_runtime_directory,
)
@@ -1395,6 +1398,7 @@ def test_run_restore_skips_missing_data_source():
}
borgmatic_runtime_directory = flexmock()
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
borgmatic_runtime_directory,
)
@@ -1479,6 +1483,7 @@ def test_run_restore_restores_data_sources_from_different_hooks():
}
borgmatic_runtime_directory = flexmock()
flexmock(module.borgmatic.borg.repo_info).should_receive('get_repository_id').and_return('id')
flexmock(module.borgmatic.config.paths).should_receive('Runtime_directory').and_return(
borgmatic_runtime_directory,
)
-48
View File
@@ -272,54 +272,6 @@ def test_make_check_name_flags_with_extract_omits_extract_flag():
assert flags == ()
def test_get_repository_id_with_valid_json_does_not_raise():
config = {}
flexmock(module.repo_info).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}',
)
assert module.get_repository_id(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
global_arguments=flexmock(),
local_path='borg',
remote_path=None,
)
def test_get_repository_id_with_json_error_raises():
config = {}
flexmock(module.repo_info).should_receive('display_repository_info').and_return(
'{"unexpected": {"id": "repo"}}',
)
with pytest.raises(ValueError):
module.get_repository_id(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
global_arguments=flexmock(),
local_path='borg',
remote_path=None,
)
def test_get_repository_id_with_missing_json_keys_raises():
config = {}
flexmock(module.repo_info).should_receive('display_repository_info').and_return('{invalid JSON')
with pytest.raises(ValueError):
module.get_repository_id(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
global_arguments=flexmock(),
local_path='borg',
remote_path=None,
)
def test_check_archives_with_progress_passes_through_to_borg():
config = {'progress': True}
flexmock(module).should_receive('make_check_name_flags').with_args(
+21 -4
View File
@@ -34,19 +34,36 @@ def test_write_patterns_file_appends_to_existing():
def test_check_all_root_patterns_exist_with_existent_pattern_path_does_not_raise():
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.os.path).should_receive('exists').with_args('foo').and_return(True)
module.check_all_root_patterns_exist([Pattern('foo')])
module.check_all_root_patterns_exist([Pattern('foo')], working_directory=None)
def test_check_all_root_patterns_exist_with_existent_relative_pattern_path_and_working_directory_does_not_raise():
flexmock(module.os.path).should_receive('exists').with_args('foo').never()
flexmock(module.os.path).should_receive('exists').with_args('/working/foo').and_return(True)
module.check_all_root_patterns_exist([Pattern('foo')], working_directory='/working')
def test_check_all_root_patterns_exist_with_non_root_pattern_skips_existence_check():
flexmock(module.os.path).should_receive('exists').never()
module.check_all_root_patterns_exist([Pattern('foo', Pattern_type.INCLUDE)])
module.check_all_root_patterns_exist(
[Pattern('foo', Pattern_type.INCLUDE)], working_directory=None
)
def test_check_all_root_patterns_exist_with_non_existent_pattern_path_raises():
flexmock(module.os.path).should_receive('exists').and_return(False)
with pytest.raises(ValueError):
module.check_all_root_patterns_exist([Pattern('foo')])
module.check_all_root_patterns_exist([Pattern('foo')], working_directory=None)
def test_check_all_root_patterns_exist_with_non_existent_relative_pattern_path_and_working_directory_raises():
flexmock(module.os.path).should_receive('exists').with_args('foo').never()
flexmock(module.os.path).should_receive('exists').with_args('/working/foo').and_return(False)
with pytest.raises(ValueError):
module.check_all_root_patterns_exist([Pattern('foo')], working_directory='/working')
+49
View File
@@ -1,5 +1,6 @@
import logging
import pytest
from flexmock import flexmock
from borgmatic.borg import repo_info as module
@@ -612,3 +613,51 @@ def test_display_repository_info_calls_borg_with_working_directory():
repo_info_arguments=flexmock(json=False),
global_arguments=flexmock(),
)
def test_get_repository_id_with_valid_json_does_not_raise():
config = {}
flexmock(module).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}',
)
assert module.get_repository_id(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
global_arguments=flexmock(),
local_path='borg',
remote_path=None,
)
def test_get_repository_id_with_json_error_raises():
config = {}
flexmock(module).should_receive('display_repository_info').and_return(
'{"unexpected": {"id": "repo"}}',
)
with pytest.raises(ValueError):
module.get_repository_id(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
global_arguments=flexmock(),
local_path='borg',
remote_path=None,
)
def test_get_repository_id_with_missing_json_keys_raises():
config = {}
flexmock(module).should_receive('display_repository_info').and_return('{invalid JSON')
with pytest.raises(ValueError):
module.get_repository_id(
repository_path='repo',
config=config,
local_borg_version='1.2.3',
global_arguments=flexmock(),
local_path='borg',
remote_path=None,
)
+81 -107
View File
@@ -62,65 +62,58 @@ def test_replace_temporary_subdirectory_with_glob_uses_custom_temporary_director
)
def test_fixed_name_temporary_directory_cleanup_does_not_raise():
flexmock(module.shutil).should_receive('rmtree')
module.Fixed_name_temporary_directory('/path').cleanup()
def test_runtime_directory_uses_config_option():
flexmock(module).should_receive('Fixed_name_temporary_directory').and_return(
flexmock(cleanup=lambda: None)
)
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os).should_receive('makedirs')
config = {'user_runtime_directory': '/run', 'borgmatic_source_directory': '/nope'}
with module.Runtime_directory(config) as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/./borgmatic'
def test_runtime_directory_uses_config_option_without_adding_duplicate_borgmatic_subdirectory():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os).should_receive('makedirs')
config = {'user_runtime_directory': '/run/borgmatic', 'borgmatic_source_directory': '/nope'}
with module.Runtime_directory(config) as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/./borgmatic'
with module.Runtime_directory(config, repository_id='id') as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/borgmatic-id/./borgmatic'
def test_runtime_directory_with_relative_config_option_errors():
flexmock(module.os).should_receive('makedirs').never()
config = {'user_runtime_directory': 'run', 'borgmatic_source_directory': '/nope'}
with pytest.raises(ValueError), module.Runtime_directory(config):
with pytest.raises(ValueError), module.Runtime_directory(config, repository_id='id'):
pass
def test_runtime_directory_falls_back_to_xdg_runtime_dir():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(
'/run',
def test_runtime_directory_falls_back_to_xdg_runtime_dir(monkeypatch):
flexmock(module).should_receive('Fixed_name_temporary_directory').and_return(
flexmock(cleanup=lambda: None)
)
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
monkeypatch.setenv('XDG_RUNTIME_DIR', '/run')
flexmock(module.os).should_receive('makedirs')
with module.Runtime_directory({}) as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/./borgmatic'
with module.Runtime_directory({}, repository_id='id') as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/borgmatic-id/./borgmatic'
def test_runtime_directory_falls_back_to_xdg_runtime_dir_without_adding_duplicate_borgmatic_subdirectory():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(
'/run/borgmatic',
)
flexmock(module.os).should_receive('makedirs')
with module.Runtime_directory({}) as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/./borgmatic'
def test_runtime_directory_with_relative_xdg_runtime_dir_errors():
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return('run')
def test_runtime_directory_with_relative_xdg_runtime_dir_errors(monkeypatch):
monkeypatch.setenv('XDG_RUNTIME_DIR', 'run')
flexmock(module.os).should_receive('makedirs').never()
with pytest.raises(ValueError), module.Runtime_directory({}):
with pytest.raises(ValueError), module.Runtime_directory({}, repository_id='id'):
pass
def test_runtime_directory_falls_back_to_runtime_directory():
def test_runtime_directory_falls_back_to_runtime_directory(monkeypatch):
flexmock(module).should_receive('Fixed_name_temporary_directory').and_return(
flexmock(cleanup=lambda: None)
)
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
@@ -128,26 +121,12 @@ def test_runtime_directory_falls_back_to_runtime_directory():
)
flexmock(module.os).should_receive('makedirs')
with module.Runtime_directory({}) as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/./borgmatic'
with module.Runtime_directory({}, repository_id='id') as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/borgmatic-id/./borgmatic'
def test_runtime_directory_falls_back_to_runtime_directory_without_adding_duplicate_borgmatic_subdirectory():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
'/run/borgmatic',
)
flexmock(module.os).should_receive('makedirs')
with module.Runtime_directory({}) as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/./borgmatic'
def test_runtime_directory_with_relative_runtime_directory_errors():
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
def test_runtime_directory_with_relative_runtime_directory_errors(monkeypatch):
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
@@ -155,19 +134,21 @@ def test_runtime_directory_with_relative_runtime_directory_errors():
)
flexmock(module.os).should_receive('makedirs').never()
with pytest.raises(ValueError), module.Runtime_directory({}):
with pytest.raises(ValueError), module.Runtime_directory({}, repository_id='id'):
pass
def test_runtime_directory_falls_back_to_tmpdir_and_adds_temporary_subdirectory_that_get_cleaned_up():
def test_runtime_directory_falls_back_to_tmpdir_and_adds_temporary_subdirectory_that_get_cleaned_up(
monkeypatch,
):
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
None,
)
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return('/run')
monkeypatch.setenv('TMPDIR', '/run')
temporary_directory = flexmock(name='/run/borgmatic-1234')
temporary_directory.should_receive('cleanup').once()
flexmock(module.tempfile).should_receive('TemporaryDirectory').with_args(
@@ -176,35 +157,37 @@ def test_runtime_directory_falls_back_to_tmpdir_and_adds_temporary_subdirectory_
).and_return(temporary_directory)
flexmock(module.os).should_receive('makedirs')
with module.Runtime_directory({}) as borgmatic_runtime_directory:
with module.Runtime_directory({}, repository_id='id') as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/borgmatic-1234/./borgmatic'
def test_runtime_directory_with_relative_tmpdir_errors():
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
def test_runtime_directory_with_relative_tmpdir_errors(monkeypatch):
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
None,
)
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return('run')
monkeypatch.setenv('TMPDIR', 'run')
flexmock(module.tempfile).should_receive('TemporaryDirectory').never()
flexmock(module.os).should_receive('makedirs').never()
with pytest.raises(ValueError), module.Runtime_directory({}):
with pytest.raises(ValueError), module.Runtime_directory({}, repository_id='id'):
pass
def test_runtime_directory_falls_back_to_temp_and_adds_temporary_subdirectory_that_get_cleaned_up():
def test_runtime_directory_falls_back_to_temp_and_adds_temporary_subdirectory_that_get_cleaned_up(
monkeypatch,
):
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
None,
)
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
flexmock(module.os.environ).should_receive('get').with_args('TEMP').and_return('/run')
monkeypatch.delenv('TMPDIR', raising=False)
monkeypatch.setenv('TEMP', '/run')
temporary_directory = flexmock(name='/run/borgmatic-1234')
temporary_directory.should_receive('cleanup').once()
flexmock(module.tempfile).should_receive('TemporaryDirectory').with_args(
@@ -213,36 +196,38 @@ def test_runtime_directory_falls_back_to_temp_and_adds_temporary_subdirectory_th
).and_return(temporary_directory)
flexmock(module.os).should_receive('makedirs')
with module.Runtime_directory({}) as borgmatic_runtime_directory:
with module.Runtime_directory({}, repository_id='id') as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/run/borgmatic-1234/./borgmatic'
def test_runtime_directory_with_relative_temp_errors():
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
def test_runtime_directory_with_relative_temp_errors(monkeypatch):
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
None,
)
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
flexmock(module.os.environ).should_receive('get').with_args('TEMP').and_return('run')
monkeypatch.delenv('TMPDIR', raising=False)
monkeypatch.setenv('TEMP', 'run')
flexmock(module.tempfile).should_receive('TemporaryDirectory').never()
flexmock(module.os).should_receive('makedirs')
with pytest.raises(ValueError), module.Runtime_directory({}):
with pytest.raises(ValueError), module.Runtime_directory({}, repository_id='id'):
pass
def test_runtime_directory_falls_back_to_hard_coded_tmp_path_and_adds_temporary_subdirectory_that_get_cleaned_up():
def test_runtime_directory_falls_back_to_hard_coded_tmp_path_and_adds_temporary_subdirectory_that_get_cleaned_up(
monkeypatch,
):
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
None,
)
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
flexmock(module.os.environ).should_receive('get').with_args('TEMP').and_return(None)
monkeypatch.delenv('TMPDIR', raising=False)
monkeypatch.delenv('TEMP', raising=False)
temporary_directory = flexmock(name='/tmp/borgmatic-1234')
temporary_directory.should_receive('cleanup').once()
flexmock(module.tempfile).should_receive('TemporaryDirectory').with_args(
@@ -251,20 +236,20 @@ def test_runtime_directory_falls_back_to_hard_coded_tmp_path_and_adds_temporary_
).and_return(temporary_directory)
flexmock(module.os).should_receive('makedirs')
with module.Runtime_directory({}) as borgmatic_runtime_directory:
with module.Runtime_directory({}, repository_id='id') as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/tmp/borgmatic-1234/./borgmatic'
def test_runtime_directory_with_erroring_cleanup_does_not_raise():
def test_runtime_directory_with_erroring_cleanup_does_not_raise(monkeypatch):
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_RUNTIME_DIR').and_return(None)
monkeypatch.delenv('XDG_RUNTIME_DIR', raising=False)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.RUNTIME_DIRECTORY
).and_return(
None,
)
flexmock(module.os.environ).should_receive('get').with_args('TMPDIR').and_return(None)
flexmock(module.os.environ).should_receive('get').with_args('TEMP').and_return(None)
monkeypatch.delenv('TMPDIR', raising=False)
monkeypatch.delenv('TEMP', raising=False)
temporary_directory = flexmock(name='/tmp/borgmatic-1234')
temporary_directory.should_receive('cleanup').and_raise(OSError).once()
flexmock(module.tempfile).should_receive('TemporaryDirectory').with_args(
@@ -273,7 +258,7 @@ def test_runtime_directory_with_erroring_cleanup_does_not_raise():
).and_return(temporary_directory)
flexmock(module.os).should_receive('makedirs')
with module.Runtime_directory({}) as borgmatic_runtime_directory:
with module.Runtime_directory({}, repository_id='id') as borgmatic_runtime_directory:
assert borgmatic_runtime_directory == '/tmp/borgmatic-1234/./borgmatic'
@@ -291,7 +276,6 @@ def test_make_runtime_directory_glob(borgmatic_runtime_directory, expected_glob)
def test_get_borgmatic_state_directory_uses_config_option():
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').never()
assert (
module.get_borgmatic_state_directory(
@@ -301,16 +285,16 @@ def test_get_borgmatic_state_directory_uses_config_option():
)
def test_get_borgmatic_state_directory_falls_back_to_xdg_state_home():
def test_get_borgmatic_state_directory_falls_back_to_xdg_state_home(monkeypatch):
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_STATE_HOME').and_return('/tmp')
monkeypatch.setenv('XDG_STATE_HOME', '/tmp')
assert module.get_borgmatic_state_directory({}) == '/tmp/borgmatic'
def test_get_borgmatic_state_directory_falls_back_to_state_directory():
def test_get_borgmatic_state_directory_falls_back_to_state_directory(monkeypatch):
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').with_args('XDG_STATE_HOME').and_return(None)
monkeypatch.delenv('XDG_STATE_HOME', raising=False)
flexmock(module).should_receive('resolve_systemd_directory').with_args(
module.Systemd_directories.STATE_DIRECTORY
).and_return(
@@ -320,32 +304,26 @@ def test_get_borgmatic_state_directory_falls_back_to_state_directory():
assert module.get_borgmatic_state_directory({}) == '/tmp/borgmatic'
def test_get_borgmatic_state_directory_defaults_to_hard_coded_path():
def test_get_borgmatic_state_directory_defaults_to_hard_coded_path(monkeypatch):
flexmock(module).should_receive('expand_user_in_path').replace_with(lambda path: path)
flexmock(module.os.environ).should_receive('get').and_return(None)
monkeypatch.delenv('XDG_STATE_HOME', raising=False)
assert module.get_borgmatic_state_directory({}) == '~/.local/state/borgmatic'
def test_resolve_systemd_directory_none():
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
None
)
flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return(None)
def test_resolve_systemd_directory_none(monkeypatch):
monkeypatch.delenv('RUNTIME_DIRECTORY', raising=False)
monkeypatch.delenv('STATE_DIRECTORY', raising=False)
assert module.resolve_systemd_directory(module.Systemd_directories.RUNTIME_DIRECTORY) is None
assert module.resolve_systemd_directory(module.Systemd_directories.STATE_DIRECTORY) is None
def test_resolve_systemd_directory_single():
def test_resolve_systemd_directory_single(monkeypatch):
runtime_dir = '/run/borgmatic'
state_dir = '/var/lib/borgmatic'
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
runtime_dir
)
flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return(
state_dir
)
monkeypatch.setenv('RUNTIME_DIRECTORY', runtime_dir)
monkeypatch.setenv('STATE_DIRECTORY', state_dir)
assert (
module.resolve_systemd_directory(module.Systemd_directories.RUNTIME_DIRECTORY)
@@ -354,16 +332,12 @@ def test_resolve_systemd_directory_single():
assert module.resolve_systemd_directory(module.Systemd_directories.STATE_DIRECTORY) == state_dir
def test_resolve_systemd_directory_multiple():
def test_resolve_systemd_directory_multiple(monkeypatch):
runtime_dirs = '/run/borgmatic:/run/second:/run/third'
state_dirs = '/var/lib/borgmatic:/var/lib/second:/var/lib/third'
flexmock(module.os.environ).should_receive('get').with_args('RUNTIME_DIRECTORY').and_return(
runtime_dirs
)
flexmock(module.os.environ).should_receive('get').with_args('STATE_DIRECTORY').and_return(
state_dirs
)
monkeypatch.setenv('RUNTIME_DIRECTORY', runtime_dirs)
monkeypatch.setenv('STATE_DIRECTORY', state_dirs)
assert (
module.resolve_systemd_directory(module.Systemd_directories.RUNTIME_DIRECTORY)
@@ -31,6 +31,55 @@ def test_load_credential_with_missing_database_raises():
)
def test_load_credential_with_secret_service_database_path_fetches_password_via_secret_tool():
flexmock(module.os.path).should_receive('expanduser').never()
flexmock(module.os.path).should_receive('exists').never()
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output',
).with_args(
(
'secret-tool',
'lookup',
'Path',
'mypassword',
),
).and_yield('password').once()
assert (
module.load_credential(
hook_config={},
config={},
credential_parameters=('secret-service', 'mypassword'),
)
== 'password'
)
def test_load_credential_with_secret_service_database_path_and_secret_tool_command_calls_it():
flexmock(module.os.path).should_receive('expanduser').never()
flexmock(module.os.path).should_receive('exists').never()
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output',
).with_args(
(
'/usr/local/bin/secret-tool',
'--some-option',
'lookup',
'Path',
'mypassword',
),
).and_yield('password').once()
assert (
module.load_credential(
hook_config={'secret_tool_command': '/usr/local/bin/secret-tool --some-option'},
config={},
credential_parameters=('secret-service', 'mypassword'),
)
== 'password'
)
def test_load_credential_with_present_database_fetches_password_from_keepassxc():
flexmock(module.os.path).should_receive('expanduser').with_args('database.kdbx').and_return(
'database.kdbx',
+102 -6
View File
@@ -64,6 +64,23 @@ def test_make_defaults_file_escapes_password_containing_backslash():
)
def test_make_defaults_file_escapes_password_containing_quote():
read_descriptor = 99
write_descriptor = flexmock()
flexmock(module.os).should_receive('pipe').and_return(read_descriptor, write_descriptor)
flexmock(module.os).should_receive('write').with_args(
write_descriptor,
b'[client]\nuser=root\n' + rb'password="trust\"some1"',
).once()
flexmock(module.os).should_receive('close')
flexmock(module.os).should_receive('set_inheritable')
assert module.make_defaults_file_options(username='root', password=r'trust"some1') == (
'--defaults-extra-file=/dev/fd/99',
)
def test_make_defaults_file_pipe_with_only_username_writes_it_to_file_descriptor():
read_descriptor = 99
write_descriptor = flexmock()
@@ -201,7 +218,7 @@ def test_database_names_to_dump_queries_mariadb_for_database_names():
),
environment=environment,
working_directory='/path/to/working/dir',
).and_yield('foo', 'bar', 'mysql').once()
).and_yield('foo', 'bar').once()
names = module.database_names_to_dump(
{'name': 'all'},
@@ -239,7 +256,7 @@ def test_database_names_to_dump_with_database_name_all_and_skip_names_filters_ou
),
environment=environment,
working_directory=None,
).and_yield('foo', 'bar', 'baz', 'mysql').once()
).and_yield('foo', 'bar', 'baz').once()
names = module.database_names_to_dump(
{'name': 'all', 'skip_names': ('foo', 'bar')},
@@ -279,7 +296,7 @@ def test_database_names_to_dump_runs_mariadb_with_socket_path():
),
environment=environment,
working_directory=None,
).and_yield('foo', 'bar', 'mysql').once()
).and_yield('foo', 'bar').once()
names = module.database_names_to_dump(
{'name': 'all', 'socket_path': '/socket'},
@@ -314,7 +331,7 @@ def test_database_names_to_dump_with_environment_password_transport_skips_defaul
),
environment=environment,
working_directory=None,
).and_yield('foo', 'bar', 'mysql').once()
).and_yield('foo', 'bar').once()
names = module.database_names_to_dump(
{'name': 'all', 'password_transport': 'environment'},
@@ -353,7 +370,7 @@ def test_database_names_to_dump_runs_mariadb_with_tls():
),
environment=environment,
working_directory=None,
).and_yield('foo', 'bar', 'mysql').once()
).and_yield('foo', 'bar').once()
names = module.database_names_to_dump(
{'name': 'all', 'tls': True},
@@ -392,7 +409,7 @@ def test_database_names_to_dump_runs_mariadb_without_tls():
),
environment=environment,
working_directory=None,
).and_yield('foo', 'bar', 'mysql').once()
).and_yield('foo', 'bar').once()
names = module.database_names_to_dump(
{'name': 'all', 'tls': False},
@@ -839,6 +856,9 @@ def test_execute_dump_command_runs_mariadb_dump():
'--add-drop-database',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'foo',
'--result-file',
'dump',
@@ -864,6 +884,58 @@ def test_execute_dump_command_runs_mariadb_dump():
)
def test_execute_dump_command_substitutes_system_flag_for_system_database_name():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module).should_receive('parse_extra_options').and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module).should_receive('make_defaults_file_options').with_args(
'root',
'trustsome1',
None,
).and_return(('--defaults-extra-file=/dev/fd/99',))
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command').with_args(
(
'mariadb-dump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--system=users,udfs,servers',
'--result-file',
'dump',
),
environment=None,
run_to_completion=False,
working_directory=None,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'mysql'},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('mysql',),
environment=None,
dry_run=False,
dry_run_label='',
)
== process
)
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')
@@ -885,6 +957,9 @@ def test_execute_dump_command_with_environment_password_transport_skips_defaults
'--user',
'root',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'foo',
'--result-file',
'dump',
@@ -933,6 +1008,9 @@ def test_execute_dump_command_runs_mariadb_dump_without_add_drop_database():
'--defaults-extra-file=/dev/fd/99',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'foo',
'--result-file',
'dump',
@@ -990,6 +1068,9 @@ def test_execute_dump_command_runs_mariadb_dump_with_hostname_and_port():
'--protocol',
'tcp',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'foo',
'--result-file',
'dump',
@@ -1040,6 +1121,9 @@ def test_execute_dump_command_runs_mariadb_dump_with_tls():
'--single-transaction',
'--ssl',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'foo',
'--result-file',
'dump',
@@ -1090,6 +1174,9 @@ def test_execute_dump_command_runs_mariadb_dump_without_tls():
'--single-transaction',
'--skip-ssl',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'foo',
'--result-file',
'dump',
@@ -1139,6 +1226,9 @@ def test_execute_dump_command_runs_mariadb_dump_with_username_and_password():
'--add-drop-database',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'foo',
'--result-file',
'dump',
@@ -1189,6 +1279,9 @@ def test_execute_dump_command_runs_mariadb_dump_with_options():
'--add-drop-database',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'foo',
'--result-file',
'dump',
@@ -1239,6 +1332,9 @@ def test_execute_dump_command_runs_non_default_mariadb_dump_with_options():
'--add-drop-database',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'foo',
'--result-file',
'dump',
@@ -719,6 +719,9 @@ def test_execute_dump_command_runs_mysqldump():
'--add-drop-database',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'foo',
'--result-file',
'dump',
@@ -769,6 +772,9 @@ def test_execute_dump_command_with_environment_password_transport_skips_defaults
'--user',
'root',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'foo',
'--result-file',
'dump',
@@ -817,6 +823,9 @@ def test_execute_dump_command_runs_mysqldump_without_add_drop_database():
'--defaults-extra-file=/dev/fd/99',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'foo',
'--result-file',
'dump',
@@ -874,6 +883,9 @@ def test_execute_dump_command_runs_mysqldump_with_hostname_and_port():
'--protocol',
'tcp',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'foo',
'--result-file',
'dump',
@@ -924,6 +936,9 @@ def test_execute_dump_command_runs_mysqldump_with_tls():
'--single-transaction',
'--ssl',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'foo',
'--result-file',
'dump',
@@ -974,6 +989,9 @@ def test_execute_dump_command_runs_mysqldump_without_tls():
'--single-transaction',
'--skip-ssl',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'foo',
'--result-file',
'dump',
@@ -1023,6 +1041,9 @@ def test_execute_dump_command_runs_mysqldump_with_username_and_password():
'--add-drop-database',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'foo',
'--result-file',
'dump',
@@ -1073,6 +1094,9 @@ def test_execute_dump_command_runs_mysqldump_with_options():
'--add-drop-database',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'foo',
'--result-file',
'dump',
@@ -1122,6 +1146,9 @@ def test_execute_dump_command_runs_non_default_mysqldump():
'--add-drop-database',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'foo',
'--result-file',
'dump',
@@ -172,13 +172,22 @@ def test_database_names_to_dump_with_all_and_format_lists_databases_with_usernam
def test_database_names_to_dump_with_all_and_format_lists_databases_with_options():
database = {'name': 'all', 'format': 'custom', 'list_options': '--harder'}
database = {'name': 'all', 'format': 'custom', 'list_options': '--harder "foo bar"'}
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
('psql', '--list', '--no-password', '--no-psqlrc', '--csv', '--tuples-only', '--harder'),
(
'psql',
'--list',
'--no-password',
'--no-psqlrc',
'--csv',
'--tuples-only',
'--harder',
"'foo bar'",
),
environment=object,
working_directory=None,
).and_yield('foo,test,', 'bar,test,"stuff and such"')
@@ -803,7 +812,7 @@ def test_dump_data_sources_runs_pg_dump_with_integer_compression():
def test_dump_data_sources_runs_pg_dump_with_options():
databases = [{'name': 'foo', 'options': '--stuff=such'}]
databases = [{'name': 'foo', 'options': '--stuff "foo bar"'}]
process = flexmock()
flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'})
flexmock(module).should_receive('make_dump_path').and_return('')
@@ -826,7 +835,8 @@ def test_dump_data_sources_runs_pg_dump_with_options():
'--if-exists',
'--format',
'custom',
'--stuff=such',
'--stuff',
"'foo bar'",
'foo',
'>',
'databases/localhost/foo',
@@ -1340,8 +1350,8 @@ def test_restore_data_source_dump_runs_pg_restore_with_options():
hook_config = [
{
'name': 'foo',
'restore_options': '--harder',
'analyze_options': '--smarter',
'restore_options': '--harder "foo bar"',
'analyze_options': '--smarter "baz quux"',
'schemas': None,
},
]
@@ -1364,6 +1374,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_options():
'--dbname',
'foo',
'--harder',
"'foo bar'",
),
processes=[extract_process],
output_log_level=logging.DEBUG,
@@ -1381,6 +1392,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_options():
'--dbname',
'foo',
'--smarter',
"'baz quux'",
'--command',
'ANALYZE',
),
+15 -12
View File
@@ -699,7 +699,7 @@ def test_configure_logging_skips_log_file_if_log_file_logging_is_disabled():
)
def test_configure_logging_to_log_file_instead_of_syslog():
def test_configure_logging_to_log_file_instead_of_syslog(tmp_path):
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.ANSWER
fake_formatter = flexmock()
@@ -716,20 +716,21 @@ def test_configure_logging_to_log_file_instead_of_syslog():
)
flexmock(module.os.path).should_receive('exists').never()
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
file_handler = logging.handlers.WatchedFileHandler('/tmp/logfile')
log_file = str(tmp_path / 'logfile')
file_handler = logging.handlers.WatchedFileHandler(log_file)
flexmock(module.logging.handlers).should_receive('WatchedFileHandler').with_args(
'/tmp/logfile',
log_file,
).and_return(file_handler).once()
module.configure_logging(
console_log_level=logging.INFO,
syslog_log_level=logging.DISABLED,
log_file_log_level=logging.DEBUG,
log_file='/tmp/logfile',
log_file=log_file,
)
def test_configure_logging_to_both_log_file_and_syslog():
def test_configure_logging_to_both_log_file_and_syslog(tmp_path):
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.ANSWER
fake_formatter = flexmock()
@@ -752,20 +753,21 @@ def test_configure_logging_to_both_log_file_and_syslog():
flexmock(module.logging.handlers).should_receive('SysLogHandler').with_args(
address='/dev/log',
).and_return(syslog_handler).once()
file_handler = logging.handlers.WatchedFileHandler('/tmp/logfile')
log_file = str(tmp_path / 'logfile')
file_handler = logging.handlers.WatchedFileHandler(log_file)
flexmock(module.logging.handlers).should_receive('WatchedFileHandler').with_args(
'/tmp/logfile',
log_file,
).and_return(file_handler).once()
module.configure_logging(
console_log_level=logging.INFO,
syslog_log_level=logging.DEBUG,
log_file_log_level=logging.DEBUG,
log_file='/tmp/logfile',
log_file=log_file,
)
def test_configure_logging_to_log_file_formats_with_custom_log_format():
def test_configure_logging_to_log_file_formats_with_custom_log_format(tmp_path):
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.ANSWER
flexmock(module).should_receive('Log_prefix_formatter').with_args(
@@ -786,15 +788,16 @@ def test_configure_logging_to_log_file_formats_with_custom_log_format():
)
flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(True)
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
file_handler = logging.handlers.WatchedFileHandler('/tmp/logfile')
log_file = str(tmp_path / 'logfile')
file_handler = logging.handlers.WatchedFileHandler(log_file)
flexmock(module.logging.handlers).should_receive('WatchedFileHandler').with_args(
'/tmp/logfile',
log_file,
).and_return(file_handler).once()
module.configure_logging(
console_log_level=logging.INFO,
log_file_log_level=logging.DEBUG,
log_file='/tmp/logfile',
log_file=log_file,
log_file_format='{message}',
)