Compare commits

...
6 changed files with 126 additions and 44 deletions
+2
View File
@@ -1,6 +1,8 @@
2.0.9.dev0 2.0.9.dev0
* #1123: Add loading of systemd credentials even when running borgmatic outside of a systemd * #1123: Add loading of systemd credentials even when running borgmatic outside of a systemd
service. service.
* #1134: Add a "steps" option to run command hooks around particular sub-action steps like
individual checks.
* #1149: Add support for Python 3.14. * #1149: Add support for Python 3.14.
* #1149: Include automated tests in the source dist tarball uploaded to PyPI. * #1149: Include automated tests in the source dist tarball uploaded to PyPI.
* #1151: Fix snapshotting in the ZFS, Btrfs, and LVM hooks to play nicely with the Borg 1.4+ * #1151: Fix snapshotting in the ZFS, Btrfs, and LVM hooks to play nicely with the Borg 1.4+
+62 -30
View File
@@ -744,6 +744,7 @@ def run_check(
global_arguments, global_arguments,
local_path, local_path,
remote_path, remote_path,
hook_context,
): ):
''' '''
Run the "check" action for the given repository. Run the "check" action for the given repository.
@@ -783,44 +784,75 @@ def run_check(
archives_check_id, archives_check_id,
) )
borg_specific_checks = set(checks).intersection({'repository', 'archives', 'data'}) borg_specific_checks = set(checks).intersection({'repository', 'archives', 'data'})
working_directory = borgmatic.config.paths.get_working_directory(config)
if borg_specific_checks: if borg_specific_checks:
borgmatic.borg.check.check_archives( with borgmatic.hooks.command.Before_after_hooks(
repository['path'], command_hooks=config.get('commands'),
config, before_after='step',
local_borg_version, umask=config.get('umask'),
check_arguments, working_directory=working_directory,
global_arguments, dry_run=global_arguments.dry_run,
borg_specific_checks, action_names=('check',),
archive_filter_flags, step_names=('archives_repository_data',),
local_path=local_path, **hook_context,
remote_path=remote_path, ):
) borgmatic.borg.check.check_archives(
for check in borg_specific_checks: repository['path'],
write_check_time(make_check_time_path(config, repository_id, check, archives_check_id)) config,
local_borg_version,
check_arguments,
global_arguments,
borg_specific_checks,
archive_filter_flags,
local_path=local_path,
remote_path=remote_path,
)
for check in borg_specific_checks:
write_check_time(make_check_time_path(config, repository_id, check, archives_check_id))
if 'extract' in checks: if 'extract' in checks:
borgmatic.borg.extract.extract_last_archive_dry_run( with borgmatic.hooks.command.Before_after_hooks(
config, command_hooks=config.get('commands'),
local_borg_version, before_after='step',
global_arguments, umask=config.get('umask'),
repository['path'], working_directory=working_directory,
config.get('lock_wait'), dry_run=global_arguments.dry_run,
local_path, action_names=('check',),
remote_path, step_names=('extract',),
) **hook_context,
write_check_time(make_check_time_path(config, repository_id, 'extract')) ):
borgmatic.borg.extract.extract_last_archive_dry_run(
if 'spot' in checks:
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
spot_check(
repository,
config, config,
local_borg_version, local_borg_version,
global_arguments, global_arguments,
repository['path'],
config.get('lock_wait'),
local_path, local_path,
remote_path, remote_path,
borgmatic_runtime_directory,
) )
write_check_time(make_check_time_path(config, repository_id, 'extract'))
write_check_time(make_check_time_path(config, repository_id, 'spot')) if 'spot' in checks:
with borgmatic.hooks.command.Before_after_hooks(
command_hooks=config.get('commands'),
before_after='step',
umask=config.get('umask'),
working_directory=working_directory,
dry_run=global_arguments.dry_run,
action_names=('check',),
step_names=('spot',),
**hook_context,
):
with borgmatic.config.paths.Runtime_directory(config) as borgmatic_runtime_directory:
spot_check(
repository,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
borgmatic_runtime_directory,
)
write_check_time(make_check_time_path(config, repository_id, 'spot'))
+1
View File
@@ -463,6 +463,7 @@ def run_actions( # noqa: PLR0912, PLR0915
global_arguments, global_arguments,
local_path, local_path,
remote_path, remote_path,
hook_context,
) )
elif action_name == 'extract': elif action_name == 'extract':
borgmatic.actions.extract.run_extract( borgmatic.actions.extract.run_extract(
+30
View File
@@ -1140,6 +1140,7 @@ properties:
before: before:
type: string type: string
enum: enum:
- step
- action - action
- repository - repository
- configuration - configuration
@@ -1148,6 +1149,8 @@ properties:
Name for the point in borgmatic's execution that Name for the point in borgmatic's execution that
the commands should be run before (required if the commands should be run before (required if
"after" isn't set): "after" isn't set):
* "step" runs before a sub-action step for each
repository, e.g. for an individual check.
* "action" runs before each action for each * "action" runs before each action for each
repository. repository.
* "repository" runs before all actions for each * "repository" runs before all actions for each
@@ -1188,6 +1191,18 @@ properties:
List of actions for which the commands will be List of actions for which the commands will be
run. Defaults to running for all actions. run. Defaults to running for all actions.
example: [create, prune, compact, check] example: [create, prune, compact, check]
steps:
type: array
items:
type: string
enum:
- archives_repository_data
- extract
- spot
description: |
List of sub-action steps for which the commands
will be run. Defaults to running for all steps.
example: [extract, spot]
run: run:
type: array type: array
items: items:
@@ -1203,6 +1218,7 @@ properties:
after: after:
type: string type: string
enum: enum:
- step
- action - action
- repository - repository
- configuration - configuration
@@ -1212,6 +1228,8 @@ properties:
Name for the point in borgmatic's execution that Name for the point in borgmatic's execution that
the commands should be run after (required if the commands should be run after (required if
"before" isn't set): "before" isn't set):
* "step" runs before a sub-action step for each
repository, e.g. for an individual check.
* "action" runs after each action for each * "action" runs after each action for each
repository. repository.
* "repository" runs after all actions for each * "repository" runs after all actions for each
@@ -1254,6 +1272,18 @@ properties:
particular actions listed here. Defaults to particular actions listed here. Defaults to
running for all actions. running for all actions.
example: [create, prune, compact, check] example: [create, prune, compact, check]
steps:
type: array
items:
type: string
enum:
- archives_repository_data
- extract
- spot
description: |
List of sub-action steps for which the commands
will be run. Defaults to running for all steps.
example: [extract, spot]
states: states:
type: array type: array
items: items:
+16 -6
View File
@@ -64,22 +64,27 @@ def make_environment(current_environment, sys_module=sys):
return environment return environment
def filter_hooks(command_hooks, before=None, after=None, action_names=None, state_names=None): def filter_hooks(command_hooks, before=None, after=None, action_names=None, step_names=None, state_names=None):
''' '''
Given a sequence of command hook dicts from configuration and one or more filters (before name, Given a sequence of command hook dicts from configuration and one or more filters (before name,
after name, a sequence of action names, and/or a sequence of execution result state names), after name, a sequence of action names, a sequence of sub-action steps, and/or a sequence of
filter down the command hooks to just the ones that match the given filters. execution result state names), filter down the command hooks to just the ones that match the
given filters.
''' '''
return tuple( return tuple(
hook_config hook_config
for hook_config in command_hooks or () for hook_config in command_hooks or ()
for config_action_names in (hook_config.get('when'),) for config_action_names in (hook_config.get('when'),)
for config_step_names in (hook_config.get('steps'),)
for config_state_names in (hook_config.get('states'),) for config_state_names in (hook_config.get('states'),)
if before is None or hook_config.get('before') == before if before is None or hook_config.get('before') == before
if after is None or hook_config.get('after') == after if after is None or hook_config.get('after') == after
if action_names is None if action_names is None
or config_action_names is None or config_action_names is None
or set(config_action_names or ()).intersection(set(action_names)) or set(config_action_names or ()).intersection(set(action_names))
if step_names is None
or config_step_names is None
or set(config_step_names or ()).intersection(set(step_names))
if state_names is None if state_names is None
or config_state_names is None or config_state_names is None
or set(config_state_names or ()).intersection(set(state_names)) or set(config_state_names or ()).intersection(set(state_names))
@@ -164,7 +169,8 @@ class Before_after_hooks:
before_after='do_stuff', before_after='do_stuff',
umask=config.get('umask'), umask=config.get('umask'),
dry_run=dry_run, dry_run=dry_run,
action_names=['create'], action_names=['check'],
step_names=['spot'],
): ):
do() do()
some() some()
@@ -182,13 +188,14 @@ class Before_after_hooks:
working_directory, working_directory,
dry_run, dry_run,
action_names=None, action_names=None,
step_names=None,
**context, **context,
): ):
''' '''
Given a sequence of command hook configuration dicts, the before/after name, a umask to run 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, a sequence of 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 action names, a sequence of sub-action step names, and any context for the executed
below. commands, save those data points for use below.
''' '''
self.command_hooks = command_hooks self.command_hooks = command_hooks
self.before_after = before_after self.before_after = before_after
@@ -196,6 +203,7 @@ class Before_after_hooks:
self.working_directory = working_directory self.working_directory = working_directory
self.dry_run = dry_run self.dry_run = dry_run
self.action_names = action_names self.action_names = action_names
self.step_names = step_names
self.context = context self.context = context
def __enter__(self): def __enter__(self):
@@ -208,6 +216,7 @@ class Before_after_hooks:
self.command_hooks, self.command_hooks,
before=self.before_after, before=self.before_after,
action_names=self.action_names, action_names=self.action_names,
step_names=self.step_names,
), ),
self.umask, self.umask,
self.working_directory, self.working_directory,
@@ -234,6 +243,7 @@ class Before_after_hooks:
self.command_hooks, self.command_hooks,
after=self.before_after, after=self.before_after,
action_names=self.action_names, action_names=self.action_names,
step_names=self.step_names,
state_names=['fail' if exception_type else 'finish'], state_names=['fail' if exception_type else 'finish'],
), ),
self.umask, self.umask,
@@ -77,12 +77,14 @@ commands:
Each command in the `commands:` list has the following options: 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: * `before` or `after`: Name for the point in borgmatic's execution that the commands should be run before or after, one of:
* <span class="minilink minilink-addedin">New in version 2.0.9</span> `step` runs before or after each sub-action step for each repository, e.g. for an individual check.
* `action` runs before or after each action for each repository. This replaces the deprecated `before_create`, `after_prune`, etc. * `action` runs before or after each action for each repository. This replaces the deprecated `before_create`, `after_prune`, etc.
* `repository` runs before or after all actions for each repository. This replaces the deprecated `before_actions` and `after_actions`. * `repository` runs before or after all actions for each repository. This replaces the deprecated `before_actions` and `after_actions`.
* `configuration` runs before or after all actions and repositories in the current configuration file. * `configuration` runs before or after all actions and repositories in the current configuration file.
* `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`. * `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. * `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. * `when`: Only trigger the hook when borgmatic is run with particular actions (`create`, `prune`, etc.) listed here. Defaults to running for all actions.
* `steps`: <span class="minilink minilink-addedin">New in version 2.0.9</span> Only trigger the hook when borgmatic runs particular sub-action steps (`extract`, `spot`, etc.) listed here. Defaults to running for all steps.
* `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: * `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. * `finish`: No errors occurred.
* `fail`: An error occurred. * `fail`: An error occurred.
@@ -107,17 +109,22 @@ execution.
Let's say you've got a borgmatic configuration file with a configured Let's say you've got a borgmatic configuration file with a configured
repository. And suppose you configure several command hooks and then run repository. And suppose you configure several command hooks and then run
borgmatic for the `create` and `prune` actions. Here's the order of execution: borgmatic for the `create` and `check` actions. Here's the order of execution:
* Run `before: everything` hooks (from all configuration files). * Run `before: everything` hooks (from all configuration files).
* Run `before: configuration` hooks (from the first configuration file). * Run `before: configuration` hooks (from the first configuration file).
* Run `before: repository` hooks (for the first repository). * Run `before: repository` hooks (for the first repository).
* Run `before: action` hooks for `create`. * Run `before: action` hooks for `create`.
* Actually run the `create` action (e.g. `borg create`). * Run the `create` action including `borg create`.
* Run `after: action` hooks for `create`. * Run `after: action` hooks for `create`.
* Run `before: action` hooks for `prune`. * Run `before: action` hooks for `check`.
* Actually run the `prune` action (e.g. `borg prune`). * Run `before: step` hooks for the `archives_repository_data` step.
* Run `after: action` hooks for `prune`. * Run the `borg check` portion of the `check` action.
* Run `after: step` hooks for the `archives_repository_data` step.
* Run `before: step` hooks for the `spot` step.
* Run the `spot` check portion of the `check` action.
* Run `after: step` hooks for the `spot` step.
* Run `after: action` hooks for `check`.
* Run `after: repository` hooks (for the first repository). * Run `after: repository` hooks (for the first repository).
* Run `after: configuration` hooks (from the first configuration file). * Run `after: configuration` hooks (from the first configuration file).
* Run `after: error` hooks (if an error occurs). * Run `after: error` hooks (if an error occurs).
@@ -134,9 +141,9 @@ 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 actions for—and repositories in—a configuration file have had a chance to
execute. execute.
And if there are multiple hooks defined for a particular step (e.g. `before: And if there are multiple hooks defined for a particular combination (e.g.
action` for `create`), then those hooks are run in the order they're defined in `before: action` for `create`), then those hooks are run in the order they're
configuration. defined in configuration.
### Deprecated command hooks ### Deprecated command hooks