mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-22 10:13:00 +02:00
Compare commits
60
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ac2c86d2d | ||
|
|
9839b3dada | ||
|
|
b7ecc9c6c3 | ||
|
|
4f728b1cd4 | ||
|
|
78db924b70 | ||
|
|
b55834c1ca | ||
|
|
411685280c | ||
|
|
cce679248f | ||
|
|
68c9516424 | ||
|
|
a508cabe3f | ||
|
|
0e1659bd73 | ||
|
|
ad61ad356e | ||
|
|
5c7d03910b | ||
|
|
468af1de0b | ||
|
|
524d8263a7 | ||
|
|
1d2aea0951 | ||
|
|
961ff7c724 | ||
|
|
3e80056956 | ||
|
|
d8f558ce0d | ||
|
|
9da75fdc58 | ||
|
|
ac9c8bb644 | ||
|
|
d085fc2398 | ||
|
|
40d2d521a7 | ||
|
|
0b34ef0e1a | ||
|
|
3f34d0848e | ||
|
|
54289e3ee0 | ||
|
|
6eea2d5323 | ||
|
|
0ca5333fd4 | ||
|
|
462e1392da | ||
|
|
71e2762aa7 | ||
|
|
edfa708fa3 | ||
|
|
aee16e32e2 | ||
|
|
f3ae04225d | ||
|
|
3f70cf0b29 | ||
|
|
731e8d7c37 | ||
|
|
3bcf592d53 | ||
|
|
9bb5791e9f | ||
|
|
8a60fb6398 | ||
|
|
9db2bb2b54 | ||
|
|
87bfd6e97f | ||
|
|
52f9442377 | ||
|
|
af841e0c89 | ||
|
|
ed8320c1bb | ||
|
|
26b3a03721 | ||
|
|
52234c47e6 | ||
|
|
b74b6aa18d | ||
|
|
0380ecd8fb | ||
|
|
32b7d1a0f7 | ||
|
|
da873c09f8 | ||
|
|
d612d398e7 | ||
|
|
1301bec702 | ||
|
|
c7fc68a49a | ||
|
|
c66e29906e | ||
|
|
475389a094 | ||
|
|
9f59bf2827 | ||
|
|
f4e9569297 | ||
|
|
f43c2f7130 | ||
|
|
889b599d55 | ||
|
|
53791d4dc9 | ||
|
|
ee58adb4eb |
@@ -1,5 +1,5 @@
|
||||
name: build
|
||||
run-name: ${{ gitea.actor }} is building
|
||||
run-name: ${{ forgejo.actor }} is building
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
docs:
|
||||
needs: [test]
|
||||
runs-on: host
|
||||
if: gitea.event_name == 'push'
|
||||
if: forgejo.event_name == 'push'
|
||||
env:
|
||||
IMAGE_NAME: projects.torsion.org/borgmatic-collective/borgmatic:docs
|
||||
|
||||
@@ -9,7 +9,7 @@ Python project using setuptools.
|
||||
|
||||
Please do not use AI agents to modify this codebase. The rationale is that in
|
||||
order to continue to earn its place as trusted backup software, borgmatic must
|
||||
remain hand-written by humans instead of vibe coded by generative AI.
|
||||
remain handwritten by humans instead of vibe coded by generative AI.
|
||||
|
||||
Additionally, if LLMs were to perform a sizeable chunk of the feature
|
||||
development on this codebase, then human borgmatic developers would lose their
|
||||
|
||||
@@ -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,63 @@
|
||||
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/
|
||||
* #1289: Add mutual TLS support for the Loki monitoring hook. See the documentation for more
|
||||
information: https://torsion.org/borgmatic/reference/configuration/monitoring/loki/
|
||||
* #1292: Fix a "source directories do not exist" regression when configuration paths are relative
|
||||
symlinks and the bootstrap data source hook is enabled.
|
||||
* #1294: Fix a regression in which SSH warnings from remote repositories broke the "spot" check
|
||||
and other actions as well.
|
||||
* #1295: Fix the ZFS hook to properly unmount snapshots for empty datasets.
|
||||
|
||||
2.1.4
|
||||
* #1266: Add a stand-alone borgmatic Linux binary to the release downloads to serve as another way
|
||||
to install borgmatic. Consider this binary a beta feature.
|
||||
* #1286: Fix a regression in which running borgmatic with no arguments and no configuration files
|
||||
doesn't error as expected.
|
||||
* #1257: Fix for the Loki monitoring hook not respecting the monitoring verbosity.
|
||||
* #1264: Improve performance of the "info" and "repo-list" actions by eliminating a second "borg
|
||||
info" call that supports a "no matching archives" warning. The warning still occurs; it's just
|
||||
done now without the extra "borg info" call.
|
||||
* #1268: Fix the "spot" check, "extract" check, and all uses of the archive name "latest" to
|
||||
respect the "match_archives" and "archives_name_format" options. This means that borgmatic now
|
||||
uses the "latest" archive that also matches those options instead of the latest archive overall.
|
||||
* When Borg exits with a warning exit code, show a description of it, so you don't have to look up
|
||||
the code.
|
||||
* Split out borgmatic installation documentation to its own page, so it's easier to find.
|
||||
* Switch the default borgmatic installation method from pipx to uv, as uv is faster and used for
|
||||
borgmatic development. If you'd like to switch, see the documentation for more information:
|
||||
https://torsion.org/borgmatic/how-to/upgrade/
|
||||
* Move the project tracker from Gitea to Forgejo.
|
||||
* Fix a regression in which borgmatic didn't show an error message when run with no configuration.
|
||||
* Fix a traceback in the "spot" check with Borg 2.
|
||||
|
||||
2.1.3
|
||||
* #1175: Add a "files_changed" option for customizing Borg's file modification detection.
|
||||
* #1175: Add a "msgpack_version_check" option to prevent Borg from validating msgpack's version.
|
||||
|
||||
@@ -94,10 +94,11 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
|
||||
|
||||
## Getting started
|
||||
|
||||
Your first step is to [install and configure
|
||||
borgmatic](https://torsion.org/borgmatic/how-to/set-up-backups/).
|
||||
Your first steps are to
|
||||
[install](https://torsion.org/borgmatic/how-to/install-borgmatic/) and
|
||||
[configure borgmatic](https://torsion.org/borgmatic/how-to/set-up-backups/).
|
||||
|
||||
For additional documentation, check out the links above (left panel on wide screens)
|
||||
For additional documentation, check out the links on the top/left
|
||||
for <a href="https://torsion.org/borgmatic/#documentation">borgmatic how-to and
|
||||
reference guides</a>.
|
||||
|
||||
@@ -183,4 +184,4 @@ Thanks to all borgmatic contributors! There are multiple ways to contribute to
|
||||
this project, so the following includes those who have fixed bugs, contributed
|
||||
features, *or* filed tickets.
|
||||
|
||||
{% include borgmatic/contributors.html %}
|
||||
{% include borgmatic/contributors.html %}
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,21 @@
|
||||
# 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
|
||||
@@ -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,
|
||||
@@ -605,7 +605,7 @@ def compare_spot_check_hashes(
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
list_paths=source_sample_paths_subset,
|
||||
path_format='{xxh64}{path}{linktarget}',
|
||||
path_format='{xxh64}{path}{linktarget}{target}',
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
):
|
||||
@@ -614,7 +614,7 @@ def compare_spot_check_hashes(
|
||||
|
||||
# Borg can't get hashes of stored hard links. So if this is a hard link path (and not
|
||||
# deemed as the "original" by Borg), then skip hashing of it.
|
||||
if entry['linktarget']:
|
||||
if entry.get('linktarget') or entry.get('target'):
|
||||
source_hashes.pop(os.path.join('/', entry['path']), None)
|
||||
continue
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
@@ -391,7 +393,7 @@ def create_archive(
|
||||
borg_exit_codes = config.get('borg_exit_codes')
|
||||
|
||||
if stream_processes:
|
||||
return '\n'.join(
|
||||
output = '\n'.join(
|
||||
execute_command_with_processes(
|
||||
create_flags + create_positional_arguments,
|
||||
stream_processes,
|
||||
@@ -403,9 +405,10 @@ def create_archive(
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
)
|
||||
)
|
||||
return output if json else None
|
||||
|
||||
if output_log_level is None:
|
||||
return '\n'.join(
|
||||
output = '\n'.join(
|
||||
execute_command_and_capture_output(
|
||||
create_flags + create_positional_arguments,
|
||||
working_directory=working_directory,
|
||||
@@ -414,6 +417,7 @@ def create_archive(
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
)
|
||||
)
|
||||
return output if json else None
|
||||
|
||||
execute_command(
|
||||
create_flags + create_positional_arguments,
|
||||
|
||||
+10
-20
@@ -1,5 +1,4 @@
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
@@ -135,27 +134,18 @@ def make_match_archives_flags( # noqa: PLR0911
|
||||
return ('--glob-archives', f'{derived_match_archives}')
|
||||
|
||||
|
||||
def warn_for_aggressive_archive_flags(json_command, json_output):
|
||||
def warn_for_aggressive_archive_flags(command, output_lines):
|
||||
'''
|
||||
Given a JSON archives command and the resulting JSON string output from running it, parse the
|
||||
JSON and warn if the command used an archive flag but the output indicates zero archives were
|
||||
found.
|
||||
Given an archives command and the resulting output lines from running it, warn if the command
|
||||
used an archive flag but the output indicates zero archives were found.
|
||||
'''
|
||||
archive_flags_used = {'--glob-archives', '--match-archives'}.intersection(set(json_command))
|
||||
|
||||
if not archive_flags_used:
|
||||
return
|
||||
|
||||
try:
|
||||
if len(json.loads(json_output)['archives']) == 0:
|
||||
logger.warning('An archive filter was applied, but no matching archives were found.')
|
||||
logger.warning(
|
||||
'Try adding --match-archives "*" or adjusting archive_name_format/match_archives in configuration.',
|
||||
)
|
||||
except json.JSONDecodeError as error:
|
||||
logger.debug(f'Cannot parse JSON output from archive command: {error}')
|
||||
except (TypeError, KeyError):
|
||||
logger.debug('Cannot parse JSON output from archive command: No "archives" key found')
|
||||
if {'--glob-archives', '--match-archives'}.intersection(set(command)) and len(
|
||||
tuple(line for line in output_lines if not line.startswith('terminating with '))
|
||||
) == 0:
|
||||
logger.warning('An archive filter was applied, but no matching archives were found.')
|
||||
logger.warning(
|
||||
'Try adding --match-archives "*" or adjusting archive_name_format/match_archives in configuration.',
|
||||
)
|
||||
|
||||
|
||||
def omit_flag(arguments, flag):
|
||||
|
||||
+16
-16
@@ -5,7 +5,7 @@ import shlex
|
||||
import borgmatic.config.paths
|
||||
import borgmatic.logger
|
||||
from borgmatic.borg import environment, feature, flags
|
||||
from borgmatic.execute import execute_command, execute_command_and_capture_output
|
||||
from borgmatic.execute import execute_command_and_capture_output
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -103,9 +103,21 @@ def display_archives_info(
|
||||
borg_exit_codes = config.get('borg_exit_codes')
|
||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||
|
||||
json_info = '\n'.join(
|
||||
if info_arguments.json:
|
||||
return '\n'.join(
|
||||
execute_command_and_capture_output(
|
||||
json_command,
|
||||
environment=environment.make_environment(config),
|
||||
working_directory=working_directory,
|
||||
borg_local_path=local_path,
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
)
|
||||
)
|
||||
|
||||
output_lines = tuple(
|
||||
execute_command_and_capture_output(
|
||||
json_command,
|
||||
main_command,
|
||||
output_log_level=logging.ANSWER,
|
||||
environment=environment.make_environment(config),
|
||||
working_directory=working_directory,
|
||||
borg_local_path=local_path,
|
||||
@@ -113,18 +125,6 @@ def display_archives_info(
|
||||
)
|
||||
)
|
||||
|
||||
if info_arguments.json:
|
||||
return json_info
|
||||
|
||||
flags.warn_for_aggressive_archive_flags(json_command, json_info)
|
||||
|
||||
execute_command(
|
||||
main_command,
|
||||
output_log_level=logging.ANSWER,
|
||||
environment=environment.make_environment(config),
|
||||
working_directory=working_directory,
|
||||
borg_local_path=local_path,
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
)
|
||||
flags.warn_for_aggressive_archive_flags(main_command, output_lines)
|
||||
|
||||
return None
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}')
|
||||
|
||||
+23
-16
@@ -6,7 +6,7 @@ import shlex
|
||||
import borgmatic.config.paths
|
||||
import borgmatic.logger
|
||||
from borgmatic.borg import environment, feature, flags
|
||||
from borgmatic.execute import execute_command, execute_command_and_capture_output
|
||||
from borgmatic.execute import execute_command_and_capture_output
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -79,6 +79,13 @@ def get_latest_archive(
|
||||
*flags.make_flags('umask', config.get('umask')),
|
||||
*('--log-json',),
|
||||
*flags.make_flags('lock-wait', config.get('lock_wait')),
|
||||
*(
|
||||
flags.make_match_archives_flags(
|
||||
config.get('match_archives'),
|
||||
config.get('archive_name_format'),
|
||||
local_borg_version,
|
||||
)
|
||||
),
|
||||
*(
|
||||
flags.make_flags('consider-checkpoints', consider_checkpoints)
|
||||
if not feature.available(feature.Feature.REPO_LIST, local_borg_version)
|
||||
@@ -219,9 +226,21 @@ def list_repository(
|
||||
working_directory = borgmatic.config.paths.get_working_directory(config)
|
||||
borg_exit_codes = config.get('borg_exit_codes')
|
||||
|
||||
json_listing = '\n'.join(
|
||||
if repo_list_arguments.json:
|
||||
return '\n'.join(
|
||||
execute_command_and_capture_output(
|
||||
json_command,
|
||||
environment=environment.make_environment(config),
|
||||
working_directory=working_directory,
|
||||
borg_local_path=local_path,
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
)
|
||||
)
|
||||
|
||||
output_lines = tuple(
|
||||
execute_command_and_capture_output(
|
||||
json_command,
|
||||
main_command,
|
||||
output_log_level=logging.ANSWER,
|
||||
environment=environment.make_environment(config),
|
||||
working_directory=working_directory,
|
||||
borg_local_path=local_path,
|
||||
@@ -229,18 +248,6 @@ def list_repository(
|
||||
)
|
||||
)
|
||||
|
||||
if repo_list_arguments.json:
|
||||
return json_listing
|
||||
|
||||
flags.warn_for_aggressive_archive_flags(json_command, json_listing)
|
||||
|
||||
execute_command(
|
||||
main_command,
|
||||
output_log_level=logging.ANSWER,
|
||||
environment=environment.make_environment(config),
|
||||
working_directory=working_directory,
|
||||
borg_local_path=local_path,
|
||||
borg_exit_codes=borg_exit_codes,
|
||||
)
|
||||
flags.warn_for_aggressive_archive_flags(main_command, output_lines)
|
||||
|
||||
return None
|
||||
|
||||
@@ -1044,16 +1044,17 @@ def exit_with_help_link(): # pragma: no cover
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def check_and_show_help_on_no_args(configs):
|
||||
def check_and_show_help_on_no_args(configs, schema):
|
||||
'''
|
||||
Given a dict of configuration filename to corresponding parsed configuration, check if the
|
||||
borgmatic command is run without any arguments. If the configuration option "default_actions" is
|
||||
set to False, show the help message. Otherwise, trigger the default backup behavior.
|
||||
Given a dict of configuration filename to corresponding parsed configuration and the
|
||||
configuration schema as a dict, check if the borgmatic command was run without any arguments. If
|
||||
the configuration option "default_actions" is set to False, then show the help message an exit.
|
||||
'''
|
||||
if len(sys.argv) == 1: # No arguments provided
|
||||
if len(sys.argv) == 1: # No arguments provided.
|
||||
default_actions = any(config.get('default_actions', True) for config in configs.values())
|
||||
if not default_actions:
|
||||
parse_arguments('--help')
|
||||
|
||||
if configs and not default_actions:
|
||||
parse_arguments(schema, '--help')
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
@@ -1122,7 +1123,7 @@ def main(extra_summary_logs=()): # pragma: no cover
|
||||
exit_with_help_link()
|
||||
except SystemExit as error:
|
||||
if error.code == 0:
|
||||
raise error
|
||||
raise
|
||||
|
||||
configure_logging(logging.CRITICAL)
|
||||
logger.critical(f"Error parsing arguments: {' '.join(sys.argv)}")
|
||||
@@ -1150,8 +1151,7 @@ def main(extra_summary_logs=()): # pragma: no cover
|
||||
resolve_env=global_arguments.resolve_env and not arguments.get('validate'),
|
||||
)
|
||||
|
||||
# Use the helper function to check and show help on no arguments, passing the preloaded configs
|
||||
check_and_show_help_on_no_args(configs)
|
||||
check_and_show_help_on_no_args(configs, schema)
|
||||
|
||||
configuration_parse_errors = (
|
||||
(max(log.levelno for log in parse_logs) >= logging.CRITICAL) if parse_logs else False
|
||||
@@ -1202,3 +1202,7 @@ def main(extra_summary_logs=()): # pragma: no cover
|
||||
)
|
||||
|
||||
display_summary(summary_logs, log_json)
|
||||
|
||||
|
||||
if __name__ == '__main__': # pragma: no cover
|
||||
main()
|
||||
|
||||
+39
-20
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -1098,9 +1101,9 @@ properties:
|
||||
default_actions:
|
||||
type: boolean
|
||||
description: |
|
||||
Whether to apply default actions (create, prune, compact and check)
|
||||
when no arguments are supplied to the borgmatic command. If set to
|
||||
false, borgmatic displays the help message instead.
|
||||
Whether to run default actions (create, prune, compact, and check)
|
||||
when no arguments are given on the command line. If set to false,
|
||||
borgmatic displays the help message instead.
|
||||
example: true
|
||||
skip_actions:
|
||||
type: array
|
||||
@@ -3098,6 +3101,24 @@ properties:
|
||||
Grafana Loki log URL to notify when a backup begins,
|
||||
ends, or fails.
|
||||
example: "http://localhost:3100/loki/api/v1/push"
|
||||
tls:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
cert_path:
|
||||
type: string
|
||||
description: |
|
||||
Path to a PEM client certificate file for mutual
|
||||
TLS authentication.
|
||||
example: /etc/borgmatic/loki-client.crt
|
||||
key_path:
|
||||
type: string
|
||||
description: |
|
||||
Path to a PEM private key file for the client
|
||||
certificate.
|
||||
example: /etc/borgmatic/loki-client.key
|
||||
description: |
|
||||
TLS options for mutual TLS (mTLS) authentication with Loki.
|
||||
labels:
|
||||
type: object
|
||||
additionalProperties:
|
||||
@@ -3291,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`).
|
||||
|
||||
+71
-54
@@ -43,6 +43,18 @@ def command_is_borg(command, borg_local_path):
|
||||
return bool(borg_local_path and parsed_command[0] == borg_local_path)
|
||||
|
||||
|
||||
BORG_EXIT_CODE_TO_DESCRIPTION = {
|
||||
100: 'File changed while we backed it up',
|
||||
101: 'Include pattern never matched',
|
||||
102: 'General backup issue',
|
||||
103: 'File type or inode changed while we backed it up',
|
||||
104: 'Backup OS issue',
|
||||
105: 'Backup permission issue',
|
||||
106: 'Backup IO issue',
|
||||
107: 'Backup file not found',
|
||||
}
|
||||
|
||||
|
||||
def interpret_exit_code(command, exit_code, borg_local_path=None, borg_exit_codes=None): # noqa: PLR0911
|
||||
'''
|
||||
Return an Exit_status value (e.g. SUCCESS, ERROR, or WARNING) based on interpreting the given
|
||||
@@ -56,46 +68,47 @@ def interpret_exit_code(command, exit_code, borg_local_path=None, borg_exit_code
|
||||
if exit_code == 0:
|
||||
return Exit_status.SUCCESS
|
||||
|
||||
if command_is_borg(command, borg_local_path):
|
||||
# First try looking for the exit code in the borg_exit_codes configuration.
|
||||
for entry in borg_exit_codes or ():
|
||||
if entry.get('code') == exit_code:
|
||||
treat_as = entry.get('treat_as')
|
||||
if not command_is_borg(command, borg_local_path):
|
||||
return Exit_status.ERROR
|
||||
|
||||
if treat_as == 'error':
|
||||
logger.error(
|
||||
f'Treating exit code {exit_code} as an error, as per configuration',
|
||||
)
|
||||
return Exit_status.ERROR
|
||||
description = BORG_EXIT_CODE_TO_DESCRIPTION.get(exit_code)
|
||||
description_parenthetical = f' ({description})' if description else ''
|
||||
|
||||
if treat_as == 'warning':
|
||||
logger.warning(
|
||||
f'Treating exit code {exit_code} as a warning, as per configuration',
|
||||
)
|
||||
return Exit_status.WARNING
|
||||
# First try looking for the exit code in the borg_exit_codes configuration.
|
||||
for entry in borg_exit_codes or ():
|
||||
if entry.get('code') == exit_code:
|
||||
treat_as = entry.get('treat_as')
|
||||
|
||||
# If the exit code doesn't have explicit configuration, then fall back to the default
|
||||
# behavior of treating Borg errors as errors and some Borg warnings as errors.
|
||||
if exit_code in BORG_WARNING_EXIT_CODES_TREATED_AS_ERRORS:
|
||||
logger.error(
|
||||
f'Treating exit code {exit_code} as an error, as per borgmatic defaults',
|
||||
)
|
||||
|
||||
return Exit_status.ERROR
|
||||
|
||||
return (
|
||||
Exit_status.ERROR
|
||||
if (
|
||||
exit_code < 0
|
||||
or (
|
||||
exit_code >= BORG_ERROR_EXIT_CODE_START
|
||||
and exit_code <= BORG_ERROR_EXIT_CODE_END
|
||||
if treat_as == 'error':
|
||||
logger.error(
|
||||
f'Treating exit code {exit_code}{description_parenthetical} as an error, as per configuration',
|
||||
)
|
||||
)
|
||||
else Exit_status.WARNING
|
||||
return Exit_status.ERROR
|
||||
|
||||
if treat_as == 'warning':
|
||||
logger.warning(
|
||||
f'Treating exit code {exit_code}{description_parenthetical} as a warning, as per configuration',
|
||||
)
|
||||
return Exit_status.WARNING
|
||||
|
||||
# If the exit code doesn't have explicit configuration, then fall back to the default
|
||||
# behavior of treating Borg errors as errors and some Borg warnings as errors.
|
||||
if exit_code in BORG_WARNING_EXIT_CODES_TREATED_AS_ERRORS:
|
||||
logger.error(
|
||||
f'Treating exit code {exit_code}{description_parenthetical} as an error, as per borgmatic defaults',
|
||||
)
|
||||
|
||||
return Exit_status.ERROR
|
||||
return Exit_status.ERROR
|
||||
|
||||
if exit_code < 0 or (
|
||||
exit_code >= BORG_ERROR_EXIT_CODE_START and exit_code <= BORG_ERROR_EXIT_CODE_END
|
||||
):
|
||||
return Exit_status.ERROR
|
||||
|
||||
logger.warning(
|
||||
f'Treating exit code {exit_code}{description_parenthetical} as a warning, as per borgmatic defaults',
|
||||
)
|
||||
return Exit_status.WARNING
|
||||
|
||||
|
||||
def command_for_process(process):
|
||||
@@ -301,8 +314,9 @@ def log_buffer_lines(
|
||||
'''
|
||||
Given a dict from buffer object to Buffer_reader, a dict from subprocess.Popen() instance to
|
||||
Process_metadata instance, a requested output log level for stdout, Borg's local path, and
|
||||
whether to capture stderr, read and log any ready output lines from the buffers. Additionally,
|
||||
if the log level is None for any log record, then yield those log messages for capture.
|
||||
whether to capture stderr, read and log any ready output lines from the buffers. Additionally,
|
||||
for any log records with a log level the same as the output log level, yield those log messages
|
||||
for capture.
|
||||
|
||||
This function just does one "turn of the crank" of logging buffer output. It is intended to be
|
||||
called repeatedly to continue to process buffers.
|
||||
@@ -352,7 +366,9 @@ def log_buffer_lines(
|
||||
last_lines=process_metadatas[reader.process].last_lines,
|
||||
)
|
||||
|
||||
if log_record.levelno is None and process_metadatas[reader.process].capture:
|
||||
if (
|
||||
log_record.levelno is None or log_record.levelno == output_log_level
|
||||
) and process_metadatas[reader.process].capture:
|
||||
yield log_record.getMessage()
|
||||
|
||||
|
||||
@@ -411,8 +427,8 @@ def log_remaining_buffer_lines(
|
||||
Given a dict from buffer object to Buffer_reader, a dict from subprocess.Popen() instance to
|
||||
Process_metadata instance, a requested output log level for stdout, Borg's local path, and
|
||||
whether to capture stderr, drain and log any remaining output lines from the buffers until
|
||||
they're empty. Additionally, if the log level is None for any log record, then yield those log
|
||||
messages for capture.
|
||||
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.
|
||||
'''
|
||||
for output_buffer, reader in buffer_readers.items():
|
||||
if not reader.process:
|
||||
@@ -432,7 +448,9 @@ def log_remaining_buffer_lines(
|
||||
),
|
||||
)
|
||||
|
||||
if log_record.levelno is None and process_metadatas[reader.process].capture:
|
||||
if (
|
||||
log_record.levelno is None or log_record.levelno == output_log_level
|
||||
) and process_metadatas[reader.process].capture:
|
||||
yield log_record.getMessage()
|
||||
|
||||
|
||||
@@ -615,6 +633,7 @@ def execute_command(
|
||||
|
||||
def execute_command_and_capture_output(
|
||||
full_command,
|
||||
output_log_level=None,
|
||||
input_file=None,
|
||||
capture_stderr=False,
|
||||
shell=False,
|
||||
@@ -629,13 +648,15 @@ def execute_command_and_capture_output(
|
||||
output (stdout) as a generator that yields one line at a time. The generator must be consumed in
|
||||
order for the called command to execute.
|
||||
|
||||
If an input file descriptor is given, then pipe it to the command's stdin. If capture stderr is
|
||||
True, then capture stderr in addition to stdout. If shell is True, execute the command within a
|
||||
shell. If an environment variables dict is given, then pass it into the command. If a working
|
||||
directory is given, use that as the present working directory when running the command. If a
|
||||
Borg local path is given, and the command matches it (regardless of arguments), treat exit code
|
||||
1 as a warning instead of an error. But if Borg exit codes are given as a sequence of exit code
|
||||
configuration dicts, then use that configuration to decide what's an error and what's a warning.
|
||||
If an output log level is given, then instead of suppressing log output, also output the
|
||||
captured lines at the given log level. If an input file descriptor is given, then pipe it to
|
||||
the command's stdin. If capture stderr is True, then capture stderr in addition to stdout. If
|
||||
shell is True, execute the command within a shell. If an environment variables dict is given,
|
||||
then pass it into the command. If a working directory is given, use that as the present working
|
||||
directory when running the command. If a Borg local path is given, and the command matches it
|
||||
(regardless of arguments), treat exit code 1 as a warning instead of an error. But if Borg exit
|
||||
codes are given as a sequence of exit code configuration dicts, then use that configuration to
|
||||
decide what's an error and what's a warning.
|
||||
|
||||
Raise subprocesses.CalledProcessError if an error occurs while running the command.
|
||||
'''
|
||||
@@ -668,17 +689,15 @@ def execute_command_and_capture_output(
|
||||
return
|
||||
|
||||
with borgmatic.logger.Log_prefix(None): # Log command output without any prefix.
|
||||
captured_lines = log_outputs(
|
||||
yield from log_outputs(
|
||||
(process,),
|
||||
(input_file,),
|
||||
None,
|
||||
output_log_level,
|
||||
borg_local_path,
|
||||
borg_exit_codes,
|
||||
capture_stderr=capture_stderr,
|
||||
)
|
||||
|
||||
yield from captured_lines
|
||||
|
||||
|
||||
def execute_command_with_processes(
|
||||
full_command,
|
||||
@@ -741,12 +760,10 @@ def execute_command_with_processes(
|
||||
raise
|
||||
|
||||
with borgmatic.logger.Log_prefix(None): # Log command output without any prefix.
|
||||
captured_lines = log_outputs(
|
||||
yield from log_outputs(
|
||||
(*processes, command_process),
|
||||
(input_file, output_file),
|
||||
output_log_level,
|
||||
borg_local_path,
|
||||
borg_exit_codes,
|
||||
)
|
||||
|
||||
yield from captured_lines
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -28,17 +28,20 @@ def resolve_config_path_symlinks(path):
|
||||
Given a path, resolve and yield each successive symlink until the final non-symlink target. If
|
||||
the given path isn't a symlink, then just yield it.
|
||||
|
||||
The purpose of this is to ensure that configuration files that are behind a symbolic link (or
|
||||
several) actually get backed up.
|
||||
|
||||
Raise ValueError if we have to follow too many symlinks without getting to the final target.
|
||||
'''
|
||||
original_path = path
|
||||
original_path = os.path.normpath(path)
|
||||
|
||||
for _ in range(MAXIMUM_CONFIG_SYMLINKS_TO_FOLLOW):
|
||||
yield os.path.abspath(path)
|
||||
yield path
|
||||
|
||||
if not os.path.islink(path):
|
||||
return
|
||||
|
||||
path = os.readlink(path)
|
||||
path = os.path.normpath(os.path.join(os.path.dirname(path), os.readlink(path)))
|
||||
|
||||
raise ValueError(f'Too many symlinks to follow for configuration path: {original_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 ()
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ import collections
|
||||
import glob
|
||||
import hashlib
|
||||
import logging
|
||||
import operator
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -123,7 +124,8 @@ def get_datasets_to_backup(zfs_command, patterns):
|
||||
|
||||
def get_all_dataset_mount_points(zfs_command):
|
||||
'''
|
||||
Given a ZFS command to run, return all ZFS datasets as a sequence of sorted mount points.
|
||||
Given a ZFS command to run, return a dict from ZFS dataset name to mount point (reverse sorted
|
||||
by mount point).
|
||||
'''
|
||||
list_lines = borgmatic.execute.execute_command_and_capture_output(
|
||||
(
|
||||
@@ -133,19 +135,23 @@ def get_all_dataset_mount_points(zfs_command):
|
||||
'-t',
|
||||
'filesystem',
|
||||
'-o',
|
||||
'mountpoint',
|
||||
'name,mountpoint',
|
||||
),
|
||||
close_fds=True,
|
||||
)
|
||||
|
||||
return tuple(
|
||||
return dict(
|
||||
sorted(
|
||||
{
|
||||
mount_point
|
||||
(
|
||||
(dataset_name, mount_point)
|
||||
for line in list_lines
|
||||
for mount_point in (line.rstrip(),)
|
||||
for (dataset_name, mount_point) in (line.rstrip().split('\t'),)
|
||||
if mount_point != 'none'
|
||||
},
|
||||
),
|
||||
key=operator.itemgetter(1),
|
||||
# Reversing the sorted datasets ensures that we unmount the longer mount point paths of
|
||||
# child datasets before the shorter mount point paths of parent datasets.
|
||||
reverse=True,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -376,7 +382,8 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, p
|
||||
zfs_command = hook_config.get('zfs_command', 'zfs')
|
||||
|
||||
try:
|
||||
dataset_mount_points = get_all_dataset_mount_points(zfs_command)
|
||||
dataset_name_to_mount_point = get_all_dataset_mount_points(zfs_command)
|
||||
full_snapshot_names = get_all_snapshots(zfs_command)
|
||||
except FileNotFoundError:
|
||||
logger.debug(f'Could not find "{zfs_command}" command')
|
||||
return
|
||||
@@ -393,19 +400,20 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, p
|
||||
)
|
||||
logger.debug(f'Looking for snapshots to remove in {snapshots_glob}{dry_run_label}')
|
||||
umount_command = hook_config.get('umount_command', 'umount')
|
||||
snapshot_dataset_names = {
|
||||
full_snapshot_name.split('@')[0] for full_snapshot_name in full_snapshot_names
|
||||
}
|
||||
|
||||
for snapshots_directory in glob.glob(snapshots_glob):
|
||||
if not os.path.isdir(snapshots_directory):
|
||||
continue
|
||||
|
||||
# Reversing the sorted datasets ensures that we unmount the longer mount point paths of
|
||||
# child datasets before the shorter mount point paths of parent datasets.
|
||||
for mount_point in reversed(dataset_mount_points):
|
||||
for dataset_name, mount_point in dataset_name_to_mount_point.items():
|
||||
snapshot_mount_path = os.path.join(snapshots_directory, mount_point.lstrip(os.path.sep))
|
||||
|
||||
# If the snapshot mount path is empty, this is probably just a "shadow" of a nested
|
||||
# dataset and therefore there's nothing to unmount.
|
||||
if not os.path.isdir(snapshot_mount_path) or not os.listdir(snapshot_mount_path):
|
||||
# If this dataset name does not correspond to a known snapshot, then this is probably
|
||||
# just a "shadow" of a nested dataset and therefore there's nothing to unmount.
|
||||
if not os.path.isdir(snapshot_mount_path) or dataset_name not in snapshot_dataset_names:
|
||||
continue
|
||||
|
||||
# This might fail if the path is already mounted, but we swallow errors here since we'll
|
||||
@@ -435,8 +443,6 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, p
|
||||
shutil.rmtree(snapshot_mount_path, ignore_errors=True)
|
||||
|
||||
# Destroy snapshots.
|
||||
full_snapshot_names = get_all_snapshots(zfs_command)
|
||||
|
||||
for full_snapshot_name in full_snapshot_names:
|
||||
# Only destroy snapshots that borgmatic actually created!
|
||||
if not full_snapshot_name.split('@')[-1].startswith(BORGMATIC_SNAPSHOT_PREFIX):
|
||||
|
||||
@@ -27,9 +27,16 @@ class Loki_log_buffer:
|
||||
adding labels to the log stream and takes care of communication with Loki.
|
||||
'''
|
||||
|
||||
def __init__(self, url, dry_run):
|
||||
def __init__(self, url, dry_run, tls_cert_path=None, tls_key_path=None):
|
||||
'''
|
||||
Given a Loki URL, a dry run flag, and optional TLS certificate and key paths for mTLS authentication,
|
||||
create an instance of Loki_log_buffer.
|
||||
'''
|
||||
|
||||
self.url = url
|
||||
self.dry_run = dry_run
|
||||
self.tls_cert_path = tls_cert_path
|
||||
self.tls_key_path = tls_key_path
|
||||
self.root = {'streams': [{'stream': {}, 'values': []}]}
|
||||
|
||||
def add_value(self, value):
|
||||
@@ -77,6 +84,7 @@ class Loki_log_buffer:
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'borgmatic',
|
||||
},
|
||||
cert=(self.tls_cert_path, self.tls_key_path) if self.tls_cert_path else None,
|
||||
)
|
||||
result.raise_for_status()
|
||||
except requests.RequestException:
|
||||
@@ -88,16 +96,19 @@ class Loki_log_handler(logging.Handler):
|
||||
A log handler that sends logs to Loki.
|
||||
'''
|
||||
|
||||
def __init__(self, url, send_logs, dry_run):
|
||||
def __init__(self, url, send_logs, log_level, dry_run, tls_cert_path=None, tls_key_path=None):
|
||||
'''
|
||||
Given a URL to send logs to, whether all borgmatic logs should be sent (or just explicitly
|
||||
added messages from this hook), and whether this is a dry run, create an instance of
|
||||
Loki_log_buffer.
|
||||
added messages from this hook), the log level to use (influencing which logs get sent), and
|
||||
whether this is a dry run, create an instance of Loki_log_buffer.
|
||||
'''
|
||||
super().__init__()
|
||||
|
||||
self.buffer = Loki_log_buffer(url, dry_run)
|
||||
self.buffer = Loki_log_buffer(
|
||||
url, dry_run, tls_cert_path=tls_cert_path, tls_key_path=tls_key_path
|
||||
)
|
||||
self.send_logs = send_logs
|
||||
self.setLevel(log_level)
|
||||
|
||||
def emit(self, record):
|
||||
'''
|
||||
@@ -138,7 +149,21 @@ def initialize_monitor(hook_config, config, config_filename, monitoring_log_leve
|
||||
Add a handler to the root logger to regularly send the logs to Loki.
|
||||
'''
|
||||
url = hook_config.get('url')
|
||||
loki = Loki_log_handler(url, hook_config.get('send_logs', False), dry_run)
|
||||
tls = hook_config.get('tls', {})
|
||||
|
||||
if bool(tls.get('cert_path')) != bool(tls.get('key_path')):
|
||||
raise ValueError(
|
||||
'Invalid Loki TLS configuration: cert_path and key_path must both be set or both be unset'
|
||||
)
|
||||
|
||||
loki = Loki_log_handler(
|
||||
url,
|
||||
hook_config.get('send_logs', False),
|
||||
monitoring_log_level,
|
||||
dry_run,
|
||||
tls_cert_path=tls.get('cert_path'),
|
||||
tls_key_path=tls.get('key_path'),
|
||||
)
|
||||
|
||||
for key, value in hook_config.get('labels').items():
|
||||
if value == '__hostname':
|
||||
@@ -150,7 +175,9 @@ def initialize_monitor(hook_config, config, config_filename, monitoring_log_leve
|
||||
else:
|
||||
loki.add_label(key, value)
|
||||
|
||||
logging.getLogger().addHandler(loki)
|
||||
global_logger = logging.getLogger()
|
||||
global_logger.addHandler(loki)
|
||||
global_logger.setLevel(min(handler.level for handler in global_logger.handlers))
|
||||
|
||||
|
||||
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
|
||||
@@ -166,9 +193,11 @@ def destroy_monitor(hook_config, config, monitoring_log_level, dry_run):
|
||||
'''
|
||||
Remove the monitor handler that was added to the root logger.
|
||||
'''
|
||||
logger = logging.getLogger()
|
||||
global_logger = logging.getLogger()
|
||||
|
||||
for handler in tuple(logger.handlers):
|
||||
for handler in tuple(global_logger.handlers):
|
||||
if isinstance(handler, Loki_log_handler):
|
||||
handler.flush()
|
||||
logger.removeHandler(handler)
|
||||
global_logger.removeHandler(handler)
|
||||
|
||||
global_logger.setLevel(min(handler.level for handler in global_logger.handlers))
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import requests
|
||||
|
||||
def list_merged_pulls(url):
|
||||
'''
|
||||
Given a Gitea or GitHub API endpoint URL for pull requests, fetch and return the corresponding
|
||||
Given a Forgejo or GitHub API endpoint URL for pull requests, fetch and return the corresponding
|
||||
JSON for all such merged pull requests.
|
||||
'''
|
||||
response = requests.get(f'{url}?state=closed', headers={'Accept': 'application/json', 'Content-Type': 'application/json'})
|
||||
|
||||
@@ -236,7 +236,7 @@ Compose.
|
||||
|
||||
Please do not use AI agents to modify this codebase. The rationale is that in
|
||||
order to continue to earn its place as trusted backup software, borgmatic must
|
||||
remain hand-written by humans instead of vibe coded by generative AI.
|
||||
remain handwritten by humans instead of vibe coded by generative AI.
|
||||
|
||||
Additionally, if LLMs were to perform a sizeable chunk of the feature
|
||||
development on this codebase, then human borgmatic developers would lose their
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: 📥 How to install borgmatic
|
||||
eleventyNavigation:
|
||||
key: 📥 Install borgmatic
|
||||
parent: How-to guides
|
||||
order: -1
|
||||
---
|
||||
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before installing borgmatic, first [install
|
||||
Borg](https://borgbackup.readthedocs.io/en/stable/installation.html), at least
|
||||
version 1.1. (borgmatic does not install Borg automatically so as to avoid
|
||||
conflicts with existing Borg installations.)
|
||||
|
||||
Then, [install uv](https://docs.astral.sh/uv/getting-started/installation/) as
|
||||
the root user (with `sudo`) to make installing borgmatic easier without
|
||||
impacting other Python applications on your system. For Debian, there is a
|
||||
[third-party package for
|
||||
uv](https://dario.griffo.io/posts/how-to-install-uv-debian/). On Ubuntu, there
|
||||
is a [snap package](https://snapcraft.io/install/astral-uv/ubuntu). On Arch, you
|
||||
can just install the `python-uv` package.
|
||||
|
||||
|
||||
### Root install
|
||||
|
||||
If you want borgmatic to run with privileged access so it can backup your system
|
||||
files, then install borgmatic as the root user by running the following
|
||||
commands:
|
||||
|
||||
```bash
|
||||
sudo uv tool update-shell
|
||||
sudo uv tool install borgmatic
|
||||
```
|
||||
|
||||
Check whether this worked with:
|
||||
|
||||
```bash
|
||||
sudo su -
|
||||
borgmatic --version
|
||||
```
|
||||
|
||||
If borgmatic is properly installed, that should output your borgmatic version.
|
||||
And if you'd also like `sudo borgmatic` to work as well, keep reading!
|
||||
|
||||
|
||||
### Non-root install
|
||||
|
||||
If you only want to run borgmatic as a non-root user (without privileged file
|
||||
access) *or* you want to make `sudo borgmatic` work so borgmatic runs as root,
|
||||
then install borgmatic as a non-root user by running the following commands as
|
||||
that user:
|
||||
|
||||
```bash
|
||||
uv tool update-shell
|
||||
uv tool install borgmatic
|
||||
```
|
||||
|
||||
This should work even if you've also installed borgmatic as the root user.
|
||||
|
||||
Check whether this worked with:
|
||||
|
||||
```bash
|
||||
borgmatic --version
|
||||
```
|
||||
|
||||
If borgmatic is properly installed, that should output your borgmatic version.
|
||||
You can also try `sudo borgmatic --version` if you intend to run borgmatic
|
||||
with `sudo`. If that doesn't work, you may need to update your [sudoers
|
||||
`secure_path` option](https://wiki.archlinux.org/title/Sudo).
|
||||
|
||||
|
||||
### Other ways to install
|
||||
|
||||
Besides the approaches described above, there are several other options for
|
||||
installing borgmatic:
|
||||
|
||||
#### <span data-pagefind-weight="7.0">Docker / Podman</span>
|
||||
|
||||
* [container image with scheduled backups](https://github.com/borgmatic-collective/docker-borgmatic) (+ Docker Compose files)
|
||||
* [container image with multi-arch and Docker CLI support](https://github.com/modem7/docker-borgmatic)
|
||||
* [Borgmatic Director UI](https://github.com/SpeedbitsInfinityTools/borgmatic-ui-community)
|
||||
|
||||
#### Operating system packages
|
||||
|
||||
* [Debian](https://tracker.debian.org/pkg/borgmatic)
|
||||
* [Ubuntu](https://launchpad.net/ubuntu/+source/borgmatic)
|
||||
* [Fedora](https://bodhi.fedoraproject.org/updates/?search=borgmatic)
|
||||
* [Gentoo](https://packages.gentoo.org/packages/app-backup/borgmatic)
|
||||
* [Arch Linux](https://archlinux.org/packages/extra/any/borgmatic/)
|
||||
* [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=borgmatic)
|
||||
* [OpenBSD](https://openports.pl/path/sysutils/borgmatic)
|
||||
* [openSUSE](https://software.opensuse.org/package/borgmatic)
|
||||
* [macOS (via Homebrew)](https://formulae.brew.sh/formula/borgmatic)
|
||||
* [macOS (via MacPorts)](https://ports.macports.org/port/borgmatic/)
|
||||
* [NixOS](https://search.nixos.org/packages?channel=unstable&show=borgmatic&query=borgmatic)
|
||||
|
||||
#### Etc.
|
||||
|
||||
* [stand-alone Linux binary](https://projects.torsion.org/borgmatic-collective/borgmatic/releases) (This is a beta feature!)
|
||||
* [Ansible role](https://github.com/borgbase/ansible-role-borgbackup)
|
||||
* [pipx](https://pipx.pypa.io/stable/)
|
||||
|
||||
|
||||
## Next steps
|
||||
|
||||
* [Set up backups](https://torsion.org/borgmatic/how-to/set-up-backups/)
|
||||
@@ -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>
|
||||
|
||||
@@ -1,104 +1,14 @@
|
||||
---
|
||||
title: 📥 How to set up backups
|
||||
title: 📋 How to set up backups
|
||||
eleventyNavigation:
|
||||
key: 📥 Set up backups
|
||||
key: 📋 Set up backups
|
||||
parent: How-to guides
|
||||
order: 0
|
||||
---
|
||||
|
||||
To install borgmatic, first [install
|
||||
Borg](https://borgbackup.readthedocs.io/en/stable/installation.html), at least
|
||||
version 1.1. (borgmatic does not install Borg automatically so as to avoid
|
||||
conflicts with existing Borg installations.)
|
||||
|
||||
Then, [install pipx](https://pypa.github.io/pipx/installation/) as the root
|
||||
user (with `sudo`) to make installing borgmatic easier without impacting other
|
||||
Python applications on your system. If you have trouble installing pipx with
|
||||
pip, then you can install a system package instead. E.g. on Ubuntu or Debian,
|
||||
run:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install pipx
|
||||
```
|
||||
|
||||
### Root install
|
||||
|
||||
If you want to run borgmatic on a schedule with privileged access to your
|
||||
files, then you should install borgmatic as the root user by running the
|
||||
following commands:
|
||||
|
||||
```bash
|
||||
sudo pipx ensurepath
|
||||
sudo pipx install borgmatic
|
||||
```
|
||||
|
||||
Check whether this worked with:
|
||||
|
||||
```bash
|
||||
sudo su -
|
||||
borgmatic --version
|
||||
```
|
||||
|
||||
If borgmatic is properly installed, that should output your borgmatic version.
|
||||
And if you'd also like `sudo borgmatic` to work, keep reading!
|
||||
|
||||
|
||||
### Non-root install
|
||||
|
||||
If you only want to run borgmatic as a non-root user (without privileged file
|
||||
access) *or* you want to make `sudo borgmatic` work so borgmatic runs as root,
|
||||
then install borgmatic as a non-root user by running the following commands as
|
||||
that user:
|
||||
|
||||
```bash
|
||||
pipx ensurepath
|
||||
pipx install borgmatic
|
||||
```
|
||||
|
||||
This should work even if you've also installed borgmatic as the root user.
|
||||
|
||||
Check whether this worked with:
|
||||
|
||||
```bash
|
||||
borgmatic --version
|
||||
```
|
||||
|
||||
If borgmatic is properly installed, that should output your borgmatic version.
|
||||
You can also try `sudo borgmatic --version` if you intend to run borgmatic
|
||||
with `sudo`. If that doesn't work, you may need to update your [sudoers
|
||||
`secure_path` option](https://wiki.archlinux.org/title/Sudo).
|
||||
|
||||
|
||||
### Other ways to install
|
||||
|
||||
Besides the approaches described above, there are several other options for
|
||||
installing borgmatic:
|
||||
|
||||
#### <span data-pagefind-weight="7.0">Docker / Podman</span>
|
||||
|
||||
* [container image with scheduled backups](https://github.com/borgmatic-collective/docker-borgmatic) (+ Docker Compose files)
|
||||
* [container image with multi-arch and Docker CLI support](https://github.com/modem7/docker-borgmatic)
|
||||
|
||||
#### Operating system packages
|
||||
|
||||
* [Debian](https://tracker.debian.org/pkg/borgmatic)
|
||||
* [Ubuntu](https://launchpad.net/ubuntu/+source/borgmatic)
|
||||
* [Fedora](https://bodhi.fedoraproject.org/updates/?search=borgmatic)
|
||||
* [Gentoo](https://packages.gentoo.org/packages/app-backup/borgmatic)
|
||||
* [Arch Linux](https://archlinux.org/packages/extra/any/borgmatic/)
|
||||
* [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=borgmatic)
|
||||
* [OpenBSD](https://openports.pl/path/sysutils/borgmatic)
|
||||
* [openSUSE](https://software.opensuse.org/package/borgmatic)
|
||||
* [macOS (via Homebrew)](https://formulae.brew.sh/formula/borgmatic)
|
||||
* [macOS (via MacPorts)](https://ports.macports.org/port/borgmatic/)
|
||||
* [NixOS](https://search.nixos.org/packages?channel=unstable&show=borgmatic&query=borgmatic)
|
||||
|
||||
#### Etc.
|
||||
|
||||
* [Ansible role](https://github.com/borgbase/ansible-role-borgbackup)
|
||||
* [uv tool install](https://docs.astral.sh/uv/)
|
||||
|
||||
Start by [installing
|
||||
borgmatic](https://torsion.org/borgmatic/how-to/install-borgmatic/) if you haven't
|
||||
already.
|
||||
|
||||
## Hosting providers
|
||||
|
||||
@@ -322,7 +232,7 @@ If you're using systemd instead of cron to run jobs, you can still configure
|
||||
borgmatic to run automatically.
|
||||
|
||||
(If you installed borgmatic from [Other ways to
|
||||
install](https://torsion.org/borgmatic/how-to/set-up-backups/#other-ways-to-install),
|
||||
install](https://torsion.org/borgmatic/how-to/install-borgmatic/#other-ways-to-install),
|
||||
you may already have borgmatic systemd service and timer files. If so, you may
|
||||
be able to skip some of the steps below.)
|
||||
|
||||
|
||||
+37
-8
@@ -5,10 +5,25 @@ eleventyNavigation:
|
||||
parent: How-to guides
|
||||
order: 14
|
||||
---
|
||||
In general, all you should need to do to upgrade borgmatic if you've
|
||||
[installed it with
|
||||
pipx](https://torsion.org/borgmatic/how-to/set-up-backups/#installation)
|
||||
is to run the following:
|
||||
In general, all you should need to do to upgrade borgmatic if you've [installed
|
||||
it with uv](https://docs.astral.sh/uv/) is to run the following:
|
||||
|
||||
```bash
|
||||
sudo uv tool upgrade borgmatic
|
||||
```
|
||||
|
||||
Omit `sudo` if you installed borgmatic as a non-root user. And if you
|
||||
installed borgmatic *both* as root and as a non-root user, you'll need to
|
||||
upgrade each installation independently.
|
||||
|
||||
|
||||
### Upgrading from other installation methods
|
||||
|
||||
#### pipx
|
||||
|
||||
If you have borgmatic installed with
|
||||
[pipx](https://pipx.pypa.io/latest/installation/), and you'd like to continue
|
||||
using pipx, then you can upgrade borgmatic with:
|
||||
|
||||
```bash
|
||||
sudo pipx upgrade borgmatic
|
||||
@@ -18,16 +33,30 @@ Omit `sudo` if you installed borgmatic as a non-root user. And if you
|
||||
installed borgmatic *both* as root and as a non-root user, you'll need to
|
||||
upgrade each installation independently.
|
||||
|
||||
But if you'd like to switch your borgmatic install from pipx to
|
||||
[uv](https://docs.astral.sh/uv/), uninstall borgmatic with pipx (`sudo pipx
|
||||
uninstall borgmatic`) and then [install borgmatic with
|
||||
uv](https://torsion.org/borgmatic/how-to/install-borgmatic/).
|
||||
|
||||
Either one should work just fine. uv is just faster than pipx and also used for
|
||||
borgmatic
|
||||
[development](https://torsion.org/borgmatic/how-to/develop-on-borgmatic/).
|
||||
|
||||
|
||||
#### pip install
|
||||
|
||||
If you originally installed borgmatic with `sudo pip3 install --user`, you can
|
||||
uninstall it first with `sudo pip3 uninstall borgmatic` and then [install it
|
||||
again with
|
||||
pipx](https://torsion.org/borgmatic/how-to/set-up-backups/#installation),
|
||||
uv](https://torsion.org/borgmatic/how-to/install-borgmatic/),
|
||||
which should better isolate borgmatic from your other Python applications.
|
||||
|
||||
But if you [installed borgmatic without pipx or
|
||||
pip3](https://torsion.org/borgmatic/how-to/set-up-backups/#other-ways-to-install),
|
||||
then your upgrade method may be different.
|
||||
|
||||
#### Etc.
|
||||
|
||||
If you installed borgmatic [some other
|
||||
way](https://torsion.org/borgmatic/how-to/install-borgmatic/#other-ways-to-install),
|
||||
then your upgrade method may be different.
|
||||
|
||||
|
||||
### Upgrading your configuration
|
||||
|
||||
@@ -97,4 +97,3 @@ option for limiting the archives used for the `check` action was a separate
|
||||
`prefix` in the `consistency` section. Both of these options are deprecated in
|
||||
favor of the auto-matching behavior (or `match_archives`/`--match-archives`)
|
||||
in newer versions of borgmatic.
|
||||
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -13,6 +13,61 @@ postgresql_databases:
|
||||
- name: users
|
||||
```
|
||||
|
||||
See below for the full set of configuration options available, including
|
||||
hostname, PostgreSQL username, password, etc.
|
||||
|
||||
|
||||
## Permissions
|
||||
|
||||
### Dumping
|
||||
|
||||
In order to dump your database as part of creating a backup, the PostgreSQL user
|
||||
performing the dump needs relevant permissions. A common way to accomplish this
|
||||
is to connect as the PostgreSQL superuser, usually `postgres`. However, if you'd
|
||||
like to connect as a non-superuser, that user will need permissions to:
|
||||
|
||||
* connect to the database
|
||||
* read tables and sequences
|
||||
|
||||
Here is one way to do that with PostgreSQL 14+:
|
||||
|
||||
```sql
|
||||
GRANT CONNECT ON DATABASE example_database TO database_user;
|
||||
GRANT pg_read_all_data TO database_user;
|
||||
```
|
||||
|
||||
And here is an alternate way to accomplish something similar that limits read access
|
||||
to a particular schema instead of the whole cluster. Replace "public" with the
|
||||
name of the schema you're using:
|
||||
|
||||
```sql
|
||||
GRANT CONNECT ON DATABASE example_database TO database_user;
|
||||
GRANT USAGE ON SCHEMA public TO database_user;
|
||||
-- Grant read privileges on all current and future tables in the schema.
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA public TO database_user;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO database_user;
|
||||
-- Grant read privileges on all current and future indexes in the schema.
|
||||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO database_user;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO database_user;
|
||||
```
|
||||
|
||||
### Restoring
|
||||
|
||||
If you also want this user to be able to restore your database, and you're not
|
||||
restoring as the PostgreSQL superuser, then you'll need to grant write and
|
||||
`ANALYZE` permissions as well. For instance:
|
||||
|
||||
```sql
|
||||
GRANT pg_write_all_data TO database_user;
|
||||
GRANT pg_maintain TO database_user;
|
||||
```
|
||||
|
||||
Or you can perform schema-level grants if you prefer.
|
||||
|
||||
For more information, see the PostgreSQL documentation on [PostgreSQL predefined
|
||||
roles](https://www.postgresql.org/docs/current/predefined-roles.html) and
|
||||
[privileges](https://www.postgresql.org/docs/current/ddl-priv.html).
|
||||
|
||||
|
||||
## Full configuration
|
||||
|
||||
@@ -21,3 +76,8 @@ postgresql_databases:
|
||||
```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/)
|
||||
|
||||
@@ -12,12 +12,11 @@ us today such as: Telegram, Discord, Slack, Amazon SNS, Gotify, etc."
|
||||
|
||||
Depending on how you installed borgmatic, it may not have come with Apprise.
|
||||
For instance, if you originally [installed borgmatic with
|
||||
pipx](https://torsion.org/borgmatic/how-to/set-up-backups/),
|
||||
uv](https://torsion.org/borgmatic/how-to/install-borgmatic/),
|
||||
run the following to install Apprise so borgmatic can use it:
|
||||
|
||||
```bash
|
||||
sudo pipx uninstall borgmatic
|
||||
sudo pipx install borgmatic[Apprise]
|
||||
sudo uv tool install borgmatic[Apprise]
|
||||
```
|
||||
|
||||
Omit `sudo` if borgmatic is installed as a non-root user.
|
||||
|
||||
@@ -89,3 +89,34 @@ for more information.
|
||||
<span class="minilink minilink-addedin">New in version 2.0.0</span>Set the
|
||||
defaults for these flags in your borgmatic configuration via the
|
||||
`monitoring_verbosity`, `list`, and `statistics` options.
|
||||
|
||||
|
||||
### Mutual TLS authentication
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 2.1.5</span> Since Loki
|
||||
does not come with a built-in authentication layer
|
||||
[(doc)](https://grafana.com/docs/loki/latest/operations/authentication/), this
|
||||
feature is typically used alongside a reverse proxy (such as
|
||||
[nginx](https://docs.nginx.com/waf/configure/secure-mtls/) or
|
||||
[Traefik](https://doc.traefik.io/traefik/reference/routing-configuration/http/tls/tls-options/#client-authentication-mtls))
|
||||
that handles mTLS termination.
|
||||
|
||||
If your setup is configured for mTLS authentication, you can provide a client
|
||||
certificate and private key:
|
||||
|
||||
```yaml
|
||||
loki:
|
||||
url: https://loki.fqdn/loki/api/v1/push
|
||||
|
||||
labels:
|
||||
app: borgmatic
|
||||
|
||||
tls:
|
||||
cert_path: /etc/borgmatic/loki-client.crt
|
||||
key_path: /etc/borgmatic/loki-client.key
|
||||
```
|
||||
|
||||
Both `cert_path` and `key_path` must be
|
||||
[PEM-encoded](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail). They are
|
||||
passed directly to the underlying HTTP client, so the standard mutual TLS
|
||||
handshake is performed for every request borgmatic sends to Loki.
|
||||
|
||||
@@ -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
@@ -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,6 +1,6 @@
|
||||
[project]
|
||||
name = "borgmatic"
|
||||
version = "2.1.3"
|
||||
version = "2.1.6.dev0"
|
||||
authors = [
|
||||
{ name="Dan Helfman", email="witten@torsion.org" },
|
||||
]
|
||||
@@ -44,7 +44,7 @@ namespaces = false
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = "tests"
|
||||
addopts = "--cov-report term-missing:skip-covered --cov=borgmatic --no-cov-on-fail --cov-fail-under=100 --ignore=tests/end-to-end"
|
||||
addopts = "--cov-report term-missing:skip-covered --cov=borgmatic --no-cov-on-fail --cov-fail-under=100 --ignore=tests/end-to-end --timeout=120"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
@@ -100,6 +100,7 @@ ignore = [
|
||||
"S404", # subprocess import
|
||||
"SIM115", # open() without context manager
|
||||
"SIM905", # split() on literal string
|
||||
"TRY004", # type check and raise something other than TypeError
|
||||
]
|
||||
|
||||
[tool.ruff.lint.flake8-quotes]
|
||||
|
||||
@@ -43,6 +43,16 @@ twine upload -r pypi --username __token__ "$wheel_path"
|
||||
scripts/export-docs-from-image
|
||||
docs_path=dist/borgmatic-docs.tar.gz
|
||||
|
||||
# Build stand-alone binary.
|
||||
uv venv --python 3.13 --clear binary
|
||||
source binary/bin/activate
|
||||
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
|
||||
standalone_binary_path="dist/borgmatic-${version}-binary-linux-glibc-x86_64"
|
||||
mv borgmatic.bin "$standalone_binary_path"
|
||||
|
||||
# Set release changelogs on projects.torsion.org and GitHub.
|
||||
release_changelog="$(cat NEWS | sed '/^$/q' | grep -v '^\S')"
|
||||
escaped_release_changelog="$(echo "$release_changelog" | sed -z 's/\n/\\n/g' | sed -z 's/\"/\\"/g')"
|
||||
@@ -68,6 +78,11 @@ curl --silent --request POST \
|
||||
--header "Authorization: token $projects_token" \
|
||||
--header "Accept: application/json" \
|
||||
--form attachment=@"$docs_path"
|
||||
curl --silent --request POST \
|
||||
"https://projects.torsion.org/api/v1/repos/borgmatic-collective/borgmatic/releases/$release_id/assets?name=$(basename $standalone_binary_path)" \
|
||||
--header "Authorization: token $projects_token" \
|
||||
--header "Accept: application/json" \
|
||||
--form attachment=@"$standalone_binary_path"
|
||||
|
||||
github-release create --token="$github_token" --owner=witten --repo=borgmatic --tag="$version" --target_commit="main" \
|
||||
--name="borgmatic $version" --body="$release_changelog"
|
||||
|
||||
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
-24
@@ -1,27 +1,30 @@
|
||||
apprise==1.9.5
|
||||
attrs==25.4.0
|
||||
certifi==2025.10.5
|
||||
charset-normalizer==3.4.4
|
||||
# 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.1
|
||||
coverage==7.11.0
|
||||
flexmock==0.12.2
|
||||
idna==3.11
|
||||
iniconfig==2.3.0
|
||||
jsonschema==4.25.1
|
||||
jsonschema-specifications==2025.9.1
|
||||
markdown==3.9
|
||||
oauthlib==3.3.1
|
||||
packaging==25.0
|
||||
pluggy==1.6.0
|
||||
pygments==2.19.2
|
||||
pytest==8.4.2
|
||||
pytest-cov==7.0.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.32.5
|
||||
requests-oauthlib==2.0.0
|
||||
rpds-py==0.28.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
|
||||
|
||||
@@ -44,7 +44,7 @@ services:
|
||||
MONGO_INITDB_ROOT_PASSWORD: test2
|
||||
command: docker-entrypoint.sh --port=27018
|
||||
tests:
|
||||
image: docker.io/alpine:3.22
|
||||
image: docker.io/alpine:3.23
|
||||
environment:
|
||||
TEST_CONTAINER: true
|
||||
volumes:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -69,6 +69,7 @@ class Background_web_server:
|
||||
self.thread.join()
|
||||
|
||||
|
||||
FINISH = 1
|
||||
START_AND_FINISH = 2
|
||||
START_LOG_AND_FINISH = 3
|
||||
|
||||
@@ -94,7 +95,7 @@ START_LOG_AND_FINISH = 3
|
||||
),
|
||||
(
|
||||
'loki:\n url: http://localhost:12345/loki/api/v1/push\n labels:\n app: borgmatic\n send_logs: true',
|
||||
START_AND_FINISH,
|
||||
FINISH,
|
||||
),
|
||||
(
|
||||
'loki:\n url: http://localhost:12345/loki/api/v1/push\n labels:\n app: borgmatic',
|
||||
|
||||
@@ -35,7 +35,7 @@ def assert_command_does_not_duplicate_flags(command, *args, **kwargs):
|
||||
if '--json' in command:
|
||||
return '{}'
|
||||
|
||||
return None
|
||||
return ''
|
||||
|
||||
|
||||
def fuzz_argument(arguments, argument_name):
|
||||
@@ -159,10 +159,7 @@ def test_make_repo_list_command_does_not_duplicate_flags_or_raise():
|
||||
def test_display_archives_info_command_does_not_duplicate_flags_or_raise():
|
||||
arguments = borgmatic.commands.arguments.parse_arguments({}, 'info')['info']
|
||||
flexmock(borgmatic.borg.info).should_receive('execute_command_and_capture_output').replace_with(
|
||||
assert_command_does_not_duplicate_flags,
|
||||
)
|
||||
flexmock(borgmatic.borg.info).should_receive('execute_command').replace_with(
|
||||
assert_command_does_not_duplicate_flags,
|
||||
lambda command, *args, **kwargs: iter((assert_command_does_not_duplicate_flags(command),)),
|
||||
)
|
||||
|
||||
for argument_name in dir(arguments):
|
||||
|
||||
@@ -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()},
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from borgmatic.hooks.monitoring import loki as module
|
||||
|
||||
|
||||
def test_loki_log_handler_raw_with_send_logs_posts_to_server_after_buffer_full():
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=True, dry_run=False)
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=True, log_level=10, dry_run=False)
|
||||
flexmock(module.requests).should_receive('post').and_return(
|
||||
flexmock(raise_for_status=lambda: ''),
|
||||
).once()
|
||||
@@ -18,7 +18,7 @@ def test_loki_log_handler_raw_with_send_logs_posts_to_server_after_buffer_full()
|
||||
|
||||
|
||||
def test_loki_log_handler_raw_without_send_logs_posts_to_server_without_buffering():
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=False, dry_run=False)
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=False, log_level=10, dry_run=False)
|
||||
flexmock(module.requests).should_receive('post').and_return(
|
||||
flexmock(raise_for_status=lambda: ''),
|
||||
).times(3)
|
||||
@@ -28,7 +28,7 @@ def test_loki_log_handler_raw_without_send_logs_posts_to_server_without_bufferin
|
||||
|
||||
|
||||
def test_loki_log_handler_raw_post_failure_does_not_raise():
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=True, dry_run=False)
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=True, log_level=10, dry_run=False)
|
||||
flexmock(module.requests).should_receive('post').and_return(
|
||||
flexmock(raise_for_status=lambda: (_ for _ in ()).throw(requests.RequestException())),
|
||||
).once()
|
||||
@@ -46,7 +46,10 @@ def test_initialize_monitor_replaces_labels():
|
||||
'labels': {'hostname': '__hostname', 'config': '__config', 'config_full': '__config_path'},
|
||||
}
|
||||
config_filename = '/mock/path/test.yaml'
|
||||
module.initialize_monitor(hook_config, flexmock(), config_filename, flexmock(), dry_run=False)
|
||||
flexmock(module.logging.getLogger()).should_receive('setLevel')
|
||||
module.initialize_monitor(
|
||||
hook_config, flexmock(), config_filename, monitoring_log_level=10, dry_run=False
|
||||
)
|
||||
|
||||
for handler in tuple(logging.getLogger().handlers):
|
||||
if isinstance(handler, module.Loki_log_handler):
|
||||
@@ -63,11 +66,12 @@ def test_initialize_monitor_adds_log_handler():
|
||||
Assert that calling initialize_monitor adds our logger to the root logger.
|
||||
'''
|
||||
hook_config = {'url': 'http://localhost:3100/loki/api/v1/push', 'labels': {'app': 'borgmatic'}}
|
||||
flexmock(module.logging.getLogger()).should_receive('setLevel')
|
||||
module.initialize_monitor(
|
||||
hook_config,
|
||||
flexmock(),
|
||||
config_filename='test.yaml',
|
||||
monitoring_log_level=flexmock(),
|
||||
monitoring_log_level=10,
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
@@ -86,7 +90,7 @@ def test_ping_monitor_sends_log_message():
|
||||
config_filename = 'test.yaml'
|
||||
post_called = False
|
||||
|
||||
def post(url, data, timeout, headers):
|
||||
def post(url, data, timeout, headers, **kwargs):
|
||||
nonlocal post_called
|
||||
post_called = True
|
||||
|
||||
@@ -99,7 +103,10 @@ def test_ping_monitor_sends_log_message():
|
||||
|
||||
flexmock(module.requests).should_receive('post').replace_with(post)
|
||||
|
||||
module.initialize_monitor(hook_config, flexmock(), config_filename, flexmock(), dry_run=False)
|
||||
flexmock(module.logging.getLogger()).should_receive('setLevel')
|
||||
module.initialize_monitor(
|
||||
hook_config, flexmock(), config_filename, monitoring_log_level=10, dry_run=False
|
||||
)
|
||||
module.ping_monitor(
|
||||
hook_config,
|
||||
flexmock(),
|
||||
@@ -108,7 +115,7 @@ def test_ping_monitor_sends_log_message():
|
||||
flexmock(),
|
||||
dry_run=False,
|
||||
)
|
||||
module.destroy_monitor(hook_config, flexmock(), flexmock(), dry_run=False)
|
||||
module.destroy_monitor(hook_config, flexmock(), monitoring_log_level=10, dry_run=False)
|
||||
|
||||
assert post_called
|
||||
|
||||
@@ -121,9 +128,11 @@ def test_destroy_monitor_removes_log_handler():
|
||||
config_filename = 'test.yaml'
|
||||
flexmock(module.requests).should_receive('post').never()
|
||||
|
||||
module.initialize_monitor(hook_config, flexmock(), config_filename, flexmock(), dry_run=False)
|
||||
module.destroy_monitor(hook_config, flexmock(), flexmock(), dry_run=False)
|
||||
flexmock(module.logging.getLogger()).should_receive('setLevel')
|
||||
module.initialize_monitor(
|
||||
hook_config, flexmock(), config_filename, monitoring_log_level=10, dry_run=False
|
||||
)
|
||||
module.destroy_monitor(hook_config, flexmock(), monitoring_log_level=10, dry_run=False)
|
||||
|
||||
for handler in tuple(logging.getLogger().handlers):
|
||||
if isinstance(handler, module.Loki_log_handler):
|
||||
raise AssertionError()
|
||||
assert not isinstance(handler, module.Loki_log_handler)
|
||||
|
||||
@@ -105,18 +105,15 @@ def test_log_outputs_logs_each_line_separately():
|
||||
(),
|
||||
).and_return((there_process.stdout,))
|
||||
|
||||
assert (
|
||||
tuple(
|
||||
module.log_outputs(
|
||||
(hi_process, there_process),
|
||||
exclude_stdouts=(),
|
||||
output_log_level=logging.INFO,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
assert tuple(
|
||||
module.log_outputs(
|
||||
(hi_process, there_process),
|
||||
exclude_stdouts=(),
|
||||
output_log_level=logging.INFO,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
== ()
|
||||
)
|
||||
) == ('there',)
|
||||
|
||||
|
||||
def test_log_outputs_logs_stderr_as_error():
|
||||
@@ -180,18 +177,15 @@ def test_log_outputs_skips_logs_for_process_with_none_stdout():
|
||||
(),
|
||||
).and_return((there_process.stdout,))
|
||||
|
||||
assert (
|
||||
tuple(
|
||||
module.log_outputs(
|
||||
(hi_process, there_process),
|
||||
exclude_stdouts=(),
|
||||
output_log_level=logging.INFO,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
assert tuple(
|
||||
module.log_outputs(
|
||||
(hi_process, there_process),
|
||||
exclude_stdouts=(),
|
||||
output_log_level=logging.INFO,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
)
|
||||
== ()
|
||||
)
|
||||
) == ('there',)
|
||||
|
||||
|
||||
def test_log_outputs_returns_output_without_logging_for_output_log_level_none():
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -1473,6 +1473,43 @@ def test_compare_spot_check_hashes_considers_symlink_path_as_not_matching():
|
||||
) == ('/bar',)
|
||||
|
||||
|
||||
def test_compare_spot_check_hashes_considers_borg_2_symlink_path_as_not_matching():
|
||||
flexmock(module.random).should_receive('SystemRandom').and_return(
|
||||
flexmock(sample=lambda population, count: population[:count]),
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
None,
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module.os.path).should_receive('islink').with_args('/foo').and_return(False)
|
||||
flexmock(module.os.path).should_receive('islink').with_args('/bar').and_return(True)
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output',
|
||||
).with_args(('xxh64sum', '/foo'), working_directory=None).and_yield('hash1 /foo')
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_yield(
|
||||
{'xxh64': 'hash1', 'path': 'foo', 'target': ''},
|
||||
{'xxh64': 'hash2', 'path': 'bar', 'target': ''},
|
||||
)
|
||||
|
||||
assert module.compare_spot_check_hashes(
|
||||
repository={'path': 'repo'},
|
||||
archive='archive',
|
||||
config={
|
||||
'checks': [
|
||||
{
|
||||
'name': 'spot',
|
||||
'data_sample_percentage': 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
local_borg_version=flexmock(),
|
||||
global_arguments=flexmock(),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
source_paths=('/foo', '/bar', '/baz', '/quux'),
|
||||
) == ('/bar',)
|
||||
|
||||
|
||||
def test_compare_spot_check_hashes_considers_non_existent_path_as_not_matching():
|
||||
flexmock(module.random).should_receive('SystemRandom').and_return(
|
||||
flexmock(sample=lambda population, count: population[:count]),
|
||||
@@ -1801,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(())
|
||||
@@ -1836,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(())
|
||||
@@ -1869,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(())
|
||||
@@ -1905,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(())
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -246,10 +246,10 @@ def test_make_match_archives_flags_accepts_default_archive_name_format():
|
||||
)
|
||||
|
||||
|
||||
def test_warn_for_aggressive_archive_flags_without_archive_flags_bails():
|
||||
def test_warn_for_aggressive_archive_flags_without_archive_flags_does_not_warn():
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
|
||||
module.warn_for_aggressive_archive_flags(('borg', '--do-stuff'), '{}')
|
||||
module.warn_for_aggressive_archive_flags(('borg', '--do-stuff'), ())
|
||||
|
||||
|
||||
def test_warn_for_aggressive_archive_flags_with_glob_archives_and_zero_archives_warns():
|
||||
@@ -257,7 +257,7 @@ def test_warn_for_aggressive_archive_flags_with_glob_archives_and_zero_archives_
|
||||
|
||||
module.warn_for_aggressive_archive_flags(
|
||||
('borg', '--glob-archives', 'foo*'),
|
||||
'{"archives": []}',
|
||||
(),
|
||||
)
|
||||
|
||||
|
||||
@@ -266,7 +266,16 @@ def test_warn_for_aggressive_archive_flags_with_match_archives_and_zero_archives
|
||||
|
||||
module.warn_for_aggressive_archive_flags(
|
||||
('borg', '--match-archives', 'foo*'),
|
||||
'{"archives": []}',
|
||||
(),
|
||||
)
|
||||
|
||||
|
||||
def test_warn_for_aggressive_archive_flags_with_match_archives_and_just_exit_code_warns():
|
||||
flexmock(module.logger).should_receive('warning').twice()
|
||||
|
||||
module.warn_for_aggressive_archive_flags(
|
||||
('borg', '--match-archives', 'foo*'),
|
||||
('terminating with success status, rc 0',),
|
||||
)
|
||||
|
||||
|
||||
@@ -275,7 +284,7 @@ def test_warn_for_aggressive_archive_flags_with_glob_archives_and_one_archive_do
|
||||
|
||||
module.warn_for_aggressive_archive_flags(
|
||||
('borg', '--glob-archives', 'foo*'),
|
||||
'{"archives": [{"name": "foo"]}',
|
||||
('this is an archive line',),
|
||||
)
|
||||
|
||||
|
||||
@@ -284,22 +293,10 @@ def test_warn_for_aggressive_archive_flags_with_match_archives_and_one_archive_d
|
||||
|
||||
module.warn_for_aggressive_archive_flags(
|
||||
('borg', '--match-archives', 'foo*'),
|
||||
'{"archives": [{"name": "foo"]}',
|
||||
('this is an archive line',),
|
||||
)
|
||||
|
||||
|
||||
def test_warn_for_aggressive_archive_flags_with_glob_archives_and_invalid_json_does_not_warn():
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
|
||||
module.warn_for_aggressive_archive_flags(('borg', '--glob-archives', 'foo*'), '{"archives": [}')
|
||||
|
||||
|
||||
def test_warn_for_aggressive_archive_flags_with_glob_archives_and_json_missing_archives_does_not_warn():
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
|
||||
module.warn_for_aggressive_archive_flags(('borg', '--glob-archives', 'foo*'), '{}')
|
||||
|
||||
|
||||
def test_omit_flag_removes_flag_from_arguments():
|
||||
assert module.omit_flag(('borg', 'create', '--flag', '--other'), '--flag') == (
|
||||
'borg',
|
||||
|
||||
@@ -565,15 +565,14 @@ def test_make_info_command_with_date_based_matching_passes_through_to_command():
|
||||
)
|
||||
|
||||
|
||||
def test_display_archives_info_calls_two_commands():
|
||||
def test_display_archives_info_calls_borg_command():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_info_command')
|
||||
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').and_yield().once()
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').and_yield('').once()
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').once()
|
||||
|
||||
module.display_archives_info(
|
||||
repository_path='repo',
|
||||
@@ -592,7 +591,6 @@ def test_display_archives_info_with_json_calls_json_command_only():
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').and_yield('{}')
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
assert (
|
||||
module.display_archives_info(
|
||||
@@ -616,20 +614,13 @@ def test_display_archives_info_calls_borg_with_working_directory():
|
||||
)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
full_command=object,
|
||||
output_log_level=int,
|
||||
environment=object,
|
||||
working_directory='/working/dir',
|
||||
borg_local_path=object,
|
||||
borg_exit_codes=object,
|
||||
).and_yield().once()
|
||||
).and_yield('').once()
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
full_command=object,
|
||||
output_log_level=object,
|
||||
environment=object,
|
||||
working_directory='/working/dir',
|
||||
borg_local_path=object,
|
||||
borg_exit_codes=object,
|
||||
).once()
|
||||
|
||||
module.display_archives_info(
|
||||
repository_path='repo',
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -115,6 +115,7 @@ def test_get_latest_archive_calls_borg_with_flags():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -144,6 +145,7 @@ def test_get_latest_archive_with_log_info_calls_borg_without_info_flag():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -174,6 +176,7 @@ def test_get_latest_archive_with_log_debug_calls_borg_without_debug_flag():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -204,6 +207,7 @@ def test_get_latest_archive_with_local_path_calls_borg_via_local_path():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -234,6 +238,7 @@ def test_get_latest_archive_with_exit_codes_calls_borg_using_them():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -267,6 +272,7 @@ def test_get_latest_archive_with_remote_path_calls_borg_with_remote_path_flags()
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -300,6 +306,7 @@ def test_get_latest_archive_with_umask_calls_borg_with_umask_flags():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -329,6 +336,7 @@ def test_get_latest_archive_without_archives_raises():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
@@ -358,6 +366,7 @@ def test_get_latest_archive_with_lock_wait_calls_borg_with_lock_wait_flags():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -380,6 +389,38 @@ def test_get_latest_archive_with_lock_wait_calls_borg_with_lock_wait_flags():
|
||||
)
|
||||
|
||||
|
||||
def test_get_latest_archive_with_match_archives_calls_borg_with_match_archives_flags():
|
||||
expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
|
||||
flexmock(module.feature).should_receive('available').and_return(False)
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(
|
||||
('--match-archives', 'foo')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
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', 'list', '--log-json', '--match-archives', 'foo', *BORG_LIST_LATEST_ARGUMENTS),
|
||||
environment=None,
|
||||
working_directory=None,
|
||||
borg_local_path='borg',
|
||||
borg_exit_codes=None,
|
||||
).and_yield(json.dumps({'archives': [expected_archive]}))
|
||||
|
||||
assert (
|
||||
module.get_latest_archive(
|
||||
'repo',
|
||||
config={'match_archives': 'foo'},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(),
|
||||
)
|
||||
== expected_archive
|
||||
)
|
||||
|
||||
|
||||
def test_get_latest_archive_calls_borg_with_list_extra_borg_options():
|
||||
expected_archive = {'name': 'archive-name', 'id': 'd34db33f'}
|
||||
flexmock(module.feature).should_receive('available').and_return(False)
|
||||
@@ -387,6 +428,7 @@ def test_get_latest_archive_calls_borg_with_list_extra_borg_options():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -424,6 +466,7 @@ def test_get_latest_archive_with_feature_available_calls_borg_with_repo_list_ext
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -464,6 +507,7 @@ def test_get_latest_archive_with_consider_checkpoints_calls_borg_with_consider_c
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -497,6 +541,7 @@ def test_get_latest_archive_with_consider_checkpoints_and_feature_available_call
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
@@ -527,6 +572,7 @@ def test_get_latest_archive_calls_borg_with_working_directory():
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('last', 1).and_return(
|
||||
('--last', '1')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
|
||||
@@ -1108,7 +1154,7 @@ def test_make_repo_list_command_with_match_archives_calls_borg_with_match_archiv
|
||||
assert command == ('borg', 'list', '--log-json', '--match-archives', 'foo-*', 'repo')
|
||||
|
||||
|
||||
def test_list_repository_calls_two_commands():
|
||||
def test_list_repository_calls_borg_command():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('make_repo_list_command')
|
||||
@@ -1116,7 +1162,6 @@ def test_list_repository_calls_two_commands():
|
||||
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').and_yield('').once()
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').once()
|
||||
|
||||
module.list_repository(
|
||||
repository_path='repo',
|
||||
@@ -1127,14 +1172,13 @@ def test_list_repository_calls_two_commands():
|
||||
)
|
||||
|
||||
|
||||
def test_list_repository_with_json_calls_json_command_only():
|
||||
def test_list_repository_with_json_calls_borg_json_command_only():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module).should_receive('make_repo_list_command')
|
||||
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').and_yield('{}')
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
assert (
|
||||
module.list_repository(
|
||||
@@ -1206,20 +1250,13 @@ def test_list_repository_calls_borg_with_working_directory():
|
||||
)
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
full_command=object,
|
||||
output_log_level=int,
|
||||
environment=object,
|
||||
working_directory='/working/dir',
|
||||
borg_local_path=object,
|
||||
borg_exit_codes=object,
|
||||
).and_yield('').once()
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
full_command=object,
|
||||
output_log_level=object,
|
||||
environment=object,
|
||||
working_directory='/working/dir',
|
||||
borg_local_path=object,
|
||||
borg_exit_codes=object,
|
||||
).once()
|
||||
|
||||
module.list_repository(
|
||||
repository_path='repo',
|
||||
|
||||
@@ -2582,23 +2582,40 @@ def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
|
||||
|
||||
def test_check_and_show_help_on_no_args_shows_help_when_no_args_and_default_actions_false():
|
||||
flexmock(module.sys).should_receive('argv').and_return(['borgmatic'])
|
||||
flexmock(module).should_receive('parse_arguments').with_args('--help').once()
|
||||
flexmock(module).should_receive('parse_arguments').with_args({}, '--help').once()
|
||||
flexmock(module.sys).should_receive('exit').with_args(0).once()
|
||||
module.check_and_show_help_on_no_args({'test.yaml': {'default_actions': False}})
|
||||
|
||||
module.check_and_show_help_on_no_args(
|
||||
configs={'test.yaml': {'default_actions': False}}, schema={}
|
||||
)
|
||||
|
||||
|
||||
def test_check_and_show_help_on_no_args_does_not_show_help_when_no_args_and_no_configurations():
|
||||
flexmock(module.sys).should_receive('argv').and_return(['borgmatic'])
|
||||
flexmock(module).should_receive('parse_arguments').never()
|
||||
flexmock(module.sys).should_receive('exit').never()
|
||||
|
||||
module.check_and_show_help_on_no_args(configs={}, schema={})
|
||||
|
||||
|
||||
def test_check_and_show_help_on_no_args_does_not_show_help_when_no_args_and_default_actions_true():
|
||||
flexmock(module.sys).should_receive('argv').and_return(['borgmatic'])
|
||||
flexmock(module).should_receive('parse_arguments').never()
|
||||
flexmock(module.sys).should_receive('exit').never()
|
||||
module.check_and_show_help_on_no_args({'test.yaml': {'default_actions': True}})
|
||||
|
||||
module.check_and_show_help_on_no_args(
|
||||
configs={'test.yaml': {'default_actions': True}}, schema={}
|
||||
)
|
||||
|
||||
|
||||
def test_check_and_show_help_on_no_args_does_not_show_help_when_args_provided():
|
||||
flexmock(module.sys).should_receive('argv').and_return(['borgmatic', '--create'])
|
||||
flexmock(module).should_receive('parse_arguments').never()
|
||||
flexmock(module.sys).should_receive('exit').never()
|
||||
module.check_and_show_help_on_no_args({'test.yaml': {'default_actions': False}})
|
||||
|
||||
module.check_and_show_help_on_no_args(
|
||||
configs={'test.yaml': {'default_actions': False}}, schema={}
|
||||
)
|
||||
|
||||
|
||||
def test_check_and_show_help_on_no_args_with_no_default_actions_in_all_configs():
|
||||
@@ -2611,10 +2628,10 @@ def test_check_and_show_help_on_no_args_with_no_default_actions_in_all_configs()
|
||||
}
|
||||
|
||||
# Expect help to be shown
|
||||
flexmock(module).should_receive('parse_arguments').with_args('--help').once()
|
||||
flexmock(module).should_receive('parse_arguments').with_args({}, '--help').once()
|
||||
flexmock(module.sys).should_receive('exit').with_args(0).once()
|
||||
|
||||
module.check_and_show_help_on_no_args(configs)
|
||||
module.check_and_show_help_on_no_args(configs=configs, schema={})
|
||||
|
||||
|
||||
def test_check_and_show_help_on_no_args_with_conflicting_configs():
|
||||
@@ -2630,7 +2647,7 @@ def test_check_and_show_help_on_no_args_with_conflicting_configs():
|
||||
flexmock(module).should_receive('parse_arguments').never()
|
||||
flexmock(module.sys).should_receive('exit').never()
|
||||
|
||||
module.check_and_show_help_on_no_args(configs)
|
||||
module.check_and_show_help_on_no_args(configs=configs, schema={})
|
||||
|
||||
|
||||
def test_get_singular_option_value_with_conflicting_values_exits():
|
||||
|
||||
@@ -47,11 +47,6 @@ from borgmatic.config import normalize as module
|
||||
{'prefix': 'foo'},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'location': {'prefix': 'foo'}, 'consistency': {'prefix': 'foo'}},
|
||||
{'prefix': 'foo'},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'location': {'prefix': 'foo'}, 'consistency': {'bar': 'baz'}},
|
||||
{'prefix': 'foo', 'bar': 'baz'},
|
||||
@@ -62,11 +57,6 @@ from borgmatic.config import normalize as module
|
||||
{'umask': 'foo'},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'storage': {'umask': 'foo'}, 'hooks': {'umask': 'foo'}},
|
||||
{'umask': 'foo'},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'storage': {'umask': 'foo'}, 'hooks': {'bar': 'baz'}},
|
||||
{'umask': 'foo', 'bar': 'baz'},
|
||||
@@ -288,11 +278,6 @@ def test_normalize_commands_moves_individual_command_hooks_to_unified_commands(
|
||||
{'checks': [{'name': 'archives'}]},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'checks': ['archives']},
|
||||
{'checks': [{'name': 'archives'}]},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'numeric_owner': False},
|
||||
{'numeric_ids': False},
|
||||
|
||||
+81
-107
@@ -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',
|
||||
|
||||
@@ -7,14 +7,12 @@ from borgmatic.hooks.data_source import bootstrap as module
|
||||
|
||||
|
||||
def test_resolve_config_path_symlinks_passes_through_non_symlink():
|
||||
flexmock(module.os.path).should_receive('abspath').replace_with(lambda path: path)
|
||||
flexmock(module.os.path).should_receive('islink').and_return(False)
|
||||
|
||||
assert tuple(module.resolve_config_path_symlinks('test.yaml')) == ('test.yaml',)
|
||||
|
||||
|
||||
def test_resolve_config_path_symlinks_follows_each_symlink():
|
||||
flexmock(module.os.path).should_receive('abspath').replace_with(lambda path: path)
|
||||
flexmock(module.os.path).should_receive('islink').with_args('test.yaml').and_return(True)
|
||||
flexmock(module.os.path).should_receive('islink').with_args('dest1.yaml').and_return(True)
|
||||
flexmock(module.os.path).should_receive('islink').with_args('dest2.yaml').and_return(False)
|
||||
@@ -29,9 +27,29 @@ def test_resolve_config_path_symlinks_follows_each_symlink():
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_config_path_symlinks_follows_each_relative_symlink():
|
||||
flexmock(module.os.path).should_receive('islink').with_args('foo/bar/test.yaml').and_return(
|
||||
True
|
||||
)
|
||||
flexmock(module.os.path).should_receive('islink').with_args('foo/dest1.yaml').and_return(True)
|
||||
flexmock(module.os.path).should_receive('islink').with_args('dest2.yaml').and_return(False)
|
||||
flexmock(module.os).should_receive('readlink').with_args('foo/bar/test.yaml').and_return(
|
||||
'../dest1.yaml'
|
||||
)
|
||||
flexmock(module.os).should_receive('readlink').with_args('foo/dest1.yaml').and_return(
|
||||
'../dest2.yaml'
|
||||
)
|
||||
flexmock(module.os).should_receive('readlink').with_args('dest2.yaml').never()
|
||||
|
||||
assert tuple(module.resolve_config_path_symlinks('foo/bar/test.yaml')) == (
|
||||
'foo/bar/test.yaml',
|
||||
'foo/dest1.yaml',
|
||||
'dest2.yaml',
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_config_path_symlinks_with_too_many_symlinks_raises():
|
||||
flexmock(module).MAXIMUM_CONFIG_SYMLINKS_TO_FOLLOW = 2
|
||||
flexmock(module.os.path).should_receive('abspath').replace_with(lambda path: path)
|
||||
flexmock(module.os.path).should_receive('islink').with_args('test.yaml').and_return(True)
|
||||
flexmock(module.os.path).should_receive('islink').with_args('dest1.yaml').and_return(True)
|
||||
flexmock(module.os.path).should_receive('islink').with_args('dest2.yaml').and_return(True)
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
|
||||
@@ -223,40 +223,34 @@ def test_get_datasets_to_backup_with_invalid_list_output_raises():
|
||||
module.get_datasets_to_backup('zfs', patterns=(Pattern('/foo'), Pattern('/bar')))
|
||||
|
||||
|
||||
def test_get_all_dataset_mount_points_omits_none():
|
||||
def test_get_all_dataset_mount_points_omits_none_and_reverse_orders_by_mount_path():
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output',
|
||||
).and_yield(
|
||||
'/dataset',
|
||||
'none',
|
||||
'/other',
|
||||
'dataset\t/path',
|
||||
'thing\tnone',
|
||||
'other\t/other',
|
||||
)
|
||||
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
|
||||
'get_contained_patterns',
|
||||
).and_return((Pattern('/dataset'),))
|
||||
|
||||
assert module.get_all_dataset_mount_points('zfs') == (
|
||||
('/dataset'),
|
||||
('/other'),
|
||||
assert tuple(module.get_all_dataset_mount_points('zfs').items()) == (
|
||||
('dataset', '/path'),
|
||||
('other', '/other'),
|
||||
)
|
||||
|
||||
|
||||
def test_get_all_dataset_mount_points_omits_duplicates():
|
||||
def test_get_all_dataset_mount_points_omits_duplicates_and_reverse_orders_by_mount_path():
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output',
|
||||
).and_return(
|
||||
'/dataset',
|
||||
'/other',
|
||||
'/dataset',
|
||||
'/other',
|
||||
'dataset\t/path',
|
||||
'other\t/other',
|
||||
'dataset\t/path',
|
||||
'other\t/other',
|
||||
)
|
||||
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
|
||||
'get_contained_patterns',
|
||||
).and_return((Pattern('/dataset'),))
|
||||
|
||||
assert module.get_all_dataset_mount_points('zfs') == (
|
||||
('/dataset'),
|
||||
('/other'),
|
||||
assert tuple(module.get_all_dataset_mount_points('zfs').items()) == (
|
||||
('dataset', '/path'),
|
||||
('other', '/other'),
|
||||
)
|
||||
|
||||
|
||||
@@ -525,7 +519,12 @@ def test_get_all_snapshots_parses_list_output():
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_unmounts_and_destroys_snapshots():
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(
|
||||
{'dataset': '/mnt/dataset'}
|
||||
)
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob',
|
||||
).and_return('/run/borgmatic')
|
||||
@@ -533,15 +532,11 @@ def test_remove_data_source_dumps_unmounts_and_destroys_snapshots():
|
||||
lambda path: [path.replace('*', 'b33f')],
|
||||
)
|
||||
flexmock(module.os.path).should_receive('isdir').and_return(True)
|
||||
flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
|
||||
flexmock(module.shutil).should_receive('rmtree')
|
||||
flexmock(module).should_receive('unmount_snapshot').with_args(
|
||||
'umount',
|
||||
'/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
|
||||
).once()
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
|
||||
)
|
||||
flexmock(module).should_receive('destroy_snapshot').with_args(
|
||||
'zfs',
|
||||
'dataset@borgmatic-1234',
|
||||
@@ -557,7 +552,12 @@ def test_remove_data_source_dumps_unmounts_and_destroys_snapshots():
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_use_custom_commands():
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(
|
||||
{'dataset': '/mnt/dataset'}
|
||||
)
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob',
|
||||
).and_return('/run/borgmatic')
|
||||
@@ -565,15 +565,11 @@ def test_remove_data_source_dumps_use_custom_commands():
|
||||
lambda path: [path.replace('*', 'b33f')],
|
||||
)
|
||||
flexmock(module.os.path).should_receive('isdir').and_return(True)
|
||||
flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
|
||||
flexmock(module.shutil).should_receive('rmtree')
|
||||
flexmock(module).should_receive('unmount_snapshot').with_args(
|
||||
'/usr/local/bin/umount',
|
||||
'/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
|
||||
).once()
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
|
||||
)
|
||||
flexmock(module).should_receive('destroy_snapshot').with_args(
|
||||
'/usr/local/bin/zfs',
|
||||
'dataset@borgmatic-1234',
|
||||
@@ -639,7 +635,12 @@ def test_remove_data_source_dumps_bails_for_zfs_command_error():
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_bails_for_missing_umount_command():
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(
|
||||
{'dataset': '/mnt/dataset'}
|
||||
)
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob',
|
||||
).and_return('/run/borgmatic')
|
||||
@@ -647,13 +648,11 @@ def test_remove_data_source_dumps_bails_for_missing_umount_command():
|
||||
lambda path: [path.replace('*', 'b33f')],
|
||||
)
|
||||
flexmock(module.os.path).should_receive('isdir').and_return(True)
|
||||
flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
|
||||
flexmock(module.shutil).should_receive('rmtree')
|
||||
flexmock(module).should_receive('unmount_snapshot').with_args(
|
||||
'/usr/local/bin/umount',
|
||||
'/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
|
||||
).and_raise(FileNotFoundError)
|
||||
flexmock(module).should_receive('get_all_snapshots').never()
|
||||
flexmock(module).should_receive('destroy_snapshot').never()
|
||||
hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'}
|
||||
|
||||
@@ -667,7 +666,12 @@ def test_remove_data_source_dumps_bails_for_missing_umount_command():
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_swallows_umount_command_error():
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(
|
||||
{'dataset': '/mnt/dataset'}
|
||||
)
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob',
|
||||
).and_return('/run/borgmatic')
|
||||
@@ -675,15 +679,11 @@ def test_remove_data_source_dumps_swallows_umount_command_error():
|
||||
lambda path: [path.replace('*', 'b33f')],
|
||||
)
|
||||
flexmock(module.os.path).should_receive('isdir').and_return(True)
|
||||
flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
|
||||
flexmock(module.shutil).should_receive('rmtree')
|
||||
flexmock(module).should_receive('unmount_snapshot').with_args(
|
||||
'/usr/local/bin/umount',
|
||||
'/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
|
||||
).and_raise(module.subprocess.CalledProcessError(1, 'wtf'))
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
|
||||
)
|
||||
flexmock(module).should_receive('destroy_snapshot').with_args(
|
||||
'/usr/local/bin/zfs',
|
||||
'dataset@borgmatic-1234',
|
||||
@@ -700,7 +700,12 @@ def test_remove_data_source_dumps_swallows_umount_command_error():
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_skips_unmount_snapshot_directories_that_are_not_actually_directories():
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(
|
||||
{'dataset': '/mnt/dataset'}
|
||||
)
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob',
|
||||
).and_return('/run/borgmatic')
|
||||
@@ -710,9 +715,6 @@ def test_remove_data_source_dumps_skips_unmount_snapshot_directories_that_are_no
|
||||
flexmock(module.os.path).should_receive('isdir').and_return(False)
|
||||
flexmock(module.shutil).should_receive('rmtree').never()
|
||||
flexmock(module).should_receive('unmount_snapshot').never()
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
|
||||
)
|
||||
flexmock(module).should_receive('destroy_snapshot').with_args(
|
||||
'zfs',
|
||||
'dataset@borgmatic-1234',
|
||||
@@ -728,7 +730,12 @@ def test_remove_data_source_dumps_skips_unmount_snapshot_directories_that_are_no
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_that_are_not_actually_directories():
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(
|
||||
{'dataset': '/mnt/dataset'}
|
||||
)
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob',
|
||||
).and_return('/run/borgmatic')
|
||||
@@ -741,12 +748,8 @@ def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_that_are_no
|
||||
flexmock(module.os.path).should_receive('isdir').with_args(
|
||||
'/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
|
||||
).and_return(False)
|
||||
flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
|
||||
flexmock(module.shutil).should_receive('rmtree')
|
||||
flexmock(module).should_receive('unmount_snapshot').never()
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
|
||||
)
|
||||
flexmock(module).should_receive('destroy_snapshot').with_args(
|
||||
'zfs',
|
||||
'dataset@borgmatic-1234',
|
||||
@@ -761,8 +764,13 @@ def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_that_are_no
|
||||
)
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_that_are_empty():
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
|
||||
def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_for_unknown_shapshots():
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(
|
||||
{'dataset': '/mnt/dataset', 'sub': '/mnt/dataset/shadow'}
|
||||
)
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob',
|
||||
).and_return('/run/borgmatic')
|
||||
@@ -775,14 +783,16 @@ def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_that_are_em
|
||||
flexmock(module.os.path).should_receive('isdir').with_args(
|
||||
'/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
|
||||
).and_return(True)
|
||||
flexmock(module.os).should_receive('listdir').with_args(
|
||||
'/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
|
||||
).and_return([])
|
||||
flexmock(module.os.path).should_receive('isdir').with_args(
|
||||
'/run/borgmatic/zfs_snapshots/b33f/mnt/dataset/shadow',
|
||||
).and_return(True)
|
||||
flexmock(module.shutil).should_receive('rmtree')
|
||||
flexmock(module).should_receive('unmount_snapshot').never()
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
|
||||
)
|
||||
flexmock(module).should_receive('unmount_snapshot').with_args(
|
||||
'umount', '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset'
|
||||
).once()
|
||||
flexmock(module).should_receive('unmount_snapshot').with_args(
|
||||
'umount', '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset/shadow'
|
||||
).never()
|
||||
flexmock(module).should_receive('destroy_snapshot').with_args(
|
||||
'zfs',
|
||||
'dataset@borgmatic-1234',
|
||||
@@ -798,7 +808,12 @@ def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_that_are_em
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_after_rmtree_succeeds():
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(
|
||||
{'dataset': '/mnt/dataset'}
|
||||
)
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob',
|
||||
).and_return('/run/borgmatic')
|
||||
@@ -811,12 +826,8 @@ def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_after_rmtre
|
||||
flexmock(module.os.path).should_receive('isdir').with_args(
|
||||
'/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
|
||||
).and_return(True).and_return(False)
|
||||
flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
|
||||
flexmock(module.shutil).should_receive('rmtree')
|
||||
flexmock(module).should_receive('unmount_snapshot').never()
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
|
||||
)
|
||||
flexmock(module).should_receive('destroy_snapshot').with_args(
|
||||
'zfs',
|
||||
'dataset@borgmatic-1234',
|
||||
@@ -832,7 +843,12 @@ def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_after_rmtre
|
||||
|
||||
|
||||
def test_remove_data_source_dumps_with_dry_run_skips_unmount_and_destroy():
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
|
||||
flexmock(module).should_receive('get_all_dataset_mount_points').and_return(
|
||||
{'dataset': '/mnt/dataset'}
|
||||
)
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
|
||||
)
|
||||
flexmock(module.borgmatic.config.paths).should_receive(
|
||||
'replace_temporary_subdirectory_with_glob',
|
||||
).and_return('/run/borgmatic')
|
||||
@@ -840,12 +856,8 @@ def test_remove_data_source_dumps_with_dry_run_skips_unmount_and_destroy():
|
||||
lambda path: [path.replace('*', 'b33f')],
|
||||
)
|
||||
flexmock(module.os.path).should_receive('isdir').and_return(True)
|
||||
flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
|
||||
flexmock(module.shutil).should_receive('rmtree').never()
|
||||
flexmock(module).should_receive('unmount_snapshot').never()
|
||||
flexmock(module).should_receive('get_all_snapshots').and_return(
|
||||
('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
|
||||
)
|
||||
flexmock(module).should_receive('destroy_snapshot').never()
|
||||
|
||||
module.remove_data_source_dumps(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.hooks.monitoring import loki as module
|
||||
@@ -50,10 +51,7 @@ def test_loki_log_buffer_json_serializes_log_lines():
|
||||
assert json.loads(buffer.to_request())['streams'][0]['values'][0][1] == 'Some test log line'
|
||||
|
||||
|
||||
def test_loki_log_handler_add_label_gets_labels():
|
||||
'''
|
||||
Assert that adding labels works.
|
||||
'''
|
||||
def test_loki_log_buffer_add_label_gets_labels():
|
||||
buffer = module.Loki_log_buffer(flexmock(), dry_run=False)
|
||||
|
||||
buffer.add_label('test', 'label')
|
||||
@@ -64,22 +62,66 @@ def test_loki_log_handler_add_label_gets_labels():
|
||||
|
||||
|
||||
def test_loki_log_handler_emit_with_send_logs_records_log_message():
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=True, dry_run=False)
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=True, log_level=10, dry_run=False)
|
||||
flexmock(handler).should_receive('raw').once()
|
||||
|
||||
handler.emit(flexmock(getMessage=lambda: 'Some test log line'))
|
||||
|
||||
|
||||
def test_loki_log_handler_emit_without_send_logs_skips_log_message():
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=False, dry_run=False)
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=False, log_level=10, dry_run=False)
|
||||
flexmock(handler).should_receive('raw').never()
|
||||
|
||||
handler.emit(flexmock(getMessage=lambda: 'Some test log line'))
|
||||
|
||||
|
||||
def test_loki_log_handler_flush_with_empty_buffer_does_not_raise():
|
||||
'''
|
||||
Test that flushing an empty buffer does indeed nothing.
|
||||
'''
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=False, dry_run=False)
|
||||
handler = module.Loki_log_handler(flexmock(), send_logs=False, log_level=10, dry_run=False)
|
||||
|
||||
handler.flush()
|
||||
|
||||
|
||||
def test_loki_log_buffer_init_with_tls_stores_cert_and_key_paths():
|
||||
buffer = module.Loki_log_buffer(
|
||||
flexmock(),
|
||||
dry_run=False,
|
||||
tls_cert_path='/path/to/cert.crt',
|
||||
tls_key_path='/path/to/key.key',
|
||||
)
|
||||
|
||||
assert buffer.tls_cert_path == '/path/to/cert.crt'
|
||||
assert buffer.tls_key_path == '/path/to/key.key'
|
||||
|
||||
|
||||
def test_loki_log_handler_init_with_tls_passes_paths_to_buffer():
|
||||
handler = module.Loki_log_handler(
|
||||
flexmock(),
|
||||
send_logs=False,
|
||||
log_level=10,
|
||||
dry_run=False,
|
||||
tls_cert_path='/path/to/cert.crt',
|
||||
tls_key_path='/path/to/key.key',
|
||||
)
|
||||
|
||||
assert handler.buffer.tls_cert_path == '/path/to/cert.crt'
|
||||
assert handler.buffer.tls_key_path == '/path/to/key.key'
|
||||
|
||||
|
||||
def test_initialize_monitor_with_only_cert_path_raises():
|
||||
hook_config = {
|
||||
'url': 'http://localhost:3100/loki/api/v1/push',
|
||||
'tls': {'cert_path': '/path/to/cert.crt'},
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.initialize_monitor(hook_config, {}, 'test.yaml', 1, False)
|
||||
|
||||
|
||||
def test_initialize_monitor_with_only_key_path_raises():
|
||||
hook_config = {
|
||||
'url': 'http://localhost:3100/loki/api/v1/push',
|
||||
'tls': {'key_path': '/path/to/key.key'},
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.initialize_monitor(hook_config, {}, 'test.yaml', 1, False)
|
||||
|
||||
+122
-24
@@ -31,8 +31,6 @@ def test_command_is_borg_matches_local_path_to_command(command, borg_local_path,
|
||||
(False, 1, None, module.Exit_status.ERROR),
|
||||
(True, 1, None, module.Exit_status.WARNING),
|
||||
(False, 100, None, module.Exit_status.ERROR),
|
||||
(False, 2, None, module.Exit_status.ERROR),
|
||||
(True, 2, None, module.Exit_status.ERROR),
|
||||
(True, 100, None, module.Exit_status.WARNING),
|
||||
(False, 0, None, module.Exit_status.SUCCESS),
|
||||
(True, 0, None, module.Exit_status.SUCCESS),
|
||||
@@ -405,7 +403,7 @@ def test_log_buffer_lines_with_ready_buffer_and_running_process_handles_each_log
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -437,7 +435,7 @@ def test_log_buffer_lines_with_ready_buffer_and_capture_process_yields_each_line
|
||||
) == ('message', 'message')
|
||||
|
||||
|
||||
def test_log_buffer_lines_with_ready_buffer_and_log_level_and_capture_process_does_not_yield_each_line():
|
||||
def test_log_buffer_lines_with_ready_buffer_and_same_log_level_and_capture_process_yields_each_line():
|
||||
process = flexmock(poll=lambda: None, stderr=flexmock(), args=flexmock())
|
||||
buffer_readers = {
|
||||
flexmock(): module.Buffer_reader(lines=iter((('hi', 'there'),)), process=process)
|
||||
@@ -448,7 +446,31 @@ def test_log_buffer_lines_with_ready_buffer_and_log_level_and_capture_process_do
|
||||
).and_return(list(buffer_readers.keys()), [], [])
|
||||
flexmock(module).should_receive('parse_log_line').and_return(flexmock())
|
||||
flexmock(module).should_receive('handle_log_record').and_return(
|
||||
flexmock(levelno=10, getMessage=lambda: 'message')
|
||||
flexmock(levelno=module.logging.INFO, getMessage=lambda: 'message')
|
||||
).twice()
|
||||
|
||||
assert tuple(
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
) == ('message', 'message')
|
||||
|
||||
|
||||
def test_log_buffer_lines_with_ready_buffer_and_higher_log_level_and_capture_process_does_not_yield_each_line():
|
||||
process = flexmock(poll=lambda: None, stderr=flexmock(), args=flexmock())
|
||||
buffer_readers = {
|
||||
flexmock(): module.Buffer_reader(lines=iter((('hi', 'there'),)), process=process)
|
||||
}
|
||||
process_metadatas = {process: module.Process_metadata(last_lines=[], capture=True)}
|
||||
flexmock(module.select).should_receive('select').with_args(
|
||||
buffer_readers.keys(), [], []
|
||||
).and_return(list(buffer_readers.keys()), [], [])
|
||||
flexmock(module).should_receive('parse_log_line').and_return(flexmock())
|
||||
flexmock(module).should_receive('handle_log_record').and_return(
|
||||
flexmock(levelno=module.logging.DEBUG, getMessage=lambda: 'message')
|
||||
).twice()
|
||||
|
||||
assert (
|
||||
@@ -456,7 +478,7 @@ def test_log_buffer_lines_with_ready_buffer_and_log_level_and_capture_process_do
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -464,6 +486,30 @@ def test_log_buffer_lines_with_ready_buffer_and_log_level_and_capture_process_do
|
||||
)
|
||||
|
||||
|
||||
def test_log_buffer_lines_with_ready_buffer_and_log_level_equal_to_output_log_level_and_capture_process_yields_each_line():
|
||||
process = flexmock(poll=lambda: None, stderr=flexmock(), args=flexmock())
|
||||
buffer_readers = {
|
||||
flexmock(): module.Buffer_reader(lines=iter((('hi', 'there'),)), process=process)
|
||||
}
|
||||
process_metadatas = {process: module.Process_metadata(last_lines=[], capture=True)}
|
||||
flexmock(module.select).should_receive('select').with_args(
|
||||
buffer_readers.keys(), [], []
|
||||
).and_return(list(buffer_readers.keys()), [], [])
|
||||
flexmock(module).should_receive('parse_log_line').and_return(flexmock())
|
||||
flexmock(module).should_receive('handle_log_record').and_return(
|
||||
flexmock(levelno=module.logging.INFO, getMessage=lambda: 'message')
|
||||
).twice()
|
||||
|
||||
assert tuple(
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
) == ('message', 'message')
|
||||
|
||||
|
||||
def test_log_buffer_lines_with_ready_buffer_and_finished_process_vents_other_processes():
|
||||
process_stdout = flexmock()
|
||||
process = flexmock(poll=lambda: 0, stdout=process_stdout, stderr=flexmock(), args=flexmock())
|
||||
@@ -487,7 +533,7 @@ def test_log_buffer_lines_with_ready_buffer_and_finished_process_vents_other_pro
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -524,7 +570,7 @@ def test_log_buffer_lines_with_ready_buffer_and_finished_process_does_not_vent_o
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -550,7 +596,7 @@ def test_log_buffer_lines_with_ready_eof_buffer_and_running_process_skips_it():
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -573,7 +619,7 @@ def test_log_buffer_lines_with_ready_buffer_with_empty_line_skips_it():
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -603,7 +649,7 @@ def test_log_buffer_lines_with_multiple_ready_buffers_and_running_processes_hand
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -631,7 +677,7 @@ def test_log_buffer_lines_with_multiple_ready_buffers_from_same_running_process_
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -659,7 +705,7 @@ def test_log_buffer_lines_with_ready_stderr_buffer_and_running_process_elevates_
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -687,7 +733,7 @@ def test_log_buffer_lines_with_ready_stdout_buffer_and_running_process_does_not_
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -715,7 +761,7 @@ def test_log_buffer_lines_with_ready_stderr_buffer_and_capture_stderr_does_not_e
|
||||
module.log_buffer_lines(
|
||||
buffer_readers=buffer_readers,
|
||||
process_metadatas=process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
capture_stderr=True,
|
||||
)
|
||||
@@ -995,7 +1041,7 @@ def test_log_remaining_buffer_lines_without_reader_process_bails():
|
||||
module.log_remaining_buffer_lines(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -1022,7 +1068,7 @@ def test_log_remaining_buffer_lines_logs_each_line():
|
||||
module.log_remaining_buffer_lines(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -1053,7 +1099,7 @@ def test_log_remaining_buffer_lines_with_multiple_buffers_logs_lines_from_each()
|
||||
module.log_remaining_buffer_lines(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -1088,7 +1134,7 @@ def test_log_remaining_buffer_lines_with_stderr_buffer_elevates_stderr():
|
||||
module.log_remaining_buffer_lines(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -1120,7 +1166,7 @@ def test_log_remaining_buffer_lines_with_stderr_buffer_and_capture_stderr_does_n
|
||||
module.log_remaining_buffer_lines(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
capture_stderr=True,
|
||||
)
|
||||
@@ -1149,13 +1195,13 @@ def test_log_remaining_buffer_lines_with_capture_process_yields_each_line():
|
||||
module.log_remaining_buffer_lines(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
) == ('message', 'message')
|
||||
|
||||
|
||||
def test_log_remaining_buffer_lines_with_log_level_and_capture_process_does_not_yield_each_line():
|
||||
def test_log_remaining_buffer_lines_with_same_log_level_and_capture_process_yields_each_line():
|
||||
process = flexmock(stderr=flexmock(), args=flexmock())
|
||||
buffer_readers = {
|
||||
flexmock(): module.Buffer_reader(
|
||||
@@ -1168,7 +1214,33 @@ def test_log_remaining_buffer_lines_with_log_level_and_capture_process_does_not_
|
||||
line=str, log_level=object, elevate_stderr=False, borg_local_path=object, command=object
|
||||
).and_return(flexmock()).twice()
|
||||
flexmock(module).should_receive('handle_log_record').and_return(
|
||||
flexmock(levelno=10, getMessage=lambda: 'message')
|
||||
flexmock(levelno=module.logging.INFO, getMessage=lambda: 'message')
|
||||
).twice()
|
||||
|
||||
assert tuple(
|
||||
module.log_remaining_buffer_lines(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
) == ('message', 'message')
|
||||
|
||||
|
||||
def test_log_remaining_buffer_lines_with_higher_log_level_and_capture_process_does_not_yield_each_line():
|
||||
process = flexmock(stderr=flexmock(), args=flexmock())
|
||||
buffer_readers = {
|
||||
flexmock(): module.Buffer_reader(
|
||||
lines=(('hi', 'there'),),
|
||||
process=process,
|
||||
)
|
||||
}
|
||||
process_metadatas = {process: module.Process_metadata(last_lines=[], capture=True)}
|
||||
flexmock(module).should_receive('parse_log_line').with_args(
|
||||
line=str, log_level=object, elevate_stderr=False, borg_local_path=object, command=object
|
||||
).and_return(flexmock()).twice()
|
||||
flexmock(module).should_receive('handle_log_record').and_return(
|
||||
flexmock(levelno=module.logging.DEBUG, getMessage=lambda: 'message')
|
||||
).twice()
|
||||
|
||||
assert (
|
||||
@@ -1176,7 +1248,7 @@ def test_log_remaining_buffer_lines_with_log_level_and_capture_process_does_not_
|
||||
module.log_remaining_buffer_lines(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=flexmock(),
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
)
|
||||
@@ -1184,6 +1256,32 @@ def test_log_remaining_buffer_lines_with_log_level_and_capture_process_does_not_
|
||||
)
|
||||
|
||||
|
||||
def test_log_remaining_buffer_lines_with_log_level_equal_to_output_log_level_and_capture_process_yields_each_line():
|
||||
process = flexmock(stderr=flexmock(), args=flexmock())
|
||||
buffer_readers = {
|
||||
flexmock(): module.Buffer_reader(
|
||||
lines=(('hi', 'there'),),
|
||||
process=process,
|
||||
)
|
||||
}
|
||||
process_metadatas = {process: module.Process_metadata(last_lines=[], capture=True)}
|
||||
flexmock(module).should_receive('parse_log_line').with_args(
|
||||
line=str, log_level=object, elevate_stderr=False, borg_local_path=object, command=object
|
||||
).and_return(flexmock()).twice()
|
||||
flexmock(module).should_receive('handle_log_record').and_return(
|
||||
flexmock(levelno=module.logging.INFO, getMessage=lambda: 'message')
|
||||
).twice()
|
||||
|
||||
assert tuple(
|
||||
module.log_remaining_buffer_lines(
|
||||
buffer_readers,
|
||||
process_metadatas,
|
||||
output_log_level=module.logging.INFO,
|
||||
borg_local_path=flexmock(),
|
||||
)
|
||||
) == ('message', 'message')
|
||||
|
||||
|
||||
def test_mask_command_secrets_masks_password_flag_value():
|
||||
assert module.mask_command_secrets(('cooldb', '--username', 'bob', '--password', 'pass')) == (
|
||||
'cooldb',
|
||||
|
||||
+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