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