Compare commits

...
17 Commits
Author SHA1 Message Date
Dan Helfman dbc3fc356c Fix schema example for "archive_username" (#1317). 2026-07-20 23:04:51 -07:00
Dan Helfman e428d74258 Fix the (unreleased) "archive_hostname" option / "--archive-hostname" flag to pass an environment variable to Borg instead of a flag. Also add an "archive_username" option and corresponding "--archive-username" flag to override the "{user}" plaecholder (#1317). 2026-07-20 18:57:11 -07:00
Dan Helfman d454f3a3af Fix an error from the "diff" action when exclude options are configured (#1327). 2026-07-19 18:59:03 -07:00
Dan Helfman 5825af9fc1 Fix shadowing of built-in function (#1331). 2026-07-16 12:28:31 -07:00
Dan Helfman eb5c9e6bf3 Fix the "repo-create" action to more surgically suppress Borg "Repository does not exist" logs and avoid inadvertently suppressing other error logs (#1331). 2026-07-16 11:58:10 -07:00
Dan Helfman b41f4cfa0d Add the "CAP_FOWNER" capability to "CapabilityBoundingSet" in the sample systemd service, so that Borg can open source files without changing file access times (#1334). 2026-07-13 16:08:52 -07:00
Dan Helfman eae3341b01 Fix the "--progress" flag on the "compact" action to actually update the progress of segment compaction (#1333). 2026-07-13 15:58:35 -07:00
Dan Helfman 42743273d8 Tweak NEWS entry to mention Borg version limitation (#1317). 2026-07-13 14:04:07 -07:00
Dan Helfman 07d5a0129c For the MariaDB and MySQL hooks, add "events", "routines", and "tablespaces" options for disabling dumping of scheduled events, stored routines, and tablespaces, respectively (#1324). 2026-06-17 16:26:01 -07:00
Dan Helfman b14425b2af Consume "--archive-hostname" flag and "archive_hostname" option in pretty much all actions (#1317). 2026-06-17 14:57:42 -07:00
Dan Helfman f768abfaa0 Update the documentation to reflect the fact that Hetzner has ended its referral program. 2026-06-15 17:13:02 -07:00
Dan Helfman a73e28155c Add an "--archive-hostname" flag and corresponding "archive_hostname" option for overriding the hostname used when creating an archive (#1317). 2026-06-15 13:48:02 -07:00
Dan Helfman beaeea25b3 Fix for the "restore" action sometimes failing to find a database dump that was dumped with a default port (#1322). 2026-06-15 11:56:04 -07:00
Dan Helfman fa099b8471 Add support for the "--quick-stats" flag and the "quick_statistics" option to the "prune" action (#1309). 2026-06-11 15:57:52 -07:00
Dan Helfman 501f12dce3 Fix the ZFS hook's overzealous unmounting of snapshot paths when a source dataset is at "/" (#1319). 2026-06-10 16:15:32 -07:00
Dan Helfman d74b340024 Fix a bug in which the "compact" action does not pass a compact threshold of zero to Borg. 2026-06-06 15:31:29 -07:00
Dan Helfman 219282b5fa Some NEWS reordering. 2026-06-01 16:02:30 -07:00
46 changed files with 1474 additions and 302 deletions
+27 -5
View File
@@ -1,3 +1,25 @@
2.1.7.dev0
* #1309: Add support for the "--quick-stats" flag and the "quick_statistics" option to the "prune"
action. Borg >= 1.4.5 and < 2 only.
* #1317: Add an "archive_hostname" option and a corresponding "--archive-hostname" flag for
overriding the hostname used for the "{hostname}" placeholder in the "archive_name_format"
option. Also add an "archive_username" option and corresponding "--archive-username" flag to
override the "{user}" plaecholder. Both options/flags are Borg 1.4.5+ only.
* #1319: Fix the ZFS hook's overzealous unmounting of snapshot paths when a source dataset is at
"/".
* #1322: Fix for the "restore" action sometimes failing to find a database dump that was dumped
with a default port.
* #1324: For the MariaDB and MySQL hooks, add "events", "routines", and "tablespaces" options for
disabling dumping of scheduled events, stored routines, and tablespaces, respectively.
* #1327: Fix an error from the "diff" action when exclude options are configured.
* #1331: Fix the "repo-create" action to more surgically suppress Borg "Repository does not exist"
logs and avoid inadvertently suppressing other error logs.
* #1333: Fix the "--progress" flag on the "compact" action to actually update the progress of
segment compaction.
* #1334: Add the "CAP_FOWNER" capability to "CapabilityBoundingSet" in the sample systemd service,
so that Borg can open source files without changing file access times.
* Fix a bug in which the "compact" action does not pass a compact threshold of zero to Borg.
2.1.6
* #1256: Fix a race condition in which borgmatic sometimes swallows Borg error output without
logging it.
@@ -10,16 +32,16 @@
* #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/
* #1314: Add a minimal stand-alone borgmatic binary in addition to the standard one. The minimal
binary omits support for the Apprise monitoring hook.
* Add an experimental "browse" action providing a console UI for browsing your backups. See the
documentation for more information:
https://torsion.org/borgmatic/how-to/inspect-your-backups/#browsing-backups
* #1308: Update the Apprise monitoring hook's "url" option to support loading credentials with the
"{credential ...}" syntax. See the documentation for more information:
https://torsion.org/borgmatic/reference/configuration/credentials/
* #1309: Add a "--quick-stats" flag and corresponding "quick_statistics" option for showing only
abbreviated statistics for the "create" action. Borg 1.4.5+ only.
* #1314: Add a minimal stand-alone borgmatic binary in addition to the standard one. The minimal
binary omits support for the Apprise monitoring hook.
* Add an experimental "browse" action providing a console UI for browsing your backups. See the
documentation for more information:
https://torsion.org/borgmatic/how-to/inspect-your-backups/#browsing-backups
* 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/
+5 -5
View File
@@ -106,16 +106,16 @@ reference guides</a>.
## Hosting providers
Need somewhere to store your encrypted off-site backups? The following hosting
providers include specific support for Borg/borgmatic—and fund borgmatic
development and hosting when you use these referral links to sign up:
provider includes specific support for Borg/borgmatic—and funds borgmatic
development and hosting when you use this referral links to sign up:
<ul>
<li class="referral"><a href="https://www.borgbase.com/?utm_source=borgmatic">BorgBase</a>: Borg hosting service with support for monitoring, 2FA, and append-only repos</li>
<li class="referral"><a href="https://hetzner.cloud/?ref=v9dOJ98Ic9I8">Hetzner</a>: A "storage box" that includes support for Borg</li>
</ul>
Additionally, rsync.net has a compatible storage offering, but does not fund
borgmatic development or hosting.
Additionally, Hetzner and rsync\.net have compatible storage offerings, but do
not fund borgmatic development or hosting.
## Support and contributing
+1 -2
View File
@@ -29,7 +29,6 @@ def run_arbitrary_borg(
command.
'''
borgmatic.logger.add_custom_log_levels()
lock_wait = config.get('lock_wait', None)
try:
options = options[1:] if options[0] == '--' else options
@@ -53,7 +52,7 @@ def run_arbitrary_borg(
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('lock-wait', lock_wait)
+ flags.make_flags('lock-wait', config.get('lock_wait'))
+ command_options
)
+2 -2
View File
@@ -21,8 +21,8 @@ def break_lock(
argparse.Namespace of global arguments, and optional local and remote Borg paths, break any
repository and cache locks leftover from Borg aborting.
'''
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
umask = config.get('umask')
lock_wait = config.get('lock_wait')
extra_borg_options = config.get('extra_borg_options', {}).get('break_lock', '')
full_command = (
+2 -2
View File
@@ -24,8 +24,8 @@ def change_passphrase(
based on an interactive prompt.
'''
borgmatic.logger.add_custom_log_levels()
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
umask = config.get('umask')
lock_wait = config.get('lock_wait')
extra_borg_options = config.get('extra_borg_options', {}).get('key_change_passphrase', '')
full_command = (
+1 -1
View File
@@ -20,7 +20,7 @@ def make_archive_filter_flags(local_borg_version, config, checks, check_argument
flag. And if "prefix" is set in configuration and "archives" is in checks, then include a
"--match-archives" flag.
'''
check_last = config.get('check_last', None)
check_last = config.get('check_last')
prefix = config.get('prefix')
if 'archives' in checks or 'data' in checks:
+5 -4
View File
@@ -3,7 +3,7 @@ import shlex
import borgmatic.config.paths
from borgmatic.borg import environment, feature, flags
from borgmatic.execute import execute_command
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
@@ -22,8 +22,8 @@ def compact_segments(
Given dry-run flag, a local or remote repository path, a configuration dict, and the local Borg
version, compact the segments in a repository.
'''
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
umask = config.get('umask')
lock_wait = config.get('lock_wait')
extra_borg_options = config.get('extra_borg_options', {}).get('compact', '')
threshold = config.get('compact_threshold')
@@ -35,7 +35,7 @@ def compact_segments(
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--progress',) if config.get('progress') else ())
+ (('--cleanup-commits',) if cleanup_commits else ())
+ (('--threshold', str(threshold)) if threshold else ())
+ (('--threshold', str(threshold)) if threshold is not None else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (
@@ -54,6 +54,7 @@ def compact_segments(
execute_command(
full_command,
output_log_level=logging.INFO,
output_file=DO_NOT_CAPTURE if config.get('progress') else None,
environment=environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
+8 -8
View File
@@ -188,14 +188,14 @@ def make_base_create_command( # noqa: PLR0912
patterns,
borgmatic_runtime_directory,
)
checkpoint_interval = config.get('checkpoint_interval', None)
checkpoint_volume = config.get('checkpoint_volume', None)
chunker_params = config.get('chunker_params', None)
compression = config.get('compression', None)
upload_rate_limit = config.get('upload_rate_limit', None)
upload_buffer_size = config.get('upload_buffer_size', None)
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
checkpoint_interval = config.get('checkpoint_interval')
checkpoint_volume = config.get('checkpoint_volume')
chunker_params = config.get('chunker_params')
compression = config.get('compression')
upload_rate_limit = config.get('upload_rate_limit')
upload_buffer_size = config.get('upload_buffer_size')
umask = config.get('umask')
lock_wait = config.get('lock_wait')
list_filter_flags = flags.make_list_filter_flags(local_borg_version, dry_run)
files_changed = config.get('files_changed')
files_cache = config.get('files_cache')
+1 -3
View File
@@ -31,8 +31,7 @@ def diff(
'''
borgmatic.logger.add_custom_log_levels()
lock_wait = config.get('lock_wait', None)
exclude_flags = flags.make_exclude_flags(config)
lock_wait = config.get('lock_wait')
extra_borg_options = config.get('extra_borg_options', {}).get('diff', '')
if diff_arguments.only_patterns:
@@ -63,7 +62,6 @@ def diff(
if patterns_file and diff_arguments.only_patterns
else ()
)
+ exclude_flags
+ numeric_ids_flags
+ (('--same-chunker-params',) if diff_arguments.same_chunker_params else ())
+ (('--sort-by', ','.join(diff_arguments.sort_keys)) if diff_arguments.sort_keys else ())
+2
View File
@@ -4,6 +4,8 @@ import borgmatic.borg.passcommand
import borgmatic.hooks.credential.parse
OPTION_TO_ENVIRONMENT_VARIABLE = {
'archive_hostname': 'BORG_HOSTNAME',
'archive_username': 'BORG_USERNAME',
'borg_base_directory': 'BORG_BASE_DIR',
'borg_config_directory': 'BORG_CONFIG_DIR',
'borg_cache_directory': 'BORG_CACHE_DIR',
+2 -2
View File
@@ -29,8 +29,8 @@ def export_key(
Raise FileExistsError if a path is given but it already exists on disk.
'''
borgmatic.logger.add_custom_log_levels()
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
umask = config.get('umask')
lock_wait = config.get('lock_wait')
working_directory = borgmatic.config.paths.get_working_directory(config)
extra_borg_options = config.get('extra_borg_options', {}).get('key_export', '')
+2 -2
View File
@@ -33,8 +33,8 @@ def export_tar_archive(
If the destination path is "-", then stream the output to stdout instead of to a file.
'''
borgmatic.logger.add_custom_log_levels()
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
umask = config.get('umask')
lock_wait = config.get('lock_wait')
extra_borg_options = config.get('extra_borg_options', {}).get('export_tar', '')
full_command = (
+2 -2
View File
@@ -104,8 +104,8 @@ def extract_archive(
If extract to stdout is True, then start the extraction streaming to stdout, and return that
extract process as an instance of subprocess.Popen.
'''
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
umask = config.get('umask')
lock_wait = config.get('lock_wait')
extra_borg_options = config.get('extra_borg_options', {}).get('extract', '')
if feature.available(feature.Feature.NUMERIC_IDS, local_borg_version):
+2 -2
View File
@@ -27,8 +27,8 @@ def import_key(
Raise ValueError if the path is given and it does not exist.
'''
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
umask = config.get('umask')
lock_wait = config.get('lock_wait')
working_directory = borgmatic.config.paths.get_working_directory(config)
extra_borg_options = config.get('extra_borg_options', {}).get('key_import', '')
+2 -2
View File
@@ -24,8 +24,8 @@ def mount_archive(
dict, the local Borg version, global arguments as an argparse.Namespace instance, and optional
local and remote Borg paths, mount the archive onto the mount point.
'''
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
umask = config.get('umask')
lock_wait = config.get('lock_wait')
extra_borg_options = config.get('extra_borg_options', {}).get('mount', '')
full_command = (
+17 -4
View File
@@ -65,8 +65,8 @@ def prune_archives(
archives according to the retention policy specified in that configuration.
'''
borgmatic.logger.add_custom_log_levels()
umask = config.get('umask', None)
lock_wait = config.get('lock_wait', None)
umask = config.get('umask')
lock_wait = config.get('lock_wait')
extra_borg_options = config.get('extra_borg_options', {}).get('prune', '')
full_command = (
@@ -83,10 +83,23 @@ def prune_archives(
and not feature.available(feature.Feature.NO_PRUNE_STATS, local_borg_version)
else ()
)
+ (
('--quick-stats',)
if config.get('quick_statistics')
and not dry_run
and not feature.available(feature.Feature.NO_PRUNE_STATS, local_borg_version)
else ()
)
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ flags.make_flags_from_arguments(
prune_arguments,
excludes=('repository', 'match_archives', 'statistics', 'list_details'),
excludes=(
'repository',
'match_archives',
'statistics',
'quick_statistics',
'list_details',
),
)
+ (('--list',) if config.get('list_details') else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
@@ -95,7 +108,7 @@ def prune_archives(
+ flags.make_repository_flags(repository_path, local_borg_version)
)
if config.get('statistics') or config.get('list_details'):
if config.get('statistics') or config.get('quick_statistics') or config.get('list_details'):
output_log_level = logging.ANSWER
else:
output_log_level = logging.INFO
+4 -4
View File
@@ -28,14 +28,14 @@ def recreate_archive(
arguments, optional local and remote Borg paths, executes the recreate command with the given
arguments.
'''
lock_wait = config.get('lock_wait', None)
lock_wait = config.get('lock_wait')
exclude_flags = flags.make_exclude_flags(config)
compression = config.get('compression', None)
chunker_params = config.get('chunker_params', None)
compression = config.get('compression')
chunker_params = config.get('chunker_params')
extra_borg_options = config.get('extra_borg_options', {}).get('recreate', '')
# Available recompress MODES: "if-different", "always", "never" (default)
recompress = config.get('recompress', None)
recompress = config.get('recompress')
# Write patterns to a temporary file and use that file with --patterns-from.
patterns_file = write_patterns_file(
+16 -11
View File
@@ -5,6 +5,7 @@ import shlex
import subprocess
import borgmatic.config.paths
import borgmatic.logger
from borgmatic.borg import environment, feature, flags, repo_info
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
@@ -40,17 +41,21 @@ def create_repository(
Raise subprocess.CalledProcessError if "borg info" returns an error exit code.
'''
try:
info_data = json.loads(
repo_info.display_repository_info(
repository_path,
config,
local_borg_version,
argparse.Namespace(json=True),
global_arguments,
local_path,
remote_path,
),
)
# Suppress Borg's "repository does not exist" error log, so the user isn't confused by
# seeing an error during successful repository creation.
with borgmatic.logger.Logs_suppressed(msgid='Repository.DoesNotExist'):
info_data = json.loads(
repo_info.display_repository_info(
repository_path,
config,
local_borg_version,
argparse.Namespace(json=True),
global_arguments,
local_path,
remote_path,
),
)
repository_encryption_mode = info_data.get('encryption', {}).get('mode')
if repository_encryption_mode != encryption_mode:
+1 -2
View File
@@ -24,7 +24,7 @@ def display_repository_info(
information for the Borg repository or return JSON summary information.
'''
borgmatic.logger.add_custom_log_levels()
lock_wait = config.get('lock_wait', None)
lock_wait = config.get('lock_wait')
extra_borg_options = config.get('extra_borg_options', {}).get(
'repo_info' if feature.available(feature.Feature.REPO_INFO, local_borg_version) else 'info',
'',
@@ -37,7 +37,6 @@ def display_repository_info(
if feature.available(feature.Feature.REPO_INFO, local_borg_version)
else ('info',)
)
+ (('--critical',) if repo_info_arguments.json else ())
+ (
('--info',)
if logger.getEffectiveLevel() == logging.INFO and not repo_info_arguments.json
+7
View File
@@ -805,6 +805,13 @@ def make_parsers(schema, unparsed_arguments): # noqa: PLR0915
action='store_true',
help='Display statistics of the pruned archive [Borg 1 only]',
)
prune_group.add_argument(
'--quick-stats',
dest='quick_statistics',
default=None,
action='store_true',
help='Display statistics of the pruned archive, skipping repository-wide "All archives" and chunk index statistics [Borg >= 1.4.5 and < 2 only]',
)
prune_group.add_argument(
'--list',
dest='list_details',
+60 -2
View File
@@ -521,6 +521,21 @@ properties:
If match_archives is not specified, borgmatic defaults to deriving
the match_archives value from archive_name_format.
example: "sh:{hostname}-*"
archive_hostname:
type: string
description: |
Hostname to use for the "{hostname}" placeholder in
"archive_name_format", "match_archives", etc. Defaults to the system
hostname. (This option is supported for Borg 1.4.5+ only.)
example: example.org
archive_username:
type: string
description: |
Username to use for the "{user}" placeholder in
"archive_name_format", "match_archives", etc. Defaults to the
username of the user running borgmatic. (This option is supported
for Borg 1.4.5+ only.)
example: backup_user
file_list_format:
type: string
description: |
@@ -1097,8 +1112,9 @@ properties:
Display statistics for an archive when running supported actions,
skipping the repository-wide "All archives" and chunk index
statistics to save some time. Corresponds to the "--quick-stats"
flag on those actions. Defaults to false. (This option is supported
for Borg 1.4.5+ only.)
flag on those actions. Defaults to false. (This option is supported
for Borg 1.4.5+ only for the "create" action and for Borg >= 1.4.5
and < Borg 2 for the "prune" action.)
example: true
list_details:
type: boolean
@@ -1841,6 +1857,27 @@ properties:
client and restore server. The default varies based on
the MariaDB version.
example: false
events:
type: boolean
description: |
Whether to include scheduled events within the dump.
Disable if your database user doesn't have the
permissions to dump events. Defaults to true.
example: false
routines:
type: boolean
description: |
Whether to include stored routines within the dump.
Disable if your user database doesn't have the
permissions to dump routines. Defaults to true.
example: false
tablespaces:
type: boolean
description: |
Whether to include tablespaces within the dump. Disable
if your database user doesn't have the permissions to
dump tablespaces. Defaults to true.
example: false
mariadb_dump_command:
type: string
description: |
@@ -2049,6 +2086,27 @@ properties:
client and restore server. The default varies based on
the MySQL installation.
example: false
events:
type: boolean
description: |
Whether to include scheduled events within the dump.
Disable if your database user doesn't have the
permissions to dump events. Defaults to true.
example: false
routines:
type: boolean
description: |
Whether to include stored routines within the dump.
Disable if your database user doesn't have the
permissions to dump routines. Defaults to true.
example: false
tablespaces:
type: boolean
description: |
Whether to include tablespaces within the dump. Disable
if your database user doesn't have the permissions to
dump tablespaces. Defaults to true.
example: false
mysql_dump_command:
type: string
description: |
+3
View File
@@ -157,6 +157,7 @@ def borg_json_log_line_to_record(line, log_level):
levelno=log_level,
created=log_data.get('time'),
msg=log_data.get('message'),
msgid=log_data.get('msgid'),
levelname=logging.getLevelName(log_level),
name=log_data.get('name'),
)
@@ -167,6 +168,7 @@ def borg_json_log_line_to_record(line, log_level):
levelno=borg_log_level,
created=log_data.get('time'),
msg=log_data.get('message'),
msgid=log_data.get('msgid'),
levelname=log_data.get('levelname'),
name=log_data.get('name'),
)
@@ -178,6 +180,7 @@ def borg_json_log_line_to_record(line, log_level):
levelno=log_level,
created=time.time(),
msg=f'{log_data.get("status")} {log_data.get("path")}',
msgid=log_data.get('msgid'),
levelname=logging.getLevelName(log_level),
name='borg.file_status',
)
+51 -18
View File
@@ -241,8 +241,11 @@ 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', '--events', '--routines', '--all-tablespaces')
+ (('--events',) if database.get('events', True) else ())
+ (('--routines',) if database.get('routines', True) else ())
+ (('--all-tablespaces',) if database.get('tablespaces', True) else ())
+ (('--system=users,udfs,servers',) if SYSTEM_DATABASE_NAME in database_names else ())
+ ('--databases',)
+ tuple(name for name in database_names if name != SYSTEM_DATABASE_NAME)
+ ('--result-file', dump_filename)
)
@@ -422,7 +425,7 @@ def make_data_source_dump_patterns(
port=None,
container=None,
label=None,
): # pragma: no cover
):
'''
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
and a database name to match, return the corresponding glob patterns to match the database dump
@@ -431,24 +434,54 @@ def make_data_source_dump_patterns(
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
return (
dump.make_data_source_dump_filename(
make_dump_path('borgmatic'), name, hostname, port, container, label
*(
dump.make_data_source_dump_filename(
make_dump_path('borgmatic'), name, hostname, port, container, label
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_runtime_directory),
name,
hostname,
port,
container,
label,
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_source_directory),
name,
hostname,
port,
container,
label,
),
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_runtime_directory),
name,
hostname,
port,
container,
label,
*(
(
dump.make_data_source_dump_filename(
make_dump_path('borgmatic'),
name,
hostname,
port=None,
container=container,
label=label,
),
)
if port == get_default_port(databases, config)
else ()
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_source_directory),
name,
hostname,
port,
container,
label,
*(
(
dump.make_data_source_dump_filename(
make_dump_path('borgmatic'),
name,
hostname,
port=get_default_port(databases, config),
container=container,
label=label,
),
)
if port is None
else ()
),
)
+47 -17
View File
@@ -216,7 +216,7 @@ def make_data_source_dump_patterns(
port=None,
container=None,
label=None,
): # pragma: no cover
):
'''
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
and a database name to match, return the corresponding glob patterns to match the database dump
@@ -225,24 +225,54 @@ def make_data_source_dump_patterns(
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
return (
dump.make_data_source_dump_filename(
make_dump_path('borgmatic'), name, hostname, port, container, label
*(
dump.make_data_source_dump_filename(
make_dump_path('borgmatic'), name, hostname, port, container, label
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_runtime_directory),
name,
hostname,
port,
container,
label,
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_source_directory),
name,
hostname,
port,
container,
label,
),
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_runtime_directory),
name,
hostname,
port,
container,
label,
*(
(
dump.make_data_source_dump_filename(
make_dump_path('borgmatic'),
name,
hostname,
port=None,
container=container,
label=label,
),
)
if port == get_default_port(databases, config)
else ()
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_source_directory),
name,
hostname,
port,
container,
label,
*(
(
dump.make_data_source_dump_filename(
make_dump_path('borgmatic'),
name,
hostname,
port=get_default_port(databases, config),
container=container,
label=label,
),
)
if port is None
else ()
),
)
+51 -18
View File
@@ -165,7 +165,10 @@ 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', '--events', '--routines', '--all-tablespaces')
+ (('--events',) if database.get('events', True) else ())
+ (('--routines',) if database.get('routines', True) else ())
+ (('--all-tablespaces',) if database.get('tablespaces', True) else ())
+ ('--databases',)
+ database_names
+ ('--result-file', dump_filename)
)
@@ -343,7 +346,7 @@ def make_data_source_dump_patterns(
port=None,
container=None,
label=None,
): # pragma: no cover
):
'''
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
and a database name to match, return the corresponding glob patterns to match the database dump
@@ -352,24 +355,54 @@ def make_data_source_dump_patterns(
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
return (
dump.make_data_source_dump_filename(
make_dump_path('borgmatic'), name, hostname, port, container, label
*(
dump.make_data_source_dump_filename(
make_dump_path('borgmatic'), name, hostname, port, container, label
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_runtime_directory),
name,
hostname,
port,
container,
label,
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_source_directory),
name,
hostname,
port,
container,
label,
),
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_runtime_directory),
name,
hostname,
port,
container,
label,
*(
(
dump.make_data_source_dump_filename(
make_dump_path('borgmatic'),
name,
hostname,
port=None,
container=container,
label=label,
),
)
if port == get_default_port(databases, config)
else ()
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_source_directory),
name,
hostname,
port,
container,
label,
*(
(
dump.make_data_source_dump_filename(
make_dump_path('borgmatic'),
name,
hostname,
port=get_default_port(databases, config),
container=container,
label=label,
),
)
if port is None
else ()
),
)
+47 -17
View File
@@ -310,7 +310,7 @@ def make_data_source_dump_patterns(
port=None,
container=None,
label=None,
): # pragma: no cover
):
'''
Given a sequence of configurations dicts, a configuration dict, the borgmatic runtime directory,
and a database name to match, return the corresponding glob patterns to match the database dump
@@ -319,24 +319,54 @@ def make_data_source_dump_patterns(
borgmatic_source_directory = borgmatic.config.paths.get_borgmatic_source_directory(config)
return (
dump.make_data_source_dump_filename(
make_dump_path('borgmatic'), name, hostname, port, container, label
*(
dump.make_data_source_dump_filename(
make_dump_path('borgmatic'), name, hostname, port, container, label
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_runtime_directory),
name,
hostname,
port,
container,
label,
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_source_directory),
name,
hostname,
port,
container,
label,
),
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_runtime_directory),
name,
hostname,
port,
container,
label,
*(
(
dump.make_data_source_dump_filename(
make_dump_path('borgmatic'),
name,
hostname,
port=None,
container=container,
label=label,
),
)
if port == get_default_port(databases, config)
else ()
),
dump.make_data_source_dump_filename(
make_dump_path(borgmatic_source_directory),
name,
hostname,
port,
container,
label,
*(
(
dump.make_data_source_dump_filename(
make_dump_path('borgmatic'),
name,
hostname,
port=get_default_port(databases, config),
container=container,
label=label,
),
)
if port is None
else ()
),
)
+42 -26
View File
@@ -403,41 +403,57 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, p
snapshot_dataset_names = {
full_snapshot_name.split('@')[0] for full_snapshot_name in full_snapshot_names
}
hash_to_dataset_mount_point = {}
# Make a map from mount point hash to the corresponding (dataset name, mount point) tuple.
for dataset_name, mount_point in dataset_name_to_mount_point.items():
mount_point_hash = hashlib.shake_256(mount_point.encode('utf-8')).hexdigest(
MOUNT_POINT_HASH_LENGTH
)
hash_to_dataset_mount_point[mount_point_hash] = (dataset_name, mount_point)
for snapshots_directory in glob.glob(snapshots_glob):
if not os.path.isdir(snapshots_directory):
continue
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))
# Get the dataset and mount point corresponding to the hash found in this snapshot directory
# path. If none is found, bail.
try:
(dataset_name, mount_point) = hash_to_dataset_mount_point[
os.path.basename(snapshots_directory)
]
except KeyError:
continue
# 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:
snapshot_mount_path = os.path.join(snapshots_directory, mount_point.lstrip(os.path.sep))
# If this dataset name doesn't 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
# do another recursive delete below. The point of doing it here is that we don't want to
# try to unmount a non-mounted directory (which *will* fail), and probing for whether a
# directory is mounted is tough to do in a cross-platform way.
if not dry_run:
shutil.rmtree(snapshot_mount_path, ignore_errors=True)
# If the delete was successful, that means there's nothing to unmount.
if not os.path.isdir(snapshot_mount_path):
continue
# This might fail if the path is already mounted, but we swallow errors here since we'll
# do another recursive delete below. The point of doing it here is that we don't want to
# try to unmount a non-mounted directory (which *will* fail), and probing for whether a
# directory is mounted is tough to do in a cross-platform way.
if not dry_run:
shutil.rmtree(snapshot_mount_path, ignore_errors=True)
logger.debug(f'Unmounting ZFS snapshot at {snapshot_mount_path}{dry_run_label}')
# If the delete was successful, that means there's nothing to unmount.
if not os.path.isdir(snapshot_mount_path):
continue
logger.debug(f'Unmounting ZFS snapshot at {snapshot_mount_path}{dry_run_label}')
if not dry_run:
try:
unmount_snapshot(umount_command, snapshot_mount_path)
except FileNotFoundError:
logger.debug(f'Could not find "{umount_command}" command')
return
except subprocess.CalledProcessError as error:
logger.debug(error)
continue
if not dry_run:
try:
unmount_snapshot(umount_command, snapshot_mount_path)
except FileNotFoundError:
logger.debug(f'Could not find "{umount_command}" command')
return
except subprocess.CalledProcessError as error:
logger.debug(error)
continue
if not dry_run:
shutil.rmtree(snapshot_mount_path, ignore_errors=True)
+88
View File
@@ -154,6 +154,8 @@ def log_record_to_json(record):
'''
Given a logging.LogRecord, return it as a JSON-encoded string containing relevant attributes.
'''
message_id = getattr(record, 'msgid', None)
return json.dumps(
dict(
type='log_message',
@@ -162,6 +164,7 @@ def log_record_to_json(record):
levelname=record.levelname,
name=record.name,
)
| ({'msgid': message_id} if message_id is not None else {})
)
@@ -336,6 +339,91 @@ class Log_prefix:
set_log_prefix(self.original_prefix)
class Log_exclude_filter(logging.Filter):
'''
A Python log filter that omits log records matching given attributes.
'''
def __init__(self, name, filter_attributes):
'''
Given a unique name for this filter and a dict of attributes to filter on, set the filter
name and save the attributes for use below.
'''
self.filter_attributes = filter_attributes
super().__init__(name)
def filter(self, log_record):
'''
Given a log record, return False (indicating the record should be omitted) if the record's
attributes match any of the saved filter attributes. Return True (indicating do not omit)
otherwise.
'''
for attribute_name, value in self.filter_attributes.items():
if getattr(log_record, attribute_name, None) == value:
return False
return True
def add_log_exclude_filter(name, filter_attributes):
'''
Given a unique filter name and a dict of attributes to filter on, create a log exclude filter
with them and add the filter to each log handler.
'''
for handler in logging.getLogger().handlers:
handler.addFilter(Log_exclude_filter(name, filter_attributes))
def remove_log_exclude_filter(name):
'''
Given a unique filter name, remove matching filters from each log handler.
'''
for handler in logging.getLogger().handlers:
for exclude_filter in handler.filters:
if getattr(exclude_filter, 'name', None) == name:
handler.removeFilter(exclude_filter)
class Logs_suppressed:
'''
A Python context manager for temporarily adding a log filter that suppresses requested log
records for the duration of the context manager.
Example use:
with borgmatic.logger.Logs_suppressed(msgid='Repository.DoesNotExist'):
do_something_that_logs()
For the scope of that "with" statement, any records logged with the given message ID are
filtered out of the log output. "msgid" is just an example; any logging.LogRecord attributes
(standard or custom) can be passed in to filter on.
Multiple instances of this context manager with different filter attributes can be in use at
once.
'''
def __init__(self, **filter_attributes):
'''
Given the desired log record filter attributes as keyword arguments, save them for use below.
'''
self.filter_attributes = filter_attributes
def __enter__(self):
'''
Create a log filter with the saved filter attributes and add the filter to every logging
handler, so that they filter out the desired log records.
'''
add_log_exclude_filter(name=str(id(self)), filter_attributes=self.filter_attributes)
def __exit__(self, exception_type, exception, traceback):
'''
Remove the previously added filter from every logging handler.
'''
remove_log_exclude_filter(name=str(id(self)))
class Delayed_logging_handler(logging.handlers.BufferingHandler):
'''
A logging handler that buffers logs and doesn't flush them until explicitly flushed (after
+4 -5
View File
@@ -13,16 +13,15 @@ already.
## Hosting providers
Need somewhere to store your encrypted off-site backups? The following hosting
providers include specific support for Borg/borgmatic—and fund borgmatic
development and hosting when you use these referral links to sign up:
provider includes specific support for Borg/borgmatic—and funds borgmatic
development and hosting when you use this referral links to sign up:
<ul>
<li class="referral"><a href="https://www.borgbase.com/?utm_source=borgmatic">BorgBase</a>: Borg hosting service with support for monitoring, 2FA, and append-only repos</li>
<li class="referral"><a href="https://hetzner.cloud/?ref=v9dOJ98Ic9I8">Hetzner</a>: A "storage box" that includes support for Borg</li>
</ul>
Additionally, rsync.net has a compatible storage offering, but does not fund
borgmatic development or hosting.
Additionally, Hetzner and rsync\.net have compatible storage offerings, but do
not fund borgmatic development or hosting.
## Configuration
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "borgmatic"
version = "2.1.6"
version = "2.1.7.dev0"
authors = [
{ name="Dan Helfman", email="witten@torsion.org" },
]
+1 -1
View File
@@ -60,7 +60,7 @@ ProtectSystem=full
# includes, for instance, programs to snapshot filesystems (e.g. ZFS, LVM, and
# Btrfs). But rather than commenting this out entirely, one workaround may be to
# add "CAP_SYS_ADMIN".
CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_NET_RAW
CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_FOWNER CAP_NET_RAW
# Lower CPU and I/O priority.
Nice=19
+11 -5
View File
@@ -191,9 +191,12 @@ def test_run_arbitrary_borg_with_exit_codes_calls_borg_using_them():
def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_flags():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module.flags).should_receive('make_flags').and_return(
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags').with_args(
'remote-path', 'borg1'
).and_return(
('--remote-path', 'borg1'),
).and_return(())
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command').with_args(
@@ -219,9 +222,12 @@ def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_flags()
def test_run_arbitrary_borg_with_remote_path_injection_attack_gets_escaped():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module.flags).should_receive('make_flags').and_return(
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags').with_args(
'remote-path', 'borg1; naughty-command'
).and_return(
('--remote-path', 'borg1; naughty-command'),
).and_return(())
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module).should_receive('execute_command').with_args(
@@ -240,7 +246,7 @@ def test_run_arbitrary_borg_with_remote_path_injection_attack_gets_escaped():
config={},
local_borg_version='1.2.3',
options=['break-lock', '::'],
remote_path='borg1',
remote_path='borg1; naughty-command',
)
+24 -19
View File
@@ -10,6 +10,7 @@ from ..test_verbosity import insert_logging_mock
def insert_execute_command_mock(
compact_command,
output_log_level,
output_file=None,
working_directory=None,
borg_exit_codes=None,
):
@@ -20,6 +21,7 @@ def insert_execute_command_mock(
flexmock(module).should_receive('execute_command').with_args(
compact_command,
output_log_level=output_log_level,
output_file=output_file,
environment=None,
working_directory=working_directory,
borg_local_path=compact_command[0],
@@ -149,26 +151,11 @@ def test_compact_segments_with_exit_codes_calls_borg_using_them():
)
def test_compact_segments_with_remote_path_calls_borg_with_remote_path_flags():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(
(*COMPACT_COMMAND, '--remote-path', 'borg1', '--log-json', 'repo'), logging.INFO
)
insert_logging_mock(logging.WARNING)
module.compact_segments(
dry_run=False,
repository_path='repo',
config={},
local_borg_version='1.2.3',
global_arguments=flexmock(),
remote_path='borg1',
)
def test_compact_segments_with_progress_calls_borg_with_progress_flag():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock((*COMPACT_COMMAND, '--progress', 'repo'), logging.INFO)
insert_execute_command_mock(
(*COMPACT_COMMAND, '--progress', 'repo'), logging.INFO, module.DO_NOT_CAPTURE
)
insert_logging_mock(logging.WARNING)
module.compact_segments(
@@ -183,7 +170,9 @@ def test_compact_segments_with_progress_calls_borg_with_progress_flag():
def test_compact_segments_with_log_json_and_progress_calls_borg_with_both_flags():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(
(*COMPACT_COMMAND, '--log-json', '--progress', 'repo'), logging.INFO
(*COMPACT_COMMAND, '--log-json', '--progress', 'repo'),
logging.INFO,
module.DO_NOT_CAPTURE,
)
insert_logging_mock(logging.WARNING)
@@ -229,6 +218,22 @@ def test_compact_segments_with_threshold_calls_borg_with_threshold_flag():
)
def test_compact_segments_with_zero_threshold_calls_borg_with_threshold_flag():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(
(*COMPACT_COMMAND, '--log-json', '--threshold', '0', 'repo'), logging.INFO
)
insert_logging_mock(logging.WARNING)
module.compact_segments(
dry_run=False,
repository_path='repo',
config={'compact_threshold': 0},
local_borg_version='1.2.3',
global_arguments=flexmock(),
)
def test_compact_segments_with_umask_calls_borg_with_umask_flags():
config = {'umask': '077'}
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
-73
View File
@@ -12,7 +12,6 @@ LOGGING_ANSWER = flexmock()
def test_diff_calls_borg_with_archives():
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
flexmock(name='test')
@@ -67,7 +66,6 @@ def test_diff_calls_borg_with_archives():
def test_diff_with_local_path_calls_borg_with_it():
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
flexmock(name='test')
@@ -122,7 +120,6 @@ def test_diff_with_local_path_calls_borg_with_it():
def test_diff_with_remote_path_calls_borg_with_it():
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
flexmock(name='test')
@@ -179,7 +176,6 @@ def test_diff_with_remote_path_calls_borg_with_it():
def test_diff_with_lock_wait_calls_borg_with_it():
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
flexmock(name='test')
@@ -236,7 +232,6 @@ def test_diff_with_lock_wait_calls_borg_with_it():
def test_diff_with_log_level_info_calls_borg_with_info_flag():
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
flexmock(name='test')
@@ -293,7 +288,6 @@ def test_diff_with_log_level_info_calls_borg_with_info_flag():
def test_diff_with_log_level_debug_calls_borg_with_debug_flags():
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
flexmock(name='test')
@@ -351,7 +345,6 @@ def test_diff_with_log_level_debug_calls_borg_with_debug_flags():
def test_diff_with_only_patterns_calls_borg_with_configured_pattern_paths():
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
@@ -406,69 +399,9 @@ def test_diff_with_only_patterns_calls_borg_with_configured_pattern_paths():
)
def test_diff_with_exclude_config_calls_borg_with_exclude_flags():
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(
('--exclude', 'stuff')
)
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
flexmock(name='test')
)
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('--repo', 'repo')
)
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_archive_flags').never()
environment = flexmock()
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
environment
)
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
full_command=(
'borg',
'diff',
'--log-json',
'--exclude',
'stuff',
'--repo',
'repo',
'archive',
'archive2',
),
output_log_level=LOGGING_ANSWER,
environment=environment,
working_directory=None,
borg_local_path='borg',
borg_exit_codes=None,
).once()
insert_logging_mock(logging.WARNING)
module.borgmatic.borg.diff.diff(
repository='repo',
archive='archive',
second_archive='archive2',
config={},
local_borg_version=None,
diff_arguments=flexmock(
same_chunker_params=False,
sort_keys=[],
content_only=False,
second_archive='archive2',
only_patterns=False,
),
global_arguments=flexmock(),
local_path='borg',
remote_path=None,
patterns=[],
)
def test_diff_with_numeric_ids_calls_borg_with_numeric_ids_flag():
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
flexmock(name='test')
@@ -524,7 +457,6 @@ def test_diff_with_numeric_ids_calls_borg_with_numeric_ids_flag():
def test_diff_with_numeric_ids_and_feature_not_available_calls_borg_with_numeric_owner_flag():
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
flexmock(name='test')
@@ -585,7 +517,6 @@ def test_diff_with_numeric_ids_and_feature_not_available_calls_borg_with_numeric
def test_diff_with_same_chunker_params_calls_borg_with_it():
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
flexmock(name='test')
@@ -641,7 +572,6 @@ def test_diff_with_same_chunker_params_calls_borg_with_it():
def test_diff_with_sort_keys_calls_borg_with_formatted_sort_by_flags():
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
flexmock(name='test')
@@ -698,7 +628,6 @@ def test_diff_with_sort_keys_calls_borg_with_formatted_sort_by_flags():
def test_diff_with_content_only_calls_borg_with_it():
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
flexmock(name='test')
@@ -754,7 +683,6 @@ def test_diff_with_content_only_calls_borg_with_it():
def test_diff_with_extra_borg_options_calls_borg_with_them():
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
flexmock(name='test')
@@ -811,7 +739,6 @@ def test_diff_with_extra_borg_options_calls_borg_with_them():
def test_diff_without_separate_repository_archive_feature_available_calls_borg_joined_repository_archive():
flexmock(module.logging).ANSWER = LOGGING_ANSWER
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.flags).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
flexmock(module.borgmatic.borg.pattern).should_receive('write_patterns_file').and_return(
flexmock(name='test')
+3 -2
View File
@@ -96,9 +96,10 @@ def test_make_list_command_includes_json():
def test_make_list_command_includes_lock_wait():
insert_logging_mock(logging.WARNING)
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags').with_args('lock-wait', 5).and_return(
('--lock-wait', '5'),
).and_return(())
)
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+48
View File
@@ -425,6 +425,31 @@ def test_prune_archives_with_stats_config_calls_borg_with_stats_flag():
)
def test_prune_archives_with_quick_stats_config_calls_borg_with_quick_stats_flag():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.feature).should_receive('available').with_args(
module.feature.Feature.NO_PRUNE_STATS,
'1.2.3',
).and_return(False)
insert_execute_command_mock(
(*PRUNE_COMMAND, '--quick-stats', 'repo'), module.borgmatic.logger.ANSWER
)
insert_logging_mock(logging.WARNING)
prune_arguments = flexmock(statistics=None, list_details=False)
module.prune_archives(
dry_run=False,
repository_path='repo',
config={'quick_statistics': True},
local_borg_version='1.2.3',
global_arguments=flexmock(),
prune_arguments=prune_arguments,
)
def test_prune_archives_with_list_config_calls_borg_with_list_flag():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
@@ -647,3 +672,26 @@ def test_prune_archives_calls_borg_without_stats_when_feature_is_not_available()
global_arguments=flexmock(),
prune_arguments=prune_arguments,
)
def test_prune_archives_calls_borg_without_quick_stats_when_feature_is_not_available():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
flexmock(module).should_receive('make_prune_flags').and_return(BASE_PRUNE_FLAGS)
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.feature).should_receive('available').with_args(
module.feature.Feature.NO_PRUNE_STATS,
'2.0.0b10',
).and_return(True)
insert_execute_command_mock((*PRUNE_COMMAND, 'repo'), logging.ANSWER)
insert_logging_mock(logging.WARNING)
prune_arguments = flexmock(quick_statistics=True, list_details=False)
module.prune_archives(
dry_run=False,
repository_path='repo',
config={'quick_statistics': True},
local_borg_version='2.0.0b10',
global_arguments=flexmock(),
prune_arguments=prune_arguments,
)
+3
View File
@@ -13,12 +13,14 @@ REPO_CREATE_COMMAND = ('borg', 'repo-create', '--encryption', 'repokey')
def insert_repo_info_command_found_mock():
flexmock(module.borgmatic.logger).should_receive('Logs_suppressed').and_return(flexmock())
flexmock(module.repo_info).should_receive('display_repository_info').and_return(
'{"encryption": {"mode": "repokey"}}',
)
def insert_repo_info_command_not_found_mock():
flexmock(module.borgmatic.logger).should_receive('Logs_suppressed').and_return(flexmock())
flexmock(module.repo_info).should_receive('display_repository_info').and_raise(
subprocess.CalledProcessError(
sorted(module.REPO_INFO_REPOSITORY_NOT_FOUND_EXIT_CODES)[0],
@@ -158,6 +160,7 @@ def test_create_repository_errors_when_repository_with_differing_encryption_mode
def test_create_repository_raises_for_unknown_repo_info_command_error():
flexmock(module.borgmatic.logger).should_receive('Logs_suppressed').and_return(flexmock())
flexmock(module.repo_info).should_receive('display_repository_info').and_raise(
subprocess.CalledProcessError(REPO_INFO_SOME_UNKNOWN_EXIT_CODE, []),
)
+3 -3
View File
@@ -144,7 +144,7 @@ def test_display_repository_info_with_log_info_and_json_suppresses_most_borg_out
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', 'repo-info', '--critical', '--log-json', '--json', '--repo', 'repo'),
('borg', 'repo-info', '--log-json', '--json', '--repo', 'repo'),
environment=None,
working_directory=None,
borg_local_path='borg',
@@ -222,7 +222,7 @@ def test_display_repository_info_with_log_debug_and_json_suppresses_most_borg_ou
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', 'repo-info', '--critical', '--log-json', '--json', '--repo', 'repo'),
('borg', 'repo-info', '--log-json', '--json', '--repo', 'repo'),
environment=None,
working_directory=None,
borg_local_path='borg',
@@ -258,7 +258,7 @@ def test_display_repository_info_with_json_calls_borg_with_json_flag():
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', 'repo-info', '--critical', '--log-json', '--json', '--repo', 'repo'),
('borg', 'repo-info', '--log-json', '--json', '--repo', 'repo'),
environment=None,
working_directory=None,
borg_local_path='borg',
+16 -9
View File
@@ -948,10 +948,13 @@ def test_make_repo_list_command_includes_umask():
def test_make_repo_list_command_transforms_prefix_into_match_archives():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags').with_args(
'match-archives', 'sh:foo*'
).and_return(
('--match-archives', 'sh:foo*'),
).and_return(())
)
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
None,
None,
@@ -971,14 +974,17 @@ def test_make_repo_list_command_transforms_prefix_into_match_archives():
global_arguments=flexmock(),
)
assert command == ('borg', 'list', '--log-json', '--match-archives', 'sh:foo*', 'repo')
assert command == ('borg', 'repo-list', '--log-json', '--match-archives', 'sh:foo*', 'repo')
def test_make_repo_list_command_prefers_prefix_over_archive_name_format():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags').with_args(
'match-archives', 'sh:foo*'
).and_return(
('--match-archives', 'sh:foo*'),
).and_return(())
)
flexmock(module.flags).should_receive('make_match_archives_flags').never()
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
@@ -994,7 +1000,7 @@ def test_make_repo_list_command_prefers_prefix_over_archive_name_format():
global_arguments=flexmock(),
)
assert command == ('borg', 'list', '--log-json', '--match-archives', 'sh:foo*', 'repo')
assert command == ('borg', 'repo-list', '--log-json', '--match-archives', 'sh:foo*', 'repo')
def test_make_repo_list_command_transforms_archive_name_format_into_match_archives():
@@ -1029,7 +1035,8 @@ def test_make_repo_list_command_transforms_archive_name_format_into_match_archiv
def test_make_repo_list_command_includes_format_from_command_line():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return(
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags').with_args('format', 'stuff').and_return(
('--format', 'stuff')
)
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
+243 -10
View File
@@ -855,10 +855,10 @@ def test_execute_dump_command_runs_mariadb_dump():
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
@@ -907,11 +907,11 @@ def test_execute_dump_command_substitutes_system_flag_for_system_database_name()
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--system=users,udfs,servers',
'--databases',
'--result-file',
'dump',
),
@@ -956,10 +956,10 @@ def test_execute_dump_command_with_environment_password_transport_skips_defaults
'--single-transaction',
'--user',
'root',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
@@ -1007,10 +1007,10 @@ def test_execute_dump_command_runs_mariadb_dump_without_add_drop_database():
'mariadb-dump',
'--defaults-extra-file=/dev/fd/99',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
@@ -1067,10 +1067,10 @@ def test_execute_dump_command_runs_mariadb_dump_with_hostname_and_port():
'5433',
'--protocol',
'tcp',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
@@ -1120,10 +1120,10 @@ def test_execute_dump_command_runs_mariadb_dump_with_tls():
'--add-drop-database',
'--single-transaction',
'--ssl',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
@@ -1173,10 +1173,10 @@ def test_execute_dump_command_runs_mariadb_dump_without_tls():
'--add-drop-database',
'--single-transaction',
'--skip-ssl',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
@@ -1202,6 +1202,159 @@ def test_execute_dump_command_runs_mariadb_dump_without_tls():
)
def test_execute_dump_command_runs_mariadb_dump_without_events():
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',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
),
environment=None,
run_to_completion=False,
working_directory=None,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo', 'events': False},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
dry_run=False,
dry_run_label='',
)
== process
)
def test_execute_dump_command_runs_mariadb_dump_without_routines():
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',
'--events',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
),
environment=None,
run_to_completion=False,
working_directory=None,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo', 'routines': False},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
dry_run=False,
dry_run_label='',
)
== process
)
def test_execute_dump_command_runs_mariadb_dump_without_tablespaces():
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',
'--events',
'--routines',
'--databases',
'foo',
'--result-file',
'dump',
),
environment=None,
run_to_completion=False,
working_directory=None,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo', 'tablespaces': False},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
dry_run=False,
dry_run_label='',
)
== process
)
def test_execute_dump_command_runs_mariadb_dump_with_username_and_password():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
@@ -1225,10 +1378,10 @@ def test_execute_dump_command_runs_mariadb_dump_with_username_and_password():
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
@@ -1278,10 +1431,10 @@ def test_execute_dump_command_runs_mariadb_dump_with_options():
'--stuff=such',
'--add-drop-database',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
@@ -1331,10 +1484,10 @@ def test_execute_dump_command_runs_non_default_mariadb_dump_with_options():
'--stuff=such',
'--add-drop-database',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
@@ -1426,6 +1579,86 @@ def test_execute_dump_command_with_dry_run_skips_mariadb_dump():
)
def test_make_data_source_dump_patterns_with_no_port_adds_pattern_with_default_port():
flexmock(module.borgmatic.config.paths).should_receive(
'get_borgmatic_source_directory'
).and_return('.borgmatic')
flexmock(module).should_receive('make_dump_path').replace_with(lambda path: path)
flexmock(module.dump).should_receive('make_data_source_dump_filename').replace_with(
lambda dump_path, name, hostname, port, container, label: '/'.join(
(dump_path, f'{hostname}:{port}' if port else hostname, name)
)
)
flexmock(module).should_receive('get_default_port').and_return(9999)
assert module.make_data_source_dump_patterns(
databases=flexmock(),
config=flexmock(),
borgmatic_runtime_directory='run',
name='db',
hostname='host',
port=None,
) == (
'borgmatic/host/db',
'run/host/db',
'.borgmatic/host/db',
'borgmatic/host:9999/db',
)
def test_make_data_source_dump_patterns_with_default_port_adds_pattern_with_no_port():
flexmock(module.borgmatic.config.paths).should_receive(
'get_borgmatic_source_directory'
).and_return('.borgmatic')
flexmock(module).should_receive('make_dump_path').replace_with(lambda path: path)
flexmock(module.dump).should_receive('make_data_source_dump_filename').replace_with(
lambda dump_path, name, hostname, port, container, label: '/'.join(
(dump_path, f'{hostname}:{port}' if port else hostname, name)
)
)
flexmock(module).should_receive('get_default_port').and_return(9999)
assert module.make_data_source_dump_patterns(
databases=flexmock(),
config=flexmock(),
borgmatic_runtime_directory='run',
name='db',
hostname='host',
port=9999,
) == (
'borgmatic/host:9999/db',
'run/host:9999/db',
'.borgmatic/host:9999/db',
'borgmatic/host/db',
)
def test_make_data_source_dump_patterns_with_non_default_port_adds_no_extra_patterns():
flexmock(module.borgmatic.config.paths).should_receive(
'get_borgmatic_source_directory'
).and_return('.borgmatic')
flexmock(module).should_receive('make_dump_path').replace_with(lambda path: path)
flexmock(module.dump).should_receive('make_data_source_dump_filename').replace_with(
lambda dump_path, name, hostname, port, container, label: '/'.join(
(dump_path, f'{hostname}:{port}' if port else hostname, name)
)
)
flexmock(module).should_receive('get_default_port').and_return(9999)
assert module.make_data_source_dump_patterns(
databases=flexmock(),
config=flexmock(),
borgmatic_runtime_directory='run',
name='db',
hostname='host',
port=1234,
) == (
'borgmatic/host:1234/db',
'run/host:1234/db',
'.borgmatic/host:1234/db',
)
def test_restore_data_source_dump_runs_mariadb_to_restore():
hook_config = [{'name': 'foo'}, {'name': 'bar'}]
extract_process = flexmock(stdout=flexmock())
@@ -378,6 +378,86 @@ def test_build_dump_command_with_username_injection_attack_gets_escaped():
assert "'bob; naughty-command'" in command
def test_make_data_source_dump_patterns_with_no_port_adds_pattern_with_default_port():
flexmock(module.borgmatic.config.paths).should_receive(
'get_borgmatic_source_directory'
).and_return('.borgmatic')
flexmock(module).should_receive('make_dump_path').replace_with(lambda path: path)
flexmock(module.dump).should_receive('make_data_source_dump_filename').replace_with(
lambda dump_path, name, hostname, port, container, label: '/'.join(
(dump_path, f'{hostname}:{port}' if port else hostname, name)
)
)
flexmock(module).should_receive('get_default_port').and_return(9999)
assert module.make_data_source_dump_patterns(
databases=flexmock(),
config=flexmock(),
borgmatic_runtime_directory='run',
name='db',
hostname='host',
port=None,
) == (
'borgmatic/host/db',
'run/host/db',
'.borgmatic/host/db',
'borgmatic/host:9999/db',
)
def test_make_data_source_dump_patterns_with_default_port_adds_pattern_with_no_port():
flexmock(module.borgmatic.config.paths).should_receive(
'get_borgmatic_source_directory'
).and_return('.borgmatic')
flexmock(module).should_receive('make_dump_path').replace_with(lambda path: path)
flexmock(module.dump).should_receive('make_data_source_dump_filename').replace_with(
lambda dump_path, name, hostname, port, container, label: '/'.join(
(dump_path, f'{hostname}:{port}' if port else hostname, name)
)
)
flexmock(module).should_receive('get_default_port').and_return(9999)
assert module.make_data_source_dump_patterns(
databases=flexmock(),
config=flexmock(),
borgmatic_runtime_directory='run',
name='db',
hostname='host',
port=9999,
) == (
'borgmatic/host:9999/db',
'run/host:9999/db',
'.borgmatic/host:9999/db',
'borgmatic/host/db',
)
def test_make_data_source_dump_patterns_with_non_default_port_adds_no_extra_patterns():
flexmock(module.borgmatic.config.paths).should_receive(
'get_borgmatic_source_directory'
).and_return('.borgmatic')
flexmock(module).should_receive('make_dump_path').replace_with(lambda path: path)
flexmock(module.dump).should_receive('make_data_source_dump_filename').replace_with(
lambda dump_path, name, hostname, port, container, label: '/'.join(
(dump_path, f'{hostname}:{port}' if port else hostname, name)
)
)
flexmock(module).should_receive('get_default_port').and_return(9999)
assert module.make_data_source_dump_patterns(
databases=flexmock(),
config=flexmock(),
borgmatic_runtime_directory='run',
name='db',
hostname='host',
port=1234,
) == (
'borgmatic/host:1234/db',
'run/host:1234/db',
'.borgmatic/host:1234/db',
)
def test_restore_data_source_dump_runs_mongorestore():
hook_config = [{'name': 'foo', 'schemas': None}, {'name': 'bar'}]
extract_process = flexmock(stdout=flexmock())
+242 -9
View File
@@ -718,10 +718,10 @@ def test_execute_dump_command_runs_mysqldump():
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
@@ -771,10 +771,10 @@ def test_execute_dump_command_with_environment_password_transport_skips_defaults
'--single-transaction',
'--user',
'root',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
@@ -822,10 +822,10 @@ def test_execute_dump_command_runs_mysqldump_without_add_drop_database():
'mysqldump',
'--defaults-extra-file=/dev/fd/99',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
@@ -882,10 +882,10 @@ def test_execute_dump_command_runs_mysqldump_with_hostname_and_port():
'5433',
'--protocol',
'tcp',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
@@ -935,10 +935,10 @@ def test_execute_dump_command_runs_mysqldump_with_tls():
'--add-drop-database',
'--single-transaction',
'--ssl',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
@@ -988,10 +988,10 @@ def test_execute_dump_command_runs_mysqldump_without_tls():
'--add-drop-database',
'--single-transaction',
'--skip-ssl',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
@@ -1017,6 +1017,159 @@ def test_execute_dump_command_runs_mysqldump_without_tls():
)
def test_execute_dump_command_runs_mysqldump_without_events():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).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(
(
'mysqldump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
),
environment=None,
run_to_completion=False,
working_directory=None,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo', 'events': False},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
dry_run=False,
dry_run_label='',
)
== process
)
def test_execute_dump_command_runs_mysqldump_without_routines():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).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(
(
'mysqldump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--events',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
),
environment=None,
run_to_completion=False,
working_directory=None,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo', 'routines': False},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
dry_run=False,
dry_run_label='',
)
== process
)
def test_execute_dump_command_runs_mysqldump_without_tablespaces():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential',
).replace_with(lambda value, config: value)
flexmock(module.borgmatic.hooks.data_source.mariadb).should_receive(
'parse_extra_options',
).and_return((), None)
flexmock(module.database_config).should_receive('resolve_database_option').and_return(None)
flexmock(module.borgmatic.hooks.data_source.mariadb).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(
(
'mysqldump',
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--events',
'--routines',
'--databases',
'foo',
'--result-file',
'dump',
),
environment=None,
run_to_completion=False,
working_directory=None,
).and_return(process).once()
assert (
module.execute_dump_command(
database={'name': 'foo', 'tablespaces': False},
config={},
username='root',
password='trustsome1',
dump_path=flexmock(),
database_names=('foo',),
environment=None,
dry_run=False,
dry_run_label='',
)
== process
)
def test_execute_dump_command_runs_mysqldump_with_username_and_password():
process = flexmock()
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
@@ -1040,10 +1193,10 @@ def test_execute_dump_command_runs_mysqldump_with_username_and_password():
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
@@ -1093,10 +1246,10 @@ def test_execute_dump_command_runs_mysqldump_with_options():
'--stuff=such',
'--add-drop-database',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
@@ -1145,10 +1298,10 @@ def test_execute_dump_command_runs_non_default_mysqldump():
'--defaults-extra-file=/dev/fd/99',
'--add-drop-database',
'--single-transaction',
'--databases',
'--events',
'--routines',
'--all-tablespaces',
'--databases',
'foo',
'--result-file',
'dump',
@@ -1239,6 +1392,86 @@ def test_execute_dump_command_with_dry_run_skips_mysqldump():
)
def test_make_data_source_dump_patterns_with_no_port_adds_pattern_with_default_port():
flexmock(module.borgmatic.config.paths).should_receive(
'get_borgmatic_source_directory'
).and_return('.borgmatic')
flexmock(module).should_receive('make_dump_path').replace_with(lambda path: path)
flexmock(module.dump).should_receive('make_data_source_dump_filename').replace_with(
lambda dump_path, name, hostname, port, container, label: '/'.join(
(dump_path, f'{hostname}:{port}' if port else hostname, name)
)
)
flexmock(module).should_receive('get_default_port').and_return(9999)
assert module.make_data_source_dump_patterns(
databases=flexmock(),
config=flexmock(),
borgmatic_runtime_directory='run',
name='db',
hostname='host',
port=None,
) == (
'borgmatic/host/db',
'run/host/db',
'.borgmatic/host/db',
'borgmatic/host:9999/db',
)
def test_make_data_source_dump_patterns_with_default_port_adds_pattern_with_no_port():
flexmock(module.borgmatic.config.paths).should_receive(
'get_borgmatic_source_directory'
).and_return('.borgmatic')
flexmock(module).should_receive('make_dump_path').replace_with(lambda path: path)
flexmock(module.dump).should_receive('make_data_source_dump_filename').replace_with(
lambda dump_path, name, hostname, port, container, label: '/'.join(
(dump_path, f'{hostname}:{port}' if port else hostname, name)
)
)
flexmock(module).should_receive('get_default_port').and_return(9999)
assert module.make_data_source_dump_patterns(
databases=flexmock(),
config=flexmock(),
borgmatic_runtime_directory='run',
name='db',
hostname='host',
port=9999,
) == (
'borgmatic/host:9999/db',
'run/host:9999/db',
'.borgmatic/host:9999/db',
'borgmatic/host/db',
)
def test_make_data_source_dump_patterns_with_non_default_port_adds_no_extra_patterns():
flexmock(module.borgmatic.config.paths).should_receive(
'get_borgmatic_source_directory'
).and_return('.borgmatic')
flexmock(module).should_receive('make_dump_path').replace_with(lambda path: path)
flexmock(module.dump).should_receive('make_data_source_dump_filename').replace_with(
lambda dump_path, name, hostname, port, container, label: '/'.join(
(dump_path, f'{hostname}:{port}' if port else hostname, name)
)
)
flexmock(module).should_receive('get_default_port').and_return(9999)
assert module.make_data_source_dump_patterns(
databases=flexmock(),
config=flexmock(),
borgmatic_runtime_directory='run',
name='db',
hostname='host',
port=1234,
) == (
'borgmatic/host:1234/db',
'run/host:1234/db',
'.borgmatic/host:1234/db',
)
def test_restore_data_source_dump_runs_mysql_to_restore():
hook_config = [{'name': 'foo'}, {'name': 'bar'}]
extract_process = flexmock(stdout=flexmock())
@@ -979,6 +979,86 @@ def test_dump_data_sources_runs_non_default_pg_dump():
) == [process]
def test_make_data_source_dump_patterns_with_no_port_adds_pattern_with_default_port():
flexmock(module.borgmatic.config.paths).should_receive(
'get_borgmatic_source_directory'
).and_return('.borgmatic')
flexmock(module).should_receive('make_dump_path').replace_with(lambda path: path)
flexmock(module.dump).should_receive('make_data_source_dump_filename').replace_with(
lambda dump_path, name, hostname, port, container, label: '/'.join(
(dump_path, f'{hostname}:{port}' if port else hostname, name)
)
)
flexmock(module).should_receive('get_default_port').and_return(9999)
assert module.make_data_source_dump_patterns(
databases=flexmock(),
config=flexmock(),
borgmatic_runtime_directory='run',
name='db',
hostname='host',
port=None,
) == (
'borgmatic/host/db',
'run/host/db',
'.borgmatic/host/db',
'borgmatic/host:9999/db',
)
def test_make_data_source_dump_patterns_with_default_port_adds_pattern_with_no_port():
flexmock(module.borgmatic.config.paths).should_receive(
'get_borgmatic_source_directory'
).and_return('.borgmatic')
flexmock(module).should_receive('make_dump_path').replace_with(lambda path: path)
flexmock(module.dump).should_receive('make_data_source_dump_filename').replace_with(
lambda dump_path, name, hostname, port, container, label: '/'.join(
(dump_path, f'{hostname}:{port}' if port else hostname, name)
)
)
flexmock(module).should_receive('get_default_port').and_return(9999)
assert module.make_data_source_dump_patterns(
databases=flexmock(),
config=flexmock(),
borgmatic_runtime_directory='run',
name='db',
hostname='host',
port=9999,
) == (
'borgmatic/host:9999/db',
'run/host:9999/db',
'.borgmatic/host:9999/db',
'borgmatic/host/db',
)
def test_make_data_source_dump_patterns_with_non_default_port_adds_no_extra_patterns():
flexmock(module.borgmatic.config.paths).should_receive(
'get_borgmatic_source_directory'
).and_return('.borgmatic')
flexmock(module).should_receive('make_dump_path').replace_with(lambda path: path)
flexmock(module.dump).should_receive('make_data_source_dump_filename').replace_with(
lambda dump_path, name, hostname, port, container, label: '/'.join(
(dump_path, f'{hostname}:{port}' if port else hostname, name)
)
)
flexmock(module).should_receive('get_default_port').and_return(9999)
assert module.make_data_source_dump_patterns(
databases=flexmock(),
config=flexmock(),
borgmatic_runtime_directory='run',
name='db',
hostname='host',
port=1234,
) == (
'borgmatic/host:1234/db',
'run/host:1234/db',
'.borgmatic/host:1234/db',
)
def test_restore_data_source_dump_runs_pg_restore():
hook_config = [{'name': 'foo', 'schemas': None}, {'name': 'bar'}]
extract_process = flexmock(stdout=flexmock())
+44 -4
View File
@@ -528,6 +528,9 @@ def test_remove_data_source_dumps_unmounts_and_destroys_snapshots():
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob',
).and_return('/run/borgmatic')
flexmock(module.hashlib).should_receive('shake_256').and_return(
flexmock(hexdigest=lambda length: 'b33f')
)
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [path.replace('*', 'b33f')],
)
@@ -561,6 +564,9 @@ def test_remove_data_source_dumps_use_custom_commands():
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob',
).and_return('/run/borgmatic')
flexmock(module.hashlib).should_receive('shake_256').and_return(
flexmock(hexdigest=lambda length: 'b33f')
)
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [path.replace('*', 'b33f')],
)
@@ -644,6 +650,9 @@ def test_remove_data_source_dumps_bails_for_missing_umount_command():
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob',
).and_return('/run/borgmatic')
flexmock(module.hashlib).should_receive('shake_256').and_return(
flexmock(hexdigest=lambda length: 'b33f')
)
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [path.replace('*', 'b33f')],
)
@@ -675,6 +684,9 @@ def test_remove_data_source_dumps_swallows_umount_command_error():
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob',
).and_return('/run/borgmatic')
flexmock(module.hashlib).should_receive('shake_256').and_return(
flexmock(hexdigest=lambda length: 'b33f')
)
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [path.replace('*', 'b33f')],
)
@@ -709,6 +721,9 @@ def test_remove_data_source_dumps_skips_unmount_snapshot_directories_that_are_no
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob',
).and_return('/run/borgmatic')
flexmock(module.hashlib).should_receive('shake_256').and_return(
flexmock(hexdigest=lambda length: 'b33f')
)
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [path.replace('*', 'b33f')],
)
@@ -739,6 +754,9 @@ def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_that_are_no
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob',
).and_return('/run/borgmatic')
flexmock(module.hashlib).should_receive('shake_256').and_return(
flexmock(hexdigest=lambda length: 'b33f')
)
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [path.replace('*', 'b33f')],
)
@@ -774,21 +792,37 @@ def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_for_unknown
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob',
).and_return('/run/borgmatic')
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [path.replace('*', 'b33f')],
flexmock(module.hashlib).should_receive('shake_256').with_args(b'/mnt/dataset').and_return(
flexmock(hexdigest=lambda length: 'd34d')
)
flexmock(module.hashlib).should_receive('shake_256').with_args(
b'/mnt/dataset/shadow'
).and_return(flexmock(hexdigest=lambda length: 'b33f'))
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [
path.replace('*', 'd34d'),
path.replace('*', 'b33f'),
path.replace('*', 'unknown'),
],
)
flexmock(module.os.path).should_receive('isdir').with_args(
'/run/borgmatic/zfs_snapshots/d34d',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/run/borgmatic/zfs_snapshots/b33f',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
'/run/borgmatic/zfs_snapshots/d34d/mnt/dataset',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/run/borgmatic/zfs_snapshots/b33f/mnt/dataset/shadow',
).and_return(True)
flexmock(module.os.path).should_receive('isdir').with_args(
'/run/borgmatic/zfs_snapshots/unknown',
).and_return(True)
flexmock(module.shutil).should_receive('rmtree')
flexmock(module).should_receive('unmount_snapshot').with_args(
'umount', '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset'
'umount', '/run/borgmatic/zfs_snapshots/d34d/mnt/dataset'
).once()
flexmock(module).should_receive('unmount_snapshot').with_args(
'umount', '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset/shadow'
@@ -817,6 +851,9 @@ def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_after_rmtre
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob',
).and_return('/run/borgmatic')
flexmock(module.hashlib).should_receive('shake_256').and_return(
flexmock(hexdigest=lambda length: 'b33f')
)
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [path.replace('*', 'b33f')],
)
@@ -852,6 +889,9 @@ def test_remove_data_source_dumps_with_dry_run_skips_unmount_and_destroy():
flexmock(module.borgmatic.config.paths).should_receive(
'replace_temporary_subdirectory_with_glob',
).and_return('/run/borgmatic')
flexmock(module.hashlib).should_receive('shake_256').and_return(
flexmock(hexdigest=lambda length: 'b33f')
)
flexmock(module.glob).should_receive('glob').replace_with(
lambda path: [path.replace('*', 'b33f')],
)
+45
View File
@@ -124,6 +124,20 @@ def test_borg_json_log_line_to_record_parses_log_message_line():
assert record.levelno == module.logging.INFO
assert record.created == 12345
assert record.msg == 'All done'
assert record.msgid is None
assert record.levelname == 'INFO'
assert record.name == 'borg.something'
def test_borg_json_log_line_to_record_parses_log_message_line_with_message_id():
line = '{"type": "log_message", "levelname": "INFO", "time": 12345, "message": "All done", "msgid": "all.done", "name": "borg.something"}'
record = module.borg_json_log_line_to_record(line, module.logging.INFO)
assert record.levelno == module.logging.INFO
assert record.created == 12345
assert record.msg == 'All done'
assert record.msgid == 'all.done'
assert record.levelname == 'INFO'
assert record.name == 'borg.something'
@@ -136,6 +150,20 @@ def test_borg_json_log_line_to_record_elevates_log_message_info_level_to_small_j
assert record.levelno == 25
assert record.created == 12345
assert record.msg == 'All done'
assert record.msgid is None
assert record.levelname in {'ANSWER', 'Level 25'}
assert record.name == 'borg.something'
def test_borg_json_log_line_to_record_elevates_log_message_and_includes_message_id():
line = '{"type": "log_message", "levelname": "INFO", "time": 12345, "message": "All done", "msgid": "all.done", "name": "borg.something"}'
record = module.borg_json_log_line_to_record(line, 25)
assert record.levelno == 25
assert record.created == 12345
assert record.msg == 'All done'
assert record.msgid == 'all.done'
assert record.levelname in {'ANSWER', 'Level 25'}
assert record.name == 'borg.something'
@@ -148,6 +176,7 @@ def test_borg_json_log_line_to_record_does_not_elevate_log_message_info_level_to
assert record.levelno == module.logging.INFO
assert record.created == 12345
assert record.msg == 'All done'
assert record.msgid is None
assert record.levelname == 'INFO'
assert record.name == 'borg.something'
@@ -160,6 +189,7 @@ def test_borg_json_log_line_with_none_log_level_parses_log_message_line():
assert record.levelno == module.logging.INFO
assert record.created == 12345
assert record.msg == 'All done'
assert record.msgid is None
assert record.levelname == 'INFO'
assert record.name == 'borg.something'
@@ -173,6 +203,21 @@ def test_borg_json_log_line_to_record_parses_file_status_line():
assert record.levelno == module.logging.INFO
assert record.created == 12345
assert record.msg == '- /foo/bar'
assert record.msgid is None
assert record.levelname == 'INFO'
assert record.name == 'borg.file_status'
def test_borg_json_log_line_to_record_parses_file_status_line_with_message_id():
flexmock(module.time).should_receive('time').and_return(12345)
line = '{"type": "file_status", "status": "-", "path": "/foo/bar", "msgid": "hi.there"}'
record = module.borg_json_log_line_to_record(line, module.logging.INFO)
assert record.levelno == module.logging.INFO
assert record.created == 12345
assert record.msg == '- /foo/bar'
assert record.msgid == 'hi.there'
assert record.levelname == 'INFO'
assert record.name == 'borg.file_status'
+128
View File
@@ -243,6 +243,23 @@ def test_log_record_to_json_formats_record_as_json():
)
def test_log_record_to_json_with_message_id_formats_record_as_json():
assert (
module.log_record_to_json(
flexmock(
created=12345,
levelno=module.logging.INFO,
levelname='INFO',
name='borg.something',
extra='ignored',
getMessage=lambda: 'All done',
msgid='all.done',
)
)
== '{"type": "log_message", "time": 12345, "message": "All done", "levelname": "INFO", "name": "borg.something", "msgid": "all.done"}'
)
def test_console_color_formatter_format_includes_log_message():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.ANSWER
@@ -417,6 +434,117 @@ def test_log_prefix_sets_prefix_and_then_restores_original_prefix_after():
pass
def test_log_exclude_filter_filter_omits_log_matching_any_attributes():
exclude_filter = module.Log_exclude_filter('my filter', {'foo': 'bar', 'baz': 'quux'})
assert exclude_filter.filter(flexmock(foo='nope', baz='quux')) is False
def test_log_exclude_filter_filter_includes_log_matching_no_attributes():
exclude_filter = module.Log_exclude_filter('my filter', {'foo': 'bar', 'baz': 'quux'})
assert exclude_filter.filter(flexmock(foo='nope', baz='uh uh')) is True
def test_log_exclude_filter_filter_includes_log_matching_no_attributes_and_in_fact_missing_them_entirely():
exclude_filter = module.Log_exclude_filter('my filter', {'foo': 'bar', 'baz': 'quux'})
assert exclude_filter.filter(flexmock(other='nope', thing='uh uh')) is True
def test_add_log_exclude_filter_adds_filter_to_each_handler():
flexmock(module).should_receive('Log_exclude_filter').and_return(flexmock())
handlers = [flexmock(), flexmock()]
handlers[0].should_receive('addFilter').once()
handlers[1].should_receive('addFilter').once()
flexmock(module.logging).should_receive('getLogger').and_return(
flexmock(handlers=handlers, removeHandler=lambda handler: None)
)
module.add_log_exclude_filter('my filter', {'foo': 'bar', 'baz': 'quux'})
def test_remove_log_exclude_filter_removes_filter_from_each_handler():
handlers = [
flexmock(
filters=[
flexmock(name='my filter'),
flexmock(name='my filter'),
],
),
flexmock(
filters=[
flexmock(name='my filter'),
],
),
]
handlers[0].should_receive('removeFilter').twice()
handlers[1].should_receive('removeFilter').once()
flexmock(module.logging).should_receive('getLogger').and_return(
flexmock(handlers=handlers, removeHandler=lambda handler: None),
)
module.remove_log_exclude_filter(name='my filter')
def test_remove_log_exclude_filter_skips_remove_for_filter_without_matching_name():
handlers = [
flexmock(
filters=[
flexmock(name='other filter'),
flexmock(name='my filter'),
],
),
flexmock(
filters=[
flexmock(name='my filter'),
],
),
]
handlers[0].should_receive('removeFilter').once()
handlers[1].should_receive('removeFilter').once()
flexmock(module.logging).should_receive('getLogger').and_return(
flexmock(handlers=handlers, removeHandler=lambda handler: None),
)
module.remove_log_exclude_filter(name='my filter')
def test_remove_log_exclude_filter_skips_remove_for_filter_without_name_attribute():
handlers = [
flexmock(
filters=[
flexmock(),
flexmock(name='my filter'),
],
),
flexmock(
filters=[
flexmock(name='my filter'),
],
),
]
handlers[0].should_receive('removeFilter').once()
handlers[1].should_receive('removeFilter').once()
flexmock(module.logging).should_receive('getLogger').and_return(
flexmock(handlers=handlers, removeHandler=lambda handler: None),
)
module.remove_log_exclude_filter(name='my filter')
def test_logs_suppressed_adds_and_removes_log_exclude_filter():
flexmock(module).should_receive('add_log_exclude_filter').once()
flexmock(module).should_receive('remove_log_exclude_filter').once()
with module.Logs_suppressed(foo='bar', baz='quux'):
pass
def test_delayed_logging_handler_should_flush_without_targets_returns_false():
handler = module.Delayed_logging_handler()