Compare commits

...

43 commits

Author SHA1 Message Date
5cea1e1b72 Fix flake error (#262). 2025-03-29 22:52:17 -07:00
5716e61f8f Code formatting (#262). 2025-03-29 19:54:40 -07:00
65d1b9235d Add "default_actions" to NEWS (#262). 2025-03-29 19:02:11 -07:00
a8362f2618 borgmatic without arguments/parameters should show usage help instead of starting a backup (#262).
Reviewed-on: borgmatic-collective/borgmatic#1046
2025-03-30 01:57:11 +00:00
36265eea7d Docs update 2025-03-30 01:34:30 +00:00
da324ebeb7 Add "recreate" action to NEWS and docs (#610). 2025-03-29 15:15:36 -07:00
59f9d56aae Add a recreate action (#1030).
Reviewed-on: borgmatic-collective/borgmatic#1030
2025-03-29 22:07:52 +00:00
Vandal
dbf2e78f62 help changes 2025-03-30 03:05:46 +05:30
Vandal
2716d9d0b0 add to schema 2025-03-29 23:25:50 +05:30
0182dbd914 Added 2 new unit tests and updated docs 2025-03-29 03:43:58 +00:00
Vandal
8b3a682edf add tests and minor fixes 2025-03-29 01:26:20 +05:30
Vandal
7020f0530a update existing tests 2025-03-28 22:22:19 +05:30
Vandal
26fd41da92 add rest of flags 2025-03-27 22:18:34 +05:30
088da19012 Added Unit Tests 2025-03-27 11:26:56 +00:00
486bec698d Add "key import" to reference documentation (#345). 2025-03-26 22:13:30 -07:00
7a766c717e 2nd Draft 2025-03-27 02:55:16 +00:00
520fb78a00 Clarify Btrfs documentation: borgmatic expects subvolume mount points in "source_directories" (#1043). 2025-03-26 11:39:16 -07:00
Vandal
acc2814f11 add archive timestamp filter 2025-03-26 23:39:06 +05:30
996b037946 1st 2025-03-26 17:39:10 +00:00
Vandal
9356924418 add archive options 2025-03-26 22:30:11 +05:30
79e4e089ee Fix typo in NEWS (#1044). 2025-03-26 09:57:53 -07:00
d2714cb706 Fix an error in the systemd credential hook when the credential name contains a "." chararcter (#1044). 2025-03-26 09:53:52 -07:00
23efbb8df3 Fix line wrapping / code style (#837). 2025-03-25 22:31:50 -07:00
9e694e4df9 Add MongoDB custom command options to NEWS (#837). 2025-03-25 22:28:14 -07:00
76f7c53a1c Add custom command options for MongoDB hook (#837).
Reviewed-on: borgmatic-collective/borgmatic#1041
2025-03-26 05:27:03 +00:00
Vandal
203e84b91f hotfix 2025-03-25 21:57:06 +05:30
Vandal
ea5a2d8a46 add tests for the flags 2025-03-25 20:39:02 +05:30
Vandal
a8726c408a add tests 2025-03-25 19:35:15 +05:30
Vandal
3542673446 add test recreate with skip action 2025-03-25 11:36:06 +05:30
532a97623c Added test_build_restore_command_prevents_shell_injection() 2025-03-25 04:50:45 +00:00
e1fdfe4c2f Add credential hook directory expansion to NEWS (#422). 2025-03-24 13:00:38 -07:00
83a56a3fef Add directory expansion for file-based and KeyPassXC credential hooks (#1042).
Reviewed-on: borgmatic-collective/borgmatic#1042
2025-03-24 19:57:18 +00:00
Vandal
b60cf2449a add recreate to schema 2025-03-25 00:48:27 +05:30
Vandal
e7f14bca87 add tests and requested changes 2025-03-25 00:16:20 +05:30
Nish_
4bca7bb198 add directory expansion for file-based and KeyPassXC credentials
Signed-off-by: Nish_ <120EE0980@nitrkl.ac.in>
2025-03-24 21:04:55 +05:30
Vandal
fa3b140590 add patterns 2025-03-24 12:09:08 +05:30
Vandal
a1d2f7f221 add path 2025-03-24 11:51:33 +05:30
6a470be924 Made some changes in test file 2025-03-24 03:53:42 +00:00
d651813601 Custom command options for MongoDB hook #837 2025-03-24 03:39:26 +00:00
Vandal
a750d58a2d add recreate action 2025-03-22 21:18:28 +05:30
2045706faa merge upstream 2025-03-22 13:00:07 +00:00
Vandal
4e2805918d update borg/recreate.py 2025-03-18 23:19:33 +05:30
Vandal
6adb0fd44c add borg recreate 2025-03-17 22:24:53 +05:30
20 changed files with 1310 additions and 29 deletions

8
NEWS
View file

@ -1,5 +1,10 @@
2.0.0.dev0
* #262: Add a "default_actions" option that supports disabling default actions when borgmatic is
run without any command-line arguments.
* #345: Add a "key import" action to import a repository key from backup.
* #422: Add home directory expansion to file-based and KeePassXC credential hooks.
* #610: Add a "recreate" action for recreating archives, for instance for retroactively excluding
particular files from existing archives.
* #790, #821: Deprecate all "before_*", "after_*" and "on_error" command hooks in favor of more
flexible "commands:". See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/
@ -9,11 +14,14 @@
* #790: BREAKING: Run all command hooks (both new and deprecated) respecting the
"working_directory" option if configured, meaning that hook commands are run in that directory.
* #836: Add a custom command option for the SQLite hook.
* #837: Add custom command options for the MongoDB hook.
* #1010: When using Borg 2, don't pass the "--stats" flag to "borg prune".
* #1020: Document a database use case involving a temporary database client container:
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#containers
* #1037: Fix an error with the "extract" action when both a remote repository and a
"working_directory" are used.
* #1044: Fix an error in the systemd credential hook when the credential name contains a "."
character.
1.9.14
* #409: With the PagerDuty monitoring hook, send borgmatic logs to PagerDuty so they show up in the

View file

@ -0,0 +1,53 @@
import logging
import borgmatic.borg.recreate
import borgmatic.config.validate
from borgmatic.actions.create import collect_patterns, process_patterns
logger = logging.getLogger(__name__)
def run_recreate(
repository,
config,
local_borg_version,
recreate_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "recreate" action for the given repository.
'''
if recreate_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, recreate_arguments.repository
):
if recreate_arguments.archive:
logger.answer(f'Recreating archive {recreate_arguments.archive}')
else:
logger.answer('Recreating repository')
# Collect and process patterns.
processed_patterns = process_patterns(
collect_patterns(config), borgmatic.config.paths.get_working_directory(config)
)
borgmatic.borg.recreate.recreate_archive(
repository['path'],
borgmatic.borg.repo_list.resolve_archive_name(
repository['path'],
recreate_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
),
config,
local_borg_version,
recreate_arguments,
global_arguments,
local_path=local_path,
remote_path=remote_path,
patterns=processed_patterns,
)

100
borgmatic/borg/recreate.py Normal file
View file

@ -0,0 +1,100 @@
import logging
import shlex
import borgmatic.borg.environment
import borgmatic.config.paths
import borgmatic.execute
from borgmatic.borg import flags
from borgmatic.borg.create import make_exclude_flags, make_list_filter_flags, write_patterns_file
logger = logging.getLogger(__name__)
def recreate_archive(
repository,
archive,
config,
local_borg_version,
recreate_arguments,
global_arguments,
local_path,
remote_path=None,
patterns=None,
):
'''
Given a local or remote repository path, an archive name, a configuration dict,
the local Borg version string, an argparse.Namespace of recreate arguments,
an argparse.Namespace of global arguments, optional local and remote Borg paths.
Executes the recreate command with the given arguments.
'''
lock_wait = config.get('lock_wait', None)
exclude_flags = make_exclude_flags(config)
compression = config.get('compression', None)
chunker_params = config.get('chunker_params', None)
# Available recompress MODES: 'if-different' (default), 'always', 'never'
recompress = config.get('recompress', None)
# Write patterns to a temporary file and use that file with --patterns-from.
patterns_file = write_patterns_file(
patterns, borgmatic.config.paths.get_working_directory(config)
)
recreate_command = (
(local_path, 'recreate')
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--log-json',) if global_arguments.log_json else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait is not None else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--patterns-from', patterns_file.name) if patterns_file else ())
+ (
(
'--list',
'--filter',
make_list_filter_flags(local_borg_version, global_arguments.dry_run),
)
if recreate_arguments.list
else ()
)
# Flag --target works only for a single archive
+ (('--target', recreate_arguments.target) if recreate_arguments.target and archive else ())
+ (
('--comment', shlex.quote(recreate_arguments.comment))
if recreate_arguments.comment
else ()
)
+ (('--timestamp', recreate_arguments.timestamp) if recreate_arguments.timestamp else ())
+ (('--compression', compression) if compression else ())
+ (('--chunker-params', chunker_params) if chunker_params else ())
+ (
flags.make_match_archives_flags(
recreate_arguments.match_archives or archive or config.get('match_archives'),
config.get('archive_name_format'),
local_borg_version,
)
if recreate_arguments.match_archives
else ()
)
+ (('--recompress', recompress) if recompress else ())
+ exclude_flags
+ (
flags.make_repository_archive_flags(repository, archive, local_borg_version)
if archive
else flags.make_repository_flags(repository, local_borg_version)
)
)
if global_arguments.dry_run:
logger.info('Skipping the archive recreation (dry run)')
return
borgmatic.execute.execute_command(
full_command=recreate_command,
output_log_level=logging.INFO,
environment=borgmatic.borg.environment.make_environment(config),
working_directory=borgmatic.config.paths.get_working_directory(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)

View file

@ -27,6 +27,7 @@ ACTION_ALIASES = {
'break-lock': [],
'key': [],
'borg': [],
'recreate': [],
}
@ -1521,6 +1522,52 @@ def make_parsers():
'-h', '--help', action='help', help='Show this help message and exit'
)
recreate_parser = action_parsers.add_parser(
'recreate',
aliases=ACTION_ALIASES['recreate'],
help='Recreate an archive in a repository',
description='Recreate an archive in a repository',
add_help=False,
)
recreate_group = recreate_parser.add_argument_group('recreate arguments')
recreate_group.add_argument(
'--repository',
help='Path of repository containing archive to recreate, defaults to the configured repository if there is only one, quoted globs supported',
)
recreate_group.add_argument(
'--archive',
help='Archive name, hash, or series to recreate',
)
recreate_group.add_argument(
'--list', dest='list', action='store_true', help='Show per-file details'
)
recreate_group.add_argument(
'--target',
metavar='TARGET',
help='Create a new archive from the specified archive (via --archive), without replacing it',
)
recreate_group.add_argument(
'--comment',
metavar='COMMENT',
help='Add a comment text to the archive or, if an archive is not provided, to all matching archives',
)
recreate_group.add_argument(
'--timestamp',
metavar='TIMESTAMP',
help='Manually override the archive creation date/time (UTC)',
)
recreate_group.add_argument(
'-a',
'--match-archives',
'--glob-archives',
dest='match_archives',
metavar='PATTERN',
help='Only consider archive names, hashes, or series matching this pattern',
)
recreate_group.add_argument(
'-h', '--help', action='help', help='Show this help message and exit'
)
borg_parser = action_parsers.add_parser(
'borg',
aliases=ACTION_ALIASES['borg'],

View file

@ -26,6 +26,7 @@ import borgmatic.actions.info
import borgmatic.actions.list
import borgmatic.actions.mount
import borgmatic.actions.prune
import borgmatic.actions.recreate
import borgmatic.actions.repo_create
import borgmatic.actions.repo_delete
import borgmatic.actions.repo_info
@ -397,6 +398,16 @@ def run_actions(
local_path,
remote_path,
)
elif action_name == 'recreate' and action_name not in skip_actions:
borgmatic.actions.recreate.run_recreate(
repository,
config,
local_borg_version,
action_arguments,
global_arguments,
local_path,
remote_path,
)
elif action_name == 'prune' and action_name not in skip_actions:
borgmatic.actions.prune.run_prune(
config_filename,
@ -943,6 +954,19 @@ def exit_with_help_link(): # pragma: no cover
sys.exit(1)
def check_and_show_help_on_no_args(configs):
'''
Check if the borgmatic command is run without any arguments. If the configuration option
"default_actions" is set to False, show the help message. Otherwise, trigger the default backup
behavior.
'''
if len(sys.argv) == 1: # No arguments provided
default_actions = any(config.get('default_actions', True) for config in configs.values())
if not default_actions:
parse_arguments('--help')
sys.exit(0)
def main(extra_summary_logs=[]): # pragma: no cover
configure_signals()
configure_delayed_logging()
@ -978,6 +1002,10 @@ def main(extra_summary_logs=[]): # pragma: no cover
global_arguments.overrides,
resolve_env=global_arguments.resolve_env and not validate,
)
# Use the helper function to check and show help on no arguments, passing the preloaded configs
check_and_show_help_on_no_args(configs)
configuration_parse_errors = (
(max(log.levelno for log in parse_logs) >= logging.CRITICAL) if parse_logs else False
)

View file

@ -284,6 +284,22 @@ properties:
http://borgbackup.readthedocs.io/en/stable/usage/create.html for
details. Defaults to "lz4".
example: lz4
recompress:
type: string
enum: ['if-different', 'always', 'never']
description: |
Mode for recompressing data chunks according to MODE.
Possible modes are:
* "if-different": Recompress if the current compression
is with a different compression algorithm.
* "always": Recompress even if the current compression
is with the same compression algorithm. Use this to change
the compression level.
* "never": Do not recompress. Use this option to explicitly
prevent recompression.
See https://borgbackup.readthedocs.io/en/stable/usage/recreate.html
for details. Defaults to "never".
example: if-different
upload_rate_limit:
type: integer
description: |
@ -767,6 +783,7 @@ properties:
- prune
- compact
- create
- recreate
- check
- delete
- extract
@ -982,6 +999,7 @@ properties:
- prune
- compact
- create
- recreate
- check
- delete
- extract
@ -1046,6 +1064,7 @@ properties:
- prune
- compact
- create
- recreate
- check
- delete
- extract
@ -1726,6 +1745,25 @@ properties:
dump command, without performing any validation on them.
See mongorestore documentation for details.
example: --restoreDbUsersAndRoles
mongodump_command:
type: string
description: |
Command to use instead of "mongodump". This can be used
to run a specific mongodump version (e.g., one inside a
running container). If you run it from within a
container, make sure to mount the path in the
"user_runtime_directory" option from the host into the
container at the same location. Defaults to
"mongodump".
example: docker exec mongodb_container mongodump
mongorestore_command:
type: string
description: |
Command to run when restoring a database instead of
"mongorestore". This can be used to run a specific
mongorestore version (e.g., one inside a running
container). Defaults to "mongorestore".
example: docker exec mongodb_container mongorestore
description: |
List of one or more MongoDB databases to dump before creating a
backup, run once per configuration file. The database dumps are
@ -2647,3 +2685,11 @@ properties:
example: /usr/local/bin/keepassxc-cli
description: |
Configuration for integration with the KeePassXC password manager.
default_actions:
type: boolean
description: |
Whether to apply default actions (e.g., backup) when no arguments
are supplied to the borgmatic command. If set to true, borgmatic
triggers the default actions (create, prune, compact and check). If
set to false, borgmatic displays the help message instead.
example: true

View file

@ -19,9 +19,11 @@ def load_credential(hook_config, config, credential_parameters):
raise ValueError(f'Cannot load invalid credential: "{name}"')
expanded_credential_path = os.path.expanduser(credential_path)
try:
with open(
os.path.join(config.get('working_directory', ''), credential_path)
os.path.join(config.get('working_directory', ''), expanded_credential_path)
) as credential_file:
return credential_file.read().rstrip(os.linesep)
except (FileNotFoundError, OSError) as error:

View file

@ -24,7 +24,9 @@ def load_credential(hook_config, config, credential_parameters):
f'Cannot load credential with invalid KeePassXC database path and attribute name: "{path_and_name}"'
)
if not os.path.exists(database_path):
expanded_database_path = os.path.expanduser(database_path)
if not os.path.exists(expanded_database_path):
raise ValueError(
f'Cannot load credential because KeePassXC database path does not exist: {database_path}'
)
@ -36,7 +38,7 @@ def load_credential(hook_config, config, credential_parameters):
'--show-protected',
'--attributes',
'Password',
database_path,
expanded_database_path,
attribute_name,
)
).rstrip(os.linesep)

View file

@ -5,7 +5,7 @@ import re
logger = logging.getLogger(__name__)
CREDENTIAL_NAME_PATTERN = re.compile(r'^\w+$')
CREDENTIAL_NAME_PATTERN = re.compile(r'^[\w.-]+$')
def load_credential(hook_config, config, credential_parameters):

View file

@ -114,14 +114,17 @@ def make_password_config_file(password):
def build_dump_command(database, config, dump_filename, dump_format):
'''
Return the mongodump command from a single database configuration.
Return the custom mongodump_command from a single database configuration.
'''
all_databases = database['name'] == 'all'
password = borgmatic.hooks.credential.parse.resolve_credential(database.get('password'), config)
dump_command = tuple(
shlex.quote(part) for part in shlex.split(database.get('mongodump_command') or 'mongodump')
)
return (
('mongodump',)
dump_command
+ (('--out', shlex.quote(dump_filename)) if dump_format == 'directory' else ())
+ (('--host', shlex.quote(database['hostname'])) if 'hostname' in database else ())
+ (('--port', shlex.quote(str(database['port']))) if 'port' in database else ())
@ -230,7 +233,7 @@ def restore_data_source_dump(
def build_restore_command(extract_process, database, config, dump_filename, connection_params):
'''
Return the mongorestore command from a single database configuration.
Return the custom mongorestore_command from a single database configuration.
'''
hostname = connection_params['hostname'] or database.get(
'restore_hostname', database.get('hostname')
@ -251,7 +254,10 @@ def build_restore_command(extract_process, database, config, dump_filename, conn
config,
)
command = ['mongorestore']
command = list(
shlex.quote(part)
for part in shlex.split(database.get('mongorestore_command') or 'mongorestore')
)
if extract_process:
command.append('--archive')
else:

View file

@ -4,7 +4,7 @@ COPY . /app
RUN apk add --no-cache py3-pip py3-ruamel.yaml py3-ruamel.yaml.clib
RUN pip install --break-system-packages --no-cache /app && borgmatic config generate && chmod +r /etc/borgmatic/config.yaml
RUN borgmatic --help > /command-line.txt \
&& for action in repo-create transfer create prune compact check delete extract config "config bootstrap" "config generate" "config validate" export-tar mount umount repo-delete restore repo-list list repo-info info break-lock "key export" "key change-passphrase" borg; do \
&& for action in repo-create transfer create prune compact check delete extract config "config bootstrap" "config generate" "config validate" export-tar mount umount repo-delete restore repo-list list repo-info info break-lock "key export" "key import" "key change-passphrase" recreate borg; do \
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
&& borgmatic $action --help >> /command-line.txt; done
RUN /app/docs/fetch-contributors >> /contributors.html

View file

@ -296,6 +296,20 @@ skip_actions:
- compact
```
### Disabling default actions
By default, running `borgmatic` without any arguments will perform the default
backup actions (create, prune, compact and check). If you want to disable this
behavior and require explicit actions to be specified, add the following to
your configuration:
```yaml
default_actions: false
```
With this setting, running `borgmatic` without arguments will show the help
message instead of performing any actions.
## Autopilot

View file

@ -148,9 +148,9 @@ feedback](https://torsion.org/borgmatic/#issues) you have on this feature.
#### Subvolume discovery
For any read-write subvolume you'd like backed up, add its path to borgmatic's
`source_directories` option. Btrfs does not support snapshotting read-only
subvolumes.
For any read-write subvolume you'd like backed up, add its mount point path to
borgmatic's `source_directories` option. Btrfs does not support snapshotting
read-only subvolumes.
<span class="minilink minilink-addedin">New in version 1.9.6</span> Or include
the mount point as a root pattern with borgmatic's `patterns` or `patterns_from`
@ -161,27 +161,27 @@ includes the snapshotted files in the paths sent to Borg. borgmatic is also
responsible for cleaning up (deleting) these snapshots after a backup completes.
borgmatic is smart enough to look at the parent (and grandparent, etc.)
directories of each of your `source_directories` to discover any subvolumes.
For instance, let's say you add `/var/log` and `/var/lib` to your source
directories, but `/var` is a subvolume. borgmatic will discover that and
snapshot `/var` accordingly. This also works even with nested subvolumes;
directories of each of your `source_directories` to discover any subvolumes. For
instance, let's say you add `/var/log` and `/var/lib` to your source
directories, but `/var` is a subvolume mount point. borgmatic will discover that
and snapshot `/var` accordingly. This also works even with nested subvolumes;
borgmatic selects the subvolume that's the "closest" parent to your source
directories.
<span class="minilink minilink-addedin">New in version 1.9.6</span> When using
[patterns](https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-help-patterns),
the initial portion of a pattern's path that you intend borgmatic to match
against a subvolume can't have globs or other non-literal characters in it—or it
won't actually match. For instance, a subvolume of `/var` would match a pattern
of `+ fm:/var/*/data`, but borgmatic isn't currently smart enough to match
`/var` to a pattern like `+ fm:/v*/lib/data`.
against a subvolume mount point can't have globs or other non-literal characters
in it—or it won't actually match. For instance, a subvolume mount point of
`/var` would match a pattern of `+ fm:/var/*/data`, but borgmatic isn't
currently smart enough to match `/var` to a pattern like `+ fm:/v*/lib/data`.
Additionally, borgmatic rewrites the snapshot file paths so that they appear
at their original subvolume locations in a Borg archive. For instance, if your
subvolume exists at `/var/subvolume`, then the snapshotted files will appear
Additionally, borgmatic rewrites the snapshot file paths so that they appear at
their original subvolume locations in a Borg archive. For instance, if your
subvolume is mounted at `/var/subvolume`, then the snapshotted files will appear
in an archive at `/var/subvolume` as well—even if borgmatic has to mount the
snapshot somewhere in `/var/subvolume/.borgmatic-snapshot-1234/` to perform
the backup.
snapshot somewhere in `/var/subvolume/.borgmatic-snapshot-1234/` to perform the
backup.
<span class="minilink minilink-addedin">With Borg version 1.2 and
earlier</span>Snapshotted files are instead stored at a path dependent on the

View file

@ -0,0 +1,39 @@
from flexmock import flexmock
from borgmatic.actions import recreate as module
def test_run_recreate_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.recreate).should_receive('recreate_archive')
recreate_arguments = flexmock(repository=flexmock(), archive=None)
module.run_recreate(
repository={'path': 'repo'},
config={},
local_borg_version=None,
recreate_arguments=recreate_arguments,
global_arguments=flexmock(),
local_path=None,
remote_path=None,
)
def test_run_recreate_with_archive_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.recreate).should_receive('recreate_archive')
recreate_arguments = flexmock(repository=flexmock(), archive='test-archive')
module.run_recreate(
repository={'path': 'repo'},
config={},
local_borg_version=None,
recreate_arguments=recreate_arguments,
global_arguments=flexmock(),
local_path=None,
remote_path=None,
)

View file

@ -0,0 +1,644 @@
import logging
import shlex
from flexmock import flexmock
from borgmatic.borg import recreate as module
from ..test_verbosity import insert_logging_mock
def insert_execute_command_mock(command, working_directory=None, borg_exit_codes=None):
flexmock(module.borgmatic.borg.environment).should_receive('make_environment')
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
full_command=command,
output_log_level=module.logging.INFO,
environment=None,
working_directory=working_directory,
borg_local_path=command[0],
borg_exit_codes=borg_exit_codes,
).once()
def mock_dependencies():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
def test_recreate_archive_dry_run_skips_execution():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
flexmock(module.borgmatic.execute).should_receive('execute_command').never()
recreate_arguments = flexmock(
repository=flexmock(),
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
)
result = module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=recreate_arguments,
global_arguments=flexmock(log_json=False, dry_run=True),
local_path='borg',
)
assert result is None
def test_recreate_calls_borg_with_required_flags():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
patterns=None,
)
def test_recreate_with_remote_path():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', '--remote-path', 'borg1', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path='borg1',
patterns=None,
)
def test_recreate_with_lock_wait():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', '--lock-wait', '5', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={'lock_wait': '5'},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_log_info():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', '--info', 'repo::archive'))
insert_logging_mock(logging.INFO)
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_log_debug():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', '--debug', '--show-rc', 'repo::archive'))
insert_logging_mock(logging.DEBUG)
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_log_json():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', '--log-json', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=True),
local_path='borg',
patterns=None,
)
def test_recreate_with_list_filter_flags():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
flexmock(module).should_receive('make_list_filter_flags').and_return('AME+-')
insert_execute_command_mock(
('borg', 'recreate', '--list', '--filter', 'AME+-', 'repo::archive')
)
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=True,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_patterns_from_flag():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
mock_patterns_file = flexmock(name='patterns_file')
flexmock(module).should_receive('write_patterns_file').and_return(mock_patterns_file)
insert_execute_command_mock(
('borg', 'recreate', '--patterns-from', 'patterns_file', 'repo::archive')
)
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=['pattern1', 'pattern2'],
)
def test_recreate_with_exclude_flags():
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
flexmock(module).should_receive('make_exclude_flags').and_return(('--exclude', 'pattern'))
insert_execute_command_mock(('borg', 'recreate', '--exclude', 'pattern', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={'exclude_patterns': ['pattern']},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_target_flag():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', '--target', 'new-archive', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target='new-archive',
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_comment_flag():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(
('borg', 'recreate', '--comment', shlex.quote('This is a test comment'), 'repo::archive')
)
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment='This is a test comment',
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_timestamp_flag():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(
('borg', 'recreate', '--timestamp', '2023-10-01T12:00:00', 'repo::archive')
)
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp='2023-10-01T12:00:00',
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_compression_flag():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', '--compression', 'lz4', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={'compression': 'lz4'},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_chunker_params_flag():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(
('borg', 'recreate', '--chunker-params', '19,23,21,4095', 'repo::archive')
)
module.recreate_archive(
repository='repo',
archive='archive',
config={'chunker_params': '19,23,21,4095'},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_recompress_flag():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', '--recompress', 'always', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={'recompress': 'always'},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives=None,
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_match_archives_star():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives='*',
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_match_archives_regex():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives='re:.*',
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_match_archives_shell():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives='sh:*',
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_glob_archives_flag():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(
('--glob-archives', 'foo-*')
)
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('repo::archive',))
insert_execute_command_mock(('borg', 'recreate', '--glob-archives', 'foo-*', 'repo::archive'))
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='1.2.3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives='foo-*',
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)
def test_recreate_with_match_archives_flag():
flexmock(module.borgmatic.borg.create).should_receive('make_exclude_flags').and_return(())
flexmock(module.borgmatic.borg.create).should_receive('write_patterns_file').and_return(None)
flexmock(module.borgmatic.borg.create).should_receive('make_list_filter_flags').and_return('')
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(
('--match-archives', 'sh:foo-*')
)
flexmock(module.borgmatic.borg.flags).should_receive(
'make_repository_archive_flags'
).and_return(('--repo', 'repo', 'archive'))
insert_execute_command_mock(
('borg', 'recreate', '--match-archives', 'sh:foo-*', '--repo', 'repo', 'archive')
)
module.recreate_archive(
repository='repo',
archive='archive',
config={},
local_borg_version='2.0.0b3',
recreate_arguments=flexmock(
list=None,
target=None,
comment=None,
timestamp=None,
match_archives='sh:foo-*',
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
patterns=None,
)

View file

@ -1039,6 +1039,47 @@ def test_run_actions_with_skip_actions_skips_create():
)
def test_run_actions_runs_recreate():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
flexmock(borgmatic.actions.recreate).should_receive('run_recreate').once()
tuple(
module.run_actions(
arguments={'global': flexmock(dry_run=False, log_file='foo'), 'recreate': flexmock()},
config_filename=flexmock(),
config={'repositories': []},
config_paths=[],
local_path=flexmock(),
remote_path=flexmock(),
local_borg_version=flexmock(),
repository={'path': 'repo'},
)
)
def test_run_actions_with_skip_actions_skips_recreate():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return(['recreate'])
flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
flexmock(borgmatic.actions.recreate).should_receive('run_recreate').never()
tuple(
module.run_actions(
arguments={'global': flexmock(dry_run=False, log_file='foo'), 'recreate': flexmock()},
config_filename=flexmock(),
config={'repositories': [], 'skip_actions': ['recreate']},
config_paths=[],
local_path=flexmock(),
remote_path=flexmock(),
local_borg_version=flexmock(),
repository={'path': 'repo'},
)
)
def test_run_actions_runs_prune():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
@ -2079,3 +2120,56 @@ def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
arguments=arguments,
)
)
def test_check_and_show_help_on_no_args_shows_help_when_no_args_and_default_actions_false():
flexmock(module.sys).should_receive('argv').and_return(['borgmatic'])
flexmock(module).should_receive('parse_arguments').with_args('--help').once()
flexmock(module.sys).should_receive('exit').with_args(0).once()
module.check_and_show_help_on_no_args({'test.yaml': {'default_actions': False}})
def test_check_and_show_help_on_no_args_does_not_show_help_when_no_args_and_default_actions_true():
flexmock(module.sys).should_receive('argv').and_return(['borgmatic'])
flexmock(module).should_receive('parse_arguments').never()
flexmock(module.sys).should_receive('exit').never()
module.check_and_show_help_on_no_args({'test.yaml': {'default_actions': True}})
def test_check_and_show_help_on_no_args_does_not_show_help_when_args_provided():
flexmock(module.sys).should_receive('argv').and_return(['borgmatic', '--create'])
flexmock(module).should_receive('parse_arguments').never()
flexmock(module.sys).should_receive('exit').never()
module.check_and_show_help_on_no_args({'test.yaml': {'default_actions': False}})
def test_check_and_show_help_on_no_args_with_no_default_actions_in_all_configs():
flexmock(module.sys).should_receive('argv').and_return(['borgmatic'])
# Both configs have default_actions set to False, so help should be shown
configs = {
'config1.yaml': {'default_actions': False},
'config2.yaml': {'default_actions': False},
}
# Expect help to be shown
flexmock(module).should_receive('parse_arguments').with_args('--help').once()
flexmock(module.sys).should_receive('exit').with_args(0).once()
module.check_and_show_help_on_no_args(configs)
def test_check_and_show_help_on_no_args_with_conflicting_configs():
flexmock(module.sys).should_receive('argv').and_return(['borgmatic'])
# Simulate two config files with conflicting 'default_actions' values
configs = {
'config1.yaml': {'default_actions': True},
'config2.yaml': {'default_actions': False},
}
# Expect help not to be shown because at least one config enables default actions
flexmock(module).should_receive('parse_arguments').never()
flexmock(module.sys).should_receive('exit').never()
module.check_and_show_help_on_no_args(configs)

View file

@ -26,6 +26,9 @@ def test_load_credential_reads_named_credential_from_file():
credential_stream = io.StringIO('password')
credential_stream.name = '/credentials/mycredential'
builtins = flexmock(sys.modules['builtins'])
flexmock(module.os.path).should_receive('expanduser').with_args(
'/credentials/mycredential'
).and_return('/credentials/mycredential')
builtins.should_receive('open').with_args('/credentials/mycredential').and_return(
credential_stream
)
@ -42,6 +45,9 @@ def test_load_credential_reads_named_credential_from_file_using_working_director
credential_stream = io.StringIO('password')
credential_stream.name = '/working/credentials/mycredential'
builtins = flexmock(sys.modules['builtins'])
flexmock(module.os.path).should_receive('expanduser').with_args(
'credentials/mycredential'
).and_return('credentials/mycredential')
builtins.should_receive('open').with_args('/working/credentials/mycredential').and_return(
credential_stream
)
@ -58,6 +64,9 @@ def test_load_credential_reads_named_credential_from_file_using_working_director
def test_load_credential_with_file_not_found_error_raises():
builtins = flexmock(sys.modules['builtins'])
flexmock(module.os.path).should_receive('expanduser').with_args(
'/credentials/mycredential'
).and_return('/credentials/mycredential')
builtins.should_receive('open').with_args('/credentials/mycredential').and_raise(
FileNotFoundError
)
@ -66,3 +75,22 @@ def test_load_credential_with_file_not_found_error_raises():
module.load_credential(
hook_config={}, config={}, credential_parameters=('/credentials/mycredential',)
)
def test_load_credential_reads_named_credential_from_expanded_directory():
credential_stream = io.StringIO('password')
credential_stream.name = '/root/credentials/mycredential'
builtins = flexmock(sys.modules['builtins'])
flexmock(module.os.path).should_receive('expanduser').with_args(
'~/credentials/mycredential'
).and_return('/root/credentials/mycredential')
builtins.should_receive('open').with_args('/root/credentials/mycredential').and_return(
credential_stream
)
assert (
module.load_credential(
hook_config={}, config={}, credential_parameters=('~/credentials/mycredential',)
)
== 'password'
)

View file

@ -15,6 +15,9 @@ def test_load_credential_with_invalid_credential_parameters_raises(credential_pa
def test_load_credential_with_missing_database_raises():
flexmock(module.os.path).should_receive('expanduser').with_args('database.kdbx').and_return(
'database.kdbx'
)
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.borgmatic.execute).should_receive('execute_command_and_capture_output').never()
@ -25,6 +28,9 @@ def test_load_credential_with_missing_database_raises():
def test_load_credential_with_present_database_fetches_password_from_keepassxc():
flexmock(module.os.path).should_receive('expanduser').with_args('database.kdbx').and_return(
'database.kdbx'
)
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
@ -51,6 +57,9 @@ def test_load_credential_with_present_database_fetches_password_from_keepassxc()
def test_load_credential_with_custom_keepassxc_cli_command_calls_it():
flexmock(module.os.path).should_receive('expanduser').with_args('database.kdbx').and_return(
'database.kdbx'
)
config = {'keepassxc': {'keepassxc_cli_command': '/usr/local/bin/keepassxc-cli --some-option'}}
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.borgmatic.execute).should_receive(
@ -78,3 +87,32 @@ def test_load_credential_with_custom_keepassxc_cli_command_calls_it():
)
== 'password'
)
def test_load_credential_with_expanded_directory_with_present_database_fetches_password_from_keepassxc():
flexmock(module.os.path).should_receive('expanduser').with_args('~/database.kdbx').and_return(
'/root/database.kdbx'
)
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output'
).with_args(
(
'keepassxc-cli',
'show',
'--show-protected',
'--attributes',
'Password',
'/root/database.kdbx',
'mypassword',
)
).and_return(
'password'
).once()
assert (
module.load_credential(
hook_config={}, config={}, credential_parameters=('~/database.kdbx', 'mypassword')
)
== 'password'
)

View file

@ -42,12 +42,12 @@ def test_load_credential_reads_named_credential_from_file():
'/var'
)
credential_stream = io.StringIO('password')
credential_stream.name = '/var/mycredential'
credential_stream.name = '/var/borgmatic.pw'
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('/var/mycredential').and_return(credential_stream)
builtins.should_receive('open').with_args('/var/borgmatic.pw').and_return(credential_stream)
assert (
module.load_credential(hook_config={}, config={}, credential_parameters=('mycredential',))
module.load_credential(hook_config={}, config={}, credential_parameters=('borgmatic.pw',))
== 'password'
)

View file

@ -681,3 +681,135 @@ def test_restore_data_source_dump_without_extract_process_restores_from_disk():
},
borgmatic_runtime_directory='/run/borgmatic',
)
def test_dump_data_sources_uses_custom_mongodump_command():
flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
flexmock()
)
databases = [{'name': 'foo', 'mongodump_command': 'custom_mongodump'}]
process = flexmock()
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
'databases/localhost/foo'
)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
(
'custom_mongodump',
'--db',
'foo',
'--archive',
'>',
'databases/localhost/foo',
),
shell=True,
run_to_completion=False,
).and_return(process).once()
assert module.dump_data_sources(
databases,
{},
config_paths=('test.yaml',),
borgmatic_runtime_directory='/run/borgmatic',
patterns=[],
dry_run=False,
) == [process]
def test_build_dump_command_prevents_shell_injection():
database = {
'name': 'testdb; rm -rf /', # Malicious input
'hostname': 'localhost',
'port': 27017,
'username': 'user',
'password': 'password',
'mongodump_command': 'mongodump',
'options': '--gzip',
}
config = {}
dump_filename = '/path/to/dump'
dump_format = 'archive'
command = module.build_dump_command(database, config, dump_filename, dump_format)
# Ensure the malicious input is properly escaped and does not execute
assert 'testdb; rm -rf /' not in command
assert any(
'testdb' in part for part in command
) # Check if 'testdb' is in any part of the tuple
def test_restore_data_source_dump_uses_custom_mongorestore_command():
hook_config = [
{
'name': 'foo',
'mongorestore_command': 'custom_mongorestore',
'schemas': None,
'restore_options': '--gzip',
}
]
extract_process = flexmock(stdout=flexmock())
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_data_source_dump_filename')
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
'resolve_credential'
).replace_with(lambda value, config: value)
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'custom_mongorestore', # Should use custom command instead of default
'--archive',
'--drop',
'--gzip', # Should include restore options
],
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
).once()
module.restore_data_source_dump(
hook_config,
{},
data_source=hook_config[0],
dry_run=False,
extract_process=extract_process,
connection_params={
'hostname': None,
'port': None,
'username': None,
'password': None,
},
borgmatic_runtime_directory='/run/borgmatic',
)
def test_build_restore_command_prevents_shell_injection():
database = {
'name': 'testdb; rm -rf /', # Malicious input
'restore_hostname': 'localhost',
'restore_port': 27017,
'restore_username': 'user',
'restore_password': 'password',
'mongorestore_command': 'mongorestore',
'restore_options': '--gzip',
}
config = {}
dump_filename = '/path/to/dump'
connection_params = {
'hostname': None,
'port': None,
'username': None,
'password': None,
}
extract_process = None
command = module.build_restore_command(
extract_process, database, config, dump_filename, connection_params
)
# print(command)
# Ensure the malicious input is properly escaped and does not execute
assert 'rm -rf /' not in command
assert ';' not in command