Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend LEAPP with actor configuration #870

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,27 @@ jobs:
- name: Run unit tests with python3.12 on el9
python: python3.12
container: ubi10
- name: Run python linters with python3.12 on el9
python: python3.12
container: ubi10-lint
- name: Run unit tests with python3.9 on el9
python: python3.9
container: ubi9
- name: Run python linters with python3.9 on el9
python: python3.9
container: ubi9-lint
- name: Run unit tests with python 3.6 on el8
python: python3.6
container: ubi8
- name: Run python linters with python 3.6 on el8
python: python3.6
container: ubi8-lint
- name: Run unit tests with python2.7 on el7
python: python2.7
container: ubi7
- name: Run python linters with python2.7 on el7
python: python2.7
container: ubi7-lint

steps:
- name: Checkout code
Expand Down
5 changes: 4 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ disable=
too-few-public-methods,
too-many-ancestors,
too-many-branches,
too-many-lines,
too-many-locals,
too-many-positional-arguments,
too-many-public-methods,
too-many-statements,
print-statement,
Expand All @@ -42,7 +44,8 @@ disable=
no-absolute-import, # XXX FIXME nice to have one day
unspecified-encoding, # XXX FIXME May be a good thing to have one day though
deprecated-class, # We still have < 3.9 to support
use-dict-literal
use-dict-literal,
use-yield-from

[FORMAT]
# Maximum number of characters on a single line.
Expand Down
2 changes: 2 additions & 0 deletions etc/leapp/leapp.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ repo_path=/etc/leapp/repos.d/
[database]
path=/var/lib/leapp/leapp.db

[actor_config]
path=/etc/leapp/actor_conf.d/
78 changes: 46 additions & 32 deletions leapp/actors/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import functools
import logging
import os
import sys

try:
# Python 3.3+
from collections.abc import Sequence
except ImportError:
# Python 2.6 through 3.2
from collections import Sequence

