mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-25 03:13:02 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb18a9a3f2 | ||
|
|
95533d2b31 | ||
|
|
3af92f8b92 | ||
|
|
d127e73590 | ||
|
|
50c4f6f2a1 | ||
|
|
37ae34a432 | ||
|
|
3664ac7418 |
@@ -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,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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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}',
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user