Compare commits

..
13 Commits
27 changed files with 321 additions and 332 deletions
+3
View File
@@ -25,3 +25,6 @@ e58246fc92bb22c2b2fd8b86a1227de69d2d0315 0.1.4
977f19c2f6a515be6c5ef69cf17b0e0989532209 github/yaml_config_files
0000000000000000000000000000000000000000 github/yaml_config_files
28434dd0440cc8da44c2f3e9bd7e9402a59c3b40 github/master
dbc96d3f83bd5570b6826537616d4160b3374836 0.1.8
0e1fbee9358de4f062fa9539e1355db83db70caa 1.0.0
de2d7721cdec93a52d20222a9ddd579ed93c1017 1.0.1
+17
View File
@@ -1,3 +1,20 @@
1.0.2
* #20: Fix for traceback when remote_path option is missing.
1.0.1
* #19: Support for Borg's --remote-path option to use an alternate Borg
executable. See sample/config.
1.0.0
* Attic is no longer supported, as there hasn't been any recent development on
it. Dropping Attic support will allow faster iteration on Borg-specific
features. If you're still using Attic, this is a good time to switch to Borg!
* Project renamed from atticmatic to borgmatic. See the borgmatic README for
information on upgrading.
0.1.8
* Fix for handling of spaces in source_directories which resulted in backup up everything.
+44 -37
View File
@@ -1,9 +1,8 @@
title: Atticmatic
title: Borgmatic
## Overview
atticmatic is a simple Python wrapper script for the
[Attic](https://attic-backup.org/) and
borgmatic (formerly atticmatic) is a simple Python wrapper script for the
[Borg](https://borgbackup.readthedocs.org/en/stable/) backup software that
initiates a backup, prunes any old backups according to a retention policy,
and validates backups for consistency. The script supports specifying your
@@ -19,7 +18,7 @@ Here's an example config file:
source_directories: /home /etc /var/log/syslog*
# Path to local or remote backup repository.
repository: user@backupserver:sourcehostname.attic
repository: user@backupserver:sourcehostname.borg
[retention]
# Retention policy for how many backups to keep in each category.
@@ -35,37 +34,30 @@ checks: repository archives
Additionally, exclude patterns can be specified in a separate excludes config
file, one pattern per line.
atticmatic is hosted at <https://torsion.org/atticmatic> with [source code
available](https://torsion.org/hg/atticmatic). It's also mirrored on
[GitHub](https://github.com/witten/atticmatic) and
[BitBucket](https://bitbucket.org/dhelfman/atticmatic) for convenience.
borgmatic is hosted at <https://torsion.org/borgmatic> with [source code
available](https://torsion.org/hg/borgmatic). It's also mirrored on
[GitHub](https://github.com/witten/borgmatic) and
[BitBucket](https://bitbucket.org/dhelfman/borgmatic) for convenience.
## Setup
To get up and running, follow the [Attic Quick
Start](https://attic-backup.org/quickstart.html) or the [Borg Quick
Start](https://borgbackup.readthedocs.org/en/latest/quickstart.html) to create a
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 atticmatic
To get up and running, follow the [Borg Quick
Start](https://borgbackup.readthedocs.org/en/latest/quickstart.html) to create
a repository on a local or remote host. Note that if you plan to run
borgmatic on a schedule with cron, and you encrypt your Borg repository with
a passphrase instead of a key file, you'll need to set the borgmatic
`encryption_passphrase` configuration variable. See the repository encryption
section of the Quick Start 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:
To install borgmatic, run the following command to download and install it:
sudo pip install --upgrade atticmatic
sudo pip install --upgrade borgmatic
If you are using Attic, copy the following configuration files:
sudo cp sample/atticmatic.cron /etc/cron.d/atticmatic
sudo mkdir /etc/atticmatic/
sudo cp sample/config sample/excludes /etc/atticmatic/
If you are using Borg, copy the files like this instead:
Then, copy the following configuration files:
sudo cp sample/borgmatic.cron /etc/cron.d/borgmatic
sudo mkdir /etc/borgmatic/
@@ -74,16 +66,31 @@ If you are using Borg, copy the files like this instead:
Lastly, modify the /etc files with your desired configuration.
## Upgrading from atticmatic
You can ignore this section if you're not an atticmatic user (the former name
of borgmatic).
borgmatic only supports Borg now and no longer supports Attic. So if you're
an Attic user, consider switching to Borg. See the [Borg upgrade
command](https://borgbackup.readthedocs.io/en/stable/usage.html#borg-upgrade)
for more information. Then, follow the instructions above about setting up
your borgmatic configuration files.
If you were already using Borg with atticmatic, then you can easily upgrade
from atticmatic to borgmatic. Simply run the following commands:
sudo pip uninstall atticmatic
sudo pip install borgmatic
That's it! borgmatic will continue using your /etc/borgmatic configuration
files.
## Usage
You can run atticmatic and start a backup simply by invoking it without
You can run borgmatic and start a backup simply by invoking it without
arguments:
atticmatic
Or, if you're using Borg, use this command instead to make use of the Borg
backend:
borgmatic
This will also prune any old backups as per the configured retention policy,
@@ -93,15 +100,15 @@ By default, the backup will proceed silently except in the case of errors. But
if you'd like to to get additional information about the progress of the
backup as it proceeds, use the verbosity option:
atticmattic --verbosity 1
borgmatic --verbosity 1
Or, for even more progress spew:
atticmattic --verbosity 2
borgmatic --verbosity 2
If you'd like to see the available command-line arguments, view the help:
atticmattic --help
borgmatic --help
## Running tests
@@ -119,12 +126,12 @@ Then, to actually run tests, run:
### Broken pipe with remote repository
When running atticmatic on a large remote repository, you may receive errors
like the following, particularly while "attic check" is validating backups for
When running borgmatic on a large remote repository, you may receive errors
like the following, particularly while "borg check" is validating backups for
consistency:
Write failed: Broken pipe
attic: Error: Connection closed by remote host
borg: Error: Connection closed by remote host
This error can be caused by an ssh timeout, which you can rectify by adding
the following to the ~/.ssh/config file on the client:
@@ -138,8 +145,8 @@ backups.
## Issues and feedback
Got an issue or an idea for a feature enhancement? Check out the [atticmatic
issue tracker](https://tree.taiga.io/project/witten-atticmatic/issues). In
Got an issue or an idea for a feature enhancement? Check out the [borgmatic
issue tracker](https://tree.taiga.io/project/witten-borgmatic/issues?page=1&status=399951,399952,399955). In
order to create a new issue or comment on an issue, you'll need to [login
first](https://tree.taiga.io/login).
-14
View File
@@ -1,14 +0,0 @@
from functools import partial
from atticmatic.backends import shared
# An atticmatic backend that supports Attic for actually handling backups.
COMMAND = 'attic'
CONFIG_FORMAT = shared.CONFIG_FORMAT
initialize = partial(shared.initialize, command=COMMAND)
create_archive = partial(shared.create_archive, command=COMMAND)
prune_archives = partial(shared.prune_archives, command=COMMAND)
check_archives = partial(shared.check_archives, command=COMMAND)
-41
View File
@@ -1,41 +0,0 @@
from functools import partial
from atticmatic.config import Section_format, option
from atticmatic.backends import shared
# An atticmatic backend that supports Borg for actually handling backups.
COMMAND = 'borg'
CONFIG_FORMAT = (
Section_format(
'location',
(
option('source_directories'),
option('one_file_system', value_type=bool, required=False),
option('repository'),
),
),
Section_format(
'storage',
(
option('encryption_passphrase', required=False),
option('compression', required=False),
option('umask', required=False),
),
),
shared.CONFIG_FORMAT[2], # retention
Section_format(
'consistency',
(
option('checks', required=False),
option('check_last', required=False),
),
)
)
initialize = partial(shared.initialize, command=COMMAND)
create_archive = partial(shared.create_archive, command=COMMAND)
prune_archives = partial(shared.prune_archives, command=COMMAND)
check_archives = partial(shared.check_archives, command=COMMAND)
-75
View File
@@ -1,75 +0,0 @@
from __future__ import print_function
from argparse import ArgumentParser
from importlib import import_module
import os
from subprocess import CalledProcessError
import sys
from atticmatic.config import parse_configuration
DEFAULT_CONFIG_FILENAME_PATTERN = '/etc/{}/config'
DEFAULT_EXCLUDES_FILENAME_PATTERN = '/etc/{}/excludes'
def parse_arguments(command_name, *arguments):
'''
Given the name of the command with which this script was invoked and command-line arguments,
parse the arguments and return them as an ArgumentParser instance. Use the command name to
determine the default configuration and excludes paths.
'''
config_filename_default = DEFAULT_CONFIG_FILENAME_PATTERN.format(command_name)
excludes_filename_default = DEFAULT_EXCLUDES_FILENAME_PATTERN.format(command_name)
parser = ArgumentParser()
parser.add_argument(
'-c', '--config',
dest='config_filename',
default=config_filename_default,
help='Configuration filename',
)
parser.add_argument(
'--excludes',
dest='excludes_filename',
default=excludes_filename_default if os.path.exists(excludes_filename_default) else None,
help='Excludes filename',
)
parser.add_argument(
'-v', '--verbosity',
type=int,
help='Display verbose progress (1 for some, 2 for lots)',
)
return parser.parse_args(arguments)
def load_backend(command_name):
'''
Given the name of the command with which this script was invoked, return the corresponding
backend module responsible for actually dealing with backups.
'''
backend_name = {
'atticmatic': 'attic',
'borgmatic': 'borg',
}.get(command_name, 'attic')
return import_module('atticmatic.backends.{}'.format(backend_name))
def main():
try:
command_name = os.path.basename(sys.argv[0])
args = parse_arguments(command_name, *sys.argv[1:])
backend = load_backend(command_name)
config = parse_configuration(args.config_filename, backend.CONFIG_FORMAT)
repository = config.location['repository']
backend.initialize(config.storage)
backend.create_archive(
args.excludes_filename, args.verbosity, config.storage, **config.location
)
backend.prune_archives(args.verbosity, repository, config.retention)
backend.check_archives(args.verbosity, repository, config.consistency)
except (ValueError, IOError, CalledProcessError) as error:
print(error, file=sys.stderr)
sys.exit(1)
View File
-33
View File
@@ -1,33 +0,0 @@
from flexmock import flexmock
from atticmatic import command as module
def test_load_backend_with_atticmatic_command_should_return_attic_backend():
backend = flexmock()
(
flexmock(module).should_receive('import_module').with_args('atticmatic.backends.attic')
.and_return(backend).once()
)
assert module.load_backend('atticmatic') == backend
def test_load_backend_with_unknown_command_should_return_attic_backend():
backend = flexmock()
(
flexmock(module).should_receive('import_module').with_args('atticmatic.backends.attic')
.and_return(backend).once()
)
assert module.load_backend('unknownmatic') == backend
def test_load_backend_with_borgmatic_command_should_return_borg_backend():
backend = flexmock()
(
flexmock(module).should_receive('import_module').with_args('atticmatic.backends.borg')
.and_return(backend).once()
)
assert module.load_backend('borgmatic') == backend
@@ -6,52 +6,16 @@ import subprocess
from glob import glob
from itertools import chain
from atticmatic.config import Section_format, option
from atticmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
# Common backend functionality shared by Attic and Borg. As the two backup
# commands diverge, these shared functions will likely need to be replaced
# with non-shared functions within atticmatic.backends.attic and
# atticmatic.backends.borg.
# Integration with Borg for actually handling backups.
CONFIG_FORMAT = (
Section_format(
'location',
(
option('source_directories'),
option('repository'),
),
),
Section_format(
'storage',
(
option('encryption_passphrase', required=False),
),
),
Section_format(
'retention',
(
option('keep_within', required=False),
option('keep_hourly', int, required=False),
option('keep_daily', int, required=False),
option('keep_weekly', int, required=False),
option('keep_monthly', int, required=False),
option('keep_yearly', int, required=False),
option('prefix', required=False),
),
),
Section_format(
'consistency',
(
option('checks', required=False),
),
)
)
COMMAND = 'borg'
def initialize(storage_config, command):
def initialize(storage_config, command=COMMAND):
passphrase = storage_config.get('encryption_passphrase')
if passphrase:
@@ -59,8 +23,8 @@ def initialize(storage_config, command):
def create_archive(
excludes_filename, verbosity, storage_config, source_directories, repository, command,
one_file_system=None
excludes_filename, verbosity, storage_config, source_directories, repository, command=COMMAND,
one_file_system=None, remote_path=None,
):
'''
Given an excludes filename (or None), a vebosity flag, a storage config dict, a space-separated
@@ -75,6 +39,7 @@ def create_archive(
umask = storage_config.get('umask', None)
umask_flags = ('--umask', str(umask)) if umask else ()
one_file_system_flags = ('--one-file-system',) if one_file_system else ()
remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
verbosity_flags = {
VERBOSITY_SOME: ('--stats',),
VERBOSITY_LOTS: ('--verbose', '--stats'),
@@ -88,7 +53,7 @@ def create_archive(
timestamp=datetime.now().isoformat(),
),
) + sources + exclude_flags + compression_flags + one_file_system_flags + \
umask_flags + verbosity_flags
remote_path_flags + umask_flags + verbosity_flags
subprocess.check_call(full_command)
@@ -115,12 +80,13 @@ def _make_prune_flags(retention_config):
)
def prune_archives(verbosity, repository, retention_config, command):
def prune_archives(verbosity, repository, retention_config, command=COMMAND, remote_path=None):
'''
Given a verbosity flag, a local or remote repository path, a retention config dict, and a
command to run, prune attic archives according the the retention policy specified in that
configuration.
'''
remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
verbosity_flags = {
VERBOSITY_SOME: ('--stats',),
VERBOSITY_LOTS: ('--verbose', '--stats'),
@@ -133,7 +99,7 @@ def prune_archives(verbosity, repository, retention_config, command):
element
for pair in _make_prune_flags(retention_config)
for element in pair
) + verbosity_flags
) + remote_path_flags + verbosity_flags
subprocess.check_call(full_command)
@@ -191,7 +157,7 @@ def _make_check_flags(checks, check_last=None):
) + last_flag
def check_archives(verbosity, repository, consistency_config, command):
def check_archives(verbosity, repository, consistency_config, command=COMMAND, remote_path=None):
'''
Given a verbosity flag, a local or remote repository path, a consistency config dict, and a
command to run, check the contained attic archives for consistency.
@@ -203,6 +169,7 @@ def check_archives(verbosity, repository, consistency_config, command):
if not checks:
return
remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
verbosity_flags = {
VERBOSITY_SOME: ('--verbose',),
VERBOSITY_LOTS: ('--verbose',),
@@ -211,7 +178,7 @@ def check_archives(verbosity, repository, consistency_config, command):
full_command = (
command, 'check',
repository,
) + _make_check_flags(checks, check_last) + verbosity_flags
) + _make_check_flags(checks, check_last) + remote_path_flags + verbosity_flags
# The check command spews to stdout/stderr even without the verbose flag. Suppress it.
stdout = None if verbosity_flags else open(os.devnull, 'w')
+58
View File
@@ -0,0 +1,58 @@
from __future__ import print_function
from argparse import ArgumentParser
import os
from subprocess import CalledProcessError
import sys
from borgmatic import borg
from borgmatic.config import parse_configuration, CONFIG_FORMAT
DEFAULT_CONFIG_FILENAME = '/etc/borgmatic/config'
DEFAULT_EXCLUDES_FILENAME = '/etc/borgmatic/excludes'
def parse_arguments(*arguments):
'''
Given the name of the command with which this script was invoked and command-line arguments,
parse the arguments and return them as an ArgumentParser instance. Use the command name to
determine the default configuration and excludes paths.
'''
parser = ArgumentParser()
parser.add_argument(
'-c', '--config',
dest='config_filename',
default=DEFAULT_CONFIG_FILENAME,
help='Configuration filename',
)
parser.add_argument(
'--excludes',
dest='excludes_filename',
default=DEFAULT_EXCLUDES_FILENAME if os.path.exists(DEFAULT_EXCLUDES_FILENAME) else None,
help='Excludes filename',
)
parser.add_argument(
'-v', '--verbosity',
type=int,
help='Display verbose progress (1 for some, 2 for lots)',
)
return parser.parse_args(arguments)
def main():
try:
args = parse_arguments(*sys.argv[1:])
config = parse_configuration(args.config_filename, CONFIG_FORMAT)
repository = config.location['repository']
remote_path = config.location.get('remote_path')
borg.initialize(config.storage)
borg.create_archive(
args.excludes_filename, args.verbosity, config.storage, **config.location
)
borg.prune_archives(args.verbosity, repository, config.retention, remote_path=remote_path)
borg.check_archives(args.verbosity, repository, config.consistency, remote_path=remote_path)
except (ValueError, IOError, CalledProcessError) as error:
print(error, file=sys.stderr)
sys.exit(1)
@@ -20,6 +20,46 @@ def option(name, value_type=str, required=True):
return Config_option(name, value_type, required)
CONFIG_FORMAT = (
Section_format(
'location',
(
option('source_directories'),
option('one_file_system', value_type=bool, required=False),
option('remote_path', required=False),
option('repository'),
),
),
Section_format(
'storage',
(
option('encryption_passphrase', required=False),
option('compression', required=False),
option('umask', required=False),
),
),
Section_format(
'retention',
(
option('keep_within', required=False),
option('keep_hourly', int, required=False),
option('keep_daily', int, required=False),
option('keep_weekly', int, required=False),
option('keep_monthly', int, required=False),
option('keep_yearly', int, required=False),
option('prefix', required=False),
),
),
Section_format(
'consistency',
(
option('checks', required=False),
option('check_last', required=False),
),
)
)
def validate_configuration_format(parser, config_format):
'''
Given an open RawConfigParser and an expected config file format, validate that the parsed
@@ -4,26 +4,23 @@ import sys
from flexmock import flexmock
import pytest
from atticmatic import command as module
COMMAND_NAME = 'foomatic'
from borgmatic import command as module
def test_parse_arguments_with_no_arguments_uses_defaults():
flexmock(os.path).should_receive('exists').and_return(True)
parser = module.parse_arguments(COMMAND_NAME)
parser = module.parse_arguments()
assert parser.config_filename == module.DEFAULT_CONFIG_FILENAME_PATTERN.format(COMMAND_NAME)
assert parser.excludes_filename == module.DEFAULT_EXCLUDES_FILENAME_PATTERN.format(COMMAND_NAME)
assert parser.config_filename == module.DEFAULT_CONFIG_FILENAME
assert parser.excludes_filename == module.DEFAULT_EXCLUDES_FILENAME
assert parser.verbosity == None
def test_parse_arguments_with_filename_arguments_overrides_defaults():
flexmock(os.path).should_receive('exists').and_return(True)
parser = module.parse_arguments(COMMAND_NAME, '--config', 'myconfig', '--excludes', 'myexcludes')
parser = module.parse_arguments('--config', 'myconfig', '--excludes', 'myexcludes')
assert parser.config_filename == 'myconfig'
assert parser.excludes_filename == 'myexcludes'
@@ -33,9 +30,9 @@ def test_parse_arguments_with_filename_arguments_overrides_defaults():
def test_parse_arguments_with_missing_default_excludes_file_sets_filename_to_none():
flexmock(os.path).should_receive('exists').and_return(False)
parser = module.parse_arguments(COMMAND_NAME)
parser = module.parse_arguments()
assert parser.config_filename == module.DEFAULT_CONFIG_FILENAME_PATTERN.format(COMMAND_NAME)
assert parser.config_filename == module.DEFAULT_CONFIG_FILENAME
assert parser.excludes_filename == None
assert parser.verbosity == None
@@ -43,9 +40,9 @@ def test_parse_arguments_with_missing_default_excludes_file_sets_filename_to_non
def test_parse_arguments_with_missing_overridden_excludes_file_retains_filename():
flexmock(os.path).should_receive('exists').and_return(False)
parser = module.parse_arguments(COMMAND_NAME, '--excludes', 'myexcludes')
parser = module.parse_arguments('--excludes', 'myexcludes')
assert parser.config_filename == module.DEFAULT_CONFIG_FILENAME_PATTERN.format(COMMAND_NAME)
assert parser.config_filename == module.DEFAULT_CONFIG_FILENAME
assert parser.excludes_filename == 'myexcludes'
assert parser.verbosity == None
@@ -53,10 +50,10 @@ def test_parse_arguments_with_missing_overridden_excludes_file_retains_filename(
def test_parse_arguments_with_verbosity_flag_overrides_default():
flexmock(os.path).should_receive('exists').and_return(True)
parser = module.parse_arguments(COMMAND_NAME, '--verbosity', '1')
parser = module.parse_arguments('--verbosity', '1')
assert parser.config_filename == module.DEFAULT_CONFIG_FILENAME_PATTERN.format(COMMAND_NAME)
assert parser.excludes_filename == module.DEFAULT_EXCLUDES_FILENAME_PATTERN.format(COMMAND_NAME)
assert parser.config_filename == module.DEFAULT_CONFIG_FILENAME
assert parser.excludes_filename == module.DEFAULT_EXCLUDES_FILENAME
assert parser.verbosity == 1
@@ -67,6 +64,6 @@ def test_parse_arguments_with_invalid_arguments_exits():
try:
with pytest.raises(SystemExit):
module.parse_arguments(COMMAND_NAME, '--posix-me-harder')
module.parse_arguments('--posix-me-harder')
finally:
sys.stderr = original_stderr
@@ -8,7 +8,7 @@ except ImportError:
from collections import OrderedDict
import string
from atticmatic import config as module
from borgmatic import config as module
def test_parse_section_options_with_punctuation_should_return_section_options():
@@ -4,9 +4,9 @@ import os
from flexmock import flexmock
from atticmatic.backends import shared as module
from atticmatic.tests.builtins import builtins_mock
from atticmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
from borgmatic import borg as module
from borgmatic.tests.builtins import builtins_mock
from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
def test_initialize_with_passphrase_should_set_environment():
@@ -14,8 +14,8 @@ def test_initialize_with_passphrase_should_set_environment():
try:
os.environ = {}
module.initialize({'encryption_passphrase': 'pass'}, command='attic')
assert os.environ.get('ATTIC_PASSPHRASE') == 'pass'
module.initialize({'encryption_passphrase': 'pass'}, command='borg')
assert os.environ.get('BORG_PASSPHRASE') == 'pass'
finally:
os.environ = orig_environ
@@ -25,8 +25,8 @@ def test_initialize_without_passphrase_should_not_set_environment():
try:
os.environ = {}
module.initialize({}, command='attic')
assert os.environ.get('ATTIC_PASSPHRASE') == None
module.initialize({}, command='borg')
assert os.environ.get('BORG_PASSPHRASE') == None
finally:
os.environ = orig_environ
@@ -53,11 +53,11 @@ def insert_datetime_mock():
).mock
CREATE_COMMAND_WITHOUT_EXCLUDES = ('attic', 'create', 'repo::host-now', 'foo', 'bar')
CREATE_COMMAND_WITHOUT_EXCLUDES = ('borg', 'create', 'repo::host-now', 'foo', 'bar')
CREATE_COMMAND = CREATE_COMMAND_WITHOUT_EXCLUDES + ('--exclude-from', 'excludes')
def test_create_archive_should_call_attic_with_parameters():
def test_create_archive_should_call_borg_with_parameters():
insert_subprocess_mock(CREATE_COMMAND)
insert_platform_mock()
insert_datetime_mock()
@@ -68,7 +68,7 @@ def test_create_archive_should_call_attic_with_parameters():
storage_config={},
source_directories='foo bar',
repository='repo',
command='attic',
command='borg',
)
@@ -83,11 +83,11 @@ def test_create_archive_with_two_spaces_in_source_directories():
storage_config={},
source_directories='foo bar',
repository='repo',
command='attic',
command='borg',
)
def test_create_archive_with_none_excludes_filename_should_call_attic_without_excludes():
def test_create_archive_with_none_excludes_filename_should_call_borg_without_excludes():
insert_subprocess_mock(CREATE_COMMAND_WITHOUT_EXCLUDES)
insert_platform_mock()
insert_datetime_mock()
@@ -98,11 +98,11 @@ def test_create_archive_with_none_excludes_filename_should_call_attic_without_ex
storage_config={},
source_directories='foo bar',
repository='repo',
command='attic',
command='borg',
)
def test_create_archive_with_verbosity_some_should_call_attic_with_stats_parameter():
def test_create_archive_with_verbosity_some_should_call_borg_with_stats_parameter():
insert_subprocess_mock(CREATE_COMMAND + ('--stats',))
insert_platform_mock()
insert_datetime_mock()
@@ -113,11 +113,11 @@ def test_create_archive_with_verbosity_some_should_call_attic_with_stats_paramet
storage_config={},
source_directories='foo bar',
repository='repo',
command='attic',
command='borg',
)
def test_create_archive_with_verbosity_lots_should_call_attic_with_verbose_parameter():
def test_create_archive_with_verbosity_lots_should_call_borg_with_verbose_parameter():
insert_subprocess_mock(CREATE_COMMAND + ('--verbose', '--stats'))
insert_platform_mock()
insert_datetime_mock()
@@ -128,11 +128,11 @@ def test_create_archive_with_verbosity_lots_should_call_attic_with_verbose_param
storage_config={},
source_directories='foo bar',
repository='repo',
command='attic',
command='borg',
)
def test_create_archive_with_compression_should_call_attic_with_compression_parameters():
def test_create_archive_with_compression_should_call_borg_with_compression_parameters():
insert_subprocess_mock(CREATE_COMMAND + ('--compression', 'rle'))
insert_platform_mock()
insert_datetime_mock()
@@ -143,11 +143,11 @@ def test_create_archive_with_compression_should_call_attic_with_compression_para
storage_config={'compression': 'rle'},
source_directories='foo bar',
repository='repo',
command='attic',
command='borg',
)
def test_create_archive_with_one_file_system_should_call_attic_with_one_file_system_parameters():
def test_create_archive_with_one_file_system_should_call_borg_with_one_file_system_parameters():
insert_subprocess_mock(CREATE_COMMAND + ('--one-file-system',))
insert_platform_mock()
insert_datetime_mock()
@@ -158,12 +158,28 @@ def test_create_archive_with_one_file_system_should_call_attic_with_one_file_sys
storage_config={},
source_directories='foo bar',
repository='repo',
command='attic',
command='borg',
one_file_system=True,
)
def test_create_archive_with_umask_should_call_attic_with_umask_parameters():
def test_create_archive_with_remote_path_should_call_borg_with_remote_path_parameters():
insert_subprocess_mock(CREATE_COMMAND + ('--remote-path', 'borg1'))
insert_platform_mock()
insert_datetime_mock()
module.create_archive(
excludes_filename='excludes',
verbosity=None,
storage_config={},
source_directories='foo bar',
repository='repo',
command='borg',
remote_path='borg1',
)
def test_create_archive_with_umask_should_call_borg_with_umask_parameters():
insert_subprocess_mock(CREATE_COMMAND + ('--umask', '740'))
insert_platform_mock()
insert_datetime_mock()
@@ -174,12 +190,12 @@ def test_create_archive_with_umask_should_call_attic_with_umask_parameters():
storage_config={'umask': 740},
source_directories='foo bar',
repository='repo',
command='attic',
command='borg',
)
def test_create_archive_with_source_directories_glob_expands():
insert_subprocess_mock(('attic', 'create', 'repo::host-now', 'foo', 'food'))
insert_subprocess_mock(('borg', 'create', 'repo::host-now', 'foo', 'food'))
insert_platform_mock()
insert_datetime_mock()
flexmock(module).should_receive('glob').with_args('foo*').and_return(['foo', 'food'])
@@ -190,12 +206,12 @@ def test_create_archive_with_source_directories_glob_expands():
storage_config={},
source_directories='foo*',
repository='repo',
command='attic',
command='borg',
)
def test_create_archive_with_non_matching_source_directories_glob_passes_through():
insert_subprocess_mock(('attic', 'create', 'repo::host-now', 'foo*'))
insert_subprocess_mock(('borg', 'create', 'repo::host-now', 'foo*'))
insert_platform_mock()
insert_datetime_mock()
flexmock(module).should_receive('glob').with_args('foo*').and_return([])
@@ -206,12 +222,12 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through
storage_config={},
source_directories='foo*',
repository='repo',
command='attic',
command='borg',
)
def test_create_archive_with_glob_should_call_attic_with_expanded_directories():
insert_subprocess_mock(('attic', 'create', 'repo::host-now', 'foo', 'food'))
def test_create_archive_with_glob_should_call_borg_with_expanded_directories():
insert_subprocess_mock(('borg', 'create', 'repo::host-now', 'foo', 'food'))
insert_platform_mock()
insert_datetime_mock()
flexmock(module).should_receive('glob').with_args('foo*').and_return(['foo', 'food'])
@@ -222,7 +238,7 @@ def test_create_archive_with_glob_should_call_attic_with_expanded_directories():
storage_config={},
source_directories='foo*',
repository='repo',
command='attic',
command='borg',
)
@@ -248,11 +264,11 @@ def test_make_prune_flags_should_return_flags_from_config():
PRUNE_COMMAND = (
'attic', 'prune', 'repo', '--keep-daily', '1', '--keep-weekly', '2', '--keep-monthly', '3',
'borg', 'prune', 'repo', '--keep-daily', '1', '--keep-weekly', '2', '--keep-monthly', '3',
)
def test_prune_archives_should_call_attic_with_parameters():
def test_prune_archives_should_call_borg_with_parameters():
retention_config = flexmock()
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS,
@@ -263,11 +279,11 @@ def test_prune_archives_should_call_attic_with_parameters():
verbosity=None,
repository='repo',
retention_config=retention_config,
command='attic',
command='borg',
)
def test_prune_archives_with_verbosity_some_should_call_attic_with_stats_parameter():
def test_prune_archives_with_verbosity_some_should_call_borg_with_stats_parameter():
retention_config = flexmock()
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS,
@@ -278,11 +294,11 @@ def test_prune_archives_with_verbosity_some_should_call_attic_with_stats_paramet
repository='repo',
verbosity=VERBOSITY_SOME,
retention_config=retention_config,
command='attic',
command='borg',
)
def test_prune_archives_with_verbosity_lots_should_call_attic_with_verbose_parameter():
def test_prune_archives_with_verbosity_lots_should_call_borg_with_verbose_parameter():
retention_config = flexmock()
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS,
@@ -293,7 +309,22 @@ def test_prune_archives_with_verbosity_lots_should_call_attic_with_verbose_param
repository='repo',
verbosity=VERBOSITY_LOTS,
retention_config=retention_config,
command='attic',
command='borg',
)
def test_prune_archive_with_remote_path_should_call_borg_with_remote_path_parameters():
retention_config = flexmock()
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS,
)
insert_subprocess_mock(PRUNE_COMMAND + ('--remote-path', 'borg1'))
module.prune_archives(
verbosity=None,
repository='repo',
retention_config=retention_config,
command='borg',
remote_path='borg1',
)
@@ -345,7 +376,7 @@ def test_make_check_flags_with_last_returns_last_flag():
assert flags == ('--last', 3)
def test_check_archives_should_call_attic_with_parameters():
def test_check_archives_should_call_borg_with_parameters():
checks = flexmock()
check_last = flexmock()
consistency_config = flexmock().should_receive('get').and_return(check_last).mock
@@ -353,7 +384,7 @@ def test_check_archives_should_call_attic_with_parameters():
flexmock(module).should_receive('_make_check_flags').with_args(checks, check_last).and_return(())
stdout = flexmock()
insert_subprocess_mock(
('attic', 'check', 'repo'),
('borg', 'check', 'repo'),
stdout=stdout, stderr=STDOUT,
)
insert_platform_mock()
@@ -365,16 +396,16 @@ def test_check_archives_should_call_attic_with_parameters():
verbosity=None,
repository='repo',
consistency_config=consistency_config,
command='attic',
command='borg',
)
def test_check_archives_with_verbosity_some_should_call_attic_with_verbose_parameter():
def test_check_archives_with_verbosity_some_should_call_borg_with_verbose_parameter():
consistency_config = flexmock().should_receive('get').and_return(None).mock
flexmock(module).should_receive('_parse_checks').and_return(flexmock())
flexmock(module).should_receive('_make_check_flags').and_return(())
insert_subprocess_mock(
('attic', 'check', 'repo', '--verbose'),
('borg', 'check', 'repo', '--verbose'),
stdout=None, stderr=STDOUT,
)
insert_platform_mock()
@@ -384,16 +415,16 @@ def test_check_archives_with_verbosity_some_should_call_attic_with_verbose_param
verbosity=VERBOSITY_SOME,
repository='repo',
consistency_config=consistency_config,
command='attic',
command='borg',
)
def test_check_archives_with_verbosity_lots_should_call_attic_with_verbose_parameter():
def test_check_archives_with_verbosity_lots_should_call_borg_with_verbose_parameter():
consistency_config = flexmock().should_receive('get').and_return(None).mock
flexmock(module).should_receive('_parse_checks').and_return(flexmock())
flexmock(module).should_receive('_make_check_flags').and_return(())
insert_subprocess_mock(
('attic', 'check', 'repo', '--verbose'),
('borg', 'check', 'repo', '--verbose'),
stdout=None, stderr=STDOUT,
)
insert_platform_mock()
@@ -403,7 +434,7 @@ def test_check_archives_with_verbosity_lots_should_call_attic_with_verbose_param
verbosity=VERBOSITY_LOTS,
repository='repo',
consistency_config=consistency_config,
command='attic',
command='borg',
)
@@ -416,5 +447,30 @@ def test_check_archives_without_any_checks_should_bail():
verbosity=None,
repository='repo',
consistency_config=consistency_config,
command='attic',
command='borg',
)
def test_check_archives_with_remote_path_should_call_borg_with_remote_path_parameters():
checks = flexmock()
check_last = flexmock()
consistency_config = flexmock().should_receive('get').and_return(check_last).mock
flexmock(module).should_receive('_parse_checks').and_return(checks)
flexmock(module).should_receive('_make_check_flags').with_args(checks, check_last).and_return(())
stdout = flexmock()
insert_subprocess_mock(
('borg', 'check', 'repo', '--remote-path', 'borg1'),
stdout=stdout, stderr=STDOUT,
)
insert_platform_mock()
insert_datetime_mock()
builtins_mock().should_receive('open').and_return(stdout)
flexmock(module.os).should_receive('devnull')
module.check_archives(
verbosity=None,
repository='repo',
consistency_config=consistency_config,
command='borg',
remote_path='borg1',
)
@@ -3,7 +3,7 @@ from collections import OrderedDict
from flexmock import flexmock
import pytest
from atticmatic import config as module
from borgmatic import config as module
def test_option_should_create_config_option():
-3
View File
@@ -1,3 +0,0 @@
# You can drop this file into /etc/cron.d/ to run atticmatic nightly.
0 3 * * * root PATH=$PATH:/usr/local/bin /usr/local/bin/atticmatic
+15 -10
View File
@@ -3,27 +3,30 @@
# Globs are expanded.
source_directories: /home /etc /var/log/syslog*
# For Borg only, you can specify to stay in same file system (do not cross
# mount points).
# Stay in same file system (do not cross mount points).
#one_file_system: True
# Alternate Borg remote executable (defaults to "borg"):
#remote_path: borg1
# Path to local or remote repository.
repository: user@backupserver:sourcehostname.attic
repository: user@backupserver:sourcehostname.borg
[storage]
# Passphrase to unlock the encryption key with. Only use on repositories that
# were initialized with passphrase/repokey encryption.
#encryption_passphrase: foo
# For Borg only, you can specify the type of compression to use when creating
# archives. See https://borgbackup.readthedocs.org/en/stable/usage.html#borg-create
# Type of compression to use when creating archives. See
# https://borgbackup.readthedocs.org/en/stable/usage.html#borg-create
# for details. Defaults to no compression.
#compression: lz4
# For Borg only, you can specify the umask to be used for borg create.
# Umask to be used for borg create.
#umask: 0740
[retention]
# Retention policy for how many backups to keep in each category. See
# https://attic-backup.org/usage.html#attic-prune or
# https://borgbackup.readthedocs.org/en/stable/usage.html#borg-prune for details.
#keep_within: 3H
#keep_hourly: 24
@@ -31,13 +34,15 @@ keep_daily: 7
keep_weekly: 4
keep_monthly: 6
keep_yearly: 1
#prefix: sourcehostname
[consistency]
# Space-separated list of consistency checks to run: "repository", "archives",
# or both. Defaults to both. Set to "disabled" to disable all consistency
# checks. See https://attic-backup.org/usage.html#attic-check or
# https://borgbackup.readthedocs.org/en/stable/usage.html#borg-check for details.
# checks. See https://borgbackup.readthedocs.org/en/stable/usage.html#borg-check
# for details.
checks: repository archives
# For Borg only, you can restrict the number of checked archives to the last n.
# Restrict the number of checked archives to the last n.
#check_last: 3
+3
View File
@@ -1,2 +1,5 @@
[metadata]
description-file=README.md
[bdist_wheel]
universal=1
+9 -7
View File
@@ -1,17 +1,17 @@
from setuptools import setup, find_packages
VERSION = '0.1.8'
VERSION = '1.0.2'
setup(
name='atticmatic',
name='borgmatic',
version=VERSION,
description='A wrapper script for Attic/Borg backup software that creates and prunes backups',
description='A wrapper script for Borg backup software that creates and prunes backups',
author='Dan Helfman',
author_email='witten@torsion.org',
url='https://torsion.org/atticmatic',
download_url='https://torsion.org/hg/atticmatic/archive/%s.tar.gz' % VERSION,
url='https://torsion.org/borgmatic',
download_url='https://torsion.org/hg/borgmatic/archive/%s.tar.gz' % VERSION,
classifiers=(
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
@@ -24,10 +24,12 @@ setup(
packages=find_packages(),
entry_points={
'console_scripts': [
'atticmatic = atticmatic.command:main',
'borgmatic = atticmatic.command:main',
'borgmatic = borgmatic.command:main',
]
},
obsoletes=[
'atticmatic',
],
tests_require=(
'flexmock',
'pytest',