Compare commits

...
17 Commits
Author SHA1 Message Date
Dan 3e26e70d0c Fix for incorrect /etc/borgmatic.d/ configuration path probing on macOS. 2017-11-10 21:33:29 -08:00
Dan 5ce25e2790 Re-fixing logo image. 2017-11-04 11:55:48 -07:00
Dan 8243552c8c Fixing PNG path. 2017-11-04 11:54:29 -07:00
Dan 425e27dee5 Add "ssh_command" to configuration for specifying a custom SSH command or options. 2017-11-03 22:01:04 -07:00
Dan 9ec9269a18 Link to repository encryption section of Borg Quick Start. 2017-11-03 20:28:31 -07:00
Dan bf5cbd1deb Mentioning use of BORG_PASSPHRASE environment variable. 2017-11-03 20:27:21 -07:00
Dan 4c09cbf1a4 Releasing. 2017-11-02 22:38:16 -07:00
Dan fc077af4ce Mentioning borgmatic --config option in docs. 2017-11-02 22:28:53 -07:00
Dan ca4312bb85 Support for Borg --remote-ratelimit for limiting upload rate. And log Borg commands. 2017-11-02 22:22:40 -07:00
Dan fc3b1fccba Support for Borg --files-cache option for setting the files cache operation mode. 2017-11-02 22:03:11 -07:00
Dan f83346b9b3 Support for using tilde in repository paths to reference home directory. 2017-11-02 21:34:04 -07:00
Dan 63c7241aec Typo in comment. 2017-10-31 22:21:49 -07:00
Dan fd77dc579e Pass through several more Unix signals that Borg happens to consume. 2017-10-31 22:10:00 -07:00
Dan f017ed648f Disabling code coverage on this one-line functions. 2017-10-31 22:01:18 -07:00
Dan 27a6745743 Passing the Unix SIGTERM signal through to child processes like Borg. 2017-10-31 21:58:35 -07:00
Dan 95be0c8e46 Removing broken download URL. 2017-10-29 21:42:01 -07:00
Dan 17a774ba7e Removing .hg* files. 2017-10-29 20:32:21 -07:00
15 changed files with 169 additions and 72 deletions
-9
View File
@@ -1,9 +0,0 @@
syntax: glob
*.egg-info
*.pyc
*.swp
.cache
.coverage
.tox
build
dist
-40
View File
@@ -1,40 +0,0 @@
467d3a3ce9185e84ee51ca9156499162efd94f9a 0.0.2
7730ae34665c0dedf46deab90b32780abf6dbaff 0.0.3
4bb2e81fc77038be4499b7ea6797ab7d109460e0 0.0.4
b31d51b633701554e84f996cc0c73bad2990780b 0.0.5
b31d51b633701554e84f996cc0c73bad2990780b 0.0.5
aa8a807f4ba28f0652764ed14713ffea2fd6922d 0.0.5
aa8a807f4ba28f0652764ed14713ffea2fd6922d 0.0.5
569aef47a9b25c55b13753f94706f5d330219995 0.0.5
569aef47a9b25c55b13753f94706f5d330219995 0.0.5
a03495a8e8b471da63b5e2ae79d3ff9065839c2a 0.0.5
7ea93ca83f426ec0a608a68580c72c0775b81f86 0.0.6
cf4c7065f0711deda1cba878398bc05390e2c3f9 0.0.7
38d72677343f0a5d6845f4ac50d6778397083d45 0.1.0
ac5dfa01e9d14d09845f5e94c2c679e21c5eb2f9 0.1.1
ac5dfa01e9d14d09845f5e94c2c679e21c5eb2f9 0.1.1
7b6c87dca7ea312b2257ac1b46857b3f8c56b39c 0.1.1
83067f995dd391e38544a7722dc3b254b59c5521 0.1.2
acc7fb61566fe8028c179f43ecc735c851220b06 0.1.3
6dda59c12de88f060eb7244e6d330173985a9639 0.1.4
6dda59c12de88f060eb7244e6d330173985a9639 0.1.4
e58246fc92bb22c2b2fd8b86a1227de69d2d0315 0.1.4
0afff209b902698c2266986129d6dc9f5f913101 0.1.5
4c63f3d90ec2bf6af1714a3acec84654a7c9edf3 0.1.6
5a458ebef804be14e30d7375e3e9fbc26aedb80d 0.1.7
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
9603d13910b32d57a887765cab694ac5d0acc1f4 1.0.2
32c6341dda9fad77a3982641bce8a3a45821842e 1.0.3
5a003056a8ff4709c5bd4d6d33354199423f8a1d 1.1.0
7d3d11eff6c0773883c48f221431f157bc7995eb 1.1.1
f052a77a8ad5a0fea7fa86a902e0e401252f7d80 1.1.2
3f838f661546e04529b453aa443529b432afc243 1.1.3
3d605962d891731a0f372b903b556ac7a8c8359f 1.1.4
64ca13bfe050f656b44ed2eb1c3db045bfddd133 1.1.5
4daa944c122c572b9b56bfcac3f4e2869181c630 1.1.6
ec7949a14a2051616f7cdcb8e05555f02e024ae8 1.1.7
+13
View File
@@ -1,3 +1,16 @@
1.1.11
* #25: Add "ssh_command" to configuration for specifying a custom SSH command or options.
* Fix for incorrect /etc/borgmatic.d/ configuration path probing on macOS. This problem manifested
as an error on startup: "[Errno 2] No such file or directory: '/etc/borgmatic.d'".
1.1.10
* Pass several Unix signals through to child processes like Borg. This means that Borg now properly
shuts down if borgmatic is terminated (e.g. due to a system suspend).
* #29: Support for using tilde in repository paths to reference home directory.
* #42: Support for Borg --files-cache option for setting the files cache operation mode.
* #44: Support for Borg --remote-ratelimit for limiting upload rate.
* Log invoked Borg commands when at highest verbosity level.
1.1.9
* #16, #38: Support for user-defined hooks before/after backup, or on error.
* #33: Improve clarity of logging spew at high verbosity levels.
+12 -6
View File
@@ -1,4 +1,4 @@
<img src="https://projects.torsion.org/witten/borgmatic/raw/master/static/borgmatic.png" width="150px" style="float: right; padding-left: 1em;">
<img src="https://projects.torsion.org/witten/borgmatic/raw/branch/master/static/borgmatic.png" width="150px" style="float: right; padding-left: 1em;">
## Overview
@@ -49,11 +49,13 @@ on [GitHub](https://github.com/witten/borgmatic) for convenience.
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.
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 either need to set the borgmatic
`encryption_passphrase` configuration variable or set the `BORG_PASSPHRASE`
environment variable. See the [repository encryption
section](https://borgbackup.readthedocs.io/en/latest/quickstart.html#repository-encryption)
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.
@@ -113,6 +115,10 @@ When you set up multiple configuration files like this, borgmatic will run
each one in turn from a single borgmatic invocation. This includes, by
default, the traditional /etc/borgmatic/config.yaml as well.
And if you need even more customizability, you can specify alternate
configuration paths on the command-line with borgmatic's `--config` option.
See `borgmatic --help` for more information.
## Upgrading
+5
View File
@@ -1,3 +1,4 @@
import logging
import os
import subprocess
@@ -8,6 +9,9 @@ from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
DEFAULT_CHECKS = ('repository', 'archives')
logger = logging.getLogger(__name__)
def _parse_checks(consistency_config):
'''
Given a consistency config with a "checks" list, transform it to a tuple of named checks to run.
@@ -79,6 +83,7 @@ def check_archives(verbosity, repository, consistency_config, remote_path=None):
# The check command spews to stdout/stderr even without the verbose flag. Suppress it.
stdout = None if verbosity_flags else open(os.devnull, 'w')
logger.debug(' '.join(full_command))
subprocess.check_call(full_command, stdout=stdout, stderr=subprocess.STDOUT)
if 'extract' in checks:
+17 -4
View File
@@ -1,5 +1,6 @@
import glob
import itertools
import logging
import os
import subprocess
import tempfile
@@ -7,12 +8,18 @@ import tempfile
from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
def initialize(storage_config):
passphrase = storage_config.get('encryption_passphrase')
logger = logging.getLogger(__name__)
def initialize_environment(storage_config):
passphrase = storage_config.get('encryption_passphrase')
if passphrase:
os.environ['BORG_PASSPHRASE'] = passphrase
ssh_command = storage_config.get('ssh_command')
if ssh_command:
os.environ['BORG_RSH'] = ssh_command
def _expand_directory(directory):
'''
@@ -81,9 +88,13 @@ def create_archive(
)
compression = storage_config.get('compression', None)
compression_flags = ('--compression', compression) if compression else ()
remote_rate_limit = storage_config.get('remote_rate_limit', None)
remote_rate_limit_flags = ('--remote-ratelimit', str(remote_rate_limit)) if remote_rate_limit else ()
umask = storage_config.get('umask', None)
umask_flags = ('--umask', str(umask)) if umask else ()
one_file_system_flags = ('--one-file-system',) if location_config.get('one_file_system') else ()
files_cache = location_config.get('files_cache')
files_cache_flags = ('--files-cache', files_cache) if files_cache else ()
remote_path = location_config.get('remote_path')
remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
verbosity_flags = {
@@ -99,7 +110,9 @@ def create_archive(
repository=repository,
archive_name_format=archive_name_format,
),
) + sources + exclude_flags + compression_flags + one_file_system_flags + \
remote_path_flags + umask_flags + verbosity_flags
) + sources + exclude_flags + compression_flags + remote_rate_limit_flags + \
one_file_system_flags + files_cache_flags + remote_path_flags + umask_flags + \
verbosity_flags
logger.debug(' '.join(full_command))
subprocess.check_call(full_command)
+5
View File
@@ -1,9 +1,13 @@
import logging
import sys
import subprocess
from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
logger = logging.getLogger(__name__)
def extract_last_archive_dry_run(verbosity, repository, remote_path=None):
'''
Perform an extraction dry-run of just the most recent archive. If there are no archives, skip
@@ -37,4 +41,5 @@ def extract_last_archive_dry_run(verbosity, repository, remote_path=None):
),
) + remote_path_flags + verbosity_flags + list_flag
logger.debug(' '.join(full_extract_command))
subprocess.check_call(full_extract_command)
+5
View File
@@ -1,8 +1,12 @@
import logging
import subprocess
from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
logger = logging.getLogger(__name__)
def _make_prune_flags(retention_config):
'''
Given a retention config dict mapping from option name to value, tranform it into an iterable of
@@ -48,4 +52,5 @@ def prune_archives(verbosity, repository, retention_config, remote_path=None):
for element in pair
) + remote_path_flags + verbosity_flags
logger.debug(' '.join(full_command))
subprocess.check_call(full_command)
+5 -2
View File
@@ -8,6 +8,7 @@ import sys
from borgmatic.borg import check, create, prune
from borgmatic.commands import hook
from borgmatic.config import collect, convert, validate
from borgmatic.signals import configure_signals
from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS, verbosity_to_log_level
@@ -93,10 +94,11 @@ def run_configuration(config_filename, args): # pragma: no cover
try:
remote_path = location.get('remote_path')
create.initialize(storage)
create.initialize_environment(storage)
hook.execute_hook(hooks.get('before_backup'), config_filename, 'pre-backup')
for repository in location['repositories']:
for unexpanded_repository in location['repositories']:
repository = os.path.expanduser(unexpanded_repository)
if args.prune:
logger.info('{}: Pruning archives'.format(repository))
prune.prune_archives(args.verbosity, repository, retention, remote_path=remote_path)
@@ -120,6 +122,7 @@ def run_configuration(config_filename, args): # pragma: no cover
def main(): # pragma: no cover
try:
configure_signals()
args = parse_arguments(*sys.argv[1:])
logging.basicConfig(level=verbosity_to_log_level(args.verbosity), format='%(message)s')
+5 -3
View File
@@ -11,13 +11,15 @@ def collect_config_filenames(config_paths):
files. This is non-recursive, so any directories within the given directories are ignored.
Return paths even if they don't exist on disk, so the user can find out about missing
configuration paths. However, skip /etc/borgmatic.d if it's missing, so the user doesn't have to
create it unless they need it.
configuration paths. However, skip a default config path if it's missing, so the user doesn't
have to create a default config path unless they need it.
'''
real_default_config_paths = set(map(os.path.realpath, DEFAULT_CONFIG_PATHS))
for path in config_paths:
exists = os.path.exists(path)
if os.path.realpath(path) in DEFAULT_CONFIG_PATHS and not exists:
if os.path.realpath(path) in real_default_config_paths and not exists:
continue
if not os.path.isdir(path) or not exists:
+17 -2
View File
@@ -22,6 +22,13 @@ map:
type: bool
desc: Stay in same file system (do not cross mount points).
example: true
files_cache:
type: scalar
desc: |
Mode in which to operate the files cache. See
https://borgbackup.readthedocs.io/en/stable/usage/create.html#description for
details.
example: ctime,size,inode
remote_path:
type: scalar
desc: Alternate Borg remote executable. Defaults to "borg".
@@ -31,8 +38,8 @@ map:
seq:
- type: scalar
desc: |
Paths to local or remote repositories (required). Multiple repositories are
backed up to in sequence.
Paths to local or remote repositories (required). Tildes are expanded. Multiple
repositories are backed up to in sequence.
example:
- user@backupserver:sourcehostname.borg
exclude_patterns:
@@ -85,6 +92,14 @@ map:
https://borgbackup.readthedocs.org/en/stable/usage.html#borg-create for details.
Defaults to no compression.
example: lz4
remote_rate_limit:
type: int
desc: Remote network upload rate limit in kiBytes/second.
example: 100
ssh_command:
type: scalar
desc: Command to use instead of just "ssh". This can be used to specify ssh options.
example: ssh -i /path/to/private/key
umask:
type: scalar
desc: Umask to be used for borg create.
+18
View File
@@ -0,0 +1,18 @@
import os
import signal
def _handle_signal(signal_number, frame): # pragma: no cover
'''
Send the signal to all processes in borgmatic's process group, which includes child process.
'''
os.killpg(os.getpgrp(), signal_number)
def configure_signals(): # pragma: no cover
'''
Configure borgmatic's signal handlers to pass relevant signals through to any child processes
like Borg. Note that SIGINT gets passed through even without these changes.
'''
for signal_number in (signal.SIGHUP, signal.SIGTERM, signal.SIGUSR1, signal.SIGUSR2):
signal.signal(signal_number, _handle_signal)
+53 -4
View File
@@ -6,24 +6,36 @@ from borgmatic.borg import create as module
from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
def test_initialize_with_passphrase_should_set_environment():
def test_initialize_environment_with_passphrase_should_set_environment():
orig_environ = os.environ
try:
os.environ = {}
module.initialize({'encryption_passphrase': 'pass'})
module.initialize_environment({'encryption_passphrase': 'pass'})
assert os.environ.get('BORG_PASSPHRASE') == 'pass'
finally:
os.environ = orig_environ
def test_initialize_without_passphrase_should_not_set_environment():
def test_initialize_environment_with_ssh_command_should_set_environment():
orig_environ = os.environ
try:
os.environ = {}
module.initialize({})
module.initialize_environment({'ssh_command': 'ssh -C'})
assert os.environ.get('BORG_RSH') == 'ssh -C'
finally:
os.environ = orig_environ
def test_initialize_environment_without_configuration_should_not_set_environment():
orig_environ = os.environ
try:
os.environ = {}
module.initialize_environment({})
assert os.environ.get('BORG_PASSPHRASE') == None
assert os.environ.get('BORG_RSH') == None
finally:
os.environ = orig_environ
@@ -231,6 +243,24 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters(
)
def test_create_archive_with_remote_rate_limit_calls_borg_with_remote_ratelimit_parameters():
flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar'])
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-ratelimit', '100'))
module.create_archive(
verbosity=None,
repository='repo',
location_config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
},
storage_config={'remote_rate_limit': 100},
)
def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_parameters():
flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar'])
flexmock(module).should_receive('_write_exclude_file').and_return(None)
@@ -250,6 +280,25 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par
)
def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters():
flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar'])
flexmock(module).should_receive('_write_exclude_file').and_return(None)
flexmock(module).should_receive('_make_exclude_flags').and_return(())
insert_subprocess_mock(CREATE_COMMAND + ('--files-cache', 'ctime,size'))
module.create_archive(
verbosity=None,
repository='repo',
location_config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'files_cache': 'ctime,size',
'exclude_patterns': None,
},
storage_config={},
)
def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters():
flexmock(module).should_receive('_expand_directory').and_return(['foo']).and_return(['bar'])
flexmock(module).should_receive('_write_exclude_file').and_return(None)
@@ -58,6 +58,19 @@ def test_collect_config_filenames_skips_etc_borgmatic_dot_d_if_it_does_not_exist
assert config_filenames == ('config.yaml',)
def test_collect_config_filenames_skips_non_canonical_etc_borgmatic_dot_d_if_it_does_not_exist():
config_paths = ('config.yaml', '/etc/../etc/borgmatic.d')
mock_path = flexmock(module.os.path)
mock_path.should_receive('exists').with_args('config.yaml').and_return(True)
mock_path.should_receive('exists').with_args('/etc/../etc/borgmatic.d').and_return(False)
mock_path.should_receive('isdir').with_args('config.yaml').and_return(False)
mock_path.should_receive('isdir').with_args('/etc/../etc/borgmatic.d').and_return(True)
config_filenames = tuple(module.collect_config_filenames(config_paths))
assert config_filenames == ('config.yaml',)
def test_collect_config_filenames_includes_other_directory_if_it_does_not_exist():
config_paths = ('config.yaml', '/my/directory')
mock_path = flexmock(module.os.path)
+1 -2
View File
@@ -1,7 +1,7 @@
from setuptools import setup, find_packages
VERSION = '1.1.9'
VERSION = '1.1.11'
setup(
@@ -11,7 +11,6 @@ setup(
author='Dan Helfman',
author_email='witten@torsion.org',
url='https://torsion.org/borgmatic',
download_url='https://projects.torsion.org/witten/borgmatic/archive/%s.tar.gz' % VERSION,
classifiers=(
'Development Status :: 5 - Production/Stable',
'Environment :: Console',