Skip to content

Commit

Permalink
Follow Borg exit status convention: 2 indicates a fatal error, 1 indi…
Browse files Browse the repository at this point in the history
…cates warning or error

Change hooks so that they report success if create command completes.
Make hooks a context manager so that there is always a terminal report.
  • Loading branch information
Ken Kundert authored and Ken Kundert committed Dec 15, 2021
1 parent f0ec0b3 commit 7591965
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 43 deletions.
15 changes: 15 additions & 0 deletions doc/commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ These commands are described in more detail below. Not everything is described
here. Run ``emborg help <cmd>`` for the details.


.. _exit status:

Exit Status
----------

*Emborg* returns with an exit status of 0 if it completes without issue. It
returns with an exit status of 1 if was able to terminate normally but some
exceptional condition was encountered along the way. For example, if the
:ref:`compare <compare>` or :ref:`diff <diff>` detects a difference or if
:due:`due <due>` command detects the backups are overdue, a 1 is returned. In
addition, 1 is returned if *Borg* detects an error but is able to complete
anyway. However, if *Emborg* or *Borg* suffers errors and cannot complete, 2 is
returned.


.. _borg:

Borg
Expand Down
14 changes: 13 additions & 1 deletion doc/monitoring.rst
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ following setting in your configuration file:
cronhub_uuid = '51cb35d8-2975-110b-67a7-11b65d432027'
If given, this setting should be specified on an individual configuration. It
causes a report to be sent to *CronHub* each time an archive is created.
A successful report is given if *Borg* returns with an exit status of 0 or 1,
which implies that the command completed as expected, though there might have
been issues with individual files or directories. If *Borg* returns with an
exit status of 2 or greater, a failure is reported.


.. _healthchecks:

Expand All @@ -187,4 +194,9 @@ following setting in your configuration file:
healthchecks_uuid = '51cb35d8-2975-110b-67a7-11b65d432027'
If given, this setting should be specified on an individual configuration. It
causes a report to be sent to *HealthChecks* each time an archive is created.
A successful report is given if *Borg* returns with an exit status of 0 or 1,
which implies that the command completed as expected, though there might have
been issues with individual files or directories. If *Borg* returns with an
exit status of 2 or greater, a failure is reported.
2 changes: 2 additions & 0 deletions doc/releases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Latest development release
| Released: 2021-12-14
- Do not signal failure to hooks if Borg only emits a warning.
- Return an exit status of 1 if *Emborg run to completion but with exceptions,
and 2 if it cannot complete normally due to a error or errors.

1.28 (2021-11-06)
Expand Down
71 changes: 50 additions & 21 deletions emborg/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,8 @@ def run(cls, command, args, settings, options):
output(out.rstrip())
rm(settings.lockfile)

return borg.status


# CheckCommand command {{{1
class CheckCommand(Command):
Expand Down Expand Up @@ -400,6 +402,8 @@ def run(cls, command, args, settings, options):
if out:
output(out.rstrip())

return borg.status


# CompareCommand command {{{1
class CompareCommand(Command):
Expand Down Expand Up @@ -633,22 +637,22 @@ def run(cls, command, args, settings, options):

# run borg
src_dirs = settings.src_dirs
settings.hooks.backups_begin()
try:
borg = settings.run_borg(
cmd = "create",
borg_opts = borg_opts,
args = [settings.destination(True)] + src_dirs,
emborg_opts = options,
show_borg_output = bool(borg_opts),
use_working_dir = True,
)
except Error as e:
if e.stderr and "is not a valid repository" in e.stderr:
e.reraise(codicil="Run 'emborg init' to initialize the repository.")
else:
raise
settings.hooks.backups_finish(borg)
with settings.hooks:
try:
borg = settings.run_borg(
cmd = "create",
borg_opts = borg_opts,
args = [settings.destination(True)] + src_dirs,
emborg_opts = options,
show_borg_output = bool(borg_opts),
use_working_dir = True,
)
create_status = borg.status
except Error as e:
if e.stderr and "is not a valid repository" in e.stderr:
e.reraise(codicil="Run 'emborg init' to initialize the repository.")
else:
raise

