This commit is contained in:
parent
bb3475b3f8
commit
80e2c023dd
7 changed files with 68 additions and 17 deletions
1
AUTHORS
1
AUTHORS
|
@ -5,3 +5,4 @@ Henning Schroeder: Copy editing
|
||||||
Michele Lazzeri: Custom archive names
|
Michele Lazzeri: Custom archive names
|
||||||
Robin `ypid` Schneider: Support additional options of Borg
|
Robin `ypid` Schneider: Support additional options of Borg
|
||||||
Scott Squires: Custom archive names
|
Scott Squires: Custom archive names
|
||||||
|
Johannes Feichtner: Support for user hooks
|
||||||
|
|
|
@ -5,6 +5,7 @@ from subprocess import CalledProcessError
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from borgmatic.borg import check, create, prune
|
from borgmatic.borg import check, create, prune
|
||||||
|
from borgmatic.commands import hook
|
||||||
from borgmatic.config import collect, convert, validate
|
from borgmatic.config import collect, convert, validate
|
||||||
|
|
||||||
|
|
||||||
|
@ -84,13 +85,15 @@ def main(): # pragma: no cover
|
||||||
|
|
||||||
for config_filename in config_filenames:
|
for config_filename in config_filenames:
|
||||||
config = validate.parse_configuration(config_filename, validate.schema_filename())
|
config = validate.parse_configuration(config_filename, validate.schema_filename())
|
||||||
(location, storage, retention, consistency) = (
|
(location, storage, retention, consistency, hooks) = (
|
||||||
config.get(section_name, {})
|
config.get(section_name, {})
|
||||||
for section_name in ('location', 'storage', 'retention', 'consistency')
|
for section_name in ('location', 'storage', 'retention', 'consistency', 'hooks')
|
||||||
)
|
)
|
||||||
remote_path = location.get('remote_path')
|
remote_path = location.get('remote_path')
|
||||||
|
|
||||||
|
try:
|
||||||
create.initialize(storage)
|
create.initialize(storage)
|
||||||
|
hook.execute_hook(hooks.get('before_backup'))
|
||||||
|
|
||||||
for repository in location['repositories']:
|
for repository in location['repositories']:
|
||||||
if args.prune:
|
if args.prune:
|
||||||
|
@ -104,6 +107,11 @@ def main(): # pragma: no cover
|
||||||
)
|
)
|
||||||
if args.check:
|
if args.check:
|
||||||
check.check_archives(args.verbosity, repository, consistency, remote_path=remote_path)
|
check.check_archives(args.verbosity, repository, consistency, remote_path=remote_path)
|
||||||
|
|
||||||
|
hook.execute_hook(hooks.get('after_backup'))
|
||||||
|
except (OSError, CalledProcessError):
|
||||||
|
hook.execute_hook(hooks.get('on_error'))
|
||||||
|
raise
|
||||||
except (ValueError, OSError, CalledProcessError) as error:
|
except (ValueError, OSError, CalledProcessError) as error:
|
||||||
print(error, file=sys.stderr)
|
print(error, file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
7
borgmatic/commands/hook.py
Normal file
7
borgmatic/commands/hook.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
def execute_hook(commands):
|
||||||
|
if commands:
|
||||||
|
for cmd in commands:
|
||||||
|
subprocess.check_call(cmd, shell=True)
|
|
@ -24,7 +24,7 @@ def _schema_to_sample_configuration(schema, level=0):
|
||||||
for each section based on the schema "desc" description.
|
for each section based on the schema "desc" description.
|
||||||
'''
|
'''
|
||||||
example = schema.get('example')
|
example = schema.get('example')
|
||||||
if example:
|
if example is not None:
|
||||||
return example
|
return example
|
||||||
|
|
||||||
config = yaml.comments.CommentedMap([
|
config = yaml.comments.CommentedMap([
|
||||||
|
|
|
@ -157,3 +157,28 @@ map:
|
||||||
desc: Restrict the number of checked archives to the last n. Applies only to the
|
desc: Restrict the number of checked archives to the last n. Applies only to the
|
||||||
"archives" check.
|
"archives" check.
|
||||||
example: 3
|
example: 3
|
||||||
|
hooks:
|
||||||
|
desc: |
|
||||||
|
Shell commands or scripts to execute before and after a backup or if an error has occurred.
|
||||||
|
IMPORTANT: All provided commands and scripts are executed with user permissions of borgmatic.
|
||||||
|
Do not forget to set secure permissions on this file as well as on any script listed (chmod 0700) to
|
||||||
|
prevent potential shell injection or privilege escalation.
|
||||||
|
map:
|
||||||
|
before_backup:
|
||||||
|
seq:
|
||||||
|
- type: scalar
|
||||||
|
desc: List of one or more shell commands or scripts to execute before creating a backup.
|
||||||
|
example:
|
||||||
|
- echo "`date` - Starting a backup job."
|
||||||
|
after_backup:
|
||||||
|
seq:
|
||||||
|
- type: scalar
|
||||||
|
desc: List of one or more shell commands or scripts to execute after creating a backup.
|
||||||
|
example:
|
||||||
|
- echo "`date` - Backup created."
|
||||||
|
on_error:
|
||||||
|
seq:
|
||||||
|
- type: scalar
|
||||||
|
desc: List of one or more shell commands or scripts to execute in case an exception has occurred.
|
||||||
|
example:
|
||||||
|
- echo "`date` - Error while creating a backup."
|
||||||
|
|
|
@ -48,7 +48,7 @@ def parse_configuration(config_filename, schema_filename):
|
||||||
# simply remove all examples before passing the schema to pykwalify.
|
# simply remove all examples before passing the schema to pykwalify.
|
||||||
for section_name, section_schema in schema['map'].items():
|
for section_name, section_schema in schema['map'].items():
|
||||||
for field_name, field_schema in section_schema['map'].items():
|
for field_name, field_schema in section_schema['map'].items():
|
||||||
field_schema.pop('example')
|
field_schema.pop('example', None)
|
||||||
|
|
||||||
validator = pykwalify.core.Core(source_data=config, schema_data=schema)
|
validator = pykwalify.core.Core(source_data=config, schema_data=schema)
|
||||||
parsed_result = validator.validate(raise_exception=False)
|
parsed_result = validator.validate(raise_exception=False)
|
||||||
|
|
10
borgmatic/tests/unit/borg/test_hook.py
Normal file
10
borgmatic/tests/unit/borg/test_hook.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.commands import hook as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_hook_invokes_each_command():
|
||||||
|
subprocess = flexmock(module.subprocess)
|
||||||
|
subprocess.should_receive('check_call').with_args(':', shell=True).once()
|
||||||
|
|
||||||
|
module.execute_hook([':'])
|
Loading…
Reference in a new issue