Initial import.
This commit is contained in:
commit
6dff335c8b
10 changed files with 212 additions and 0 deletions
3
.hgignore
Normal file
3
.hgignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
syntax: glob
|
||||
*.pyc
|
||||
*.egg-info
|
51
README
Normal file
51
README
Normal 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
0
atticmatic/__init__.py
Normal file
34
atticmatic/attic.py
Normal file
34
atticmatic/attic.py
Normal 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
38
atticmatic/command.py
Normal 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
57
atticmatic/config.py
Normal 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
3
sample/atticmatic.cron
Normal 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
12
sample/config
Normal 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
3
sample/excludes
Normal file
|
@ -0,0 +1,3 @@
|
|||
*.pyc
|
||||
/home/*/.cache
|
||||
/etc/ssl
|
11
setup.py
Normal file
11
setup.py
Normal 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']},
|
||||
)
|
Loading…
Reference in a new issue