# update the date files
narrate("update date file")
Expand All @@ -669,7 +673,7 @@ def run(cls, command, args, settings, options):
e.reraise(culprit=(setting, cmd.split()[0]))

if cmdline["--fast"]:
return
return create_status

# check and prune the archives if requested
try:
Expand All @@ -692,22 +696,27 @@ def run(cls, command, args, settings, options):
)
args = []
check = CheckCommand()
check.run("check", args, settings, options)
check_status = check.run("check", args, settings, options)
else:
check_status = 0

activity = "pruning"
if settings.prune_after_create:
announce("Pruning archives ...")
prune = PruneCommand()
args = ["--stats"] if cmdline["--stats"] else []
prune.run("prune", args, settings, options)
prune_status = prune.run("prune", args, settings, options)
else:
prune_status = 0

except Error as e:
e.reraise(
codicil = (
f"This error occurred while {activity} the archives.",
"No error was reported while creating the archive.",
)
)

return max([create_status, check_status, prune_status])

# DeleteCommand command {{{1
class DeleteCommand(Command):
Expand Down Expand Up @@ -757,6 +766,8 @@ def run(cls, command, args, settings, options):
if out:
output(out.rstrip())

return borg.status


# DiffCommand command {{{1
class DiffCommand(Command):
Expand Down Expand Up @@ -1082,6 +1093,8 @@ def run(cls, command, args, settings, options):
if out:
output(out.rstrip())

return borg.status


# HelpCommand {{{1
class HelpCommand(Command):
Expand All @@ -1105,6 +1118,7 @@ def run_early(cls, command, args, settings, options):
from .help import HelpMessage

HelpMessage.show(cmdline["<topic>"])

return 0


Expand Down Expand Up @@ -1166,6 +1180,8 @@ def run(cls, command, args, settings, options):
output()
output(out.rstrip())

return borg.status


# InitializeCommand command {{{1
class InitializeCommand(Command):
Expand Down Expand Up @@ -1194,6 +1210,8 @@ def run(cls, command, args, settings, options):
if out:
output(out.rstrip())

return borg.status


# ListCommand command {{{1
class ListCommand(Command):
Expand Down Expand Up @@ -1232,6 +1250,8 @@ def run(cls, command, args, settings, options):
if out:
output(out.rstrip())

return borg.status


# LogCommand command {{{1
class LogCommand(Command):
Expand Down Expand Up @@ -1500,6 +1520,8 @@ def run(cls, command, args, settings, options):
except KeyError as e:
raise Error('Unknown key in:', culprit=e, codicil=template)

return borg.status


# MountCommand command {{{1
class MountCommand(Command):
Expand Down Expand Up @@ -1599,6 +1621,8 @@ def run(cls, command, args, settings, options):
if out:
output(out.rstrip())

return borg.status


# PruneCommand command {{{1
class PruneCommand(Command):
Expand Down Expand Up @@ -1655,6 +1679,8 @@ def run(cls, command, args, settings, options):
if out:
output(out.rstrip())

return borg.status


# RestoreCommand command {{{1
class RestoreCommand(Command):
Expand Down Expand Up @@ -1737,6 +1763,8 @@ def run(cls, command, args, settings, options):
if out:
output(out.rstrip())

return borg.status


# SettingsCommand command {{{1
class SettingsCommand(Command):
Expand Down Expand Up @@ -1841,7 +1869,7 @@ def run(cls, command, args, settings, options):

