From 3a9e32a4119a0a26b2c4679e22f13af56700c4a6 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Wed, 2 Sep 2015 22:48:07 -0700 Subject: [PATCH] #9: New configuration option for the encryption passphrase. #10: Support for Borg's new archive compression feature. --- NEWS | 5 +++ README.md | 6 +-- atticmatic/backends/attic.py | 2 + atticmatic/backends/borg.py | 10 ++++- atticmatic/backends/shared.py | 27 ++++++++++-- atticmatic/command.py | 5 ++- atticmatic/tests/unit/backends/test_shared.py | 42 +++++++++++++++++++ sample/config | 9 ++++ setup.py | 2 +- 9 files changed, 98 insertions(+), 10 deletions(-) diff --git a/NEWS b/NEWS index 482da6b..d518502 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,8 @@ +0.1.6 + + * #9: New configuration option for the encryption passphrase. + * #10: Support for Borg's new archive compression feature. + 0.1.5 * Changes to support release on PyPI. Now pip installable by name! diff --git a/README.md b/README.md index 246b86a..3d9d999 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,9 @@ Start](https://attic-backup.org/quickstart.html) or the [Borg Quick Start](https://borgbackup.github.io/borgbackup/quickstart.html) to create a repository on a local or remote host. Note that if you plan to run atticmatic on a schedule with cron, and you encrypt your attic repository with a -passphrase instead of a key file, you'll need to set the `ATTIC_PASSPHRASE` -environment variable. See the repository encryption section of the Quick Start -for more info. +passphrase instead of a key file, you'll need to set the atticmatic +`encryption_passphrase` configuration variable. See the repository encryption +section of the Quick Start for more info. If the repository is on a remote host, make sure that your local root user has key-based ssh access to the desired user account on the remote host. diff --git a/atticmatic/backends/attic.py b/atticmatic/backends/attic.py index fcacce3..daa0b29 100644 --- a/atticmatic/backends/attic.py +++ b/atticmatic/backends/attic.py @@ -7,6 +7,8 @@ from atticmatic.backends import shared COMMAND = 'attic' CONFIG_FORMAT = shared.CONFIG_FORMAT + +initialize = partial(shared.initialize, command=COMMAND) create_archive = partial(shared.create_archive, command=COMMAND) prune_archives = partial(shared.prune_archives, command=COMMAND) check_archives = partial(shared.check_archives, command=COMMAND) diff --git a/atticmatic/backends/borg.py b/atticmatic/backends/borg.py index bd6a386..222c702 100644 --- a/atticmatic/backends/borg.py +++ b/atticmatic/backends/borg.py @@ -8,7 +8,14 @@ from atticmatic.backends import shared COMMAND = 'borg' CONFIG_FORMAT = ( shared.CONFIG_FORMAT[0], # location - shared.CONFIG_FORMAT[1], # retention + Section_format( + 'storage', + ( + option('encryption_passphrase', required=False), + option('compression', required=False), + ), + ), + shared.CONFIG_FORMAT[2], # retention Section_format( 'consistency', ( @@ -19,6 +26,7 @@ CONFIG_FORMAT = ( ) +initialize = partial(shared.initialize, command=COMMAND) create_archive = partial(shared.create_archive, command=COMMAND) prune_archives = partial(shared.prune_archives, command=COMMAND) check_archives = partial(shared.check_archives, command=COMMAND) diff --git a/atticmatic/backends/shared.py b/atticmatic/backends/shared.py index 74f5533..5baff3a 100644 --- a/atticmatic/backends/shared.py +++ b/atticmatic/backends/shared.py @@ -21,6 +21,12 @@ CONFIG_FORMAT = ( option('repository'), ), ), + Section_format( + 'storage', + ( + option('encryption_passphrase', required=False), + ), + ), Section_format( 'retention', ( @@ -41,13 +47,26 @@ CONFIG_FORMAT = ( ) ) -def create_archive(excludes_filename, verbosity, source_directories, repository, command): + +def initialize(storage_config, command): + passphrase = storage_config.get('encryption_passphrase') + + if passphrase: + os.environ['{}_PASSPHRASE'.format(command.upper())] = passphrase + + +def create_archive( + excludes_filename, verbosity, storage_config, source_directories, repository, command, +): ''' - Given an excludes filename (or None), a vebosity flag, a space-separated list of source - directories, a local or remote repository path, and a command to run, create an attic archive. + Given an excludes filename (or None), a vebosity flag, a storage config dict, a space-separated + list of source directories, a local or remote repository path, and a command to run, create an + attic archive. ''' sources = tuple(source_directories.split(' ')) exclude_flags = ('--exclude-from', excludes_filename) if excludes_filename else () + compression = storage_config.get('compression', None) + compression_flags = ('--compression', compression) if compression else () verbosity_flags = { VERBOSITY_SOME: ('--stats',), VERBOSITY_LOTS: ('--verbose', '--stats'), @@ -60,7 +79,7 @@ def create_archive(excludes_filename, verbosity, source_directories, repository, hostname=platform.node(), timestamp=datetime.now().isoformat(), ), - ) + sources + exclude_flags + verbosity_flags + ) + sources + exclude_flags + compression_flags + verbosity_flags subprocess.check_call(full_command) diff --git a/atticmatic/command.py b/atticmatic/command.py index 0f512e1..08ea49e 100644 --- a/atticmatic/command.py +++ b/atticmatic/command.py @@ -64,7 +64,10 @@ def main(): config = parse_configuration(args.config_filename, backend.CONFIG_FORMAT) repository = config.location['repository'] - backend.create_archive(args.excludes_filename, args.verbosity, **config.location) + backend.initialize(config.storage) + backend.create_archive( + args.excludes_filename, args.verbosity, config.storage, **config.location + ) backend.prune_archives(args.verbosity, repository, config.retention) backend.check_archives(args.verbosity, repository, config.consistency) except (ValueError, IOError, CalledProcessError) as error: diff --git a/atticmatic/tests/unit/backends/test_shared.py b/atticmatic/tests/unit/backends/test_shared.py index c742087..6449231 100644 --- a/atticmatic/tests/unit/backends/test_shared.py +++ b/atticmatic/tests/unit/backends/test_shared.py @@ -1,4 +1,5 @@ from collections import OrderedDict +import os from flexmock import flexmock @@ -7,6 +8,28 @@ from atticmatic.tests.builtins import builtins_mock from atticmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS +def test_initialize_with_passphrase_should_set_environment(): + orig_environ = os.environ + + try: + os.environ = {} + module.initialize({'encryption_passphrase': 'pass'}, command='attic') + assert os.environ.get('ATTIC_PASSPHRASE') == 'pass' + finally: + os.environ = orig_environ + + +def test_initialize_without_passphrase_should_not_set_environment(): + orig_environ = os.environ + + try: + os.environ = {} + module.initialize({}, command='attic') + assert os.environ.get('ATTIC_PASSPHRASE') == None + finally: + os.environ = orig_environ + + def insert_subprocess_mock(check_call_command, **kwargs): subprocess = flexmock() subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once() @@ -41,6 +64,7 @@ def test_create_archive_should_call_attic_with_parameters(): module.create_archive( excludes_filename='excludes', verbosity=None, + storage_config={}, source_directories='foo bar', repository='repo', command='attic', @@ -55,6 +79,7 @@ def test_create_archive_with_none_excludes_filename_should_call_attic_without_ex module.create_archive( excludes_filename=None, verbosity=None, + storage_config={}, source_directories='foo bar', repository='repo', command='attic', @@ -69,6 +94,7 @@ def test_create_archive_with_verbosity_some_should_call_attic_with_stats_paramet module.create_archive( excludes_filename='excludes', verbosity=VERBOSITY_SOME, + storage_config={}, source_directories='foo bar', repository='repo', command='attic', @@ -83,6 +109,22 @@ def test_create_archive_with_verbosity_lots_should_call_attic_with_verbose_param module.create_archive( excludes_filename='excludes', verbosity=VERBOSITY_LOTS, + storage_config={}, + source_directories='foo bar', + repository='repo', + command='attic', + ) + + +def test_create_archive_with_compression_should_call_attic_with_compression_parameters(): + insert_subprocess_mock(CREATE_COMMAND + ('--compression', 'rle')) + insert_platform_mock() + insert_datetime_mock() + + module.create_archive( + excludes_filename='excludes', + verbosity=None, + storage_config={'compression': 'rle'}, source_directories='foo bar', repository='repo', command='attic', diff --git a/sample/config b/sample/config index 82d77d1..cf4b391 100644 --- a/sample/config +++ b/sample/config @@ -5,6 +5,15 @@ source_directories: /home /etc # Path to local or remote repository. repository: user@backupserver:sourcehostname.attic +[storage] +# Passphrase to unlock the encryption key with. Only use on repositories that +# were initialized with passphrase/repokey encryption. +#encryption_passphrase: foo +# For Borg only, you can specify the type of compression to use when creating +# archives. See https://borgbackup.github.io/borgbackup/usage.html#borg-create +# for details. Defaults to no compression. +#compression: lz4 + [retention] # Retention policy for how many backups to keep in each category. See # https://attic-backup.org/usage.html#attic-prune or diff --git a/setup.py b/setup.py index ad32a14..73a7796 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages -VERSION = '0.1.5' +VERSION = '0.1.6' setup(