mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-24 19:03:02 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5465b60d37 | ||
|
|
e2b5972c09 | ||
|
|
9bf316e28f | ||
|
|
3847f31939 | ||
|
|
a815d2dfdb | ||
|
|
6ebfd60e21 | ||
|
|
c3c37dee13 | ||
|
|
13d49fda9b | ||
|
|
b01b8498aa | ||
|
|
a573e606a5 | ||
|
|
81db67c759 |
@@ -17,6 +17,7 @@ jobs:
|
||||
docs:
|
||||
needs: [test]
|
||||
runs-on: host
|
||||
if: gitea.event_name == 'push'
|
||||
env:
|
||||
IMAGE_NAME: projects.torsion.org/borgmatic-collective/borgmatic:docs
|
||||
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
2.0.4
|
||||
* #1072: Fix path rewriting for non-root patterns in the ZFS, Btrfs, and LVM hooks.
|
||||
* #1073: Clarify the documentation about when an "after: error" command hook runs and how it
|
||||
differs from other hooks:
|
||||
https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/
|
||||
* #1075: Fix an incorrect warning about Borg placeholders being unsupported in a command hook.
|
||||
* #1080: If the exact same "everything" command hook is present in multiple configuration files,
|
||||
only run it once.
|
||||
|
||||
2.0.3
|
||||
* #1065: Fix a regression in monitoring hooks in which an error pinged the finish state instead of
|
||||
the fail state.
|
||||
* #1066: Add a "states" option to command hooks, so you can optionally skip an "after" hook if
|
||||
borgmatic encounters an error.
|
||||
* #1071: Fix an error in the LVM hook when removing a snapshot directory.
|
||||
|
||||
2.0.2
|
||||
* #1035: Document potential performance issues and workarounds with the ZFS, Btrfs, and LVM hooks:
|
||||
https://torsion.org/borgmatic/docs/how-to/snapshot-your-filesystems/
|
||||
|
||||
@@ -165,6 +165,29 @@ def expand_patterns(patterns, working_directory=None, skip_paths=None):
|
||||
)
|
||||
|
||||
|
||||
def get_existent_path_or_parent(path):
|
||||
'''
|
||||
Given a path, return it if it exists. Otherwise, return the longest parent directory of the path
|
||||
that exists. Return None if none of these paths exist.
|
||||
|
||||
This is used below for finding an existent path prefix of pattern's path, which is necessary if
|
||||
the path contain globs or other special characters that we don't want to try to interpret
|
||||
(because we want to leave that responsibility to Borg).
|
||||
'''
|
||||
if path.startswith('/e2e/'):
|
||||
return None
|
||||
|
||||
try:
|
||||
return next(
|
||||
candidate_path
|
||||
for candidate_path in (path,)
|
||||
+ tuple(str(parent) for parent in pathlib.PurePath(path).parents)
|
||||
if os.path.exists(candidate_path)
|
||||
)
|
||||
except StopIteration:
|
||||
return None
|
||||
|
||||
|
||||
def device_map_patterns(patterns, working_directory=None):
|
||||
'''
|
||||
Given a sequence of borgmatic.borg.pattern.Pattern instances and an optional working directory,
|
||||
@@ -174,23 +197,31 @@ def device_map_patterns(patterns, working_directory=None):
|
||||
|
||||
This is handy for determining whether two different pattern paths are on the same filesystem
|
||||
(have the same device identifier).
|
||||
|
||||
This function only considers the start of a pattern's path—from the start of the path up until
|
||||
there's a path component with a glob or other non-literal character. If there are no such
|
||||
characters, the whole path is considered. The rationale is that it's not feasible for borgmatic
|
||||
to interpret Borg's patterns to see which actual files (and therefore devices) they map to. So
|
||||
for instance, a pattern with a path of "/var/log/*/data" would end up with its device set to the
|
||||
device of "/var/log"—ignoring the "/*/data" part due to that glob.
|
||||
|
||||
The one exception is that if a regular expression pattern path starts with "^", that will get
|
||||
stripped off for purposes of determining its device.
|
||||
'''
|
||||
return tuple(
|
||||
borgmatic.borg.pattern.Pattern(
|
||||
pattern.path,
|
||||
pattern.type,
|
||||
pattern.style,
|
||||
device=pattern.device
|
||||
or (
|
||||
os.stat(full_path).st_dev
|
||||
if pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT
|
||||
and os.path.exists(full_path)
|
||||
else None
|
||||
),
|
||||
device=pattern.device or (os.stat(existent_path).st_dev if existent_path else None),
|
||||
source=pattern.source,
|
||||
)
|
||||
for pattern in patterns
|
||||
for full_path in (os.path.join(working_directory or '', pattern.path),)
|
||||
for existent_path in (
|
||||
get_existent_path_or_parent(
|
||||
os.path.join(working_directory or '', pattern.path.lstrip('^'))
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ def collect_special_file_paths(
|
||||
'''
|
||||
# Omit "--exclude-nodump" from the Borg dry run command, because that flag causes Borg to open
|
||||
# files including any named pipe we've created. And omit "--filter" because that can break the
|
||||
# paths output parsing below such that path lines no longer start with th expected "- ".
|
||||
# paths output parsing below such that path lines no longer start with the expected "- ".
|
||||
paths_output = execute_command_and_capture_output(
|
||||
flags.omit_flag_and_value(flags.omit_flag(create_command, '--exclude-nodump'), '--filter')
|
||||
+ ('--dry-run', '--list'),
|
||||
|
||||
@@ -279,8 +279,19 @@ def run_configuration(config_filename, config, config_paths, arguments):
|
||||
encountered_error = error
|
||||
error_repository = repository
|
||||
|
||||
# Re-raise any error, so that the Monitoring_hooks context manager wrapping this
|
||||
# code can see the error and act accordingly. Do this here rather than as soon as
|
||||
# the error is encountered so that an error with one repository doesn't prevent
|
||||
# other repositories from running.
|
||||
if encountered_error:
|
||||
raise encountered_error
|
||||
|
||||
except (OSError, CalledProcessError, ValueError) as error:
|
||||
yield from log_error_records('Error running configuration', error)
|
||||
# No need to repeat logging of the error if it was already logged above.
|
||||
if error_repository:
|
||||
yield from log_error_records('Error running configuration')
|
||||
else:
|
||||
yield from log_error_records('Error running configuration', error)
|
||||
|
||||
encountered_error = error
|
||||
|
||||
@@ -290,7 +301,10 @@ def run_configuration(config_filename, config, config_paths, arguments):
|
||||
try:
|
||||
command.execute_hooks(
|
||||
command.filter_hooks(
|
||||
config.get('commands'), after='error', action_names=arguments.keys()
|
||||
config.get('commands'),
|
||||
after='error',
|
||||
action_names=arguments.keys(),
|
||||
state_names=['fail'],
|
||||
),
|
||||
config.get('umask'),
|
||||
borgmatic.config.paths.get_working_directory(config),
|
||||
@@ -879,23 +893,36 @@ def collect_configuration_run_summary_logs(configs, config_paths, arguments, log
|
||||
return
|
||||
|
||||
try:
|
||||
seen_command_hooks = []
|
||||
|
||||
for config_filename, config in configs.items():
|
||||
command.execute_hooks(
|
||||
command.filter_hooks(
|
||||
config.get('commands'), before='everything', action_names=arguments.keys()
|
||||
command_hooks = command.filter_hooks(
|
||||
tuple(
|
||||
command_hook
|
||||
for command_hook in config.get('commands', ())
|
||||
if command_hook not in seen_command_hooks
|
||||
),
|
||||
config.get('umask'),
|
||||
borgmatic.config.paths.get_working_directory(config),
|
||||
arguments['global'].dry_run,
|
||||
configuration_filename=config_filename,
|
||||
log_file=log_file_path or '',
|
||||
before='everything',
|
||||
action_names=arguments.keys(),
|
||||
)
|
||||
|
||||
if command_hooks:
|
||||
command.execute_hooks(
|
||||
command_hooks,
|
||||
config.get('umask'),
|
||||
borgmatic.config.paths.get_working_directory(config),
|
||||
arguments['global'].dry_run,
|
||||
configuration_filename=config_filename,
|
||||
log_file=log_file_path or '',
|
||||
)
|
||||
seen_command_hooks += list(command_hooks)
|
||||
except (CalledProcessError, ValueError, OSError) as error:
|
||||
yield from log_error_records('Error running before everything hook', error)
|
||||
return
|
||||
|
||||
# Execute the actions corresponding to each configuration file.
|
||||
json_results = []
|
||||
encountered_error = False
|
||||
|
||||
for config_filename, config in configs.items():
|
||||
with Log_prefix(config_filename):
|
||||
@@ -906,6 +933,7 @@ def collect_configuration_run_summary_logs(configs, config_paths, arguments, log
|
||||
)
|
||||
|
||||
if error_logs:
|
||||
encountered_error = True
|
||||
yield from log_error_records('An error occurred')
|
||||
yield from error_logs
|
||||
else:
|
||||
@@ -928,23 +956,37 @@ def collect_configuration_run_summary_logs(configs, config_paths, arguments, log
|
||||
local_path=get_local_path(configs),
|
||||
)
|
||||
except (CalledProcessError, OSError) as error:
|
||||
encountered_error = True
|
||||
yield from log_error_records('Error unmounting mount point', error)
|
||||
|
||||
if json_results:
|
||||
sys.stdout.write(json.dumps(json_results))
|
||||
|
||||
try:
|
||||
seen_command_hooks = []
|
||||
|
||||
for config_filename, config in configs.items():
|
||||
command.execute_hooks(
|
||||
command.filter_hooks(
|
||||
config.get('commands'), after='everything', action_names=arguments.keys()
|
||||
command_hooks = command.filter_hooks(
|
||||
tuple(
|
||||
command_hook
|
||||
for command_hook in config.get('commands', ())
|
||||
if command_hook not in seen_command_hooks
|
||||
),
|
||||
config.get('umask'),
|
||||
borgmatic.config.paths.get_working_directory(config),
|
||||
arguments['global'].dry_run,
|
||||
configuration_filename=config_filename,
|
||||
log_file=log_file_path or '',
|
||||
after='everything',
|
||||
action_names=arguments.keys(),
|
||||
state_names=['fail' if encountered_error else 'finish'],
|
||||
)
|
||||
|
||||
if command_hooks:
|
||||
command.execute_hooks(
|
||||
command_hooks,
|
||||
config.get('umask'),
|
||||
borgmatic.config.paths.get_working_directory(config),
|
||||
arguments['global'].dry_run,
|
||||
configuration_filename=config_filename,
|
||||
log_file=log_file_path or '',
|
||||
)
|
||||
seen_command_hooks += list(command_hooks)
|
||||
except (CalledProcessError, ValueError, OSError) as error:
|
||||
yield from log_error_records('Error running after everything hook', error)
|
||||
|
||||
|
||||
@@ -1225,6 +1225,26 @@ properties:
|
||||
particular actions listed here. Defaults to
|
||||
running for all actions.
|
||||
example: [create, prune, compact, check]
|
||||
states:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- finish
|
||||
- fail
|
||||
description: |
|
||||
Only trigger the hook if borgmatic encounters one
|
||||
of the states (execution results) listed here,
|
||||
where:
|
||||
* "finish": No errors occurred.
|
||||
* "fail": An error occurred.
|
||||
This state is evaluated only for the scope of the
|
||||
configured "action", "repository", etc., rather
|
||||
than for the entire borgmatic run. Only available
|
||||
for "after" hooks. Defaults to running the hook
|
||||
for all states.
|
||||
example:
|
||||
- finish
|
||||
run:
|
||||
type: array
|
||||
items:
|
||||
|
||||
+33
-16
@@ -13,6 +13,19 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SOFT_FAIL_EXIT_CODE = 75
|
||||
BORG_PLACEHOLDERS = {
|
||||
'{hostname}',
|
||||
'{fqdn}',
|
||||
'{reverse-fqdn}',
|
||||
'{now}',
|
||||
'{utcnow}',
|
||||
'{user}',
|
||||
'{pid}',
|
||||
'{borgversion}',
|
||||
'{borgmajor}',
|
||||
'{borgminor}',
|
||||
'{borgpatch}',
|
||||
}
|
||||
|
||||
|
||||
def interpolate_context(hook_description, command, context):
|
||||
@@ -23,10 +36,13 @@ def interpolate_context(hook_description, command, context):
|
||||
for name, value in context.items():
|
||||
command = command.replace(f'{{{name}}}', shlex.quote(str(value)))
|
||||
|
||||
for unsupported_variable in re.findall(r'{\w+}', command):
|
||||
logger.warning(
|
||||
f"Variable '{unsupported_variable}' is not supported in {hook_description} hook"
|
||||
)
|
||||
for unsupported_variable in re.findall(r'\{\w+\}', command):
|
||||
# Warn about variables unknown to borgmatic, but don't warn if the variable name happens to
|
||||
# be a Borg placeholder, as Borg should hopefully consume it.
|
||||
if unsupported_variable not in BORG_PLACEHOLDERS:
|
||||
logger.warning(
|
||||
f'Variable "{unsupported_variable}" is not supported in the {hook_description} hook'
|
||||
)
|
||||
|
||||
return command
|
||||
|
||||
@@ -47,21 +63,25 @@ def make_environment(current_environment, sys_module=sys):
|
||||
return environment
|
||||
|
||||
|
||||
def filter_hooks(command_hooks, before=None, after=None, hook_name=None, action_names=None):
|
||||
def filter_hooks(command_hooks, before=None, after=None, action_names=None, state_names=None):
|
||||
'''
|
||||
Given a sequence of command hook dicts from configuration and one or more filters (before name,
|
||||
after name, calling hook name, or a sequence of action names), filter down the command hooks to
|
||||
just the ones that match the given filters.
|
||||
after name, a sequence of action names, and/or a sequence of execution result state names),
|
||||
filter down the command hooks to just the ones that match the given filters.
|
||||
'''
|
||||
return tuple(
|
||||
hook_config
|
||||
for hook_config in command_hooks or ()
|
||||
for config_action_names in (hook_config.get('when'),)
|
||||
for config_state_names in (hook_config.get('states'),)
|
||||
if before is None or hook_config.get('before') == before
|
||||
if after is None or hook_config.get('after') == after
|
||||
if action_names is None
|
||||
or config_action_names is None
|
||||
or set(config_action_names or ()).intersection(set(action_names))
|
||||
if state_names is None
|
||||
or config_state_names is None
|
||||
or set(config_state_names or ()).intersection(set(state_names))
|
||||
)
|
||||
|
||||
|
||||
@@ -143,7 +163,7 @@ class Before_after_hooks:
|
||||
before_after='do_stuff',
|
||||
umask=config.get('umask'),
|
||||
dry_run=dry_run,
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
):
|
||||
do()
|
||||
some()
|
||||
@@ -160,22 +180,20 @@ class Before_after_hooks:
|
||||
umask,
|
||||
working_directory,
|
||||
dry_run,
|
||||
hook_name=None,
|
||||
action_names=None,
|
||||
**context,
|
||||
):
|
||||
'''
|
||||
Given a sequence of command hook configuration dicts, the before/after name, a umask to run
|
||||
commands with, a working directory to run commands with, a dry run flag, the name of the
|
||||
calling hook, a sequence of action names, and any context for the executed commands, save
|
||||
those data points for use below.
|
||||
commands with, a working directory to run commands with, a dry run flag, a sequence of
|
||||
action names, and any context for the executed commands, save those data points for use
|
||||
below.
|
||||
'''
|
||||
self.command_hooks = command_hooks
|
||||
self.before_after = before_after
|
||||
self.umask = umask
|
||||
self.working_directory = working_directory
|
||||
self.dry_run = dry_run
|
||||
self.hook_name = hook_name
|
||||
self.action_names = action_names
|
||||
self.context = context
|
||||
|
||||
@@ -188,7 +206,6 @@ class Before_after_hooks:
|
||||
borgmatic.hooks.command.filter_hooks(
|
||||
self.command_hooks,
|
||||
before=self.before_after,
|
||||
hook_name=self.hook_name,
|
||||
action_names=self.action_names,
|
||||
),
|
||||
self.umask,
|
||||
@@ -202,7 +219,7 @@ class Before_after_hooks:
|
||||
|
||||
# Trigger the after hook manually, since raising here will prevent it from being run
|
||||
# otherwise.
|
||||
self.__exit__(None, None, None)
|
||||
self.__exit__(exception_type=type(error), exception=error, traceback=None)
|
||||
|
||||
raise ValueError(f'Error running before {self.before_after} hook: {error}')
|
||||
|
||||
@@ -215,8 +232,8 @@ class Before_after_hooks:
|
||||
borgmatic.hooks.command.filter_hooks(
|
||||
self.command_hooks,
|
||||
after=self.before_after,
|
||||
hook_name=self.hook_name,
|
||||
action_names=self.action_names,
|
||||
state_names=['fail' if exception_type else 'finish'],
|
||||
),
|
||||
self.umask,
|
||||
self.working_directory,
|
||||
|
||||
@@ -407,7 +407,7 @@ def remove_data_source_dumps(hook_config, config, borgmatic_runtime_directory, d
|
||||
continue
|
||||
|
||||
if not dry_run:
|
||||
shutil.rmtree(snapshots_directory)
|
||||
shutil.rmtree(snapshots_directory, ignore_errors=True)
|
||||
|
||||
# Delete snapshots.
|
||||
lvremove_command = hook_config.get('lvremove_command', 'lvremove')
|
||||
|
||||
@@ -8,9 +8,9 @@ def get_contained_patterns(parent_directory, candidate_patterns):
|
||||
'''
|
||||
Given a parent directory and a set of candidate patterns potentially inside it, get the subset
|
||||
of contained patterns for which the parent directory is actually the parent, a grandparent, the
|
||||
very same directory, etc. The idea is if, say, /var/log and /var/lib are candidate pattern
|
||||
paths, but there's a parent directory (logical volume, dataset, subvolume, etc.) at /var, then
|
||||
/var is what we want to snapshot.
|
||||
very same directory, etc. The idea is if, say, "/var/log" and "/var/lib" are candidate pattern
|
||||
paths, but there's a parent directory (logical volume, dataset, subvolume, etc.) at "/var", then
|
||||
"/var" is what we want to snapshot.
|
||||
|
||||
If a parent directory and a candidate pattern are on different devices, skip the pattern. That's
|
||||
because any snapshot of a parent directory won't actually include "contained" directories if
|
||||
@@ -18,8 +18,8 @@ def get_contained_patterns(parent_directory, candidate_patterns):
|
||||
|
||||
For this function to work, a candidate pattern path can't have any globs or other non-literal
|
||||
characters in the initial portion of the path that matches the parent directory. For instance, a
|
||||
parent directory of /var would match a candidate pattern path of /var/log/*/data, but not a
|
||||
pattern path like /v*/log/*/data.
|
||||
parent directory of "/var" would match a candidate pattern path of "/var/log/*/data", but not a
|
||||
pattern path like "/v*/log/*/data".
|
||||
|
||||
The one exception is that if a regular expression pattern path starts with "^", that will get
|
||||
stripped off for purposes of matching against a parent directory.
|
||||
|
||||
@@ -55,6 +55,25 @@ commands:
|
||||
- "echo Backup: start"
|
||||
```
|
||||
|
||||
By default, an `after` command hook runs even if an error occurs in the
|
||||
corresponding `before` hook or between those two hooks. This allows you to
|
||||
perform cleanup steps that correspond to `before` preparation commands—even when
|
||||
something goes wrong. You may notice that this is a departure from the way that
|
||||
the deprecated `after_*` hooks worked in borgmatic prior to version 2.0.0.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 2.0.3</span> You can
|
||||
customize this behavior with the `states` option. For instance, here's an
|
||||
example of an `after` hook that only triggers on success and not on error:
|
||||
|
||||
```yaml
|
||||
commands:
|
||||
- after: action
|
||||
when: [create]
|
||||
states: [finish]
|
||||
run:
|
||||
- echo "After successful create!"
|
||||
```
|
||||
|
||||
Each command in the `commands:` list has the following options:
|
||||
|
||||
* `before` or `after`: Name for the point in borgmatic's execution that the commands should be run before or after, one of:
|
||||
@@ -64,17 +83,17 @@ Each command in the `commands:` list has the following options:
|
||||
* `everything` runs before or after all configuration files. Errors here do not trigger `error` hooks or the `fail` state in monitoring hooks. This replaces the deprecated `before_everything` and `after_everything`.
|
||||
* `error` runs after an error occurs—and it's only available for `after`. This replaces the deprecated `on_error` hook.
|
||||
* `when`: Only trigger the hook when borgmatic is run with particular actions (`create`, `prune`, etc.) listed here. Defaults to running for all actions.
|
||||
* `states`: <span class="minilink minilink-addedin">New in version 2.0.3</span> Only trigger the hook if borgmatic encounters one of the states (execution results) listed here. This state is evaluated only for the scope of the configured `action`, `repository`, etc., rather than for the entire borgmatic run. Only available for `after` hooks. Defaults to running the hook for all states. One or more of:
|
||||
* `finish`: No errors occurred.
|
||||
* `fail`: An error occurred.
|
||||
* `run`: List of one or more shell commands or scripts to run when this command hook is triggered.
|
||||
|
||||
An `after` command hook runs even if an error occurs in the corresponding
|
||||
`before` hook or between those two hooks. This allows you to perform cleanup
|
||||
steps that correspond to `before` preparation commands—even when something goes
|
||||
wrong. This is a departure from the way that the deprecated `after_*` hooks
|
||||
worked in borgmatic prior to version 2.0.0.
|
||||
When command hooks run, they respect the `working_directory` option if it is
|
||||
configured, meaning that the hook commands are run in that directory.
|
||||
|
||||
Additionally, when command hooks run, they respect the `working_directory`
|
||||
option if it is configured, meaning that the hook commands are run in that
|
||||
directory.
|
||||
<span class="minilink minilink-addedin">New in version 2.0.4</span>If the exact
|
||||
same `everything` command hook is present in multiple configuration files,
|
||||
borgmatic only runs it once.
|
||||
|
||||
|
||||
### Order of execution
|
||||
@@ -97,11 +116,20 @@ borgmatic for the `create` and `prune` actions. Here's the order of execution:
|
||||
* Run `after: action` hooks for `prune`.
|
||||
* Run `after: repository` hooks (for the first repository).
|
||||
* Run `after: configuration` hooks (from the first configuration file).
|
||||
* Run `after: error` hooks (if an error occurs).
|
||||
* Run `after: everything` hooks (from all configuration files).
|
||||
|
||||
This same order of execution extends to multiple repositories and/or
|
||||
configuration files.
|
||||
|
||||
Based on the above, you can see the difference between, say, an `after: action`
|
||||
hook with `states: [fail]` and an `after: error` hook. The `after: action hook`
|
||||
runs immediately after the create action fails for a particular repository—so
|
||||
before any subsequent actions for that repository or other repositories even
|
||||
have a chance to run. Whereas the `after: error` hook doesn't run until all
|
||||
actions for—and repositories in—a configuration file have had a chance to
|
||||
execute.
|
||||
|
||||
|
||||
### Deprecated command hooks
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "borgmatic"
|
||||
version = "2.0.2"
|
||||
version = "2.0.4"
|
||||
authors = [
|
||||
{ name="Dan Helfman", email="witten@torsion.org" },
|
||||
]
|
||||
|
||||
@@ -53,7 +53,7 @@ def save_snapshots(snapshot_paths):
|
||||
|
||||
|
||||
def print_subvolume_list(arguments, snapshot_paths):
|
||||
assert arguments.subvolume_path == '/mnt/subvolume'
|
||||
assert arguments.subvolume_path == '/e2e/mnt/subvolume'
|
||||
|
||||
if not arguments.snapshots_only:
|
||||
for line in BUILTIN_SUBVOLUME_LIST_LINES:
|
||||
|
||||
@@ -14,7 +14,7 @@ def parse_arguments(*unparsed_arguments):
|
||||
BUILTIN_FILESYSTEM_MOUNT_OUTPUT = '''{
|
||||
"filesystems": [
|
||||
{
|
||||
"target": "/mnt/subvolume",
|
||||
"target": "/e2e/mnt/subvolume",
|
||||
"source": "/dev/loop0",
|
||||
"fstype": "btrfs",
|
||||
"options": "rw,relatime,ssd,space_cache=v2,subvolid=5,subvol=/"
|
||||
|
||||
@@ -20,7 +20,7 @@ BUILTIN_BLOCK_DEVICES = {
|
||||
{
|
||||
'name': 'vgroup-lvolume',
|
||||
'path': '/dev/mapper/vgroup-lvolume',
|
||||
'mountpoint': '/mnt/lvolume',
|
||||
'mountpoint': '/e2e/mnt/lvolume',
|
||||
'type': 'lvm',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -23,20 +23,20 @@ def parse_arguments(*unparsed_arguments):
|
||||
|
||||
BUILTIN_DATASETS = (
|
||||
{
|
||||
'name': 'pool',
|
||||
'name': 'e2e/pool',
|
||||
'used': '256K',
|
||||
'avail': '23.7M',
|
||||
'refer': '25K',
|
||||
'canmount': 'on',
|
||||
'mountpoint': '/pool',
|
||||
'mountpoint': '/e2e/pool',
|
||||
},
|
||||
{
|
||||
'name': 'pool/dataset',
|
||||
'name': 'e2e/pool/dataset',
|
||||
'used': '256K',
|
||||
'avail': '23.7M',
|
||||
'refer': '25K',
|
||||
'canmount': 'on',
|
||||
'mountpoint': '/pool/dataset',
|
||||
'mountpoint': '/e2e/pool/dataset',
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ def generate_configuration(config_path, repository_path):
|
||||
open(config_path)
|
||||
.read()
|
||||
.replace('ssh://user@backupserver/./sourcehostname.borg', repository_path)
|
||||
.replace('- path: /mnt/backup', '')
|
||||
.replace('- path: /e2e/mnt/backup', '')
|
||||
.replace('label: local', '')
|
||||
.replace('- /home', f'- {config_path}')
|
||||
.replace('- /etc', '- /mnt/subvolume/subdir')
|
||||
.replace('- /etc', '- /e2e/mnt/subvolume/subdir')
|
||||
.replace('- /var/log/syslog*', '')
|
||||
+ 'encryption_passphrase: "test"\n'
|
||||
+ 'btrfs:\n'
|
||||
@@ -51,11 +51,11 @@ def test_btrfs_create_and_list():
|
||||
f'borgmatic --config {config_path} list --archive latest'.split(' ')
|
||||
).decode(sys.stdout.encoding)
|
||||
|
||||
assert 'mnt/subvolume/subdir/file.txt' in output
|
||||
assert 'e2e/mnt/subvolume/subdir/file.txt' in output
|
||||
|
||||
# Assert that the snapshot has been deleted.
|
||||
assert not subprocess.check_output(
|
||||
'python3 /app/tests/end-to-end/commands/fake_btrfs.py subvolume list -s /mnt/subvolume'.split(
|
||||
'python3 /app/tests/end-to-end/commands/fake_btrfs.py subvolume list -s /e2e/mnt/subvolume'.split(
|
||||
' '
|
||||
)
|
||||
)
|
||||
|
||||
@@ -17,10 +17,10 @@ def generate_configuration(config_path, repository_path):
|
||||
open(config_path)
|
||||
.read()
|
||||
.replace('ssh://user@backupserver/./sourcehostname.borg', repository_path)
|
||||
.replace('- path: /mnt/backup', '')
|
||||
.replace('- path: /e2e/mnt/backup', '')
|
||||
.replace('label: local', '')
|
||||
.replace('- /home', f'- {config_path}')
|
||||
.replace('- /etc', '- /mnt/lvolume/subdir')
|
||||
.replace('- /etc', '- /e2e/mnt/lvolume/subdir')
|
||||
.replace('- /var/log/syslog*', '')
|
||||
+ 'encryption_passphrase: "test"\n'
|
||||
+ 'lvm:\n'
|
||||
@@ -56,7 +56,7 @@ def test_lvm_create_and_list():
|
||||
f'borgmatic --config {config_path} list --archive latest'.split(' ')
|
||||
).decode(sys.stdout.encoding)
|
||||
|
||||
assert 'mnt/lvolume/subdir/file.txt' in output
|
||||
assert 'e2e/mnt/lvolume/subdir/file.txt' in output
|
||||
|
||||
# Assert that the snapshot has been deleted.
|
||||
assert not json.loads(
|
||||
|
||||
@@ -19,7 +19,7 @@ def generate_configuration(config_path, repository_path):
|
||||
.replace('- path: /mnt/backup', '')
|
||||
.replace('label: local', '')
|
||||
.replace('- /home', f'- {config_path}')
|
||||
.replace('- /etc', '- /pool/dataset/subdir')
|
||||
.replace('- /etc', '- /e2e/pool/dataset/subdir')
|
||||
.replace('- /var/log/syslog*', '')
|
||||
+ 'encryption_passphrase: "test"\n'
|
||||
+ 'zfs:\n'
|
||||
@@ -45,14 +45,14 @@ def test_zfs_create_and_list():
|
||||
)
|
||||
|
||||
# Run a create action to exercise ZFS snapshotting and backup.
|
||||
subprocess.check_call(f'borgmatic --config {config_path} create'.split(' '))
|
||||
subprocess.check_call(f'borgmatic -v 2 --config {config_path} create'.split(' '))
|
||||
|
||||
# List the resulting archive and assert that the snapshotted files are there.
|
||||
output = subprocess.check_output(
|
||||
f'borgmatic --config {config_path} list --archive latest'.split(' ')
|
||||
).decode(sys.stdout.encoding)
|
||||
|
||||
assert 'pool/dataset/subdir/file.txt' in output
|
||||
assert 'e2e/pool/dataset/subdir/file.txt' in output
|
||||
|
||||
# Assert that the snapshot has been deleted.
|
||||
assert not subprocess.check_output(
|
||||
|
||||
@@ -12,3 +12,59 @@ def test_borgmatic_version_matches_news_version():
|
||||
news_version = open('NEWS').readline()
|
||||
|
||||
assert borgmatic_version == news_version
|
||||
|
||||
|
||||
def test_run_configuration_without_error_pings_monitoring_hooks_start_and_finish():
|
||||
config = {'repositories': [{'path': 'foo'}]}
|
||||
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
|
||||
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
|
||||
flexmock(module).should_receive('run_actions').and_return([])
|
||||
flexmock(module.dispatch).should_receive('call_hooks')
|
||||
flexmock(module.dispatch).should_receive('call_hooks').with_args(
|
||||
'ping_monitor',
|
||||
config,
|
||||
module.dispatch.Hook_type.MONITORING,
|
||||
'test.yaml',
|
||||
module.monitor.State.START,
|
||||
object,
|
||||
object,
|
||||
).once()
|
||||
flexmock(module.dispatch).should_receive('call_hooks').with_args(
|
||||
'ping_monitor',
|
||||
config,
|
||||
module.dispatch.Hook_type.MONITORING,
|
||||
'test.yaml',
|
||||
module.monitor.State.FINISH,
|
||||
object,
|
||||
object,
|
||||
).once()
|
||||
|
||||
list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
|
||||
|
||||
|
||||
def test_run_configuration_with_action_error_pings_monioring_hooks_start_and_fail():
|
||||
config = {'repositories': [{'path': 'foo'}]}
|
||||
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
|
||||
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
|
||||
flexmock(module).should_receive('run_actions').and_raise(OSError)
|
||||
flexmock(module.dispatch).should_receive('call_hooks')
|
||||
flexmock(module.dispatch).should_receive('call_hooks').with_args(
|
||||
'ping_monitor',
|
||||
config,
|
||||
module.dispatch.Hook_type.MONITORING,
|
||||
'test.yaml',
|
||||
module.monitor.State.START,
|
||||
object,
|
||||
object,
|
||||
).once()
|
||||
flexmock(module.dispatch).should_receive('call_hooks').with_args(
|
||||
'ping_monitor',
|
||||
config,
|
||||
module.dispatch.Hook_type.MONITORING,
|
||||
'test.yaml',
|
||||
module.monitor.State.FAIL,
|
||||
object,
|
||||
object,
|
||||
).once()
|
||||
|
||||
list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
|
||||
|
||||
@@ -245,36 +245,69 @@ def test_expand_patterns_expands_only_tildes_in_non_root_patterns():
|
||||
assert paths == (Pattern('/root/bar/*', Pattern_type.INCLUDE),)
|
||||
|
||||
|
||||
def test_device_map_patterns_gives_device_id_per_path():
|
||||
def test_get_existent_path_or_parent_passes_through_existent_path():
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
|
||||
assert module.get_existent_path_or_parent('/foo/bar/baz') == '/foo/bar/baz'
|
||||
|
||||
|
||||
def test_get_existent_path_or_parent_with_non_existent_path_returns_none():
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
|
||||
assert module.get_existent_path_or_parent('/foo/bar/baz') is None
|
||||
|
||||
|
||||
def test_get_existent_path_or_parent_with_non_existent_path_returns_existent_parent():
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/foo/bar/baz*').and_return(False)
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/foo/bar').and_return(True)
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/foo').never()
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/').never()
|
||||
|
||||
assert module.get_existent_path_or_parent('/foo/bar/baz*') == '/foo/bar'
|
||||
|
||||
|
||||
def test_get_existent_path_or_parent_with_non_existent_path_returns_existent_grandparent():
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/foo/bar/baz*').and_return(False)
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/foo/bar').and_return(False)
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/foo').and_return(True)
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/').never()
|
||||
|
||||
assert module.get_existent_path_or_parent('/foo/bar/baz*') == '/foo'
|
||||
|
||||
|
||||
def test_get_existent_path_or_parent_with_end_to_end_test_prefix_returns_none():
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
|
||||
assert module.get_existent_path_or_parent('/e2e/foo/bar/baz') is None
|
||||
|
||||
|
||||
def test_device_map_patterns_gives_device_id_per_path():
|
||||
flexmock(module).should_receive('get_existent_path_or_parent').replace_with(lambda path: path)
|
||||
flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55))
|
||||
flexmock(module.os).should_receive('stat').with_args('/bar').and_return(flexmock(st_dev=66))
|
||||
|
||||
device_map = module.device_map_patterns((Pattern('/foo'), Pattern('/bar')))
|
||||
|
||||
assert device_map == (
|
||||
Pattern('/foo', device=55),
|
||||
Pattern('/bar', device=66),
|
||||
)
|
||||
|
||||
|
||||
def test_device_map_patterns_only_considers_root_patterns():
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55))
|
||||
flexmock(module.os).should_receive('stat').with_args('/bar*').never()
|
||||
|
||||
device_map = module.device_map_patterns(
|
||||
(Pattern('/foo'), Pattern('/bar*', Pattern_type.INCLUDE))
|
||||
(
|
||||
Pattern('/foo'),
|
||||
Pattern('^/bar', type=Pattern_type.INCLUDE, style=Pattern_style.REGULAR_EXPRESSION),
|
||||
)
|
||||
)
|
||||
|
||||
assert device_map == (
|
||||
Pattern('/foo', device=55),
|
||||
Pattern('/bar*', Pattern_type.INCLUDE),
|
||||
Pattern(
|
||||
'^/bar', type=Pattern_type.INCLUDE, style=Pattern_style.REGULAR_EXPRESSION, device=66
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_device_map_patterns_with_missing_path_does_not_error():
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True).and_return(False)
|
||||
flexmock(module).should_receive('get_existent_path_or_parent').with_args('/foo').and_return(
|
||||
'/foo'
|
||||
)
|
||||
flexmock(module).should_receive('get_existent_path_or_parent').with_args('/bar').and_return(
|
||||
None
|
||||
)
|
||||
flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55))
|
||||
flexmock(module.os).should_receive('stat').with_args('/bar').never()
|
||||
|
||||
@@ -287,7 +320,7 @@ def test_device_map_patterns_with_missing_path_does_not_error():
|
||||
|
||||
|
||||
def test_device_map_patterns_uses_working_directory_to_construct_path():
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module).should_receive('get_existent_path_or_parent').replace_with(lambda path: path)
|
||||
flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55))
|
||||
flexmock(module.os).should_receive('stat').with_args('/working/dir/bar').and_return(
|
||||
flexmock(st_dev=66)
|
||||
@@ -304,7 +337,7 @@ def test_device_map_patterns_uses_working_directory_to_construct_path():
|
||||
|
||||
|
||||
def test_device_map_patterns_with_existing_device_id_does_not_overwrite_it():
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module).should_receive('get_existent_path_or_parent').replace_with(lambda path: path)
|
||||
flexmock(module.os).should_receive('stat').with_args('/foo').and_return(flexmock(st_dev=55))
|
||||
flexmock(module.os).should_receive('stat').with_args('/bar').and_return(flexmock(st_dev=100))
|
||||
|
||||
|
||||
@@ -465,8 +465,10 @@ def test_run_configuration_logs_actions_error():
|
||||
flexmock(module).should_receive('Monitoring_hooks').and_return(flexmock())
|
||||
flexmock(module.command).should_receive('Before_after_hooks').and_return(flexmock())
|
||||
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
|
||||
expected_results = [flexmock()]
|
||||
flexmock(module).should_receive('log_error_records').and_return(expected_results)
|
||||
expected_results = [flexmock(), flexmock()]
|
||||
flexmock(module).should_receive('log_error_records').and_return(
|
||||
expected_results[:1]
|
||||
).and_return(expected_results[1:])
|
||||
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module).should_receive('run_actions').and_raise(OSError)
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
@@ -532,7 +534,7 @@ def test_run_configuration_logs_on_error_hook_error():
|
||||
expected_results = [flexmock(), flexmock()]
|
||||
flexmock(module).should_receive('log_error_records').and_return(
|
||||
expected_results[:1]
|
||||
).and_return(expected_results[1:])
|
||||
).and_return(expected_results[1:2]).and_return(expected_results[2:])
|
||||
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module).should_receive('run_actions').and_raise(OSError)
|
||||
config = {'repositories': [{'path': 'foo'}]}
|
||||
@@ -601,8 +603,10 @@ def test_run_configuration_bails_for_on_error_hook_soft_failure():
|
||||
error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
flexmock(module.command).should_receive('execute_hooks').and_raise(error)
|
||||
expected_results = [flexmock()]
|
||||
flexmock(module).should_receive('log_error_records').and_return(expected_results)
|
||||
expected_results = [flexmock(), flexmock()]
|
||||
flexmock(module).should_receive('log_error_records').and_return(
|
||||
expected_results[:1]
|
||||
).and_return(expected_results[1:])
|
||||
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module).should_receive('run_actions').and_raise(OSError)
|
||||
config = {'repositories': [{'path': 'foo'}]}
|
||||
@@ -654,11 +658,14 @@ def test_run_configuration_retries_hard_error():
|
||||
levelno=logging.WARNING,
|
||||
log_command_error_output=True,
|
||||
).and_return([flexmock()])
|
||||
error_logs = [flexmock()]
|
||||
error_logs = [flexmock(), flexmock()]
|
||||
flexmock(module).should_receive('log_error_records').with_args(
|
||||
'Error running actions for repository',
|
||||
OSError,
|
||||
).and_return(error_logs)
|
||||
).and_return(error_logs[:1]).ordered()
|
||||
flexmock(module).should_receive('log_error_records').with_args(
|
||||
'Error running configuration',
|
||||
).and_return(error_logs[1:]).ordered()
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
flexmock(module.command).should_receive('execute_hooks')
|
||||
config = {'repositories': [{'path': 'foo'}], 'retries': 1}
|
||||
@@ -680,13 +687,16 @@ def test_run_configuration_retries_repositories_in_order():
|
||||
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
|
||||
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module).should_receive('run_actions').and_raise(OSError).times(2)
|
||||
expected_results = [flexmock(), flexmock()]
|
||||
expected_results = [flexmock(), flexmock(), flexmock()]
|
||||
flexmock(module).should_receive('log_error_records').with_args(
|
||||
'Error running actions for repository', OSError
|
||||
).and_return(expected_results[:1]).ordered()
|
||||
flexmock(module).should_receive('log_error_records').with_args(
|
||||
'Error running actions for repository', OSError
|
||||
).and_return(expected_results[1:]).ordered()
|
||||
).and_return(expected_results[1:2]).ordered()
|
||||
flexmock(module).should_receive('log_error_records').with_args(
|
||||
'Error running configuration',
|
||||
).and_return(expected_results[2:]).ordered()
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
flexmock(module.command).should_receive('execute_hooks')
|
||||
config = {'repositories': [{'path': 'foo'}, {'path': 'bar'}]}
|
||||
@@ -728,6 +738,10 @@ def test_run_configuration_retries_round_robin():
|
||||
flexmock(module).should_receive('log_error_records').with_args(
|
||||
'Error running actions for repository', OSError
|
||||
).and_return(bar_error_logs).ordered()
|
||||
config_error_logs = [flexmock()]
|
||||
flexmock(module).should_receive('log_error_records').with_args(
|
||||
'Error running configuration',
|
||||
).and_return(config_error_logs).ordered()
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
flexmock(module.command).should_receive('execute_hooks')
|
||||
config = {
|
||||
@@ -741,7 +755,7 @@ def test_run_configuration_retries_round_robin():
|
||||
|
||||
results = list(module.run_configuration('test.yaml', config, ['/tmp/test.yaml'], arguments))
|
||||
|
||||
assert results == foo_error_logs + bar_error_logs
|
||||
assert results == foo_error_logs + bar_error_logs + config_error_logs
|
||||
|
||||
|
||||
def test_run_configuration_with_one_retry():
|
||||
@@ -766,10 +780,13 @@ def test_run_configuration_with_one_retry():
|
||||
levelno=logging.WARNING,
|
||||
log_command_error_output=True,
|
||||
).and_return(flexmock()).ordered()
|
||||
error_logs = [flexmock()]
|
||||
error_logs = [flexmock(), flexmock()]
|
||||
flexmock(module).should_receive('log_error_records').with_args(
|
||||
'Error running actions for repository', OSError
|
||||
).and_return(error_logs).ordered()
|
||||
).and_return(error_logs[:1]).ordered()
|
||||
flexmock(module).should_receive('log_error_records').with_args(
|
||||
'Error running configuration',
|
||||
).and_return(error_logs[1:]).ordered()
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
flexmock(module.command).should_receive('execute_hooks')
|
||||
config = {
|
||||
@@ -818,10 +835,13 @@ def test_run_configuration_with_retry_wait_does_backoff_after_each_retry():
|
||||
).and_return([flexmock()]).ordered()
|
||||
|
||||
flexmock(time).should_receive('sleep').with_args(30).and_return().ordered()
|
||||
error_logs = [flexmock()]
|
||||
error_logs = [flexmock(), flexmock()]
|
||||
flexmock(module).should_receive('log_error_records').with_args(
|
||||
'Error running actions for repository', OSError
|
||||
).and_return(error_logs).ordered()
|
||||
).and_return(error_logs[:1]).ordered()
|
||||
flexmock(module).should_receive('log_error_records').with_args(
|
||||
'Error running configuration',
|
||||
).and_return(error_logs[1:]).ordered()
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
flexmock(module.command).should_receive('execute_hooks')
|
||||
config = {
|
||||
@@ -867,10 +887,13 @@ def test_run_configuration_with_multiple_repositories_retries_with_timeout():
|
||||
|
||||
# Sleep before retrying bar (and failing)
|
||||
flexmock(time).should_receive('sleep').with_args(10).and_return().ordered()
|
||||
error_logs = [flexmock()]
|
||||
error_logs = [flexmock(), flexmock()]
|
||||
flexmock(module).should_receive('log_error_records').with_args(
|
||||
'Error running actions for repository', OSError
|
||||
).and_return(error_logs).ordered()
|
||||
).and_return(error_logs[:1]).ordered()
|
||||
flexmock(module).should_receive('log_error_records').with_args(
|
||||
'Error running configuration',
|
||||
).and_return(error_logs[1:]).ordered()
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
flexmock(module.command).should_receive('execute_hooks')
|
||||
config = {
|
||||
@@ -1909,7 +1932,12 @@ def test_collect_highlander_action_summary_logs_error_on_run_validate_failure():
|
||||
|
||||
def test_collect_configuration_run_summary_logs_info_for_success():
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, before='everything', action_names=object
|
||||
)
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, after='everything', action_names=object, state_names=['finish']
|
||||
)
|
||||
flexmock(module.command).should_receive('execute_hooks')
|
||||
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
@@ -1928,9 +1956,17 @@ def test_collect_configuration_run_summary_logs_info_for_success():
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_executes_hooks_for_create():
|
||||
before_everything_hook = {'before': 'everything', 'run': ['echo hi']}
|
||||
after_everything_hook = {'after': 'everything', 'run': ['echo hi']}
|
||||
command_hooks = (before_everything_hook, after_everything_hook)
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
flexmock(module.command).should_receive('execute_hooks')
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
command_hooks, before='everything', action_names=object
|
||||
).and_return([before_everything_hook])
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
command_hooks, after='everything', action_names=object, state_names=['finish']
|
||||
).and_return([after_everything_hook])
|
||||
flexmock(module.command).should_receive('execute_hooks').twice()
|
||||
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
arguments = {
|
||||
@@ -1940,7 +1976,7 @@ def test_collect_configuration_run_summary_executes_hooks_for_create():
|
||||
|
||||
logs = tuple(
|
||||
module.collect_configuration_run_summary_logs(
|
||||
{'test.yaml': {}},
|
||||
{'test.yaml': {'commands': command_hooks}},
|
||||
config_paths=['/tmp/test.yaml'],
|
||||
arguments=arguments,
|
||||
log_file_path=None,
|
||||
@@ -1950,9 +1986,51 @@ def test_collect_configuration_run_summary_executes_hooks_for_create():
|
||||
assert {log.levelno for log in logs} == {logging.INFO}
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_deduplicates_everything_hooks_across_config_files():
|
||||
before_everything_hook = {'before': 'everything', 'run': ['echo hi']}
|
||||
after_everything_hook = {'after': 'everything', 'run': ['echo hi']}
|
||||
command_hooks = (before_everything_hook, after_everything_hook)
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
command_hooks, before='everything', action_names=object
|
||||
).and_return([before_everything_hook]).once()
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
(after_everything_hook,), before='everything', action_names=object
|
||||
).and_return([]).once()
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
command_hooks, after='everything', action_names=object, state_names=['finish']
|
||||
).and_return([after_everything_hook]).once()
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
(before_everything_hook,), after='everything', action_names=object, state_names=['finish']
|
||||
).and_return([]).once()
|
||||
flexmock(module.command).should_receive('execute_hooks').twice()
|
||||
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
arguments = {
|
||||
'create': flexmock(),
|
||||
'global': flexmock(monitoring_verbosity=1, dry_run=False),
|
||||
}
|
||||
|
||||
logs = tuple(
|
||||
module.collect_configuration_run_summary_logs(
|
||||
{'test.yaml': {'commands': command_hooks}, 'other.yaml': {'commands': command_hooks}},
|
||||
config_paths=['/tmp/test.yaml', '/tmp/other.yaml'],
|
||||
arguments=arguments,
|
||||
log_file_path=None,
|
||||
)
|
||||
)
|
||||
|
||||
assert {log.levelno for log in logs} == {logging.INFO}
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_info_for_success_with_extract():
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, before='everything', action_names=object
|
||||
)
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, after='everything', action_names=object, state_names=['finish']
|
||||
)
|
||||
flexmock(module.command).should_receive('execute_hooks')
|
||||
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
@@ -1995,7 +2073,12 @@ def test_collect_configuration_run_summary_logs_extract_with_repository_error():
|
||||
|
||||
def test_collect_configuration_run_summary_logs_info_for_success_with_mount():
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, before='everything', action_names=object
|
||||
)
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, after='everything', action_names=object, state_names=['finish']
|
||||
)
|
||||
flexmock(module.command).should_receive('execute_hooks')
|
||||
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
@@ -2041,7 +2124,12 @@ def test_collect_configuration_run_summary_logs_mount_with_repository_error():
|
||||
|
||||
def test_collect_configuration_run_summary_logs_missing_configs_error():
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, before='everything', action_names=object
|
||||
)
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, after='everything', action_names=object, state_names=['fail']
|
||||
)
|
||||
flexmock(module.command).should_receive('execute_hooks')
|
||||
arguments = {'global': flexmock(config_paths=[])}
|
||||
expected_logs = (flexmock(),)
|
||||
@@ -2056,9 +2144,17 @@ def test_collect_configuration_run_summary_logs_missing_configs_error():
|
||||
assert logs == expected_logs
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_pre_hook_error():
|
||||
def test_collect_configuration_run_summary_logs_before_hook_error():
|
||||
before_everything_hook = {'before': 'everything', 'run': ['echo hi']}
|
||||
after_everything_hook = {'after': 'everything', 'run': ['echo hi']}
|
||||
command_hooks = (before_everything_hook, after_everything_hook)
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, before='everything', action_names=object
|
||||
).and_return([before_everything_hook])
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, after='everything', action_names=object, state_names=['fail']
|
||||
).and_return([after_everything_hook])
|
||||
flexmock(module.command).should_receive('execute_hooks').and_raise(ValueError)
|
||||
expected_logs = (flexmock(),)
|
||||
flexmock(module).should_receive('log_error_records').and_return(expected_logs)
|
||||
@@ -2069,7 +2165,7 @@ def test_collect_configuration_run_summary_logs_pre_hook_error():
|
||||
|
||||
logs = tuple(
|
||||
module.collect_configuration_run_summary_logs(
|
||||
{'test.yaml': {}},
|
||||
{'test.yaml': {'commands': command_hooks}},
|
||||
config_paths=['/tmp/test.yaml'],
|
||||
arguments=arguments,
|
||||
log_file_path=None,
|
||||
@@ -2079,9 +2175,17 @@ def test_collect_configuration_run_summary_logs_pre_hook_error():
|
||||
assert logs == expected_logs
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_post_hook_error():
|
||||
def test_collect_configuration_run_summary_logs_after_hook_error():
|
||||
before_everything_hook = {'before': 'everything', 'run': ['echo hi']}
|
||||
after_everything_hook = {'after': 'everything', 'run': ['echo hi']}
|
||||
command_hooks = (before_everything_hook, after_everything_hook)
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, before='everything', action_names=object
|
||||
).and_return([before_everything_hook])
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, after='everything', action_names=object, state_names=['finish']
|
||||
).and_return([after_everything_hook])
|
||||
flexmock(module.command).should_receive('execute_hooks').and_return(None).and_raise(ValueError)
|
||||
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
@@ -2094,7 +2198,7 @@ def test_collect_configuration_run_summary_logs_post_hook_error():
|
||||
|
||||
logs = tuple(
|
||||
module.collect_configuration_run_summary_logs(
|
||||
{'test.yaml': {}},
|
||||
{'test.yaml': {'commands': command_hooks}},
|
||||
config_paths=['/tmp/test.yaml'],
|
||||
arguments=arguments,
|
||||
log_file_path=None,
|
||||
@@ -2129,7 +2233,12 @@ def test_collect_configuration_run_summary_logs_for_list_with_archive_and_reposi
|
||||
|
||||
def test_collect_configuration_run_summary_logs_info_for_success_with_list():
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, before='everything', action_names=object
|
||||
)
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, after='everything', action_names=object, state_names=['finish']
|
||||
)
|
||||
flexmock(module.command).should_receive('execute_hooks')
|
||||
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
@@ -2152,7 +2261,12 @@ def test_collect_configuration_run_summary_logs_info_for_success_with_list():
|
||||
|
||||
def test_collect_configuration_run_summary_logs_run_configuration_error_logs():
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, before='everything', action_names=object
|
||||
)
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, after='everything', action_names=object, state_names=['fail']
|
||||
)
|
||||
flexmock(module.command).should_receive('execute_hooks')
|
||||
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module).should_receive('run_configuration').and_return(
|
||||
@@ -2175,7 +2289,12 @@ def test_collect_configuration_run_summary_logs_run_configuration_error_logs():
|
||||
|
||||
def test_collect_configuration_run_summary_logs_run_umount_error():
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, before='everything', action_names=object
|
||||
)
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, after='everything', action_names=object, state_names=['fail']
|
||||
)
|
||||
flexmock(module.command).should_receive('execute_hooks')
|
||||
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
@@ -2202,7 +2321,12 @@ def test_collect_configuration_run_summary_logs_run_umount_error():
|
||||
|
||||
def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||
flexmock(module.command).should_receive('filter_hooks')
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, before='everything', action_names=object
|
||||
)
|
||||
flexmock(module.command).should_receive('filter_hooks').with_args(
|
||||
object, after='everything', action_names=object, state_names=['finish']
|
||||
)
|
||||
flexmock(module.command).should_receive('execute_hooks')
|
||||
flexmock(module).should_receive('Log_prefix').and_return(flexmock())
|
||||
flexmock(module).should_receive('run_configuration').and_return(['foo', 'bar']).and_return(
|
||||
|
||||
@@ -11,8 +11,16 @@ def test_interpolate_context_passes_through_command_without_variable():
|
||||
assert module.interpolate_context('pre-backup', 'ls', {'foo': 'bar'}) == 'ls'
|
||||
|
||||
|
||||
def test_interpolate_context_passes_through_command_with_unknown_variable():
|
||||
def test_interpolate_context_warns_and_passes_through_command_with_unknown_variable():
|
||||
command = 'ls {baz}' # noqa: FS003
|
||||
flexmock(module.logger).should_receive('warning').once()
|
||||
|
||||
assert module.interpolate_context('pre-backup', command, {'foo': 'bar'}) == command
|
||||
|
||||
|
||||
def test_interpolate_context_does_not_warn_and_passes_through_command_with_unknown_variable_matching_borg_placeholder():
|
||||
command = 'ls {hostname}' # noqa: FS003
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
|
||||
assert module.interpolate_context('pre-backup', command, {'foo': 'bar'}) == command
|
||||
|
||||
@@ -167,6 +175,44 @@ def test_make_environment_with_pyinstaller_and_LD_LIBRARY_PATH_ORIG_copies_it_in
|
||||
},
|
||||
),
|
||||
),
|
||||
(
|
||||
(
|
||||
{
|
||||
'before': 'action',
|
||||
'states': ['finish'], # Not actually valid; only valid for "after".
|
||||
'run': ['foo'],
|
||||
},
|
||||
{
|
||||
'after': 'action',
|
||||
'run': ['bar'],
|
||||
},
|
||||
{
|
||||
'after': 'action',
|
||||
'states': ['finish'],
|
||||
'run': ['baz'],
|
||||
},
|
||||
{
|
||||
'after': 'action',
|
||||
'states': ['fail'],
|
||||
'run': ['quux'],
|
||||
},
|
||||
),
|
||||
{
|
||||
'after': 'action',
|
||||
'state_names': ['finish'],
|
||||
},
|
||||
(
|
||||
{
|
||||
'after': 'action',
|
||||
'run': ['bar'],
|
||||
},
|
||||
{
|
||||
'after': 'action',
|
||||
'states': ['finish'],
|
||||
'run': ['baz'],
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_filter_hooks(hooks, filters, expected_hooks):
|
||||
@@ -336,14 +382,13 @@ def test_before_after_hooks_calls_command_hooks():
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
before='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
after='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
state_names=['finish'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('execute_hooks').twice()
|
||||
|
||||
@@ -353,7 +398,6 @@ def test_before_after_hooks_calls_command_hooks():
|
||||
umask=1234,
|
||||
working_directory='/working',
|
||||
dry_run=False,
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
context1='stuff',
|
||||
context2='such',
|
||||
@@ -369,14 +413,13 @@ def test_before_after_hooks_with_before_error_runs_after_hook_and_raises():
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
before='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
after='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
state_names=['fail'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('execute_hooks').and_raise(OSError).and_return(None)
|
||||
flexmock(module).should_receive('considered_soft_failure').and_return(False)
|
||||
@@ -388,7 +431,6 @@ def test_before_after_hooks_with_before_error_runs_after_hook_and_raises():
|
||||
umask=1234,
|
||||
working_directory='/working',
|
||||
dry_run=False,
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
context1='stuff',
|
||||
context2='such',
|
||||
@@ -404,14 +446,13 @@ def test_before_after_hooks_with_before_soft_failure_raises():
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
before='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
after='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
state_names=['finish'],
|
||||
).never()
|
||||
flexmock(module).should_receive('execute_hooks').and_raise(OSError)
|
||||
flexmock(module).should_receive('considered_soft_failure').and_return(True)
|
||||
@@ -423,7 +464,6 @@ def test_before_after_hooks_with_before_soft_failure_raises():
|
||||
umask=1234,
|
||||
working_directory='/working',
|
||||
dry_run=False,
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
context1='stuff',
|
||||
context2='such',
|
||||
@@ -431,6 +471,38 @@ def test_before_after_hooks_with_before_soft_failure_raises():
|
||||
pass
|
||||
|
||||
|
||||
def test_before_after_hooks_with_wrapped_code_error_runs_after_hook_and_raises():
|
||||
commands = [
|
||||
{'before': 'repository', 'run': ['foo', 'bar']},
|
||||
{'after': 'repository', 'run': ['baz']},
|
||||
]
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
before='action',
|
||||
action_names=['create'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
after='action',
|
||||
action_names=['create'],
|
||||
state_names=['fail'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('execute_hooks').twice()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with module.Before_after_hooks(
|
||||
command_hooks=commands,
|
||||
before_after='action',
|
||||
umask=1234,
|
||||
working_directory='/working',
|
||||
dry_run=False,
|
||||
action_names=['create'],
|
||||
context1='stuff',
|
||||
context2='such',
|
||||
):
|
||||
raise ValueError()
|
||||
|
||||
|
||||
def test_before_after_hooks_with_after_error_raises():
|
||||
commands = [
|
||||
{'before': 'repository', 'run': ['foo', 'bar']},
|
||||
@@ -439,14 +511,13 @@ def test_before_after_hooks_with_after_error_raises():
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
before='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
after='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
state_names=['finish'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('execute_hooks').and_return(None).and_raise(OSError)
|
||||
flexmock(module).should_receive('considered_soft_failure').and_return(False)
|
||||
@@ -458,7 +529,6 @@ def test_before_after_hooks_with_after_error_raises():
|
||||
umask=1234,
|
||||
working_directory='/working',
|
||||
dry_run=False,
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
context1='stuff',
|
||||
context2='such',
|
||||
@@ -474,14 +544,13 @@ def test_before_after_hooks_with_after_soft_failure_raises():
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
before='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('filter_hooks').with_args(
|
||||
commands,
|
||||
after='action',
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
state_names=['finish'],
|
||||
).and_return(flexmock()).once()
|
||||
flexmock(module).should_receive('execute_hooks').and_return(None).and_raise(OSError)
|
||||
flexmock(module).should_receive('considered_soft_failure').and_return(True)
|
||||
@@ -493,7 +562,6 @@ def test_before_after_hooks_with_after_soft_failure_raises():
|
||||
umask=1234,
|
||||
working_directory='/working',
|
||||
dry_run=False,
|
||||
hook_name='myhook',
|
||||
action_names=['create'],
|
||||
context1='stuff',
|
||||
context2='such',
|
||||
|
||||
Reference in New Issue
Block a user