Initial import.

This commit is contained in:
Dan Helfman 2014-10-30 22:34:03 -07:00
commit 6dff335c8b
10 changed files with 212 additions and 0 deletions

3
.hgignore Normal file
View File

@ -0,0 +1,3 @@
syntax: glob
*.pyc
*.egg-info

51
README Normal file
View File

@ -0,0 +1,51 @@
Overview
--------
atticmatic is a simple Python wrapper script for the Attic backup software
that initiates a backup and prunes any old backups according to a retention
policy. The script supports specifying your settings in a declarative
configuration file rather than having to put them all on the command-line, and
handles common errors.
Read more about Attic at https://attic-backup.org/
Setup
-----
To get up and running with Attic, follow the Attic Quick Start guide at
https://attic-backup.org/quickstart.html to create an Attic repository on a
local or remote host.
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 from the directory containing this
README:
python setup.py install
Then copy the following configuration files:
sudo cp sample/atticmatic.cron /etc/init.d/atticmatic
sudo cp sample/config sample/excludes /etc/atticmatic/
Lastly, modify those files with your desired configuration.
Usage
-----
You can run atticmatic and start a backup simply by invoking it without
arguments:
atticmatic
To get additional information about the progress of the backup, use the
verbose option:
atticmattic --verbose
If you'd like to see the available command-line arguments, view the help:
atticmattic --help

0
atticmatic/__init__.py Normal file
View File

34
atticmatic/attic.py Normal file
View File

@ -0,0 +1,34 @@
from datetime import datetime
import platform
import subprocess
def create_archive(excludes_filename, verbose, source_directories, repository):
sources = tuple(source_directories.split(' '))
command = (
'attic', 'create',
'--exclude-from', excludes_filename,
'{repo}::{hostname}-{timestamp}'.format(
repo=repository,
hostname=platform.node(),
timestamp=datetime.now().isoformat(),
),
) + sources + (
('--verbose', '--stats') if verbose else ()
)
subprocess.check_call(command)
def prune_archives(repository, verbose, keep_daily, keep_weekly, keep_monthly):
command = (
'attic', 'prune',
repository,
'--keep-daily', str(keep_daily),
'--keep-weekly', str(keep_weekly),
'--keep-monthly', str(keep_monthly),
) + (('--verbose',) if verbose else ())
subprocess.check_call(command)

38
atticmatic/command.py Normal file
View File

@ -0,0 +1,38 @@
from __future__ import print_function
from argparse import ArgumentParser
from subprocess import CalledProcessError
import sys
from atticmatic.attic import create_archive, prune_archives
from atticmatic.config import parse_configuration
def main():
parser = ArgumentParser()
parser.add_argument(
'--config',
dest='config_filename',
default='/etc/atticmatic/config',
help='Configuration filename',
)
parser.add_argument(
'--excludes',
dest='excludes_filename',
default='/etc/atticmatic/excludes',
help='Excludes filename',
)
parser.add_argument(
'--verbose',
action='store_true',
help='Display verbose progress information',
)
args = parser.parse_args()
try:
location_config, retention_config = parse_configuration(args.config_filename)
create_archive(args.excludes_filename, args.verbose, *location_config)
prune_archives(location_config.repository, args.verbose, *retention_config)
except (ValueError, CalledProcessError), error:
print(error, file=sys.stderr)
sys.exit(1)

57
atticmatic/config.py Normal file
View File

@ -0,0 +1,57 @@
from collections import namedtuple
from ConfigParser import SafeConfigParser
CONFIG_SECTION_LOCATION = 'location'
CONFIG_SECTION_RETENTION = 'retention'
CONFIG_FORMAT = {
CONFIG_SECTION_LOCATION: ('source_directories', 'repository'),
CONFIG_SECTION_RETENTION: ('keep_daily', 'keep_weekly', 'keep_monthly'),
}
LocationConfig = namedtuple('LocationConfig', CONFIG_FORMAT[CONFIG_SECTION_LOCATION])
RetentionConfig = namedtuple('RetentionConfig', CONFIG_FORMAT[CONFIG_SECTION_RETENTION])
def parse_configuration(config_filename):
'''
Given a config filename of the expected format, return the parse configuration as a tuple of
(LocationConfig, RetentionConfig). Raise if the format is not as expected.
'''
parser = SafeConfigParser()
parser.read((config_filename,))
section_names = parser.sections()
expected_section_names = CONFIG_FORMAT.keys()
if set(section_names) != set(expected_section_names):
raise ValueError(
'Expected config sections {} but found sections: {}'.format(
', '.join(expected_section_names),
', '.join(section_names)
)
)
for section_name in section_names:
option_names = parser.options(section_name)
expected_option_names = CONFIG_FORMAT[section_name]
if set(option_names) != set(expected_option_names):
raise ValueError(
'Expected options {} in config section {} but found options: {}'.format(
', '.join(expected_option_names),
section_name,
', '.join(option_names)
)
)
return (
LocationConfig(*(
parser.get(CONFIG_SECTION_LOCATION, option_name)
for option_name in CONFIG_FORMAT[CONFIG_SECTION_LOCATION]
)),
RetentionConfig(*(
parser.getint(CONFIG_SECTION_RETENTION, option_name)
for option_name in CONFIG_FORMAT[CONFIG_SECTION_RETENTION]
))
)

3
sample/atticmatic.cron Normal file
View File

@ -0,0 +1,3 @@
# You can drop this file into /etc/cron.d/ to run atticmatic nightly.
0 3 * * * root /usr/local/bin/atticmatic

12
sample/config Normal file
View File

@ -0,0 +1,12 @@
[location]
# Space-separated list of source directories to backup.
source_directories: /home /etc
# Path to local or remote Attic repository.
repository: user@backupserver:sourcehostname.attic
# Retention policy for how many backups to keep in each category.
[retention]
keep_daily: 7
keep_weekly: 4
keep_monthly: 6

3
sample/excludes Normal file
View File

@ -0,0 +1,3 @@
*.pyc
/home/*/.cache
/etc/ssl

11
setup.py Normal file
View File

@ -0,0 +1,11 @@
from setuptools import setup, find_packages
setup(
name='atticmatic',
version='0.0.1',
description='A wrapper script for Attic backup software',
author='Dan Helfman',
author_email='witten@torsion.org',
packages=find_packages(),
entry_points={'console_scripts': ['atticmatic = atticmatic.command:main']},
)