You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

243 lines
8.3 KiB

9 months ago
import functools
import os
import sys
import six
from leapp.exceptions import StopActorExecutionError
from leapp.libraries.common import mounting
from leapp.libraries.stdlib import api, CalledProcessError, config, run, STDOUT
from leapp.utils.deprecation import deprecated
def parse_config(cfg=None, strict=True):
"""
Applies a workaround to parse a config file using py3 AND py2
ConfigParser has a new def to read strings/iles in Py3, making
the old ones (Py2) obsoletes, these function was created to use the
ConfigParser on Py2 and Py3
:type cfg: str
:type strict: bool
"""
if six.PY3:
parser = six.moves.configparser.ConfigParser(strict=strict) # pylint: disable=unexpected-keyword-arg
else:
parser = six.moves.configparser.ConfigParser()
# we do not handle exception here, handle with it when these function is called
if cfg and six.PY3:
# Python 3
if isinstance(cfg, six.string_types):
parser.read_string(cfg)
else:
parser.read_file(cfg)
elif cfg:
# Python 2
from cStringIO import StringIO # pylint: disable=import-outside-toplevel
if isinstance(cfg, six.string_types):
parser.readfp(StringIO(cfg)) # pylint: disable=deprecated-method
else:
parser.readfp(cfg) # pylint: disable=deprecated-method
return parser
def makedirs(path, mode=0o777, exists_ok=True):
mounting._makedirs(path=path, mode=mode, exists_ok=exists_ok)
@deprecated(since='2022-02-03', message=(
'The "apply_yum_workaround" function has been deprecated, use "DNFWorkaround" '
'message as used in the successive "RegisterYumAdjustment" actor.'
)
)
def apply_yum_workaround(context=None):
"""
Applies a workaround on the system to allow the upgrade to succeed for yum/dnf.
"""
yum_script_path = api.get_tool_path('handleyumconfig')
if not yum_script_path:
raise StopActorExecutionError(
message='Failed to find mandatory script to apply',
details=reinstall_leapp_repository_hint()
)
cmd = ['/bin/bash', '-c', yum_script_path]
try:
context = context or mounting.NotIsolatedActions(base_dir='/')
context.call(cmd)
except OSError as e:
raise StopActorExecutionError(
message='Failed to execute script to apply yum adjustment. Message: {}'.format(str(e))
)
except CalledProcessError as e:
raise StopActorExecutionError(
message='Failed to apply yum adjustment. Message: {}'.format(str(e))
)
def logging_handler(fd_info, buf):
"""
Custom log handler to always show stdout to console and stderr only in DEBUG mode
"""
(_unused, fd_type) = fd_info
if fd_type != STDOUT and not config.is_debug():
return
target = sys.stdout if fd_type == STDOUT else sys.stderr
if sys.version_info > (3, 0):
os.writev(target.fileno(), [buf])
else:
target.write(buf)
def reinstall_leapp_repository_hint():
"""
Convenience function for creating a detail for StopActorExecutionError with a hint to reinstall the
leapp-repository package
"""
return {
'hint': 'Try to reinstall the `leapp-repository` package.'
}
def report_and_ignore_shutil_rmtree_error(func, path, exc_info):
"""
Helper function for shutil.rmtree to only report errors but don't fail.
"""
api.current_logger().warning(
'While trying to remove directories: %s failed at %s with an exception %s message: %s',
func.__name__, path, exc_info[0].__name__, exc_info[1]
)
def call_with_oserror_handled(cmd):
"""
Perform run with already handled OSError for some convenience.
"""
try:
run(cmd)
except OSError as e:
if cmd:
raise StopActorExecutionError(
message=str(e),
details={
'hint': 'Please ensure that {} is installed and executable.'.format(cmd[0])
}
)
raise StopActorExecutionError(
message='Failed to execute command {} with: {}'.format(''.join(cmd), str(e))
)
def call_with_failure_hint(cmd, hint):
"""
Perform `run` which handles OSError through call_with_oserror_handled and transforms CalledProcessError to a
StopActorExecutionError with a hint given by the caller.
"""
try:
call_with_oserror_handled(cmd)
except CalledProcessError as e:
raise StopActorExecutionError(
message='Failed to execute command `{}`. Error: {}'.format(' '.join(cmd), str(e)),
details={hint: hint}
)
def clean_guard(cleanup_function):
"""
Decorator to handle any exception going through and running cleanup tasks through the given cleanup_function
parameter.
"""
def clean_wrapper(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
try:
return f(*args, **kwargs)
except Exception: # Broad exception handler to handle all cases but rethrows
api.current_logger().debug('Clean guard caught an exception - Calling cleanup function.')
try:
cleanup_function(*args, **kwargs)
except Exception: # pylint: disable=broad-except
# Broad exception handler to handle all cases however, swallowed, to avoid losing the original
# error. Logging for debuggability.
api.current_logger().warning('Caught and swallowed an exception during cleanup.', exc_info=True)
raise # rethrow original exception
return wrapper
return clean_wrapper
def read_file(path):
"""
Reads the file specified by path in text mode and returns the contents.
"""
with open(path, 'r') as f:
return f.read()
def _require_exactly_one_message_of_type(model_class, error_callback=None):
"""
Consume and return exactly one message of the given type, error if there are none or more than one available.
Calls ``error_callback`` if there are none or more than one messages available of the requested type
with a string describing the error condition.
Note: this function is private, experimental and will likely be subject to change.
:param model_class: Message type to consume
:param Callable[[str], None] error_callback: Callback to call when error condition arises, e.g., raising the
StopActorExecutionError (default).
"""
def default_callback(msg):
raise StopActorExecutionError(msg)
if not error_callback:
error_callback = default_callback
model_instances = api.consume(model_class)
model_instance = next(model_instances, None)
if not model_instance:
msg = 'Exactly one {cls_name} message of type is required, however, none was received.'
msg = msg.format(cls_name=model_class.__name__)
error_callback(msg)
next_instance = next(model_instances, None)
if next_instance:
msg = 'Exactly one {cls_name} message is required, however, more than one messages were received.'
msg = msg.format(cls_name=model_class.__name__)
error_callback(msg)
return model_instance
def _require_some_message_of_type(model_class, error_callback=None):
"""
Consume and return one message of the given type, error if there are no messages available.
Calls ``error_callback`` if there are no messages available of the requested type
with a string describing the error condition.
Note: this function is private, experimental and will likely be subject to change.
:param model_class: Message type to consume
:param Callable[[str], None] error_callback: Callback to call when error condition arises, e.g., raising the
StopActorExecutionError (default).
"""
def default_callback(msg):
raise StopActorExecutionError(msg)
if not error_callback:
error_callback = default_callback
model_instances = api.consume(model_class)
model_instance = next(model_instances, None)
if not model_instance:
msg = 'Exactly one {cls_name} message of type is required, however, none was received.'
msg = msg.format(cls_name=model_class.__name__)
error_callback(msg)
return model_instance