1import logging 2import os 3 4from borgmatic import execute 5 6logger = logging.getLogger(__name__) 7 8 9SOFT_FAIL_EXIT_CODE = 75 10 11 12def interpolate_context(command, context): 13 ''' 14 Given a single hook command and a dict of context names/values, interpolate the values by 15 "{name}" into the command and return the result. 16 ''' 17 for name, value in context.items(): 18 command = command.replace('{%s}' % name, str(value)) 19 20 return command 21 22 23def execute_hook(commands, umask, config_filename, description, dry_run, **context): 24 ''' 25 Given a list of hook commands to execute, a umask to execute with (or None), a config filename, 26 a hook description, and whether this is a dry run, run the given commands. Or, don't run them 27 if this is a dry run. 28 29 The context contains optional values interpolated by name into the hook commands. Currently, 30 this only applies to the on_error hook. 31 32 Raise ValueError if the umask cannot be parsed. 33 Raise subprocesses.CalledProcessError if an error occurs in a hook. 34 ''' 35 if not commands: 36 logger.debug('{}: No commands to run for {} hook'.format(config_filename, description)) 37 return 38 39 dry_run_label = ' (dry run; not actually running hooks)' if dry_run else '' 40 41 context['configuration_filename'] = config_filename 42 commands = [interpolate_context(command, context) for command in commands] 43 44 if len(commands) == 1: 45 logger.info( 46 '{}: Running command for {} hook{}'.format(config_filename, description, dry_run_label) 47 ) 48 else: 49 logger.info( 50 '{}: Running {} commands for {} hook{}'.format( 51 config_filename, len(commands), description, dry_run_label 52 ) 53 ) 54 55 if umask: 56 parsed_umask = int(str(umask), 8) 57 logger.debug('{}: Set hook umask to {}'.format(config_filename, oct(parsed_umask))) 58 original_umask = os.umask(parsed_umask) 59 else: 60 original_umask = None 61 62 try: 63 for command in commands: 64 if not dry_run: 65 execute.execute_command( 66 [command], 67 output_log_level=logging.ERROR 68 if description == 'on-error' 69 else logging.WARNING, 70 shell=True, 71 ) 72 finally: 73 if original_umask: 74 os.umask(original_umask) 75 76 77def considered_soft_failure(config_filename, error): 78 ''' 79 Given a configuration filename and an exception object, return whether the exception object 80 represents a subprocess.CalledProcessError with a return code of SOFT_FAIL_EXIT_CODE. If so, 81 that indicates that the error is a "soft failure", and should not result in an error. 82 ''' 83 exit_code = getattr(error, 'returncode', None) 84 if exit_code is None: 85 return False 86 87 if exit_code == SOFT_FAIL_EXIT_CODE: 88 logger.info( 89 '{}: Command hook exited with soft failure exit code ({}); skipping remaining actions'.format( 90 config_filename, SOFT_FAIL_EXIT_CODE 91 ) 92 ) 93 return True 94 95 return False 96