Pārlūkot izejas kodu

Initial import.

Dan Helfman 10 gadi atpakaļ
revīzija
6dff335c8b
10 mainītis faili ar 212 papildinājumiem un 0 dzēšanām
  1. 3 0
      .hgignore
  2. 51 0
      README
  3. 0 0
      atticmatic/__init__.py
  4. 34 0
      atticmatic/attic.py
  5. 38 0
      atticmatic/command.py
  6. 57 0
      atticmatic/config.py
  7. 3 0
      sample/atticmatic.cron
  8. 12 0
      sample/config
  9. 3 0
      sample/excludes
  10. 11 0
      setup.py

+ 3 - 0
.hgignore

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

+ 51 - 0
README

@@ -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 - 0
atticmatic/__init__.py


+ 34 - 0
atticmatic/attic.py

@@ -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 - 0
atticmatic/command.py

@@ -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 - 0
atticmatic/config.py

@@ -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 - 0
sample/atticmatic.cron

@@ -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 - 0
sample/config

@@ -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 - 0
sample/excludes

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

+ 11 - 0
setup.py

@@ -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']},
+)