From aa48b95ee7dadac145e9bf513b38e440f9a7bb68 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Sun, 15 Mar 2015 09:52:40 -0700 Subject: [PATCH] Bumping setup.py version. --- NEWS | 1 + README.md | 31 ++++++----- atticmatic/attic.py | 12 ++++- atticmatic/command.py | 2 +- atticmatic/tests/unit/test_attic.py | 81 +++++++++++++++++++++++++---- setup.py | 2 +- 6 files changed, 104 insertions(+), 25 deletions(-) diff --git a/NEWS b/NEWS index c2a9dff..1464d2b 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,6 @@ 0.0.4-dev + * Helpful error message about how to create a repository if one is missing. * Added a troubleshooting section with steps to deal with broken pipes. * Added nosetests config file (setup.cfg) with defaults. diff --git a/README.md b/README.md index 825ca5e..cd4298d 100644 --- a/README.md +++ b/README.md @@ -37,18 +37,6 @@ available](https://torsion.org/hg/atticmatic). It's also mirrored on ## Setup -To get up and running with Attic, follow the [Attic Quick -Start](https://attic-backup.org/quickstart.html) guide to create an Attic -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 [attic's repository encryption -documentation](https://attic-backup.org/quickstart.html#encrypted-repos) 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 @@ -59,7 +47,24 @@ Then copy the following configuration files: sudo mkdir /etc/atticmatic/ sudo cp sample/config sample/excludes /etc/atticmatic/ -Lastly, modify those files with your desired configuration. +Modify those files with your desired configuration, including the path to an +attic repository. + +If you don't yet have an attic repository, then the first time you run +atticmatic, you'll get an error with information on how to create a repository +on a local or remote host. + +And 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. + +It is recommended that you create your attic repository with keyfile +encryption, as passphrase-based encryption is less suited for automated +backups. If you do 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 [attic's +repository encryption +documentation](https://attic-backup.org/quickstart.html#encrypted-repos) for +more info. ## Usage diff --git a/atticmatic/attic.py b/atticmatic/attic.py index d0c678d..f0c9e7e 100644 --- a/atticmatic/attic.py +++ b/atticmatic/attic.py @@ -1,7 +1,10 @@ +from __future__ import print_function from datetime import datetime import os import platform +import re import subprocess +import sys def create_archive(excludes_filename, verbose, source_directories, repository): @@ -23,7 +26,14 @@ def create_archive(excludes_filename, verbose, source_directories, repository): ('--verbose', '--stats') if verbose else () ) - subprocess.check_call(command) + try: + subprocess.check_output(command, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError, error: + print(error.output.strip(), file=sys.stderr) + + if re.search('Error: Repository .* does not exist', error.output): + raise RuntimeError('To create a repository, run: attic init --encryption=keyfile {}'.format(repository)) + raise error def make_prune_flags(retention_config): diff --git a/atticmatic/command.py b/atticmatic/command.py index 92976f0..e0f4eb1 100644 --- a/atticmatic/command.py +++ b/atticmatic/command.py @@ -46,6 +46,6 @@ def main(): create_archive(args.excludes_filename, args.verbose, **location_config) prune_archives(args.verbose, repository, retention_config) check_archives(args.verbose, repository) - except (ValueError, IOError, CalledProcessError) as error: + except (ValueError, IOError, CalledProcessError, RuntimeError) as error: print(error, file=sys.stderr) sys.exit(1) diff --git a/atticmatic/tests/unit/test_attic.py b/atticmatic/tests/unit/test_attic.py index 2c93e8c..30431cc 100644 --- a/atticmatic/tests/unit/test_attic.py +++ b/atticmatic/tests/unit/test_attic.py @@ -1,14 +1,44 @@ from collections import OrderedDict +import sys from flexmock import flexmock +from nose.tools import assert_raises from atticmatic import attic as module -def insert_subprocess_mock(check_call_command, **kwargs): - subprocess = flexmock() - subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once() +class MockCalledProcessError(Exception): + def __init__(self, output): + self.output = output + + +def insert_subprocess_check_output_mock(call_command, error_output=None, **kwargs): + subprocess = flexmock(CalledProcessError=MockCalledProcessError, STDOUT=flexmock()) + + expectation = subprocess.should_receive('check_output').with_args( + call_command, + stderr=subprocess.STDOUT, + **kwargs + ).once() + + if error_output: + expectation.and_raise(MockCalledProcessError, output=error_output) + flexmock(sys.modules['__builtin__']).should_receive('print') + flexmock(module).subprocess = subprocess + return subprocess + + +def insert_subprocess_check_call_mock(call_command, **kwargs): + subprocess = flexmock() + + subprocess.should_receive('check_call').with_args( + call_command, + **kwargs + ).once() + + flexmock(module).subprocess = subprocess + return subprocess def insert_platform_mock(): @@ -22,7 +52,7 @@ def insert_datetime_mock(): def test_create_archive_should_call_attic_with_parameters(): - insert_subprocess_mock( + insert_subprocess_check_output_mock( ('attic', 'create', '--exclude-from', 'excludes', 'repo::host-now', 'foo', 'bar'), ) insert_platform_mock() @@ -37,7 +67,7 @@ def test_create_archive_should_call_attic_with_parameters(): def test_create_archive_with_verbose_should_call_attic_with_verbose_parameters(): - insert_subprocess_mock( + insert_subprocess_check_output_mock( ( 'attic', 'create', '--exclude-from', 'excludes', 'repo::host-now', 'foo', 'bar', '--verbose', '--stats', @@ -53,6 +83,39 @@ def test_create_archive_with_verbose_should_call_attic_with_verbose_parameters() repository='repo', ) +def test_create_archive_with_missing_repository_should_raise(): + insert_subprocess_check_output_mock( + ('attic', 'create', '--exclude-from', 'excludes', 'repo::host-now', 'foo', 'bar'), + error_output='Error: Repository repo does not exist', + ) + insert_platform_mock() + insert_datetime_mock() + + with assert_raises(RuntimeError): + module.create_archive( + excludes_filename='excludes', + verbose=False, + source_directories='foo bar', + repository='repo', + ) + + +def test_create_archive_with_other_error_should_raise(): + subprocess = insert_subprocess_check_output_mock( + ('attic', 'create', '--exclude-from', 'excludes', 'repo::host-now', 'foo', 'bar'), + error_output='Something went wrong', + ) + insert_platform_mock() + insert_datetime_mock() + + with assert_raises(subprocess.CalledProcessError): + module.create_archive( + excludes_filename='excludes', + verbose=False, + source_directories='foo bar', + repository='repo', + ) + BASE_PRUNE_FLAGS = ( ('--keep-daily', '1'), @@ -80,7 +143,7 @@ def test_prune_archives_should_call_attic_with_parameters(): flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS, ) - insert_subprocess_mock( + insert_subprocess_check_call_mock( ( 'attic', 'prune', 'repo', '--keep-daily', '1', '--keep-weekly', '2', '--keep-monthly', '3', @@ -99,7 +162,7 @@ def test_prune_archives_with_verbose_should_call_attic_with_verbose_parameters() flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS, ) - insert_subprocess_mock( + insert_subprocess_check_call_mock( ( 'attic', 'prune', 'repo', '--keep-daily', '1', '--keep-weekly', '2', '--keep-monthly', '3', '--verbose', @@ -115,7 +178,7 @@ def test_prune_archives_with_verbose_should_call_attic_with_verbose_parameters() def test_check_archives_should_call_attic_with_parameters(): stdout = flexmock() - insert_subprocess_mock( + insert_subprocess_check_call_mock( ('attic', 'check', 'repo'), stdout=stdout, ) @@ -131,7 +194,7 @@ def test_check_archives_should_call_attic_with_parameters(): def test_check_archives_with_verbose_should_call_attic_with_verbose_parameters(): - insert_subprocess_mock( + insert_subprocess_check_call_mock( ('attic', 'check', 'repo', '--verbose'), stdout=None, ) diff --git a/setup.py b/setup.py index e737112..9bcd56d 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name='atticmatic', - version='0.0.2', + version='0.0.4', description='A wrapper script for Attic backup software that creates and prunes backups', author='Dan Helfman', author_email='witten@torsion.org',