Compare commits

...
7 Commits
10 changed files with 115 additions and 58 deletions
+1
View File
@@ -36,3 +36,4 @@ f052a77a8ad5a0fea7fa86a902e0e401252f7d80 1.1.2
3f838f661546e04529b453aa443529b432afc243 1.1.3
3d605962d891731a0f372b903b556ac7a8c8359f 1.1.4
64ca13bfe050f656b44ed2eb1c3db045bfddd133 1.1.5
4daa944c122c572b9b56bfcac3f4e2869181c630 1.1.6
+2
View File
@@ -2,4 +2,6 @@ Dan Helfman <witten@torsion.org>: Main developer
Alexander Görtz: Python 3 compatibility
Henning Schroeder: Copy editing
Michele Lazzeri: Custom archive names
Robin `ypid` Schneider: Support additional options of Borg
Scott Squires: Custom archive names
+7
View File
@@ -1,3 +1,10 @@
1.1.7
* #28: Add "archive_name_format" to configuration for customizing archive names.
* Fix for traceback when "exclude_from" value is empty in configuration file.
* When pruning, make highest verbosity level list archives kept and pruned.
* Clarification of Python 3 pip usage in documentation.
1.1.6
* #12, #35: Support for Borg --exclude-from, --exclude-caches, and --exclude-if-present options.
+13 -11
View File
@@ -61,10 +61,10 @@ key-based ssh access to the desired user account on the remote host.
To install borgmatic, run the following command to download and install it:
sudo pip install --upgrade borgmatic
sudo pip3 install --upgrade borgmatic
Make sure you're using Python 3, as borgmatic does not support Python 2. (You
may have to use "pip3" or similar instead of "pip".)
Note that your pip binary may have a different name than "pip3". Make sure
you're using Python 3, as borgmatic does not support Python 2.
## Configuration
@@ -108,10 +108,7 @@ default, the traditional /etc/borgmatic/config.yaml as well.
In general, all you should need to do to upgrade borgmatic is run the
following:
sudo pip install --upgrade borgmatic
(You may have to use "pip3" or similar instead of "pip", so Python 3 gets
used.)
sudo pip3 install --upgrade borgmatic
However, see below about special cases.
@@ -127,7 +124,7 @@ As of version 1.1.0, borgmatic no longer supports Python 2. If you were
already running borgmatic with Python 3, then you can simply upgrade borgmatic
in-place:
sudo pip install --upgrade borgmatic
sudo pip3 install --upgrade borgmatic
But if you were running borgmatic with Python 2, uninstall and reinstall instead:
@@ -162,8 +159,8 @@ 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
sudo pip3 uninstall atticmatic
sudo pip3 install borgmatic
That's it! borgmatic will continue using your /etc/borgmatic configuration
files.
@@ -183,6 +180,11 @@ If you'd like to see the available command-line arguments, view the help:
borgmatic --help
Note that borgmatic prunes archives *before* creating an archive, so as to
free up space for archiving. This means that when a borgmatic run finishes,
there may still be prune-able archives. Not to worry, as they will get cleaned
up at the start of the next run.
### Verbosity
By default, the backup will proceed silently except in the case of errors. But
@@ -247,7 +249,7 @@ borgmatic to run.
First install tox, which is used for setting up testing environments:
pip install tox
pip3 install tox
Then, to actually run tests, run:
+6 -7
View File
@@ -1,8 +1,6 @@
from datetime import datetime
import glob
import itertools
import os
import platform
import subprocess
import tempfile
@@ -36,7 +34,7 @@ def _make_exclude_flags(location_config, exclude_patterns_filename=None):
Given a location config dict with various exclude options, and a filename containing any exclude
patterns, return the corresponding Borg flags as a tuple.
'''
exclude_filenames = tuple(location_config.get('exclude_from', ())) + (
exclude_filenames = tuple(location_config.get('exclude_from') or ()) + (
(exclude_patterns_filename,) if exclude_patterns_filename else ()
)
exclude_from_flags = tuple(
@@ -82,15 +80,16 @@ def create_archive(
VERBOSITY_SOME: ('--info', '--stats',),
VERBOSITY_LOTS: ('--debug', '--list', '--stats'),
}.get(verbosity, ())
default_archive_name_format = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'
archive_name_format = storage_config.get('archive_name_format', default_archive_name_format)
full_command = (
'borg', 'create',
'{repository}::{hostname}-{timestamp}'.format(
'{repository}::{archive_name_format}'.format(
repository=repository,
hostname=platform.node(),
timestamp=datetime.now().isoformat(),
archive_name_format=archive_name_format,
),
) + sources + exclude_flags + compression_flags + one_file_system_flags + \
remote_path_flags + umask_flags + verbosity_flags
subprocess.check_call(full_command)
subprocess.check_call(full_command)
+1 -1
View File
@@ -33,7 +33,7 @@ def prune_archives(verbosity, repository, retention_config, remote_path=None):
remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
verbosity_flags = {
VERBOSITY_SOME: ('--info', '--stats',),
VERBOSITY_LOTS: ('--debug', '--stats'),
VERBOSITY_LOTS: ('--debug', '--stats', '--list'),
}.get(verbosity, ())
full_command = (
+11 -1
View File
@@ -88,6 +88,13 @@ map:
type: scalar
desc: Umask to be used for borg create.
example: 0077
archive_name_format:
type: scalar
desc: |
Name of the archive. Borg placeholders can be used. See
https://borgbackup.readthedocs.io/en/stable/usage.html#borg-help-placeholders
Default is "{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}"
example: "{hostname}-documents-{now}"
retention:
desc: |
Retention policy for how many backups to keep in each category. See
@@ -119,7 +126,10 @@ map:
example: 1
prefix:
type: scalar
desc: When pruning, only consider archive names starting with this prefix.
desc: |
When pruning, only consider archive names starting with this prefix.
Borg placeholders can be used. See
https://borgbackup.readthedocs.io/en/stable/usage.html#borg-help-placeholders
example: sourcehostname
consistency:
desc: |
+53 -36
View File
@@ -48,16 +48,6 @@ def insert_subprocess_mock(check_call_command, **kwargs):
subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once()
def insert_platform_mock():
flexmock(module.platform).should_receive('node').and_return('host')
def insert_datetime_mock():
flexmock(module).datetime = flexmock().should_receive('now').and_return(
flexmock().should_receive('isoformat').and_return('now').mock
).mock
def test_make_exclude_flags_includes_exclude_patterns_filename_when_given():
exclude_flags = module._make_exclude_flags(
location_config={'exclude_patterns': ['*.pyc', '/var']},
@@ -88,6 +78,16 @@ def test_make_exclude_flags_includes_both_filenames_when_patterns_given_and_excl
assert exclude_flags == ('--exclude-from', 'excludes', '--exclude-from', '/tmp/excludes')
def test_make_exclude_flags_considers_none_exclude_from_filenames_as_empty():
flexmock(module).should_receive('_write_exclude_file').and_return(None)
exclude_flags = module._make_exclude_flags(
location_config={'exclude_from': None},
)
assert exclude_flags == ()
def test_make_exclude_flags_includes_exclude_caches_when_true_in_config():
exclude_flags = module._make_exclude_flags(
location_config={'exclude_caches': True},
@@ -118,15 +118,14 @@ def test_make_exclude_flags_is_empty_when_config_has_no_excludes():
assert exclude_flags == ()
CREATE_COMMAND = ('borg', 'create', 'repo::host-now', 'foo', 'bar')
DEFAULT_ARCHIVE_NAME = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'
CREATE_COMMAND = ('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'bar')
def test_create_archive_should_call_borg_with_parameters():
flexmock(module).should_receive('_write_exclude_file').and_return(None)
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(CREATE_COMMAND)
insert_platform_mock()
insert_datetime_mock()
module.create_archive(
verbosity=None,
@@ -145,8 +144,6 @@ def test_create_archive_with_exclude_patterns_should_call_borg_with_excludes():
flexmock(module).should_receive('_write_exclude_file').and_return(flexmock(name='/tmp/excludes'))
flexmock(module).should_receive('_make_exclude_flags').and_return(exclude_flags)
insert_subprocess_mock(CREATE_COMMAND + exclude_flags)
insert_platform_mock()
insert_datetime_mock()
module.create_archive(
verbosity=None,
@@ -164,8 +161,6 @@ def test_create_archive_with_verbosity_some_should_call_borg_with_info_parameter
flexmock(module).should_receive('_write_exclude_file').and_return(None)
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(CREATE_COMMAND + ('--info', '--stats',))
insert_platform_mock()
insert_datetime_mock()
module.create_archive(
verbosity=VERBOSITY_SOME,
@@ -183,8 +178,6 @@ def test_create_archive_with_verbosity_lots_should_call_borg_with_debug_paramete
flexmock(module).should_receive('_write_exclude_file').and_return(None)
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(CREATE_COMMAND + ('--debug', '--list', '--stats'))
insert_platform_mock()
insert_datetime_mock()
module.create_archive(
verbosity=VERBOSITY_LOTS,
@@ -202,8 +195,6 @@ def test_create_archive_with_compression_should_call_borg_with_compression_param
flexmock(module).should_receive('_write_exclude_file').and_return(None)
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(CREATE_COMMAND + ('--compression', 'rle'))
insert_platform_mock()
insert_datetime_mock()
module.create_archive(
verbosity=None,
@@ -221,8 +212,6 @@ def test_create_archive_with_one_file_system_should_call_borg_with_one_file_syst
flexmock(module).should_receive('_write_exclude_file').and_return(None)
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(CREATE_COMMAND + ('--one-file-system',))
insert_platform_mock()
insert_datetime_mock()
module.create_archive(
verbosity=None,
@@ -241,8 +230,6 @@ def test_create_archive_with_remote_path_should_call_borg_with_remote_path_param
flexmock(module).should_receive('_write_exclude_file').and_return(None)
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(CREATE_COMMAND + ('--remote-path', 'borg1'))
insert_platform_mock()
insert_datetime_mock()
module.create_archive(
verbosity=None,
@@ -261,8 +248,6 @@ def test_create_archive_with_umask_should_call_borg_with_umask_parameters():
flexmock(module).should_receive('_write_exclude_file').and_return(None)
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(CREATE_COMMAND + ('--umask', '740'))
insert_platform_mock()
insert_datetime_mock()
module.create_archive(
verbosity=None,
@@ -279,9 +264,7 @@ def test_create_archive_with_umask_should_call_borg_with_umask_parameters():
def test_create_archive_with_source_directories_glob_expands():
flexmock(module).should_receive('_write_exclude_file').and_return(None)
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(('borg', 'create', 'repo::host-now', 'foo', 'food'))
insert_platform_mock()
insert_datetime_mock()
insert_subprocess_mock(('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food'))
flexmock(module.glob).should_receive('glob').with_args('foo*').and_return(['foo', 'food'])
module.create_archive(
@@ -299,9 +282,7 @@ def test_create_archive_with_source_directories_glob_expands():
def test_create_archive_with_non_matching_source_directories_glob_passes_through():
flexmock(module).should_receive('_write_exclude_file').and_return(None)
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(('borg', 'create', 'repo::host-now', 'foo*'))
insert_platform_mock()
insert_datetime_mock()
insert_subprocess_mock(('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo*'))
flexmock(module.glob).should_receive('glob').with_args('foo*').and_return([])
module.create_archive(
@@ -319,9 +300,7 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through
def test_create_archive_with_glob_should_call_borg_with_expanded_directories():
flexmock(module).should_receive('_write_exclude_file').and_return(None)
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(('borg', 'create', 'repo::host-now', 'foo', 'food'))
insert_platform_mock()
insert_datetime_mock()
insert_subprocess_mock(('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food'))
flexmock(module.glob).should_receive('glob').with_args('foo*').and_return(['foo', 'food'])
module.create_archive(
@@ -334,3 +313,41 @@ def test_create_archive_with_glob_should_call_borg_with_expanded_directories():
},
storage_config={},
)
def test_create_archive_with_archive_name_format_without_placeholders():
flexmock(module).should_receive('_write_exclude_file').and_return(None)
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(('borg', 'create', 'repo::ARCHIVE_NAME', 'foo', 'bar'))
module.create_archive(
verbosity=None,
repository='repo',
location_config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
},
storage_config={
'archive_name_format': 'ARCHIVE_NAME',
},
)
def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
flexmock(module).should_receive('_write_exclude_file').and_return(None)
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(('borg', 'create', 'repo::Documents_{hostname}-{now}', 'foo', 'bar'))
module.create_archive(
verbosity=None,
repository='repo',
location_config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
},
storage_config={
'archive_name_format': 'Documents_{hostname}-{now}',
},
)
+20 -1
View File
@@ -32,6 +32,24 @@ def test_make_prune_flags_should_return_flags_from_config():
assert tuple(result) == BASE_PRUNE_FLAGS
def test_make_prune_flags_accepts_prefix_with_placeholders():
retention_config = OrderedDict(
(
('keep_daily', 1),
('prefix', 'Documents_{hostname}-{now}'),
)
)
result = module._make_prune_flags(retention_config)
expected = (
('--keep-daily', '1'),
('--prefix', 'Documents_{hostname}-{now}'),
)
assert tuple(result) == expected
PRUNE_COMMAND = (
'borg', 'prune', 'repo', '--keep-daily', '1', '--keep-weekly', '2', '--keep-monthly', '3',
)
@@ -70,7 +88,7 @@ def test_prune_archives_with_verbosity_lots_should_call_borg_with_debug_paramete
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS,
)
insert_subprocess_mock(PRUNE_COMMAND + ('--debug', '--stats',))
insert_subprocess_mock(PRUNE_COMMAND + ('--debug', '--stats', '--list'))
module.prune_archives(
repository='repo',
@@ -78,6 +96,7 @@ def test_prune_archives_with_verbosity_lots_should_call_borg_with_debug_paramete
retention_config=retention_config,
)
def test_prune_archives_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(
+1 -1
View File
@@ -1,7 +1,7 @@
from setuptools import setup, find_packages
VERSION = '1.1.6'
VERSION = '1.1.7'
setup(