from leapp.actors.config import Config, retrieve_config
from leapp.compat import string_types
from leapp.dialogs import Dialog
from leapp.exceptions import (MissingActorAttributeError, RequestStopAfterPhase, StopActorExecution,
Expand Down Expand Up @@ -41,6 +50,11 @@ class Actor(object):
Write the actor's description as a docstring.
"""

config_schemas = ()
"""
Defines the structure of the configuration that the actor uses.
"""

consumes = ()
"""
Tuple of :py:class:`leapp.models.Model` derived classes defined in the :ref:`repositories <terminology:repository>`
Expand Down Expand Up @@ -86,6 +100,7 @@ def serialize(self):
'path': os.path.dirname(sys.modules[type(self).__module__].__file__),
'class_name': type(self).__name__,
'description': self.description or type(self).__doc__,
'config_schemas': [c.__name__ for c in self.config_schemas],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am thinking here, whether we only add the name of the config or a whole serialized representation as we do with dialogs. In my head c.serialize() makes more sense, however it is true that the schema is an entity that can be shared across actors opposed to dialog which is always specific to the given actor, so doing it this way has merit also. What do you think? @abadger @pirat89

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we keep it like this, this also brings to my attention the issue where we store config information in leappdb. Is config part of actor metadata? I argue that the config scheme (the name) is actor metadata, however the schema itself is not. Maybe we should create another table and store the schema information there instead of in metadata.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether name only or a serialization of the schema class is appropriate here. @pirat89 ?

I'm not sure what leappdb is used for either: Would we want to store both the config_schema in it and the actual actor config settings? Would we want to store just the config settings from config files or would we want to store the config dictionary that has been expanded to include default values as well?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason we would like to have this information in the database is for debugging and troubleshooting purposes (using for example leapp-inspector). My idea is that we would like to have a snapshot of the leapp configuration just as the user sees it. This means that in case something goes wrong for the user we can see what is the state of leapp when he provides the leapp.db file.

So I personally would store both the entire schema, so we can see the setup of the configuration as well as the actual values provided by the user. The format in which this is stored does not matters from my point of view only in terms of how easily we can work with it later when this information is retrieved from processing by other tools (such as the leapp-inspector I mentioned)

'consumes': [c.__name__ for c in self.consumes],
'produces': [p.__name__ for p in self.produces],
'tags': [t.__name__ for t in self.tags],
Expand All @@ -100,15 +115,20 @@ def __init__(self, messaging=None, logger=None, config_model=None, skip_dialogs=
This depends on the definition of such a configuration model being defined by the workflow
and an actor that provides such a message.
"""

Actor.current_instance = self
install_translation_for_actor(type(self))
self._messaging = messaging
self.log = (logger or logging.getLogger('leapp.actors')).getChild(self.name)
self.skip_dialogs = skip_dialogs
""" A configured logger instance for the current actor. """

# self._configuration is the workflow configuration.
# self.config_schemas is the actor defined configuration.
# self.config is the actual actor configuration
if config_model:
self._configuration = next(self.consume(config_model), None)
self.config = retrieve_config(self.config_schemas)

self._path = path

Expand Down Expand Up @@ -359,6 +379,15 @@ def report_error(self, message, severity=ErrorSeverity.ERROR, details=None):
actor=self,
details=details)

def retrieve_config(self):
"""
Retrieve the configuration described by self.config_schema.

:return: Dictionary containing requested configuration.
:rtype: dict
"""
return retrieve_config(self.config_schema)


def _is_type(value_type):
def validate(actor, name, value):
Expand Down Expand Up @@ -390,17 +419,23 @@ def _lint_warn(actor, name, type_name):
logging.getLogger("leapp.linter").warning("Actor %s field %s should be a tuple of %s", actor, name, type_name)


def _is_model_tuple(actor, name, value):
if isinstance(value, type) and issubclass(value, Model):
_lint_warn(actor, name, "Models")
def _is_foo_sequence(cls, cls_name, actor, name, value):
if isinstance(value, type) and issubclass(value, cls):
_lint_warn(actor, name, cls_name)
value = (value,)
_is_type(tuple)(actor, name, value)
if not all([True] + [isinstance(item, type) and issubclass(item, Model) for item in value]):
_is_type(Sequence)(actor, name, value)
if not all(isinstance(item, type) and issubclass(item, cls) for item in value):
raise WrongAttributeTypeError(
'Actor {} attribute {} should contain only Models'.format(actor, name))
'Actor {} attribute {} should contain only {}'.format(actor, name, cls_name))
return value


_is_config_sequence = functools.partial(_is_foo_sequence, Config, "Configs")
_is_model_sequence = functools.partial(_is_foo_sequence, Model, "Models")
_is_tag_sequence = functools.partial(_is_foo_sequence, Tag, "Tags")
_is_api_sequence = functools.partial(_is_foo_sequence, WorkflowAPI, "WorkflowAPIs")


def _is_dialog_tuple(actor, name, value):
if isinstance(value, Dialog):
_lint_warn(actor, name, "Dialogs")
Expand All @@ -412,28 +447,6 @@ def _is_dialog_tuple(actor, name, value):
return value


def _is_tag_tuple(actor, name, value):
if isinstance(value, type) and issubclass(value, Tag):
_lint_warn(actor, name, "Tags")
value = (value,)
_is_type(tuple)(actor, name, value)
if not all([True] + [isinstance(item, type) and issubclass(item, Tag) for item in value]):
raise WrongAttributeTypeError(
'Actor {} attribute {} should contain only Tags'.format(actor, name))
return value


def _is_api_tuple(actor, name, value):
if isinstance(value, type) and issubclass(value, WorkflowAPI):
_lint_warn(actor, name, "Apis")
value = (value,)
_is_type(tuple)(actor, name, value)
if not all([True] + [isinstance(item, type) and issubclass(item, WorkflowAPI) for item in value]):
raise WrongAttributeTypeError(
'Actor {} attribute {} should contain only WorkflowAPIs'.format(actor, name))
return value


def _get_attribute(actor, name, validator, required=False, default_value=None, additional_info='', resolve=None):
if resolve:
value = resolve(actor, name)
Expand Down Expand Up @@ -464,13 +477,14 @@ def get_actor_metadata(actor):
# # if path is not transformed into the realpath.
('path', os.path.dirname(os.path.realpath(sys.modules[actor.__module__].__file__))),
_get_attribute(actor, 'name', _is_type(string_types), required=True),
_get_attribute(actor, 'tags', _is_tag_tuple, required=True, additional_info=additional_tag_info),
_get_attribute(actor, 'consumes', _is_model_tuple, required=False, default_value=(), resolve=get_api_models),
_get_attribute(actor, 'produces', _is_model_tuple, required=False, default_value=(), resolve=get_api_models),
_get_attribute(actor, 'tags', _is_tag_sequence, required=True, additional_info=additional_tag_info),
_get_attribute(actor, 'consumes', _is_model_sequence, required=False, default_value=(), resolve=get_api_models),
_get_attribute(actor, 'produces', _is_model_sequence, required=False, default_value=(), resolve=get_api_models),
_get_attribute(actor, 'dialogs', _is_dialog_tuple, required=False, default_value=()),
_get_attribute(actor, 'description', _is_type(string_types), required=False,
default_value=actor.__doc__ or 'There has been no description provided for this actor.'),
_get_attribute(actor, 'apis', _is_api_tuple, required=False, default_value=())
_get_attribute(actor, 'config_schemas', _is_config_sequence, required=False, default_value=()),
_get_attribute(actor, 'apis', _is_api_sequence, required=False, default_value=())
])


Expand Down
Loading