浏览代码

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

Johannes Feichtner 7 年之前
父节点
当前提交
80e2c023dd

+ 1 - 0
AUTHORS

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

+ 23 - 15
borgmatic/commands/borgmatic.py

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

+ 7 - 0
borgmatic/commands/hook.py

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

+ 1 - 1
borgmatic/config/generate.py

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

+ 25 - 0
borgmatic/config/schema.yaml

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

+ 1 - 1
borgmatic/config/validate.py

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

+ 10 - 0
borgmatic/tests/unit/borg/test_hook.py

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