#9: New configuration option for the encryption passphrase. #10: Support for Borg's new archive compression feature.
This commit is contained in:
parent
178e56e77b
commit
a44212ff00
9 changed files with 98 additions and 10 deletions
5
NEWS
5
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
|
0.1.5
|
||||||
|
|
||||||
* Changes to support release on PyPI. Now pip installable by name!
|
* Changes to support release on PyPI. Now pip installable by name!
|
||||||
|
|
|
@ -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
|
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
|
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
|
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`
|
passphrase instead of a key file, you'll need to set the atticmatic
|
||||||
environment variable. See the repository encryption section of the Quick Start
|
`encryption_passphrase` configuration variable. See the repository encryption
|
||||||
for more info.
|
section of the Quick Start for more info.
|
||||||
|
|
||||||
If the repository is on a remote host, make sure that your local root user has
|
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.
|
key-based ssh access to the desired user account on the remote host.
|
||||||
|
|
|
@ -7,6 +7,8 @@ from atticmatic.backends import shared
|
||||||
COMMAND = 'attic'
|
COMMAND = 'attic'
|
||||||
CONFIG_FORMAT = shared.CONFIG_FORMAT
|
CONFIG_FORMAT = shared.CONFIG_FORMAT
|
||||||
|
|
||||||
|
|
||||||
|
initialize = partial(shared.initialize, command=COMMAND)
|
||||||
create_archive = partial(shared.create_archive, command=COMMAND)
|
create_archive = partial(shared.create_archive, command=COMMAND)
|
||||||
prune_archives = partial(shared.prune_archives, command=COMMAND)
|
prune_archives = partial(shared.prune_archives, command=COMMAND)
|
||||||
check_archives = partial(shared.check_archives, command=COMMAND)
|
check_archives = partial(shared.check_archives, command=COMMAND)
|
||||||
|
|
|
@ -8,7 +8,14 @@ from atticmatic.backends import shared
|
||||||
COMMAND = 'borg'
|
COMMAND = 'borg'
|
||||||
CONFIG_FORMAT = (
|
CONFIG_FORMAT = (
|
||||||
shared.CONFIG_FORMAT[0], # location
|
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(
|
Section_format(
|
||||||
'consistency',
|
'consistency',
|
||||||
(
|
(
|
||||||
|
@ -19,6 +26,7 @@ CONFIG_FORMAT = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
initialize = partial(shared.initialize, command=COMMAND)
|
||||||
create_archive = partial(shared.create_archive, command=COMMAND)
|
create_archive = partial(shared.create_archive, command=COMMAND)
|
||||||
prune_archives = partial(shared.prune_archives, command=COMMAND)
|
prune_archives = partial(shared.prune_archives, command=COMMAND)
|
||||||
check_archives = partial(shared.check_archives, command=COMMAND)
|
check_archives = partial(shared.check_archives, command=COMMAND)
|
||||||
|
|
|
@ -21,6 +21,12 @@ CONFIG_FORMAT = (
|
||||||
option('repository'),
|
option('repository'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Section_format(
|
||||||
|
'storage',
|
||||||
|
(
|
||||||
|
option('encryption_passphrase', required=False),
|
||||||
|
),
|
||||||
|
),
|
||||||
Section_format(
|
Section_format(
|
||||||
'retention',
|
'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
|
Given an excludes filename (or None), a vebosity flag, a storage config dict, a space-separated
|
||||||
directories, a local or remote repository path, and a command to run, create an attic archive.
|
list of source directories, a local or remote repository path, and a command to run, create an
|
||||||
|
attic archive.
|
||||||
'''
|
'''
|
||||||
sources = tuple(source_directories.split(' '))
|
sources = tuple(source_directories.split(' '))
|
||||||
exclude_flags = ('--exclude-from', excludes_filename) if excludes_filename else ()
|
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_flags = {
|
||||||
VERBOSITY_SOME: ('--stats',),
|
VERBOSITY_SOME: ('--stats',),
|
||||||
VERBOSITY_LOTS: ('--verbose', '--stats'),
|
VERBOSITY_LOTS: ('--verbose', '--stats'),
|
||||||
|
@ -60,7 +79,7 @@ def create_archive(excludes_filename, verbosity, source_directories, repository,
|
||||||
hostname=platform.node(),
|
hostname=platform.node(),
|
||||||
timestamp=datetime.now().isoformat(),
|
timestamp=datetime.now().isoformat(),
|
||||||
),
|
),
|
||||||
) + sources + exclude_flags + verbosity_flags
|
) + sources + exclude_flags + compression_flags + verbosity_flags
|
||||||
|
|
||||||
subprocess.check_call(full_command)
|
subprocess.check_call(full_command)
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,10 @@ def main():
|
||||||
config = parse_configuration(args.config_filename, backend.CONFIG_FORMAT)
|
config = parse_configuration(args.config_filename, backend.CONFIG_FORMAT)
|
||||||
repository = config.location['repository']
|
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.prune_archives(args.verbosity, repository, config.retention)
|
||||||
backend.check_archives(args.verbosity, repository, config.consistency)
|
backend.check_archives(args.verbosity, repository, config.consistency)
|
||||||
except (ValueError, IOError, CalledProcessError) as error:
|
except (ValueError, IOError, CalledProcessError) as error:
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
import os
|
||||||
|
|
||||||
from flexmock import flexmock
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
@ -7,6 +8,28 @@ from atticmatic.tests.builtins import builtins_mock
|
||||||
from atticmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
|
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):
|
def insert_subprocess_mock(check_call_command, **kwargs):
|
||||||
subprocess = flexmock()
|
subprocess = flexmock()
|
||||||
subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once()
|
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(
|
module.create_archive(
|
||||||
excludes_filename='excludes',
|
excludes_filename='excludes',
|
||||||
verbosity=None,
|
verbosity=None,
|
||||||
|
storage_config={},
|
||||||
source_directories='foo bar',
|
source_directories='foo bar',
|
||||||
repository='repo',
|
repository='repo',
|
||||||
command='attic',
|
command='attic',
|
||||||
|
@ -55,6 +79,7 @@ def test_create_archive_with_none_excludes_filename_should_call_attic_without_ex
|
||||||
module.create_archive(
|
module.create_archive(
|
||||||
excludes_filename=None,
|
excludes_filename=None,
|
||||||
verbosity=None,
|
verbosity=None,
|
||||||
|
storage_config={},
|
||||||
source_directories='foo bar',
|
source_directories='foo bar',
|
||||||
repository='repo',
|
repository='repo',
|
||||||
command='attic',
|
command='attic',
|
||||||
|
@ -69,6 +94,7 @@ def test_create_archive_with_verbosity_some_should_call_attic_with_stats_paramet
|
||||||
module.create_archive(
|
module.create_archive(
|
||||||
excludes_filename='excludes',
|
excludes_filename='excludes',
|
||||||
verbosity=VERBOSITY_SOME,
|
verbosity=VERBOSITY_SOME,
|
||||||
|
storage_config={},
|
||||||
source_directories='foo bar',
|
source_directories='foo bar',
|
||||||
repository='repo',
|
repository='repo',
|
||||||
command='attic',
|
command='attic',
|
||||||
|
@ -83,6 +109,22 @@ def test_create_archive_with_verbosity_lots_should_call_attic_with_verbose_param
|
||||||
module.create_archive(
|
module.create_archive(
|
||||||
excludes_filename='excludes',
|
excludes_filename='excludes',
|
||||||
verbosity=VERBOSITY_LOTS,
|
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',
|
source_directories='foo bar',
|
||||||
repository='repo',
|
repository='repo',
|
||||||
command='attic',
|
command='attic',
|
||||||
|
|
|
@ -5,6 +5,15 @@ source_directories: /home /etc
|
||||||
# Path to local or remote repository.
|
# Path to local or remote repository.
|
||||||
repository: user@backupserver:sourcehostname.attic
|
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]
|
||||||
# Retention policy for how many backups to keep in each category. See
|
# Retention policy for how many backups to keep in each category. See
|
||||||
# https://attic-backup.org/usage.html#attic-prune or
|
# https://attic-backup.org/usage.html#attic-prune or
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -1,7 +1,7 @@
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
|
||||||
VERSION = '0.1.5'
|
VERSION = '0.1.6'
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
|
Loading…
Reference in a new issue