Compare commits

..
43 Commits
Author SHA1 Message Date
Dan Helfman 20a3995977 Bump version for release. 2024-07-04 08:54:09 -07:00
Dan Helfman 66aa953371 Add additional tests (#298). 2024-07-04 08:42:05 -07:00
Dan Helfman ba053de8f7 Add delete and rdelete actions (#298).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/893
2024-07-04 06:07:30 +00:00
Dan Helfman 2f844d65d5 Automated tests complete (#298). 2024-07-02 23:11:58 -07:00
Dan Helfman 2dca5e1834 Initial automated tests for delete action (#298). 2024-07-02 20:26:29 -07:00
Dan Helfman 36197ce027 Dry run support for delete/rdelete actions (#298). 2024-06-28 20:56:42 -07:00
Dan Helfman e9a0226ee0 Initial work on delete/rdelete actions (#298). 2024-06-28 16:20:10 -07:00
Dan Helfman fc3b4a653e Add a development documentation note about using fully qualified imports. 2024-06-28 15:00:25 -07:00
Dan Helfman 3673abb01e Update documentation links that link to beta versions of Borg docs. 2024-06-27 14:42:53 -07:00
Dan Helfman ac4277d36c Add "new in version" label to Uptime Kuma documentation (#885). 2024-06-27 09:29:53 -07:00
Dan Helfman 21cbc99d9e Fix uptime_kuma monitor not being called after name change (#891).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/891
2024-06-27 16:27:07 +00:00
Paul Wilde d080bf2ae9 [bugfix] uptime_kuma monitor is not called after name change 2024-06-27 12:49:07 +01:00
Dan Helfman 2a1c790655 Minor reorganization of borgmatic source code reference. 2024-06-26 19:39:32 -07:00
Dan Helfman 410204a70d Formatting, whitespace, and minor fixes for Uptime Kuma hook (#885). 2024-06-26 16:09:14 -07:00
Dan Helfman 4a0c167c1c Add Uptime Kuma monitoring hook (#885).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/885
Reviewed-by: Dan Helfman <witten@torsion.org>
2024-06-26 22:50:11 +00:00
Dan Helfman 593c956d33 Add an "only_run_on" option to consistency checks so you can limit a check to running on particular days of the week (#785). 2024-06-26 14:57:59 -07:00
pswilde d18cb89493 Merge branch 'main' into main 2024-06-26 19:58:56 +00:00
Paul Wilde 067c79c606 renamed push_monitor back to ping_monitor in uptime kuma hook 2024-06-26 20:57:37 +01:00
Dan Helfman ebde88ccaa Fix the Healthchecks ping body size limit, restoring it to the documented 100,000 bytes (#889). 2024-06-25 12:45:44 -07:00
Dan Helfman cc402487d9 Minor development documentation clarifications. 2024-06-24 10:48:13 -07:00
Paul Wilde d108e6102b some minor corrections in how to uptime kuma docs 2024-06-24 12:00:22 +01:00
Paul Wilde 3e60043632 some minor corrections in how to uptime kuma docs 2024-06-24 11:57:12 +01:00
Paul Wilde a8d691169a some minor corrections in how to uptime kuma docs 2024-06-24 11:54:34 +01:00
Paul Wilde 939c2f6718 some minor corrections in how to uptime kuma docs 2024-06-24 11:54:12 +01:00
Paul Wilde 0837059e21 some minor corrections in how to uptime kuma docs 2024-06-24 11:51:41 +01:00
Paul Wilde 0ee166fdf0 added Uptime Kuma how-to docs 2024-06-24 11:46:38 +01:00
Paul Wilde b50996b864 change uptimekuma method names to 'push_*' instead of 'ping' 2024-06-24 11:07:09 +01:00
Paul Wilde 8f423c7293 black formatting on uptimekuma hook 2024-06-24 10:46:04 +01:00
Paul Wilde 14ce88e04b black formatting on test_uptimekuma.py 2024-06-24 10:37:44 +01:00
Paul Wilde f97968b72d variable renaming 2024-06-24 10:34:52 +01:00
Paul Wilde 612f867ea8 Merge remote-tracking branch 'upstream/main' 2024-06-24 10:28:02 +01:00
Paul Wilde 303d6609e4 removed unecessary tests for 'default urls' as these would never really be used anyway 2024-06-24 10:27:23 +01:00
Paul Wilde bf7b163ccd added early out for dry run 2024-06-24 10:25:08 +01:00
Paul Wilde 4bd798f0ad alpha ordered dispatch monitor hook names (including loki) 2024-06-24 10:22:27 +01:00
Paul Wilde 52aa7c5d21 switched to using full 'push_url' instead of separate 'server' and 'push_code' 2024-06-24 10:21:31 +01:00
Dan Helfman f5a1dd31c8 Fix PagerDuty hook traceback with Python < 3.10 (#886). 2024-06-23 18:28:41 -07:00
Paul Wilde a509cdedd5 Added Uptime Kuma tests 2024-06-22 10:46:17 +01:00
Paul Wilde dcbc30b164 WIP add uptime kuma tests 2024-06-22 10:19:34 +01:00
Paul Wilde 5ab99b4cc0 Added Uptime Kuma image to readme 2024-06-21 21:21:04 +01:00
Paul Wilde 27c90b7cf1 Added Uptime Kuma hook 2024-06-21 21:10:14 +01:00
Paul Wilde 6eb76454bb WIP added some schema info for uptime kuma 2024-06-21 17:00:44 +01:00
Paul Wilde 83bcea98dc WIP added some schema info for uptime kuma 2024-06-21 16:57:20 +01:00
Paul Wilde 4db09a73b3 WIP implement Uptime Kuma hook 2024-06-21 16:47:51 +01:00
37 changed files with 1855 additions and 50 deletions
+10
View File
@@ -1,3 +1,13 @@
1.8.13
* #298: Add "delete" and "rdelete" actions to delete archives or entire repositories.
* #785: Add an "only_run_on" option to consistency checks so you can limit a check to running on
particular days of the week. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/#check-days
* #885: Add an Uptime Kuma monitoring hook. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#uptime-kuma-hook
* #886: Fix a PagerDuty hook traceback with Python < 3.10.
* #889: Fix the Healthchecks ping body size limit, restoring it to the documented 100,000 bytes.
1.8.12
* #817: Add a "--max-duration" flag to the "check" action and a "max_duration" option to the
repository check configuration. This tells Borg to interrupt a repository check after a certain
+1
View File
@@ -62,6 +62,7 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
<a href="https://www.mongodb.com/"><img src="docs/static/mongodb.png" alt="MongoDB" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://sqlite.org/"><img src="docs/static/sqlite.png" alt="SQLite" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://healthchecks.io/"><img src="docs/static/healthchecks.png" alt="Healthchecks" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://uptime.kuma.pet/"><img src="docs/static/uptimekuma.png" alt="Uptime Kuma" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://cronitor.io/"><img src="docs/static/cronitor.png" alt="Cronitor" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://cronhub.io/"><img src="docs/static/cronhub.png" alt="Cronhub" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://www.pagerduty.com/"><img src="docs/static/pagerduty.png" alt="PagerDuty" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
+31 -2
View File
@@ -1,3 +1,4 @@
import calendar
import datetime
import hashlib
import itertools
@@ -99,12 +100,17 @@ def parse_frequency(frequency):
raise ValueError(f"Could not parse consistency check frequency '{frequency}'")
WEEKDAY_DAYS = calendar.day_name[0:5]
WEEKEND_DAYS = calendar.day_name[5:7]
def filter_checks_on_frequency(
config,
borg_repository_id,
checks,
force,
archives_check_id=None,
datetime_now=datetime.datetime.now,
):
'''
Given a configuration dict with a "checks" sequence of dicts, a Borg repository ID, a sequence
@@ -143,6 +149,29 @@ def filter_checks_on_frequency(
if checks and check not in checks:
continue
only_run_on = check_config.get('only_run_on')
if only_run_on:
# Use a dict instead of a set to preserve ordering.
days = dict.fromkeys(only_run_on)
if 'weekday' in days:
days = {
**dict.fromkeys(day for day in days if day != 'weekday'),
**dict.fromkeys(WEEKDAY_DAYS),
}
if 'weekend' in days:
days = {
**dict.fromkeys(day for day in days if day != 'weekend'),
**dict.fromkeys(WEEKEND_DAYS),
}
if calendar.day_name[datetime_now().weekday()] not in days:
logger.info(
f"Skipping {check} check due to day of the week; check only runs on {'/'.join(days)} (use --force to check anyway)"
)
filtered_checks.remove(check)
continue
frequency_delta = parse_frequency(check_config.get('frequency'))
if not frequency_delta:
continue
@@ -153,8 +182,8 @@ def filter_checks_on_frequency(
# If we've not yet reached the time when the frequency dictates we're ready for another
# check, skip this check.
if datetime.datetime.now() < check_time + frequency_delta:
remaining = check_time + frequency_delta - datetime.datetime.now()
if datetime_now() < check_time + frequency_delta:
remaining = check_time + frequency_delta - datetime_now()
logger.info(
f'Skipping {check} check due to configured frequency; {remaining} until next check (use --force to check anyway)'
)
+50
View File
@@ -0,0 +1,50 @@
import logging
import borgmatic.actions.arguments
import borgmatic.borg.delete
import borgmatic.borg.rdelete
import borgmatic.borg.rlist
logger = logging.getLogger(__name__)
def run_delete(
repository,
config,
local_borg_version,
delete_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "delete" action for the given repository and archive(s).
'''
if delete_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, delete_arguments.repository
):
logger.answer(f'{repository.get("label", repository["path"])}: Deleting archives')
archive_name = (
borgmatic.borg.rlist.resolve_archive_name(
repository['path'],
delete_arguments.archive,
config,
local_borg_version,
global_arguments,
local_path,
remote_path,
)
if delete_arguments.archive
else None
)
borgmatic.borg.delete.delete_archives(
repository,
config,
local_borg_version,
borgmatic.actions.arguments.update_arguments(delete_arguments, archive=archive_name),
global_arguments,
local_path,
remote_path,
)
+36
View File
@@ -0,0 +1,36 @@
import logging
import borgmatic.borg.rdelete
logger = logging.getLogger(__name__)
def run_rdelete(
repository,
config,
local_borg_version,
rdelete_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Run the "rdelete" action for the given repository.
'''
if rdelete_arguments.repository is None or borgmatic.config.validate.repositories_match(
repository, rdelete_arguments.repository
):
logger.answer(
f'{repository.get("label", repository["path"])}: Deleting repository'
+ (' cache' if rdelete_arguments.cache_only else '')
)
borgmatic.borg.rdelete.delete_repository(
repository,
config,
local_borg_version,
rdelete_arguments,
global_arguments,
local_path,
remote_path,
)
+132
View File
@@ -0,0 +1,132 @@
import argparse
import logging
import borgmatic.borg.environment
import borgmatic.borg.feature
import borgmatic.borg.flags
import borgmatic.borg.rdelete
import borgmatic.execute
logger = logging.getLogger(__name__)
def make_delete_command(
repository,
config,
local_borg_version,
delete_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Given a local or remote repository dict, a configuration dict, the local Borg version, the
arguments to the delete action as an argparse.Namespace, and global arguments, return a command
as a tuple to delete archives from the repository.
'''
return (
(local_path, 'delete')
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ borgmatic.borg.flags.make_flags('dry-run', global_arguments.dry_run)
+ borgmatic.borg.flags.make_flags('remote-path', remote_path)
+ borgmatic.borg.flags.make_flags('log-json', global_arguments.log_json)
+ borgmatic.borg.flags.make_flags('lock-wait', config.get('lock_wait'))
+ borgmatic.borg.flags.make_flags('list', delete_arguments.list_archives)
+ (
(('--force',) + (('--force',) if delete_arguments.force >= 2 else ()))
if delete_arguments.force
else ()
)
# Ignore match_archives and archive_name_format options from configuration, so the user has
# to be explicit on the command-line about the archives they want to delete.
+ borgmatic.borg.flags.make_match_archives_flags(
delete_arguments.match_archives or delete_arguments.archive,
archive_name_format=None,
local_borg_version=local_borg_version,
default_archive_name_format='*',
)
+ borgmatic.borg.flags.make_flags_from_arguments(
delete_arguments,
excludes=('list_archives', 'force', 'match_archives', 'archive', 'repository'),
)
+ borgmatic.borg.flags.make_repository_flags(repository['path'], local_borg_version)
)
ARCHIVE_RELATED_ARGUMENT_NAMES = (
'archive',
'match_archives',
'first',
'last',
'oldest',
'newest',
'older',
'newer',
)
def delete_archives(
repository,
config,
local_borg_version,
delete_arguments,
global_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository dict, a configuration dict, the local Borg version, the
arguments to the delete action as an argparse.Namespace, global arguments as an
argparse.Namespace, and local and remote Borg paths, delete the selected archives from the
repository. If no archives are selected, then delete the entire repository.
'''
borgmatic.logger.add_custom_log_levels()
if not any(
getattr(delete_arguments, argument_name, None)
for argument_name in ARCHIVE_RELATED_ARGUMENT_NAMES
):
if borgmatic.borg.feature.available(
borgmatic.borg.feature.Feature.RDELETE, local_borg_version
):
logger.warning(
'Deleting an entire repository with the delete action is deprecated when using Borg 2.x+. Use the rdelete action instead.'
)
rdelete_arguments = argparse.Namespace(
repository=repository['path'],
list_archives=delete_arguments.list_archives,
force=delete_arguments.force,
cache_only=delete_arguments.cache_only,
keep_security_info=delete_arguments.keep_security_info,
)
borgmatic.borg.rdelete.delete_repository(
repository,
config,
local_borg_version,
rdelete_arguments,
global_arguments,
local_path,
remote_path,
)
return
command = make_delete_command(
repository,
config,
local_borg_version,
delete_arguments,
global_arguments,
local_path,
remote_path,
)
borgmatic.execute.execute_command(
command,
output_log_level=logging.ANSWER,
extra_environment=borgmatic.borg.environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
+4 -2
View File
@@ -13,8 +13,9 @@ class Feature(Enum):
RCREATE = 7
RLIST = 8
RINFO = 9
MATCH_ARCHIVES = 10
EXCLUDED_FILES_MINUS = 11
RDELETE = 10
MATCH_ARCHIVES = 11
EXCLUDED_FILES_MINUS = 12
FEATURE_TO_MINIMUM_BORG_VERSION = {
@@ -27,6 +28,7 @@ FEATURE_TO_MINIMUM_BORG_VERSION = {
Feature.RCREATE: parse('2.0.0a2'), # borg rcreate
Feature.RLIST: parse('2.0.0a2'), # borg rlist
Feature.RINFO: parse('2.0.0a2'), # borg rinfo
Feature.RDELETE: parse('2.0.0a2'), # borg rdelete
Feature.MATCH_ARCHIVES: parse('2.0.0b3'), # borg --match-archives
Feature.EXCLUDED_FILES_MINUS: parse('2.0.0b5'), # --list --filter uses "-" for excludes
}
+7 -2
View File
@@ -66,7 +66,12 @@ def make_repository_archive_flags(repository_path, archive, local_borg_version):
DEFAULT_ARCHIVE_NAME_FORMAT = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}' # noqa: FS003
def make_match_archives_flags(match_archives, archive_name_format, local_borg_version):
def make_match_archives_flags(
match_archives,
archive_name_format,
local_borg_version,
default_archive_name_format=DEFAULT_ARCHIVE_NAME_FORMAT,
):
'''
Return match archives flags based on the given match archives value, if any. If it isn't set,
return match archives flags to match archives created with the given (or default) archive name
@@ -83,7 +88,7 @@ def make_match_archives_flags(match_archives, archive_name_format, local_borg_ve
return ('--glob-archives', re.sub(r'^sh:', '', match_archives))
derived_match_archives = re.sub(
r'\{(now|utcnow|pid)([:%\w\.-]*)\}', '*', archive_name_format or DEFAULT_ARCHIVE_NAME_FORMAT
r'\{(now|utcnow|pid)([:%\w\.-]*)\}', '*', archive_name_format or default_archive_name_format
)
if derived_match_archives == '*':
+6 -6
View File
@@ -144,12 +144,12 @@ def list_archive(
remote_path=None,
):
'''
Given a local or remote repository path, a configuration dict, the local Borg version, global
arguments as an argparse.Namespace, the arguments to the list action as an argparse.Namespace,
and local and remote Borg paths, display the output of listing the files of a Borg archive (or
return JSON output). If list_arguments.find_paths are given, list the files by searching across
multiple archives. If neither find_paths nor archive name are given, instead list the archives
in the given repository.
Given a local or remote repository path, a configuration dict, the local Borg version, the
arguments to the list action as an argparse.Namespace, global arguments as an
argparse.Namespace, and local and remote Borg paths, display the output of listing the files of
a Borg archive (or return JSON output). If list_arguments.find_paths are given, list the files
by searching across multiple archives. If neither find_paths nor archive name are given, instead
list the archives in the given repository.
'''
borgmatic.logger.add_custom_log_levels()
+92
View File
@@ -0,0 +1,92 @@
import logging
import borgmatic.borg.environment
import borgmatic.borg.feature
import borgmatic.borg.flags
import borgmatic.execute
logger = logging.getLogger(__name__)
def make_rdelete_command(
repository,
config,
local_borg_version,
rdelete_arguments,
global_arguments,
local_path,
remote_path,
):
'''
Given a local or remote repository dict, a configuration dict, the local Borg version, the
arguments to the rdelete action as an argparse.Namespace, and global arguments, return a command
as a tuple to rdelete the entire repository.
'''
return (
(local_path,)
+ (
('rdelete',)
if borgmatic.borg.feature.available(
borgmatic.borg.feature.Feature.RDELETE, local_borg_version
)
else ('delete',)
)
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ borgmatic.borg.flags.make_flags('dry-run', global_arguments.dry_run)
+ borgmatic.borg.flags.make_flags('remote-path', remote_path)
+ borgmatic.borg.flags.make_flags('log-json', global_arguments.log_json)
+ borgmatic.borg.flags.make_flags('lock-wait', config.get('lock_wait'))
+ borgmatic.borg.flags.make_flags('list', rdelete_arguments.list_archives)
+ (
(('--force',) + (('--force',) if rdelete_arguments.force >= 2 else ()))
if rdelete_arguments.force
else ()
)
+ borgmatic.borg.flags.make_flags_from_arguments(
rdelete_arguments, excludes=('list_archives', 'force', 'repository')
)
+ borgmatic.borg.flags.make_repository_flags(repository['path'], local_borg_version)
)
def delete_repository(
repository,
config,
local_borg_version,
rdelete_arguments,
global_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository dict, a configuration dict, the local Borg version, the
arguments to the rdelete action as an argparse.Namespace, global arguments as an
argparse.Namespace, and local and remote Borg paths, rdelete the entire repository.
'''
borgmatic.logger.add_custom_log_levels()
command = make_rdelete_command(
repository,
config,
local_borg_version,
rdelete_arguments,
global_arguments,
local_path,
remote_path,
)
borgmatic.execute.execute_command(
command,
output_log_level=logging.ANSWER,
# Don't capture output when Borg is expected to prompt for interactive confirmation, or the
# prompt won't work.
output_file=(
None
if rdelete_arguments.force or rdelete_arguments.cache_only
else borgmatic.execute.DO_NOT_CAPTURE
),
extra_environment=borgmatic.borg.environment.make_environment(config),
borg_local_path=local_path,
borg_exit_codes=config.get('borg_exit_codes'),
)
+131 -1
View File
@@ -12,11 +12,13 @@ ACTION_ALIASES = {
'create': ['-C'],
'check': ['-k'],
'config': [],
'delete': [],
'extract': ['-x'],
'export-tar': [],
'mount': ['-m'],
'umount': ['-u'],
'restore': ['-r'],
'rdelete': [],
'rlist': [],
'list': ['-l'],
'rinfo': [],
@@ -538,7 +540,7 @@ def make_parsers():
dest='stats',
default=False,
action='store_true',
help='Display statistics of archive',
help='Display statistics of the pruned archive',
)
prune_group.add_argument(
'--list', dest='list_archives', action='store_true', help='List archives kept/pruned'
@@ -689,6 +691,97 @@ def make_parsers():
)
check_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
delete_parser = action_parsers.add_parser(
'delete',
aliases=ACTION_ALIASES['delete'],
help='Delete an archive from a repository or delete an entire repository (with Borg 1.2+, you must run compact afterwards to actually free space)',
description='Delete an archive from a repository or delete an entire repository (with Borg 1.2+, you must run compact afterwards to actually free space)',
add_help=False,
)
delete_group = delete_parser.add_argument_group('delete arguments')
delete_group.add_argument(
'--repository',
help='Path of repository to delete or delete archives from, defaults to the configured repository if there is only one',
)
delete_group.add_argument(
'--archive',
help='Archive to delete',
)
delete_group.add_argument(
'--list',
dest='list_archives',
action='store_true',
help='Show details for the deleted archives',
)
delete_group.add_argument(
'--stats',
action='store_true',
help='Display statistics for the deleted archives',
)
delete_group.add_argument(
'--cache-only',
action='store_true',
help='Delete only the local cache for the given repository',
)
delete_group.add_argument(
'--force',
action='count',
help='Force deletion of corrupted archives, can be given twice if once does not work',
)
delete_group.add_argument(
'--keep-security-info',
action='store_true',
help='Do not delete the local security info when deleting a repository',
)
delete_group.add_argument(
'--save-space',
action='store_true',
help='Work slower, but using less space [Not supported in Borg 2.x+]',
)
delete_group.add_argument(
'--checkpoint-interval',
type=int,
metavar='SECONDS',
help='Write a checkpoint at the given interval, defaults to 1800 seconds (30 minutes)',
)
delete_group.add_argument(
'-a',
'--match-archives',
'--glob-archives',
metavar='PATTERN',
help='Only delete archives matching this pattern',
)
delete_group.add_argument(
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
)
delete_group.add_argument(
'--first', metavar='N', help='Delete first N archives after other filters are applied'
)
delete_group.add_argument(
'--last', metavar='N', help='Delete last N archives after other filters are applied'
)
delete_group.add_argument(
'--oldest',
metavar='TIMESPAN',
help='Delete archives within a specified time range starting from the timestamp of the oldest archive (e.g. 7d or 12m) [Borg 2.x+ only]',
)
delete_group.add_argument(
'--newest',
metavar='TIMESPAN',
help='Delete archives within a time range that ends at timestamp of the newest archive and starts a specified time range ago (e.g. 7d or 12m) [Borg 2.x+ only]',
)
delete_group.add_argument(
'--older',
metavar='TIMESPAN',
help='Delete archives that are older than the specified time range (e.g. 7d or 12m) from the current time [Borg 2.x+ only]',
)
delete_group.add_argument(
'--newer',
metavar='TIMESPAN',
help='Delete archives that are newer than the specified time range (e.g. 7d or 12m) from the current time [Borg 2.x+ only]',
)
delete_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
extract_parser = action_parsers.add_parser(
'extract',
aliases=ACTION_ALIASES['extract'],
@@ -977,6 +1070,43 @@ def make_parsers():
)
umount_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
rdelete_parser = action_parsers.add_parser(
'rdelete',
aliases=ACTION_ALIASES['rdelete'],
help='Delete an entire repository (with Borg 1.2+, you must run compact afterwards to actually free space)',
description='Delete an entire repository (with Borg 1.2+, you must run compact afterwards to actually free space)',
add_help=False,
)
rdelete_group = rdelete_parser.add_argument_group('delete arguments')
rdelete_group.add_argument(
'--repository',
help='Path of repository to delete, defaults to the configured repository if there is only one',
)
rdelete_group.add_argument(
'--list',
dest='list_archives',
action='store_true',
help='Show details for the archives in the given repository',
)
rdelete_group.add_argument(
'--force',
action='count',
help='Force deletion of corrupted archives, can be given twice if once does not work',
)
rdelete_group.add_argument(
'--cache-only',
action='store_true',
help='Delete only the local cache for the given repository',
)
rdelete_group.add_argument(
'--keep-security-info',
action='store_true',
help='Do not delete the local security info when deleting a repository',
)
rdelete_group.add_argument(
'-h', '--help', action='help', help='Show this help message and exit'
)
restore_parser = action_parsers.add_parser(
'restore',
aliases=ACTION_ALIASES['restore'],
+22
View File
@@ -18,6 +18,7 @@ import borgmatic.actions.config.bootstrap
import borgmatic.actions.config.generate
import borgmatic.actions.config.validate
import borgmatic.actions.create
import borgmatic.actions.delete
import borgmatic.actions.export_key
import borgmatic.actions.export_tar
import borgmatic.actions.extract
@@ -26,6 +27,7 @@ import borgmatic.actions.list
import borgmatic.actions.mount
import borgmatic.actions.prune
import borgmatic.actions.rcreate
import borgmatic.actions.rdelete
import borgmatic.actions.restore
import borgmatic.actions.rinfo
import borgmatic.actions.rlist
@@ -479,6 +481,26 @@ def run_actions(
local_path,
remote_path,
)
elif action_name == 'delete' and action_name not in skip_actions:
borgmatic.actions.delete.run_delete(
repository,
config,
local_borg_version,
action_arguments,
global_arguments,
local_path,
remote_path,
)
elif action_name == 'rdelete' and action_name not in skip_actions:
borgmatic.actions.rdelete.run_rdelete(
repository,
config,
local_borg_version,
action_arguments,
global_arguments,
local_path,
remote_path,
)
elif action_name == 'borg' and action_name not in skip_actions:
borgmatic.actions.borg.run_borg(
repository,
+76
View File
@@ -546,6 +546,20 @@ properties:
"always": running this check every time checks
are run.
example: 2 weeks
only_run_on:
type: array
items:
type: string
description: |
After the "frequency" duration has elapsed, only
run this check if the current day of the week
matches one of these values (the name of a day of
the week in the current locale). "weekday" and
"weekend" are also accepted. Defaults to running
the check on any day of the week.
example:
- Saturday
- Sunday
- required: [name]
additionalProperties: false
properties:
@@ -579,6 +593,20 @@ properties:
"always": running this check every time checks
are run.
example: 2 weeks
only_run_on:
type: array
items:
type: string
description: |
After the "frequency" duration has elapsed, only
run this check if the current day of the week
matches one of these values (the name of a day of
the week in the current locale). "weekday" and
"weekend" are also accepted. Defaults to running
the check on any day of the week.
example:
- Saturday
- Sunday
max_duration:
type: integer
description: |
@@ -627,6 +655,20 @@ properties:
"always": running this check every time checks
are run.
example: 2 weeks
only_run_on:
type: array
items:
type: string
description: |
After the "frequency" duration has elapsed, only
run this check if the current day of the week
matches one of these values (the name of a day of
the week in the current locale). "weekday" and
"weekend" are also accepted. Defaults to running
the check on any day of the week.
example:
- Saturday
- Sunday
count_tolerance_percentage:
type: number
description: |
@@ -705,11 +747,13 @@ properties:
- compact
- create
- check
- delete
- extract
- config
- export-tar
- mount
- umount
- rdelete
- restore
- rlist
- list
@@ -1724,6 +1768,38 @@ properties:
an account at https://healthchecks.io (or self-host Healthchecks) if
you'd like to use this service. See borgmatic monitoring
documentation for details.
uptime_kuma:
type: object
required: ['push_url']
additionalProperties: false
properties:
push_url:
type: string
description: |
Uptime Kuma push URL without query string (do not include the
question mark or anything after it).
example: https://example.uptime.kuma/api/push/abcd1234
states:
type: array
items:
type: string
enum:
- start
- finish
- fail
uniqueItems: true
description: |
List of one or more monitoring states to push for: "start",
"finish", and/or "fail". Defaults to pushing for all
states.
example:
- start
- finish
- fail
description: |
Configuration for a monitoring integration with Uptime Kuma using
the Push monitor type.
See more information here: https://uptime.kuma.pet
cronitor:
type: object
required: ['ping_url']
+3 -1
View File
@@ -13,6 +13,7 @@ from borgmatic.hooks import (
pagerduty,
postgresql,
sqlite,
uptimekuma,
)
logger = logging.getLogger(__name__)
@@ -22,6 +23,7 @@ HOOK_NAME_TO_MODULE = {
'cronhub': cronhub,
'cronitor': cronitor,
'healthchecks': healthchecks,
'loki': loki,
'mariadb_databases': mariadb,
'mongodb_databases': mongodb,
'mysql_databases': mysql,
@@ -29,7 +31,7 @@ HOOK_NAME_TO_MODULE = {
'pagerduty': pagerduty,
'postgresql_databases': postgresql,
'sqlite_databases': sqlite,
'loki': loki,
'uptime_kuma': uptimekuma,
}
+1 -1
View File
@@ -15,7 +15,7 @@ MONITOR_STATE_TO_HEALTHCHECKS = {
monitor.State.LOG: 'log',
}
DEFAULT_PING_BODY_LIMIT_BYTES = 1500
DEFAULT_PING_BODY_LIMIT_BYTES = 100000
HANDLER_IDENTIFIER = 'healthchecks'
+10 -1
View File
@@ -1,6 +1,15 @@
from enum import Enum
MONITOR_HOOK_NAMES = ('apprise', 'healthchecks', 'cronitor', 'cronhub', 'pagerduty', 'ntfy', 'loki')
MONITOR_HOOK_NAMES = (
'apprise',
'cronhub',
'cronitor',
'healthchecks',
'loki',
'ntfy',
'pagerduty',
'uptime_kuma',
)
class State(Enum):
+1 -1
View File
@@ -40,7 +40,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
return
hostname = platform.node()
local_timestamp = datetime.datetime.now(datetime.UTC).astimezone().isoformat()
local_timestamp = datetime.datetime.now(datetime.timezone.utc).astimezone().isoformat()
payload = json.dumps(
{
'routing_key': hook_config['integration_key'],
+55
View File
@@ -0,0 +1,55 @@
import logging
import requests
logger = logging.getLogger(__name__)
def initialize_monitor(
push_url, config, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
'''
No initialization is necessary for this monitor.
'''
pass
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
'''
Make a get request to the configured Uptime Kuma push_url. Use the given configuration filename
in any log entries. If this is a dry run, then don't actually push anything.
'''
run_states = hook_config.get('states', ['start', 'finish', 'fail'])
if state.name.lower() not in run_states:
return
dry_run_label = ' (dry run; not actually pushing)' if dry_run else ''
status = 'down' if state.name.lower() == 'fail' else 'up'
push_url = hook_config.get('push_url', 'https://example.uptime.kuma/api/push/abcd1234')
query = f'status={status}&msg={state.name.lower()}'
logger.info(
f'{config_filename}: Pushing Uptime Kuma push_url {push_url}?{query} {dry_run_label}'
)
logger.debug(f'{config_filename}: Full Uptime Kuma state URL {push_url}?{query}')
if dry_run:
return
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
response = requests.get(f'{push_url}?{query}')
if not response.ok:
response.raise_for_status()
except requests.exceptions.RequestException as error:
logger.warning(f'{config_filename}: Uptime Kuma error: {error}')
def destroy_monitor(
push_url_or_uuid, config, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
'''
No destruction is necessary for this monitor.
'''
pass
+1 -1
View File
@@ -4,7 +4,7 @@ COPY . /app
RUN apk add --no-cache py3-pip py3-ruamel.yaml py3-ruamel.yaml.clib
RUN pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
RUN borgmatic --help > /command-line.txt \
&& for action in rcreate transfer create prune compact check extract config "config bootstrap" "config generate" "config validate" export-tar mount umount restore rlist list rinfo info break-lock borg; do \
&& for action in rcreate transfer create prune compact check delete extract config "config bootstrap" "config generate" "config validate" export-tar mount umount rdelete restore rlist list rinfo info break-lock borg; do \
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
&& borgmatic $action --help >> /command-line.txt; done
+4 -5
View File
@@ -167,12 +167,11 @@ li {
padding: .25em 0;
}
li ul {
margin: .5em 0;
padding-left: 1em;
list-style-type: disc;
padding-left: 2em;
}
li li {
padding-top: .1em;
padding-bottom: .1em;
li li:last-child {
padding-bottom: 0em;
}
/* Syntax highlighting and Code blocks */
+1 -1
View File
@@ -48,7 +48,7 @@ support for [more granular exit
codes](https://borgbackup.readthedocs.io/en/1.4-maint/usage/general.html#return-codes)
means that you can configure borgmatic to respond to specific Borg conditions.
See the full list of [Borg 1.4 error and warning exit
codes](https://borgbackup.readthedocs.io/en/1.4.0b1/internals/frontends.html#message-ids).
codes](https://borgbackup.readthedocs.io/en/1.4.0b2/internals/frontends.html#message-ids).
The `rc:` numeric value there tells you the exit code for each.
For instance, this borgmatic configuration elevates all Borg backup file
@@ -242,6 +242,57 @@ check --force` runs `check` even if it's specified in the `skip_actions`
option.
### Check days
<span class="minilink minilink-addedin">New in version 1.8.13</span> You can
optionally configure checks to only run on particular days of the week. For
instance:
```yaml
checks:
- name: repository
only_run_on:
- Saturday
- Sunday
- name: archives
only_run_on:
- weekday
- name: spot
only_run_on:
- Friday
- weekend
```
Each day of the week is specified in the current locale (system
language/country settings). `weekend` and `weekday` are also accepted.
Just like with `frequency`, borgmatic only makes a best effort to run checks
on the given day of the week. For instance, if you run `borgmatic check`
daily, then every day borgmatic will have an opportunity to determine whether
your checks are configured to run on that day. If they are, then the checks
run. If not, they are skipped.
For instance, with the above configuration, if borgmatic is run on a Saturday,
the `repository` check will run. But on a Monday? The repository check will
get skipped. And if borgmatic is never run on a Saturday or a Sunday, that
check will never get a chance to run.
Also, the day of the week configuration applies *after* any configured
`frequency` for a check. So for instance, imagine the following configuration:
```yaml
checks:
- name: repository
frequency: 2 weeks
only_run_on:
- Monday
```
If you run borgmatic daily with that configuration, then borgmatic will first
wait two weeks after the previous check before running the check again—on the
first Monday after the `frequency` duration elapses.
### Running only checks
<span class="minilink minilink-addedin">New in version 1.7.1</span> If you
+10 -5
View File
@@ -102,9 +102,9 @@ and depend on containers for runtime dependencies. These tests do run on the
continuous integration (CI) server, and running them on your developer machine
is the closest thing to dev-CI parity.
If you would like to run the full test suite, first install Docker (or Podman;
see below) and [Docker Compose](https://docs.docker.com/compose/install/).
Then run:
If you would like to run the end-to-end tests, first install Docker (or
Podman; see below) and [Docker
Compose](https://docs.docker.com/compose/install/). Then run:
```bash
scripts/run-end-to-end-tests
@@ -152,12 +152,17 @@ the following deviations from it:
* In general, spell out words in variable names instead of shortening them.
So, think `index` instead of `idx`. There are some notable exceptions to
this though (like `config`).
* Favor blank lines around logical code groupings, `if` statements,
`return`s, etc. Readability is more important than packing code tightly.
* Import fully qualified Python modules instead of importing individual
functions, classes, or constants. E.g., do `import os.path` instead of
`from os import path`. (Some exceptions to this are made in tests.)
borgmatic code uses the [Black](https://black.readthedocs.io/en/stable/) code
formatter, the [Flake8](http://flake8.pycqa.org/en/latest/) code checker, and
the [isort](https://github.com/timothycrosley/isort) import orderer, so
certain code style requirements will be enforced when running automated tests.
See the Black, Flake8, and isort documentation for more information.
certain code style requirements are enforced when running automated tests. See
the Black, Flake8, and isort documentation for more information.
## Continuous integration
+1 -1
View File
@@ -133,7 +133,7 @@ For Borg 1.x, use a shell pattern for the `match_archives` value and see the
[Borg patterns
documentation](https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-help-patterns)
for more information. For Borg 2.x, see the [match archives
documentation](https://borgbackup.readthedocs.io/en/2.0.0b5/usage/help.html#borg-help-match-archives).
documentation](https://borgbackup.readthedocs.io/en/2.0.0b8/usage/help.html#borg-help-match-archives).
Some borgmatic command-line actions also have a `--match-archives` flag that
overrides both the auto-matching behavior and the `match_archives`
+63 -6
View File
@@ -39,13 +39,14 @@ below for how to configure this.
borgmatic integrates with these monitoring services and libraries, pinging
them as backups happen:
* [Healthchecks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook)
* [Cronitor](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronitor-hook)
* [Cronhub](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronhub-hook)
* [PagerDuty](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook)
* [ntfy](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#ntfy-hook)
* [Grafana Loki](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#loki-hook)
* [Apprise](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#apprise-hook)
* [Cronhub](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronhub-hook)
* [Cronitor](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronitor-hook)
* [Grafana Loki](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#loki-hook)
* [Healthchecks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook)
* [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)
* [Uptime Kuma](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#uptime-kuma-hook)
The idea is that you'll receive an alert when something goes wrong or when the
service doesn't hear from borgmatic for a configured interval (if supported).
@@ -505,6 +506,62 @@ See the [configuration
reference](https://torsion.org/borgmatic/docs/reference/configuration/) for
details.
## Uptime Kuma hook
<span class="minilink minilink-addedin">New in version 1.8.13</span> [Uptime
Kuma](https://uptime.kuma.pet) is an easy-to-use, self-hosted monitoring tool
and can provide a Push monitor type to accept HTTP `GET` requests from a
service instead of contacting it directly.
Uptime Kuma allows you to see a history of monitor states and can in turn
alert via ntfy, Gotify, Matrix, Apprise, Email, and many more.
An example configuration is shown here with all the available options:
```yaml
uptime_kuma:
push_url: https://kuma.my-domain.com/api/push/abcd1234
states:
- start
- finish
- fail
```
The `push_url` is provided to your from your Uptime Kuma service and
originally includes a query string—the text including and after the question
mark (`?`). But please do not include the query string in the `push_url`
configuration; borgmatic will add this automatically depending on the state of
your backup.
Using `start`, `finish` and `fail` states means you will get two "up beats" in
Uptime Kuma for successful backups and the ability to see failures if and when
the backup started (was there a `start` beat?).
A reasonable base-level configuration for an Uptime Kuma Monitor for a backup
is below:
```ini
# These are to be entered into Uptime Kuma and not into your borgmatic
# configuration.
# Push monitors wait for the client to contact Uptime Kuma instead of Uptime
# Kuma contacting the client. This is perfect for backup monitoring.
Monitor Type = Push
Heartbeat Interval = 90000 # = 25 hours = 1 day + 1 hour
# Wait 6 times the Heartbeat Retry (below) before logging a heartbeat missed.
Retries = 6
# Multiplied by Retries this gives a grace period within which the monitor
# goes into the "Pending" state.
Heartbeat Retry = 360 # = 10 minutes
# For each Heartbeat Interval if the backup fails repeatedly, a notification
# is sent each time.
Resend Notification every X times = 1
```
## Scripting borgmatic
+2 -2
View File
@@ -151,7 +151,7 @@ borgmatic rcreate --verbosity 1 --encryption repokey-aes-ocb \
```
Read about [Borg encryption
modes](https://borgbackup.readthedocs.io/en/2.0.0b5/usage/rcreate.html#encryption-mode-tldr)
modes](https://borgbackup.readthedocs.io/en/2.0.0b8/usage/rcreate.html#encryption-mode-tldr)
for more details.
To transfer data from your original Borg 1 repository to your newly created
@@ -174,7 +174,7 @@ confirmation of success—or tells you if something hasn't been transferred yet.
Note that by omitting the `--upgrader` flag, you can also do archive transfers
between related Borg 2 repositories without upgrading, even down to individual
archives. For more on that functionality, see the [Borg transfer
documentation](https://borgbackup.readthedocs.io/en/2.0.0b5/usage/transfer.html).
documentation](https://borgbackup.readthedocs.io/en/2.0.0b8/usage/transfer.html).
That's it! Now you can use your new Borg 2 repository as normal with
borgmatic. If you've got multiple repositories, repeat the above process for
+7 -10
View File
@@ -10,20 +10,17 @@ eleventyNavigation:
If case you're interested in [developing on
borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/),
here's an abridged primer on how its Python source code is organized to help
you get started. At the top level we have:
you get started. Starting at the top level, we have:
* [borgmatic](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic): The main borgmatic source module. Most of the code is here.
* [borgmatic](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic): The main borgmatic source module. Most of the code is here. Within that:
* [actions](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/actions): borgmatic-specific logic for running each action (create, list, check, etc.).
* [borg](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/borg): Lower-level code that's responsible for interacting with Borg to run each action.
* [commands](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/commands): Looking to add a new flag or action? Start here. This contains borgmatic's entry point, argument parsing, and shell completion.
* [config](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/config): Code responsible for loading, normalizing, and validating borgmatic's configuration.
* [hooks](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/hooks): Looking to add a new database or monitoring integration? Start here.
* [docs](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/docs): How-to and reference documentation, including the document you're reading now.
* [sample](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/sample): Example configurations for cron and systemd.
* [scripts](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/scripts): Dev-facing scripts for things like building documentation and running end-to-end tests.
* [tests](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/tests): Automated tests organized by: end-to-end, integration, and unit.
Within the `borgmatic` directory you'll find:
* [actions](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/actions): Mid-level code for running each borgmatic action (create, list, check, etc.).
* [borg](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/borg): Lower-level code that actually shells out to Borg for each action.
* [commands](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/commands): Looking to add a new flag or action? Start here. This contains borgmatic's entry point, argument parsing, and shell completion.
* [config](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/config): Code responsible for loading, normalizing, and validating borgmatic's configuration.
* [hooks](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/hooks): Looking to add a new database or monitoring integration? Start here.
So, broadly speaking, the control flow goes: `commands``config` followed by `commands``actions``borg` and `hooks`.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
from setuptools import find_packages, setup
VERSION = '1.8.12'
VERSION = '1.8.13'
setup(
+69 -1
View File
@@ -113,6 +113,74 @@ def test_filter_checks_on_frequency_retains_check_without_frequency():
) == ('archives',)
def test_filter_checks_on_frequency_retains_check_with_empty_only_run_on():
flexmock(module).should_receive('parse_frequency').and_return(None)
assert module.filter_checks_on_frequency(
config={'checks': [{'name': 'archives', 'only_run_on': []}]},
borg_repository_id='repo',
checks=('archives',),
force=False,
archives_check_id='1234',
datetime_now=flexmock(weekday=lambda: 0),
) == ('archives',)
def test_filter_checks_on_frequency_retains_check_with_only_run_on_matching_today():
flexmock(module).should_receive('parse_frequency').and_return(None)
assert module.filter_checks_on_frequency(
config={'checks': [{'name': 'archives', 'only_run_on': [module.calendar.day_name[0]]}]},
borg_repository_id='repo',
checks=('archives',),
force=False,
archives_check_id='1234',
datetime_now=flexmock(weekday=lambda: 0),
) == ('archives',)
def test_filter_checks_on_frequency_retains_check_with_only_run_on_matching_today_via_weekday_value():
flexmock(module).should_receive('parse_frequency').and_return(None)
assert module.filter_checks_on_frequency(
config={'checks': [{'name': 'archives', 'only_run_on': ['weekday']}]},
borg_repository_id='repo',
checks=('archives',),
force=False,
archives_check_id='1234',
datetime_now=flexmock(weekday=lambda: 0),
) == ('archives',)
def test_filter_checks_on_frequency_retains_check_with_only_run_on_matching_today_via_weekend_value():
flexmock(module).should_receive('parse_frequency').and_return(None)
assert module.filter_checks_on_frequency(
config={'checks': [{'name': 'archives', 'only_run_on': ['weekend']}]},
borg_repository_id='repo',
checks=('archives',),
force=False,
archives_check_id='1234',
datetime_now=flexmock(weekday=lambda: 6),
) == ('archives',)
def test_filter_checks_on_frequency_skips_check_with_only_run_on_not_matching_today():
flexmock(module).should_receive('parse_frequency').and_return(None)
assert (
module.filter_checks_on_frequency(
config={'checks': [{'name': 'archives', 'only_run_on': [module.calendar.day_name[5]]}]},
borg_repository_id='repo',
checks=('archives',),
force=False,
archives_check_id='1234',
datetime_now=flexmock(weekday=lambda: 0),
)
== ()
)
def test_filter_checks_on_frequency_retains_check_with_elapsed_frequency():
flexmock(module).should_receive('parse_frequency').and_return(
module.datetime.timedelta(hours=1)
@@ -168,7 +236,7 @@ def test_filter_checks_on_frequency_skips_check_with_unelapsed_frequency():
)
def test_filter_checks_on_frequency_restains_check_with_unelapsed_frequency_and_force():
def test_filter_checks_on_frequency_retains_check_with_unelapsed_frequency_and_force():
assert module.filter_checks_on_frequency(
config={'checks': [{'name': 'archives', 'frequency': '1 hour'}]},
borg_repository_id='repo',
+43
View File
@@ -0,0 +1,43 @@
from flexmock import flexmock
from borgmatic.actions import delete as module
def test_run_delete_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name')
flexmock(module.borgmatic.actions.arguments).should_receive('update_arguments').and_return(
flexmock()
)
flexmock(module.borgmatic.borg.delete).should_receive('delete_archives')
module.run_delete(
repository={'path': 'repo'},
config={},
local_borg_version=None,
delete_arguments=flexmock(repository=flexmock(), archive=flexmock()),
global_arguments=flexmock(),
local_path=None,
remote_path=None,
)
def test_run_delete_without_archive_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name')
flexmock(module.borgmatic.actions.arguments).should_receive('update_arguments').and_return(
flexmock()
)
flexmock(module.borgmatic.borg.delete).should_receive('delete_archives')
module.run_delete(
repository={'path': 'repo'},
config={},
local_borg_version=None,
delete_arguments=flexmock(repository=flexmock(), archive=None),
global_arguments=flexmock(),
local_path=None,
remote_path=None,
)
+41
View File
@@ -0,0 +1,41 @@
from flexmock import flexmock
from borgmatic.actions import rdelete as module
def test_run_rdelete_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.actions.arguments).should_receive('update_arguments').and_return(
flexmock()
)
flexmock(module.borgmatic.borg.rdelete).should_receive('delete_repository')
module.run_rdelete(
repository={'path': 'repo'},
config={},
local_borg_version=None,
rdelete_arguments=flexmock(repository=flexmock(), cache_only=False),
global_arguments=flexmock(),
local_path=None,
remote_path=None,
)
def test_run_rdelete_with_cache_only_does_not_raise():
flexmock(module.logger).answer = lambda message: None
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
flexmock(module.borgmatic.actions.arguments).should_receive('update_arguments').and_return(
flexmock()
)
flexmock(module.borgmatic.borg.rdelete).should_receive('delete_repository')
module.run_rdelete(
repository={'path': 'repo'},
config={},
local_borg_version=None,
rdelete_arguments=flexmock(repository=flexmock(), cache_only=True),
global_arguments=flexmock(),
local_path=None,
remote_path=None,
)
+338
View File
@@ -0,0 +1,338 @@
import logging
import pytest
from flexmock import flexmock
from borgmatic.borg import delete as module
from ..test_verbosity import insert_logging_mock
def test_make_delete_command_includes_log_info():
insert_logging_mock(logging.INFO)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_delete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=False, force=0, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'delete', '--info', 'repo')
def test_make_delete_command_includes_log_debug():
insert_logging_mock(logging.DEBUG)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_delete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=False, force=0, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'delete', '--debug', '--show-rc', 'repo')
def test_make_delete_command_includes_dry_run():
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').with_args(
'dry-run', True
).and_return(('--dry-run',))
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_delete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=False, force=0, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=True, log_json=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'delete', '--dry-run', 'repo')
def test_make_delete_command_includes_remote_path():
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').with_args(
'remote-path', 'borg1'
).and_return(('--remote-path', 'borg1'))
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_delete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=False, force=0, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path='borg1',
)
assert command == ('borg', 'delete', '--remote-path', 'borg1', 'repo')
def test_make_delete_command_includes_log_json():
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').with_args(
'log-json', True
).and_return(('--log-json',))
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_delete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=False, force=0, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=False, log_json=True),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'delete', '--log-json', 'repo')
def test_make_delete_command_includes_lock_wait():
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').with_args(
'lock-wait', 5
).and_return(('--lock-wait', '5'))
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_delete_command(
repository={'path': 'repo'},
config={'lock_wait': 5},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=False, force=0, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'delete', '--lock-wait', '5', 'repo')
def test_make_delete_command_includes_list():
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').with_args(
'list', True
).and_return(('--list',))
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_delete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=True, force=0, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'delete', '--list', 'repo')
def test_make_delete_command_includes_force():
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_delete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=False, force=1, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'delete', '--force', 'repo')
def test_make_delete_command_includes_force_twice():
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_delete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(list_archives=False, force=2, match_archives=None, archive=None),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'delete', '--force', '--force', 'repo')
def test_make_delete_command_includes_archive():
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(
('--match-archives', 'archive')
)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_delete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(
list_archives=False, force=0, match_archives=None, archive='archive'
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'delete', '--match-archives', 'archive', 'repo')
def test_make_delete_command_includes_match_archives():
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_match_archives_flags').and_return(
('--match-archives', 'sh:foo*')
)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_delete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
delete_arguments=flexmock(
list_archives=False, force=0, match_archives='sh:foo*', archive='archive'
),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'delete', '--match-archives', 'sh:foo*', 'repo')
def test_delete_archives_with_archive_calls_borg_delete():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.rdelete).should_receive('delete_repository').never()
flexmock(module).should_receive('make_delete_command').and_return(flexmock())
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
flexmock()
)
flexmock(module.borgmatic.execute).should_receive('execute_command').once()
module.delete_archives(
repository={'path': 'repo'},
config={},
local_borg_version=flexmock(),
delete_arguments=flexmock(archive='archive'),
global_arguments=flexmock(),
)
def test_delete_archives_with_match_archives_calls_borg_delete():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.rdelete).should_receive('delete_repository').never()
flexmock(module).should_receive('make_delete_command').and_return(flexmock())
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
flexmock()
)
flexmock(module.borgmatic.execute).should_receive('execute_command').once()
module.delete_archives(
repository={'path': 'repo'},
config={},
local_borg_version=flexmock(),
delete_arguments=flexmock(match_archives='sh:foo*'),
global_arguments=flexmock(),
)
@pytest.mark.parametrize('argument_name', module.ARCHIVE_RELATED_ARGUMENT_NAMES[2:])
def test_delete_archives_with_archive_related_argument_calls_borg_delete(argument_name):
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.rdelete).should_receive('delete_repository').never()
flexmock(module).should_receive('make_delete_command').and_return(flexmock())
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
flexmock()
)
flexmock(module.borgmatic.execute).should_receive('execute_command').once()
module.delete_archives(
repository={'path': 'repo'},
config={},
local_borg_version=flexmock(),
delete_arguments=flexmock(archive='archive', **{argument_name: 'value'}),
global_arguments=flexmock(),
)
def test_delete_archives_without_archive_related_argument_calls_borg_rdelete():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.rdelete).should_receive('delete_repository').once()
flexmock(module).should_receive('make_delete_command').never()
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').never()
flexmock(module.borgmatic.execute).should_receive('execute_command').never()
module.delete_archives(
repository={'path': 'repo'},
config={},
local_borg_version=flexmock(),
delete_arguments=flexmock(
list_archives=True, force=False, cache_only=False, keep_security_info=False
),
global_arguments=flexmock(),
)
+14
View File
@@ -190,6 +190,20 @@ def test_make_match_archives_flags_makes_flags_with_globs(
)
def test_make_match_archives_flags_accepts_default_archive_name_format():
flexmock(module.feature).should_receive('available').and_return(True)
assert (
module.make_match_archives_flags(
match_archives=None,
archive_name_format=None,
local_borg_version=flexmock(),
default_archive_name_format='*',
)
== ()
)
def test_warn_for_aggressive_archive_flags_without_archive_flags_bails():
flexmock(module.logger).should_receive('warning').never()
+336
View File
@@ -0,0 +1,336 @@
import logging
from flexmock import flexmock
from borgmatic.borg import rdelete as module
from ..test_verbosity import insert_logging_mock
def test_make_rdelete_command_with_feature_available_runs_borg_rdelete():
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_rdelete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
rdelete_arguments=flexmock(list_archives=False, force=0),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'rdelete', 'repo')
def test_make_rdelete_command_without_feature_available_runs_borg_delete():
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(False)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_rdelete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
rdelete_arguments=flexmock(list_archives=False, force=0),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'delete', 'repo')
def test_make_rdelete_command_includes_log_info():
insert_logging_mock(logging.INFO)
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_rdelete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
rdelete_arguments=flexmock(list_archives=False, force=0),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'rdelete', '--info', 'repo')
def test_make_rdelete_command_includes_log_debug():
insert_logging_mock(logging.DEBUG)
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_rdelete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
rdelete_arguments=flexmock(list_archives=False, force=0),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'rdelete', '--debug', '--show-rc', 'repo')
def test_make_rdelete_command_includes_dry_run():
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').with_args(
'dry-run', True
).and_return(('--dry-run',))
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_rdelete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
rdelete_arguments=flexmock(list_archives=False, force=0),
global_arguments=flexmock(dry_run=True, log_json=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'rdelete', '--dry-run', 'repo')
def test_make_rdelete_command_includes_remote_path():
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').with_args(
'remote-path', 'borg1'
).and_return(('--remote-path', 'borg1'))
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_rdelete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
rdelete_arguments=flexmock(list_archives=False, force=0),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path='borg1',
)
assert command == ('borg', 'rdelete', '--remote-path', 'borg1', 'repo')
def test_make_rdelete_command_includes_log_json():
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').with_args(
'log-json', True
).and_return(('--log-json',))
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_rdelete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
rdelete_arguments=flexmock(list_archives=False, force=0),
global_arguments=flexmock(dry_run=False, log_json=True),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'rdelete', '--log-json', 'repo')
def test_make_rdelete_command_includes_lock_wait():
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').with_args(
'lock-wait', 5
).and_return(('--lock-wait', '5'))
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_rdelete_command(
repository={'path': 'repo'},
config={'lock_wait': 5},
local_borg_version='1.2.3',
rdelete_arguments=flexmock(list_archives=False, force=0),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'rdelete', '--lock-wait', '5', 'repo')
def test_make_rdelete_command_includes_list():
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').with_args(
'list', True
).and_return(('--list',))
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_rdelete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
rdelete_arguments=flexmock(list_archives=True, force=0),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'rdelete', '--list', 'repo')
def test_make_rdelete_command_includes_force():
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_rdelete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
rdelete_arguments=flexmock(list_archives=False, force=1),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'rdelete', '--force', 'repo')
def test_make_rdelete_command_includes_force_twice():
flexmock(module.borgmatic.borg.feature).should_receive('available').and_return(True)
flexmock(module.borgmatic.borg.flags).should_receive('make_flags').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.borgmatic.borg.flags).should_receive('make_repository_flags').and_return(
('repo',)
)
command = module.make_rdelete_command(
repository={'path': 'repo'},
config={},
local_borg_version='1.2.3',
rdelete_arguments=flexmock(list_archives=False, force=2),
global_arguments=flexmock(dry_run=False, log_json=False),
local_path='borg',
remote_path=None,
)
assert command == ('borg', 'rdelete', '--force', '--force', 'repo')
def test_delete_repository_with_defaults_does_not_capture_output():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
command = flexmock()
flexmock(module).should_receive('make_rdelete_command').and_return(command)
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
flexmock()
)
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
command,
output_log_level=module.logging.ANSWER,
output_file=module.borgmatic.execute.DO_NOT_CAPTURE,
extra_environment=object,
borg_local_path='borg',
borg_exit_codes=None,
).once()
module.delete_repository(
repository={'path': 'repo'},
config={},
local_borg_version=flexmock(),
rdelete_arguments=flexmock(force=False, cache_only=False),
global_arguments=flexmock(),
local_path='borg',
remote_path=None,
)
def test_delete_repository_with_force_captures_output():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
command = flexmock()
flexmock(module).should_receive('make_rdelete_command').and_return(command)
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
flexmock()
)
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
command,
output_log_level=module.logging.ANSWER,
output_file=None,
extra_environment=object,
borg_local_path='borg',
borg_exit_codes=None,
).once()
module.delete_repository(
repository={'path': 'repo'},
config={},
local_borg_version=flexmock(),
rdelete_arguments=flexmock(force=True, cache_only=False),
global_arguments=flexmock(),
local_path='borg',
remote_path=None,
)
def test_delete_repository_with_cache_only_captures_output():
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
command = flexmock()
flexmock(module).should_receive('make_rdelete_command').and_return(command)
flexmock(module.borgmatic.borg.environment).should_receive('make_environment').and_return(
flexmock()
)
flexmock(module.borgmatic.execute).should_receive('execute_command').with_args(
command,
output_log_level=module.logging.ANSWER,
output_file=None,
extra_environment=object,
borg_local_path='borg',
borg_exit_codes=None,
).once()
module.delete_repository(
repository={'path': 'repo'},
config={},
local_borg_version=flexmock(),
rdelete_arguments=flexmock(force=False, cache_only=True),
global_arguments=flexmock(),
local_path='borg',
remote_path=None,
)
+40
View File
@@ -978,6 +978,46 @@ def test_run_actions_runs_export_key():
)
def test_run_actions_runs_delete():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
flexmock(borgmatic.actions.delete).should_receive('run_delete').once()
tuple(
module.run_actions(
arguments={'global': flexmock(dry_run=False, log_file='foo'), 'delete': flexmock()},
config_filename=flexmock(),
config={'repositories': []},
config_paths=[],
local_path=flexmock(),
remote_path=flexmock(),
local_borg_version=flexmock(),
repository={'path': 'repo'},
)
)
def test_run_actions_runs_rdelete():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
flexmock(module.command).should_receive('execute_hook')
flexmock(borgmatic.actions.rdelete).should_receive('run_rdelete').once()
tuple(
module.run_actions(
arguments={'global': flexmock(dry_run=False, log_file='foo'), 'rdelete': flexmock()},
config_filename=flexmock(),
config={'repositories': []},
config_paths=[],
local_path=flexmock(),
remote_path=flexmock(),
local_borg_version=flexmock(),
repository={'path': 'repo'},
)
)
def test_run_actions_runs_borg():
flexmock(module).should_receive('add_custom_log_levels')
flexmock(module).should_receive('get_skip_actions').and_return([])
+165
View File
@@ -0,0 +1,165 @@
from flexmock import flexmock
import borgmatic.hooks.monitor
from borgmatic.hooks import uptimekuma as module
DEFAULT_PUSH_URL = 'https://example.uptime.kuma/api/push/abcd1234'
CUSTOM_PUSH_URL = 'https://uptime.example.com/api/push/efgh5678'
def test_ping_monitor_hits_default_uptimekuma_on_fail():
hook_config = {}
flexmock(module.requests).should_receive('get').with_args(
f'{DEFAULT_PUSH_URL}?status=down&msg=fail'
).and_return(flexmock(ok=True)).once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_hits_custom_uptimekuma_on_fail():
hook_config = {'push_url': CUSTOM_PUSH_URL}
flexmock(module.requests).should_receive('get').with_args(
f'{CUSTOM_PUSH_URL}?status=down&msg=fail'
).and_return(flexmock(ok=True)).once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_custom_uptimekuma_on_start():
hook_config = {'push_url': CUSTOM_PUSH_URL}
flexmock(module.requests).should_receive('get').with_args(
f'{CUSTOM_PUSH_URL}?status=up&msg=start'
).and_return(flexmock(ok=True)).once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.START,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_custom_uptimekuma_on_finish():
hook_config = {'push_url': CUSTOM_PUSH_URL}
flexmock(module.requests).should_receive('get').with_args(
f'{CUSTOM_PUSH_URL}?status=up&msg=finish'
).and_return(flexmock(ok=True)).once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FINISH,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_does_not_hit_custom_uptimekuma_on_fail_dry_run():
hook_config = {'push_url': CUSTOM_PUSH_URL}
flexmock(module.requests).should_receive('get').never()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=True,
)
def test_ping_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run():
hook_config = {'push_url': CUSTOM_PUSH_URL}
flexmock(module.requests).should_receive('get').never()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.START,
monitoring_log_level=1,
dry_run=True,
)
def test_ping_monitor_does_not_hit_custom_uptimekuma_on_finish_dry_run():
hook_config = {'push_url': CUSTOM_PUSH_URL}
flexmock(module.requests).should_receive('get').never()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FINISH,
monitoring_log_level=1,
dry_run=True,
)
def test_ping_monitor_with_connection_error_logs_warning():
hook_config = {'push_url': CUSTOM_PUSH_URL}
flexmock(module.requests).should_receive('get').with_args(
f'{CUSTOM_PUSH_URL}?status=down&msg=fail'
).and_raise(module.requests.exceptions.ConnectionError)
flexmock(module.logger).should_receive('warning').once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_with_other_error_logs_warning():
hook_config = {'push_url': CUSTOM_PUSH_URL}
response = flexmock(ok=False)
response.should_receive('raise_for_status').and_raise(
module.requests.exceptions.RequestException
)
flexmock(module.requests).should_receive('get').with_args(
f'{CUSTOM_PUSH_URL}?status=down&msg=fail'
).and_return(response)
flexmock(module.logger).should_receive('warning').once()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
def test_ping_monitor_with_invalid_run_state():
hook_config = {'push_url': CUSTOM_PUSH_URL}
flexmock(module.requests).should_receive('get').never()
module.ping_monitor(
hook_config,
{},
'config.yaml',
borgmatic.hooks.monitor.State.LOG,
monitoring_log_level=1,
dry_run=True,
)