2019-06-12 20:09:04 +00:00
|
|
|
import logging
|
2019-06-14 00:05:26 +00:00
|
|
|
import os
|
2022-05-23 22:27:54 +00:00
|
|
|
import re
|
2017-10-26 04:38:27 +00:00
|
|
|
|
2019-06-12 20:09:04 +00:00
|
|
|
from borgmatic import execute
|
2017-10-26 04:38:27 +00:00
|
|
|
|
2019-06-17 18:53:08 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
2017-10-26 05:32:06 +00:00
|
|
|
|
|
|
|
|
2020-01-25 04:52:48 +00:00
|
|
|
SOFT_FAIL_EXIT_CODE = 75
|
|
|
|
|
|
|
|
|
2022-05-23 22:27:54 +00:00
|
|
|
def interpolate_context(config_filename, hook_description, command, context):
|
2019-10-01 19:23:16 +00:00
|
|
|
'''
|
2022-05-23 22:27:54 +00:00
|
|
|
Given a config filename, a hook description, a single hook command, and a dict of context
|
|
|
|
names/values, interpolate the values by "{name}" into the command and return the result.
|
2019-10-01 19:23:16 +00:00
|
|
|
'''
|
|
|
|
for name, value in context.items():
|
2023-03-24 06:11:14 +00:00
|
|
|
command = command.replace(f'{{{name}}}', str(value))
|
2019-10-01 19:23:16 +00:00
|
|
|
|
2022-05-23 22:27:54 +00:00
|
|
|
for unsupported_variable in re.findall(r'{\w+}', command):
|
2022-05-24 22:50:04 +00:00
|
|
|
logger.warning(
|
2022-05-23 22:27:54 +00:00
|
|
|
f"{config_filename}: Variable '{unsupported_variable}' is not supported in {hook_description} hook"
|
|
|
|
)
|
|
|
|
|
2019-10-01 19:23:16 +00:00
|
|
|
return command
|
|
|
|
|
|
|
|
|
|
|
|
def execute_hook(commands, umask, config_filename, description, dry_run, **context):
|
2019-05-07 23:06:31 +00:00
|
|
|
'''
|
2019-06-14 00:05:26 +00:00
|
|
|
Given a list of hook commands to execute, a umask to execute with (or None), a config filename,
|
|
|
|
a hook description, and whether this is a dry run, run the given commands. Or, don't run them
|
|
|
|
if this is a dry run.
|
|
|
|
|
2022-05-23 17:59:56 +00:00
|
|
|
The context contains optional values interpolated by name into the hook commands.
|
2019-10-01 19:23:16 +00:00
|
|
|
|
2019-06-14 00:05:26 +00:00
|
|
|
Raise ValueError if the umask cannot be parsed.
|
2019-09-28 23:18:10 +00:00
|
|
|
Raise subprocesses.CalledProcessError if an error occurs in a hook.
|
2019-05-07 23:06:31 +00:00
|
|
|
'''
|
2017-10-26 05:32:06 +00:00
|
|
|
if not commands:
|
2023-03-24 06:11:14 +00:00
|
|
|
logger.debug(f'{config_filename}: No commands to run for {description} hook')
|
2017-10-26 05:32:06 +00:00
|
|
|
return
|
|
|
|
|
2019-05-07 23:06:31 +00:00
|
|
|
dry_run_label = ' (dry run; not actually running hooks)' if dry_run else ''
|
|
|
|
|
2019-10-01 19:23:16 +00:00
|
|
|
context['configuration_filename'] = config_filename
|
2022-05-23 22:27:54 +00:00
|
|
|
commands = [
|
|
|
|
interpolate_context(config_filename, description, command, context) for command in commands
|
|
|
|
]
|
2019-10-01 19:23:16 +00:00
|
|
|
|
2017-10-26 05:32:06 +00:00
|
|
|
if len(commands) == 1:
|
2023-03-24 06:11:14 +00:00
|
|
|
logger.info(f'{config_filename}: Running command for {description} hook{dry_run_label}')
|
2017-10-26 05:32:06 +00:00
|
|
|
else:
|
2018-09-30 05:45:00 +00:00
|
|
|
logger.info(
|
2023-03-24 06:11:14 +00:00
|
|
|
f'{config_filename}: Running {len(commands)} commands for {description} hook{dry_run_label}',
|
2018-09-30 05:45:00 +00:00
|
|
|
)
|
2017-10-26 05:32:06 +00:00
|
|
|
|
2019-06-14 00:05:26 +00:00
|
|
|
if umask:
|
|
|
|
parsed_umask = int(str(umask), 8)
|
2023-03-24 06:11:14 +00:00
|
|
|
logger.debug(f'{config_filename}: Set hook umask to {oct(parsed_umask)}')
|
2019-06-14 00:05:26 +00:00
|
|
|
original_umask = os.umask(parsed_umask)
|
|
|
|
else:
|
|
|
|
original_umask = None
|
|
|
|
|
|
|
|
try:
|
|
|
|
for command in commands:
|
|
|
|
if not dry_run:
|
|
|
|
execute.execute_command(
|
|
|
|
[command],
|
|
|
|
output_log_level=logging.ERROR
|
|
|
|
if description == 'on-error'
|
|
|
|
else logging.WARNING,
|
|
|
|
shell=True,
|
|
|
|
)
|
|
|
|
finally:
|
|
|
|
if original_umask:
|
|
|
|
os.umask(original_umask)
|
2020-01-25 04:52:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
def considered_soft_failure(config_filename, error):
|
|
|
|
'''
|
|
|
|
Given a configuration filename and an exception object, return whether the exception object
|
|
|
|
represents a subprocess.CalledProcessError with a return code of SOFT_FAIL_EXIT_CODE. If so,
|
|
|
|
that indicates that the error is a "soft failure", and should not result in an error.
|
|
|
|
'''
|
|
|
|
exit_code = getattr(error, 'returncode', None)
|
|
|
|
if exit_code is None:
|
|
|
|
return False
|
|
|
|
|
|
|
|
if exit_code == SOFT_FAIL_EXIT_CODE:
|
|
|
|
logger.info(
|
2023-03-24 06:11:14 +00:00
|
|
|
f'{config_filename}: Command hook exited with soft failure exit code ({SOFT_FAIL_EXIT_CODE}); skipping remaining actions',
|
2020-01-25 04:52:48 +00:00
|
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|