1be26899be
- Fix broken leapp db queries on rerun - Port all code to be Python-3.12 compatible. - Resolves: RHEL-40363
1336 lines
49 KiB
Diff
1336 lines
49 KiB
Diff
From 91b309ac8c4cf6a709694144cb1e451ff1fa72f7 Mon Sep 17 00:00:00 2001
|
|
From: David Kubek <dkubek@redhat.com>
|
|
Date: Mon, 13 May 2024 18:07:26 +0200
|
|
Subject: [PATCH 07/23] Extend information from leapp saved to leappdb (#847)
|
|
|
|
This commit combines multiple additions to the leapp database.
|
|
|
|
First addition is tracking of entity metadata. The `Metadata` model
|
|
stores the metadata of entities such as `Actor` or `Workflow`. This data
|
|
is stored in a new table `metadata` of the `leapp.db` file.
|
|
|
|
1. metadata of *discovered* actors.
|
|
|
|
For an actor, the metadata stored contain:
|
|
|
|
`class_name` - the name of the actor class
|
|
`name` - the name given to the actor
|
|
`description` - the actor's description
|
|
`phase` - phase of execution of the actor
|
|
`tags` - names of any tags associated with an actor
|
|
`consumes` - list of all messages the actor consumes
|
|
`produces` - list of all messages the actor produces
|
|
`path` - the path to the actor source file
|
|
|
|
2. workflow metadata.
|
|
|
|
For a workflow, the metadata stored contain:
|
|
|
|
`name` - name of the workflow
|
|
`short_name` - short name of the workflow
|
|
`tag` - workflow tag
|
|
`description` - workflow description
|
|
`phases` - all phases associated with the workflow
|
|
|
|
Next addition is tracking of dialog question. Previously leapp was not
|
|
able to detect the actual question asked from the user as it could be
|
|
generated dynamically when actor is called and depend on the
|
|
configuration of the user's system.
|
|
|
|
Last addition includes storing the actor exit status. Exit status is now
|
|
saved as an audit event `actor-exit-status`. Exit status 0 represents
|
|
successful execution or `StopActorExecution`/`StopActorExecutionError`,
|
|
while 1 indicates an unexpected and unhandled exception.
|
|
|
|
These changes collectively improve the metadata handling capabilities
|
|
of, ensuring accurate storage and retrieval of essential information for
|
|
various entities.
|
|
|
|
Jira: OAMG-8402
|
|
---
|
|
leapp/actors/__init__.py | 13 +-
|
|
leapp/dialogs/dialog.py | 1 +
|
|
leapp/messaging/answerstore.py | 3 +
|
|
leapp/utils/audit/__init__.py | 199 +++++++++++++++-
|
|
leapp/utils/audit/contextclone.py | 22 ++
|
|
leapp/workflows/__init__.py | 22 +-
|
|
res/schema/audit-layout.sql | 26 +-
|
|
.../0002-add-metadata-dialog-tables.sql | 27 +++
|
|
tests/data/leappdb-tests/.leapp/info | 1 +
|
|
tests/data/leappdb-tests/.leapp/leapp.conf | 6 +
|
|
.../actors/configprovider/actor.py | 17 ++
|
|
.../leappdb-tests/actors/dialogactor/actor.py | 36 +++
|
|
.../actors/exitstatusactor/actor.py | 31 +++
|
|
.../leappdb-tests/libraries/test_helper.py | 7 +
|
|
.../leappdb-tests/models/unittestconfig.py | 7 +
|
|
tests/data/leappdb-tests/tags/firstphase.py | 5 +
|
|
tests/data/leappdb-tests/tags/secondphase.py | 5 +
|
|
.../leappdb-tests/tags/unittestworkflow.py | 5 +
|
|
tests/data/leappdb-tests/topics/config.py | 5 +
|
|
.../data/leappdb-tests/workflows/unit_test.py | 37 +++
|
|
tests/scripts/test_actor_api.py | 6 +
|
|
tests/scripts/test_dialog_db.py | 186 +++++++++++++++
|
|
tests/scripts/test_exit_status.py | 68 ++++++
|
|
tests/scripts/test_metadata.py | 223 ++++++++++++++++++
|
|
24 files changed, 949 insertions(+), 9 deletions(-)
|
|
create mode 100644 res/schema/migrations/0002-add-metadata-dialog-tables.sql
|
|
create mode 100644 tests/data/leappdb-tests/.leapp/info
|
|
create mode 100644 tests/data/leappdb-tests/.leapp/leapp.conf
|
|
create mode 100644 tests/data/leappdb-tests/actors/configprovider/actor.py
|
|
create mode 100644 tests/data/leappdb-tests/actors/dialogactor/actor.py
|
|
create mode 100644 tests/data/leappdb-tests/actors/exitstatusactor/actor.py
|
|
create mode 100644 tests/data/leappdb-tests/libraries/test_helper.py
|
|
create mode 100644 tests/data/leappdb-tests/models/unittestconfig.py
|
|
create mode 100644 tests/data/leappdb-tests/tags/firstphase.py
|
|
create mode 100644 tests/data/leappdb-tests/tags/secondphase.py
|
|
create mode 100644 tests/data/leappdb-tests/tags/unittestworkflow.py
|
|
create mode 100644 tests/data/leappdb-tests/topics/config.py
|
|
create mode 100644 tests/data/leappdb-tests/workflows/unit_test.py
|
|
create mode 100644 tests/scripts/test_dialog_db.py
|
|
create mode 100644 tests/scripts/test_exit_status.py
|
|
create mode 100644 tests/scripts/test_metadata.py
|
|
|
|
diff --git a/leapp/actors/__init__.py b/leapp/actors/__init__.py
|
|
index 7ae18ea..9d83bf1 100644
|
|
--- a/leapp/actors/__init__.py
|
|
+++ b/leapp/actors/__init__.py
|
|
@@ -10,6 +10,7 @@ from leapp.models import DialogModel, Model
|
|
from leapp.models.error_severity import ErrorSeverity
|
|
from leapp.tags import Tag
|
|
from leapp.utils import get_api_models, path
|
|
+from leapp.utils.audit import store_dialog
|
|
from leapp.utils.i18n import install_translation_for_actor
|
|
from leapp.utils.meta import get_flattened_subclasses
|
|
from leapp.workflows.api import WorkflowAPI
|
|
@@ -122,12 +123,17 @@ class Actor(object):
|
|
:return: dictionary with the requested answers, None if not a defined dialog
|
|
"""
|
|
self._messaging.register_dialog(dialog, self)
|
|
+ answer = None
|
|
if dialog in type(self).dialogs:
|
|
if self.skip_dialogs:
|
|
# non-interactive mode of operation
|
|
- return self._messaging.get_answers(dialog)
|
|
- return self._messaging.request_answers(dialog)
|
|
- return None
|
|
+ answer = self._messaging.get_answers(dialog)
|
|
+ else:
|
|
+ answer = self._messaging.request_answers(dialog)
|
|
+
|
|
+ store_dialog(dialog, answer)
|
|
+
|
|
+ return answer
|
|
|
|
def show_message(self, message):
|
|
"""
|
|
@@ -285,6 +291,7 @@ class Actor(object):
|
|
def run(self, *args):
|
|
""" Runs the actor calling the method :py:func:`process`. """
|
|
os.environ['LEAPP_CURRENT_ACTOR'] = self.name
|
|
+
|
|
try:
|
|
self.process(*args)
|
|
except StopActorExecution:
|
|
diff --git a/leapp/dialogs/dialog.py b/leapp/dialogs/dialog.py
|
|
index 3ead810..320be1b 100644
|
|
--- a/leapp/dialogs/dialog.py
|
|
+++ b/leapp/dialogs/dialog.py
|
|
@@ -114,4 +114,5 @@ class Dialog(object):
|
|
self._store = store
|
|
renderer.render(self)
|
|
self._store = None
|
|
+
|
|
return store.get(self.scope, {})
|
|
diff --git a/leapp/messaging/answerstore.py b/leapp/messaging/answerstore.py
|
|
index 3e55e8a..b2c707d 100644
|
|
--- a/leapp/messaging/answerstore.py
|
|
+++ b/leapp/messaging/answerstore.py
|
|
@@ -117,6 +117,9 @@ class AnswerStore(object):
|
|
# NOTE(ivasilev) self.storage.get() will return a DictProxy. To avoid TypeError during later
|
|
# JSON serialization a copy() should be invoked to get a shallow copy of data
|
|
answer = self._storage.get(scope, fallback).copy()
|
|
+
|
|
+ # NOTE(dkubek): It is possible that we do not need to save the 'answer'
|
|
+ # here as it is being stored with dialog question right after query
|
|
create_audit_entry('dialog-answer', {'scope': scope, 'fallback': fallback, 'answer': answer})
|
|
return answer
|
|
|
|
diff --git a/leapp/utils/audit/__init__.py b/leapp/utils/audit/__init__.py
|
|
index 6b00413..16db107 100644
|
|
--- a/leapp/utils/audit/__init__.py
|
|
+++ b/leapp/utils/audit/__init__.py
|
|
@@ -3,6 +3,7 @@ import datetime
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
+import hashlib
|
|
|
|
from leapp.config import get_config
|
|
from leapp.compat import string_types
|
|
@@ -221,6 +222,72 @@ class DataSource(Host):
|
|
self._data_source_id = cursor.fetchone()[0]
|
|
|
|
|
|
+class Metadata(Storable):
|
|
+ """
|
|
+ Metadata of an Entity
|
|
+ """
|
|
+
|
|
+ def __init__(self, metadata=None, hash_id=None):
|
|
+ """
|
|
+ :param metadata: Entity metadata
|
|
+ :type metadata: str
|
|
+ :param hash_id: SHA256 hash in hexadecimal representation of data
|
|
+ :type hash_id: str
|
|
+ """
|
|
+ super(Metadata, self).__init__()
|
|
+ self.metadata = metadata
|
|
+ self.hash_id = hash_id
|
|
+
|
|
+ def do_store(self, connection):
|
|
+ super(Metadata, self).do_store(connection)
|
|
+ connection.execute('INSERT OR IGNORE INTO metadata (hash, metadata) VALUES(?, ?)',
|
|
+ (self.hash_id, self.metadata))
|
|
+
|
|
+
|
|
+class Entity(Host):
|
|
+ """
|
|
+ Leapp framework entity (e.g. actor, workflow)
|
|
+ """
|
|
+
|
|
+ def __init__(self, context=None, hostname=None, kind=None, metadata=None, name=None):
|
|
+ """
|
|
+ :param context: The execution context
|
|
+ :type context: str
|
|
+ :param hostname: Hostname of the system that produced the entry
|
|
+ :type hostname: str
|
|
+ :param kind: Kind of the entity for which metadata is stored
|
|
+ :type kind: str
|
|
+ :param metadata: Entity metadata
|
|
+ :type metadata: :py:class:`leapp.utils.audit.Metadata`
|
|
+ :param name: Name of the entity
|
|
+ :type name: str
|
|
+ """
|
|
+ super(Entity, self).__init__(context=context, hostname=hostname)
|
|
+ self.kind = kind
|
|
+ self.name = name
|
|
+ self.metadata = metadata
|
|
+ self._entity_id = None
|
|
+
|
|
+ @property
|
|
+ def entity_id(self):
|
|
+ """
|
|
+ Returns the id of the entry, which is only set when already stored.
|
|
+ :return: Integer id or None
|
|
+ """
|
|
+ return self._entity_id
|
|
+
|
|
+ def do_store(self, connection):
|
|
+ super(Entity, self).do_store(connection)
|
|
+ self.metadata.do_store(connection)
|
|
+ connection.execute(
|
|
+ 'INSERT OR IGNORE INTO entity (context, kind, name, metadata_hash) VALUES(?, ?, ?, ?)',
|
|
+ (self.context, self.kind, self.name, self.metadata.hash_id))
|
|
+ cursor = connection.execute(
|
|
+ 'SELECT id FROM entity WHERE context = ? AND kind = ? AND name = ?',
|
|
+ (self.context, self.kind, self.name))
|
|
+ self._entity_id = cursor.fetchone()[0]
|
|
+
|
|
+
|
|
class Message(DataSource):
|
|
def __init__(self, stamp=None, msg_type=None, topic=None, data=None, actor=None, phase=None,
|
|
hostname=None, context=None):
|
|
@@ -267,6 +334,47 @@ class Message(DataSource):
|
|
self._message_id = cursor.lastrowid
|
|
|
|
|
|
+class Dialog(DataSource):
|
|
+ """
|
|
+ Stores information about dialog questions and their answers
|
|
+ """
|
|
+
|
|
+ def __init__(self, scope=None, data=None, actor=None, phase=None, hostname=None, context=None):
|
|
+ """
|
|
+ :param scope: Dialog scope
|
|
+ :type scope: str
|
|
+ :param data: Payload data
|
|
+ :type data: dict
|
|
+ :param actor: Name of the actor that triggered the entry
|
|
+ :type actor: str
|
|
+ :param phase: In which phase of the workflow execution the dialog was triggered
|
|
+ :type phase: str
|
|
+ :param hostname: Hostname of the system that produced the message
|
|
+ :type hostname: str
|
|
+ :param context: The execution context
|
|
+ :type context: str
|
|
+ """
|
|
+ super(Dialog, self).__init__(actor=actor, phase=phase, hostname=hostname, context=context)
|
|
+ self.scope = scope or ''
|
|
+ self.data = data
|
|
+ self._dialog_id = None
|
|
+
|
|
+ @property
|
|
+ def dialog_id(self):
|
|
+ """
|
|
+ Returns the id of the entry, which is only set when already stored.
|
|
+ :return: Integer id or None
|
|
+ """
|
|
+ return self._dialog_id
|
|
+
|
|
+ def do_store(self, connection):
|
|
+ super(Dialog, self).do_store(connection)
|
|
+ cursor = connection.execute(
|
|
+ 'INSERT OR IGNORE INTO dialog (context, scope, data, data_source_id) VALUES(?, ?, ?, ?)',
|
|
+ (self.context, self.scope, json.dumps(self.data), self.data_source_id))
|
|
+ self._dialog_id = cursor.lastrowid
|
|
+
|
|
+
|
|
def create_audit_entry(event, data, message=None):
|
|
"""
|
|
Create an audit entry
|
|
@@ -291,10 +399,10 @@ def get_audit_entry(event, context):
|
|
"""
|
|
Retrieve audit entries stored in the database for the given context
|
|
|
|
- :param context: The execution context
|
|
- :type context: str
|
|
:param event: Event type identifier
|
|
:type event: str
|
|
+ :param context: The execution context
|
|
+ :type context: str
|
|
:return: list of dicts with id, time stamp, actor and phase fields
|
|
"""
|
|
with get_connection(None) as conn:
|
|
@@ -470,3 +578,90 @@ def get_checkpoints(context):
|
|
''', (context, _AUDIT_CHECKPOINT_EVENT))
|
|
cursor.row_factory = dict_factory
|
|
return cursor.fetchall()
|
|
+
|
|
+
|
|
+def store_dialog(dialog, answer):
|
|
+ """
|
|
+ Store ``dialog`` with accompanying ``answer``.
|
|
+
|
|
+ :param dialog: instance of a workflow to store.
|
|
+ :type dialog: :py:class:`leapp.dialogs.Dialog`
|
|
+ :param answer: Answer to for each component of the dialog
|
|
+ :type answer: dict
|
|
+ """
|
|
+
|
|
+ component_keys = ('key', 'label', 'description', 'default', 'value', 'reason')
|
|
+ dialog_keys = ('title', 'reason') # + 'components'
|
|
+
|
|
+ tmp = dialog.serialize()
|
|
+ data = {
|
|
+ 'components': [dict((key, component[key]) for key in component_keys) for component in tmp['components']],
|
|
+
|
|
+ # NOTE(dkubek): Storing answer here is redundant as it is already
|
|
+ # being stored in audit when we query from the answerstore, however,
|
|
+ # this keeps the information coupled with the question more closely
|
|
+ 'answer': answer
|
|
+ }
|
|
+ data.update((key, tmp[key]) for key in dialog_keys)
|
|
+
|
|
+ e = Dialog(
|
|
+ scope=dialog.scope,
|
|
+ data=data,
|
|
+ context=os.environ['LEAPP_EXECUTION_ID'],
|
|
+ actor=os.environ['LEAPP_CURRENT_ACTOR'],
|
|
+ phase=os.environ['LEAPP_CURRENT_PHASE'],
|
|
+ hostname=os.environ['LEAPP_HOSTNAME'],
|
|
+ )
|
|
+ e.store()
|
|
+
|
|
+ return e
|
|
+
|
|
+
|
|
+def store_workflow_metadata(workflow):
|
|
+ """
|
|
+ Store the metadata of the given ``workflow`` into the database.
|
|
+
|
|
+ :param workflow: Workflow to store.
|
|
+ :type workflow: :py:class:`leapp.workflows.Workflow`
|
|
+ """
|
|
+
|
|
+ metadata = json.dumps(type(workflow).serialize(), sort_keys=True)
|
|
+ metadata_hash_id = hashlib.sha256(metadata.encode('utf-8')).hexdigest()
|
|
+
|
|
+ md = Metadata(metadata=metadata, hash_id=metadata_hash_id)
|
|
+ ent = Entity(kind='workflow',
|
|
+ name=workflow.name,
|
|
+ context=os.environ['LEAPP_EXECUTION_ID'],
|
|
+ hostname=os.environ['LEAPP_HOSTNAME'],
|
|
+ metadata=md)
|
|
+ ent.store()
|
|
+
|
|
+
|
|
+def store_actor_metadata(actor_definition, phase):
|
|
+ """
|
|
+ Store the metadata of the given actor given as an ``actor_definition``
|
|
+ object into the database.
|
|
+
|
|
+ :param actor_definition: Actor to store
|
|
+ :type actor_definition: :py:class:`leapp.repository.actor_definition.ActorDefinition`
|
|
+ """
|
|
+
|
|
+ _metadata = dict(actor_definition.discover())
|
|
+ _metadata.update({
|
|
+ 'consumes': sorted(model.__name__ for model in _metadata.get('consumes', ())),
|
|
+ 'produces': sorted(model.__name__ for model in _metadata.get('produces', ())),
|
|
+ 'tags': sorted(tag.__name__ for tag in _metadata.get('tags', ())),
|
|
+ })
|
|
+ _metadata['phase'] = phase
|
|
+
|
|
+ actor_metadata_fields = ('class_name', 'name', 'description', 'phase', 'tags', 'consumes', 'produces', 'path')
|
|
+ metadata = json.dumps({field: _metadata[field] for field in actor_metadata_fields}, sort_keys=True)
|
|
+ metadata_hash_id = hashlib.sha256(metadata.encode('utf-8')).hexdigest()
|
|
+
|
|
+ md = Metadata(metadata=metadata, hash_id=metadata_hash_id)
|
|
+ ent = Entity(kind='actor',
|
|
+ name=actor_definition.name,
|
|
+ context=os.environ['LEAPP_EXECUTION_ID'],
|
|
+ hostname=os.environ['LEAPP_HOSTNAME'],
|
|
+ metadata=md)
|
|
+ ent.store()
|
|
diff --git a/leapp/utils/audit/contextclone.py b/leapp/utils/audit/contextclone.py
|
|
index 2e28b70..1c80f2c 100644
|
|
--- a/leapp/utils/audit/contextclone.py
|
|
+++ b/leapp/utils/audit/contextclone.py
|
|
@@ -70,6 +70,26 @@ def _dup_audit(db, message, data_source, newcontext, oldcontext):
|
|
return lookup
|
|
|
|
|
|
+def _dup_metadata(db, newcontext, oldcontext):
|
|
+ for row in _fetch_table_for_context(db, 'metadata', oldcontext):
|
|
+ # id context kind name metadata
|
|
+ row_id, kind, name, metadata = _row_tuple(row, 'id', 'kind', 'name', 'metadata')
|
|
+
|
|
+ db.execute(
|
|
+ 'INSERT INTO metadata (context, kind, name, metadata) VALUES(?, ?, ?, ?)',
|
|
+ (newcontext, kind, name, metadata))
|
|
+
|
|
+
|
|
+def _dup_dialog(db, data_source, newcontext, oldcontext):
|
|
+ for row in _fetch_table_for_context(db, 'dialog', oldcontext):
|
|
+ # id context scope data data_source_id
|
|
+ row_id, scope, data, data_source_id = _row_tuple(row, 'id', 'scope', 'data', 'data_source_id')
|
|
+
|
|
+ db.execute(
|
|
+ 'INSERT INTO dialog (context, scope, data, data_source_id) VALUES(?, ?, ?, ?)',
|
|
+ (newcontext, scope, data, data_source[data_source_id]))
|
|
+
|
|
+
|
|
def clone_context(oldcontext, newcontext, use_db=None):
|
|
# Enter transaction - In case of any exception automatic rollback is issued
|
|
# and it is automatically committed if there was no exception
|
|
@@ -82,3 +102,5 @@ def clone_context(oldcontext, newcontext, use_db=None):
|
|
message = _dup_message(db=db, data_source=data_source, newcontext=newcontext, oldcontext=oldcontext)
|
|
# Last clone message entries and use the lookup table generated by the data_source and message duplications
|
|
_dup_audit(db=db, data_source=data_source, message=message, newcontext=newcontext, oldcontext=oldcontext)
|
|
+ _dup_metadata(db=db, oldcontext=oldcontext, newcontext=newcontext)
|
|
+ _dup_dialog(db=db, data_source=data_source, oldcontext=oldcontext, newcontext=newcontext)
|
|
diff --git a/leapp/workflows/__init__.py b/leapp/workflows/__init__.py
|
|
index 7f01e0d..1b6fc98 100644
|
|
--- a/leapp/workflows/__init__.py
|
|
+++ b/leapp/workflows/__init__.py
|
|
@@ -11,7 +11,7 @@ from leapp.messaging.inprocess import InProcessMessaging
|
|
from leapp.messaging.commands import SkipPhasesUntilCommand
|
|
from leapp.tags import ExperimentalTag
|
|
from leapp.utils import reboot_system
|
|
-from leapp.utils.audit import checkpoint, get_errors
|
|
+from leapp.utils.audit import checkpoint, get_errors, create_audit_entry, store_workflow_metadata, store_actor_metadata
|
|
from leapp.utils.meta import with_metaclass, get_flattened_subclasses
|
|
from leapp.utils.output import display_status_current_phase, display_status_current_actor
|
|
from leapp.workflows.phases import Phase
|
|
@@ -165,7 +165,7 @@ class Workflow(with_metaclass(WorkflowMeta)):
|
|
self.description = self.description or type(self).__doc__
|
|
|
|
for phase in self.phases:
|
|
- phase.filter.tags += (self.tag,)
|
|
+ phase.filter.tags += (self.tag,) if self.tag not in phase.filter.tags else ()
|
|
self._phase_actors.append((
|
|
phase,
|
|
# filters all actors with the give tags
|
|
@@ -279,6 +279,8 @@ class Workflow(with_metaclass(WorkflowMeta)):
|
|
self.log.info('Starting workflow execution: {name} - ID: {id}'.format(
|
|
name=self.name, id=os.environ['LEAPP_EXECUTION_ID']))
|
|
|
|
+ store_workflow_metadata(self)
|
|
+
|
|
skip_phases_until = (skip_phases_until or '').lower()
|
|
needle_phase = until_phase or ''
|
|
needle_stage = None
|
|
@@ -295,6 +297,12 @@ class Workflow(with_metaclass(WorkflowMeta)):
|
|
if phase and not self.is_valid_phase(phase):
|
|
raise CommandError('Phase {phase} does not exist in the workflow'.format(phase=phase))
|
|
|
|
+ # Save metadata of all discovered actors
|
|
+ for phase in self._phase_actors:
|
|
+ for stage in phase[1:]:
|
|
+ for actor in stage.actors:
|
|
+ store_actor_metadata(actor, phase[0].name)
|
|
+
|
|
self._stop_after_phase_requested = False
|
|
for phase in self._phase_actors:
|
|
os.environ['LEAPP_CURRENT_PHASE'] = phase[0].name
|
|
@@ -332,10 +340,12 @@ class Workflow(with_metaclass(WorkflowMeta)):
|
|
display_status_current_actor(actor, designation=designation)
|
|
current_logger.info("Executing actor {actor} {designation}".format(designation=designation,
|
|
actor=actor.name))
|
|
+
|
|
messaging = InProcessMessaging(config_model=config_model, answer_store=self._answer_store)
|
|
messaging.load(actor.consumes)
|
|
instance = actor(logger=current_logger, messaging=messaging,
|
|
config_model=config_model, skip_dialogs=skip_dialogs)
|
|
+
|
|
try:
|
|
instance.run()
|
|
except BaseException as exc:
|
|
@@ -346,6 +356,14 @@ class Workflow(with_metaclass(WorkflowMeta)):
|
|
current_logger.error('Actor {actor} has crashed: {trace}'.format(actor=actor.name,
|
|
trace=exc.exception_info))
|
|
raise
|
|
+ finally:
|
|
+ # Set and unset the enviromental variable so that audit
|
|
+ # associates the entry with the correct data source
|
|
+ os.environ['LEAPP_CURRENT_ACTOR'] = actor.name
|
|
+ create_audit_entry(
|
|
+ event='actor-exit-status',
|
|
+ data={'exit_status': 1 if self._unhandled_exception else 0})
|
|
+ os.environ.pop('LEAPP_CURRENT_ACTOR')
|
|
|
|
self._stop_after_phase_requested = messaging.stop_after_phase or self._stop_after_phase_requested
|
|
|
|
diff --git a/res/schema/audit-layout.sql b/res/schema/audit-layout.sql
|
|
index dd88a45..d567ce4 100644
|
|
--- a/res/schema/audit-layout.sql
|
|
+++ b/res/schema/audit-layout.sql
|
|
@@ -1,6 +1,6 @@
|
|
BEGIN;
|
|
|
|
-PRAGMA user_version = 2;
|
|
+PRAGMA user_version = 3;
|
|
|
|
CREATE TABLE IF NOT EXISTS execution (
|
|
id INTEGER PRIMARY KEY NOT NULL,
|
|
@@ -42,6 +42,28 @@ CREATE TABLE IF NOT EXISTS message (
|
|
message_data_hash VARCHAR(64) NOT NULL REFERENCES message_data (hash)
|
|
);
|
|
|
|
+CREATE TABLE IF NOT EXISTS metadata (
|
|
+ hash VARCHAR(64) PRIMARY KEY NOT NULL,
|
|
+ metadata TEXT
|
|
+);
|
|
+
|
|
+CREATE TABLE IF NOT EXISTS entity (
|
|
+ id INTEGER PRIMARY KEY NOT NULL,
|
|
+ context VARCHAR(36) NOT NULL REFERENCES execution (context),
|
|
+ kind VARCHAR(256) NOT NULL DEFAULT '',
|
|
+ name VARCHAR(1024) NOT NULL DEFAULT '',
|
|
+ metadata_hash VARCHAR(64) NOT NULL REFERENCES metadata (hash),
|
|
+ UNIQUE (context, kind, name)
|
|
+);
|
|
+
|
|
+CREATE TABLE IF NOT EXISTS dialog (
|
|
+ id INTEGER PRIMARY KEY NOT NULL,
|
|
+ context VARCHAR(36) NOT NULL REFERENCES execution (context),
|
|
+ scope VARCHAR(1024) NOT NULL DEFAULT '',
|
|
+ data TEXT DEFAULT NULL,
|
|
+ data_source_id INTEGER NOT NULL REFERENCES data_source (id)
|
|
+);
|
|
+
|
|
|
|
CREATE TABLE IF NOT EXISTS audit (
|
|
id INTEGER PRIMARY KEY NOT NULL,
|
|
@@ -74,4 +96,4 @@ CREATE VIEW IF NOT EXISTS messages_data AS
|
|
host ON host.id = data_source.host_id
|
|
;
|
|
|
|
-COMMIT;
|
|
\ No newline at end of file
|
|
+COMMIT;
|
|
diff --git a/res/schema/migrations/0002-add-metadata-dialog-tables.sql b/res/schema/migrations/0002-add-metadata-dialog-tables.sql
|
|
new file mode 100644
|
|
index 0000000..476a0c3
|
|
--- /dev/null
|
|
+++ b/res/schema/migrations/0002-add-metadata-dialog-tables.sql
|
|
@@ -0,0 +1,27 @@
|
|
+BEGIN;
|
|
+
|
|
+CREATE TABLE IF NOT EXISTS metadata (
|
|
+ hash VARCHAR(64) PRIMARY KEY NOT NULL,
|
|
+ metadata TEXT
|
|
+);
|
|
+
|
|
+CREATE TABLE IF NOT EXISTS entity (
|
|
+ id INTEGER PRIMARY KEY NOT NULL,
|
|
+ context VARCHAR(36) NOT NULL REFERENCES execution (context),
|
|
+ kind VARCHAR(256) NOT NULL DEFAULT '',
|
|
+ name VARCHAR(1024) NOT NULL DEFAULT '',
|
|
+ metadata_hash VARCHAR(64) NOT NULL REFERENCES metadata (hash),
|
|
+ UNIQUE (context, kind, name)
|
|
+);
|
|
+
|
|
+CREATE TABLE IF NOT EXISTS dialog (
|
|
+ id INTEGER PRIMARY KEY NOT NULL,
|
|
+ context VARCHAR(36) NOT NULL REFERENCES execution (context),
|
|
+ scope VARCHAR(1024) NOT NULL DEFAULT '',
|
|
+ data TEXT DEFAULT NULL,
|
|
+ data_source_id INTEGER NOT NULL REFERENCES data_source (id)
|
|
+);
|
|
+
|
|
+PRAGMA user_version = 3;
|
|
+
|
|
+COMMIT;
|
|
diff --git a/tests/data/leappdb-tests/.leapp/info b/tests/data/leappdb-tests/.leapp/info
|
|
new file mode 100644
|
|
index 0000000..2c42aa6
|
|
--- /dev/null
|
|
+++ b/tests/data/leappdb-tests/.leapp/info
|
|
@@ -0,0 +1 @@
|
|
+{"name": "workflow-tests", "id": "07005707-67bc-46e5-9732-a10fb13d4e7d"}
|
|
\ No newline at end of file
|
|
diff --git a/tests/data/leappdb-tests/.leapp/leapp.conf b/tests/data/leappdb-tests/.leapp/leapp.conf
|
|
new file mode 100644
|
|
index 0000000..b459134
|
|
--- /dev/null
|
|
+++ b/tests/data/leappdb-tests/.leapp/leapp.conf
|
|
@@ -0,0 +1,6 @@
|
|
+
|
|
+[repositories]
|
|
+repo_path=${repository:root_dir}
|
|
+
|
|
+[database]
|
|
+path=${repository:state_dir}/leapp.db
|
|
diff --git a/tests/data/leappdb-tests/actors/configprovider/actor.py b/tests/data/leappdb-tests/actors/configprovider/actor.py
|
|
new file mode 100644
|
|
index 0000000..985de52
|
|
--- /dev/null
|
|
+++ b/tests/data/leappdb-tests/actors/configprovider/actor.py
|
|
@@ -0,0 +1,17 @@
|
|
+from leapp.actors import Actor
|
|
+from leapp.models import UnitTestConfig
|
|
+from leapp.tags import UnitTestWorkflowTag
|
|
+
|
|
+
|
|
+class ConfigProvider(Actor):
|
|
+ """
|
|
+ No documentation has been provided for the config_provider actor.
|
|
+ """
|
|
+
|
|
+ name = 'config_provider'
|
|
+ consumes = ()
|
|
+ produces = (UnitTestConfig,)
|
|
+ tags = (UnitTestWorkflowTag,)
|
|
+
|
|
+ def process(self):
|
|
+ self.produce(UnitTestConfig())
|
|
diff --git a/tests/data/leappdb-tests/actors/dialogactor/actor.py b/tests/data/leappdb-tests/actors/dialogactor/actor.py
|
|
new file mode 100644
|
|
index 0000000..f9d5f77
|
|
--- /dev/null
|
|
+++ b/tests/data/leappdb-tests/actors/dialogactor/actor.py
|
|
@@ -0,0 +1,36 @@
|
|
+from leapp.actors import Actor
|
|
+from leapp.tags import SecondPhaseTag, UnitTestWorkflowTag
|
|
+from leapp.dialogs import Dialog
|
|
+from leapp.dialogs.components import BooleanComponent, ChoiceComponent, NumberComponent, TextComponent
|
|
+
|
|
+
|
|
+class DialogActor(Actor):
|
|
+ name = 'dialog_actor'
|
|
+ description = 'No description has been provided for the dialog_actor actor.'
|
|
+ consumes = ()
|
|
+ produces = ()
|
|
+ tags = (SecondPhaseTag, UnitTestWorkflowTag)
|
|
+ dialogs = (Dialog(
|
|
+ scope='unique_dialog_scope',
|
|
+ reason='Confirmation',
|
|
+ components=(
|
|
+ TextComponent(
|
|
+ key='text',
|
|
+ label='text',
|
|
+ description='a text value is needed',
|
|
+ ),
|
|
+ BooleanComponent(key='bool', label='bool', description='a boolean value is needed'),
|
|
+ NumberComponent(key='num', label='num', description='a numeric value is needed'),
|
|
+ ChoiceComponent(
|
|
+ key='choice',
|
|
+ label='choice',
|
|
+ description='need to choose one of these choices',
|
|
+ choices=('One', 'Two', 'Three', 'Four', 'Five'),
|
|
+ ),
|
|
+ ),
|
|
+ ),)
|
|
+
|
|
+ def process(self):
|
|
+ from leapp.libraries.common.test_helper import log_execution
|
|
+ log_execution(self)
|
|
+ self.get_answers(self.dialogs[0]).get('confirm', False)
|
|
diff --git a/tests/data/leappdb-tests/actors/exitstatusactor/actor.py b/tests/data/leappdb-tests/actors/exitstatusactor/actor.py
|
|
new file mode 100644
|
|
index 0000000..ae41aa5
|
|
--- /dev/null
|
|
+++ b/tests/data/leappdb-tests/actors/exitstatusactor/actor.py
|
|
@@ -0,0 +1,31 @@
|
|
+import os
|
|
+
|
|
+from leapp.actors import Actor
|
|
+from leapp.tags import FirstPhaseTag, UnitTestWorkflowTag
|
|
+from leapp.exceptions import StopActorExecution, StopActorExecutionError
|
|
+
|
|
+
|
|
+class ExitStatusActor(Actor):
|
|
+ name = 'exit_status_actor'
|
|
+ description = 'No description has been provided for the exit_status_actor actor.'
|
|
+ consumes = ()
|
|
+ produces = ()
|
|
+ tags = (FirstPhaseTag, UnitTestWorkflowTag)
|
|
+
|
|
+ def process(self):
|
|
+ from leapp.libraries.common.test_helper import log_execution
|
|
+ log_execution(self)
|
|
+ if not self.configuration or self.configuration.value != 'unit-test':
|
|
+ self.report_error('Unit test failed due missing or invalid workflow provided configuration')
|
|
+
|
|
+ if os.environ.get('ExitStatusActor-Error') == 'StopActorExecution':
|
|
+ self.report_error('Unit test requested StopActorExecution error')
|
|
+ raise StopActorExecution
|
|
+
|
|
+ if os.environ.get('ExitStatusActor-Error') == 'StopActorExecutionError':
|
|
+ self.report_error('Unit test requested StopActorExecutionError error')
|
|
+ raise StopActorExecutionError('StopActorExecutionError message')
|
|
+
|
|
+ if os.environ.get('ExitStatusActor-Error') == 'UnhandledError':
|
|
+ self.report_error('Unit test requested unhandled error')
|
|
+ assert 0 == 1, '0 == 1'
|
|
diff --git a/tests/data/leappdb-tests/libraries/test_helper.py b/tests/data/leappdb-tests/libraries/test_helper.py
|
|
new file mode 100644
|
|
index 0000000..fd5b910
|
|
--- /dev/null
|
|
+++ b/tests/data/leappdb-tests/libraries/test_helper.py
|
|
@@ -0,0 +1,7 @@
|
|
+import os
|
|
+import json
|
|
+
|
|
+
|
|
+def log_execution(actor):
|
|
+ with open(os.environ['LEAPP_TEST_EXECUTION_LOG'], 'a+') as f:
|
|
+ f.write(json.dumps(dict(name=actor.name, class_name=type(actor).__name__)) + '\n')
|
|
diff --git a/tests/data/leappdb-tests/models/unittestconfig.py b/tests/data/leappdb-tests/models/unittestconfig.py
|
|
new file mode 100644
|
|
index 0000000..10fad83
|
|
--- /dev/null
|
|
+++ b/tests/data/leappdb-tests/models/unittestconfig.py
|
|
@@ -0,0 +1,7 @@
|
|
+from leapp.models import Model, fields
|
|
+from leapp.topics import ConfigTopic
|
|
+
|
|
+
|
|
+class UnitTestConfig(Model):
|
|
+ topic = ConfigTopic
|
|
+ value = fields.String(default='unit-test')
|
|
diff --git a/tests/data/leappdb-tests/tags/firstphase.py b/tests/data/leappdb-tests/tags/firstphase.py
|
|
new file mode 100644
|
|
index 0000000..e465892
|
|
--- /dev/null
|
|
+++ b/tests/data/leappdb-tests/tags/firstphase.py
|
|
@@ -0,0 +1,5 @@
|
|
+from leapp.tags import Tag
|
|
+
|
|
+
|
|
+class FirstPhaseTag(Tag):
|
|
+ name = 'first_phase'
|
|
diff --git a/tests/data/leappdb-tests/tags/secondphase.py b/tests/data/leappdb-tests/tags/secondphase.py
|
|
new file mode 100644
|
|
index 0000000..ead6c95
|
|
--- /dev/null
|
|
+++ b/tests/data/leappdb-tests/tags/secondphase.py
|
|
@@ -0,0 +1,5 @@
|
|
+from leapp.tags import Tag
|
|
+
|
|
+
|
|
+class SecondPhaseTag(Tag):
|
|
+ name = 'second_phase'
|
|
diff --git a/tests/data/leappdb-tests/tags/unittestworkflow.py b/tests/data/leappdb-tests/tags/unittestworkflow.py
|
|
new file mode 100644
|
|
index 0000000..4a45594
|
|
--- /dev/null
|
|
+++ b/tests/data/leappdb-tests/tags/unittestworkflow.py
|
|
@@ -0,0 +1,5 @@
|
|
+from leapp.tags import Tag
|
|
+
|
|
+
|
|
+class UnitTestWorkflowTag(Tag):
|
|
+ name = 'unit_test_workflow'
|
|
diff --git a/tests/data/leappdb-tests/topics/config.py b/tests/data/leappdb-tests/topics/config.py
|
|
new file mode 100644
|
|
index 0000000..9ed3140
|
|
--- /dev/null
|
|
+++ b/tests/data/leappdb-tests/topics/config.py
|
|
@@ -0,0 +1,5 @@
|
|
+from leapp.topics import Topic
|
|
+
|
|
+
|
|
+class ConfigTopic(Topic):
|
|
+ name = 'config_topic'
|
|
diff --git a/tests/data/leappdb-tests/workflows/unit_test.py b/tests/data/leappdb-tests/workflows/unit_test.py
|
|
new file mode 100644
|
|
index 0000000..856d8e9
|
|
--- /dev/null
|
|
+++ b/tests/data/leappdb-tests/workflows/unit_test.py
|
|
@@ -0,0 +1,37 @@
|
|
+from leapp.models import UnitTestConfig
|
|
+from leapp.workflows import Workflow
|
|
+from leapp.workflows.phases import Phase
|
|
+from leapp.workflows.flags import Flags
|
|
+from leapp.workflows.tagfilters import TagFilter
|
|
+from leapp.workflows.policies import Policies
|
|
+from leapp.tags import UnitTestWorkflowTag, FirstPhaseTag, SecondPhaseTag
|
|
+
|
|
+
|
|
+class UnitTestWorkflow(Workflow):
|
|
+ name = 'LeappDBUnitTest'
|
|
+ tag = UnitTestWorkflowTag
|
|
+ short_name = 'unit_test'
|
|
+ description = '''No description has been provided for the UnitTest workflow.'''
|
|
+ configuration = UnitTestConfig
|
|
+
|
|
+ class FirstPhase(Phase):
|
|
+ name = 'first-phase'
|
|
+ filter = TagFilter(FirstPhaseTag)
|
|
+ policies = Policies(Policies.Errors.FailImmediately, Policies.Retry.Phase)
|
|
+ flags = Flags()
|
|
+
|
|
+ class SecondPhase(Phase):
|
|
+ name = 'second-phase'
|
|
+ filter = TagFilter(SecondPhaseTag)
|
|
+ policies = Policies(Policies.Errors.FailPhase, Policies.Retry.Phase)
|
|
+ flags = Flags()
|
|
+
|
|
+ # Template for phase definition - The order in which the phase classes are defined
|
|
+ # within the Workflow class represents the execution order
|
|
+ #
|
|
+ # class PhaseName(Phase):
|
|
+ # name = 'phase_name'
|
|
+ # filter = TagFilter(PhaseTag)
|
|
+ # policies = Policies(Policies.Errors.FailPhase,
|
|
+ # Policies.Retry.Phase)
|
|
+ # flags = Flags()
|
|
diff --git a/tests/scripts/test_actor_api.py b/tests/scripts/test_actor_api.py
|
|
index f009e68..2e626da 100644
|
|
--- a/tests/scripts/test_actor_api.py
|
|
+++ b/tests/scripts/test_actor_api.py
|
|
@@ -188,7 +188,13 @@ def test_actor_get_answers(monkeypatch, leapp_forked, setup_database, repository
|
|
def mocked_input(title):
|
|
return user_responses[title.split()[0].split(':')[0].lower()][0]
|
|
|
|
+ def mocked_store_dialog(dialog, answer):
|
|
+ # Silence warnings
|
|
+ dialog = answer
|
|
+ answer = dialog
|
|
+
|
|
monkeypatch.setattr('leapp.dialogs.renderer.input', mocked_input)
|
|
+ monkeypatch.setattr('leapp.actors.store_dialog', mocked_store_dialog)
|
|
|
|
messaging = _TestableMessaging()
|
|
with _with_loaded_actor(repository, actor_name, messaging) as (_unused, actor):
|
|
diff --git a/tests/scripts/test_dialog_db.py b/tests/scripts/test_dialog_db.py
|
|
new file mode 100644
|
|
index 0000000..e73eac8
|
|
--- /dev/null
|
|
+++ b/tests/scripts/test_dialog_db.py
|
|
@@ -0,0 +1,186 @@
|
|
+import os
|
|
+import json
|
|
+import tempfile
|
|
+
|
|
+import mock
|
|
+import py
|
|
+import pytest
|
|
+
|
|
+from leapp.repository.scan import scan_repo
|
|
+from leapp.dialogs import Dialog
|
|
+from leapp.dialogs.components import BooleanComponent, ChoiceComponent, NumberComponent, TextComponent
|
|
+from leapp.utils.audit import get_connection, dict_factory, store_dialog
|
|
+from leapp.utils.audit import Dialog as DialogDB
|
|
+from leapp.config import get_config
|
|
+
|
|
+_HOSTNAME = 'test-host.example.com'
|
|
+_CONTEXT_NAME = 'test-context-name-dialogdb'
|
|
+_ACTOR_NAME = 'test-actor-name'
|
|
+_PHASE_NAME = 'test-phase-name'
|
|
+_DIALOG_SCOPE = 'test-dialog'
|
|
+
|
|
+_TEXT_COMPONENT_METADATA = {
|
|
+ 'default': None,
|
|
+ 'description': 'a text value is needed',
|
|
+ 'key': 'text',
|
|
+ 'label': 'text',
|
|
+ 'reason': None,
|
|
+ 'value': None
|
|
+}
|
|
+_BOOLEAN_COMPONENT_METADATA = {
|
|
+ 'default': None,
|
|
+ 'description': 'a boolean value is needed',
|
|
+ 'key': 'bool',
|
|
+ 'label': 'bool',
|
|
+ 'reason': None,
|
|
+ 'value': None
|
|
+}
|
|
+
|
|
+_NUMBER_COMPONENT_METADATA = {
|
|
+ 'default': -1,
|
|
+ 'description': 'a numeric value is needed',
|
|
+ 'key': 'num',
|
|
+ 'label': 'num',
|
|
+ 'reason': None,
|
|
+ 'value': None
|
|
+}
|
|
+_CHOICE_COMPONENT_METADATA = {
|
|
+ 'default': None,
|
|
+ 'description': 'need to choose one of these choices',
|
|
+ 'key': 'choice',
|
|
+ 'label': 'choice',
|
|
+ 'reason': None,
|
|
+ 'value': None
|
|
+}
|
|
+_COMPONENT_METADATA = [
|
|
+ _TEXT_COMPONENT_METADATA, _BOOLEAN_COMPONENT_METADATA, _NUMBER_COMPONENT_METADATA, _CHOICE_COMPONENT_METADATA
|
|
+]
|
|
+_COMPONENT_METADATA_FIELDS = ('default', 'description', 'key', 'label', 'reason', 'value')
|
|
+_DIALOG_METADATA_FIELDS = ('answer', 'title', 'reason', 'components')
|
|
+
|
|
+_TEST_DIALOG = Dialog(
|
|
+ scope=_DIALOG_SCOPE,
|
|
+ reason='need to test dialogs',
|
|
+ components=(
|
|
+ TextComponent(
|
|
+ key='text',
|
|
+ label='text',
|
|
+ description='a text value is needed',
|
|
+ ),
|
|
+ BooleanComponent(key='bool', label='bool', description='a boolean value is needed'),
|
|
+ NumberComponent(key='num', label='num', description='a numeric value is needed'),
|
|
+ ChoiceComponent(
|
|
+ key='choice',
|
|
+ label='choice',
|
|
+ description='need to choose one of these choices',
|
|
+ choices=('One', 'Two', 'Three', 'Four', 'Five'),
|
|
+ ),
|
|
+ ),
|
|
+)
|
|
+
|
|
+
|
|
+@pytest.fixture(scope='module')
|
|
+def repository():
|
|
+ repository_path = py.path.local(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'leappdb-tests'))
|
|
+ with repository_path.as_cwd():
|
|
+ repo = scan_repo('.')
|
|
+ repo.load(resolve=True)
|
|
+ yield repo
|
|
+
|
|
+
|
|
+def setup_module():
|
|
+ get_config().set('database', 'path', '/tmp/leapp-test.db')
|
|
+
|
|
+
|
|
+def setup():
|
|
+ path = get_config().get('database', 'path')
|
|
+ if os.path.isfile(path):
|
|
+ os.unlink(path)
|
|
+
|
|
+
|
|
+def fetch_dialog(dialog_id=None):
|
|
+ entry = None
|
|
+ with get_connection(None) as conn:
|
|
+
|
|
+ if dialog_id is not None:
|
|
+ cursor = conn.execute('SELECT * FROM dialog WHERE id = ?;', (dialog_id,))
|
|
+ else: # Fetch last saved dialog
|
|
+ cursor = conn.execute('SELECT * FROM dialog ORDER BY id DESC LIMIT 1;',)
|
|
+
|
|
+ cursor.row_factory = dict_factory
|
|
+ entry = cursor.fetchone()
|
|
+
|
|
+ return entry
|
|
+
|
|
+
|
|
+def test_save_empty_dialog():
|
|
+ e = DialogDB(
|
|
+ scope=_DIALOG_SCOPE,
|
|
+ data=None,
|
|
+ context=_CONTEXT_NAME,
|
|
+ actor=_ACTOR_NAME,
|
|
+ phase=_PHASE_NAME,
|
|
+ hostname=_HOSTNAME,
|
|
+ )
|
|
+ e.store()
|
|
+
|
|
+ assert e.dialog_id
|
|
+ assert e.data_source_id
|
|
+ assert e.host_id
|
|
+
|
|
+ entry = fetch_dialog(e.dialog_id)
|
|
+ assert entry is not None
|
|
+ assert entry['data_source_id'] == e.data_source_id
|
|
+ assert entry['context'] == _CONTEXT_NAME
|
|
+ assert entry['scope'] == _DIALOG_SCOPE
|
|
+ assert entry['data'] == 'null'
|
|
+
|
|
+
|
|
+def test_save_dialog(monkeypatch):
|
|
+ monkeypatch.setenv('LEAPP_CURRENT_ACTOR', _ACTOR_NAME)
|
|
+ monkeypatch.setenv('LEAPP_CURRENT_PHASE', _PHASE_NAME)
|
|
+ monkeypatch.setenv('LEAPP_EXECUTION_ID', _CONTEXT_NAME)
|
|
+ monkeypatch.setenv('LEAPP_HOSTNAME', _HOSTNAME)
|
|
+ e = store_dialog(_TEST_DIALOG, {})
|
|
+ monkeypatch.delenv('LEAPP_CURRENT_ACTOR')
|
|
+ monkeypatch.delenv('LEAPP_CURRENT_PHASE')
|
|
+ monkeypatch.delenv('LEAPP_EXECUTION_ID')
|
|
+ monkeypatch.delenv('LEAPP_HOSTNAME')
|
|
+
|
|
+ entry = fetch_dialog(e.dialog_id)
|
|
+ assert entry is not None
|
|
+ assert entry['data_source_id'] == e.data_source_id
|
|
+ assert entry['context'] == _CONTEXT_NAME
|
|
+ assert entry['scope'] == _TEST_DIALOG.scope
|
|
+
|
|
+ entry_data = json.loads(entry['data'])
|
|
+
|
|
+ assert sorted(entry_data.keys()) == sorted(_DIALOG_METADATA_FIELDS)
|
|
+
|
|
+ assert entry_data['answer'] == {}
|
|
+ assert entry_data['reason'] == 'need to test dialogs'
|
|
+ assert entry_data['title'] is None
|
|
+ for component_metadata in _COMPONENT_METADATA:
|
|
+ assert sorted(component_metadata.keys()) == sorted(_COMPONENT_METADATA_FIELDS)
|
|
+ assert component_metadata in entry_data['components']
|
|
+
|
|
+
|
|
+def test_save_dialog_workflow(monkeypatch, repository):
|
|
+ workflow = repository.lookup_workflow('LeappDBUnitTest')()
|
|
+ with tempfile.NamedTemporaryFile(mode='w') as stdin_dialog:
|
|
+ monkeypatch.setenv('LEAPP_TEST_EXECUTION_LOG', '/dev/null')
|
|
+ stdin_dialog.write('my answer\n')
|
|
+ stdin_dialog.write('yes\n')
|
|
+ stdin_dialog.write('42\n')
|
|
+ stdin_dialog.write('0\n')
|
|
+ stdin_dialog.seek(0)
|
|
+ with mock.patch('sys.stdin.fileno', return_value=stdin_dialog.fileno()):
|
|
+ workflow.run(skip_dialogs=False)
|
|
+
|
|
+ monkeypatch.delenv('LEAPP_TEST_EXECUTION_LOG', '/dev/null')
|
|
+
|
|
+ entry = fetch_dialog()
|
|
+ assert entry is not None
|
|
+ assert entry['scope'] == 'unique_dialog_scope'
|
|
+ data = json.loads(entry['data'])
|
|
+ assert data['answer'] == {'text': 'my answer', 'num': 42, 'bool': True, 'choice': 'One'}
|
|
diff --git a/tests/scripts/test_exit_status.py b/tests/scripts/test_exit_status.py
|
|
new file mode 100644
|
|
index 0000000..11e8583
|
|
--- /dev/null
|
|
+++ b/tests/scripts/test_exit_status.py
|
|
@@ -0,0 +1,68 @@
|
|
+import os
|
|
+import json
|
|
+import tempfile
|
|
+
|
|
+import py
|
|
+import pytest
|
|
+
|
|
+from leapp.repository.scan import scan_repo
|
|
+from leapp.config import get_config
|
|
+from leapp.utils.audit import get_audit_entry
|
|
+
|
|
+_HOSTNAME = 'test-host.example.com'
|
|
+_CONTEXT_NAME = 'test-context-name-exit-status'
|
|
+_ACTOR_NAME = 'test-actor-name'
|
|
+_PHASE_NAME = 'test-phase-name'
|
|
+_DIALOG_SCOPE = 'test-dialog'
|
|
+
|
|
+
|
|
+@pytest.fixture(scope='module')
|
|
+def repository():
|
|
+ repository_path = py.path.local(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'leappdb-tests'))
|
|
+ with repository_path.as_cwd():
|
|
+ repo = scan_repo('.')
|
|
+ repo.load(resolve=True)
|
|
+ yield repo
|
|
+
|
|
+
|
|
+def setup_module():
|
|
+ get_config().set('database', 'path', '/tmp/leapp-test.db')
|
|
+
|
|
+
|
|
+def setup():
|
|
+ path = get_config().get('database', 'path')
|
|
+ if os.path.isfile(path):
|
|
+ os.unlink(path)
|
|
+
|
|
+
|
|
+@pytest.mark.parametrize('error, code', [(None, 0), ('StopActorExecution', 0), ('StopActorExecutionError', 0),
|
|
+ ('UnhandledError', 1)])
|
|
+def test_exit_status_stopactorexecution(monkeypatch, repository, error, code):
|
|
+
|
|
+ workflow = repository.lookup_workflow('LeappDBUnitTest')()
|
|
+
|
|
+ if error is not None:
|
|
+ os.environ['ExitStatusActor-Error'] = error
|
|
+ else:
|
|
+ os.environ.pop('ExitStatusActor-Error', None)
|
|
+
|
|
+ with tempfile.NamedTemporaryFile() as test_log_file:
|
|
+ monkeypatch.setenv('LEAPP_TEST_EXECUTION_LOG', test_log_file.name)
|
|
+ monkeypatch.setenv('LEAPP_HOSTNAME', _HOSTNAME)
|
|
+ try:
|
|
+ workflow.run(skip_dialogs=True, context=_CONTEXT_NAME, until_actor='ExitStatusActor')
|
|
+ except BaseException: # pylint: disable=broad-except
|
|
+ pass
|
|
+
|
|
+ ans = get_audit_entry('actor-exit-status', _CONTEXT_NAME).pop()
|
|
+
|
|
+ assert ans is not None
|
|
+ assert ans['actor'] == 'exit_status_actor'
|
|
+ assert ans['context'] == _CONTEXT_NAME
|
|
+ assert ans['hostname'] == _HOSTNAME
|
|
+ data = json.loads(ans['data'])
|
|
+ assert data['exit_status'] == code
|
|
+
|
|
+
|
|
+def teardown():
|
|
+ os.environ.pop('ExitStatusActor-Error', None)
|
|
diff --git a/tests/scripts/test_metadata.py b/tests/scripts/test_metadata.py
|
|
new file mode 100644
|
|
index 0000000..9b5c07f
|
|
--- /dev/null
|
|
+++ b/tests/scripts/test_metadata.py
|
|
@@ -0,0 +1,223 @@
|
|
+import os
|
|
+import json
|
|
+import logging
|
|
+import hashlib
|
|
+
|
|
+import mock
|
|
+import py
|
|
+import pytest
|
|
+
|
|
+from leapp.repository.scan import scan_repo
|
|
+from leapp.repository.actor_definition import ActorDefinition
|
|
+from leapp.utils.audit import (get_connection, dict_factory, Metadata, Entity, store_actor_metadata,
|
|
+ store_workflow_metadata)
|
|
+from leapp.config import get_config
|
|
+
|
|
+_HOSTNAME = 'test-host.example.com'
|
|
+_CONTEXT_NAME = 'test-context-name-metadata'
|
|
+_ACTOR_NAME = 'test-actor-name'
|
|
+_PHASE_NAME = 'test-phase-name'
|
|
+_DIALOG_SCOPE = 'test-dialog'
|
|
+
|
|
+_WORKFLOW_METADATA_FIELDS = ('description', 'name', 'phases', 'short_name', 'tag')
|
|
+_ACTOR_METADATA_FIELDS = ('class_name', 'name', 'description', 'phase', 'tags', 'consumes', 'produces', 'path')
|
|
+
|
|
+_TEST_WORKFLOW_METADATA = {
|
|
+ 'description': 'No description has been provided for the UnitTest workflow.',
|
|
+ 'name': 'LeappDBUnitTest',
|
|
+ 'phases': [{
|
|
+ 'class_name': 'FirstPhase',
|
|
+ 'filter': {
|
|
+ 'phase': 'FirstPhaseTag',
|
|
+ 'tags': ['UnitTestWorkflowTag']
|
|
+ },
|
|
+ 'flags': {
|
|
+ 'is_checkpoint': False,
|
|
+ 'request_restart_after_phase': False,
|
|
+ 'restart_after_phase': False
|
|
+ },
|
|
+ 'index': 4,
|
|
+ 'name': 'first-phase',
|
|
+ 'policies': {
|
|
+ 'error': 'FailImmediately',
|
|
+ 'retry': 'Phase'
|
|
+ }
|
|
+ }, {
|
|
+ 'class_name': 'SecondPhase',
|
|
+ 'filter': {
|
|
+ 'phase': 'SecondPhaseTag',
|
|
+ 'tags': ['UnitTestWorkflowTag']
|
|
+ },
|
|
+ 'flags': {
|
|
+ 'is_checkpoint': False,
|
|
+ 'request_restart_after_phase': False,
|
|
+ 'restart_after_phase': False
|
|
+ },
|
|
+ 'index': 5,
|
|
+ 'name': 'second-phase',
|
|
+ 'policies': {
|
|
+ 'error': 'FailPhase',
|
|
+ 'retry': 'Phase'
|
|
+ }
|
|
+ }],
|
|
+ 'short_name': 'unit_test',
|
|
+ 'tag': 'UnitTestWorkflowTag'
|
|
+}
|
|
+_TEST_ACTOR_METADATA = {
|
|
+ 'description': 'Test Description',
|
|
+ 'class_name': 'TestActor',
|
|
+ 'name': 'test-actor',
|
|
+ 'path': 'actors/test',
|
|
+ 'tags': (),
|
|
+ 'consumes': (),
|
|
+ 'produces': (),
|
|
+ 'dialogs': (),
|
|
+ 'apis': ()
|
|
+}
|
|
+
|
|
+
|
|
+@pytest.fixture(scope='module')
|
|
+def repository():
|
|
+ repository_path = py.path.local(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'leappdb-tests'))
|
|
+ with repository_path.as_cwd():
|
|
+ repo = scan_repo('.')
|
|
+ repo.load(resolve=True)
|
|
+ yield repo
|
|
+
|
|
+
|
|
+def setup_module():
|
|
+ get_config().set('database', 'path', '/tmp/leapp-test.db')
|
|
+
|
|
+
|
|
+def setup():
|
|
+ path = get_config().get('database', 'path')
|
|
+ if os.path.isfile(path):
|
|
+ os.unlink(path)
|
|
+
|
|
+
|
|
+def test_save_empty_metadata():
|
|
+ hash_id = hashlib.sha256('test-empty-metadata'.encode('utf-8')).hexdigest()
|
|
+ md = Metadata(hash_id=hash_id, metadata='')
|
|
+ md.store()
|
|
+
|
|
+ entry = None
|
|
+ with get_connection(None) as conn:
|
|
+ cursor = conn.execute('SELECT * FROM metadata WHERE hash = ?;', (hash_id,))
|
|
+ cursor.row_factory = dict_factory
|
|
+ entry = cursor.fetchone()
|
|
+
|
|
+ assert entry is not None
|
|
+ assert entry['metadata'] == ''
|
|
+
|
|
+
|
|
+def test_save_empty_entity():
|
|
+ hash_id = hashlib.sha256('test-empty-entity'.encode('utf-8')).hexdigest()
|
|
+ md = Metadata(hash_id=hash_id, metadata='')
|
|
+ e = Entity(
|
|
+ name='test-name',
|
|
+ metadata=md,
|
|
+ kind='test-kind',
|
|
+ context=_CONTEXT_NAME,
|
|
+ hostname=_HOSTNAME,
|
|
+ )
|
|
+ e.store()
|
|
+
|
|
+ assert e.entity_id
|
|
+ assert e.host_id
|
|
+
|
|
+ entry = None
|
|
+ with get_connection(None) as conn:
|
|
+ cursor = conn.execute('SELECT * FROM entity WHERE id = ?;', (e.entity_id,))
|
|
+ cursor.row_factory = dict_factory
|
|
+ entry = cursor.fetchone()
|
|
+
|
|
+ assert entry is not None
|
|
+ assert entry['kind'] == 'test-kind'
|
|
+ assert entry['name'] == 'test-name'
|
|
+ assert entry['context'] == _CONTEXT_NAME
|
|
+ assert entry['metadata_hash'] == hash_id
|
|
+
|
|
+
|
|
+def test_store_actor_metadata(monkeypatch, repository_dir):
|
|
+ # ---
|
|
+ # Test store actor metadata without error
|
|
+ # ---
|
|
+ with repository_dir.as_cwd():
|
|
+ logger = logging.getLogger('leapp.actor.test')
|
|
+ with mock.patch.object(logger, 'log') as log_mock:
|
|
+ definition = ActorDefinition('actors/test', '.', log=log_mock)
|
|
+ with mock.patch('leapp.repository.actor_definition.get_actor_metadata', return_value=_TEST_ACTOR_METADATA):
|
|
+ with mock.patch('leapp.repository.actor_definition.get_actors', return_value=[True]):
|
|
+ definition._module = True
|
|
+
|
|
+ monkeypatch.setenv('LEAPP_EXECUTION_ID', _CONTEXT_NAME)
|
|
+ monkeypatch.setenv('LEAPP_HOSTNAME', _HOSTNAME)
|
|
+ store_actor_metadata(definition, 'test-phase')
|
|
+ monkeypatch.delenv('LEAPP_EXECUTION_ID')
|
|
+ monkeypatch.delenv('LEAPP_HOSTNAME')
|
|
+
|
|
+ # ---
|
|
+ # Test retrieve correct actor metadata
|
|
+ # ---
|
|
+ entry = None
|
|
+ with get_connection(None) as conn:
|
|
+ cursor = conn.execute('SELECT * '
|
|
+ 'FROM entity '
|
|
+ 'JOIN metadata '
|
|
+ 'ON entity.metadata_hash = metadata.hash '
|
|
+ 'WHERE name="test-actor";')
|
|
+ cursor.row_factory = dict_factory
|
|
+ entry = cursor.fetchone()
|
|
+
|
|
+ assert entry is not None
|
|
+ assert entry['kind'] == 'actor'
|
|
+ assert entry['name'] == _TEST_ACTOR_METADATA['name']
|
|
+ assert entry['context'] == _CONTEXT_NAME
|
|
+
|
|
+ metadata = json.loads(entry['metadata'])
|
|
+ assert sorted(metadata.keys()) == sorted(_ACTOR_METADATA_FIELDS)
|
|
+ assert metadata['class_name'] == _TEST_ACTOR_METADATA['class_name']
|
|
+ assert metadata['name'] == _TEST_ACTOR_METADATA['name']
|
|
+ assert metadata['description'] == _TEST_ACTOR_METADATA['description']
|
|
+ assert metadata['phase'] == 'test-phase'
|
|
+ assert sorted(metadata['tags']) == sorted(_TEST_ACTOR_METADATA['tags'])
|
|
+ assert sorted(metadata['consumes']) == sorted(_TEST_ACTOR_METADATA['consumes'])
|
|
+ assert sorted(metadata['produces']) == sorted(_TEST_ACTOR_METADATA['produces'])
|
|
+
|
|
+
|
|
+def test_workflow_metadata(monkeypatch, repository):
|
|
+ # ---
|
|
+ # Test store workflow metadata without error
|
|
+ # ---
|
|
+ workflow = repository.lookup_workflow('LeappDBUnitTest')()
|
|
+
|
|
+ monkeypatch.setenv('LEAPP_EXECUTION_ID', _CONTEXT_NAME)
|
|
+ monkeypatch.setenv('LEAPP_HOSTNAME', _HOSTNAME)
|
|
+ store_workflow_metadata(workflow)
|
|
+ monkeypatch.delenv('LEAPP_EXECUTION_ID')
|
|
+ monkeypatch.delenv('LEAPP_HOSTNAME')
|
|
+
|
|
+ # ---
|
|
+ # Test retrieve correct workflow metadata
|
|
+ # ---
|
|
+ entry = None
|
|
+ with get_connection(None) as conn:
|
|
+ cursor = conn.execute(
|
|
+ 'SELECT * '
|
|
+ 'FROM entity '
|
|
+ 'JOIN metadata '
|
|
+ 'ON entity.metadata_hash = metadata.hash '
|
|
+ 'WHERE kind == "workflow" AND context = ? '
|
|
+ 'ORDER BY id DESC '
|
|
+ 'LIMIT 1;', (_CONTEXT_NAME,))
|
|
+ cursor.row_factory = dict_factory
|
|
+ entry = cursor.fetchone()
|
|
+
|
|
+ assert entry is not None
|
|
+ assert entry['kind'] == 'workflow'
|
|
+ assert entry['name'] == 'LeappDBUnitTest'
|
|
+ assert entry['context'] == _CONTEXT_NAME
|
|
+
|
|
+ metadata = json.loads(entry['metadata'])
|
|
+ assert sorted(metadata.keys()) == sorted(_WORKFLOW_METADATA_FIELDS)
|
|
+ assert metadata == _TEST_WORKFLOW_METADATA
|
|
--
|
|
2.42.0
|
|
|