This commit is contained in:
Dan Helfman 2017-10-25 21:54:50 -07:00
commit a5aa9355f5
8 changed files with 78 additions and 20 deletions

View file

@ -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

View file

@ -66,6 +66,12 @@ To install borgmatic, run the following command to download and install it:
Note that your pip binary may have a different name than "pip3". Make sure Note that your pip binary may have a different name than "pip3". Make sure
you're using Python 3, as borgmatic does not support Python 2. you're using Python 3, as borgmatic does not support Python 2.
### Docker
If you would like to run borgmatic within Docker, please take a look at
[b3vis/borgmatic](https://hub.docker.com/r/b3vis/borgmatic/) for more
information.
## Configuration ## Configuration
After you install borgmatic, generate a sample configuration file: After you install borgmatic, generate a sample configuration file:

View file

@ -1,3 +1,4 @@
from argparse import ArgumentParser from argparse import ArgumentParser
import logging import logging
import os import os
@ -5,6 +6,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
from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS, verbosity_to_log_level from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS, verbosity_to_log_level
@ -92,13 +94,15 @@ def main(): # pragma: no cover
for config_filename in config_filenames: for config_filename in config_filenames:
logger.info('{}: Parsing configuration file'.format(config_filename)) logger.info('{}: Parsing configuration file'.format(config_filename))
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:
@ -115,6 +119,11 @@ def main(): # pragma: no cover
if args.check: if args.check:
logger.info('{}: Running consistency checks'.format(repository)) logger.info('{}: Running consistency checks'.format(repository))
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)

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. 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([

View file

@ -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."

View file

@ -50,7 +50,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)

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