# run borg
try:
settings.run_borg(
borg = settings.run_borg(
cmd="umount", args=[mount_point], emborg_opts=options,
)
try:
Expand All @@ -1853,6 +1881,7 @@ def run(cls, command, args, settings, options):
e.reraise(
codicil = f"Try running 'lsof +D {mount_point!s}' to find culprit."
)
return borg.status


# VersionCommand {{{1
Expand Down
35 changes: 18 additions & 17 deletions emborg/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ def __init__(self, settings):
if c.is_active():
self.active_hooks.append(c)

def backups_begin(self):
def __enter__(self):
for hook in self.active_hooks:
hook.signal_start()

def backups_finish(self, borg):
def __exit__(self, exc_type, exc_value, exc_traceback):
for hook in self.active_hooks:
hook.signal_end(borg)
hook.signal_end(exc_value)

def is_active(self):
return bool(self.uuid)
Expand All @@ -55,10 +55,8 @@ def signal_start(self):
except requests.exceptions.RequestException as e:
raise Error(f'{self.NAME} connection error.', codicil=full_stop(e))

def signal_end(self, borg):
# Borg returns 0 on clear success, 1 if there was a warning, and 2 or
# greater if there was an error that prevented normal termination.
if borg.status > 1:
def signal_end(self, exception):
if exception:
url = self.FAIL_URL.format(url=self.url, uuid=self.uuid)
result = 'failure'
else:
Expand Down Expand Up @@ -94,20 +92,23 @@ def signal_start(self):
except requests.exceptions.RequestException as e:
raise Error('{self.NAME} connection error.', codicil=full_stop(e))

def signal_end(self, borg):
status = borg.status
# Borg returns 0 on clear success, 1 if there was a warning, and 2 or
# greater if there was an error that prevented normal termination.
result = 'failure' if status > 1 else 'success'
payload = borg.stderr
def signal_end(self, exception):
if exception:
result = 'failure'
status = exception.status
payload = exception.stderr
else:
result = 'success'
status = 0
payload = ''

url = f'{self.url}/{self.uuid}/{status}'
log(f'signaling {result} of backups to {self.NAME}: {url}.')
try:
if payload is None:
log('log unavailable to upload to healthchecks.io.')
response = requests.post(url)
else:
if payload:
response = requests.post(url, data=payload.encode('utf-8'))
else:
response = requests.post(url)
except requests.exceptions.RequestException as e:
raise Error('{self.NAME} connection error.', codicil=full_stop(e))

Expand Down
10 changes: 7 additions & 3 deletions emborg/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
import sys
from docopt import docopt
from inform import (
Error, Inform, LoggingCache, cull, display, fatal, os_error, terminate
Error, Inform, LoggingCache, cull, display, error, os_error, terminate
)
from . import __released__, __version__
from .command import Command
Expand Down Expand Up @@ -112,6 +112,7 @@ def main():
cmd_name, args, settings, emborg_opts
)
except Error as e:
exit_status = 2
settings.fail(e, cmd=' '.join(sys.argv))
e.terminate()

Expand All @@ -126,7 +127,10 @@ def main():
except KeyboardInterrupt:
display("Terminated by user.")
except Error as e:
e.terminate()
exit_status = 2
except OSError as e:
fatal(os_error(e))
exit_status = 2
error(os_error(e))
if exit_status and exit_status > worst_exit_status:
worst_exit_status = exit_status
terminate(worst_exit_status)
3 changes: 2 additions & 1 deletion tests/test-cases.nt
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,7 @@ emborg with configs:
args: --quiet --config test5 extract configs
expected:
> Include pattern 'configs' never matched.
expected_type: diff

-
args: --quiet --config test5 extract /«TESTS»/configs
Expand Down Expand Up @@ -824,7 +825,7 @@ emborg with configs:
> Archive: test7-\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d
> emborg warning: Warning emitted by Borg:
> Include pattern 'configs' never matched.
expected_type: regex
expected_type: regex diff

-
args: --quiet --config test7 extract /«TESTS»/configs
Expand Down

0 comments on commit 7591965

Please sign in to comment.