Compare commits

...

23 Commits
1.8.10 ... main

Author SHA1 Message Date
Dan Helfman 5752373009 When color output is disabled (explicitly or implicitly), don't prefix each log line with the log level (#863).
build / test (push) Successful in 7m59s Details
build / docs (push) Successful in 2m26s Details
2024-05-11 22:40:13 -07:00
Dan Helfman fecae39fcd To avoid duplicate install, update docs to uninstall borgmatic before re-installing with Apprise (#862).
build / test (push) Successful in 7m57s Details
build / docs (push) Successful in 2m23s Details
2024-05-03 16:48:35 -07:00
Dan Helfman 38bc4fbfe2 Fix interaction between environment variable interpolation in constants and shell escaping (#860).
build / test (push) Successful in 7m52s Details
build / docs (push) Successful in 2m19s Details
2024-04-30 09:36:26 -07:00
Dan Helfman 92ed7573d4 Fix NEWS formatting.
build / test (push) Successful in 7m22s Details
build / docs (push) Successful in 2m7s Details
2024-04-29 09:39:40 -07:00
Dan Helfman 80f0e92462 Bump version for release. 2024-04-29 09:38:02 -07:00
Dan Helfman 5f10b1b2ca Clarify database limitations.
build / test (push) Successful in 6m10s Details
build / docs (push) Successful in 1m23s Details
2024-04-28 16:55:24 -07:00
Dan Helfman 4f83b1e6b3 [Documentation] Add compression level explanation and example.
build / test (push) Successful in 7m24s Details
build / docs (push) Successful in 2m27s Details
Reviewed-on: #859
2024-04-28 16:50:09 +00:00
Codimp 15d5a687fb
make parenthetical its own sentence 2024-04-28 18:41:05 +02:00
Codimp eb1fce3787
documentation: add compression level explanation and example 2024-04-28 18:24:23 +02:00
Dan Helfman 7f735cbe59 Fix a traceback with "check --only spot" when the "spot" check is unconfigured (#857).
build / test (push) Successful in 7m42s Details
build / docs (push) Successful in 2m10s Details
2024-04-24 16:12:58 -07:00
Dan Helfman a690ea4016 Add Healtchecks auto-provisioning to NEWS (#815).
build / test (push) Successful in 5m49s Details
build / docs (push) Successful in 2m16s Details
2024-04-23 09:25:29 -07:00
Dan Helfman 7a110c7acd Add Healthchecks auto-provisionning (#815).
build / docs (push) Blocked by required conditions Details
build / test (push) Has been cancelled Details
Reviewed-on: #852
Reviewed-by: Dan Helfman <witten@torsion.org>
2024-04-23 16:23:26 +00:00
estebanthilliez 407bb33359 Fix schema.yaml to comply with maximum line length 2024-04-22 20:47:03 +02:00
estebanthilliez 4b7f7bba04 Issue warning if using UUID URL scheme with create_slug 2024-04-22 20:45:36 +02:00
estebanthilliez cfdc0a1f2a Fix Healthchecks UUID regex 2024-04-22 20:44:31 +02:00
Dan Helfman f926055e67 Fix a traceback when the "data" consistency check is used (#854).
build / test (push) Successful in 7m36s Details
build / docs (push) Successful in 2m26s Details
2024-04-21 14:55:02 -07:00
Dan Helfman 058af95d70 Document limitation about using database hooks and "one_file_system" (#853).
build / test (push) Successful in 4m20s Details
build / docs (push) Successful in 52s Details
2024-04-20 14:53:41 -07:00
Dan Helfman 54facdc391 Clarify Apprise states configuration.
build / test (push) Successful in 6m2s Details
build / docs (push) Successful in 1m29s Details
2024-04-20 08:26:06 -07:00
estebanthi 2e4c0cc7e7 Support for healthchecks auto provisionning 2024-04-19 10:43:45 +02:00
Dan Helfman cb2fd7c5e8 Fix lack of file extraction when using "extract --strip-components all" on a path with a leading slash (#851).
build / test (push) Successful in 6m0s Details
build / docs (push) Successful in 1m30s Details
2024-04-17 16:50:09 -07:00
Dan Helfman 94133cc8b1 Add note about running spot check on a separate schedule (#656).
build / test (push) Successful in 4m15s Details
build / docs (push) Successful in 52s Details
2024-04-16 10:57:34 -07:00
Dan Helfman dcec89be90 Wording tweak (#656).
build / test (push) Successful in 4m17s Details
build / docs (push) Has been cancelled Details
2024-04-16 10:52:56 -07:00
Dan Helfman fefd5d1d0e Wording tweak (#656).
build / docs (push) Blocked by required conditions Details
build / test (push) Has been cancelled Details
2024-04-16 10:50:37 -07:00
18 changed files with 292 additions and 62 deletions

12
NEWS
View File

@ -1,3 +1,15 @@
1.8.12.dev0
* #860: Fix interaction between environment variable interpolation in constants and shell escaping.
* #863: When color output is disabled (explicitly or implicitly), don't prefix each log line with
the log level.
1.8.11
* #815: Add optional Healthchecks auto-provisioning via "create_slug" option.
* #851: Fix lack of file extraction when using "extract --strip-components all" on a path with a
leading slash.
* #854: Fix a traceback when the "data" consistency check is used.
* #857: Fix a traceback with "check --only spot" when the "spot" check is unconfigured.
1.8.10 1.8.10
* #656 (beta): Add a "spot" consistency check that compares file counts and contents between your * #656 (beta): Add a "spot" consistency check that compares file counts and contents between your
source files and the latest archive, ensuring they fall within configured tolerances. This can source files and the latest archive, ensuring they fall within configured tolerances. This can

View File

@ -480,7 +480,13 @@ def spot_check(
''' '''
log_label = f'{repository.get("label", repository["path"])}' log_label = f'{repository.get("label", repository["path"])}'
logger.debug(f'{log_label}: Running spot check') logger.debug(f'{log_label}: Running spot check')
spot_check_config = next(check for check in config['checks'] if check['name'] == 'spot')
try:
spot_check_config = next(
check for check in config.get('checks', ()) if check.get('name') == 'spot'
)
except StopIteration:
raise ValueError('Cannot run spot check because it is unconfigured')
if spot_check_config['data_tolerance_percentage'] > spot_check_config['data_sample_percentage']: if spot_check_config['data_tolerance_percentage'] > spot_check_config['data_sample_percentage']:
raise ValueError( raise ValueError(

View File

@ -52,8 +52,8 @@ def make_archive_filter_flags(local_borg_version, config, checks, check_argument
def make_check_flags(checks, archive_filter_flags): def make_check_flags(checks, archive_filter_flags):
''' '''
Given a parsed sequence of checks and a sequence of flags to filter archives, transform the Given a parsed checks set and a sequence of flags to filter archives,
checks into tuple of command-line check flags. transform the checks into tuple of command-line check flags.
For example, given parsed checks of: For example, given parsed checks of:
@ -68,13 +68,13 @@ def make_check_flags(checks, archive_filter_flags):
''' '''
if 'data' in checks: if 'data' in checks:
data_flags = ('--verify-data',) data_flags = ('--verify-data',)
checks += ('archives',) checks.update({'archives'})
else: else:
data_flags = () data_flags = ()
common_flags = (archive_filter_flags if 'archives' in checks else ()) + data_flags common_flags = (archive_filter_flags if 'archives' in checks else ()) + data_flags
if {'repository', 'archives'}.issubset(set(checks)): if {'repository', 'archives'}.issubset(checks):
return common_flags return common_flags
return ( return (

View File

@ -104,8 +104,13 @@ def extract_archive(
if not paths: if not paths:
raise ValueError('The --strip-components flag with "all" requires at least one --path') raise ValueError('The --strip-components flag with "all" requires at least one --path')
# Calculate the maximum number of leading path components of the given paths. # Calculate the maximum number of leading path components of the given paths. "if piece"
strip_components = max(0, *(len(path.split(os.path.sep)) - 1 for path in paths)) # ignores empty path components, e.g. those resulting from a leading slash. And the "- 1"
# is so this doesn't count the final path component, e.g. the filename itself.
strip_components = max(
0,
*(len(tuple(piece for piece in path.split(os.path.sep) if piece)) - 1 for path in paths)
)
full_command = ( full_command = (
(local_path, 'extract') (local_path, 'extract')

View File

@ -50,12 +50,15 @@ def apply_constants(value, constants, shell_escape=False):
value[index] = apply_constants(list_value, constants, shell_escape) value[index] = apply_constants(list_value, constants, shell_escape)
elif isinstance(value, dict): elif isinstance(value, dict):
for option_name, option_value in value.items(): for option_name, option_value in value.items():
shell_escape = ( value[option_name] = apply_constants(
shell_escape option_value,
or option_name.startswith('before_') constants,
or option_name.startswith('after_') shell_escape=(
or option_name == 'on_error' shell_escape
or option_name.startswith('before_')
or option_name.startswith('after_')
or option_name == 'on_error'
),
) )
value[option_name] = apply_constants(option_value, constants, shell_escape)
return value return value

View File

@ -269,7 +269,8 @@ properties:
compression: compression:
type: string type: string
description: | description: |
Type of compression to use when creating archives. See Type of compression to use when creating archives. (Compression
level can be added separated with a comma, like "zstd,7".) See
http://borgbackup.readthedocs.io/en/stable/usage/create.html for http://borgbackup.readthedocs.io/en/stable/usage/create.html for
details. Defaults to "lz4". details. Defaults to "lz4".
example: lz4 example: lz4
@ -1662,6 +1663,14 @@ properties:
states. states.
example: example:
- finish - finish
create_slug:
type: boolean
description: |
Create the check if it does not exist. Only works with
the slug URL scheme (https://hc-ping.com/<ping-key>/<slug>
as opposed to https://hc-ping.com/<uuid>).
Defaults to false.
example: true
description: | description: |
Configuration for a monitoring integration with Healthchecks. Create Configuration for a monitoring integration with Healthchecks. Create
an account at https://healthchecks.io (or self-host Healthchecks) if an account at https://healthchecks.io (or self-host Healthchecks) if

View File

@ -1,4 +1,5 @@
import logging import logging
import re
import requests import requests
@ -59,10 +60,20 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
) )
return return
ping_url_is_uuid = re.search(r'\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$', ping_url)
healthchecks_state = MONITOR_STATE_TO_HEALTHCHECKS.get(state) healthchecks_state = MONITOR_STATE_TO_HEALTHCHECKS.get(state)
if healthchecks_state: if healthchecks_state:
ping_url = f'{ping_url}/{healthchecks_state}' ping_url = f'{ping_url}/{healthchecks_state}'
if hook_config.get('create_slug'):
if ping_url_is_uuid:
logger.warning(
f'{config_filename}: Healthchecks UUIDs do not support auto provisionning; ignoring'
)
else:
ping_url = f'{ping_url}?create=1'
logger.info(f'{config_filename}: Pinging Healthchecks {state.name.lower()}{dry_run_label}') logger.info(f'{config_filename}: Pinging Healthchecks {state.name.lower()}{dry_run_label}')
logger.debug(f'{config_filename}: Using Healthchecks ping URL {ping_url}') logger.debug(f'{config_filename}: Using Healthchecks ping URL {ping_url}')

View File

@ -88,6 +88,11 @@ class Multi_stream_handler(logging.Handler):
handler.setLevel(level) handler.setLevel(level)
class Console_no_color_formatter(logging.Formatter):
def format(self, record):
return record.msg
class Console_color_formatter(logging.Formatter): class Console_color_formatter(logging.Formatter):
def format(self, record): def format(self, record):
add_custom_log_levels() add_custom_log_levels()
@ -198,6 +203,8 @@ def configure_logging(
if color_enabled: if color_enabled:
console_handler.setFormatter(Console_color_formatter()) console_handler.setFormatter(Console_color_formatter())
else:
console_handler.setFormatter(Console_no_color_formatter())
console_handler.setLevel(console_log_level) console_handler.setLevel(console_log_level)

View File

@ -437,19 +437,28 @@ borgmatic's own configuration file. So include your configuration file in
backups to avoid getting caught without a way to restore a database. backups to avoid getting caught without a way to restore a database.
3. borgmatic does not currently support backing up or restoring multiple 3. borgmatic does not currently support backing up or restoring multiple
databases that share the exact same name on different hosts. databases that share the exact same name on different hosts.
4. Because database hooks implicitly enable the `read_special` configuration, 4. When database hooks are enabled, borgmatic instructs Borg to consume
any special files are excluded from backups (named pipes, block devices, special files (via `--read-special`) to support database dump
character devices, and sockets) to prevent hanging. Try a command like `find streaming—regardless of the value of your `read_special` configuration option.
/your/source/path -type b -or -type c -or -type p -or -type s` to find such And because this can cause Borg to hang, borgmatic also automatically excludes
files. Common directories to exclude are `/dev` and `/run`, but that may not special files (and symlinks to them) that Borg may get stuck on. Even so,
be exhaustive. <span class="minilink minilink-addedin">New in version there are still potential edge cases in which applications on your system
1.7.3</span> When database hooks are enabled, borgmatic automatically excludes create new special files *after* borgmatic constructs its exclude list,
special files (and symlinks to special files) that may cause Borg to hang, so resulting in Borg hangs. If that occurs, you can resort to manually excluding
generally you no longer need to manually exclude them. There are potential those files. And if you explicitly set the `read-special` option to `true`,
edge cases though in which applications on your system create new special files borgmatic will opt you out of the auto-exclude feature entirely, but will
*after* borgmatic constructs its exclude list, resulting in Borg hangs. If that still instruct Borg to consume special files—you will just be on your own to
occurs, you can resort to the manual excludes described above. And to opt out exclude them. <span class="minilink minilink-addedin">Prior to version
of the auto-exclude feature entirely, explicitly set `read_special` to true. 1.7.3</span>Special files were not auto-excluded, and you were responsible for
excluding them yourself. Common directories to exclude are `/dev` and `/run`,
but that may not be exhaustive.
5. Database hooks also implicitly enable the `one_file_system` option, which
means Borg won't cross filesystem boundaries when looking for files to backup.
This is especially important when running borgmatic in a container, as
container volumes are mounted as separate filesystems. One work-around is to
explicitly add each mounted volume you'd like to backup to
`source_directories` instead of relying on Borg to include them implicitly via
a parent directory.
### Manual restoration ### Manual restoration

View File

@ -121,7 +121,7 @@ incorrect excludes, inadvertent deletes, files changed by malware, etc.
However, because an exhaustive comparison of all source files against the However, because an exhaustive comparison of all source files against the
latest archive might be too slow, the spot check supports *sampling* a latest archive might be too slow, the spot check supports *sampling* a
percentage of your source files for the comparison, ensuring it falls within percentage of your source files for the comparison, ensuring they fall within
configured tolerances. configured tolerances.
Here's how it works. Start by installing the `xxhash` OS package if you don't Here's how it works. Start by installing the `xxhash` OS package if you don't
@ -149,12 +149,11 @@ fail.)
The `data_sample_percentage` is the percentage of total files in the source The `data_sample_percentage` is the percentage of total files in the source
directories to randomly sample and compare to their corresponding files in the directories to randomly sample and compare to their corresponding files in the
latest backup archive. A higher value allows a more accurate check—and a latest backup archive. A higher value allows a more accurate check—and a
slower one. The comparison is performed by hashing the selected files in each slower one. The comparison is performed by hashing the selected source files
of the source paths and counting hashes that don't match the latest archive. and counting hashes that don't match the latest archive. For instance, if you
For instance, if you have 1,000 source files and your sample percentage is 1%, have 1,000 source files and your sample percentage is 1%, then only 10 source
then only 10 source files will be compared against the latest archive. These files will be compared against the latest archive. These sampled files are
sampled files are selected randomly each time, so in effect the spot check is selected randomly each time, so in effect the spot check is probabilistic.
probabilistic.
The `data_tolerance_percentage` is the percentage of total files in the source The `data_tolerance_percentage` is the percentage of total files in the source
directories that can fail a spot check data comparison without failing the directories that can fail a spot check data comparison without failing the
@ -175,6 +174,11 @@ want the spot check to fail the next time it's run? Run `borgmatic create` to
create a new backup, thereby allowing the next spot check to run against an create a new backup, thereby allowing the next spot check to run against an
archive that contains your recent changes. archive that contains your recent changes.
Because the spot check only looks at the most recent archive, you may not want
to run it immediately after a `create` action (borgmatic's default behavior).
Instead, it may make more sense to run the spot check on a separate schedule
from `create`.
As long as the spot check feature is in beta, it may be subject to breaking As long as the spot check feature is in beta, it may be subject to breaking
changes. But feel free to use it in production if you're okay with that changes. But feel free to use it in production if you're okay with that
caveat, and please [provide any caveat, and please [provide any

View File

@ -420,7 +420,8 @@ pipx](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#installation),
run the following to install Apprise so borgmatic can use it: run the following to install Apprise so borgmatic can use it:
```bash ```bash
sudo pipx install --force borgmatic[Apprise] sudo pipx uninstall borgmatic
sudo pipx install borgmatic[Apprise]
``` ```
Omit `sudo` if borgmatic is installed as a non-root user. Omit `sudo` if borgmatic is installed as a non-root user.
@ -435,11 +436,16 @@ apprise:
label: gotify label: gotify
- url: mastodons://access_key@hostname/@user - url: mastodons://access_key@hostname/@user
label: mastodon label: mastodon
states:
- start
- finish
- fail
``` ```
With this configuration, borgmatic pings each of the configured Apprise With this configuration, borgmatic pings each of the configured Apprise
services when a backup begins, ends, or errors, but only when any of the services when a backup begins, ends, or errors, but only when any of the
`prune`, `compact`, `create`, or `check` actions are run. `prune`, `compact`, `create`, or `check` actions are run. (By default, if
`states` is not specified, Apprise services are only pinged on error.)
You can optionally customize the contents of the default messages sent to You can optionally customize the contents of the default messages sent to
these services: these services:

View File

@ -1,6 +1,6 @@
from setuptools import find_packages, setup from setuptools import find_packages, setup
VERSION = '1.8.10' VERSION = '1.8.12.dev0'
setup( setup(

View File

@ -769,6 +769,36 @@ def test_compare_spot_check_hashes_considers_non_existent_path_as_not_matching()
) == ('/bar',) ) == ('/bar',)
def test_spot_check_without_spot_configuration_errors():
with pytest.raises(ValueError):
module.spot_check(
repository={'path': 'repo'},
config={
'checks': [
{
'name': 'archives',
},
]
},
local_borg_version=flexmock(),
global_arguments=flexmock(),
local_path=flexmock(),
remote_path=flexmock(),
)
def test_spot_check_without_any_configuration_errors():
with pytest.raises(ValueError):
module.spot_check(
repository={'path': 'repo'},
config={},
local_borg_version=flexmock(),
global_arguments=flexmock(),
local_path=flexmock(),
remote_path=flexmock(),
)
def test_spot_check_data_tolerance_percenatge_greater_than_data_sample_percentage_errors(): def test_spot_check_data_tolerance_percenatge_greater_than_data_sample_percentage_errors():
with pytest.raises(ValueError): with pytest.raises(ValueError):
module.spot_check( module.spot_check(

View File

@ -223,25 +223,25 @@ def test_make_archive_filter_flags_with_default_checks_and_prefix_includes_match
def test_make_check_flags_with_repository_check_returns_flag(): def test_make_check_flags_with_repository_check_returns_flag():
flags = module.make_check_flags(('repository',), ()) flags = module.make_check_flags({'repository'}, ())
assert flags == ('--repository-only',) assert flags == ('--repository-only',)
def test_make_check_flags_with_archives_check_returns_flag(): def test_make_check_flags_with_archives_check_returns_flag():
flags = module.make_check_flags(('archives',), ()) flags = module.make_check_flags({'archives'}, ())
assert flags == ('--archives-only',) assert flags == ('--archives-only',)
def test_make_check_flags_with_archives_check_and_archive_filter_flags_includes_those_flags(): def test_make_check_flags_with_archives_check_and_archive_filter_flags_includes_those_flags():
flags = module.make_check_flags(('archives',), ('--match-archives', 'sh:foo-*')) flags = module.make_check_flags({'archives'}, ('--match-archives', 'sh:foo-*'))
assert flags == ('--archives-only', '--match-archives', 'sh:foo-*') assert flags == ('--archives-only', '--match-archives', 'sh:foo-*')
def test_make_check_flags_without_archives_check_and_with_archive_filter_flags_includes_those_flags(): def test_make_check_flags_without_archives_check_and_with_archive_filter_flags_includes_those_flags():
flags = module.make_check_flags(('repository',), ('--match-archives', 'sh:foo-*')) flags = module.make_check_flags({'repository'}, ('--match-archives', 'sh:foo-*'))
assert flags == ('--repository-only',) assert flags == ('--repository-only',)
@ -250,7 +250,7 @@ def test_make_check_flags_with_data_check_returns_flag_and_implies_archives():
flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
flags = module.make_check_flags(('data',), ()) flags = module.make_check_flags({'data'}, ())
assert flags == ( assert flags == (
'--archives-only', '--archives-only',
@ -262,7 +262,7 @@ def test_make_check_flags_with_extract_omits_extract_flag():
flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
flags = module.make_check_flags(('extract',), ()) flags = module.make_check_flags({'extract'}, ())
assert flags == () assert flags == ()
@ -272,10 +272,10 @@ def test_make_check_flags_with_repository_and_data_checks_does_not_return_reposi
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(()) flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
flags = module.make_check_flags( flags = module.make_check_flags(
( {
'repository', 'repository',
'data', 'data',
), },
(), (),
) )

View File

@ -507,6 +507,39 @@ def test_extract_archive_calls_borg_with_strip_components_calculated_from_all():
) )
def test_extract_archive_calls_borg_with_strip_components_calculated_from_all_with_leading_slash():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
(
'borg',
'extract',
'--strip-components',
'2',
'repo::archive',
'/foo/bar/baz.txt',
'/foo/bar.txt',
)
)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
flexmock(module.borgmatic.config.validate).should_receive(
'normalize_repository_path'
).and_return('repo')
module.extract_archive(
dry_run=False,
repository='repo',
archive='archive',
paths=['/foo/bar/baz.txt', '/foo/bar.txt'],
config={},
local_borg_version='1.2.3',
global_arguments=flexmock(log_json=False),
strip_components='all',
)
def test_extract_archive_with_strip_components_all_and_no_paths_raises(): def test_extract_archive_with_strip_components_all_and_no_paths_raises():
flexmock(module.os.path).should_receive('abspath').and_return('repo') flexmock(module.os.path).should_receive('abspath').and_return('repo')
flexmock(module.feature).should_receive('available').and_return(True) flexmock(module.feature).should_receive('available').and_return(True)

View File

@ -50,6 +50,16 @@ def test_apply_constants_with_empty_constants_passes_through_value():
({'before_backup': '{inject}'}, {'before_backup': "'echo hi; naughty-command'"}), ({'before_backup': '{inject}'}, {'before_backup': "'echo hi; naughty-command'"}),
({'after_backup': '{inject}'}, {'after_backup': "'echo hi; naughty-command'"}), ({'after_backup': '{inject}'}, {'after_backup': "'echo hi; naughty-command'"}),
({'on_error': '{inject}'}, {'on_error': "'echo hi; naughty-command'"}), ({'on_error': '{inject}'}, {'on_error': "'echo hi; naughty-command'"}),
(
{
'before_backup': '{env_pass}',
'postgresql_databases': [{'name': 'users', 'password': '{env_pass}'}],
},
{
'before_backup': "'${PASS}'",
'postgresql_databases': [{'name': 'users', 'password': '${PASS}'}],
},
),
(3, 3), (3, 3),
(True, True), (True, True),
(False, False), (False, False),
@ -63,6 +73,7 @@ def test_apply_constants_makes_string_substitutions(value, expected_value):
'int': 3, 'int': 3,
'bool': True, 'bool': True,
'inject': 'echo hi; naughty-command', 'inject': 'echo hi; naughty-command',
'env_pass': '${PASS}',
} }
assert module.apply_constants(value, constants) == expected_value assert module.apply_constants(value, constants) == expected_value

View File

@ -264,6 +264,72 @@ def test_ping_monitor_hits_ping_url_when_states_matching():
) )
def test_ping_monitor_adds_create_query_parameter_when_create_slug_true():
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
hook_config = {'ping_url': 'https://example.com', 'create_slug': True}
flexmock(module.requests).should_receive('post').with_args(
'https://example.com/start?create=1', data=''.encode('utf-8'), verify=True
).and_return(flexmock(ok=True))
module.ping_monitor(
hook_config,
{},
'config.yaml',
state=module.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_does_not_add_create_query_parameter_when_create_slug_false():
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
hook_config = {'ping_url': 'https://example.com', 'create_slug': False}
flexmock(module.requests).should_receive('post').with_args(
'https://example.com/start', data=''.encode('utf-8'), verify=True
).and_return(flexmock(ok=True))
module.ping_monitor(
hook_config,
{},
'config.yaml',
state=module.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_does_not_add_create_query_parameter_when_ping_url_is_uuid():
hook_config = {'ping_url': 'b3611b24-df9c-4d36-9203-fa292820bf2a', 'create_slug': True}
flexmock(module.requests).should_receive('post').with_args(
f"https://hc-ping.com/{hook_config['ping_url']}",
data=''.encode('utf-8'),
verify=True,
).and_return(flexmock(ok=True))
module.ping_monitor(
hook_config,
{},
'config.yaml',
state=module.monitor.State.FINISH,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_issues_warning_when_ping_url_is_uuid_and_create_slug_true():
hook_config = {'ping_url': 'b3611b24-df9c-4d36-9203-fa292820bf2a', 'create_slug': True}
flexmock(module.logger).should_receive('warning').once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
state=module.monitor.State.FINISH,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_with_connection_error_logs_warning(): def test_ping_monitor_with_connection_error_logs_warning():
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never() flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
hook_config = {'ping_url': 'https://example.com'} hook_config = {'ping_url': 'https://example.com'}

View File

@ -217,10 +217,11 @@ def test_add_logging_level_skips_global_setting_if_already_set():
def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_linux(): def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_linux():
flexmock(module).should_receive('add_custom_log_levels') flexmock(module).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.ANSWER flexmock(module.logging).ANSWER = module.ANSWER
fake_formatter = flexmock()
flexmock(module).should_receive('Console_color_formatter').and_return(fake_formatter)
multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO) multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
multi_stream_handler.should_receive('setFormatter').once() multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler) flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module).should_receive('Console_color_formatter')
flexmock(module).should_receive('interactive_console').and_return(False) flexmock(module).should_receive('interactive_console').and_return(False)
flexmock(module.logging).should_receive('basicConfig').with_args( flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.DEBUG, handlers=list level=logging.DEBUG, handlers=list
@ -237,10 +238,11 @@ def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_linux(
def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_macos(): def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_macos():
flexmock(module).should_receive('add_custom_log_levels') flexmock(module).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.ANSWER flexmock(module.logging).ANSWER = module.ANSWER
fake_formatter = flexmock()
flexmock(module).should_receive('Console_color_formatter').and_return(fake_formatter)
multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO) multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
multi_stream_handler.should_receive('setFormatter').once() multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler) flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module).should_receive('Console_color_formatter')
flexmock(module).should_receive('interactive_console').and_return(False) flexmock(module).should_receive('interactive_console').and_return(False)
flexmock(module.logging).should_receive('basicConfig').with_args( flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.DEBUG, handlers=list level=logging.DEBUG, handlers=list
@ -258,10 +260,11 @@ def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_macos(
def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_freebsd(): def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_freebsd():
flexmock(module).should_receive('add_custom_log_levels') flexmock(module).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.ANSWER flexmock(module.logging).ANSWER = module.ANSWER
fake_formatter = flexmock()
flexmock(module).should_receive('Console_color_formatter').and_return(fake_formatter)
multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO) multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
multi_stream_handler.should_receive('setFormatter').once() multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler) flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module).should_receive('Console_color_formatter')
flexmock(module).should_receive('interactive_console').and_return(False) flexmock(module).should_receive('interactive_console').and_return(False)
flexmock(module.logging).should_receive('basicConfig').with_args( flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.DEBUG, handlers=list level=logging.DEBUG, handlers=list
@ -280,10 +283,11 @@ def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_freebs
def test_configure_logging_without_syslog_log_level_skips_syslog(): def test_configure_logging_without_syslog_log_level_skips_syslog():
flexmock(module).should_receive('add_custom_log_levels') flexmock(module).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.ANSWER flexmock(module.logging).ANSWER = module.ANSWER
fake_formatter = flexmock()
flexmock(module).should_receive('Console_color_formatter').and_return(fake_formatter)
multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO) multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
multi_stream_handler.should_receive('setFormatter').once() multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler) flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module).should_receive('Console_color_formatter')
flexmock(module.logging).should_receive('basicConfig').with_args( flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.INFO, handlers=list level=logging.INFO, handlers=list
) )
@ -296,10 +300,11 @@ def test_configure_logging_without_syslog_log_level_skips_syslog():
def test_configure_logging_skips_syslog_if_not_found(): def test_configure_logging_skips_syslog_if_not_found():
flexmock(module).should_receive('add_custom_log_levels') flexmock(module).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.ANSWER flexmock(module.logging).ANSWER = module.ANSWER
fake_formatter = flexmock()
flexmock(module).should_receive('Console_color_formatter').and_return(fake_formatter)
multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO) multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
multi_stream_handler.should_receive('setFormatter').once() multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler) flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module).should_receive('Console_color_formatter')
flexmock(module.logging).should_receive('basicConfig').with_args( flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.INFO, handlers=list level=logging.INFO, handlers=list
) )
@ -312,8 +317,10 @@ def test_configure_logging_skips_syslog_if_not_found():
def test_configure_logging_skips_log_file_if_log_file_logging_is_disabled(): def test_configure_logging_skips_log_file_if_log_file_logging_is_disabled():
flexmock(module).should_receive('add_custom_log_levels') flexmock(module).should_receive('add_custom_log_levels')
flexmock(module.logging).DISABLED = module.DISABLED flexmock(module.logging).DISABLED = module.DISABLED
fake_formatter = flexmock()
flexmock(module).should_receive('Console_color_formatter').and_return(fake_formatter)
multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO) multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
multi_stream_handler.should_receive('setFormatter').once() multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler) flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module.logging).should_receive('basicConfig').with_args( flexmock(module.logging).should_receive('basicConfig').with_args(
@ -331,8 +338,10 @@ def test_configure_logging_skips_log_file_if_log_file_logging_is_disabled():
def test_configure_logging_to_log_file_instead_of_syslog(): def test_configure_logging_to_log_file_instead_of_syslog():
flexmock(module).should_receive('add_custom_log_levels') flexmock(module).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.ANSWER flexmock(module.logging).ANSWER = module.ANSWER
fake_formatter = flexmock()
flexmock(module).should_receive('Console_color_formatter').and_return(fake_formatter)
multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO) multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
multi_stream_handler.should_receive('setFormatter').once() multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler) flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module.logging).should_receive('basicConfig').with_args( flexmock(module.logging).should_receive('basicConfig').with_args(
@ -356,8 +365,10 @@ def test_configure_logging_to_log_file_instead_of_syslog():
def test_configure_logging_to_both_log_file_and_syslog(): def test_configure_logging_to_both_log_file_and_syslog():
flexmock(module).should_receive('add_custom_log_levels') flexmock(module).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.ANSWER flexmock(module.logging).ANSWER = module.ANSWER
fake_formatter = flexmock()
flexmock(module).should_receive('Console_color_formatter').and_return(fake_formatter)
multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO) multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
multi_stream_handler.should_receive('setFormatter').once() multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler) flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module.logging).should_receive('basicConfig').with_args( flexmock(module.logging).should_receive('basicConfig').with_args(
@ -387,8 +398,10 @@ def test_configure_logging_to_log_file_formats_with_custom_log_format():
flexmock(module.logging).should_receive('Formatter').with_args( flexmock(module.logging).should_receive('Formatter').with_args(
'{message}', style='{' # noqa: FS003 '{message}', style='{' # noqa: FS003
).once() ).once()
fake_formatter = flexmock()
flexmock(module).should_receive('Console_color_formatter').and_return(fake_formatter)
multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO) multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
multi_stream_handler.should_receive('setFormatter').once() multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler) flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module).should_receive('interactive_console').and_return(False) flexmock(module).should_receive('interactive_console').and_return(False)
@ -413,8 +426,10 @@ def test_configure_logging_to_log_file_formats_with_custom_log_format():
def test_configure_logging_skips_log_file_if_argument_is_none(): def test_configure_logging_skips_log_file_if_argument_is_none():
flexmock(module).should_receive('add_custom_log_levels') flexmock(module).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.ANSWER flexmock(module.logging).ANSWER = module.ANSWER
fake_formatter = flexmock()
flexmock(module).should_receive('Console_color_formatter').and_return(fake_formatter)
multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO) multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
multi_stream_handler.should_receive('setFormatter').once() multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler) flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module.logging).should_receive('basicConfig').with_args( flexmock(module.logging).should_receive('basicConfig').with_args(
@ -426,11 +441,14 @@ def test_configure_logging_skips_log_file_if_argument_is_none():
module.configure_logging(console_log_level=logging.INFO, log_file=None) module.configure_logging(console_log_level=logging.INFO, log_file=None)
def test_configure_logging_skips_console_color_formatter_if_color_disabled(): def test_configure_logging_uses_console_no_color_formatter_if_color_disabled():
flexmock(module).should_receive('add_custom_log_levels') flexmock(module).should_receive('add_custom_log_levels')
flexmock(module.logging).ANSWER = module.ANSWER flexmock(module.logging).ANSWER = module.ANSWER
fake_formatter = flexmock()
flexmock(module).should_receive('Console_color_formatter').never()
flexmock(module).should_receive('Console_no_color_formatter').and_return(fake_formatter)
multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO) multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
multi_stream_handler.should_receive('setFormatter').never() multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler) flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
flexmock(module.logging).should_receive('basicConfig').with_args( flexmock(module.logging).should_receive('basicConfig').with_args(