From 6f55fbad448e7c99c0529106dbf28ff5f65e88b6 Mon Sep 17 00:00:00 2001 From: Vinzenz Feenstra Date: Tue, 12 May 2020 16:31:38 +0200 Subject: [PATCH] Add --actor-config to snactor run This patch introduces support for specifying a model which the actor should consume for configuration data. Using this feature requires to run an actor before which produces this model and saves the output. Additionally to facilitate the testing needs for this PR `snactor messages add` has been added, which allows injecting messages from the commandline. Jira-Task: OAMG-1092 Signed-off-by: Vinzenz Feenstra --- Makefile | 2 +- docs/source/el7toel8/actor-rhel7-to-rhel8.md | 19 ++++++- leapp/messaging/__init__.py | 4 +- leapp/snactor/commands/messages/__init__.py | 54 ++++++++++++++++++-- leapp/snactor/commands/run.py | 20 ++++---- leapp/utils/workarounds/mp.py | 2 +- tests/scripts/test_snactor.py | 40 +++++++++++++++ 7 files changed, 122 insertions(+), 19 deletions(-) diff --git a/Makefile b/Makefile index 24e710649..f746b1b98 100644 --- a/Makefile +++ b/Makefile @@ -122,7 +122,7 @@ container-test: docker run --rm -ti -v ${PWD}:/payload leapp-tests test: lint - pytest --cov-report term-missing --cov=leapp tests/scripts + pytest -vv --cov-report term-missing --cov=leapp tests/scripts lint: pytest --cache-clear --pylint -m pylint leapp tests/scripts/*.py diff --git a/docs/source/el7toel8/actor-rhel7-to-rhel8.md b/docs/source/el7toel8/actor-rhel7-to-rhel8.md index 19bf394cd..b51a2cbb2 100644 --- a/docs/source/el7toel8/actor-rhel7-to-rhel8.md +++ b/docs/source/el7toel8/actor-rhel7-to-rhel8.md @@ -269,7 +269,6 @@ reporting.RelatedResource('pam', 'pam_securetty') The related resources are especially useful when you have a lot of accompanied objects like files or directories by your report and you would like to present it to the user in a specific way. - ## Testing your new actor During development of your new actor, it is expected that you will test your work to verify that results match your expectations. You can do that by manually executing your actor, or writing tests on various levels (i.e unit tests, component tests, E2E tests). @@ -313,6 +312,22 @@ As you can see the actor is executed without errors. But, by default, snactor do Now we can see that the _OSReleaseCollector_ actor produced a message of the _OSReleaseFacts_ model, containing data like OS Release name and version. +### Executing a single actor that uses the workflow config + +If you need to execute an actor on its own that requires the `IPUConfig` model you can execute the actor with the +following command: + +```shell +snactor run --actor-config IPUConfig ActorName +``` + +In order for this to work you have to run the `IPUWorkflowConfig` actor before and save its output, so that the config +data is stored in the database for the current session: + +```shell +snactor run --save-output IPUWorkflowConfig +``` + ### Executing the whole upgrade workflow with the new actor Finally, you can make your actor part of the “leapp upgrade” process and check how it behaves when executed together with all the other actors in the workflow. Assuming that your new actor is tagged properly, being part of _IPUWorkflow_, and part of an existing phase, you can place it inside an existing leapp repository on a testing RHEL 7 system. All Leapp components (i.e actors, models, tags) placed inside **/etc/leapp/repos.d/system_upgrade/el7toel8/** will be used by the “leapp upgrade” command during upgrade process. @@ -435,4 +450,4 @@ When filing an issue, include: ## Where can I seek help? -We’ll gladly answer your questions and lead you to through any troubles with the actor development. You can reach us, the OS and Application Modernization Group, at freenode IRC server in channel __#leapp__. +We’ll gladly answer your questions and lead you to through any troubles with the actor development. You can reach us, the OS and Application Modernization Group, at freenode IRC server in channel **#leapp**. diff --git a/leapp/messaging/__init__.py b/leapp/messaging/__init__.py index 98a7b57b8..f25226483 100644 --- a/leapp/messaging/__init__.py +++ b/leapp/messaging/__init__.py @@ -9,10 +9,10 @@ from leapp.dialogs import RawMessageDialog from leapp.dialogs.renderer import CommandlineRenderer -from leapp.messaging.answerstore import AnswerStore from leapp.exceptions import CannotConsumeErrorMessages -from leapp.models import DialogModel, ErrorModel +from leapp.messaging.answerstore import AnswerStore from leapp.messaging.commands import WorkflowCommand +from leapp.models import DialogModel, ErrorModel from leapp.utils import get_api_models diff --git a/leapp/snactor/commands/messages/__init__.py b/leapp/snactor/commands/messages/__init__.py index dd5d00475..e202558ed 100644 --- a/leapp/snactor/commands/messages/__init__.py +++ b/leapp/snactor/commands/messages/__init__.py @@ -1,9 +1,17 @@ +import json import os +import sys +from importlib import import_module -from leapp.utils.clicmd import command -from leapp.utils.repository import requires_repository +from leapp.exceptions import CommandError, LeappError, ModelDefinitionError +from leapp.logger import configure_logger +from leapp.messaging.inprocess import InProcessMessaging +from leapp.models.fields import ModelViolationError +from leapp.repository.scan import find_and_scan_repositories from leapp.snactor.context import with_snactor_context -from leapp.utils.audit import get_connection, get_config +from leapp.utils.audit import get_config, get_connection +from leapp.utils.clicmd import command, command_arg, command_opt +from leapp.utils.repository import find_repository_basedir, requires_repository _MAIN_LONG_DESCRIPTION = ''' This group of commands are around managing messages stored in the @@ -29,8 +37,18 @@ def messages(args): # noqa; pylint: disable=unused-argument ''' +_ADD_LONG_DESCRIPTION = ''' +With this command messages in the current repository scope can be added. +This gives the developer control over the input data available to actors +run and developed in this repository. + +For more information please consider reading the documentation at: +https://red.ht/leapp-docs +''' + + @messages.command('clear', help='Deletes all messages from the current repository scope', - description=_CLEAR_LONG_DESCRIPTION) + description=_ADD_LONG_DESCRIPTION) @requires_repository @with_snactor_context def clear(args): # noqa; pylint: disable=unused-argument @@ -40,3 +58,31 @@ def clear(args): # noqa; pylint: disable=unused-argument )) with get_connection(None) as con: con.execute("DELETE FROM message WHERE context = ?", (os.environ["LEAPP_EXECUTION_ID"],)) + + +class Injector(object): + name = 'injector' + + +@messages.command('add', help='Adds a message to the current repository scope', + description=_CLEAR_LONG_DESCRIPTION) +@command_opt('model', short_name='m', help='Model to add') +@command_arg('data', help='Data to add in JSON format') +@requires_repository +@with_snactor_context +def add(args): # noqa; pylint: disable=unused-argument + basedir = find_repository_basedir('.') + repository = find_and_scan_repositories(basedir, include_locals=True) + try: + repository.load() + except LeappError as exc: + sys.stderr.write(exc.message) + sys.stderr.write('\n') + sys.exit(1) + try: + model = getattr(import_module('leapp.models'), args.model, None) + InProcessMessaging().produce(model.create(json.loads(args.data)), Injector()) + except ModelDefinitionError: + raise CommandError('No such model: {}'.format(args.model)) + except ModelViolationError as e: + raise CommandError('Invalid data format for model: {}\n => {}'.format(args.model, e.message)) diff --git a/leapp/snactor/commands/run.py b/leapp/snactor/commands/run.py index e47b80cfd..143d12b78 100644 --- a/leapp/snactor/commands/run.py +++ b/leapp/snactor/commands/run.py @@ -1,15 +1,15 @@ +from importlib import import_module import json import sys -from leapp.exceptions import LeappError, CommandError -from leapp.utils.clicmd import command, command_opt, command_arg -from leapp.utils.repository import requires_repository, find_repository_basedir +from leapp.exceptions import CommandError, LeappError from leapp.logger import configure_logger from leapp.messaging.inprocess import InProcessMessaging -from leapp.utils.output import report_errors, beautify_actor_exception from leapp.repository.scan import find_and_scan_repositories from leapp.snactor.context import with_snactor_context - +from leapp.utils.clicmd import command, command_arg, command_opt +from leapp.utils.output import beautify_actor_exception, report_errors +from leapp.utils.repository import find_repository_basedir, requires_repository _LONG_DESCRIPTION = ''' Runs the given actor as specified as `actor_name` in a testing environment. @@ -21,8 +21,9 @@ @command('run', help='Execute the given actor', description=_LONG_DESCRIPTION) @command_arg('actor-name') -@command_opt('--save-output', is_flag=True) -@command_opt('--print-output', is_flag=True) +@command_opt('--actor-config', help='Name of the workflow config model to use') +@command_opt('--save-output', is_flag=True, help='Save the produced messages by this actor.') +@command_opt('--print-output', is_flag=True, help='Print the produced messages by this actor.') @requires_repository @with_snactor_context def cli(args): @@ -39,13 +40,14 @@ def cli(args): actor = repository.lookup_actor(args.actor_name) if not actor: raise CommandError('Actor "{}" not found!'.format(args.actor_name)) - messaging = InProcessMessaging(stored=args.save_output) + config_model = getattr(import_module('leapp.models'), args.actor_config) if args.actor_config else None + messaging = InProcessMessaging(stored=args.save_output, config_model=config_model) messaging.load(actor.consumes) failure = False with beautify_actor_exception(): try: - actor(messaging=messaging, logger=actor_logger).run() + actor(messaging=messaging, logger=actor_logger, config_model=config_model).run() except BaseException: failure = True raise diff --git a/leapp/utils/workarounds/mp.py b/leapp/utils/workarounds/mp.py index 434b308c8..339323929 100644 --- a/leapp/utils/workarounds/mp.py +++ b/leapp/utils/workarounds/mp.py @@ -16,7 +16,7 @@ def __init__(self, *args, **kwargs): super(FixedFinalize, self).__init__(*args, **kwargs) self._pid = os.getpid() - def __call__(self, *args, **kwargs): # pylint: disable=signature-differs + def __call__(self, *args, **kwargs): # pylint: disable=signature-differs if self._pid != os.getpid(): return None return super(FixedFinalize, self).__call__(*args, **kwargs) diff --git a/tests/scripts/test_snactor.py b/tests/scripts/test_snactor.py index b241581a7..718ae2a95 100644 --- a/tests/scripts/test_snactor.py +++ b/tests/scripts/test_snactor.py @@ -131,6 +131,7 @@ def test_new_actor(repository_dir): with pytest.raises(CalledProcessError): check_call(['snactor', 'discover']) repository_dir.join('actors/test/actor.py').write(''' +from __future__ import print_function from leapp.actors import Actor from leapp.models import TestModel from leapp.tags import TestTag @@ -144,6 +145,8 @@ class Test(Actor): def process(self): self.produce(TestModel(value='Some testing value')) + if self._configuration: + print(self.configuration.value) ''') check_call(['snactor', 'discover']) @@ -211,6 +214,43 @@ def test_run_actor(repository_dir): check_call(['snactor', '--debug', 'run', '--print-output', '--save-output', 'Test']) +def test_actor_config(repository_dir): + with repository_dir.as_cwd(): + repository_dir.join('models/testconfig.py').write(''' +from leapp.models import Model, fields +from leapp.topics import TestTopic + + +class TestConfig(Model): + topic = TestTopic + value = fields.String(default='Test config value') +''') + check_call(['snactor', 'new-actor', 'ConfigTest']) + repository_dir.join('actors/configtest/actor.py').write(''' +from __future__ import print_function +from leapp.actors import Actor +from leapp.models import TestConfig +from leapp.tags import TestTag + +class ConfigTest(Actor): + name = 'test' + description = 'No description has been provided for the test actor.' + consumes = (TestConfig,) + produces = () + tags = (TestTag,) + + def process(self): + print(self.configuration.value) +''') + with pytest.raises(CalledProcessError): + # Raises if actor-config is not specified + check_call(['snactor', 'run', 'ConfigTest']) + # Inject message + check_call(['snactor', 'messages', 'add', '-m', 'TestConfig', '{"value": "ConfigTest calls"}']) + # Ensure the injected data is now available to the actor + check_call(['snactor', 'run', '--actor-config', 'TestConfig', 'Test']) + + def test_clear_messages(repository_dir): with repository_dir.as_cwd(): check_call(['snactor', 'messages', 'clear'])