Skip to content

Commit

Permalink
Extend leappdb for leapp-inspector
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dkubek committed Mar 1, 2024
1 parent 1c9e2ee commit a079b94
Show file tree
Hide file tree
Showing 23 changed files with 857 additions and 9 deletions.
13 changes: 10 additions & 3 deletions leapp/actors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
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
Expand Down Expand Up @@ -122,12 +123,17 @@ def get_answers(self, dialog):
: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):
"""
Expand Down Expand Up @@ -285,6 +291,7 @@ def get_actor_tool_path(self, name):
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:
Expand Down
1 change: 1 addition & 0 deletions leapp/dialogs/dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,5 @@ def request_answers(self, store, renderer):
self._store = store
renderer.render(self)
self._store = None

return store.get(self.scope, {})
3 changes: 3 additions & 0 deletions leapp/messaging/answerstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ def get(self, scope, fallback=None):
# 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

Expand Down
166 changes: 164 additions & 2 deletions leapp/utils/audit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,49 @@ def do_store(self, connection):
self._data_source_id = cursor.fetchone()[0]


class Metadata(Host):
"""
Metadata of an 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: Actual metadata
:type metadata: dict
:param name: Name of the entity
:type name: str
"""
super(Metadata, self).__init__(context=context, hostname=hostname)
self.kind = kind
self.name = name
self.metadata = metadata
self._metadata_id = None

@property
def metadata_id(self):
"""
Returns the id of the entry, which is only set when already stored.
:return: Integer id or None
"""
return self._metadata_id

def do_store(self, connection):
super(Metadata, self).do_store(connection)
connection.execute(
'INSERT OR IGNORE INTO metadata (context, kind, name, metadata) VALUES(?, ?, ?, ?)',
(self.context, self.kind, self.name, json.dumps(self.metadata)))
cursor = connection.execute(
'SELECT id FROM metadata WHERE context = ? AND kind = ? AND name = ?',
(self.context, self.kind, self.name))
self._metadata_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):
Expand Down Expand Up @@ -267,6 +310,47 @@ def do_store(self, connection):
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
Expand All @@ -291,10 +375,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:
Expand Down Expand Up @@ -470,3 +554,81 @@ 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 = type(workflow).serialize()
md = Metadata(kind='workflow', name=workflow.name, context=os.environ['LEAPP_EXECUTION_ID'],
hostname=os.environ['LEAPP_HOSTNAME'], metadata=metadata)
md.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': [model.__name__ for model in metadata.get('consumes', ())],
'produces': [model.__name__ for model in metadata.get('produces', ())],
'tags': [tag.__name__ for tag in metadata.get('tags', ())],
})
metadata['phase'] = phase

actor_metadata_fields = (
'class_name', 'name', 'description', 'phase', 'tags', 'consumes', 'produces', 'path'
)
md = Metadata(kind='actor', name=actor_definition.name,
context=os.environ['LEAPP_EXECUTION_ID'],
hostname=os.environ['LEAPP_HOSTNAME'],
metadata={field: metadata[field] for field in actor_metadata_fields})
md.store()
22 changes: 20 additions & 2 deletions leapp/workflows/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
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
Expand Down Expand Up @@ -165,7 +165,7 @@ def __init__(self, logger=None, auto_reboot=False):
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
Expand Down Expand Up @@ -279,6 +279,8 @@ def run(self, context=None, until_phase=None, until_actor=None, skip_phases_unti
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
Expand All @@ -295,6 +297,12 @@ def run(self, context=None, until_phase=None, until_actor=None, skip_phases_unti
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
Expand Down Expand Up @@ -332,10 +340,12 @@ def run(self, context=None, until_phase=None, until_actor=None, skip_phases_unti
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:
Expand All @@ -346,6 +356,14 @@ def run(self, context=None, until_phase=None, until_actor=None, skip_phases_unti
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

Expand Down
22 changes: 20 additions & 2 deletions res/schema/audit-layout.sql
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -43,6 +43,24 @@ CREATE TABLE IF NOT EXISTS message (
);


CREATE TABLE IF NOT EXISTS metadata (
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 TEXT DEFAULT NULL,
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,
event VARCHAR(256) NOT NULL REFERENCES execution (context),
Expand Down Expand Up @@ -74,4 +92,4 @@ CREATE VIEW IF NOT EXISTS messages_data AS
host ON host.id = data_source.host_id
;

COMMIT;
COMMIT;
22 changes: 22 additions & 0 deletions res/schema/migrations/0002-add-metadata-dialog-tables.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
BEGIN;

CREATE TABLE metadata (
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 TEXT DEFAULT NULL,
UNIQUE (context, kind, name)
);

CREATE TABLE 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;
1 change: 1 addition & 0 deletions tests/data/leappdb-tests/.leapp/info
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name": "workflow-tests", "id": "07005707-67bc-46e5-9732-a10fb13d4e7d"}
6 changes: 6 additions & 0 deletions tests/data/leappdb-tests/.leapp/leapp.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

[repositories]
repo_path=${repository:root_dir}

[database]
path=${repository:state_dir}/leapp.db
Loading

0 comments on commit a079b94

Please sign in to comment.