Compare commits

...
11 Commits
15 changed files with 139 additions and 64 deletions
+1
View File
@@ -3,3 +3,4 @@ syntax: glob
*.pyc
*.swp
.tox
dist
+5
View File
@@ -14,3 +14,8 @@ cf4c7065f0711deda1cba878398bc05390e2c3f9 0.0.7
ac5dfa01e9d14d09845f5e94c2c679e21c5eb2f9 0.1.1
ac5dfa01e9d14d09845f5e94c2c679e21c5eb2f9 0.1.1
7b6c87dca7ea312b2257ac1b46857b3f8c56b39c 0.1.1
83067f995dd391e38544a7722dc3b254b59c5521 0.1.2
acc7fb61566fe8028c179f43ecc735c851220b06 0.1.3
6dda59c12de88f060eb7244e6d330173985a9639 0.1.4
6dda59c12de88f060eb7244e6d330173985a9639 0.1.4
e58246fc92bb22c2b2fd8b86a1227de69d2d0315 0.1.4
+12
View File
@@ -1,3 +1,15 @@
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.
0.1.3
* #1: Add support for "borg check --last N" to Borg backend.
0.1.2
* As a convenience to new users, allow a missing default excludes file.
+1 -3
View File
@@ -1,6 +1,4 @@
title: Atticmatic
date:
save_as: atticmatic/index.html
## Overview
@@ -56,7 +54,7 @@ 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:
+1 -1
View File
@@ -5,7 +5,7 @@ from atticmatic.backends import shared
# An atticmatic backend that supports Attic for actually handling backups.
COMMAND = 'attic'
CONFIG_FORMAT = shared.CONFIG_FORMAT
create_archive = partial(shared.create_archive, command=COMMAND)
prune_archives = partial(shared.prune_archives, command=COMMAND)
+12 -1
View File
@@ -1,13 +1,24 @@
from functools import partial
from atticmatic.config import Section_format, option
from atticmatic.backends import shared
# An atticmatic backend that supports Borg for actually handling backups.
COMMAND = 'borg'
CONFIG_FORMAT = (
shared.CONFIG_FORMAT[0], # location
shared.CONFIG_FORMAT[1], # retention
Section_format(
'consistency',
(
option('checks', required=False),
option('check_last', required=False),
),
)
)
create_archive = partial(shared.create_archive, command=COMMAND)
prune_archives = partial(shared.prune_archives, command=COMMAND)
check_archives = partial(shared.check_archives, command=COMMAND)
+38 -4
View File
@@ -3,6 +3,7 @@ import os
import platform
import subprocess
from atticmatic.config import Section_format, option
from atticmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
@@ -12,6 +13,34 @@ from atticmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
# atticmatic.backends.borg.
CONFIG_FORMAT = (
Section_format(
'location',
(
option('source_directories'),
option('repository'),
),
),
Section_format(
'retention',
(
option('keep_within', required=False),
option('keep_hourly', int, required=False),
option('keep_daily', int, required=False),
option('keep_weekly', int, required=False),
option('keep_monthly', int, required=False),
option('keep_yearly', int, required=False),
option('prefix', required=False),
),
),
Section_format(
'consistency',
(
option('checks', required=False),
),
)
)
def create_archive(excludes_filename, verbosity, source_directories, repository, command):
'''
Given an excludes filename (or None), a vebosity flag, a space-separated list of source
@@ -110,7 +139,7 @@ def _parse_checks(consistency_config):
)
def _make_check_flags(checks):
def _make_check_flags(checks, check_last=None):
'''
Given a parsed sequence of checks, transform it into tuple of command-line flags.
@@ -121,13 +150,17 @@ def _make_check_flags(checks):
This will be returned as:
('--repository-only',)
Additionally, if a check_last value is given, a "--last" flag will be added. Note that only
Borg supports this flag.
'''
last_flag = ('--last', check_last) if check_last else ()
if checks == DEFAULT_CHECKS:
return ()
return last_flag
return tuple(
'--{}-only'.format(check) for check in checks
)
) + last_flag
def check_archives(verbosity, repository, consistency_config, command):
@@ -138,6 +171,7 @@ def check_archives(verbosity, repository, consistency_config, command):
If there are no consistency checks to run, skip running them.
'''
checks = _parse_checks(consistency_config)
check_last = consistency_config.get('check_last', None)
if not checks:
return
@@ -149,7 +183,7 @@ def check_archives(verbosity, repository, consistency_config, command):
full_command = (
command, 'check',
repository,
) + _make_check_flags(checks) + verbosity_flags
) + _make_check_flags(checks, check_last) + verbosity_flags
# The check command spews to stdout even without the verbose flag. Suppress it.
stdout = None if verbosity_flags else open(os.devnull, 'w')
+2 -2
View File
@@ -60,9 +60,9 @@ def main():
try:
command_name = os.path.basename(sys.argv[0])
args = parse_arguments(command_name, *sys.argv[1:])
config = parse_configuration(args.config_filename)
repository = config.location['repository']
backend = load_backend(command_name)
config = parse_configuration(args.config_filename, backend.CONFIG_FORMAT)
repository = config.location['repository']
backend.create_archive(args.excludes_filename, args.verbosity, **config.location)
backend.prune_archives(args.verbosity, repository, config.retention)
+9 -39
View File
@@ -20,35 +20,6 @@ def option(name, value_type=str, required=True):
return Config_option(name, value_type, required)
CONFIG_FORMAT = (
Section_format(
'location',
(
option('source_directories'),
option('repository'),
),
),
Section_format(
'retention',
(
option('keep_within', required=False),
option('keep_hourly', int, required=False),
option('keep_daily', int, required=False),
option('keep_weekly', int, required=False),
option('keep_monthly', int, required=False),
option('keep_yearly', int, required=False),
option('prefix', required=False),
),
),
Section_format(
'consistency',
(
option('checks', required=False),
),
)
)
def validate_configuration_format(parser, config_format):
'''
Given an open ConfigParser and an expected config file format, validate that the parsed
@@ -110,11 +81,6 @@ def validate_configuration_format(parser, config_format):
)
# Describes a parsed configuration, where each attribute is the name of a configuration file section
# and each value is a dict of that section's parsed options.
Parsed_config = namedtuple('Config', (section_format.name for section_format in CONFIG_FORMAT))
def parse_section_options(parser, section_format):
'''
Given an open ConfigParser and an expected section format, return the option values from that
@@ -135,21 +101,25 @@ def parse_section_options(parser, section_format):
)
def parse_configuration(config_filename):
def parse_configuration(config_filename, config_format):
'''
Given a config filename of the expected format, return the parsed configuration as Parsed_config
data structure.
Given a config filename and an expected config file format, return the parsed configuration
as a namedtuple with one attribute for each parsed section.
Raise IOError if the file cannot be read, or ValueError if the format is not as expected.
'''
parser = ConfigParser()
parser.read(config_filename)
validate_configuration_format(parser, CONFIG_FORMAT)
validate_configuration_format(parser, config_format)
# Describes a parsed configuration, where each attribute is the name of a configuration file
# section and each value is a dict of that section's parsed options.
Parsed_config = namedtuple('Parsed_config', (section_format.name for section_format in config_format))
return Parsed_config(
*(
parse_section_options(parser, section_format)
for section_format in CONFIG_FORMAT
for section_format in config_format
)
)
@@ -0,0 +1,8 @@
import subprocess
def test_setup_version_matches_news_version():
setup_version = subprocess.check_output(('python', 'setup.py', '--version')).decode('ascii')
news_version = open('NEWS').readline()
assert setup_version == news_version
+20 -6
View File
@@ -196,10 +196,24 @@ def test_make_check_flags_with_default_checks_returns_no_flags():
assert flags == ()
def test_make_check_flags_with_checks_and_last_returns_flags_including_last():
flags = module._make_check_flags(('foo', 'bar'), check_last=3)
assert flags == ('--foo-only', '--bar-only', '--last', 3)
def test_make_check_flags_with_last_returns_last_flag():
flags = module._make_check_flags(module.DEFAULT_CHECKS, check_last=3)
assert flags == ('--last', 3)
def test_check_archives_should_call_attic_with_parameters():
consistency_config = flexmock()
flexmock(module).should_receive('_parse_checks').and_return(flexmock())
flexmock(module).should_receive('_make_check_flags').and_return(())
checks = flexmock()
check_last = flexmock()
consistency_config = flexmock().should_receive('get').and_return(check_last).mock
flexmock(module).should_receive('_parse_checks').and_return(checks)
flexmock(module).should_receive('_make_check_flags').with_args(checks, check_last).and_return(())
stdout = flexmock()
insert_subprocess_mock(
('attic', 'check', 'repo'),
@@ -219,7 +233,7 @@ def test_check_archives_should_call_attic_with_parameters():
def test_check_archives_with_verbosity_some_should_call_attic_with_verbose_parameter():
consistency_config = flexmock()
consistency_config = flexmock().should_receive('get').and_return(None).mock
flexmock(module).should_receive('_parse_checks').and_return(flexmock())
flexmock(module).should_receive('_make_check_flags').and_return(())
insert_subprocess_mock(
@@ -238,7 +252,7 @@ def test_check_archives_with_verbosity_some_should_call_attic_with_verbose_param
def test_check_archives_with_verbosity_lots_should_call_attic_with_verbose_parameter():
consistency_config = flexmock()
consistency_config = flexmock().should_receive('get').and_return(None).mock
flexmock(module).should_receive('_parse_checks').and_return(flexmock())
flexmock(module).should_receive('_make_check_flags').and_return(())
insert_subprocess_mock(
@@ -257,7 +271,7 @@ def test_check_archives_with_verbosity_lots_should_call_attic_with_verbose_param
def test_check_archives_without_any_checks_should_bail():
consistency_config = flexmock()
consistency_config = flexmock().should_receive('get').and_return(None).mock
flexmock(module).should_receive('_parse_checks').and_return(())
insert_subprocess_never()
+6 -5
View File
@@ -205,17 +205,18 @@ def insert_mock_parser():
def test_parse_configuration_should_return_section_configs():
parser = insert_mock_parser()
config_format = (flexmock(name='items'), flexmock(name='things'))
mock_module = flexmock(module)
mock_module.should_receive('validate_configuration_format').with_args(
parser, module.CONFIG_FORMAT,
parser, config_format,
).once()
mock_section_configs = (flexmock(),) * len(module.CONFIG_FORMAT)
mock_section_configs = (flexmock(), flexmock())
for section_format, section_config in zip(module.CONFIG_FORMAT, mock_section_configs):
for section_format, section_config in zip(config_format, mock_section_configs):
mock_module.should_receive('parse_section_options').with_args(
parser, section_format,
).and_return(section_config).once()
parsed_config = module.parse_configuration('filename')
parsed_config = module.parse_configuration('filename', config_format)
assert parsed_config == module.Parsed_config(*mock_section_configs)
assert parsed_config == type(parsed_config)(*mock_section_configs)
+5 -2
View File
@@ -9,7 +9,7 @@ repository: user@backupserver:sourcehostname.attic
# Retention policy for how many backups to keep in each category. See
# https://attic-backup.org/usage.html#attic-prune or
# https://borgbackup.github.io/borgbackup/usage.html#borg-prune for details.
#keep_within: 3h
#keep_within: 3H
#keep_hourly: 24
keep_daily: 7
keep_weekly: 4
@@ -20,5 +20,8 @@ keep_yearly: 1
[consistency]
# Space-separated list of consistency checks to run: "repository", "archives",
# or both. Defaults to both. Set to "disabled" to disable all consistency
# checks. See https://attic-backup.org/usage.html#attic-check for details.
# checks. See https://attic-backup.org/usage.html#attic-check or
# https://borgbackup.github.io/borgbackup/usage.html#borg-check for details.
checks: repository archives
# For Borg only, you can restrict the number of checked archives to the last n.
#check_last: 3
+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.5'
setup(
name='atticmatic',
version='0.1.1',
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': [