def _init(): cfg = config.initUp2dateConfig() cfg_dict = dict(cfg.items()) server_url = config.getServerlURL() cfg_dict['proto'], cfg_dict['server_name'] = utils.parse_url(server_url[0], scheme="https")[:2] if len(server_url) > 1: cfg_dict['server_list'] = server_url local_config.init('rhncfg-client', defaults=cfg_dict) set_debug_level(int(local_config.get('debug_level') or 0)) set_logfile("/var/log/rhncfg-actions")
# FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 # along with this software; if not, see # http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. # # Red Hat trademarks are not licensed under GPLv2. No permission is # granted to use or replicate Red Hat trademarks that are incorporated # in this software or its documentation. # import os import fnmatch from actions import configfiles from config_common import local_config, rhn_log local_config.init("rhncfg") rhn_log.set_debug_level(local_config.get('debug')) def test(msg, test): ok_str = 'NOT OK' if test: ok_str = 'ok' print("%s: %s" % (msg, ok_str)) def stray_files(path): (directory, filename) = os.path.split(path) for path in os.listdir(directory): if fnmatch.fnmatch(path, "%s*" % filename):
def _init(): up2date_config = utils.get_up2date_config() local_config.init('rhncfg-client', defaults=up2date_config) set_debug_level(int(local_config.get('debug_level') or 0)) set_logfile("/var/log/rhncfg-actions")
# FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 # along with this software; if not, see # http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. # # Red Hat trademarks are not licensed under GPLv2. No permission is # granted to use or replicate Red Hat trademarks that are incorporated # in this software or its documentation. # import os import fnmatch from actions import configfiles from config_common import local_config, rhn_log local_config.init("rhncfg") rhn_log.set_debug_level(local_config.get('debug')) def test(msg, test): ok_str = 'NOT OK' if test: ok_str = 'ok' print "%s: %s" % (msg, ok_str) def stray_files(path): (directory, filename) = os.path.split(path) for path in os.listdir(directory): if fnmatch.fnmatch(path, "%s*" % filename): return 1 return 0
def run(action_id, params, cache_only=None): cfg = config.initUp2dateConfig() local_config.init('rhncfg-client', defaults=dict(cfg.items())) tempfile.tempdir = local_config.get('script_tmp_dir') logfile_name = local_config.get('script_log_file') log_output = local_config.get('script_log_file_enable') if log_output: # If we're going to log, make sure we can create the logfile _create_path(logfile_name) if cache_only: return (0, "no-ops for caching", {}) action_type = 'script.run' if not _local_permission_check(action_type): return _perm_error(action_type) extras = {'output': ''} script = params.get('script') if not script: return (1, "No script to execute", {}) username = params.get('username') groupname = params.get('groupname') if not username: return (1, "No username given to execute script as", {}) if not groupname: return (1, "No groupname given to execute script as", {}) timeout = params.get('timeout') if timeout: try: timeout = int(timeout) except ValueError: return (1, "Invalid timeout value", {}) else: timeout = None db_now = params.get('now') if not db_now: return (1, "'now' argument missing", {}) db_now = time.mktime(time.strptime(db_now, "%Y-%m-%d %H:%M:%S")) now = time.time() process_start = None process_end = None child_pid = None # determine uid/ugid for script ownership, uid also used for setuid... try: user_record = pwd.getpwnam(username) except KeyError: return 1, "No such user %s" % username, extras uid = user_record[2] ugid = user_record[3] # create the script on disk try: script_path = _create_script_file(script, uid=uid, gid=ugid) except OSError: e = sys.exc_info()[1] return 1, "Problem creating script file: %s" % e, extras # determine gid to run script as try: group_record = grp.getgrnam(groupname) except KeyError: return 1, "No such group %s" % groupname, extras run_as_gid = group_record[2] # create some pipes to communicate w/ the child process (pipe_read, pipe_write) = os.pipe() process_start = time.time() child_pid = os.fork() if not child_pid: # Parent doesn't write to child, so close that part os.close(pipe_read) # Redirect both stdout and stderr to the pipe os.dup2(pipe_write, sys.stdout.fileno()) os.dup2(pipe_write, sys.stderr.fileno()) # Close unnecessary file descriptors (including pipe since it's duped) for i in range(3, MAXFD): try: os.close(i) except: pass # all scripts initial working directory will be / # puts burden on script writer to ensure cwd is correct within the # script os.chdir('/') # the child process gets the desired uid/gid os.setgid(run_as_gid) groups = [ g.gr_gid for g in grp.getgrall() if username in g.gr_mem or username in g.gr_name ] os.setgroups(groups) os.setuid(uid) # give this its own process group (which happens to be equal to its # pid) os.setpgrp() # Finally, exec the script try: os.umask(int("022", 8)) os.execv(script_path, [ script_path, ]) finally: # This code can be reached only when script_path can not be # executed as otherwise execv never returns. # (The umask syscall always succeeds.) os._exit(1) # Parent doesn't write to child, so close that part os.close(pipe_write) output = None timed_out = None out_stream = tempfile.TemporaryFile() while 1: select_wait = None if timeout: elapsed = time.time() - process_start if elapsed >= timeout: timed_out = 1 # Send TERM to all processes in the child's process group # Send KILL after that, just to make sure the child died os.kill(-child_pid, signal.SIGTERM) time.sleep(2) os.kill(-child_pid, signal.SIGKILL) break select_wait = timeout - elapsed # XXX try-except here for interrupted system calls input_fds, output_fds, error_fds = select.select([pipe_read], [], [], select_wait) if error_fds: # when would this happen? os.close(pipe_read) return 1, "Fatal exceptional case", extras if not (pipe_read in input_fds): # Read timed out, should be caught in the next loop continue output = os.read(pipe_read, 4096) if not output: # End of file from the child break out_stream.write(output) os.close(pipe_read) # wait for the child to complete (somepid, exit_status) = os.waitpid(child_pid, 0) process_end = time.time() # Copy the output from the temporary file out_stream.seek(0, 0) extras['output'] = out_stream.read() out_stream.close() # Log script-output locally, unless we're asked not to if log_output: set_logfile(logfile_name) log_to_file(0, extras['output']) # since output can contain chars that won't make xmlrpc very happy, # base64 encode it... extras['base64enc'] = 1 extras['output'] = base64.encodestring(extras['output']) extras['return_code'] = exit_status # calculate start and end times in db's timespace extras['process_start'] = db_now + (process_start - now) extras['process_end'] = db_now + (process_end - now) for key in ('process_start', 'process_end'): extras[key] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(extras[key])) # clean up the script os.unlink(script_path) if timed_out: return 1, "Script killed, timeout of %s seconds exceeded" % timeout, extras if exit_status == 0: return 0, "Script executed", extras return 1, "Script failed", extras
def run(action_id, params, cache_only=None): cfg = config.initUp2dateConfig() local_config.init('rhncfg-client', defaults=dict(cfg.items())) tempfile.tempdir = local_config.get('script_tmp_dir') logfile_name = local_config.get('script_log_file') log_output = local_config.get('script_log_file_enable') if log_output: # If we're going to log, make sure we can create the logfile _create_path(logfile_name) if cache_only: return (0, "no-ops for caching", {}) action_type = 'script.run' if not _local_permission_check(action_type): return _perm_error(action_type) extras = {'output': ''} script = params.get('script') if not script: return (1, "No script to execute", {}) username = params.get('username') groupname = params.get('groupname') if not username: return (1, "No username given to execute script as", {}) if not groupname: return (1, "No groupname given to execute script as", {}) timeout = params.get('timeout') if timeout: try: timeout = int(timeout) except ValueError: return (1, "Invalid timeout value", {}) else: timeout = None db_now = params.get('now') if not db_now: return (1, "'now' argument missing", {}) db_now = time.mktime(time.strptime(db_now, "%Y-%m-%d %H:%M:%S")) now = time.time() process_start = None process_end = None child_pid = None # determine uid/ugid for script ownership, uid also used for setuid... try: user_record = pwd.getpwnam(username) except KeyError: return 1, "No such user %s" % username, extras uid = user_record[2] ugid = user_record[3] # create the script on disk try: script_path = _create_script_file(script, uid=uid, gid=ugid) except OSError, e: return 1, "Problem creating script file: %s" % e, extras
# # Copyright (c) 2008--2016 Red Hat, Inc. # # This software is licensed to you under the GNU General Public License, # version 2 (GPLv2). There is NO WARRANTY for this software, express or # implied, including the implied warranties of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 # along with this software; if not, see # http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. # # Red Hat trademarks are not licensed under GPLv2. No permission is # granted to use or replicate Red Hat trademarks that are incorporated # in this software or its documentation. # from actions import configfiles from config_common import local_config local_config.init('rhncfgcli') print(configfiles.upload("foo"))
def main(self): args = [] show_help = None debug_level = 3 mode = None dict_name_opt={'--server-name': None,'--password': None,'--username': None,} for index in range(1,len(sys.argv)): arg=sys.argv[index] param = [x for x in dict_name_opt.items() if x[1] == 0] if param: if arg.startswith('-') or arg in self.modes: # not perfect, but at least a little bit better print("Option %s requires an argument" % dict_name_opt[param[0][0]]) return 1 dict_name_opt[param[0][0]] = arg continue if arg in ('--help', '-h'): show_help = 1 continue param = [s for s in dict_name_opt.keys() if arg.startswith(s)] if param: rarg = arg[len(param[0]):] if not rarg: dict_name_opt[param[0]] = 0 if index == len(sys.argv) - 1: print("Option %s requires an argument" % param[0]) return 1 continue if rarg[0] == '=': if len(rarg) == 1: print("Option %s requires an argument" % param[0]) return 1 dict_name_opt[param[0]] = rarg[1:] continue print("Unknown option %s" % arg) return 1 if mode is None: # This should be the mode mode = arg if mode == '': # Bad self.usage(1) if mode[0] == '-': # Mode can't be an option self.usage(1) if mode not in self.modes: print("Unknown mode %s" % mode) self.usage(1) continue args.append(arg) server_name = dict_name_opt['--server-name'] password = dict_name_opt['--password'] username = dict_name_opt['--username'] rhn_log.set_debug_level(debug_level) if mode is None: # No args were specified self.usage(0) execname = os.path.basename(sys.argv[0]) # Class names cannot have dot in them, so strip the extension execname = execname.split('.', 1)[0] mode_module = mode.replace('-', '_') module_name = "%s_%s" % (self.mode_prefix, mode_module) full_module_name = "%s.%s" % (self.plugins_dir, module_name) try: module = __import__(full_module_name) except ImportError: e = sys.exc_info()[1] rhn_log.die(1, "Unable to load plugin for mode '%s': %s" % (mode, e)) module = getattr(module, module_name) if show_help: # Display the per-module help handler = module.Handler(args, None, mode=mode, exec_name=execname) handler.usage() return 0 cfg = config.initUp2dateConfig() up2date_cfg = dict(cfg.items()) if server_name: local_config.init(self.config_section, defaults=up2date_cfg, server_url="https://" + server_name) else: local_config.init(self.config_section, defaults=up2date_cfg) try: server_name = local_config.get('server_url') except InterpolationError: e = sys.exc_info()[1] if e.option == 'server_url': server_name = config.getServerlURL() up2date_cfg['proto'] = urlsplit(server_name[0])[0] if up2date_cfg['proto'] == '': up2date_cfg['proto'] = 'https' up2date_cfg['server_list'] = [urlsplit(x)[2] for x in server_name] else: up2date_cfg['server_list'] = [urlsplit(x)[1] for x in server_name] server_name = (up2date_cfg['server_list'])[0] local_config.init(self.config_section, defaults=up2date_cfg, server_name=server_name) print("Using server name %s" % server_name) # set the debug level through the config rhn_log.set_debug_level(int(local_config.get('debug_level') or 0)) rhn_log.set_logfile(local_config.get('logfile') or "/var/log/rhncfg") # Multi-level import - __import__("foo.bar") does not return the bar # module, but the foo module with bar loaded # XXX Error checking repo_class = local_config.get('repository_type') if repo_class is None: rhn_log.die(1, "repository_type not set (missing configuration file?)") repo_module_name = "%s.%s" % (self.plugins_dir, repo_class) try: repo_module = __import__(repo_module_name) except ImportError: e = sys.exc_info()[1] rhn_log.die(1, "Unable to load repository module: %s" % e) try: repo_module = getattr(repo_module, repo_class) except AttributeError: rhn_log.die(1, "Malformed repository module") try: repo = getattr(repo_module, self.repository_class_name)() except AttributeError: rhn_log.die(1, "Missing repository class") except InterpolationError: e = sys.exc_info()[1] if e.option == 'server_url': #pkilambi: bug#179367# backtic is replaced with single quote rhn_log.die(1, "Missing 'server_url' configuration variable; please refer to the config file") raise except cfg_exceptions.ConfigurationError: e = sys.exc_info()[1] rhn_log.die(e) except gaierror: e = sys.exc_info()[1] print("Socket Error: %s" % (e.args[1],)) sys.exit(1) handler = module.Handler(args, repo, mode=mode, exec_name=execname) try: try: handler.authenticate(username, password) handler.run() except cfg_exceptions.AuthenticationError: e = sys.exc_info()[1] rhn_log.die(1, "Authentication failed: %s" % e) except Exception: raise finally: repo.cleanup() return 0
def run(action_id, params, cache_only=None): cfg = config.initUp2dateConfig() local_config.init('rhncfg-client', defaults=dict(cfg.items())) tempfile.tempdir = local_config.get('script_tmp_dir') logfile_name = local_config.get('script_log_file') log_output = local_config.get('script_log_file_enable') if log_output: # If we're going to log, make sure we can create the logfile _create_path(logfile_name) if cache_only: return (0, "no-ops for caching", {}) action_type = 'script.run' if not _local_permission_check(action_type): return _perm_error(action_type) extras = {'output':''} script = params.get('script') if not script: return (1, "No script to execute", {}) username = params.get('username') groupname = params.get('groupname') if not username: return (1, "No username given to execute script as", {}) if not groupname: return (1, "No groupname given to execute script as", {}) timeout = params.get('timeout') if timeout: try: timeout = int(timeout) except ValueError: return (1, "Invalid timeout value", {}) else: timeout = None db_now = params.get('now') if not db_now: return (1, "'now' argument missing", {}) db_now = time.mktime(time.strptime(db_now, "%Y-%m-%d %H:%M:%S")) now = time.time() process_start = None process_end = None child_pid = None # determine uid/ugid for script ownership, uid also used for setuid... try: user_record = pwd.getpwnam(username) except KeyError: return 1, "No such user %s" % username, extras uid = user_record[2] ugid = user_record[3] # create the script on disk try: script_path = _create_script_file(script, uid=uid, gid=ugid) except OSError: e = sys.exc_info()[1] return 1, "Problem creating script file: %s" % e, extras # determine gid to run script as try: group_record = grp.getgrnam(groupname) except KeyError: return 1, "No such group %s" % groupname, extras run_as_gid = group_record[2] # create some pipes to communicate w/ the child process (pipe_read, pipe_write) = os.pipe() process_start = time.time() child_pid = os.fork() if not child_pid: # Parent doesn't write to child, so close that part os.close(pipe_read) # Redirect both stdout and stderr to the pipe os.dup2(pipe_write, sys.stdout.fileno()) os.dup2(pipe_write, sys.stderr.fileno()) # Close unnecessary file descriptors (including pipe since it's duped) for i in range(3, MAXFD): try: os.close(i) except: pass # all scripts initial working directory will be / # puts burden on script writer to ensure cwd is correct within the # script os.chdir('/') # the child process gets the desired uid/gid os.setgid(run_as_gid) groups=[g.gr_gid for g in grp.getgrall() if username in g.gr_mem or username in g.gr_name] os.setgroups(groups) os.setuid(uid) # give this its own process group (which happens to be equal to its # pid) os.setpgrp() clean_env = {"PATH": "/sbin:/bin:/usr/sbin:/usr/bin", "TERM": "xterm"} # Finally, exec the script try: os.umask(int("022", 8)) os.execve(script_path, [script_path, ], clean_env) finally: # This code can be reached only when script_path can not be # executed as otherwise execv never returns. # (The umask syscall always succeeds.) os._exit(1) # Parent doesn't write to child, so close that part os.close(pipe_write) output = None timed_out = None out_stream = open('/var/lib/up2date/action.%s' % str(action_id), 'ab+', 0) while 1: select_wait = None if timeout: elapsed = time.time() - process_start if elapsed >= timeout: timed_out = 1 # Send TERM to all processes in the child's process group # Send KILL after that, just to make sure the child died os.kill(-child_pid, signal.SIGTERM) time.sleep(2) os.kill(-child_pid, signal.SIGKILL) break select_wait = timeout - elapsed try: input_fds, output_fds, error_fds = select.select([pipe_read], [], [], select_wait) except select.error: return 255, "Termination signal occurred during execution.", {} if error_fds: # when would this happen? os.close(pipe_read) return 1, "Fatal exceptional case", extras if not (pipe_read in input_fds): # Read timed out, should be caught in the next loop continue output = os.read(pipe_read, 4096) if not output: # End of file from the child break out_stream.write(output) os.close(pipe_read) # wait for the child to complete (somepid, exit_status) = os.waitpid(child_pid, 0) process_end = time.time() # Copy the output from the temporary file out_stream.seek(0, 0) extras['output'] = out_stream.read() out_stream.close() # Log script-output locally, unless we're asked not to if log_output : set_logfile(logfile_name) log_to_file(0, extras['output']) # since output can contain chars that won't make xmlrpc very happy, # base64 encode it... extras['base64enc'] = 1 extras['output'] = base64.encodestring(extras['output']) extras['return_code'] = exit_status # calculate start and end times in db's timespace extras['process_start'] = db_now + (process_start - now) extras['process_end'] = db_now + (process_end - now) for key in ('process_start', 'process_end'): extras[key] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(extras[key])) # clean up the script os.unlink(script_path) if timed_out: return 1, "Script killed, timeout of %s seconds exceeded" % timeout, extras if exit_status == 0: return 0, "Script executed", extras return 1, "Script failed", extras
def run(action_id, params, cache_only=None): cfg = config.initUp2dateConfig() local_config.init('rhncfg-client', defaults=dict(cfg.items())) tempfile.tempdir = local_config.get('script_tmp_dir') logfile_name = local_config.get('script_log_file') log_output = local_config.get('script_log_file_enable') if log_output: # If we're going to log, make sure we can create the logfile _create_path(logfile_name) if cache_only: return (0, "no-ops for caching", {}) action_type = 'script.run' if not _local_permission_check(action_type): return _perm_error(action_type) extras = {'output':''} script = params.get('script') if not script: return (1, "No script to execute", {}) username = params.get('username') groupname = params.get('groupname') if not username: return (1, "No username given to execute script as", {}) if not groupname: return (1, "No groupname given to execute script as", {}) timeout = params.get('timeout') if timeout: try: timeout = int(timeout) except ValueError: return (1, "Invalid timeout value", {}) else: timeout = None db_now = params.get('now') if not db_now: return (1, "'now' argument missing", {}) db_now = time.mktime(time.strptime(db_now, "%Y-%m-%d %H:%M:%S")) now = time.time() process_start = None process_end = None child_pid = None # determine uid/ugid for script ownership, uid also used for setuid... try: user_record = pwd.getpwnam(username) except KeyError: return 1, "No such user %s" % username, extras uid = user_record[2] ugid = user_record[3] # create the script on disk try: script_path = _create_script_file(script, uid=uid, gid=ugid) except OSError, e: return 1, "Problem creating script file: %s" % e, extras
def main(self): args = [] show_help = None debug_level = 3 mode = None dict_name_opt = { '--server-name': None, '--password': None, '--username': None, '--config': None, } for index in range(1, len(sys.argv)): arg = sys.argv[index] param = [x for x in dict_name_opt.items() if x[1] == 0] if param: if arg.startswith('-') or arg in self.modes: # not perfect, but at least a little bit better print("Option %s requires an argument" % dict_name_opt[param[0][0]]) return 1 dict_name_opt[param[0][0]] = arg continue if arg in ('--help', '-h'): show_help = 1 continue param = [s for s in dict_name_opt.keys() if arg.startswith(s)] if param: rarg = arg[len(param[0]):] if not rarg: dict_name_opt[param[0]] = 0 if index == len(sys.argv) - 1: print("Option %s requires an argument" % param[0]) return 1 continue if rarg[0] == '=': if len(rarg) == 1: print("Option %s requires an argument" % param[0]) return 1 dict_name_opt[param[0]] = rarg[1:] continue print("Unknown option %s" % arg) return 1 if mode is None: # This should be the mode mode = arg if mode == '': # Bad self.usage(1) if mode[0] == '-': # Mode can't be an option self.usage(1) if mode not in self.modes: print("Unknown mode %s" % mode) self.usage(1) continue args.append(arg) server_name = dict_name_opt['--server-name'] password = dict_name_opt['--password'] username = dict_name_opt['--username'] config_file_override = dict_name_opt['--config'] if config_file_override and not os.path.isfile(config_file_override): rhn_log.die(1, "Config file %s does not exist.", config_file_override) rhn_log.set_debug_level(debug_level) if mode is None: # No args were specified self.usage(0) execname = os.path.basename(sys.argv[0]) # Class names cannot have dot in them, so strip the extension execname = execname.split('.', 1)[0] mode_module = mode.replace('-', '_') module_name = "%s_%s" % (self.mode_prefix, mode_module) full_module_name = "%s.%s" % (self.plugins_dir, module_name) try: module = __import__(full_module_name) except ImportError: e = sys.exc_info()[1] rhn_log.die(1, "Unable to load plugin for mode '%s': %s" % (mode, e)) module = getattr(module, module_name) if show_help: # Display the per-module help handler = module.Handler(args, None, mode=mode, exec_name=execname) handler.usage() return 0 cfg = config.initUp2dateConfig() up2date_cfg = dict(cfg.items()) if server_name: local_config.init(self.config_section, defaults=up2date_cfg, config_file_override=config_file_override, server_url="https://" + server_name) else: local_config.init(self.config_section, defaults=up2date_cfg, config_file_override=config_file_override) try: server_name = local_config.get('server_url') except InterpolationError: e = sys.exc_info()[1] if e.option == 'server_url': server_name = config.getServerlURL() up2date_cfg['proto'] = urlsplit(server_name[0])[0] if up2date_cfg['proto'] == '': up2date_cfg['proto'] = 'https' up2date_cfg['server_list'] = [ urlsplit(x)[2] for x in server_name ] else: up2date_cfg['server_list'] = [ urlsplit(x)[1] for x in server_name ] server_name = (up2date_cfg['server_list'])[0] local_config.init( self.config_section, defaults=up2date_cfg, config_file_override=config_file_override, server_name=server_name) print("Using server name %s" % server_name) # set the debug level through the config rhn_log.set_debug_level(int(local_config.get('debug_level') or 0)) rhn_log.set_logfile(local_config.get('logfile') or "/var/log/rhncfg") # Multi-level import - __import__("foo.bar") does not return the bar # module, but the foo module with bar loaded # XXX Error checking repo_class = local_config.get('repository_type') if repo_class is None: rhn_log.die( 1, "repository_type not set (missing configuration file?)") repo_module_name = "%s.%s" % (self.plugins_dir, repo_class) try: repo_module = __import__(repo_module_name) except ImportError: e = sys.exc_info()[1] rhn_log.die(1, "Unable to load repository module: %s" % e) try: repo_module = getattr(repo_module, repo_class) except AttributeError: rhn_log.die(1, "Malformed repository module") try: repo = getattr(repo_module, self.repository_class_name)() except AttributeError: rhn_log.die(1, "Missing repository class") except InterpolationError: e = sys.exc_info()[1] if e.option == 'server_url': #pkilambi: bug#179367# backtic is replaced with single quote rhn_log.die( 1, "Missing 'server_url' configuration variable; please refer to the config file" ) raise except cfg_exceptions.ConfigurationError: e = sys.exc_info()[1] rhn_log.die(e) except gaierror: e = sys.exc_info()[1] print("Socket Error: %s" % (e.args[1], )) sys.exit(1) handler = module.Handler(args, repo, mode=mode, exec_name=execname) try: try: handler.authenticate(username, password) handler.run() except cfg_exceptions.AuthenticationError: e = sys.exc_info()[1] rhn_log.die(1, "Authentication failed: %s" % e) except Exception: raise finally: repo.cleanup() return 0