mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-24 19:03:02 +02:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afeaa86cbf | ||
|
|
7e7c3c273e | ||
|
|
704db9c9d7 | ||
|
|
b6ab326bb4 | ||
|
|
aaf5812a3f | ||
|
|
47a6691886 | ||
|
|
596d59ef60 | ||
|
|
9839b3dada | ||
|
|
411685280c | ||
|
|
cce679248f | ||
|
|
68c9516424 | ||
|
|
a508cabe3f | ||
|
|
0e1659bd73 | ||
|
|
ad61ad356e | ||
|
|
5c7d03910b | ||
|
|
468af1de0b | ||
|
|
524d8263a7 | ||
|
|
1d2aea0951 | ||
|
|
961ff7c724 | ||
|
|
3e80056956 | ||
|
|
d8f558ce0d | ||
|
|
9da75fdc58 | ||
|
|
ac9c8bb644 | ||
|
|
0b34ef0e1a |
@@ -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
|
||||
|
||||
@@ -1,3 +1,32 @@
|
||||
2.1.6.dev0
|
||||
* #1256: Fix a race condition in which borgmatic sometimes swallows Borg error output without
|
||||
logging it.
|
||||
* #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/
|
||||
* #1308: Update the Apprise monitoring hook's "url" option to support loading credentials with the
|
||||
"{credential ...}" syntax. See the documentation for more information:
|
||||
https://torsion.org/borgmatic/reference/configuration/credentials/
|
||||
* #1309: Add a "--quick-stats" flag and corresponding "quick_statistics" option for showing only
|
||||
abbreviated statistics for the "create" action. Borg 1.4.5+ only.
|
||||
* 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/
|
||||
|
||||
@@ -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
@@ -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.14 # 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.34.0 # 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.7.0 # via requests, -r binary_requirements.in
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import argparse
|
||||
import datetime
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import borgmatic.borg.extract
|
||||
import borgmatic.borg.info
|
||||
import borgmatic.borg.repo_list
|
||||
import borgmatic.config.paths
|
||||
|
||||
@@ -100,6 +104,39 @@ def run_bootstrap(bootstrap_arguments, global_arguments, local_borg_version):
|
||||
Raise CalledProcessError or OSError if Borg could not be run.
|
||||
'''
|
||||
config = make_bootstrap_config(bootstrap_arguments)
|
||||
|
||||
if bootstrap_arguments.archive == 'latest':
|
||||
archives_data = json.loads(
|
||||
borgmatic.borg.info.display_archives_info(
|
||||
bootstrap_arguments.repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
info_arguments=argparse.Namespace(archive=None, prefix=None, json=True),
|
||||
global_arguments=global_arguments,
|
||||
local_path=bootstrap_arguments.local_path,
|
||||
remote_path=bootstrap_arguments.remote_path,
|
||||
)
|
||||
)['archives']
|
||||
|
||||
get_repo_archive_format = lambda archive_data: archive_data['command_line'][-1]
|
||||
get_archive_start = lambda archive_data: datetime.datetime.fromisoformat(
|
||||
archive_data['start']
|
||||
)
|
||||
|
||||
latest_archives = {
|
||||
repo_archive_format: max(archives_data, key=get_archive_start)['name']
|
||||
for repo_archive_format, archives_data in itertools.groupby(
|
||||
sorted(archives_data, key=get_repo_archive_format),
|
||||
key=get_repo_archive_format,
|
||||
)
|
||||
if not repo_archive_format.endswith('checkpoint')
|
||||
}
|
||||
|
||||
if len(latest_archives) > 1:
|
||||
raise ValueError(
|
||||
f'The repository appears to have multiple "latest" archives, each with a different archive name format: {", ".join(sorted(latest_archives.values()))}. Please select one with --archive.'
|
||||
)
|
||||
|
||||
archive_name = borgmatic.borg.repo_list.resolve_archive_name(
|
||||
bootstrap_arguments.repository,
|
||||
bootstrap_arguments.archive,
|
||||
|
||||
@@ -45,7 +45,7 @@ def run_create(
|
||||
|
||||
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
|
||||
patterns = pattern.process_patterns(
|
||||
pattern.collect_patterns(config),
|
||||
pattern.collect_patterns(config, working_directory),
|
||||
config,
|
||||
working_directory,
|
||||
borgmatic_runtime_directory,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -539,7 +539,7 @@ def run_restore(
|
||||
|
||||
with borgmatic.config.paths.Runtime_directory(config) 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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
@@ -372,7 +374,9 @@ def create_archive(
|
||||
|
||||
if json:
|
||||
output_log_level = None
|
||||
elif config.get('list_details') or (config.get('statistics') and not dry_run):
|
||||
elif config.get('list_details') or (
|
||||
(config.get('statistics') or config.get('quick_statistics')) and not dry_run
|
||||
):
|
||||
output_log_level = logging.ANSWER
|
||||
else:
|
||||
output_log_level = logging.INFO
|
||||
@@ -384,6 +388,11 @@ def create_archive(
|
||||
create_flags += (
|
||||
(('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
|
||||
+ (('--stats',) if config.get('statistics') and not json and not dry_run else ())
|
||||
+ (
|
||||
('--quick-stats',)
|
||||
if config.get('quick_statistics') and not json and not dry_run
|
||||
else ()
|
||||
)
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
|
||||
+ (('--progress',) if config.get('progress') else ())
|
||||
+ (('--json',) if json else ())
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -896,6 +896,13 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
|
||||
action='store_true',
|
||||
help='Display statistics of archive',
|
||||
)
|
||||
create_group.add_argument(
|
||||
'--quick-stats',
|
||||
dest='quick_statistics',
|
||||
default=None,
|
||||
action='store_true',
|
||||
help='Display statistics of archive, skipping repository-wide "All archives" and chunk index statistics [Borg 1.4.5+ only]',
|
||||
)
|
||||
create_group.add_argument(
|
||||
'--list',
|
||||
'--files',
|
||||
|
||||
@@ -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
|
||||
@@ -1088,6 +1091,15 @@ properties:
|
||||
Corresponds to the "--stats" flag on those actions. Defaults to
|
||||
false.
|
||||
example: true
|
||||
quick_statistics:
|
||||
type: boolean
|
||||
description: |
|
||||
Display statistics for an archive when running supported actions,
|
||||
skipping the repository-wide "All archives" and chunk index
|
||||
statistics to save some time. Corresponds to the "--quick-stats"
|
||||
flag on those actions. Defaults to false. (This option is supported
|
||||
for Borg 1.4.5+ only.)
|
||||
example: true
|
||||
list_details:
|
||||
type: boolean
|
||||
description: |
|
||||
@@ -2822,7 +2834,9 @@ properties:
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
description: URL of this Apprise service.
|
||||
description: |
|
||||
URL of this Apprise service. Supports the
|
||||
"{credential ...}" syntax.
|
||||
example: "gotify://hostname/token"
|
||||
label:
|
||||
type: string
|
||||
@@ -3309,24 +3323,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`).
|
||||
|
||||
+31
-7
@@ -372,13 +372,21 @@ def log_buffer_lines(
|
||||
yield log_record.getMessage()
|
||||
|
||||
|
||||
def raise_for_process_errors(buffer_readers, process_metadatas, borg_local_path, borg_exit_codes):
|
||||
def raise_for_process_errors(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level,
|
||||
borg_local_path,
|
||||
borg_exit_codes,
|
||||
capture_stderr=False,
|
||||
):
|
||||
'''
|
||||
Given a dict from buffer object to Buffer_reader, a dict from subprocess.Popen() instance to
|
||||
Process_metadata instance, Borg's local path, a sequence of exit code configuration dicts, check
|
||||
the given processes for error or warning exit codes. If found, vent or kill any running
|
||||
processes. In the case of an error exit code, raise. In the case of warning, return
|
||||
Exit_status.WARNING. Otherwise, return None.
|
||||
Process_metadata instance, a requested output log level for stdout, Borg's local path, a
|
||||
sequence of exit code configuration dicts, and whether to capture stderr, check the given
|
||||
processes for error or warning exit codes. If found, vent or kill any running processes and
|
||||
drain any remaining buffer lines. In the case of an error exit code, raise. In the case of
|
||||
warning, return Exit_status.WARNING. Otherwise, return None.
|
||||
'''
|
||||
result_status = None
|
||||
|
||||
@@ -404,6 +412,13 @@ def raise_for_process_errors(buffer_readers, process_metadatas, borg_local_path,
|
||||
result_status = Exit_status.WARNING
|
||||
continue
|
||||
|
||||
# Drain and log remaining buffer lines, so no output gets lost. But swallow captured output,
|
||||
# since we're raising below instead of yielding captured lines.
|
||||
tuple(
|
||||
log_remaining_buffer_lines(
|
||||
buffer_readers, process_metadatas, output_log_level, borg_local_path, capture_stderr
|
||||
)
|
||||
)
|
||||
last_lines = process_metadatas[process].last_lines
|
||||
|
||||
# If an error occurs, include its output in the raised exception so that we don't
|
||||
@@ -429,6 +444,9 @@ def log_remaining_buffer_lines(
|
||||
whether to capture stderr, drain and log any remaining output lines from the buffers until
|
||||
they're empty. Additionally, for any log records with a log level the same as the output log
|
||||
level, yield those log messages for capture.
|
||||
|
||||
The main difference between this function and log_buffer_lines() is that this one doesn't just
|
||||
do one "turn of the crank" of logging buffer output; it completely drains any remaining output.
|
||||
'''
|
||||
for output_buffer, reader in buffer_readers.items():
|
||||
if not reader.process:
|
||||
@@ -446,6 +464,7 @@ def log_remaining_buffer_lines(
|
||||
borg_local_path=borg_local_path,
|
||||
command=reader.process.args,
|
||||
),
|
||||
last_lines=process_metadatas[reader.process].last_lines,
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -494,7 +513,7 @@ def log_outputs(
|
||||
for buffer in output_buffers_for_process(process, exclude_stdouts)
|
||||
}
|
||||
|
||||
# Log output lines for each process until they all exit.
|
||||
# Log output lines for each process until they all exit or one errors.
|
||||
while True:
|
||||
yield from log_buffer_lines(
|
||||
buffer_readers, process_metadatas, output_log_level, borg_local_path, capture_stderr
|
||||
@@ -502,7 +521,12 @@ def log_outputs(
|
||||
|
||||
if (
|
||||
raise_for_process_errors(
|
||||
buffer_readers, process_metadatas, borg_local_path, borg_exit_codes
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level,
|
||||
borg_local_path,
|
||||
borg_exit_codes,
|
||||
capture_stderr,
|
||||
)
|
||||
== Exit_status.WARNING
|
||||
):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
import logging
|
||||
import operator
|
||||
|
||||
import borgmatic.hooks.credential.parse
|
||||
import borgmatic.hooks.monitoring.logs
|
||||
import borgmatic.hooks.monitoring.monitor
|
||||
|
||||
@@ -76,7 +77,12 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||
logger.info(f'Pinging Apprise services: {labels_string}{dry_run_string}')
|
||||
|
||||
apprise_object = apprise.Apprise()
|
||||
apprise_object.add(list(map(operator.itemgetter('url'), hook_config.get('services'))))
|
||||
apprise_object.add(
|
||||
[
|
||||
borgmatic.hooks.credential.parse.resolve_credential(service['url'], config)
|
||||
for service in hook_config.get('services')
|
||||
]
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -5,9 +5,9 @@ eleventyNavigation:
|
||||
parent: 🚨 Monitoring
|
||||
---
|
||||
<span class="minilink minilink-addedin">New in version 1.8.4</span>
|
||||
[Apprise](https://github.com/caronc/apprise/wiki) is a local notification library
|
||||
[Apprise](https://appriseit.com/) is a local notification library
|
||||
that "allows you to send a notification to almost all of the most popular
|
||||
[notification services](https://github.com/caronc/apprise/wiki) available to
|
||||
[notification services](https://appriseit.com/services/) available to
|
||||
us today such as: Telegram, Discord, Slack, Amazon SNS, Gotify, etc."
|
||||
|
||||
Depending on how you installed borgmatic, it may not have come with Apprise.
|
||||
@@ -22,7 +22,7 @@ sudo uv tool install borgmatic[Apprise]
|
||||
Omit `sudo` if borgmatic is installed as a non-root user.
|
||||
|
||||
Once Apprise is installed, configure borgmatic to notify one or more [Apprise
|
||||
services](https://github.com/caronc/apprise/wiki). For example:
|
||||
services](https://appriseit.com/services/). For example:
|
||||
|
||||
```yaml
|
||||
apprise:
|
||||
|
||||
+40
-6
@@ -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>
|
||||
|
||||
+3
-2
@@ -1,17 +1,18 @@
|
||||
[project]
|
||||
name = "borgmatic"
|
||||
version = "2.1.5"
|
||||
version = "2.1.6.dev0"
|
||||
authors = [
|
||||
{ name="Dan Helfman", email="witten@torsion.org" },
|
||||
]
|
||||
description = "Simple, configuration-driven backup software for servers and workstations"
|
||||
license = "GPL-3.0-or-later"
|
||||
license-files = ["LICENSE"]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: System Administrators",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Programming Language :: Python",
|
||||
"Topic :: Security :: Cryptography",
|
||||
"Topic :: System :: Archiving :: Backup",
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
Executable
+16
@@ -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
|
||||
@@ -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
@@ -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.14.0 # via pytest-cov, -r test_requirements.in
|
||||
flexmock==0.13.0 # via -r test_requirements.in
|
||||
idna==3.14 # 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.34.0 # 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.7.0 # 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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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()},
|
||||
)
|
||||
|
||||
|
||||
@@ -557,6 +557,43 @@ def test_log_outputs_with_unfinished_process_re_polls():
|
||||
)
|
||||
|
||||
|
||||
def test_log_outputs_includes_error_output_when_output_spans_multiple_chunks():
|
||||
flexmock(module.logger).should_receive('log')
|
||||
flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.ERROR)
|
||||
flexmock(module).should_receive('command_for_process').and_return('python')
|
||||
|
||||
process = subprocess.Popen(
|
||||
[
|
||||
sys.executable,
|
||||
'-c',
|
||||
(
|
||||
'import os, sys; '
|
||||
f'os.write(sys.stdout.fileno(), b"x" * {module.READ_CHUNK_SIZE + 10}); '
|
||||
'os.write(sys.stdout.fileno(), b"\\nERROR: critical failure"); '
|
||||
'os.close(1); '
|
||||
'os._exit(2)'
|
||||
),
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
flexmock(module).should_receive('output_buffers_for_process').and_return((process.stdout,))
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError) as error:
|
||||
tuple(
|
||||
module.log_outputs(
|
||||
(process,),
|
||||
exclude_stdouts=(),
|
||||
output_log_level=logging.INFO,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
)
|
||||
|
||||
assert error.value.output
|
||||
assert 'ERROR: critical failure' in error.value.output
|
||||
|
||||
|
||||
def test_read_lines_uses_system_locale_when_decoding_output():
|
||||
flexmock(module.locale).should_receive('getpreferredencoding').and_return('ISO-8859-1')
|
||||
process = subprocess.Popen(['echo', b'\xc4pple'], stdout=subprocess.PIPE)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -1477,6 +1477,44 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats():
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': None,
|
||||
'statistics': True,
|
||||
},
|
||||
patterns=[Pattern('foo'), Pattern('bar')],
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
borgmatic_runtime_directory='/borgmatic/run',
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_quick_stats_and_dry_run_calls_borg_without_quick_stats():
|
||||
# --dry-run and --quick-stats are mutually exclusive, see:
|
||||
# https://borgbackup.readthedocs.io/en/stable/usage/create.html#description
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_base_create_command').and_return(
|
||||
(('borg', 'create', '--dry-run'), REPO_ARCHIVE, flexmock()),
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--dry-run', '--info', *REPO_ARCHIVE),
|
||||
output_log_level=logging.INFO,
|
||||
output_file=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
working_directory=None,
|
||||
environment=None,
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=True,
|
||||
repository_path='repo',
|
||||
config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': None,
|
||||
'quick_statistics': True,
|
||||
},
|
||||
patterns=[Pattern('foo'), Pattern('bar')],
|
||||
local_borg_version='1.2.3',
|
||||
@@ -1806,6 +1844,42 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_flag():
|
||||
'source_directories': ['foo*'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': None,
|
||||
'statistics': True,
|
||||
},
|
||||
patterns=[Pattern('foo'), Pattern('bar')],
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
borgmatic_runtime_directory='/borgmatic/run',
|
||||
json=True,
|
||||
)
|
||||
|
||||
assert json_output == '[]'
|
||||
|
||||
|
||||
def test_create_archive_with_quick_stats_and_json_calls_borg_without_quick_stats_flag():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_base_create_command').and_return(
|
||||
(('borg', 'create'), REPO_ARCHIVE, flexmock()),
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg', 'create', '--json', *REPO_ARCHIVE),
|
||||
working_directory=None,
|
||||
environment=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).and_yield('[]')
|
||||
|
||||
json_output = module.create_archive(
|
||||
dry_run=False,
|
||||
repository_path='repo',
|
||||
config={
|
||||
'source_directories': ['foo*'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': None,
|
||||
'quick_statistics': True,
|
||||
},
|
||||
patterns=[Pattern('foo'), Pattern('bar')],
|
||||
local_borg_version='1.2.3',
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
|
||||
@@ -66,7 +66,9 @@ def test_initialize_monitor_without_send_logs_does_not_add_handler():
|
||||
|
||||
|
||||
def test_ping_monitor_respects_dry_run():
|
||||
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda url, config: url)
|
||||
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
|
||||
'format_buffered_logs_for_payload',
|
||||
).and_return('loggy log')
|
||||
@@ -83,7 +85,9 @@ def test_ping_monitor_respects_dry_run():
|
||||
|
||||
|
||||
def test_ping_monitor_with_no_states_does_not_notify():
|
||||
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler').never()
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda url, config: url)
|
||||
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
|
||||
'format_buffered_logs_for_payload',
|
||||
).never()
|
||||
@@ -100,7 +104,9 @@ def test_ping_monitor_with_no_states_does_not_notify():
|
||||
|
||||
|
||||
def test_ping_monitor_notifies_fail_by_default():
|
||||
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda url, config: url)
|
||||
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
|
||||
'format_buffered_logs_for_payload',
|
||||
).and_return('')
|
||||
@@ -123,7 +129,9 @@ def test_ping_monitor_notifies_fail_by_default():
|
||||
|
||||
|
||||
def test_ping_monitor_with_logs_appends_logs_to_body():
|
||||
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda url, config: url)
|
||||
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
|
||||
'format_buffered_logs_for_payload',
|
||||
).and_return('loggy log')
|
||||
@@ -146,7 +154,9 @@ def test_ping_monitor_with_logs_appends_logs_to_body():
|
||||
|
||||
|
||||
def test_ping_monitor_with_finish_default_config_notifies():
|
||||
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive('get_handler')
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda url, config: url)
|
||||
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
|
||||
'format_buffered_logs_for_payload',
|
||||
).and_return('')
|
||||
|
||||
@@ -779,6 +779,7 @@ def test_raise_for_process_errors_with_no_processes_bails():
|
||||
module.raise_for_process_errors(
|
||||
buffer_readers,
|
||||
process_metadatas={},
|
||||
output_log_level=None,
|
||||
borg_local_path=flexmock(),
|
||||
borg_exit_codes=flexmock(),
|
||||
)
|
||||
@@ -795,6 +796,7 @@ def test_raise_for_process_errors_with_running_process_bails():
|
||||
module.raise_for_process_errors(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=None,
|
||||
borg_local_path=flexmock(),
|
||||
borg_exit_codes=flexmock(),
|
||||
)
|
||||
@@ -812,6 +814,7 @@ def test_raise_for_process_errors_with_running_process_and_no_buffer_readers_wai
|
||||
module.raise_for_process_errors(
|
||||
buffer_readers={},
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=None,
|
||||
borg_local_path=flexmock(),
|
||||
borg_exit_codes=flexmock(),
|
||||
)
|
||||
@@ -829,6 +832,7 @@ def test_raise_for_process_errors_with_successful_process_bails():
|
||||
module.raise_for_process_errors(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=None,
|
||||
borg_local_path=flexmock(),
|
||||
borg_exit_codes=flexmock(),
|
||||
)
|
||||
@@ -846,6 +850,7 @@ def test_raise_for_process_errors_with_warning_process_returns_warning_status():
|
||||
module.raise_for_process_errors(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=None,
|
||||
borg_local_path=flexmock(),
|
||||
borg_exit_codes=flexmock(),
|
||||
)
|
||||
@@ -860,6 +865,7 @@ def test_raise_for_process_errors_with_error_process_raises():
|
||||
process: module.Process_metadata(last_lines=['hi', 'there'], capture=False)
|
||||
}
|
||||
flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.ERROR)
|
||||
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
|
||||
command = flexmock()
|
||||
flexmock(module).should_receive('command_for_process').and_return(command)
|
||||
|
||||
@@ -867,6 +873,7 @@ def test_raise_for_process_errors_with_error_process_raises():
|
||||
module.raise_for_process_errors(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=None,
|
||||
borg_local_path=flexmock(),
|
||||
borg_exit_codes=flexmock(),
|
||||
)
|
||||
@@ -890,12 +897,14 @@ def test_raise_for_process_errors_with_success_process_and_warning_process_retur
|
||||
flexmock(module).should_receive('interpret_exit_code').with_args(
|
||||
object, 1, object, object
|
||||
).and_return(module.Exit_status.WARNING)
|
||||
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
|
||||
flexmock(module).should_receive('command_for_process').never()
|
||||
|
||||
assert (
|
||||
module.raise_for_process_errors(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=None,
|
||||
borg_local_path=flexmock(),
|
||||
borg_exit_codes=flexmock(),
|
||||
)
|
||||
@@ -917,6 +926,7 @@ def test_raise_for_process_errors_with_warning_process_and_error_process_raises(
|
||||
flexmock(module).should_receive('interpret_exit_code').with_args(
|
||||
object, 3, object, object
|
||||
).and_return(module.Exit_status.ERROR)
|
||||
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
|
||||
command = flexmock()
|
||||
flexmock(module).should_receive('command_for_process').and_return(command)
|
||||
|
||||
@@ -924,6 +934,7 @@ def test_raise_for_process_errors_with_warning_process_and_error_process_raises(
|
||||
module.raise_for_process_errors(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=None,
|
||||
borg_local_path=flexmock(),
|
||||
borg_exit_codes=flexmock(),
|
||||
)
|
||||
@@ -947,6 +958,7 @@ def test_raise_for_process_errors_with_warning_process_and_running_process_kills
|
||||
flexmock(module).should_receive('interpret_exit_code').with_args(
|
||||
object, 1, object, object
|
||||
).and_return(module.Exit_status.WARNING)
|
||||
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
|
||||
command = flexmock()
|
||||
flexmock(module).should_receive('command_for_process').and_return(command)
|
||||
|
||||
@@ -954,6 +966,7 @@ def test_raise_for_process_errors_with_warning_process_and_running_process_kills
|
||||
module.raise_for_process_errors(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=None,
|
||||
borg_local_path=flexmock(),
|
||||
borg_exit_codes=flexmock(),
|
||||
)
|
||||
@@ -975,6 +988,7 @@ def test_raise_for_process_errors_with_error_process_and_running_process_kills_a
|
||||
flexmock(module).should_receive('interpret_exit_code').with_args(
|
||||
object, 3, object, object
|
||||
).and_return(module.Exit_status.ERROR)
|
||||
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
|
||||
command = flexmock()
|
||||
flexmock(module).should_receive('command_for_process').and_return(command)
|
||||
|
||||
@@ -982,6 +996,7 @@ def test_raise_for_process_errors_with_error_process_and_running_process_kills_a
|
||||
module.raise_for_process_errors(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=None,
|
||||
borg_local_path=flexmock(),
|
||||
borg_exit_codes=flexmock(),
|
||||
)
|
||||
@@ -998,6 +1013,7 @@ def test_raise_for_process_errors_with_warning_process_and_long_output_raises_wi
|
||||
process: module.Process_metadata(last_lines=['hi', 'there'], capture=False)
|
||||
}
|
||||
flexmock(module).should_receive('interpret_exit_code').and_return(module.Exit_status.ERROR)
|
||||
flexmock(module).should_receive('log_remaining_buffer_lines').and_return(())
|
||||
command = flexmock()
|
||||
flexmock(module).should_receive('command_for_process').and_return(command)
|
||||
|
||||
@@ -1005,6 +1021,7 @@ def test_raise_for_process_errors_with_warning_process_and_long_output_raises_wi
|
||||
flexmock(module, ERROR_OUTPUT_MAX_LINE_COUNT=2).raise_for_process_errors(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=None,
|
||||
borg_local_path=flexmock(),
|
||||
borg_exit_codes=flexmock(),
|
||||
)
|
||||
|
||||
+15
-12
@@ -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}',
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user