mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-25 19:23:00 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbdb09b87d | ||
|
|
bec5a0c0ca | ||
|
|
4ee7f72696 | ||
|
|
68b6d01071 | ||
|
|
86b138e73b | ||
|
|
c76a108422 | ||
|
|
eb5dc128bf | ||
|
|
1d486d024b | ||
|
|
5a8f27d75c | ||
|
|
a926b413bc | ||
|
|
18ffd96d62 | ||
|
|
c0135864c2 | ||
|
|
ddfd3c6ca1 |
@@ -1,3 +1,15 @@
|
||||
1.9.14
|
||||
* #409: With the PagerDuty monitoring hook, send borgmatic logs to PagerDuty so they show up in the
|
||||
incident UI. See the documentation for more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook
|
||||
* #936: Clarify Zabbix monitoring hook documentation about creating items:
|
||||
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#zabbix-hook
|
||||
* #1017: Fix a regression in which some MariaDB/MySQL passwords were not escaped correctly.
|
||||
* #1021: Fix a regression in which the "exclude_patterns" option didn't expand "~" (the user's
|
||||
home directory). This fix means that all "patterns" and "patterns_from" also now expand "~".
|
||||
* #1023: Fix an error in the Btrfs hook when attempting to snapshot a read-only subvolume. Now,
|
||||
read-only subvolumes are ignored since Btrfs can't actually snapshot them.
|
||||
|
||||
1.9.13
|
||||
* #975: Add a "compression" option to the PostgreSQL database hook.
|
||||
* #1001: Fix a ZFS error during snapshot cleanup.
|
||||
|
||||
@@ -130,8 +130,11 @@ def expand_directory(directory, working_directory):
|
||||
def expand_patterns(patterns, working_directory=None, skip_paths=None):
|
||||
'''
|
||||
Given a sequence of borgmatic.borg.pattern.Pattern instances and an optional working directory,
|
||||
expand tildes and globs in each root pattern. Return all the resulting patterns (not just the
|
||||
root patterns) as a tuple.
|
||||
expand tildes and globs in each root pattern and expand just tildes in each non-root pattern.
|
||||
The idea is that non-root patterns may be regular expressions or other pattern styles containing
|
||||
"*" that borgmatic should not expand as a shell glob.
|
||||
|
||||
Return all the resulting patterns as a tuple.
|
||||
|
||||
If a set of paths are given to skip, then don't expand any patterns matching them.
|
||||
'''
|
||||
@@ -153,7 +156,15 @@ def expand_patterns(patterns, working_directory=None, skip_paths=None):
|
||||
)
|
||||
if pattern.type == borgmatic.borg.pattern.Pattern_type.ROOT
|
||||
and pattern.path not in (skip_paths or ())
|
||||
else (pattern,)
|
||||
else (
|
||||
borgmatic.borg.pattern.Pattern(
|
||||
os.path.expanduser(pattern.path),
|
||||
pattern.type,
|
||||
pattern.style,
|
||||
pattern.device,
|
||||
pattern.source,
|
||||
),
|
||||
)
|
||||
)
|
||||
for pattern in patterns
|
||||
)
|
||||
|
||||
@@ -2279,6 +2279,12 @@ properties:
|
||||
PagerDuty integration key used to notify PagerDuty when a
|
||||
backup errors. Supports the "{credential ...}" syntax.
|
||||
example: a177cad45bd374409f78906a810a3074
|
||||
send_logs:
|
||||
type: boolean
|
||||
description: |
|
||||
Send borgmatic logs to PagerDuty when a backup errors.
|
||||
Defaults to true.
|
||||
example: false
|
||||
description: |
|
||||
Configuration for a monitoring integration with PagerDuty. Create an
|
||||
account at https://www.pagerduty.com if you'd like to use this
|
||||
|
||||
@@ -48,6 +48,47 @@ def get_subvolume_mount_points(findmnt_command):
|
||||
Subvolume = collections.namedtuple('Subvolume', ('path', 'contained_patterns'), defaults=((),))
|
||||
|
||||
|
||||
def get_subvolume_property(btrfs_command, subvolume_path, property_name):
|
||||
output = borgmatic.execute.execute_command_and_capture_output(
|
||||
tuple(btrfs_command.split(' '))
|
||||
+ (
|
||||
'property',
|
||||
'get',
|
||||
'-t', # Type.
|
||||
'subvol',
|
||||
subvolume_path,
|
||||
property_name,
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
value = output.strip().split('=')[1]
|
||||
except IndexError:
|
||||
raise ValueError(f'Invalid {btrfs_command} property output')
|
||||
|
||||
return {
|
||||
'true': True,
|
||||
'false': False,
|
||||
}.get(value, value)
|
||||
|
||||
|
||||
def omit_read_only_subvolume_mount_points(btrfs_command, subvolume_paths):
|
||||
'''
|
||||
Given a Btrfs command to run and a sequence of Btrfs subvolume mount points, filter them down to
|
||||
just those that are read-write. The idea is that Btrfs can't actually snapshot a read-only
|
||||
subvolume, so we should just ignore them.
|
||||
'''
|
||||
retained_subvolume_paths = []
|
||||
|
||||
for subvolume_path in subvolume_paths:
|
||||
if get_subvolume_property(btrfs_command, subvolume_path, 'ro'):
|
||||
logger.debug(f'Ignoring Btrfs subvolume {subvolume_path} because it is read-only')
|
||||
else:
|
||||
retained_subvolume_paths.append(subvolume_path)
|
||||
|
||||
return tuple(retained_subvolume_paths)
|
||||
|
||||
|
||||
def get_subvolumes(btrfs_command, findmnt_command, patterns=None):
|
||||
'''
|
||||
Given a Btrfs command to run and a sequence of configured patterns, find the intersection
|
||||
@@ -67,7 +108,11 @@ def get_subvolumes(btrfs_command, findmnt_command, patterns=None):
|
||||
# backup. Sort the subvolumes from longest to shortest mount points, so longer mount points get
|
||||
# a whack at the candidate pattern piñata before their parents do. (Patterns are consumed during
|
||||
# this process, so no two subvolumes end up with the same contained patterns.)
|
||||
for mount_point in reversed(get_subvolume_mount_points(findmnt_command)):
|
||||
for mount_point in reversed(
|
||||
omit_read_only_subvolume_mount_points(
|
||||
btrfs_command, get_subvolume_mount_points(findmnt_command)
|
||||
)
|
||||
):
|
||||
subvolumes.extend(
|
||||
Subvolume(mount_point, contained_patterns)
|
||||
for contained_patterns in (
|
||||
|
||||
@@ -65,10 +65,12 @@ def make_defaults_file_options(username=None, password=None, defaults_extra_file
|
||||
Do not use the returned value for multiple different command invocations. That will not work
|
||||
because each pipe is "used up" once read.
|
||||
'''
|
||||
escaped_password = None if password is None else password.replace('\\', '\\\\')
|
||||
|
||||
values = '\n'.join(
|
||||
(
|
||||
(f'user={username}' if username is not None else ''),
|
||||
(f'password={password}' if password is not None else ''),
|
||||
(f'password="{escaped_password}"' if escaped_password is not None else ''),
|
||||
)
|
||||
).strip()
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ def get_handler(identifier):
|
||||
def format_buffered_logs_for_payload(identifier):
|
||||
'''
|
||||
Get the handler previously added to the root logger, and slurp buffered logs out of it to
|
||||
send to Healthchecks.
|
||||
send to the monitoring service.
|
||||
'''
|
||||
try:
|
||||
buffering_handler = get_handler(identifier)
|
||||
|
||||
@@ -6,20 +6,36 @@ import platform
|
||||
import requests
|
||||
|
||||
import borgmatic.hooks.credential.parse
|
||||
import borgmatic.hooks.monitoring.logs
|
||||
from borgmatic.hooks.monitoring import monitor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EVENTS_API_URL = 'https://events.pagerduty.com/v2/enqueue'
|
||||
DEFAULT_LOGS_PAYLOAD_LIMIT_BYTES = 10000
|
||||
HANDLER_IDENTIFIER = 'pagerduty'
|
||||
|
||||
|
||||
def initialize_monitor(
|
||||
integration_key, config, config_filename, monitoring_log_level, dry_run
|
||||
): # pragma: no cover
|
||||
def initialize_monitor(hook_config, config, config_filename, monitoring_log_level, dry_run):
|
||||
'''
|
||||
No initialization is necessary for this monitor.
|
||||
Add a handler to the root logger that stores in memory the most recent logs emitted. That way,
|
||||
we can send them all to PagerDuty upon a failure state. But skip this if the "send_logs" option
|
||||
is false.
|
||||
'''
|
||||
pass
|
||||
if hook_config.get('send_logs') is False:
|
||||
return
|
||||
|
||||
ping_body_limit = max(
|
||||
DEFAULT_LOGS_PAYLOAD_LIMIT_BYTES
|
||||
- len(borgmatic.hooks.monitoring.logs.PAYLOAD_TRUNCATION_INDICATOR),
|
||||
0,
|
||||
)
|
||||
|
||||
borgmatic.hooks.monitoring.logs.add_handler(
|
||||
borgmatic.hooks.monitoring.logs.Forgetful_buffering_handler(
|
||||
HANDLER_IDENTIFIER, ping_body_limit, monitoring_log_level
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
|
||||
@@ -37,9 +53,6 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||
dry_run_label = ' (dry run; not actually sending)' if dry_run else ''
|
||||
logger.info(f'Sending failure event to PagerDuty {dry_run_label}')
|
||||
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
try:
|
||||
integration_key = borgmatic.hooks.credential.parse.resolve_credential(
|
||||
hook_config.get('integration_key'), config
|
||||
@@ -48,6 +61,10 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||
logger.warning(f'PagerDuty credential error: {error}')
|
||||
return
|
||||
|
||||
logs_payload = borgmatic.hooks.monitoring.logs.format_buffered_logs_for_payload(
|
||||
HANDLER_IDENTIFIER
|
||||
)
|
||||
|
||||
hostname = platform.node()
|
||||
local_timestamp = datetime.datetime.now(datetime.timezone.utc).astimezone().isoformat()
|
||||
payload = json.dumps(
|
||||
@@ -66,11 +83,14 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||
'hostname': hostname,
|
||||
'configuration filename': config_filename,
|
||||
'server time': local_timestamp,
|
||||
'logs': logs_payload,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
logger.debug(f'Using PagerDuty payload: {payload}')
|
||||
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||
try:
|
||||
@@ -83,6 +103,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||
|
||||
def destroy_monitor(ping_url_or_uuid, config, monitoring_log_level, dry_run): # pragma: no cover
|
||||
'''
|
||||
No destruction is necessary for this monitor.
|
||||
Remove the monitor handler that was added to the root logger. This prevents the handler from
|
||||
getting reused by other instances of this monitor.
|
||||
'''
|
||||
pass
|
||||
borgmatic.hooks.monitoring.logs.remove_handler(HANDLER_IDENTIFIER)
|
||||
|
||||
@@ -7,12 +7,13 @@ eleventyNavigation:
|
||||
---
|
||||
## Preparation and cleanup hooks
|
||||
|
||||
If you find yourself performing preparation tasks before your backup runs, or
|
||||
cleanup work afterwards, borgmatic hooks may be of interest. Hooks are shell
|
||||
commands that borgmatic executes for you at various points as it runs, and
|
||||
they're configured in the `hooks` section of your configuration file. But if
|
||||
you're looking to backup a database, it's probably easier to use the [database
|
||||
backup
|
||||
If you find yourself performing preparation tasks before your backup runs or
|
||||
cleanup work afterwards, borgmatic command hooks may be of interest. These are
|
||||
custom shell commands you can configure borgmatic to execute at various points
|
||||
as it runs.
|
||||
|
||||
But if you're looking to backup a database, it's probably easier to use the
|
||||
[database backup
|
||||
feature](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/)
|
||||
instead.
|
||||
|
||||
|
||||
@@ -292,6 +292,27 @@ If you have any issues with the integration, [please contact
|
||||
us](https://torsion.org/borgmatic/#support-and-contributing).
|
||||
|
||||
|
||||
### Sending logs
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.9.14</span> borgmatic
|
||||
logs are included in the payload data sent to PagerDuty. This means that
|
||||
(truncated) borgmatic logs, including error messages, show up in the PagerDuty
|
||||
incident UI and corresponding notification emails.
|
||||
|
||||
You can customize the verbosity of the logs that are sent with borgmatic's
|
||||
`--monitoring-verbosity` flag. The `--list` and `--stats` flags may also be of
|
||||
use. See `borgmatic create --help` for more information.
|
||||
|
||||
If you don't want any logs sent, you can disable this feature by setting
|
||||
`send_logs` to `false`:
|
||||
|
||||
```yaml
|
||||
pagerduty:
|
||||
integration_key: a177cad45bd374409f78906a810a3074
|
||||
send_logs: false
|
||||
```
|
||||
|
||||
|
||||
## Pushover hook
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.9.2</span>
|
||||
@@ -724,11 +745,19 @@ Authentication can be accomplished via `api_key` or both `username` and
|
||||
|
||||
### Items
|
||||
|
||||
The item to be updated can be chosen by either declaring the `itemid` or both
|
||||
`host` and `key`. If all three are declared, only `itemid` is used.
|
||||
borgmatic writes its monitoring updates to a particular Zabbix item, which
|
||||
you'll need to create in advance. In the Zabbix web UI, [make a new item with a
|
||||
Type of "Zabbix
|
||||
trapper"](https://www.zabbix.com/documentation/current/en/manual/config/items/itemtypes/trapper)
|
||||
and a named Key. The "Type of information" for the item should be "Text", and
|
||||
"History" designates how much data you want to retain.
|
||||
|
||||
Keep in mind that `host` is referring to the "Host name" on the Zabbix server
|
||||
and not the "Visual name".
|
||||
When configuring borgmatic with this item to be updated, you can either declare
|
||||
the `itemid` or both `host` and `key`. If all three are declared, only `itemid`
|
||||
is used.
|
||||
|
||||
Keep in mind that `host` refers to the "Host name" on the Zabbix server and not
|
||||
the "Visual name".
|
||||
|
||||
|
||||
## Scripting borgmatic
|
||||
|
||||
@@ -148,8 +148,9 @@ feedback](https://torsion.org/borgmatic/#issues) you have on this feature.
|
||||
|
||||
#### Subvolume discovery
|
||||
|
||||
For any subvolume you'd like backed up, add its path to borgmatic's
|
||||
`source_directories` option.
|
||||
For any read-write subvolume you'd like backed up, add its path to borgmatic's
|
||||
`source_directories` option. Btrfs does not support snapshotting read-only
|
||||
subvolumes.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.9.6</span> Or include
|
||||
the mount point as a root pattern with borgmatic's `patterns` or `patterns_from`
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "borgmatic"
|
||||
version = "1.9.13"
|
||||
version = "1.9.14"
|
||||
authors = [
|
||||
{ name="Dan Helfman", email="witten@torsion.org" },
|
||||
]
|
||||
|
||||
@@ -24,7 +24,14 @@ def parse_arguments(*unparsed_arguments):
|
||||
delete_parser = subvolume_subparser.add_parser('delete')
|
||||
delete_parser.add_argument('snapshot_path')
|
||||
|
||||
return global_parser.parse_args(unparsed_arguments)
|
||||
property_parser = action_parsers.add_parser('property')
|
||||
property_subparser = property_parser.add_subparsers(dest='subaction')
|
||||
get_parser = property_subparser.add_parser('get')
|
||||
get_parser.add_argument('-t', dest='type')
|
||||
get_parser.add_argument('subvolume_path')
|
||||
get_parser.add_argument('property_name')
|
||||
|
||||
return (global_parser, global_parser.parse_args(unparsed_arguments))
|
||||
|
||||
|
||||
BUILTIN_SUBVOLUME_LIST_LINES = (
|
||||
@@ -60,9 +67,13 @@ def print_subvolume_list(arguments, snapshot_paths):
|
||||
|
||||
|
||||
def main():
|
||||
arguments = parse_arguments(*sys.argv[1:])
|
||||
(global_parser, arguments) = parse_arguments(*sys.argv[1:])
|
||||
snapshot_paths = load_snapshots()
|
||||
|
||||
if not hasattr(arguments, 'subaction'):
|
||||
global_parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
if arguments.subaction == 'list':
|
||||
print_subvolume_list(arguments, snapshot_paths)
|
||||
elif arguments.subaction == 'snapshot':
|
||||
@@ -84,6 +95,8 @@ def main():
|
||||
if snapshot_path.endswith('/' + arguments.snapshot_path)
|
||||
]
|
||||
save_snapshots(snapshot_paths)
|
||||
elif arguments.action == 'property' and arguments.subaction == 'get':
|
||||
print(f'{arguments.property_name}=false')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -225,15 +225,24 @@ def test_expand_patterns_considers_none_as_no_patterns():
|
||||
assert module.expand_patterns(None) == ()
|
||||
|
||||
|
||||
def test_expand_patterns_only_considers_root_patterns():
|
||||
flexmock(module).should_receive('expand_directory').with_args('~/foo', None).and_return(
|
||||
['/root/foo']
|
||||
def test_expand_patterns_expands_tildes_and_globs_in_root_patterns():
|
||||
flexmock(module.os.path).should_receive('expanduser').never()
|
||||
flexmock(module).should_receive('expand_directory').and_return(
|
||||
['/root/foo/one', '/root/foo/two']
|
||||
)
|
||||
flexmock(module).should_receive('expand_directory').with_args('bar*', None).never()
|
||||
|
||||
paths = module.expand_patterns((Pattern('~/foo'), Pattern('bar*', Pattern_type.INCLUDE)))
|
||||
paths = module.expand_patterns((Pattern('~/foo/*'),))
|
||||
|
||||
assert paths == (Pattern('/root/foo'), Pattern('bar*', Pattern_type.INCLUDE))
|
||||
assert paths == (Pattern('/root/foo/one'), Pattern('/root/foo/two'))
|
||||
|
||||
|
||||
def test_expand_patterns_expands_only_tildes_in_non_root_patterns():
|
||||
flexmock(module).should_receive('expand_directory').never()
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('/root/bar/*')
|
||||
|
||||
paths = module.expand_patterns((Pattern('~/bar/*', Pattern_type.INCLUDE),))
|
||||
|
||||
assert paths == (Pattern('/root/bar/*', Pattern_type.INCLUDE),)
|
||||
|
||||
|
||||
def test_device_map_patterns_gives_device_id_per_path():
|
||||
|
||||
@@ -49,8 +49,61 @@ def test_get_subvolume_mount_points_with_findmnt_json_missing_filesystems_errors
|
||||
module.get_subvolume_mount_points('findmnt')
|
||||
|
||||
|
||||
def test_get_subvolume_property_with_invalid_btrfs_output_errors():
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output'
|
||||
).and_return('invalid')
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.get_subvolume_property('btrfs', '/foo', 'ro')
|
||||
|
||||
|
||||
def test_get_subvolume_property_with_true_output_returns_true_bool():
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output'
|
||||
).and_return('ro=true')
|
||||
|
||||
assert module.get_subvolume_property('btrfs', '/foo', 'ro') is True
|
||||
|
||||
|
||||
def test_get_subvolume_property_with_false_output_returns_false_bool():
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output'
|
||||
).and_return('ro=false')
|
||||
|
||||
assert module.get_subvolume_property('btrfs', '/foo', 'ro') is False
|
||||
|
||||
|
||||
def test_get_subvolume_property_passes_through_general_value():
|
||||
flexmock(module.borgmatic.execute).should_receive(
|
||||
'execute_command_and_capture_output'
|
||||
).and_return('thing=value')
|
||||
|
||||
assert module.get_subvolume_property('btrfs', '/foo', 'thing') == 'value'
|
||||
|
||||
|
||||
def test_omit_read_only_subvolume_mount_points_filters_out_read_only():
|
||||
flexmock(module).should_receive('get_subvolume_property').with_args(
|
||||
'btrfs', '/foo', 'ro'
|
||||
).and_return(False)
|
||||
flexmock(module).should_receive('get_subvolume_property').with_args(
|
||||
'btrfs', '/bar', 'ro'
|
||||
).and_return(True)
|
||||
flexmock(module).should_receive('get_subvolume_property').with_args(
|
||||
'btrfs', '/baz', 'ro'
|
||||
).and_return(False)
|
||||
|
||||
assert module.omit_read_only_subvolume_mount_points('btrfs', ('/foo', '/bar', '/baz')) == (
|
||||
'/foo',
|
||||
'/baz',
|
||||
)
|
||||
|
||||
|
||||
def test_get_subvolumes_collects_subvolumes_matching_patterns():
|
||||
flexmock(module).should_receive('get_subvolume_mount_points').and_return(('/mnt1', '/mnt2'))
|
||||
flexmock(module).should_receive('omit_read_only_subvolume_mount_points').and_return(
|
||||
('/mnt1', '/mnt2')
|
||||
)
|
||||
|
||||
contained_pattern = Pattern(
|
||||
'/mnt1',
|
||||
@@ -76,6 +129,9 @@ def test_get_subvolumes_collects_subvolumes_matching_patterns():
|
||||
|
||||
def test_get_subvolumes_skips_non_root_patterns():
|
||||
flexmock(module).should_receive('get_subvolume_mount_points').and_return(('/mnt1', '/mnt2'))
|
||||
flexmock(module).should_receive('omit_read_only_subvolume_mount_points').and_return(
|
||||
('/mnt1', '/mnt2')
|
||||
)
|
||||
|
||||
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
|
||||
'get_contained_patterns'
|
||||
@@ -107,6 +163,9 @@ def test_get_subvolumes_skips_non_root_patterns():
|
||||
|
||||
def test_get_subvolumes_skips_non_config_patterns():
|
||||
flexmock(module).should_receive('get_subvolume_mount_points').and_return(('/mnt1', '/mnt2'))
|
||||
flexmock(module).should_receive('omit_read_only_subvolume_mount_points').and_return(
|
||||
('/mnt1', '/mnt2')
|
||||
)
|
||||
|
||||
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
|
||||
'get_contained_patterns'
|
||||
@@ -138,6 +197,9 @@ def test_get_subvolumes_skips_non_config_patterns():
|
||||
|
||||
def test_get_subvolumes_without_patterns_collects_all_subvolumes():
|
||||
flexmock(module).should_receive('get_subvolume_mount_points').and_return(('/mnt1', '/mnt2'))
|
||||
flexmock(module).should_receive('omit_read_only_subvolume_mount_points').and_return(
|
||||
('/mnt1', '/mnt2')
|
||||
)
|
||||
|
||||
flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
|
||||
'get_contained_patterns'
|
||||
|
||||
@@ -36,7 +36,7 @@ def test_make_defaults_file_option_with_username_and_password_writes_them_to_fil
|
||||
|
||||
flexmock(module.os).should_receive('pipe').and_return(read_descriptor, write_descriptor)
|
||||
flexmock(module.os).should_receive('write').with_args(
|
||||
write_descriptor, b'[client]\nuser=root\npassword=trustsome1'
|
||||
write_descriptor, b'[client]\nuser=root\npassword="trustsome1"'
|
||||
).once()
|
||||
flexmock(module.os).should_receive('close')
|
||||
flexmock(module.os).should_receive('set_inheritable')
|
||||
@@ -46,6 +46,22 @@ def test_make_defaults_file_option_with_username_and_password_writes_them_to_fil
|
||||
)
|
||||
|
||||
|
||||
def test_make_defaults_file_escapes_password_containing_backslash():
|
||||
read_descriptor = 99
|
||||
write_descriptor = flexmock()
|
||||
|
||||
flexmock(module.os).should_receive('pipe').and_return(read_descriptor, write_descriptor)
|
||||
flexmock(module.os).should_receive('write').with_args(
|
||||
write_descriptor, b'[client]\nuser=root\n' + br'password="trust\\nsome1"'
|
||||
).once()
|
||||
flexmock(module.os).should_receive('close')
|
||||
flexmock(module.os).should_receive('set_inheritable')
|
||||
|
||||
assert module.make_defaults_file_options(username='root', password=r'trust\nsome1') == (
|
||||
'--defaults-extra-file=/dev/fd/99',
|
||||
)
|
||||
|
||||
|
||||
def test_make_defaults_file_pipe_with_only_username_writes_it_to_file_descriptor():
|
||||
read_descriptor = 99
|
||||
write_descriptor = flexmock()
|
||||
@@ -68,7 +84,7 @@ def test_make_defaults_file_pipe_with_only_password_writes_it_to_file_descriptor
|
||||
|
||||
flexmock(module.os).should_receive('pipe').and_return(read_descriptor, write_descriptor)
|
||||
flexmock(module.os).should_receive('write').with_args(
|
||||
write_descriptor, b'[client]\npassword=trustsome1'
|
||||
write_descriptor, b'[client]\npassword="trustsome1"'
|
||||
).once()
|
||||
flexmock(module.os).should_receive('close')
|
||||
flexmock(module.os).should_receive('set_inheritable')
|
||||
@@ -84,7 +100,7 @@ def test_make_defaults_file_option_with_defaults_extra_filename_includes_it_in_f
|
||||
|
||||
flexmock(module.os).should_receive('pipe').and_return(read_descriptor, write_descriptor)
|
||||
flexmock(module.os).should_receive('write').with_args(
|
||||
write_descriptor, b'!include extra.cnf\n[client]\nuser=root\npassword=trustsome1'
|
||||
write_descriptor, b'!include extra.cnf\n[client]\nuser=root\npassword="trustsome1"'
|
||||
).once()
|
||||
flexmock(module.os).should_receive('close')
|
||||
flexmock(module.os).should_receive('set_inheritable')
|
||||
|
||||
@@ -3,6 +3,46 @@ from flexmock import flexmock
|
||||
from borgmatic.hooks.monitoring import pagerduty as module
|
||||
|
||||
|
||||
def mock_logger():
|
||||
logger = flexmock()
|
||||
logger.should_receive('addHandler')
|
||||
logger.should_receive('removeHandler')
|
||||
flexmock(module.logging).should_receive('getLogger').and_return(logger)
|
||||
|
||||
|
||||
def test_initialize_monitor_creates_log_handler():
|
||||
monitoring_log_level = 1
|
||||
|
||||
mock_logger()
|
||||
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
|
||||
'Forgetful_buffering_handler'
|
||||
).once()
|
||||
|
||||
module.initialize_monitor({}, {}, 'test.yaml', monitoring_log_level, dry_run=False)
|
||||
|
||||
|
||||
def test_initialize_monitor_creates_log_handler_when_send_logs_true():
|
||||
mock_logger()
|
||||
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
|
||||
'Forgetful_buffering_handler'
|
||||
).once()
|
||||
|
||||
module.initialize_monitor(
|
||||
{'send_logs': True}, {}, 'test.yaml', monitoring_log_level=1, dry_run=False
|
||||
)
|
||||
|
||||
|
||||
def test_initialize_monitor_bails_when_send_logs_false():
|
||||
mock_logger()
|
||||
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
|
||||
'Forgetful_buffering_handler'
|
||||
).never()
|
||||
|
||||
module.initialize_monitor(
|
||||
{'send_logs': False}, {}, 'test.yaml', monitoring_log_level=1, dry_run=False
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_ignores_start_state():
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
@@ -39,6 +79,9 @@ def test_ping_monitor_calls_api_for_fail_state():
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
|
||||
'format_buffered_logs_for_payload'
|
||||
).and_return('loggy\nlogs')
|
||||
flexmock(module.requests).should_receive('post').and_return(flexmock(ok=True))
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -55,6 +98,9 @@ def test_ping_monitor_dry_run_does_not_call_api():
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
|
||||
'format_buffered_logs_for_payload'
|
||||
).and_return('loggy\nlogs')
|
||||
flexmock(module.requests).should_receive('post').never()
|
||||
|
||||
module.ping_monitor(
|
||||
@@ -71,6 +117,9 @@ def test_ping_monitor_with_connection_error_logs_warning():
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
|
||||
'format_buffered_logs_for_payload'
|
||||
).and_return('loggy\nlogs')
|
||||
flexmock(module.requests).should_receive('post').and_raise(
|
||||
module.requests.exceptions.ConnectionError
|
||||
)
|
||||
@@ -108,6 +157,9 @@ def test_ping_monitor_with_other_error_logs_warning():
|
||||
flexmock(module.borgmatic.hooks.credential.parse).should_receive(
|
||||
'resolve_credential'
|
||||
).replace_with(lambda value, config: value)
|
||||
flexmock(module.borgmatic.hooks.monitoring.logs).should_receive(
|
||||
'format_buffered_logs_for_payload'
|
||||
).and_return('loggy\nlogs')
|
||||
response.should_receive('raise_for_status').and_raise(
|
||||
module.requests.exceptions.RequestException
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user