Compare commits

..
19 Commits
Author SHA1 Message Date
Dan Helfman 730350b31a Fix incorrect option name within schema description. 2025-01-25 08:04:13 -08:00
Dan Helfman 203e1f4e99 Bump version for release. 2025-01-25 08:01:34 -08:00
Dan Helfman 4c35a564ef Fix root patterns so they don't have an invalid "sh:" prefix before getting passed to Borg (#979). 2025-01-25 07:59:53 -08:00
Dan Helfman 7551810ea6 Clarify/correct documentation about dumping databases when using containers (#978). 2025-01-24 14:31:38 -08:00
Dan Helfman ce523eeed6 Add a blurb about recent contributors. 2025-01-23 15:11:54 -08:00
Dan Helfman 3c0def6d6d Expand the recent contributors documentation section to ticket submitters. 2025-01-23 14:41:26 -08:00
Dan Helfman f08014e3be Code formatting. 2025-01-23 12:11:27 -08:00
Dan Helfman 86ad93676d Bump version for release. 2025-01-23 12:09:20 -08:00
Dan Helfman e1825d2bcb Add #977 to NEWS. 2025-01-23 12:08:34 -08:00
Dan Helfman 92b8c0230e Fix exclude patterns parsing to support pattern styles (#977).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/976
2025-01-23 20:06:11 +00:00
Pavel Andreev 73c196aa70 Fix according to review comments 2025-01-23 19:49:10 +00:00
Pavel Andreev 5d390d7953 Fix patterns parsing 2025-01-23 15:58:43 +00:00
Dan Helfman ffb342780b Link to Sentry's DSN documentation (#855). 2025-01-21 17:28:32 -08:00
Dan Helfman 9871267f97 Add a Sentry monitoring hook (#855). 2025-01-21 17:23:56 -08:00
Dan Helfman 914c2b17e9 Add a Sentry monitoring hook (#855). 2025-01-21 17:23:18 -08:00
Dan Helfman 804455ac9f Fix for "exclude_from" files being completely ignored (#971). 2025-01-19 10:27:13 -08:00
Dan Helfman 4fe0fd1576 Fix version number in NEWS. 2025-01-18 09:55:03 -08:00
Dan Helfman e3d40125cb Fix for a "spot" check error when a filename in the most recent archive contains a newline (#968). 2025-01-18 09:54:30 -08:00
Dan Helfman e66df22a6e Fix for an error when a blank line occurs in the configured patterns or excludes (#970). 2025-01-18 09:25:29 -08:00
15 changed files with 381 additions and 42 deletions
+15
View File
@@ -1,3 +1,18 @@
1.9.8
* #979: Fix root patterns so they don't have an invalid "sh:" prefix before getting passed to Borg.
* Expand the recent contributors documentation section to include ticket submitters—not just code
contributors—because there are multiple ways to contribute to the project! See:
https://torsion.org/borgmatic/#recent-contributors
1.9.7
* #855: Add a Sentry monitoring hook. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#sentry-hook
* #968: Fix for a "spot" check error when a filename in the most recent archive contains a newline.
* #970: Fix for an error when there's a blank line in the configured patterns or excludes.
* #971: Fix for "exclude_from" files being completely ignored.
* #977: Fix for "exclude_patterns" and "exclude_from" not supporting explicit pattern styles (e.g.,
"sh:" or "re:").
1.9.6
* #959: Fix an error in the Btrfs hook when a subvolume mounted at "/" is configured in borgmatic's
source directories.
+5
View File
@@ -75,6 +75,7 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
<a href="https://grafana.com/oss/loki/"><img src="docs/static/loki.png" alt="Loki" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://github.com/caronc/apprise/wiki"><img src="docs/static/apprise.png" alt="Apprise" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://www.zabbix.com/"><img src="docs/static/zabbix.png" alt="Zabbix" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://sentry.io/"><img src="docs/static/sentry.png" alt="Sentry" height="40px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://www.borgbase.com/?utm_source=borgmatic"><img src="docs/static/borgbase.png" alt="BorgBase" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
@@ -164,4 +165,8 @@ info on cloning source code, running tests, etc.
### Recent contributors
Thanks to all borgmatic contributors! There are multiple ways to contribute to
this project, so the following includes those who have fixed bugs, contributed
features, *or* filed tickets.
{% include borgmatic/contributors.html %}
+1 -1
View File
@@ -401,7 +401,7 @@ def collect_spot_check_source_paths(
paths = tuple(
path_line.split(' ', 1)[1]
for path_line in paths_output.split('\n')
for path_line in paths_output.splitlines()
if path_line and path_line.startswith('- ') or path_line.startswith('+ ')
)
+14 -12
View File
@@ -15,7 +15,7 @@ import borgmatic.hooks.dispatch
logger = logging.getLogger(__name__)
def parse_pattern(pattern_line):
def parse_pattern(pattern_line, default_style=borgmatic.borg.pattern.Pattern_style.NONE):
'''
Given a Borg pattern as a string, parse it into a borgmatic.borg.pattern.Pattern instance and
return it.
@@ -23,12 +23,13 @@ def parse_pattern(pattern_line):
try:
(pattern_type, remainder) = pattern_line.split(' ', maxsplit=1)
except ValueError:
raise ValueError('Invalid pattern:', pattern_line)
raise ValueError(f'Invalid pattern: {pattern_line}')
try:
(pattern_style, path) = remainder.split(':', maxsplit=1)
(parsed_pattern_style, path) = remainder.split(':', maxsplit=1)
pattern_style = borgmatic.borg.pattern.Pattern_style(parsed_pattern_style)
except ValueError:
pattern_style = ''
pattern_style = default_style
path = remainder
return borgmatic.borg.pattern.Pattern(
@@ -57,11 +58,11 @@ def collect_patterns(config):
parse_pattern(pattern_line.strip())
for pattern_line in config.get('patterns', ())
if not pattern_line.lstrip().startswith('#')
if pattern_line.strip()
)
+ tuple(
borgmatic.borg.pattern.Pattern(
exclude_line.strip(),
borgmatic.borg.pattern.Pattern_type.EXCLUDE,
parse_pattern(
f'{borgmatic.borg.pattern.Pattern_type.EXCLUDE.value} {exclude_line.strip()}',
borgmatic.borg.pattern.Pattern_style.FNMATCH,
)
for exclude_line in config.get('exclude_patterns', ())
@@ -71,22 +72,23 @@ def collect_patterns(config):
for filename in config.get('patterns_from', ())
for pattern_line in open(filename).readlines()
if not pattern_line.lstrip().startswith('#')
if pattern_line.strip()
)
+ tuple(
borgmatic.borg.pattern.Pattern(
exclude_line.strip(),
borgmatic.borg.pattern.Pattern_type.EXCLUDE,
parse_pattern(
f'{borgmatic.borg.pattern.Pattern_type.EXCLUDE.value} {exclude_line.strip()}',
borgmatic.borg.pattern.Pattern_style.FNMATCH,
)
for filename in config.get('excludes_from', ())
for filename in config.get('exclude_from', ())
for exclude_line in open(filename).readlines()
if not exclude_line.lstrip().startswith('#')
if exclude_line.strip()
)
)
except (FileNotFoundError, OSError) as error:
logger.debug(error)
raise ValueError(f'Cannot read patterns_from/excludes_from file: {error.filename}')
raise ValueError(f'Cannot read patterns_from/exclude_from file: {error.filename}')
def expand_directory(directory, working_directory):
+3 -3
View File
@@ -120,7 +120,7 @@ def capture_archive_listing(
paths=[path for path in list_paths] if list_paths else None,
find_paths=None,
json=None,
format=path_format or '{path}{NL}', # noqa: FS003
format=path_format or '{path}{NUL}', # noqa: FS003
),
global_arguments,
local_path,
@@ -132,7 +132,7 @@ def capture_archive_listing(
borg_exit_codes=config.get('borg_exit_codes'),
)
.strip('\n')
.split('\n')
.split('\0')
)
@@ -230,7 +230,7 @@ def list_archive(
borg_exit_codes=borg_exit_codes,
)
.strip('\n')
.split('\n')
.splitlines()
)
else:
archive_lines = (list_arguments.archive,)
+41 -3
View File
@@ -205,8 +205,8 @@ properties:
description: |
Deprecated. Only used for locating database dumps and bootstrap
metadata within backup archives created prior to deprecation.
Replaced by borgmatic_runtime_directory and
borgmatic_state_directory. Defaults to ~/.borgmatic
Replaced by user_runtime_directory and user_state_directory.
Defaults to ~/.borgmatic
example: /tmp/borgmatic
user_runtime_directory:
type: string
@@ -2259,7 +2259,45 @@ properties:
can send the logs to a self-hosted instance or create an account at
https://grafana.com/auth/sign-up/create-user. See borgmatic
monitoring documentation for details.
sentry:
type: object
required: ['data_source_name_url', 'monitor_slug']
additionalProperties: false
properties:
data_source_name_url:
type: string
description: |
Sentry Data Source Name (DSN) URL, associated with a
particular Sentry project. Used to construct a cron URL,
notified when a backup begins, ends, or errors.
example: https://5f80ec@o294220.ingest.us.sentry.io/203069
monitor_slug:
type: string
description: |
Sentry monitor slug, associated with a particular Sentry
project monitor. Used along with the data source name URL to
construct a cron URL.
example: mymonitor
states:
type: array
items:
type: string
enum:
- start
- finish
- fail
uniqueItems: true
description: |
List of one or more monitoring states to ping for: "start",
"finish", and/or "fail". Defaults to pinging for all states.
example:
- start
- finish
description: |
Configuration for a monitoring integration with Sentry. You can use
a self-hosted instance via https://develop.sentry.dev/self-hosted/
or create a cloud-hosted account at https://sentry.io. See borgmatic
monitoring documentation for details.
zfs:
type: ["object", "null"]
additionalProperties: false
-1
View File
@@ -19,7 +19,6 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
Ping the configured Ntfy topic. Use the given configuration filename in any log entries.
If this is a dry run, then don't actually ping anything.
'''
run_states = hook_config.get('states', ['fail'])
if state.name.lower() in run_states:
+79
View File
@@ -0,0 +1,79 @@
import logging
import re
import requests
logger = logging.getLogger(__name__)
def initialize_monitor(
ping_url, config, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
'''
No initialization is necessary for this monitor.
'''
pass
DATA_SOURCE_NAME_URL_PATTERN = re.compile(
'^(?P<protocol>.*)://(?P<username>.*)@(?P<hostname>.*)/(?P<project_id>.*)$'
)
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
'''
Construct and ping a Sentry cron URL, based on the configured DSN URL and monitor slug. Use the
given configuration filename in any log entries. If this is a dry run, then don't actually ping
anything.
'''
run_states = hook_config.get('states', ['start', 'finish', 'fail'])
if not state.name.lower() in run_states:
return
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
data_source_name_url = hook_config.get('data_source_name_url')
monitor_slug = hook_config.get('monitor_slug')
match = DATA_SOURCE_NAME_URL_PATTERN.match(data_source_name_url)
if not match:
logger.warning(
'f{config_filename}: Invalid Sentry data source name URL: {data_source_name_url}'
)
return
cron_url = f'{match.group("protocol")}://{match.group("hostname")}/api/{match.group("project_id")}/cron/{monitor_slug}/{match.group("username")}/'
logger.info(f'{config_filename}: Pinging Sentry {state.name.lower()}{dry_run_label}')
logger.debug(f'{config_filename}: Using Sentry cron URL {cron_url}')
status = {
'start': 'in_progress',
'finish': 'ok',
'fail': 'error',
}.get(state.name.lower())
if not status:
logger.warning('f{config_filename}: Invalid Sentry state')
return
if dry_run:
return
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
response = requests.post(f'{cron_url}?status={status}')
if not response.ok:
response.raise_for_status()
except requests.exceptions.RequestException as error:
logger.warning(f'{config_filename}: Sentry error: {error}')
def destroy_monitor(
ping_url_or_uuid, config, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
'''
No destruction is necessary for this monitor.
'''
pass
+23 -7
View File
@@ -25,27 +25,43 @@ def list_merged_pulls(url):
return tuple(pull for pull in response.json() if pull.get('merged_at'))
API_ENDPOINT_URLS = (
def list_contributing_issues(url):
# labels = bug, design finalized, etc.
response = requests.get(f'{url}?labels=19,20,22,23,32,52,53,54', headers={'Accept': 'application/json', 'Content-Type': 'application/json'})
if not response.ok:
response.raise_for_status()
return tuple(response.json())
PULLS_API_ENDPOINT_URLS = (
'https://projects.torsion.org/api/v1/repos/borgmatic-collective/borgmatic/pulls',
'https://api.github.com/repos/borgmatic-collective/borgmatic/pulls',
)
ISSUES_API_ENDPOINT_URL = 'https://projects.torsion.org/api/v1/repos/borgmatic-collective/borgmatic/issues'
RECENT_CONTRIBUTORS_CUTOFF_DAYS = 365
def get_item_timestamp(item):
return item.get('merged_at') or item.get('created_at')
def print_contributors():
'''
Display the recent contributors as a row of avatars in an HTML fragment.
'''
pulls = tuple(itertools.chain.from_iterable(list_merged_pulls(url) for url in API_ENDPOINT_URLS))
pulls = tuple(itertools.chain.from_iterable(list_merged_pulls(url) for url in PULLS_API_ENDPOINT_URLS))
issues = list_contributing_issues(ISSUES_API_ENDPOINT_URL)
seen_user_ids = set()
print('<p>')
for pull in sorted(pulls, key=operator.itemgetter('merged_at'), reverse=True):
merged_at = pull.get('merged_at')
user = pull.get('user')
for item in sorted(pulls + issues, key=get_item_timestamp, reverse=True):
timestamp = get_item_timestamp(item)
user = item.get('user')
if not merged_at or not user:
if not timestamp or not user:
continue
user_id = user.get('id')
@@ -53,7 +69,7 @@ def print_contributors():
if not user_id or user_id in seen_user_ids:
continue
if datetime.datetime.fromisoformat(merged_at) < datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=RECENT_CONTRIBUTORS_CUTOFF_DAYS):
if datetime.datetime.fromisoformat(timestamp) < datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=RECENT_CONTRIBUTORS_CUTOFF_DAYS):
continue
seen_user_ids.add(user_id)
+9 -5
View File
@@ -135,6 +135,9 @@ temporary file storage, probing the following locations (in order) to find it:
Hard-coded `/tmp`. <span class="minilink minilink-addedin">Prior to
version 1.9.2</span>This was instead hard-coded to `/run/user/$UID`.
You can see the runtime directory path that borgmatic selects by running with
`--verbosity 2` and looking for "Using runtime directory" in the output.
Regardless of the runtime directory selected, borgmatic stores its files
within a `borgmatic` subdirectory of the runtime directory. Additionally, in
the case of `TMPDIR`, `TEMP`, and the hard-coded `/tmp`, borgmatic creates a
@@ -260,17 +263,18 @@ hooks:
example, you'd also need to set the `pg_restore_command` and `psql_command`
options. If you choose to use the `pg_dump` command within the container
though, note that it will output the database dump to a file inside the
container. So you'll have to mount the `.borgmatic` folder from your host's
home folder into the container using the same directory structure.
container. So you'll have to mount the [runtime directory](#runtime-directory)
from your host into the container using the same directory structure.
See the following Docker compose file an as example:
For example, with Docker Compose and a runtime directory located at
`/run/user/1000`:
```yaml
services:
db:
image: postgres
volumes:
- /home/USERNAME/.borgmatic:/home/USERNAME/.borgmatic
- /run/user/1000:/run/user/1000
```
Similar command override options are available for (some of) the other
@@ -547,7 +551,7 @@ extraction destination path. For example, if you're extracting to `/tmp`, then
the dump will be in `/tmp/borgmatic/`.
<span class="minilink minilink-addedin">Prior to version 1.9.0</span> borgmatic
extracts the dump file into the *`username`*`/.borgmatic/` directory within the
extracted the dump file into the *`username`*`/.borgmatic/` directory within the
extraction destination path, where *`username`* is the user that created the
backup. For example, if you created the backup with the `root` user and you're
extracting to `/tmp`, then the dump will be in `/tmp/root/.borgmatic`.
+41 -1
View File
@@ -47,6 +47,7 @@ them as backups happen:
* [ntfy](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#ntfy-hook)
* [PagerDuty](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook)
* [Pushover](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pushover-hook)
* [Sentry](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#sentry-hook)
* [Uptime Kuma](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#uptime-kuma-hook)
* [Zabbix](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#zabbix-hook)
@@ -361,10 +362,49 @@ pushover:
```
## Sentry hook
<span class="minilink minilink-addedin">New in version 1.9.7</span>
[Sentry](https://sentry.io/) is an application monitoring service that
includes cron-style monitoring (either cloud-hosted or
[self-hosted](https://develop.sentry.dev/self-hosted/)).
To get started, create a [Sentry cron
monitor](https://docs.sentry.io/product/crons/) in the Sentry UI. Under
"Instrument your monitor," select "Sentry CLI" and copy the URL value for the
displayed
[`SENTRY_DSN`](https://docs.sentry.io/concepts/key-terms/dsn-explainer/)
environment variable into borgmatic's Sentry `data_source_name_url`
configuration option. For example:
```
sentry:
data_source_name_url: https://5f80ec@o294220.ingest.us.sentry.io/203069
monitor_slug: mymonitor
```
The `monitor_slug` value comes from the "Monitor Slug" under "Cron Details" on
the same Sentry monitor page.
With this configuration, borgmatic pings Sentry whenever borgmatic starts,
finishes, or fails, but only when any of the `create`, `prune`, `compact`, or
`check` actions are run. You can optionally override the start/finish/fail
behavior with the `states` configuration option. For instance, to only ping
Sentry on failure:
```
sentry:
data_source_name_url: https://5f80ec@o294220.ingest.us.sentry.io/203069
monitor_slug: mymonitor
states:
- fail
```
## ntfy hook
<span class="minilink minilink-addedin">New in version 1.6.3</span>
[ntfy](https://ntfy.sh) is a free, simple, service (either hosted or
[ntfy](https://ntfy.sh) is a free, simple, service (either cloud-hosted or
self-hosted) which offers simple pub/sub push notifications to multiple
platforms including [web](https://ntfy.sh/stats),
[Android](https://play.google.com/store/apps/details?id=io.heckel.ntfy) and
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "borgmatic"
version = "1.9.6"
version = "1.9.8"
authors = [
{ name="Dan Helfman", email="witten@torsion.org" },
]
+25 -8
View File
@@ -36,18 +36,21 @@ def test_collect_patterns_converts_source_directories():
def test_collect_patterns_parses_config_patterns():
flexmock(module).should_receive('parse_pattern').with_args('R /foo').and_return(Pattern('/foo'))
flexmock(module).should_receive('parse_pattern').with_args('# comment').never()
flexmock(module).should_receive('parse_pattern').with_args('').never()
flexmock(module).should_receive('parse_pattern').with_args(' ').never()
flexmock(module).should_receive('parse_pattern').with_args('R /bar').and_return(Pattern('/bar'))
assert module.collect_patterns({'patterns': ['R /foo', '# comment', 'R /bar']}) == (
assert module.collect_patterns({'patterns': ['R /foo', '# comment', '', ' ', 'R /bar']}) == (
Pattern('/foo'),
Pattern('/bar'),
)
def test_collect_patterns_converts_exclude_patterns():
assert module.collect_patterns({'exclude_patterns': ['/foo', '/bar']}) == (
assert module.collect_patterns({'exclude_patterns': ['/foo', '/bar', 'sh:**/baz']}) == (
Pattern('/foo', Pattern_type.EXCLUDE, Pattern_style.FNMATCH),
Pattern('/bar', Pattern_type.EXCLUDE, Pattern_style.FNMATCH),
Pattern('**/baz', Pattern_type.EXCLUDE, Pattern_style.SHELL),
)
@@ -55,10 +58,12 @@ def test_collect_patterns_reads_config_patterns_from_file():
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('file1.txt').and_return(io.StringIO('R /foo'))
builtins.should_receive('open').with_args('file2.txt').and_return(
io.StringIO('R /bar\n# comment\nR /baz')
io.StringIO('R /bar\n# comment\n\n \nR /baz')
)
flexmock(module).should_receive('parse_pattern').with_args('R /foo').and_return(Pattern('/foo'))
flexmock(module).should_receive('parse_pattern').with_args('# comment').never()
flexmock(module).should_receive('parse_pattern').with_args('').never()
flexmock(module).should_receive('parse_pattern').with_args(' ').never()
flexmock(module).should_receive('parse_pattern').with_args('R /bar').and_return(Pattern('/bar'))
flexmock(module).should_receive('parse_pattern').with_args('R /baz').and_return(Pattern('/baz'))
@@ -78,27 +83,39 @@ def test_collect_patterns_errors_on_missing_config_patterns_from_file():
module.collect_patterns({'patterns_from': ['file1.txt', 'file2.txt']})
def test_collect_patterns_reads_config_excludes_from_file():
def test_collect_patterns_reads_config_exclude_from_file():
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('file1.txt').and_return(io.StringIO('/foo'))
builtins.should_receive('open').with_args('file2.txt').and_return(
io.StringIO('/bar\n# comment\n/baz')
io.StringIO('/bar\n# comment\n\n \n/baz')
)
flexmock(module).should_receive('parse_pattern').with_args(
'- /foo', default_style=Pattern_style.FNMATCH
).and_return(Pattern('/foo', Pattern_type.EXCLUDE, Pattern_style.FNMATCH))
flexmock(module).should_receive('parse_pattern').with_args(
'- /bar', default_style=Pattern_style.FNMATCH
).and_return(Pattern('/bar', Pattern_type.EXCLUDE, Pattern_style.FNMATCH))
flexmock(module).should_receive('parse_pattern').with_args('# comment').never()
flexmock(module).should_receive('parse_pattern').with_args('').never()
flexmock(module).should_receive('parse_pattern').with_args(' ').never()
flexmock(module).should_receive('parse_pattern').with_args(
'- /baz', default_style=Pattern_style.FNMATCH
).and_return(Pattern('/baz', Pattern_type.EXCLUDE, Pattern_style.FNMATCH))
assert module.collect_patterns({'excludes_from': ['file1.txt', 'file2.txt']}) == (
assert module.collect_patterns({'exclude_from': ['file1.txt', 'file2.txt']}) == (
Pattern('/foo', Pattern_type.EXCLUDE, Pattern_style.FNMATCH),
Pattern('/bar', Pattern_type.EXCLUDE, Pattern_style.FNMATCH),
Pattern('/baz', Pattern_type.EXCLUDE, Pattern_style.FNMATCH),
)
def test_collect_patterns_errors_on_missing_config_excludes_from_file():
def test_collect_patterns_errors_on_missing_config_exclude_from_file():
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('file1.txt').and_raise(OSError)
flexmock(module).should_receive('parse_pattern').never()
with pytest.raises(ValueError):
module.collect_patterns({'excludes_from': ['file1.txt', 'file2.txt']})
module.collect_patterns({'exclude_from': ['file1.txt', 'file2.txt']})
def test_expand_directory_with_basic_path_passes_it_through():
+124
View File
@@ -0,0 +1,124 @@
import pytest
from flexmock import flexmock
import borgmatic.hooks.monitoring.monitor
from borgmatic.hooks.monitoring import sentry as module
@pytest.mark.parametrize(
'state,configured_states,expected_status',
(
(borgmatic.hooks.monitoring.monitor.State.START, ['start'], 'in_progress'),
(
borgmatic.hooks.monitoring.monitor.State.START,
['start', 'finish', 'fail'],
'in_progress',
),
(borgmatic.hooks.monitoring.monitor.State.START, None, 'in_progress'),
(borgmatic.hooks.monitoring.monitor.State.FINISH, ['finish'], 'ok'),
(borgmatic.hooks.monitoring.monitor.State.FAIL, ['fail'], 'error'),
),
)
def test_ping_monitor_constructs_cron_url_and_pings_it(state, configured_states, expected_status):
hook_config = {
'data_source_name_url': 'https://5f80ec@o294220.ingest.us.sentry.io/203069',
'monitor_slug': 'test',
}
if configured_states:
hook_config['states'] = configured_states
flexmock(module.requests).should_receive('get').with_args(
f'https://o294220.ingest.us.sentry.io/api/203069/cron/test/5f80ec/?status={expected_status}'
).and_return(flexmock(ok=True))
module.ping_monitor(
hook_config,
{},
'config.yaml',
state,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_with_unconfigured_state_bails():
hook_config = {
'data_source_name_url': 'https://5f80ec@o294220.ingest.us.sentry.io/203069',
'monitor_slug': 'test',
'states': ['fail'],
}
flexmock(module.requests).should_receive('get').never()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitoring.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
@pytest.mark.parametrize(
'data_source_name_url',
(
'5f80ec@o294220.ingest.us.sentry.io/203069',
'https://o294220.ingest.us.sentry.io/203069',
'https://5f80ec@/203069',
'https://5f80ec@o294220.ingest.us.sentry.io',
'https://5f80ec@o294220.ingest.us.sentry.io/203069/',
),
)
def test_ping_monitor_with_invalid_data_source_name_url_bails(data_source_name_url):
hook_config = {
'data_source_name_url': data_source_name_url,
'monitor_slug': 'test',
}
flexmock(module.requests).should_receive('get').never()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitoring.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_with_invalid_sentry_state_bails():
hook_config = {
'data_source_name_url': 'https://5f80ec@o294220.ingest.us.sentry.io/203069',
'monitor_slug': 'test',
# This should never actually happen in practice, because the config schema prevents it.
'states': ['log'],
}
flexmock(module.requests).should_receive('get').never()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitoring.monitor.State.LOG,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_with_dry_run_bails():
hook_config = {
'data_source_name_url': 'https://5f80ec@o294220.ingest.us.sentry.io/203069',
'monitor_slug': 'test',
}
flexmock(module.requests).should_receive('get').never()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitoring.monitor.State.START,
monitoring_log_level=1,
dry_run=True,
)