#16, #38: Support for user-defined hooks before/after backup, or on error.

This commit is contained in:
Johannes Feichtner 2017-10-26 06:38:27 +02:00
parent bb3475b3f8
commit 80e2c023dd
7 changed files with 68 additions and 17 deletions

View File

@ -5,3 +5,4 @@ Henning Schroeder: Copy editing
Michele Lazzeri: Custom archive names
Robin `ypid` Schneider: Support additional options of Borg
Scott Squires: Custom archive names
Johannes Feichtner: Support for user hooks

View File

@ -5,6 +5,7 @@ from subprocess import CalledProcessError
import sys
from borgmatic.borg import check, create, prune
from borgmatic.commands import hook
from borgmatic.config import collect, convert, validate
@ -84,26 +85,33 @@ def main(): # pragma: no cover
for config_filename in config_filenames:
config = validate.parse_configuration(config_filename, validate.schema_filename())
(location, storage, retention, consistency) = (
(location, storage, retention, consistency, hooks) = (
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')
create.initialize(storage)
try:
create.initialize(storage)
hook.execute_hook(hooks.get('before_backup'))
for repository in location['repositories']:
if args.prune:
prune.prune_archives(args.verbosity, repository, retention, remote_path=remote_path)
if args.create:
create.create_archive(
args.verbosity,
repository,
location,
storage,
)
if args.check:
check.check_archives(args.verbosity, repository, consistency, remote_path=remote_path)
for repository in location['repositories']:
if args.prune:
prune.prune_archives(args.verbosity, repository, retention, remote_path=remote_path)
if args.create:
create.create_archive(
args.verbosity,
repository,
location,
storage,
)
if args.check:
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:
print(error, file=sys.stderr)
sys.exit(1)

View File

@ -0,0 +1,7 @@
import subprocess
def execute_hook(commands):
if commands:
for cmd in commands:
subprocess.check_call(cmd, shell=True)

View File

@ -24,7 +24,7 @@ def _schema_to_sample_configuration(schema, level=0):
for each section based on the schema "desc" description.
'''
example = schema.get('example')
if example:
if example is not None:
return example
config = yaml.comments.CommentedMap([

View File

@ -157,3 +157,28 @@ map:
desc: Restrict the number of checked archives to the last n. Applies only to the
"archives" check.
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."

View File

@ -48,7 +48,7 @@ def parse_configuration(config_filename, schema_filename):
# simply remove all examples before passing the schema to pykwalify.
for section_name, section_schema in 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)
parsed_result = validator.validate(raise_exception=False)

View 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([':'])