Compare commits

..
10 Commits
15 changed files with 181 additions and 20 deletions
+2
View File
@@ -3,3 +3,5 @@ syntax: glob
*.pyc
*.swp
.tox
build
dist
+4
View File
@@ -17,3 +17,7 @@ ac5dfa01e9d14d09845f5e94c2c679e21c5eb2f9 0.1.1
83067f995dd391e38544a7722dc3b254b59c5521 0.1.2
acc7fb61566fe8028c179f43ecc735c851220b06 0.1.3
6dda59c12de88f060eb7244e6d330173985a9639 0.1.4
6dda59c12de88f060eb7244e6d330173985a9639 0.1.4
e58246fc92bb22c2b2fd8b86a1227de69d2d0315 0.1.4
0afff209b902698c2266986129d6dc9f5f913101 0.1.5
4c63f3d90ec2bf6af1714a3acec84654a7c9edf3 0.1.6
+14
View File
@@ -1,3 +1,17 @@
0.1.7
* #11: Fixed parsing of punctuation in configuration file.
* Better error message when configuration file is missing.
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!
0.1.4
* Adding test that setup.py version matches release version.
+7 -5
View File
@@ -45,16 +45,16 @@ 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.
To install atticmatic, run the following command to download and install it:
sudo pip install --upgrade hg+https://torsion.org/hg/atticmatic
sudo pip install --upgrade atticmatic
If you are using Attic, copy the following configuration files:
@@ -136,6 +136,8 @@ backups.
## Issues and feedback
Got an issue or an idea for a feature enhancement? Check out the [atticmatic
issue tracker](https://tree.taiga.io/project/witten-atticmatic/issues).
issue tracker](https://tree.taiga.io/project/witten-atticmatic/issues). In
order to create a new issue or comment on an issue, you'll need to [login
first](https://tree.taiga.io/login).
Other questions or comments? Contact <mailto:witten@torsion.org>.
+2
View File
@@ -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)
+9 -1
View File
@@ -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)
+23 -4
View File
@@ -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)
+4 -1
View File
@@ -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:
+7 -6
View File
@@ -2,10 +2,10 @@ from collections import OrderedDict, namedtuple
try:
# Python 2
from ConfigParser import ConfigParser
from ConfigParser import RawConfigParser
except ImportError:
# Python 3
from configparser import ConfigParser
from configparser import RawConfigParser
Section_format = namedtuple('Section_format', ('name', 'options'))
@@ -22,7 +22,7 @@ def option(name, value_type=str, required=True):
def validate_configuration_format(parser, config_format):
'''
Given an open ConfigParser and an expected config file format, validate that the parsed
Given an open RawConfigParser and an expected config file format, validate that the parsed
configuration file has the expected sections, that any required options are present in those
sections, and that there aren't any unexpected options.
@@ -83,7 +83,7 @@ def validate_configuration_format(parser, config_format):
def parse_section_options(parser, section_format):
'''
Given an open ConfigParser and an expected section format, return the option values from that
Given an open RawConfigParser and an expected section format, return the option values from that
section as a dict mapping from option name to value. Omit those options that are not present in
the parsed options.
@@ -108,8 +108,9 @@ def parse_configuration(config_filename, config_format):
Raise IOError if the file cannot be read, or ValueError if the format is not as expected.
'''
parser = ConfigParser()
parser.read(config_filename)
parser = RawConfigParser()
if not parser.read(config_filename):
raise ValueError('Configuration file cannot be opened: {}'.format(config_filename))
validate_configuration_format(parser, config_format)
@@ -0,0 +1,29 @@
try:
# Python 2
from cStringIO import StringIO
except ImportError:
# Python 3
from io import StringIO
from collections import OrderedDict
import string
from atticmatic import config as module
def test_parse_section_options_with_punctuation_should_return_section_options():
parser = module.RawConfigParser()
parser.readfp(StringIO('[section]\nfoo: {}\n'.format(string.punctuation)))
section_format = module.Section_format(
'section',
(module.Config_option('foo', str, required=True),),
)
config = module.parse_section_options(parser, section_format)
assert config == OrderedDict(
(
('foo', string.punctuation),
)
)
@@ -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',
+10 -2
View File
@@ -197,8 +197,8 @@ def test_parse_section_options_for_missing_section_should_return_empty_dict():
def insert_mock_parser():
parser = flexmock()
parser.should_receive('read')
module.ConfigParser = lambda: parser
parser.should_receive('read').and_return([flexmock()])
module.RawConfigParser = lambda: parser
return parser
@@ -220,3 +220,11 @@ def test_parse_configuration_should_return_section_configs():
parsed_config = module.parse_configuration('filename', config_format)
assert parsed_config == type(parsed_config)(*mock_section_configs)
def test_parse_configuration_with_file_open_error_should_raise():
parser = insert_mock_parser()
parser.should_receive('read').and_return([])
with assert_raises(ValueError):
module.parse_configuration('filename', config_format=flexmock())
+9
View File
@@ -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
+3
View File
@@ -1,2 +1,5 @@
[metadata]
description-file=README.md
[nosetests]
detailed-errors=1
+16 -1
View File
@@ -1,11 +1,26 @@
from setuptools import setup, find_packages
VERSION = '0.1.7'
setup(
name='atticmatic',
version='0.1.4',
version=VERSION,
description='A wrapper script for Attic/Borg backup software that creates and prunes backups',
author='Dan Helfman',
author_email='witten@torsion.org',
url='https://torsion.org/atticmatic',
download_url='https://torsion.org/hg/atticmatic/archive/%s.tar.gz' % VERSION,
classifiers=(
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Programming Language :: Python',
'Topic :: Security :: Cryptography',
'Topic :: System :: Archiving :: Backup',
),
packages=find_packages(),
entry_points={
'console_scripts': [