mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-08-06 16:23:00 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac777965d0 | ||
|
|
31d3bc9bd8 | ||
|
|
2115eeb6a2 | ||
|
|
08f017bc3e | ||
|
|
7bc9482970 | ||
|
|
57ffad4e04 | ||
|
|
5422d14f93 | ||
|
|
e6d8c736d0 | ||
|
|
18d3542fbc | ||
|
|
93f453cecf | ||
|
|
505bb778fa | ||
|
|
b09d464162 | ||
|
|
a9104ed090 | ||
|
|
06f134cc71 | ||
|
|
584359b6c0 | ||
|
|
26a1a3d1e0 | ||
|
|
6da05cbe2d | ||
|
|
f48f52079d | ||
|
|
76c569cf84 | ||
|
|
b121290c0f | ||
|
|
8fd46b8c70 | ||
|
|
603f525352 | ||
|
|
8c8640d0ab | ||
|
|
22640a9ca0 |
+10
@@ -40,6 +40,16 @@ steps:
|
|||||||
- scripts/run-tests
|
- scripts/run-tests
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
|
name: python-3-8-alpine-3-10
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build
|
||||||
|
image: python:3.8-alpine3.10
|
||||||
|
pull: always
|
||||||
|
commands:
|
||||||
|
- scripts/run-tests
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
name: documentation
|
name: documentation
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
1.4.7
|
||||||
|
* #238: In documentation, clarify when Healthchecks/Cronitor hooks fire in relation to other hooks.
|
||||||
|
* #239: Upgrade your borgmatic configuration to get new options and comments via
|
||||||
|
"generate-borgmatic-config --source". See the documentation for more information:
|
||||||
|
https://torsion.org/borgmatic/docs/how-to/upgrade/#upgrading-your-configuration
|
||||||
|
|
||||||
|
1.4.6
|
||||||
|
* Verbosity level "-1" for even quieter output: Errors only (#236).
|
||||||
|
|
||||||
|
1.4.5
|
||||||
|
* Log to file instead of syslog via command-line "--log-file" flag (#233).
|
||||||
|
|
||||||
|
1.4.4
|
||||||
|
* #234: Support for Borg --keep-exclude-tags and --exclude-nodump options.
|
||||||
|
|
||||||
|
1.4.3
|
||||||
|
* Monitor backups with Cronitor hook integration. See the documentation for more information:
|
||||||
|
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronitor-hook
|
||||||
|
|
||||||
1.4.2
|
1.4.2
|
||||||
* Extract files to a particular directory via "borgmatic extract --destination" flag.
|
* Extract files to a particular directory via "borgmatic extract --destination" flag.
|
||||||
* Rename "borgmatic extract --restore-path" flag to "--path" to reduce confusion with the separate
|
* Rename "borgmatic extract --restore-path" flag to "--path" to reduce confusion with the separate
|
||||||
|
|||||||
@@ -90,8 +90,18 @@ def _make_exclude_flags(location_config, exclude_filename=None):
|
|||||||
caches_flag = ('--exclude-caches',) if location_config.get('exclude_caches') else ()
|
caches_flag = ('--exclude-caches',) if location_config.get('exclude_caches') else ()
|
||||||
if_present = location_config.get('exclude_if_present')
|
if_present = location_config.get('exclude_if_present')
|
||||||
if_present_flags = ('--exclude-if-present', if_present) if if_present else ()
|
if_present_flags = ('--exclude-if-present', if_present) if if_present else ()
|
||||||
|
keep_exclude_tags_flags = (
|
||||||
|
('--keep-exclude-tags',) if location_config.get('keep_exclude_tags') else ()
|
||||||
|
)
|
||||||
|
exclude_nodump_flags = ('--exclude-nodump',) if location_config.get('exclude_nodump') else ()
|
||||||
|
|
||||||
return exclude_from_flags + caches_flag + if_present_flags
|
return (
|
||||||
|
exclude_from_flags
|
||||||
|
+ caches_flag
|
||||||
|
+ if_present_flags
|
||||||
|
+ keep_exclude_tags_flags
|
||||||
|
+ exclude_nodump_flags
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
BORGMATIC_SOURCE_DIRECTORY = '~/.borgmatic'
|
BORGMATIC_SOURCE_DIRECTORY = '~/.borgmatic'
|
||||||
|
|||||||
@@ -138,16 +138,29 @@ def parse_arguments(*unparsed_arguments):
|
|||||||
'-v',
|
'-v',
|
||||||
'--verbosity',
|
'--verbosity',
|
||||||
type=int,
|
type=int,
|
||||||
choices=range(0, 3),
|
choices=range(-1, 3),
|
||||||
default=0,
|
default=0,
|
||||||
help='Display verbose progress to the console (from none to lots: 0, 1, or 2)',
|
help='Display verbose progress to the console (from none to lots: 0, 1, or 2) or only errors (-1)',
|
||||||
)
|
)
|
||||||
global_group.add_argument(
|
global_group.add_argument(
|
||||||
'--syslog-verbosity',
|
'--syslog-verbosity',
|
||||||
type=int,
|
type=int,
|
||||||
choices=range(0, 3),
|
choices=range(-1, 3),
|
||||||
default=0,
|
default=0,
|
||||||
help='Display verbose progress to syslog (from none to lots: 0, 1, or 2). Ignored when console is interactive',
|
help='Log verbose progress to syslog (from none to lots: 0, 1, or 2) or only errors (-1). Ignored when console is interactive or --log-file is given',
|
||||||
|
)
|
||||||
|
global_group.add_argument(
|
||||||
|
'--log-file-verbosity',
|
||||||
|
type=int,
|
||||||
|
choices=range(-1, 3),
|
||||||
|
default=0,
|
||||||
|
help='Log verbose progress to log file (from none to lots: 0, 1, or 2) or only errors (-1). Only used when --log-file is given',
|
||||||
|
)
|
||||||
|
global_group.add_argument(
|
||||||
|
'--log-file',
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help='Write log messages to this file instead of syslog',
|
||||||
)
|
)
|
||||||
global_group.add_argument(
|
global_group.add_argument(
|
||||||
'--version',
|
'--version',
|
||||||
@@ -273,7 +286,7 @@ def parse_arguments(*unparsed_arguments):
|
|||||||
'--repository',
|
'--repository',
|
||||||
help='Path of repository to extract, defaults to the configured repository if there is only one',
|
help='Path of repository to extract, defaults to the configured repository if there is only one',
|
||||||
)
|
)
|
||||||
extract_group.add_argument('--archive', help='Name of archive to operate on', required=True)
|
extract_group.add_argument('--archive', help='Name of archive to extract', required=True)
|
||||||
extract_group.add_argument(
|
extract_group.add_argument(
|
||||||
'--path',
|
'--path',
|
||||||
'--restore-path',
|
'--restore-path',
|
||||||
@@ -311,7 +324,7 @@ def parse_arguments(*unparsed_arguments):
|
|||||||
'--repository',
|
'--repository',
|
||||||
help='Path of repository to restore from, defaults to the configured repository if there is only one',
|
help='Path of repository to restore from, defaults to the configured repository if there is only one',
|
||||||
)
|
)
|
||||||
restore_group.add_argument('--archive', help='Name of archive to operate on', required=True)
|
restore_group.add_argument('--archive', help='Name of archive to restore from', required=True)
|
||||||
restore_group.add_argument(
|
restore_group.add_argument(
|
||||||
'--database',
|
'--database',
|
||||||
metavar='NAME',
|
metavar='NAME',
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from borgmatic.borg import list as borg_list
|
|||||||
from borgmatic.borg import prune as borg_prune
|
from borgmatic.borg import prune as borg_prune
|
||||||
from borgmatic.commands.arguments import parse_arguments
|
from borgmatic.commands.arguments import parse_arguments
|
||||||
from borgmatic.config import checks, collect, convert, validate
|
from borgmatic.config import checks, collect, convert, validate
|
||||||
from borgmatic.hooks import command, healthchecks, postgresql
|
from borgmatic.hooks import command, cronitor, healthchecks, postgresql
|
||||||
from borgmatic.logger import configure_logging, should_do_markup
|
from borgmatic.logger import configure_logging, should_do_markup
|
||||||
from borgmatic.signals import configure_signals
|
from borgmatic.signals import configure_signals
|
||||||
from borgmatic.verbosity import verbosity_to_log_level
|
from borgmatic.verbosity import verbosity_to_log_level
|
||||||
@@ -56,6 +56,9 @@ def run_configuration(config_filename, config, arguments):
|
|||||||
healthchecks.ping_healthchecks(
|
healthchecks.ping_healthchecks(
|
||||||
hooks.get('healthchecks'), config_filename, global_arguments.dry_run, 'start'
|
hooks.get('healthchecks'), config_filename, global_arguments.dry_run, 'start'
|
||||||
)
|
)
|
||||||
|
cronitor.ping_cronitor(
|
||||||
|
hooks.get('cronitor'), config_filename, global_arguments.dry_run, 'run'
|
||||||
|
)
|
||||||
command.execute_hook(
|
command.execute_hook(
|
||||||
hooks.get('before_backup'),
|
hooks.get('before_backup'),
|
||||||
hooks.get('umask'),
|
hooks.get('umask'),
|
||||||
@@ -108,6 +111,9 @@ def run_configuration(config_filename, config, arguments):
|
|||||||
healthchecks.ping_healthchecks(
|
healthchecks.ping_healthchecks(
|
||||||
hooks.get('healthchecks'), config_filename, global_arguments.dry_run
|
hooks.get('healthchecks'), config_filename, global_arguments.dry_run
|
||||||
)
|
)
|
||||||
|
cronitor.ping_cronitor(
|
||||||
|
hooks.get('cronitor'), config_filename, global_arguments.dry_run, 'complete'
|
||||||
|
)
|
||||||
except (OSError, CalledProcessError) as error:
|
except (OSError, CalledProcessError) as error:
|
||||||
encountered_error = error
|
encountered_error = error
|
||||||
yield from make_error_log_records(
|
yield from make_error_log_records(
|
||||||
@@ -129,6 +135,9 @@ def run_configuration(config_filename, config, arguments):
|
|||||||
healthchecks.ping_healthchecks(
|
healthchecks.ping_healthchecks(
|
||||||
hooks.get('healthchecks'), config_filename, global_arguments.dry_run, 'fail'
|
hooks.get('healthchecks'), config_filename, global_arguments.dry_run, 'fail'
|
||||||
)
|
)
|
||||||
|
cronitor.ping_cronitor(
|
||||||
|
hooks.get('cronitor'), config_filename, global_arguments.dry_run, 'fail'
|
||||||
|
)
|
||||||
except (OSError, CalledProcessError) as error:
|
except (OSError, CalledProcessError) as error:
|
||||||
yield from make_error_log_records(
|
yield from make_error_log_records(
|
||||||
'{}: Error running on-error hook'.format(config_filename), error
|
'{}: Error running on-error hook'.format(config_filename), error
|
||||||
@@ -475,10 +484,17 @@ def main(): # pragma: no cover
|
|||||||
configs, parse_logs = load_configurations(config_filenames)
|
configs, parse_logs = load_configurations(config_filenames)
|
||||||
|
|
||||||
colorama.init(autoreset=True, strip=not should_do_markup(global_arguments.no_color, configs))
|
colorama.init(autoreset=True, strip=not should_do_markup(global_arguments.no_color, configs))
|
||||||
configure_logging(
|
try:
|
||||||
verbosity_to_log_level(global_arguments.verbosity),
|
configure_logging(
|
||||||
verbosity_to_log_level(global_arguments.syslog_verbosity),
|
verbosity_to_log_level(global_arguments.verbosity),
|
||||||
)
|
verbosity_to_log_level(global_arguments.syslog_verbosity),
|
||||||
|
verbosity_to_log_level(global_arguments.log_file_verbosity),
|
||||||
|
global_arguments.log_file,
|
||||||
|
)
|
||||||
|
except (FileNotFoundError, PermissionError) as error:
|
||||||
|
configure_logging(logging.CRITICAL)
|
||||||
|
logger.critical('Error configuring logging: {}'.format(error))
|
||||||
|
exit_with_help_link()
|
||||||
|
|
||||||
logger.debug('Ensuring legacy configuration is upgraded')
|
logger.debug('Ensuring legacy configuration is upgraded')
|
||||||
convert.guard_configuration_upgraded(LEGACY_CONFIG_PATH, config_filenames)
|
convert.guard_configuration_upgraded(LEGACY_CONFIG_PATH, config_filenames)
|
||||||
|
|||||||
@@ -12,12 +12,18 @@ def parse_arguments(*arguments):
|
|||||||
them as an ArgumentParser instance.
|
them as an ArgumentParser instance.
|
||||||
'''
|
'''
|
||||||
parser = ArgumentParser(description='Generate a sample borgmatic YAML configuration file.')
|
parser = ArgumentParser(description='Generate a sample borgmatic YAML configuration file.')
|
||||||
|
parser.add_argument(
|
||||||
|
'-s',
|
||||||
|
'--source',
|
||||||
|
dest='source_filename',
|
||||||
|
help='Optional YAML configuration file to merge into the generated configuration, useful for upgrading your configuration',
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-d',
|
'-d',
|
||||||
'--destination',
|
'--destination',
|
||||||
dest='destination_filename',
|
dest='destination_filename',
|
||||||
default=DEFAULT_DESTINATION_CONFIG_FILENAME,
|
default=DEFAULT_DESTINATION_CONFIG_FILENAME,
|
||||||
help='Destination YAML configuration filename. Default: {}'.format(
|
help='Destination YAML configuration file. Default: {}'.format(
|
||||||
DEFAULT_DESTINATION_CONFIG_FILENAME
|
DEFAULT_DESTINATION_CONFIG_FILENAME
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -30,11 +36,21 @@ def main(): # pragma: no cover
|
|||||||
args = parse_arguments(*sys.argv[1:])
|
args = parse_arguments(*sys.argv[1:])
|
||||||
|
|
||||||
generate.generate_sample_configuration(
|
generate.generate_sample_configuration(
|
||||||
args.destination_filename, validate.schema_filename()
|
args.source_filename, args.destination_filename, validate.schema_filename()
|
||||||
)
|
)
|
||||||
|
|
||||||
print('Generated a sample configuration file at {}.'.format(args.destination_filename))
|
print('Generated a sample configuration file at {}.'.format(args.destination_filename))
|
||||||
print()
|
print()
|
||||||
|
if args.source_filename:
|
||||||
|
print(
|
||||||
|
'Merged in the contents of configuration file at {}.'.format(args.source_filename)
|
||||||
|
)
|
||||||
|
print('To review the changes made, run:')
|
||||||
|
print()
|
||||||
|
print(
|
||||||
|
' diff --unified {} {}'.format(args.source_filename, args.destination_filename)
|
||||||
|
)
|
||||||
|
print()
|
||||||
print('Please edit the file to suit your needs. The values are representative.')
|
print('Please edit the file to suit your needs. The values are representative.')
|
||||||
print('All fields are optional except where indicated.')
|
print('All fields are optional except where indicated.')
|
||||||
print()
|
print()
|
||||||
|
|||||||
+117
-30
@@ -1,9 +1,12 @@
|
|||||||
|
import collections
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from ruamel import yaml
|
from ruamel import yaml
|
||||||
|
|
||||||
|
from borgmatic.config import load
|
||||||
|
|
||||||
INDENT = 4
|
INDENT = 4
|
||||||
SEQUENCE_INDENT = 2
|
SEQUENCE_INDENT = 2
|
||||||
|
|
||||||
@@ -40,8 +43,8 @@ def _schema_to_sample_configuration(schema, level=0, parent_is_sequence=False):
|
|||||||
elif 'map' in schema:
|
elif 'map' in schema:
|
||||||
config = yaml.comments.CommentedMap(
|
config = yaml.comments.CommentedMap(
|
||||||
[
|
[
|
||||||
(section_name, _schema_to_sample_configuration(section_schema, level + 1))
|
(field_name, _schema_to_sample_configuration(sub_schema, level + 1))
|
||||||
for section_name, section_schema in schema['map'].items()
|
for field_name, sub_schema in schema['map'].items()
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
indent = (level * INDENT) + (SEQUENCE_INDENT if parent_is_sequence else 0)
|
indent = (level * INDENT) + (SEQUENCE_INDENT if parent_is_sequence else 0)
|
||||||
@@ -68,37 +71,32 @@ def _comment_out_line(line):
|
|||||||
return '# '.join((indent_spaces, line[count_indent_spaces:]))
|
return '# '.join((indent_spaces, line[count_indent_spaces:]))
|
||||||
|
|
||||||
|
|
||||||
REQUIRED_KEYS = {'source_directories', 'repositories', 'keep_daily'}
|
|
||||||
REQUIRED_SECTION_NAMES = {'location', 'retention'}
|
|
||||||
|
|
||||||
|
|
||||||
def _comment_out_optional_configuration(rendered_config):
|
def _comment_out_optional_configuration(rendered_config):
|
||||||
'''
|
'''
|
||||||
Post-process a rendered configuration string to comment out optional key/values. The idea is
|
Post-process a rendered configuration string to comment out optional key/values, as determined
|
||||||
that this prevents the user from having to comment out a bunch of configuration they don't care
|
by a sentinel in the comment before each key.
|
||||||
about to get to a minimal viable configuration file.
|
|
||||||
|
|
||||||
Ideally ruamel.yaml would support this during configuration generation, but it's not terribly
|
The idea is that the pre-commented configuration prevents the user from having to comment out a
|
||||||
easy to accomplish that way.
|
bunch of configuration they don't care about to get to a minimal viable configuration file.
|
||||||
|
|
||||||
|
Ideally ruamel.yaml would support commenting out keys during configuration generation, but it's
|
||||||
|
not terribly easy to accomplish that way.
|
||||||
'''
|
'''
|
||||||
lines = []
|
lines = []
|
||||||
required = False
|
optional = False
|
||||||
|
|
||||||
for line in rendered_config.split('\n'):
|
for line in rendered_config.split('\n'):
|
||||||
key = line.strip().split(':')[0]
|
# Upon encountering an optional configuration option, commenting out lines until the next
|
||||||
|
# blank line.
|
||||||
if key in REQUIRED_SECTION_NAMES:
|
if line.strip().startswith('# {}'.format(COMMENTED_OUT_SENTINEL)):
|
||||||
lines.append(line)
|
optional = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Upon encountering a required configuration option, skip commenting out lines until the
|
# Hit a blank line, so reset commenting.
|
||||||
# next blank line.
|
if not line.strip():
|
||||||
if key in REQUIRED_KEYS:
|
optional = False
|
||||||
required = True
|
|
||||||
elif not key:
|
|
||||||
required = False
|
|
||||||
|
|
||||||
lines.append(_comment_out_line(line) if not required else line)
|
lines.append(_comment_out_line(line) if optional else line)
|
||||||
|
|
||||||
return '\n'.join(lines)
|
return '\n'.join(lines)
|
||||||
|
|
||||||
@@ -168,6 +166,11 @@ def add_comments_to_configuration_sequence(config, schema, indent=0):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
REQUIRED_SECTION_NAMES = {'location', 'retention'}
|
||||||
|
REQUIRED_KEYS = {'source_directories', 'repositories', 'keep_daily'}
|
||||||
|
COMMENTED_OUT_SENTINEL = 'COMMENT_OUT'
|
||||||
|
|
||||||
|
|
||||||
def add_comments_to_configuration_map(config, schema, indent=0, skip_first=False):
|
def add_comments_to_configuration_map(config, schema, indent=0, skip_first=False):
|
||||||
'''
|
'''
|
||||||
Using descriptions from a schema as a source, add those descriptions as comments to the given
|
Using descriptions from a schema as a source, add those descriptions as comments to the given
|
||||||
@@ -178,10 +181,20 @@ def add_comments_to_configuration_map(config, schema, indent=0, skip_first=False
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
field_schema = schema['map'].get(field_name, {})
|
field_schema = schema['map'].get(field_name, {})
|
||||||
description = field_schema.get('desc')
|
description = field_schema.get('desc', '').strip()
|
||||||
|
|
||||||
|
# If this is an optional key, add an indicator to the comment flagging it to be commented
|
||||||
|
# out from the sample configuration. This sentinel is consumed by downstream processing that
|
||||||
|
# does the actual commenting out.
|
||||||
|
if field_name not in REQUIRED_SECTION_NAMES and field_name not in REQUIRED_KEYS:
|
||||||
|
description = (
|
||||||
|
'\n'.join((description, COMMENTED_OUT_SENTINEL))
|
||||||
|
if description
|
||||||
|
else COMMENTED_OUT_SENTINEL
|
||||||
|
)
|
||||||
|
|
||||||
# No description to use? Skip it.
|
# No description to use? Skip it.
|
||||||
if not field_schema or not description:
|
if not field_schema or not description: # pragma: no cover
|
||||||
continue
|
continue
|
||||||
|
|
||||||
config.yaml_set_comment_before_after_key(key=field_name, before=description, indent=indent)
|
config.yaml_set_comment_before_after_key(key=field_name, before=description, indent=indent)
|
||||||
@@ -190,14 +203,88 @@ def add_comments_to_configuration_map(config, schema, indent=0, skip_first=False
|
|||||||
_insert_newline_before_comment(config, field_name)
|
_insert_newline_before_comment(config, field_name)
|
||||||
|
|
||||||
|
|
||||||
def generate_sample_configuration(config_filename, schema_filename):
|
RUAMEL_YAML_COMMENTS_INDEX = 1
|
||||||
|
|
||||||
|
|
||||||
|
def remove_commented_out_sentinel(config, field_name):
|
||||||
'''
|
'''
|
||||||
Given a target config filename and the path to a schema filename in pykwalify YAML schema
|
Given a configuration CommentedMap and a top-level field name in it, remove any "commented out"
|
||||||
format, write out a sample configuration file based on that schema.
|
sentinel found at the end of its YAML comments. This prevents the given field name from getting
|
||||||
|
commented out by downstream processing that consumes the sentinel.
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
last_comment_value = config.ca.items[field_name][RUAMEL_YAML_COMMENTS_INDEX][-1].value
|
||||||
|
except KeyError:
|
||||||
|
return
|
||||||
|
|
||||||
|
if last_comment_value == '# {}\n'.format(COMMENTED_OUT_SENTINEL):
|
||||||
|
config.ca.items[field_name][RUAMEL_YAML_COMMENTS_INDEX].pop()
|
||||||
|
|
||||||
|
|
||||||
|
def merge_source_configuration_into_destination(destination_config, source_config):
|
||||||
|
'''
|
||||||
|
Deep merge the given source configuration dict into the destination configuration CommentedMap,
|
||||||
|
favoring values from the source when there are collisions.
|
||||||
|
|
||||||
|
The purpose of this is to upgrade configuration files from old versions of borgmatic by adding
|
||||||
|
new
|
||||||
|
configuration keys and comments.
|
||||||
|
'''
|
||||||
|
if not source_config:
|
||||||
|
return destination_config
|
||||||
|
if not destination_config or not isinstance(source_config, collections.abc.Mapping):
|
||||||
|
return source_config
|
||||||
|
|
||||||
|
for field_name, source_value in source_config.items():
|
||||||
|
# Since this key/value is from the source configuration, leave it uncommented and remove any
|
||||||
|
# sentinel that would cause it to get commented out.
|
||||||
|
remove_commented_out_sentinel(destination_config, field_name)
|
||||||
|
|
||||||
|
# This is a mapping. Recurse for this key/value.
|
||||||
|
if isinstance(source_value, collections.abc.Mapping):
|
||||||
|
destination_config[field_name] = merge_source_configuration_into_destination(
|
||||||
|
destination_config[field_name], source_value
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# This is a sequence. Recurse for each item in it.
|
||||||
|
if isinstance(source_value, collections.abc.Sequence) and not isinstance(source_value, str):
|
||||||
|
destination_value = destination_config[field_name]
|
||||||
|
destination_config[field_name] = yaml.comments.CommentedSeq(
|
||||||
|
[
|
||||||
|
merge_source_configuration_into_destination(
|
||||||
|
destination_value[index] if index < len(destination_value) else None,
|
||||||
|
source_item,
|
||||||
|
)
|
||||||
|
for index, source_item in enumerate(source_value)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# This is some sort of scalar. Simply set it into the destination.
|
||||||
|
destination_config[field_name] = source_config[field_name]
|
||||||
|
|
||||||
|
return destination_config
|
||||||
|
|
||||||
|
|
||||||
|
def generate_sample_configuration(source_filename, destination_filename, schema_filename):
|
||||||
|
'''
|
||||||
|
Given an optional source configuration filename, and a required destination configuration
|
||||||
|
filename, and the path to a schema filename in pykwalify YAML schema format, write out a
|
||||||
|
sample configuration file based on that schema. If a source filename is provided, merge the
|
||||||
|
parsed contents of that configuration into the generated configuration.
|
||||||
'''
|
'''
|
||||||
schema = yaml.round_trip_load(open(schema_filename))
|
schema = yaml.round_trip_load(open(schema_filename))
|
||||||
config = _schema_to_sample_configuration(schema)
|
source_config = None
|
||||||
|
|
||||||
|
if source_filename:
|
||||||
|
source_config = load.load_configuration(source_filename)
|
||||||
|
|
||||||
|
destination_config = merge_source_configuration_into_destination(
|
||||||
|
_schema_to_sample_configuration(schema), source_config
|
||||||
|
)
|
||||||
|
|
||||||
write_configuration(
|
write_configuration(
|
||||||
config_filename, _comment_out_optional_configuration(_render_configuration(config))
|
destination_filename,
|
||||||
|
_comment_out_optional_configuration(_render_configuration(destination_config)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -126,6 +126,17 @@ map:
|
|||||||
Exclude directories that contain a file with the given filename. Defaults to not
|
Exclude directories that contain a file with the given filename. Defaults to not
|
||||||
set.
|
set.
|
||||||
example: .nobackup
|
example: .nobackup
|
||||||
|
keep_exclude_tags:
|
||||||
|
type: bool
|
||||||
|
desc: |
|
||||||
|
If true, the exclude_if_present filename is included in backups. Defaults to
|
||||||
|
false, meaning that the exclude_if_present filename is omitted from backups.
|
||||||
|
example: true
|
||||||
|
exclude_nodump:
|
||||||
|
type: bool
|
||||||
|
desc: |
|
||||||
|
Exclude files with the NODUMP flag. Defaults to false.
|
||||||
|
example: true
|
||||||
storage:
|
storage:
|
||||||
desc: |
|
desc: |
|
||||||
Repository storage options. See
|
Repository storage options. See
|
||||||
@@ -428,8 +439,19 @@ map:
|
|||||||
desc: |
|
desc: |
|
||||||
Healthchecks ping URL or UUID to notify when a backup begins, ends, or errors.
|
Healthchecks ping URL or UUID to notify when a backup begins, ends, or errors.
|
||||||
Create an account at https://healthchecks.io if you'd like to use this service.
|
Create an account at https://healthchecks.io if you'd like to use this service.
|
||||||
|
See http://localhost:8080/docs/how-to/monitor-your-backups/#healthchecks-hook
|
||||||
|
for details.
|
||||||
example:
|
example:
|
||||||
https://hc-ping.com/your-uuid-here
|
https://hc-ping.com/your-uuid-here
|
||||||
|
cronitor:
|
||||||
|
type: str
|
||||||
|
desc: |
|
||||||
|
Cronitor ping URL to notify when a backup begins, ends, or errors. Create an
|
||||||
|
account at https://cronitor.io if you'd like to use this service. See
|
||||||
|
http://localhost:8080/docs/how-to/monitor-your-backups/#cronitor-hook for
|
||||||
|
details.
|
||||||
|
example:
|
||||||
|
https://cronitor.link/d3x0c1
|
||||||
before_everything:
|
before_everything:
|
||||||
seq:
|
seq:
|
||||||
- type: str
|
- type: str
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def ping_cronitor(ping_url, config_filename, dry_run, append):
|
||||||
|
'''
|
||||||
|
Ping the given Cronitor URL, appending the append string. Use the given configuration filename
|
||||||
|
in any log entries. If this is a dry run, then don't actually ping anything.
|
||||||
|
'''
|
||||||
|
if not ping_url:
|
||||||
|
logger.debug('{}: No Cronitor hook set'.format(config_filename))
|
||||||
|
return
|
||||||
|
|
||||||
|
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
||||||
|
ping_url = '{}/{}'.format(ping_url, append)
|
||||||
|
|
||||||
|
logger.info('{}: Pinging Cronitor {}{}'.format(config_filename, append, dry_run_label))
|
||||||
|
logger.debug('{}: Using Cronitor ping URL {}'.format(config_filename, ping_url))
|
||||||
|
|
||||||
|
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||||
|
requests.get(ping_url)
|
||||||
@@ -7,12 +7,12 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def ping_healthchecks(ping_url_or_uuid, config_filename, dry_run, append=None):
|
def ping_healthchecks(ping_url_or_uuid, config_filename, dry_run, append=None):
|
||||||
'''
|
'''
|
||||||
Ping the given healthchecks.io URL or UUID, appending the append string if any. Use the given
|
Ping the given Healthchecks URL or UUID, appending the append string if any. Use the given
|
||||||
configuration filename in any log entries. If this is a dry run, then don't actually ping
|
configuration filename in any log entries. If this is a dry run, then don't actually ping
|
||||||
anything.
|
anything.
|
||||||
'''
|
'''
|
||||||
if not ping_url_or_uuid:
|
if not ping_url_or_uuid:
|
||||||
logger.debug('{}: No healthchecks hook set'.format(config_filename))
|
logger.debug('{}: No Healthchecks hook set'.format(config_filename))
|
||||||
return
|
return
|
||||||
|
|
||||||
ping_url = (
|
ping_url = (
|
||||||
@@ -26,11 +26,11 @@ def ping_healthchecks(ping_url_or_uuid, config_filename, dry_run, append=None):
|
|||||||
ping_url = '{}/{}'.format(ping_url, append)
|
ping_url = '{}/{}'.format(ping_url, append)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'{}: Pinging healthchecks.io{}{}'.format(
|
'{}: Pinging Healthchecks{}{}'.format(
|
||||||
config_filename, ' ' + append if append else '', dry_run_label
|
config_filename, ' ' + append if append else '', dry_run_label
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
logger.debug('{}: Using healthchecks.io ping URL {}'.format(config_filename, ping_url))
|
logger.debug('{}: Using Healthchecks ping URL {}'.format(config_filename, ping_url))
|
||||||
|
|
||||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||||
requests.get(ping_url)
|
requests.get(ping_url)
|
||||||
|
|||||||
+22
-7
@@ -73,29 +73,44 @@ def color_text(color, message):
|
|||||||
return '{}{}{}'.format(color, message, colorama.Style.RESET_ALL)
|
return '{}{}{}'.format(color, message, colorama.Style.RESET_ALL)
|
||||||
|
|
||||||
|
|
||||||
def configure_logging(console_log_level, syslog_log_level=None):
|
def configure_logging(
|
||||||
|
console_log_level, syslog_log_level=None, log_file_log_level=None, log_file=None
|
||||||
|
):
|
||||||
'''
|
'''
|
||||||
Configure logging to go to both the console and syslog. Use the given log levels, respectively.
|
Configure logging to go to both the console and (syslog or log file). Use the given log levels,
|
||||||
|
respectively.
|
||||||
|
|
||||||
|
Raise FileNotFoundError or PermissionError if the log file could not be opened for writing.
|
||||||
'''
|
'''
|
||||||
if syslog_log_level is None:
|
if syslog_log_level is None:
|
||||||
syslog_log_level = console_log_level
|
syslog_log_level = console_log_level
|
||||||
|
if log_file_log_level is None:
|
||||||
|
log_file_log_level = console_log_level
|
||||||
|
|
||||||
console_handler = logging.StreamHandler()
|
console_handler = logging.StreamHandler()
|
||||||
console_handler.setFormatter(Console_color_formatter())
|
console_handler.setFormatter(Console_color_formatter())
|
||||||
console_handler.setLevel(console_log_level)
|
console_handler.setLevel(console_log_level)
|
||||||
|
|
||||||
syslog_path = None
|
syslog_path = None
|
||||||
if os.path.exists('/dev/log'):
|
if log_file is None:
|
||||||
syslog_path = '/dev/log'
|
if os.path.exists('/dev/log'):
|
||||||
elif os.path.exists('/var/run/syslog'):
|
syslog_path = '/dev/log'
|
||||||
syslog_path = '/var/run/syslog'
|
elif os.path.exists('/var/run/syslog'):
|
||||||
|
syslog_path = '/var/run/syslog'
|
||||||
|
|
||||||
if syslog_path and not interactive_console():
|
if syslog_path and not interactive_console():
|
||||||
syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
|
syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
|
||||||
syslog_handler.setFormatter(logging.Formatter('borgmatic: %(levelname)s %(message)s'))
|
syslog_handler.setFormatter(logging.Formatter('borgmatic: %(levelname)s %(message)s'))
|
||||||
syslog_handler.setLevel(syslog_log_level)
|
syslog_handler.setLevel(syslog_log_level)
|
||||||
handlers = (console_handler, syslog_handler)
|
handlers = (console_handler, syslog_handler)
|
||||||
|
elif log_file:
|
||||||
|
file_handler = logging.FileHandler(log_file)
|
||||||
|
file_handler.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s'))
|
||||||
|
file_handler.setLevel(log_file_log_level)
|
||||||
|
handlers = (console_handler, file_handler)
|
||||||
else:
|
else:
|
||||||
handlers = (console_handler,)
|
handlers = (console_handler,)
|
||||||
|
|
||||||
logging.basicConfig(level=min(console_log_level, syslog_log_level), handlers=handlers)
|
logging.basicConfig(
|
||||||
|
level=min(console_log_level, syslog_log_level, log_file_log_level), handlers=handlers
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
VERBOSITY_ERROR = -1
|
||||||
VERBOSITY_WARNING = 0
|
VERBOSITY_WARNING = 0
|
||||||
VERBOSITY_SOME = 1
|
VERBOSITY_SOME = 1
|
||||||
VERBOSITY_LOTS = 2
|
VERBOSITY_LOTS = 2
|
||||||
@@ -10,6 +11,7 @@ def verbosity_to_log_level(verbosity):
|
|||||||
Given a borgmatic verbosity value, return the corresponding Python log level.
|
Given a borgmatic verbosity value, return the corresponding Python log level.
|
||||||
'''
|
'''
|
||||||
return {
|
return {
|
||||||
|
VERBOSITY_ERROR: logging.ERROR,
|
||||||
VERBOSITY_WARNING: logging.WARNING,
|
VERBOSITY_WARNING: logging.WARNING,
|
||||||
VERBOSITY_SOME: logging.INFO,
|
VERBOSITY_SOME: logging.INFO,
|
||||||
VERBOSITY_LOTS: logging.DEBUG,
|
VERBOSITY_LOTS: logging.DEBUG,
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ following:
|
|||||||
tox -e black
|
tox -e black
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note that Black requires at minimum Python 3.6.
|
||||||
|
|
||||||
And if you get a complaint from the
|
And if you get a complaint from the
|
||||||
[isort](https://github.com/timothycrosley/isort) Python import orderer, you
|
[isort](https://github.com/timothycrosley/isort) Python import orderer, you
|
||||||
can ask isort to order your imports for you:
|
can ask isort to order your imports for you:
|
||||||
|
|||||||
@@ -70,6 +70,20 @@ Or to increase syslog logging to include debug spew:
|
|||||||
borgmatic --syslog-verbosity 2
|
borgmatic --syslog-verbosity 2
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Logging to file
|
||||||
|
|
||||||
|
If you don't want to use syslog, and you'd rather borgmatic log to a plain
|
||||||
|
file, use the `--log-file` flag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borgmatic --log-file /path/to/file.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that if you use the `--log-file` flag, you are responsible for rotating
|
||||||
|
the log file so it doesn't grow too large. Also, there is a
|
||||||
|
`--log-file-verbosity` flag to customize the log file's log level.
|
||||||
|
|
||||||
|
|
||||||
### systemd journal
|
### systemd journal
|
||||||
|
|
||||||
If your local syslog daemon is systemd's journal, be aware that journald by
|
If your local syslog daemon is systemd's journal, be aware that journald by
|
||||||
|
|||||||
@@ -26,12 +26,15 @@ alert. But note that if borgmatic doesn't actually run, this alert won't fire.
|
|||||||
See [error
|
See [error
|
||||||
hooks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#error-hooks)
|
hooks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#error-hooks)
|
||||||
below for how to configure this.
|
below for how to configure this.
|
||||||
4. **borgmatic Healthchecks hook**: This feature integrates with the
|
4. **borgmatic monitoring hooks**: This feature integrates with monitoring
|
||||||
[Healthchecks](https://healthchecks.io/) service, and pings Healthchecks
|
services like [Healthchecks](https://healthchecks.io/) and
|
||||||
whenever borgmatic runs. That way, Healthchecks can alert you when something
|
[Cronitor](https://cronitor.io), and pings these services whenever borgmatic
|
||||||
goes wrong or it doesn't hear from borgmatic for a configured interval. See
|
runs. That way, you'll receive an alert when something goes wrong or the
|
||||||
|
service doesn't hear from borgmatic for a configured interval. See
|
||||||
[Healthchecks
|
[Healthchecks
|
||||||
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook)
|
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook)
|
||||||
|
and [Cronitor
|
||||||
|
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronitor-hook)
|
||||||
below for how to configure this.
|
below for how to configure this.
|
||||||
3. **Third-party monitoring software**: You can use traditional monitoring
|
3. **Third-party monitoring software**: You can use traditional monitoring
|
||||||
software to consume borgmatic JSON output and track when the last
|
software to consume borgmatic JSON output and track when the last
|
||||||
@@ -108,13 +111,46 @@ hooks:
|
|||||||
healthchecks: https://hc-ping.com/addffa72-da17-40ae-be9c-ff591afb942a
|
healthchecks: https://hc-ping.com/addffa72-da17-40ae-be9c-ff591afb942a
|
||||||
```
|
```
|
||||||
|
|
||||||
With this hook in place, borgmatic will ping your Healthchecks project when a
|
With this hook in place, borgmatic pings your Healthchecks project when a
|
||||||
backup begins, ends, or errors. Then you can configure Healthchecks to notify
|
backup begins, ends, or errors. Specifically, before the <a
|
||||||
you by a [variety of
|
href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup`
|
||||||
|
hooks</a> run, borgmatic lets Healthchecks know that a backup has started.
|
||||||
|
Then, if the backup completes successfully, borgmatic notifies Healthchecks of
|
||||||
|
the success after the `after_backup` hooks run. And if an error occurs during
|
||||||
|
the backup, borgmatic notifies Healthchecks after the `on_error` hooks run.
|
||||||
|
|
||||||
|
You can configure Healthchecks to notify you by a [variety of
|
||||||
mechanisms](https://healthchecks.io/#welcome-integrations) when backups fail
|
mechanisms](https://healthchecks.io/#welcome-integrations) when backups fail
|
||||||
or it doesn't hear from borgmatic for a certain period of time.
|
or it doesn't hear from borgmatic for a certain period of time.
|
||||||
|
|
||||||
|
|
||||||
|
## Cronitor hook
|
||||||
|
|
||||||
|
[Cronitor](https://cronitor.io/) provides "Cron monitoring and uptime healthchecks
|
||||||
|
for websites, services and APIs", and borgmatic has built-in
|
||||||
|
integration with it. Once you create a Cronitor account and cron job monitor on
|
||||||
|
their site, all you need to do is configure borgmatic with the unique "Ping
|
||||||
|
API URL" for your monitor. Here's an example:
|
||||||
|
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
hooks:
|
||||||
|
cronitor: https://cronitor.link/d3x0c1
|
||||||
|
```
|
||||||
|
|
||||||
|
With this hook in place, borgmatic pings your Cronitor monitor when a backup
|
||||||
|
begins, ends, or errors. Specifically, before the <a
|
||||||
|
href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup`
|
||||||
|
hooks</a> run, borgmatic lets Cronitor know that a backup has started. Then,
|
||||||
|
if the backup completes successfully, borgmatic notifies Cronitor of the
|
||||||
|
success after the `after_backup` hooks run. And if an error occurs during the
|
||||||
|
backup, borgmatic notifies Cronitor after the `on_error` hooks run.
|
||||||
|
|
||||||
|
You can configure Cronitor to notify you by a [variety of
|
||||||
|
mechanisms](https://cronitor.io/docs/cron-job-notifications) when backups fail
|
||||||
|
or it doesn't hear from borgmatic for a certain period of time.
|
||||||
|
|
||||||
|
|
||||||
## Scripting borgmatic
|
## Scripting borgmatic
|
||||||
|
|
||||||
To consume the output of borgmatic in other software, you can include an
|
To consume the output of borgmatic in other software, you can include an
|
||||||
|
|||||||
@@ -77,9 +77,12 @@ else borgmatic won't recognize the option. Also be sure to use spaces rather
|
|||||||
than tabs for indentation; YAML does not allow tabs.
|
than tabs for indentation; YAML does not allow tabs.
|
||||||
|
|
||||||
You can also get the same sample configuration file from the [configuration
|
You can also get the same sample configuration file from the [configuration
|
||||||
reference](https://torsion.org/borgmatic/docs/reference/configuration/), the authoritative set of
|
reference](https://torsion.org/borgmatic/docs/reference/configuration/), the
|
||||||
all configuration options. This is handy if borgmatic has added new options
|
authoritative set of all configuration options. This is handy if borgmatic has
|
||||||
since you originally created your configuration file.
|
added new options
|
||||||
|
since you originally created your configuration file. Also check out how to
|
||||||
|
[upgrade your
|
||||||
|
configuration](https://torsion.org/borgmatic/docs/how-to/upgrade/#upgrading-your-configuration).
|
||||||
|
|
||||||
|
|
||||||
### Encryption
|
### Encryption
|
||||||
@@ -248,5 +251,6 @@ it.
|
|||||||
* [Deal with very large backups](https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/)
|
* [Deal with very large backups](https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/)
|
||||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
|
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
|
||||||
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
|
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
|
||||||
|
* [Upgrade borgmatic](https://torsion.org/borgmatic/docs/how-to/upgrade/)
|
||||||
* [borgmatic configuration reference](https://torsion.org/borgmatic/docs/reference/configuration/)
|
* [borgmatic configuration reference](https://torsion.org/borgmatic/docs/reference/configuration/)
|
||||||
* [borgmatic command-line reference](https://torsion.org/borgmatic/docs/reference/command-line/)
|
* [borgmatic command-line reference](https://torsion.org/borgmatic/docs/reference/command-line/)
|
||||||
|
|||||||
+40
-1
@@ -10,7 +10,46 @@ following:
|
|||||||
sudo pip3 install --user --upgrade borgmatic
|
sudo pip3 install --user --upgrade borgmatic
|
||||||
```
|
```
|
||||||
|
|
||||||
See below about special cases.
|
See below about special cases with old versions of borgmatic. Additionally, if
|
||||||
|
you installed borgmatic [without using `pip3 install
|
||||||
|
--user`](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#other-ways-to-install),
|
||||||
|
then your upgrade process may be different.
|
||||||
|
|
||||||
|
|
||||||
|
### Upgrading your configuration
|
||||||
|
|
||||||
|
The borgmatic configuration file format is almost always backwards-compatible
|
||||||
|
from release to release without any changes, but you may still want to update
|
||||||
|
your configuration file when you upgrade to take advantage of new
|
||||||
|
configuration options. This is completely optional. If you prefer, you can add
|
||||||
|
new configuration options manually.
|
||||||
|
|
||||||
|
If you do want to upgrade your configuration file to include new options, use
|
||||||
|
the `generate-borgmatic-config` script with its optional `--source` flag that
|
||||||
|
takes the path to your original configuration file. If provided with this
|
||||||
|
path, `generate-borgmatic-config` merges your original configuration into the
|
||||||
|
generated configuration file, so you get all the newest options and comments.
|
||||||
|
|
||||||
|
Here's an example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
generate-borgmatic-config --source config.yaml --destination config-new.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
New options start as commented out, so you can edit the file and decide
|
||||||
|
whether you want to use each one.
|
||||||
|
|
||||||
|
There are a few caveats to this process, however. First, when generating the
|
||||||
|
new configuration file, `generate-borgmatic-config` replaces any comments
|
||||||
|
you've written in your original configuration file with the newest generated
|
||||||
|
comments. Second, the script adds back any options you had originally deleted,
|
||||||
|
although it does so with the options commented out. And finally, any YAML
|
||||||
|
includes you've used in the source configuration get flattened out into a
|
||||||
|
single generated file.
|
||||||
|
|
||||||
|
As a safety measure, `generate-borgmatic-config` refuses to modify
|
||||||
|
configuration files in-place. So it's up to you to review the generated file
|
||||||
|
and, if desired, replace your original configuration file with it.
|
||||||
|
|
||||||
|
|
||||||
### Upgrading from borgmatic 1.0.x
|
### Upgrading from borgmatic 1.0.x
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
VERSION = '1.4.2'
|
VERSION = '1.4.7'
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
appdirs==1.4.3
|
appdirs==1.4.3
|
||||||
atomicwrites==1.3.0
|
atomicwrites==1.3.0
|
||||||
attrs==19.1.0
|
attrs==19.3.0
|
||||||
black==19.3b0; python_version >= '3.6'
|
black==19.3b0; python_version >= '3.6'
|
||||||
click==7.0
|
click==7.0
|
||||||
colorama==0.4.1
|
colorama==0.4.1
|
||||||
coverage==4.5.4
|
coverage==4.5.4
|
||||||
docopt==0.6.2
|
docopt==0.6.2
|
||||||
flake8==3.7.8
|
flake8==3.7.9
|
||||||
flexmock==0.10.4
|
flexmock==0.10.4
|
||||||
isort==4.3.21
|
isort==4.3.21
|
||||||
mccabe==0.6.1
|
mccabe==0.6.1
|
||||||
@@ -16,8 +16,8 @@ py==1.8.0
|
|||||||
pycodestyle==2.5.0
|
pycodestyle==2.5.0
|
||||||
pyflakes==2.1.1
|
pyflakes==2.1.1
|
||||||
pykwalify==1.7.0
|
pykwalify==1.7.0
|
||||||
pytest==5.1.2
|
pytest==5.2.2
|
||||||
pytest-cov==2.7.1
|
pytest-cov==2.8.1
|
||||||
python-dateutil==2.8.0
|
python-dateutil==2.8.0
|
||||||
PyYAML==5.1.2
|
PyYAML==5.1.2
|
||||||
requests==2.22.0
|
requests==2.22.0
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ def test_parse_arguments_with_no_arguments_uses_defaults():
|
|||||||
assert global_arguments.excludes_filename is None
|
assert global_arguments.excludes_filename is None
|
||||||
assert global_arguments.verbosity == 0
|
assert global_arguments.verbosity == 0
|
||||||
assert global_arguments.syslog_verbosity == 0
|
assert global_arguments.syslog_verbosity == 0
|
||||||
|
assert global_arguments.log_file_verbosity == 0
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_multiple_config_paths_parses_as_list():
|
def test_parse_arguments_with_multiple_config_paths_parses_as_list():
|
||||||
@@ -26,6 +27,7 @@ def test_parse_arguments_with_multiple_config_paths_parses_as_list():
|
|||||||
assert global_arguments.config_paths == ['myconfig', 'otherconfig']
|
assert global_arguments.config_paths == ['myconfig', 'otherconfig']
|
||||||
assert global_arguments.verbosity == 0
|
assert global_arguments.verbosity == 0
|
||||||
assert global_arguments.syslog_verbosity == 0
|
assert global_arguments.syslog_verbosity == 0
|
||||||
|
assert global_arguments.log_file_verbosity == 0
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_verbosity_overrides_default():
|
def test_parse_arguments_with_verbosity_overrides_default():
|
||||||
@@ -39,6 +41,7 @@ def test_parse_arguments_with_verbosity_overrides_default():
|
|||||||
assert global_arguments.excludes_filename is None
|
assert global_arguments.excludes_filename is None
|
||||||
assert global_arguments.verbosity == 1
|
assert global_arguments.verbosity == 1
|
||||||
assert global_arguments.syslog_verbosity == 0
|
assert global_arguments.syslog_verbosity == 0
|
||||||
|
assert global_arguments.log_file_verbosity == 0
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_syslog_verbosity_overrides_default():
|
def test_parse_arguments_with_syslog_verbosity_overrides_default():
|
||||||
@@ -54,6 +57,20 @@ def test_parse_arguments_with_syslog_verbosity_overrides_default():
|
|||||||
assert global_arguments.syslog_verbosity == 2
|
assert global_arguments.syslog_verbosity == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_log_file_verbosity_overrides_default():
|
||||||
|
config_paths = ['default']
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths)
|
||||||
|
|
||||||
|
arguments = module.parse_arguments('--log-file-verbosity', '-1')
|
||||||
|
|
||||||
|
global_arguments = arguments['global']
|
||||||
|
assert global_arguments.config_paths == config_paths
|
||||||
|
assert global_arguments.excludes_filename is None
|
||||||
|
assert global_arguments.verbosity == 0
|
||||||
|
assert global_arguments.syslog_verbosity == 0
|
||||||
|
assert global_arguments.log_file_verbosity == -1
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_list_json_overrides_default():
|
def test_parse_arguments_with_list_json_overrides_default():
|
||||||
arguments = module.parse_arguments('list', '--json')
|
arguments = module.parse_arguments('list', '--json')
|
||||||
|
|
||||||
|
|||||||
@@ -47,9 +47,13 @@ def test_comment_out_line_comments_twice_indented_option():
|
|||||||
|
|
||||||
|
|
||||||
def test_comment_out_optional_configuration_comments_optional_config_only():
|
def test_comment_out_optional_configuration_comments_optional_config_only():
|
||||||
|
# The "# COMMENT_OUT" comment is a sentinel used to express that the following key is optional.
|
||||||
|
# It's stripped out of the final output.
|
||||||
flexmock(module)._comment_out_line = lambda line: '# ' + line
|
flexmock(module)._comment_out_line = lambda line: '# ' + line
|
||||||
config = '''
|
config = '''
|
||||||
|
# COMMENT_OUT
|
||||||
foo:
|
foo:
|
||||||
|
# COMMENT_OUT
|
||||||
bar:
|
bar:
|
||||||
- baz
|
- baz
|
||||||
- quux
|
- quux
|
||||||
@@ -59,6 +63,8 @@ location:
|
|||||||
- one
|
- one
|
||||||
- two
|
- two
|
||||||
|
|
||||||
|
# This comment should be kept.
|
||||||
|
# COMMENT_OUT
|
||||||
other: thing
|
other: thing
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -68,12 +74,13 @@ location:
|
|||||||
# bar:
|
# bar:
|
||||||
# - baz
|
# - baz
|
||||||
# - quux
|
# - quux
|
||||||
#
|
|
||||||
location:
|
location:
|
||||||
repositories:
|
repositories:
|
||||||
- one
|
- one
|
||||||
- two
|
- two
|
||||||
#
|
|
||||||
|
# This comment should be kept.
|
||||||
# other: thing
|
# other: thing
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -142,12 +149,67 @@ def test_add_comments_to_configuration_map_does_not_raise():
|
|||||||
module.add_comments_to_configuration_map(config, schema)
|
module.add_comments_to_configuration_map(config, schema)
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_comments_to_configuration_map_with_skip_first_does_not_raise():
|
||||||
|
config = module.yaml.comments.CommentedMap([('foo', 33)])
|
||||||
|
schema = {'map': {'foo': {'desc': 'Foo'}}}
|
||||||
|
|
||||||
|
module.add_comments_to_configuration_map(config, schema, skip_first=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_commented_out_sentinel_keeps_other_comments():
|
||||||
|
field_name = 'foo'
|
||||||
|
config = module.yaml.comments.CommentedMap([(field_name, 33)])
|
||||||
|
config.yaml_set_comment_before_after_key(key=field_name, before='Actual comment.\nCOMMENT_OUT')
|
||||||
|
|
||||||
|
module.remove_commented_out_sentinel(config, field_name)
|
||||||
|
|
||||||
|
comments = config.ca.items[field_name][module.RUAMEL_YAML_COMMENTS_INDEX]
|
||||||
|
assert len(comments) == 1
|
||||||
|
assert comments[0].value == '# Actual comment.\n'
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_commented_out_sentinel_without_sentinel_keeps_other_comments():
|
||||||
|
field_name = 'foo'
|
||||||
|
config = module.yaml.comments.CommentedMap([(field_name, 33)])
|
||||||
|
config.yaml_set_comment_before_after_key(key=field_name, before='Actual comment.')
|
||||||
|
|
||||||
|
module.remove_commented_out_sentinel(config, field_name)
|
||||||
|
|
||||||
|
comments = config.ca.items[field_name][module.RUAMEL_YAML_COMMENTS_INDEX]
|
||||||
|
assert len(comments) == 1
|
||||||
|
assert comments[0].value == '# Actual comment.\n'
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_commented_out_sentinel_on_unknown_field_does_not_raise():
|
||||||
|
field_name = 'foo'
|
||||||
|
config = module.yaml.comments.CommentedMap([(field_name, 33)])
|
||||||
|
config.yaml_set_comment_before_after_key(key=field_name, before='Actual comment.')
|
||||||
|
|
||||||
|
module.remove_commented_out_sentinel(config, 'unknown')
|
||||||
|
|
||||||
|
|
||||||
def test_generate_sample_configuration_does_not_raise():
|
def test_generate_sample_configuration_does_not_raise():
|
||||||
builtins = flexmock(sys.modules['builtins'])
|
builtins = flexmock(sys.modules['builtins'])
|
||||||
builtins.should_receive('open').with_args('schema.yaml').and_return('')
|
builtins.should_receive('open').with_args('schema.yaml').and_return('')
|
||||||
|
flexmock(module.yaml).should_receive('round_trip_load')
|
||||||
flexmock(module).should_receive('_schema_to_sample_configuration')
|
flexmock(module).should_receive('_schema_to_sample_configuration')
|
||||||
|
flexmock(module).should_receive('merge_source_configuration_into_destination')
|
||||||
flexmock(module).should_receive('_render_configuration')
|
flexmock(module).should_receive('_render_configuration')
|
||||||
flexmock(module).should_receive('_comment_out_optional_configuration')
|
flexmock(module).should_receive('_comment_out_optional_configuration')
|
||||||
flexmock(module).should_receive('write_configuration')
|
flexmock(module).should_receive('write_configuration')
|
||||||
|
|
||||||
module.generate_sample_configuration('config.yaml', 'schema.yaml')
|
module.generate_sample_configuration(None, 'dest.yaml', 'schema.yaml')
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_sample_configuration_with_source_filename_does_not_raise():
|
||||||
|
builtins = flexmock(sys.modules['builtins'])
|
||||||
|
builtins.should_receive('open').with_args('schema.yaml').and_return('')
|
||||||
|
flexmock(module.yaml).should_receive('round_trip_load')
|
||||||
|
flexmock(module.load).should_receive('load_configuration')
|
||||||
|
flexmock(module).should_receive('_schema_to_sample_configuration')
|
||||||
|
flexmock(module).should_receive('merge_source_configuration_into_destination')
|
||||||
|
flexmock(module).should_receive('_render_configuration')
|
||||||
|
flexmock(module).should_receive('_comment_out_optional_configuration')
|
||||||
|
flexmock(module).should_receive('write_configuration')
|
||||||
|
|
||||||
|
module.generate_sample_configuration('source.yaml', 'dest.yaml', 'schema.yaml')
|
||||||
|
|||||||
@@ -150,6 +150,30 @@ def test_make_exclude_flags_includes_exclude_if_present_when_in_config():
|
|||||||
assert exclude_flags == ('--exclude-if-present', 'exclude_me')
|
assert exclude_flags == ('--exclude-if-present', 'exclude_me')
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_exclude_flags_includes_keep_exclude_tags_when_true_in_config():
|
||||||
|
exclude_flags = module._make_exclude_flags(location_config={'keep_exclude_tags': True})
|
||||||
|
|
||||||
|
assert exclude_flags == ('--keep-exclude-tags',)
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_exclude_flags_does_not_include_keep_exclude_tags_when_false_in_config():
|
||||||
|
exclude_flags = module._make_exclude_flags(location_config={'keep_exclude_tags': False})
|
||||||
|
|
||||||
|
assert exclude_flags == ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_exclude_flags_includes_exclude_nodump_when_true_in_config():
|
||||||
|
exclude_flags = module._make_exclude_flags(location_config={'exclude_nodump': True})
|
||||||
|
|
||||||
|
assert exclude_flags == ('--exclude-nodump',)
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_exclude_flags_does_not_include_exclude_nodump_when_false_in_config():
|
||||||
|
exclude_flags = module._make_exclude_flags(location_config={'exclude_nodump': False})
|
||||||
|
|
||||||
|
assert exclude_flags == ()
|
||||||
|
|
||||||
|
|
||||||
def test_make_exclude_flags_is_empty_when_config_has_no_excludes():
|
def test_make_exclude_flags_is_empty_when_config_has_no_excludes():
|
||||||
exclude_flags = module._make_exclude_flags(location_config={})
|
exclude_flags = module._make_exclude_flags(location_config={})
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ def test_convert_section_generates_integer_value_for_integer_type_in_schema():
|
|||||||
|
|
||||||
def test_convert_legacy_parsed_config_transforms_source_config_to_mapping():
|
def test_convert_legacy_parsed_config_transforms_source_config_to_mapping():
|
||||||
flexmock(module.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
|
flexmock(module.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
|
||||||
|
flexmock(module.generate).should_receive('add_comments_to_configuration_map')
|
||||||
source_config = Parsed_config(
|
source_config = Parsed_config(
|
||||||
location=OrderedDict([('source_directories', '/home'), ('repository', 'hostname.borg')]),
|
location=OrderedDict([('source_directories', '/home'), ('repository', 'hostname.borg')]),
|
||||||
storage=OrderedDict([('encryption_passphrase', 'supersecret')]),
|
storage=OrderedDict([('encryption_passphrase', 'supersecret')]),
|
||||||
@@ -53,6 +54,7 @@ def test_convert_legacy_parsed_config_transforms_source_config_to_mapping():
|
|||||||
|
|
||||||
def test_convert_legacy_parsed_config_splits_space_separated_values():
|
def test_convert_legacy_parsed_config_splits_space_separated_values():
|
||||||
flexmock(module.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
|
flexmock(module.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
|
||||||
|
flexmock(module.generate).should_receive('add_comments_to_configuration_map')
|
||||||
source_config = Parsed_config(
|
source_config = Parsed_config(
|
||||||
location=OrderedDict(
|
location=OrderedDict(
|
||||||
[('source_directories', '/home /etc'), ('repository', 'hostname.borg')]
|
[('source_directories', '/home /etc'), ('repository', 'hostname.borg')]
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ def test_schema_to_sample_configuration_generates_config_sequence_of_strings_wit
|
|||||||
def test_schema_to_sample_configuration_generates_config_sequence_of_maps_with_examples():
|
def test_schema_to_sample_configuration_generates_config_sequence_of_maps_with_examples():
|
||||||
flexmock(module.yaml.comments).should_receive('CommentedSeq').replace_with(list)
|
flexmock(module.yaml.comments).should_receive('CommentedSeq').replace_with(list)
|
||||||
flexmock(module).should_receive('add_comments_to_configuration_sequence')
|
flexmock(module).should_receive('add_comments_to_configuration_sequence')
|
||||||
|
flexmock(module).should_receive('add_comments_to_configuration_map')
|
||||||
schema = {
|
schema = {
|
||||||
'seq': [
|
'seq': [
|
||||||
{
|
{
|
||||||
@@ -71,3 +72,63 @@ def test_schema_to_sample_configuration_with_unsupported_schema_raises():
|
|||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
module._schema_to_sample_configuration(schema)
|
module._schema_to_sample_configuration(schema)
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_source_configuration_into_destination_inserts_map_fields():
|
||||||
|
destination_config = {'foo': 'dest1', 'bar': 'dest2'}
|
||||||
|
source_config = {'foo': 'source1', 'baz': 'source2'}
|
||||||
|
flexmock(module).should_receive('remove_commented_out_sentinel')
|
||||||
|
flexmock(module).should_receive('yaml.comments.CommentedSeq').replace_with(list)
|
||||||
|
|
||||||
|
module.merge_source_configuration_into_destination(destination_config, source_config)
|
||||||
|
|
||||||
|
assert destination_config == {'foo': 'source1', 'bar': 'dest2', 'baz': 'source2'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_source_configuration_into_destination_inserts_nested_map_fields():
|
||||||
|
destination_config = {'foo': {'first': 'dest1', 'second': 'dest2'}, 'bar': 'dest3'}
|
||||||
|
source_config = {'foo': {'first': 'source1'}}
|
||||||
|
flexmock(module).should_receive('remove_commented_out_sentinel')
|
||||||
|
flexmock(module).should_receive('yaml.comments.CommentedSeq').replace_with(list)
|
||||||
|
|
||||||
|
module.merge_source_configuration_into_destination(destination_config, source_config)
|
||||||
|
|
||||||
|
assert destination_config == {'foo': {'first': 'source1', 'second': 'dest2'}, 'bar': 'dest3'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_source_configuration_into_destination_inserts_sequence_fields():
|
||||||
|
destination_config = {'foo': ['dest1', 'dest2'], 'bar': ['dest3'], 'baz': ['dest4']}
|
||||||
|
source_config = {'foo': ['source1'], 'bar': ['source2', 'source3']}
|
||||||
|
flexmock(module).should_receive('remove_commented_out_sentinel')
|
||||||
|
flexmock(module).should_receive('yaml.comments.CommentedSeq').replace_with(list)
|
||||||
|
|
||||||
|
module.merge_source_configuration_into_destination(destination_config, source_config)
|
||||||
|
|
||||||
|
assert destination_config == {
|
||||||
|
'foo': ['source1'],
|
||||||
|
'bar': ['source2', 'source3'],
|
||||||
|
'baz': ['dest4'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_source_configuration_into_destination_inserts_sequence_of_maps():
|
||||||
|
destination_config = {'foo': [{'first': 'dest1', 'second': 'dest2'}], 'bar': 'dest3'}
|
||||||
|
source_config = {'foo': [{'first': 'source1'}, {'other': 'source2'}]}
|
||||||
|
flexmock(module).should_receive('remove_commented_out_sentinel')
|
||||||
|
flexmock(module).should_receive('yaml.comments.CommentedSeq').replace_with(list)
|
||||||
|
|
||||||
|
module.merge_source_configuration_into_destination(destination_config, source_config)
|
||||||
|
|
||||||
|
assert destination_config == {
|
||||||
|
'foo': [{'first': 'source1', 'second': 'dest2'}, {'other': 'source2'}],
|
||||||
|
'bar': 'dest3',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_source_configuration_into_destination_without_source_does_nothing():
|
||||||
|
original_destination_config = {'foo': 'dest1', 'bar': 'dest2'}
|
||||||
|
destination_config = dict(original_destination_config)
|
||||||
|
|
||||||
|
module.merge_source_configuration_into_destination(destination_config, None)
|
||||||
|
|
||||||
|
assert destination_config == original_destination_config
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.hooks import cronitor as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_ping_cronitor_hits_ping_url():
|
||||||
|
ping_url = 'https://example.com'
|
||||||
|
append = 'failed-so-hard'
|
||||||
|
flexmock(module.requests).should_receive('get').with_args('{}/{}'.format(ping_url, append))
|
||||||
|
|
||||||
|
module.ping_cronitor(ping_url, 'config.yaml', dry_run=False, append=append)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ping_cronitor_without_ping_url_does_not_raise():
|
||||||
|
flexmock(module.requests).should_receive('get').never()
|
||||||
|
|
||||||
|
module.ping_cronitor(ping_url=None, config_filename='config.yaml', dry_run=False, append='oops')
|
||||||
@@ -193,3 +193,33 @@ def test_configure_logging_skips_syslog_if_interactive_console():
|
|||||||
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
||||||
|
|
||||||
module.configure_logging(console_log_level=logging.INFO)
|
module.configure_logging(console_log_level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
def test_configure_logging_to_logfile_instead_of_syslog():
|
||||||
|
# syslog skipped in non-interactive console if --log-file argument provided
|
||||||
|
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||||
|
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||||
|
level=logging.DEBUG, handlers=tuple
|
||||||
|
)
|
||||||
|
flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(True)
|
||||||
|
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
||||||
|
file_handler = logging.FileHandler('/tmp/logfile')
|
||||||
|
flexmock(module.logging).should_receive('FileHandler').with_args('/tmp/logfile').and_return(
|
||||||
|
file_handler
|
||||||
|
).once()
|
||||||
|
|
||||||
|
module.configure_logging(
|
||||||
|
console_log_level=logging.INFO, log_file_log_level=logging.DEBUG, log_file='/tmp/logfile'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_configure_logging_skips_logfile_if_argument_is_none():
|
||||||
|
# No FileHandler added if argument --log-file is None
|
||||||
|
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||||
|
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||||
|
level=logging.INFO, handlers=tuple
|
||||||
|
)
|
||||||
|
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||||
|
flexmock(module.logging).should_receive('FileHandler').never()
|
||||||
|
|
||||||
|
module.configure_logging(console_log_level=logging.INFO, log_file=None)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ def insert_logging_mock(log_level):
|
|||||||
def test_verbosity_to_log_level_maps_known_verbosity_to_log_level():
|
def test_verbosity_to_log_level_maps_known_verbosity_to_log_level():
|
||||||
assert module.verbosity_to_log_level(module.VERBOSITY_SOME) == logging.INFO
|
assert module.verbosity_to_log_level(module.VERBOSITY_SOME) == logging.INFO
|
||||||
assert module.verbosity_to_log_level(module.VERBOSITY_LOTS) == logging.DEBUG
|
assert module.verbosity_to_log_level(module.VERBOSITY_LOTS) == logging.DEBUG
|
||||||
|
assert module.verbosity_to_log_level(module.VERBOSITY_ERROR) == logging.ERROR
|
||||||
|
|
||||||
|
|
||||||
def test_verbosity_to_log_level_maps_unknown_verbosity_to_warning_level():
|
def test_verbosity_to_log_level_maps_unknown_verbosity_to_warning_level():
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = py35,py36,py37
|
envlist = py35,py36,py37,py38
|
||||||
skip_missing_interpreters = True
|
skip_missing_interpreters = True
|
||||||
skipsdist = True
|
skipsdist = True
|
||||||
minversion = 3.14.0
|
minversion = 3.14.0
|
||||||
@@ -14,12 +14,11 @@ commands_pre =
|
|||||||
find {toxinidir} -type f -not -path '{toxinidir}/.tox/*' -path '*/__pycache__/*' -name '*.py[c|o]' -delete
|
find {toxinidir} -type f -not -path '{toxinidir}/.tox/*' -path '*/__pycache__/*' -name '*.py[c|o]' -delete
|
||||||
commands =
|
commands =
|
||||||
pytest {posargs}
|
pytest {posargs}
|
||||||
py36,py37: black --check .
|
py36,py37,py38: black --check .
|
||||||
isort --recursive --check-only --settings-path setup.cfg .
|
isort --recursive --check-only --settings-path setup.cfg .
|
||||||
flake8 borgmatic tests
|
flake8 borgmatic tests
|
||||||
|
|
||||||
[testenv:black]
|
[testenv:black]
|
||||||
basepython = python3.7
|
|
||||||
commands =
|
commands =
|
||||||
black {posargs} .
|
black {posargs} .
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user