forked from msvsphere/leapp-repository
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
243 lines
8.3 KiB
10 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
|