From d1f553e8f9d9ea97642602d395a2f364d4056d07 Mon Sep 17 00:00:00 2001 From: David Kubek Date: Tue, 2 Jul 2024 13:44:54 +0200 Subject: [PATCH 1/9] Add discovered actors and metadata --- scripts/leappinspector/leapp-inspector | 206 +++++++++++++++---------- 1 file changed, 128 insertions(+), 78 deletions(-) diff --git a/scripts/leappinspector/leapp-inspector b/scripts/leappinspector/leapp-inspector index ee0992c..f2fbafe 100755 --- a/scripts/leappinspector/leapp-inspector +++ b/scripts/leappinspector/leapp-inspector @@ -38,6 +38,7 @@ It's expected that people implement additional extensions / subcommands which serve well for them. """ + class LeappDatabaseEmpty(Exception): pass @@ -74,6 +75,10 @@ def print_rows(rows): print_row(row) +def _id(x): + return x + + class Database(object): """ Class to get various data about SQLite db in convenient way. @@ -86,7 +91,7 @@ class Database(object): raise EnvironmentError( "The {} file doesn't exist. Specify the correct path to the" " leapp database file.".format(db_file) - ) + ) self._db_file = db_file self._debug = debug @@ -119,7 +124,8 @@ class Database(object): """ Get list of tables. """ - cursor = self.execute("SELECT name FROM sqlite_master WHERE type='table'") + cursor = self.execute( + "SELECT name FROM sqlite_master WHERE type='table'") return [row["name"] for row in cursor.fetchall()] def get_table_info(self, table): @@ -130,8 +136,8 @@ class Database(object): to be able to get info using `.schema` cmd. """ cursor = self.execute( - "SELECT * from sqlite_master WHERE type='table' AND name='{}'" - .format(table)) + "SELECT * from sqlite_master WHERE type='table' AND name='{}'" + .format(table)) return cursor.fetchall() @@ -241,12 +247,31 @@ class LeappDatabase(Database): """ raise NotImplementedError() + def get_entity_metadata(self, kind, name): + """ + Get information about specific actor including all metadata. + + # TODO: kind is one of actor, workflow .... + """ + cmd = ("SELECT metadata.metadata FROM entity" + " LEFT JOIN metadata ON metadata_hash = metadata.hash" + " WHERE context = '{context}' AND kind = '{kind}' AND name = '{name}'" + .format(context=self._context, kind=kind, name=name)) + result = self.execute(cmd).fetchone() + + return json.loads(result["metadata"]) + def get_actors(self): """ Return a set of discovered actors """ - # for row in self.get_logs(phase=""): - raise NotImplementedError("Requires change in the framework first") + + cmd = ("SELECT name FROM entity" + " WHERE kind = 'actor' AND context = '{context}'" + .format(context=self._context)) + results = self.execute(cmd).fetchall() + + return [row["name"] for row in results] def get_productive_actors(self): """ @@ -372,8 +397,8 @@ class LeappDatabase(Database): # - Keep it as it is (it's not convenient, but....) # ... and it this function/method really good idea at all? - - errors = self.get_messages(msg_type="ErrorModel", phase=phase, actor=actor) + errors = self.get_messages( + msg_type="ErrorModel", phase=phase, actor=actor) if check_logs: for log in self.get_logs(phase=phase, actor=actor, log_level=LogLevels.ERROR): @@ -406,7 +431,8 @@ class LeappDataPrinter(LeappDatabase): SIMPLE_SEP_CHAR = "-" def __init__(self, db_file='leapp.db', context=None): - super(LeappDataPrinter, self).__init__(db_file=db_file, context=context) + super(LeappDataPrinter, self).__init__( + db_file=db_file, context=context) self._width = 70 def _print_header(self, header_title): @@ -444,19 +470,30 @@ class LeappDataPrinter(LeappDatabase): # recursive processing. Add in case any example is discovered. if recursive and isinstance(json_data, dict): for key, val in json_data.items(): - json_data[key] = LeappDataPrinter._fmt_msg_data( - val, recursive, stack + 1 - ) + json_data[key] = LeappDataPrinter._fmt_msg_data( + val, recursive, stack + 1 + ) if stack > 0: return json_data return json.dumps(json_data, indent=4, sort_keys=True) + @staticmethod + def _fmt_as_bullet_points(t): + lines = [''] + for entry in t: + lines.append(" - {}".format(entry)) + + if not t: + lines.append(" ----") + + return '\n'.join(lines) @staticmethod def print_message(msg, recursive=False): for i in ("stamp", "actor", "phase", "type"): print("{}: {}".format(i.capitalize(), msg[i])) - data = LeappDataPrinter._fmt_msg_data(msg["message_data"], recursive=recursive) + data = LeappDataPrinter._fmt_msg_data( + msg["message_data"], recursive=recursive) print("Message_data:\n{}".format(data)) def print_messages(self, actor=None, phase=None, msg_type=None, recursive=False): @@ -490,28 +527,36 @@ class LeappDataPrinter(LeappDatabase): # this could be possibly later replace by function like # self.get_actor(actor_name) # of course, with additional data like produced msgs, .... + metadata = self.get_entity_metadata('actor', actor_name) + actors_regexp = re.compile(r"Executing actor ([^\s]+)") for log in self._filter_data(event="log-message"): match = actors_regexp.search(log["data"]) if match and match.group(1) == actor_name: - return { - "phase": log["phase"], - "stamp": log["stamp"], - } - return {} + metadata["stamp"] = log["stamp"] + break + + return metadata - msgs = [msg["type"] for msg in self.get_messages(actor=actor_name)] actor = get_metadata(actor_name) - print("Actor: {}".format(actor_name)) - print("Executed: {}".format(actor.get("stamp", None) is not None)) - print("Phase: {}".format(actor.get("phase",""))) - print("Started: {}".format(actor.get("stamp",""))) - print("Produced messages:") - if msgs: - for msg in msgs: - print(" - {}".format(msg)) - else: - print(" ----") + actor["msgs"] = [msg["type"] for msg in self.get_messages(actor=actor_name)] + + fields = [ + # (field to print, field, default value, fn) + ("Actor", "name", "", _id), + ("Class", "class_name", "", _id), + ("Path", "path", "", _id), + ("Executed", "stamp", None, lambda _: _ is not None), + ("Started", "stamp", "", _id), + ("Phase", "phase", "", _id), + ("Tags", "tags", (), LeappDataPrinter._fmt_as_bullet_points), + ("Consumes", "consumes", (), LeappDataPrinter._fmt_as_bullet_points), + ("Produces", "produces", (), LeappDataPrinter._fmt_as_bullet_points), + ("Produced during execution", "msgs", (), LeappDataPrinter._fmt_as_bullet_points), + ("Description", "description", "", _id), + ] + for print_field, field, default, fn in fields: + print("{}: {}".format(print_field, fn(actor.get(field, default)))) print("Executed shell commands:") # NOTE(pstodulk): That regular is terrible, I know @@ -545,14 +590,17 @@ class LeappDataPrinter(LeappDatabase): """ # FIXME: currently prints info just about executed actors, add # possibility of filtering, and detection of all actors!! - actors = self.get_executed_actors() - self._print_header("EXECUTED ACTORS") + actors = self.get_actors() + self._print_header("DISCOVERED ACTORS") try: + # TODO: really execution order? # pop actors in the execution order - self.print_actor(actors.pop(0), log_level=log_level, terminal_like_logs=terminal_like_logs) + self.print_actor(actors.pop(0), log_level=log_level, + terminal_like_logs=terminal_like_logs) for actor_name in actors: self._print_separator() - self.print_actor(actor_name, log_level=log_level, terminal_like_logs=terminal_like_logs) + self.print_actor(actor_name, log_level=log_level, + terminal_like_logs=terminal_like_logs) except IndexError: print() self._print_tail() @@ -585,6 +633,7 @@ class LeappDataPrinter(LeappDatabase): # Stuff related to the tool and CLI # ################################# + class SubCommandBaseClass(): """ This is base class for CLI subcommands of Leapp Inspector @@ -619,7 +668,8 @@ class SubCommandBaseClass(): if type(self) is SubCommandBaseClass: raise Exception("The base class cannot be instantiated directly.") if not isinstance(leapp_inspector_cli, LeappInspectorCLI): - raise ValueError("leapp_inspector_cli must be instance of LeappInspectorCLI") + raise ValueError( + "leapp_inspector_cli must be instance of LeappInspectorCLI") self.li_cli = leapp_inspector_cli self._register_cmd() @@ -641,7 +691,6 @@ class SubCommandBaseClass(): ) return self.help_short_str - def set_arguments(self): raise NotImplementedError("Must be implemented in derived class.") @@ -675,7 +724,7 @@ class LeappInspectorCLI: Adjustable parsing of the CLI based on registered sub-commands. """ - DEFAULT_DB_PATHS= ["leapp.db", "/var/lib/leapp/leapp.db"] + DEFAULT_DB_PATHS = ["leapp.db", "/var/lib/leapp/leapp.db"] LEAPP_CONFIG_FILE = "/etc/leapp/leapp.conf" def __init__(self): @@ -703,7 +752,6 @@ class LeappInspectorCLI: """ return self._subparsers.add_parser(name, **kwargs) - def add_subcommand(self, subcmd_class): """ Add/register new subcommand for the Leapp Inspector tool @@ -717,7 +765,7 @@ class LeappInspectorCLI: """ Parse cmdline input """ - self.cmdline = self._parser.parse_args() + self.cmdline = self._parser.parse_args() def process(self): """ @@ -837,19 +885,19 @@ class MessagesCLI(SubCommandBaseClass): def set_arguments(self): self.subparser.add_argument("--list", dest="msgs", action="store_true", - help="List types of all produced messages.") + help="List types of all produced messages.") self.subparser.add_argument("--actor", dest="actor", default=None, - help="Print only messages produced by the actor.") + help="Print only messages produced by the actor.") self.subparser.add_argument("--type", dest="msg_type", default=None, - help="Print only messages ot the specified type.") + help="Print only messages ot the specified type.") self.subparser.add_argument("--phase", dest="phase", default=None, - help="Print only messages produced during the specified phase.") + help="Print only messages produced during the specified phase.") self.subparser.add_argument("--recursive-expand", dest="recursive", action="store_true", - help=( - "Expand all JSON data recursively. IOW, any json encoded" - " as a string is decoded. Try it with the Report type to see" - " the difference." - )) + help=( + "Expand all JSON data recursively. IOW, any json encoded" + " as a string is decoded. Try it with the Report type to see" + " the difference." + )) def process(self): cmdline = self.li_cli.cmdline @@ -881,32 +929,33 @@ class ActorsCLI(SubCommandBaseClass): } def set_arguments(self): - group = self.subparser.add_argument_group('List options').add_mutually_exclusive_group() + group = self.subparser.add_argument_group( + 'List options').add_mutually_exclusive_group() # TODO: currently we can obtain only dirnames of actors, not names of actors, # - which means we cannot effectively use this information right now. # - propose change in leapp to be able to get name of every discovered actor - # group.add_argument("--list", dest="list", action="store_const", const=ActorSelector.ANY, - # help="List all discovered actors") + group.add_argument("--list", dest="list", action="store_const", const=ActorSelector.ANY, + help="List all discovered actors") group.add_argument("--list-executed", dest="list", action="store_const", const=ActorSelector.EXECUTED, - help="List all executed actors") + help="List all executed actors") group.add_argument("--list-producers", dest="list", action="store_const", const=ActorSelector.PRODUCER, - help="List all actors that produced any messages") + help="List all actors that produced any messages") group.add_argument("--actor", dest="actor", default=None, - help="Print data related just to the specified actor.") + help="Print data related just to the specified actor.") self.subparser.add_argument("--log-level", dest="log_level", default="DEBUG", - choices=self._log_level_map.keys(), - help=( - "Print logs of the given level and lower. The DEBUG level" - " is the highest one and set by default. The ERROR level" - " is the lowest." - )) + choices=self._log_level_map.keys(), + help=( + "Print logs of the given level and lower. The DEBUG level" + " is the highest one and set by default. The ERROR level" + " is the lowest." + )) self.subparser.add_argument("--terminal-like", dest="terminal_like", action="store_true", - help=( - "Logs are usually stored with additional metadata. Using this" - " option, logs are printed like they are printed in terminal" - " when leapp is executed - just indentation is added, for" - " better readability, on the beginning of every log." - )) + help=( + "Logs are usually stored with additional metadata. Using this" + " option, logs are printed like they are printed in terminal" + " when leapp is executed - just indentation is added, for" + " better readability, on the beginning of every log." + )) def process(self): cmdline = self.li_cli.cmdline @@ -991,16 +1040,16 @@ class InspectionCLI(SubCommandBaseClass): def set_arguments(self): self.subparser.add_argument("--paranoid", dest="is_paranoid", action="store_true", - help=( - "Set inspection to the paranoid mode. Print possible errors" - " when any sub command executed by actor return non-zero exit" - " code. This can produce a lot of 'false positive' messages" - " as many such failed subcommands are expected to be valid" - " output. E.g. when detecting whether something is set, is" - " expected the subcommand could return non-zero exit code." - " That's why the mode is called paranoid. But sometimes could" - " point on hidden issue.") - ) + help=( + "Set inspection to the paranoid mode. Print possible errors" + " when any sub command executed by actor return non-zero exit" + " code. This can produce a lot of 'false positive' messages" + " as many such failed subcommands are expected to be valid" + " output. E.g. when detecting whether something is set, is" + " expected the subcommand could return non-zero exit code." + " That's why the mode is called paranoid. But sometimes could" + " point on hidden issue.") + ) def process(self): ldp = self.LeappDataPrinter @@ -1020,7 +1069,8 @@ class InspectionCLI(SubCommandBaseClass): print("==> {} ({})".format(name, phase_type)) else: print("====> {}".format(name)) - errors = ldp.get_errors(actor=name, check_logs=True, check_cmd_exit=self.cmdline.is_paranoid) + errors = ldp.get_errors( + actor=name, check_logs=True, check_cmd_exit=self.cmdline.is_paranoid) if not errors: continue ldp._print_separator(msg="(Possible) Errors") @@ -1033,7 +1083,6 @@ class InspectionCLI(SubCommandBaseClass): LeappDataPrinter.print_message(err, True) ldp._print_separator() - def _error_exists(self): # Ok, currently this returns just errors that have been reported (ErrorModel) # IOW, only fatal errors. Not errors logged used the logger. @@ -1058,7 +1107,7 @@ class InspectionCLI(SubCommandBaseClass): for log in self.LeappDatabase.get_logs(): match = phase_regexp.search(log["data"]) if match: - yield True, match.group(1) ,log["phase"] + yield True, match.group(1), log["phase"] continue match = actors_regexp.search(log["data"]) if match: @@ -1077,6 +1126,7 @@ def set_default_subcommands(cli): cli.add_subcommand(InteractiveCLI) cli.add_subcommand(InspectionCLI) + if __name__ == '__main__': cli = LeappInspectorCLI() set_default_subcommands(cli) From 918f3ba04a28a39f2bc3dde945b518dbfcfd6d4b Mon Sep 17 00:00:00 2001 From: David Kubek Date: Wed, 3 Jul 2024 10:57:44 +0200 Subject: [PATCH 2/9] Add actor exit status --- scripts/leappinspector/leapp-inspector | 28 +++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/scripts/leappinspector/leapp-inspector b/scripts/leappinspector/leapp-inspector index f2fbafe..e58d864 100755 --- a/scripts/leappinspector/leapp-inspector +++ b/scripts/leappinspector/leapp-inspector @@ -259,7 +259,10 @@ class LeappDatabase(Database): .format(context=self._context, kind=kind, name=name)) result = self.execute(cmd).fetchone() - return json.loads(result["metadata"]) + if result: + return json.loads(result["metadata"]) + + return None def get_actors(self): """ @@ -273,6 +276,21 @@ class LeappDatabase(Database): return [row["name"] for row in results] + def get_actor_exit_status(self, actor_name): + # TODO: doc + """ + Return the exit status of the actor. + """ + + event = next(self._filter_data(actor=actor_name, event="actor-exit-status"), None) + if event is None: + return + + data = json.loads(event["data"]) + + return data.get("exit_status", None) + + def get_productive_actors(self): """ Return a set of actors that produced a msg @@ -352,6 +370,7 @@ class LeappDatabase(Database): TODO: investigate the messages in audit whether it is possible to detect it """ + # TODO: WORK FOR ME WEEEEEE raise NotImplementedError() def _get_cmd_results(self, phase=None, actor=None, failed_only=False): @@ -523,6 +542,7 @@ class LeappDataPrinter(LeappDatabase): and print the pure data only (like printed in terminal during the leapp execution). """ + def get_metadata(actor_name): # this could be possibly later replace by function like # self.get_actor(actor_name) @@ -539,6 +559,11 @@ class LeappDataPrinter(LeappDatabase): return metadata actor = get_metadata(actor_name) + if actor is None: + print("Actor {} not found!".format(actor_name), file=sys.stderr) + return + + actor["exit_status"] = self.get_actor_exit_status(actor_name) actor["msgs"] = [msg["type"] for msg in self.get_messages(actor=actor_name)] fields = [ @@ -546,6 +571,7 @@ class LeappDataPrinter(LeappDatabase): ("Actor", "name", "", _id), ("Class", "class_name", "", _id), ("Path", "path", "", _id), + ("Exit Status", "exit_status", None, _id), ("Executed", "stamp", None, lambda _: _ is not None), ("Started", "stamp", "", _id), ("Phase", "phase", "", _id), From 2d3630b72e052e0f44ec6c0d291f169080c57b4b Mon Sep 17 00:00:00 2001 From: David Kubek Date: Wed, 3 Jul 2024 12:48:38 +0200 Subject: [PATCH 3/9] Add workflow info --- scripts/leappinspector/leapp-inspector | 121 ++++++++++++++++++++++--- 1 file changed, 109 insertions(+), 12 deletions(-) diff --git a/scripts/leappinspector/leapp-inspector b/scripts/leappinspector/leapp-inspector index e58d864..faf271c 100755 --- a/scripts/leappinspector/leapp-inspector +++ b/scripts/leappinspector/leapp-inspector @@ -247,6 +247,19 @@ class LeappDatabase(Database): """ raise NotImplementedError() + def get_entities(self, kind): + # TODO: doc what kind of entities + """ + Get a list of entities of the given kind for the selected execution. + """ + + cmd = ("SELECT name FROM entity" + " WHERE kind = '{kind}' AND context = '{context}'" + .format(kind=kind, context=self._context)) + results = self.execute(cmd).fetchall() + + return [row["name"] for row in results] + def get_entity_metadata(self, kind, name): """ Get information about specific actor including all metadata. @@ -264,17 +277,19 @@ class LeappDatabase(Database): return None + def get_workflows(self): + """ + Return a discovered workflows. + """ + + return self.get_entities('workflow') + def get_actors(self): """ Return a set of discovered actors """ - cmd = ("SELECT name FROM entity" - " WHERE kind = 'actor' AND context = '{context}'" - .format(context=self._context)) - results = self.execute(cmd).fetchall() - - return [row["name"] for row in results] + return self.get_entities('actor') def get_actor_exit_status(self, actor_name): # TODO: doc @@ -290,7 +305,6 @@ class LeappDatabase(Database): return data.get("exit_status", None) - def get_productive_actors(self): """ Return a set of actors that produced a msg @@ -573,7 +587,7 @@ class LeappDataPrinter(LeappDatabase): ("Path", "path", "", _id), ("Exit Status", "exit_status", None, _id), ("Executed", "stamp", None, lambda _: _ is not None), - ("Started", "stamp", "", _id), + ("Started", "stamp", None, lambda _: _ or "---"), ("Phase", "phase", "", _id), ("Tags", "tags", (), LeappDataPrinter._fmt_as_bullet_points), ("Consumes", "consumes", (), LeappDataPrinter._fmt_as_bullet_points), @@ -614,13 +628,9 @@ class LeappDataPrinter(LeappDatabase): """ Print various information about actors """ - # FIXME: currently prints info just about executed actors, add - # possibility of filtering, and detection of all actors!! actors = self.get_actors() self._print_header("DISCOVERED ACTORS") try: - # TODO: really execution order? - # pop actors in the execution order self.print_actor(actors.pop(0), log_level=log_level, terminal_like_logs=terminal_like_logs) for actor_name in actors: @@ -631,6 +641,64 @@ class LeappDataPrinter(LeappDatabase): print() self._print_tail() + def print_workflow(self, workflow_name): + """ + Print information about the specified workflow. + """ + + workflow = self.get_entity_metadata('workflow', workflow_name) + if workflow is None: + print("Workflow {} not found!".format(workflow_name), file=sys.stderr) + return + + fields = [ + # (field to print, field, default value, fn) + ("Name", "name", "", _id), + ("Short Name", "short_name", "", _id), + ("Tag", "tag", "", _id), + ("Description", "description", "", _id), + ] + for print_field, field, default, fn in fields: + print("{}: {}".format(print_field, fn(workflow.get(field, default)))) + + phases = sorted( + workflow.get("phases", list()), + key=lambda phase: phase["index"] + ) + print("Phases:") + for phase in phases: + print(" │ ") + print(" v ", "Name:", phase['name']) + print(" │ ", "Class Name:", phase['class_name']) + print(" │ ", "Index:", phase['index']) + print(" │ ", "Policies:") + for policy in phase['policies']: + print(" " * 3 + "│" + " -", "{}:".format(policy), phase['policies'][policy]) + print(" │ ", "Flags:") + for flag in phase['flags']: + print(" " * 3 + "│" + " -", "{}:".format(flag), phase['flags'][flag]) + print(" │ ", "Filter:") + for filter in phase['filter']: + print(" " * 3 + "│" + " -", "{}:".format(filter), phase['filter'][filter]) + print(" │ ") + print(" ├" + "─" * 20) + + + def print_workflows(self, log_level=LogLevels.DEBUG, terminal_like_logs=True): + """ + Print various information about workflows + """ + workflows = self.get_workflows() + self._print_header("DISCOVERED WORKFLOWS") + try: + self.print_workflow(workflows.pop(0)) + for workflow_name in workflows: + self._print_separator() + self.print_workflow(workflow_name) + except IndexError: + print() + self._print_tail() + def print_execution_info(self, execution): """Print info about defined execution.""" # TODO: add info about used envars as well from the IPUConfig msg @@ -901,6 +969,34 @@ class HelpCLI(SubCommandBaseClass): self.li_cli.print_help() +class WorkflowsCLI(SubCommandBaseClass): + """ + The workflow subcommand for actions with workflows + """ + + name = "workflows" + help_short_str = "Print information about workflows" + + def set_arguments(self): + self.subparser.add_argument("--list", dest="list", action="store_true", + help="List types of all produced messages.") + self.subparser.add_argument("--workflow", dest="workflow", default=None, + help="Print information about selected workflow.") + + def process(self): + cmdline = self.li_cli.cmdline + + if cmdline.list: + for actor in sorted(self.LeappDatabase.get_models()): + print(" {}".format(actor)) + return + + if cmdline.workflow: + self.LeappDataPrinter.print_workflow(cmdline.workflow) + + self.LeappDataPrinter.print_workflows() + + class MessagesCLI(SubCommandBaseClass): """ The message subcommand for actions with messages @@ -1146,6 +1242,7 @@ class InspectionCLI(SubCommandBaseClass): def set_default_subcommands(cli): cli.add_subcommand(HelpCLI) + cli.add_subcommand(WorkflowsCLI) cli.add_subcommand(ActorsCLI) cli.add_subcommand(MessagesCLI) cli.add_subcommand(ExecutionsCLI) From 01724beebcb13151005b29fbd9bfeb75aae76bc3 Mon Sep 17 00:00:00 2001 From: David Kubek Date: Thu, 4 Jul 2024 12:14:39 +0200 Subject: [PATCH 4/9] Add dialog info --- scripts/leappinspector/leapp-inspector | 40 ++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/scripts/leappinspector/leapp-inspector b/scripts/leappinspector/leapp-inspector index faf271c..024f34f 100755 --- a/scripts/leappinspector/leapp-inspector +++ b/scripts/leappinspector/leapp-inspector @@ -266,6 +266,7 @@ class LeappDatabase(Database): # TODO: kind is one of actor, workflow .... """ + cmd = ("SELECT metadata.metadata FROM entity" " LEFT JOIN metadata ON metadata_hash = metadata.hash" " WHERE context = '{context}' AND kind = '{kind}' AND name = '{name}'" @@ -370,6 +371,27 @@ class LeappDatabase(Database): except IndexError: return None + def get_dialogs(self, scope=None): + # TODO: doc + + cond = ["context = '{}'".format(self._context)] + if scope: + cond.append("scope = '{}'".format(scope)) + + cmd = ("SELECT scope, data FROM dialog WHERE {cond}" + .format(cond=' AND '.join(cond))) + results = self.execute(cmd).fetchall() + + dialogs = [] + for result in results: + dialogs.append({ + "scope": result["scope"], + "data": json.loads(result["data"]) + }) + + return dialogs + + def get_status(self): """ Return status of the execution @@ -598,6 +620,21 @@ class LeappDataPrinter(LeappDatabase): for print_field, field, default, fn in fields: print("{}: {}".format(print_field, fn(actor.get(field, default)))) + print("Dialogs:") + dialogs = self.get_dialogs(actor_name) + for dialog in dialogs: + data = dialog["data"] + print(" Title: {}".format(data.get("title", None))) + print(" Reason: {}".format(data.get("reason", None))) + for component in data["components"]: + print(" Label: {}".format(component.get("label", "NO-LABEL"))) + print(" Description: {}".format(component.get("description", None) or "----")) + print(" Reason: {}".format(component.get("reason", None) or "----")) + print(" Key: {}".format(component.get("key", None) or "----")) + print(" Value: {}".format(component.get("value", None) or "----")) + if not dialogs: + print(" ----") + print("Executed shell commands:") # NOTE(pstodulk): That regular is terrible, I know # be aware that order of json fields in the log is different on @@ -683,7 +720,6 @@ class LeappDataPrinter(LeappDatabase): print(" │ ") print(" ├" + "─" * 20) - def print_workflows(self, log_level=LogLevels.DEBUG, terminal_like_logs=True): """ Print various information about workflows @@ -971,7 +1007,7 @@ class HelpCLI(SubCommandBaseClass): class WorkflowsCLI(SubCommandBaseClass): """ - The workflow subcommand for actions with workflows + The workflow subcommand for actions with workflows. """ name = "workflows" From 3d382b92c573d8322d0ff777d6804116557ac942 Mon Sep 17 00:00:00 2001 From: David Kubek Date: Mon, 8 Jul 2024 12:59:07 +0200 Subject: [PATCH 5/9] Add unified formated printing --- scripts/leappinspector/leapp-inspector | 288 ++++++++++++++++++------- 1 file changed, 206 insertions(+), 82 deletions(-) diff --git a/scripts/leappinspector/leapp-inspector b/scripts/leappinspector/leapp-inspector index 024f34f..369ae38 100755 --- a/scripts/leappinspector/leapp-inspector +++ b/scripts/leappinspector/leapp-inspector @@ -6,6 +6,8 @@ import os import re import sqlite3 import sys +import itertools +from collections.abc import Mapping, Iterable try: from json.decoder import JSONDecodeError except ImportError: @@ -74,9 +76,126 @@ def print_rows(rows): for row in rows: print_row(row) +class Color(object): + reset = "\033[0m" if sys.stdout.isatty() else "" + bold = "\033[1m" if sys.stdout.isatty() else "" + red = "\033[1;31m" if sys.stdout.isatty() else "" + green = "\033[1;32m" if sys.stdout.isatty() else "" + yellow = "\033[1;33m" if sys.stdout.isatty() else "" + blue = "\033[1;34m" if sys.stdout.isatty() else "" def _id(x): - return x + return str(x) + + +def _format(elem, fmt): + """ + Format given element using given format specification + """ + + if fmt is None: + fmt = _id + + if callable(fmt): + return str(fmt(elem)) + + if isinstance(elem, Mapping): + return format_dict(elem, fmt) + + if isinstance(elem, Iterable): + return format_list(elem, fmt) + + assert ValueError("Formatting {} with {} not implemented!".format(type(elem), fmt)) + + +def format_list( + t, + fmt=None, + indent_char=" ", list_bullet="- ", + list_start=None, list_continue=None, list_end=None, + list_empty="---", +): + """ + Format given list + + transform can be: + - function => applies function to all elements of the list + - list => assumes a list of transforms. must be same size az the list + """ + + list_start = list_start or list_bullet + list_continue = list_continue or indent_char + list_end = list_end or list_bullet + + if not t: + return list_empty + + # Validate and normalize format specification + if isinstance(fmt, list): + if len(fmt) != len(t): + msg = "The fmt is of type {} and size {} which does not match the input size {}!".format( + type(fmt), len(fmt), len(t)) + raise ValueError(msg) + + transform = iter(fmt) + else: + transform = itertools.repeat(fmt) + + lines = [] + for elem, tr in zip(t, transform): + result = _format(elem, tr).split('\n') + + lines.append([list_start, result[0]]) + for i in range(1, len(result)): + lines.append([list_continue, result[i]]) + + if lines: + lines[-1][0] = list_end + + return '\n'.join(''.join(line) for line in lines) + + +def format_dict(d, format=None, indent=4, indent_char=" ", empty_field="---", justify="ljust"): + """ + Format given dict + + transform can be: + - function => applies function to all elements of the list + - list => assumes a list of transforms. must be same size az the list + """ + + if not d: + return empty_field + + if isinstance(format, Iterable): + for fmt in format: + if isinstance(fmt, tuple) and len(fmt) != 5: + msg = "Entry {} is not a valid format specification".format(fmt) + raise ValueError(msg) + else: + format = [(key, key, None, format, False) for key in d] + + fieldname_align_width = max(len(f[0]) for f in format) + + lines = [] + + for fieldname, field, default, field_fmt, is_child in format: + offset = indent if is_child else fieldname_align_width + 2 + + result = _format(d.get(field, default), field_fmt).split('\n') + + fieldname = getattr(fieldname + ":", justify)(fieldname_align_width + 1) + for i, line in enumerate(result): + if i == 0: + if not is_child: + lines.append(["{} {}".format(fieldname, line)]) + continue + + lines.append(["{}".format(fieldname)]) + + lines.append([indent_char * offset, line]) + + return '\n'.join(''.join(line) for line in lines) class Database(object): @@ -391,7 +510,6 @@ class LeappDatabase(Database): return dialogs - def get_status(self): """ Return status of the execution @@ -406,7 +524,6 @@ class LeappDatabase(Database): TODO: investigate the messages in audit whether it is possible to detect it """ - # TODO: WORK FOR ME WEEEEEE raise NotImplementedError() def _get_cmd_results(self, phase=None, actor=None, failed_only=False): @@ -532,17 +649,6 @@ class LeappDataPrinter(LeappDatabase): return json_data return json.dumps(json_data, indent=4, sort_keys=True) - @staticmethod - def _fmt_as_bullet_points(t): - lines = [''] - for entry in t: - lines.append(" - {}".format(entry)) - - if not t: - lines.append(" ----") - - return '\n'.join(lines) - @staticmethod def print_message(msg, recursive=False): for i in ("stamp", "actor", "phase", "type"): @@ -580,9 +686,6 @@ class LeappDataPrinter(LeappDatabase): """ def get_metadata(actor_name): - # this could be possibly later replace by function like - # self.get_actor(actor_name) - # of course, with additional data like produced msgs, .... metadata = self.get_entity_metadata('actor', actor_name) actors_regexp = re.compile(r"Executing actor ([^\s]+)") @@ -602,40 +705,64 @@ class LeappDataPrinter(LeappDatabase): actor["exit_status"] = self.get_actor_exit_status(actor_name) actor["msgs"] = [msg["type"] for msg in self.get_messages(actor=actor_name)] - fields = [ + indicator = "○" if actor["exit_status"] is None else "●" + color = Color.blue + if actor["exit_status"] == 0: + color = Color.green + elif actor["exit_status"] == 1: + color = Color.red + + print("{color}{indicator}{reset} {actor_name}".format( + color=color, indicator=indicator, reset=Color.reset, actor_name=actor_name)) + + list_style = { + "indent_char": " ", "list_bullet": "- ", + "list_start": "├ ", "list_continue": "│ ", "list_end": "└ ", + "list_empty": "---"} + actor_fmt = [ # (field to print, field, default value, fn) - ("Actor", "name", "", _id), - ("Class", "class_name", "", _id), - ("Path", "path", "", _id), - ("Exit Status", "exit_status", None, _id), - ("Executed", "stamp", None, lambda _: _ is not None), - ("Started", "stamp", None, lambda _: _ or "---"), - ("Phase", "phase", "", _id), - ("Tags", "tags", (), LeappDataPrinter._fmt_as_bullet_points), - ("Consumes", "consumes", (), LeappDataPrinter._fmt_as_bullet_points), - ("Produces", "produces", (), LeappDataPrinter._fmt_as_bullet_points), - ("Produced during execution", "msgs", (), LeappDataPrinter._fmt_as_bullet_points), - ("Description", "description", "", _id), + ("Actor", "name", "", None, False), + ("Class", "class_name", "", None, False), + ("Description", "description", "", None, True), + ("Path", "path", "", None, False), + ("Exit Status", "exit_status", None, None, False), + ("Executed", "stamp", None, lambda _: _ is not None, False), + ("Started", "stamp", None, lambda _: _ or "---", False), + ("Phase", "phase", "", None, False), + ("Tags", "tags", (), lambda _: format_list(_, **list_style), True), + ("Consumes", "consumes", (), lambda _: format_list(_, **list_style), True), + ("Produces", "produces", (), lambda _: format_list(_, **list_style), True), + ("Produced", "msgs", (), lambda _: format_list(_, **list_style), True), ] - for print_field, field, default, fn in fields: - print("{}: {}".format(print_field, fn(actor.get(field, default)))) + print(format_dict(actor, actor_fmt)) + print() - print("Dialogs:") dialogs = self.get_dialogs(actor_name) - for dialog in dialogs: - data = dialog["data"] - print(" Title: {}".format(data.get("title", None))) - print(" Reason: {}".format(data.get("reason", None))) - for component in data["components"]: - print(" Label: {}".format(component.get("label", "NO-LABEL"))) - print(" Description: {}".format(component.get("description", None) or "----")) - print(" Reason: {}".format(component.get("reason", None) or "----")) - print(" Key: {}".format(component.get("key", None) or "----")) - print(" Value: {}".format(component.get("value", None) or "----")) - if not dialogs: - print(" ----") - - print("Executed shell commands:") + + def component_fmt(comp): + fmt = [ + ("Label", "label", "", None, False), + ("Description", "description", "", None, False), + ("Reason", "reason", "", None, False), + ("Key", "key", "", None, False), + ("Value", "value", "", None, False), + ] + return format_dict(comp, format=fmt) + + def dialog_fmt(dialog): + fmt = [ + ("Title", "title", "", None, False), + ("Reason", "reason", "", None, False), + ("Components", "components", [], lambda _: format_list( + _, fmt=component_fmt, **list_style), True), + ] + return format_dict(dialog["data"], format=fmt) + + if dialogs: + print("Dialogs:") + print(format_list(dialogs, fmt=dialog_fmt, **list_style)) + print() + # NOTE(pstodulk): That regular is terrible, I know # be aware that order of json fields in the log is different on # py2 and py3 @@ -645,21 +772,21 @@ class LeappDataPrinter(LeappDatabase): match = cmd_regexp.search(log["data"]) if match: cmds.append(match.group(1)) + if cmds: + print("Executed shell commands:") for cmd in cmds: print(" - {}".format(cmd)) - else: - print(" ----") - print("Logs:") - log = None - for log in self.get_logs(actor=actor_name, log_level=log_level): + logs = self.get_logs(actor=actor_name, log_level=log_level) + for i, log in enumerate(logs): + if i == 0: + print("Logs:") + if terminal_like_logs: print(" {}".format(json.loads(log["data"])["message"])) else: print("--- {}".format(log["data"])) - if not log: - print(" ----") def print_actors(self, log_level=LogLevels.DEBUG, terminal_like_logs=True): """ @@ -688,37 +815,34 @@ class LeappDataPrinter(LeappDatabase): print("Workflow {} not found!".format(workflow_name), file=sys.stderr) return - fields = [ - # (field to print, field, default value, fn) - ("Name", "name", "", _id), - ("Short Name", "short_name", "", _id), - ("Tag", "tag", "", _id), - ("Description", "description", "", _id), - ] - for print_field, field, default, fn in fields: - print("{}: {}".format(print_field, fn(workflow.get(field, default)))) - - phases = sorted( + workflow['phases'] = sorted( workflow.get("phases", list()), key=lambda phase: phase["index"] ) - print("Phases:") - for phase in phases: - print(" │ ") - print(" v ", "Name:", phase['name']) - print(" │ ", "Class Name:", phase['class_name']) - print(" │ ", "Index:", phase['index']) - print(" │ ", "Policies:") - for policy in phase['policies']: - print(" " * 3 + "│" + " -", "{}:".format(policy), phase['policies'][policy]) - print(" │ ", "Flags:") - for flag in phase['flags']: - print(" " * 3 + "│" + " -", "{}:".format(flag), phase['flags'][flag]) - print(" │ ", "Filter:") - for filter in phase['filter']: - print(" " * 3 + "│" + " -", "{}:".format(filter), phase['filter'][filter]) - print(" │ ") - print(" ├" + "─" * 20) + + list_style = { + "indent_char": " ", "list_bullet": "● ", + "list_start": "│\n● ", "list_continue": "│ ", "list_end": "└ ", + "list_empty": "---"} + def phase_fmt(phase): + fmt = [ + ("Name", "name", "", None, False), + ("Class name", "class_name", "", None, False), + ("Index", "index", "", None, False), + ("Policies", "policies", [], format_dict, True), + ("Flags", "flags", [], format_dict, True), + ("Filter", "filter", [], format_dict, True), + ] + return format_dict(phase, format=fmt) + workflow_fmt = [ + ("Name", "name", "", None, False), + ("Short Name", "short_name", "", None, False), + ("Tag", "tag", "", None, False), + ("Description", "description", "", None, False), + ("Phases", "phases", [], lambda _: format_list(_, fmt=phase_fmt, **list_style), True) + ] + print(format_dict(workflow, format=workflow_fmt)) + def print_workflows(self, log_level=LogLevels.DEBUG, terminal_like_logs=True): """ From 817f2034d85d6dd2568a22bc388fa03827090ee8 Mon Sep 17 00:00:00 2001 From: David Kubek Date: Tue, 9 Jul 2024 10:47:56 +0200 Subject: [PATCH 6/9] Unxif formatting --- scripts/leappinspector/leapp-inspector | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/scripts/leappinspector/leapp-inspector b/scripts/leappinspector/leapp-inspector index 369ae38..1e39cba 100755 --- a/scripts/leappinspector/leapp-inspector +++ b/scripts/leappinspector/leapp-inspector @@ -243,8 +243,7 @@ class Database(object): """ Get list of tables. """ - cursor = self.execute( - "SELECT name FROM sqlite_master WHERE type='table'") + cursor = self.execute("SELECT name FROM sqlite_master WHERE type='table'") return [row["name"] for row in cursor.fetchall()] def get_table_info(self, table): @@ -569,8 +568,7 @@ class LeappDatabase(Database): # - Keep it as it is (it's not convenient, but....) # ... and it this function/method really good idea at all? - errors = self.get_messages( - msg_type="ErrorModel", phase=phase, actor=actor) + errors = self.get_messages(msg_type="ErrorModel", phase=phase, actor=actor) if check_logs: for log in self.get_logs(phase=phase, actor=actor, log_level=LogLevels.ERROR): @@ -603,8 +601,7 @@ class LeappDataPrinter(LeappDatabase): SIMPLE_SEP_CHAR = "-" def __init__(self, db_file='leapp.db', context=None): - super(LeappDataPrinter, self).__init__( - db_file=db_file, context=context) + super(LeappDataPrinter, self).__init__(db_file=db_file, context=context) self._width = 70 def _print_header(self, header_title): @@ -653,8 +650,7 @@ class LeappDataPrinter(LeappDatabase): def print_message(msg, recursive=False): for i in ("stamp", "actor", "phase", "type"): print("{}: {}".format(i.capitalize(), msg[i])) - data = LeappDataPrinter._fmt_msg_data( - msg["message_data"], recursive=recursive) + data = LeappDataPrinter._fmt_msg_data(msg["message_data"], recursive=recursive) print("Message_data:\n{}".format(data)) def print_messages(self, actor=None, phase=None, msg_type=None, recursive=False): @@ -824,6 +820,7 @@ class LeappDataPrinter(LeappDatabase): "indent_char": " ", "list_bullet": "● ", "list_start": "│\n● ", "list_continue": "│ ", "list_end": "└ ", "list_empty": "---"} + def phase_fmt(phase): fmt = [ ("Name", "name", "", None, False), @@ -834,6 +831,7 @@ class LeappDataPrinter(LeappDatabase): ("Filter", "filter", [], format_dict, True), ] return format_dict(phase, format=fmt) + workflow_fmt = [ ("Name", "name", "", None, False), ("Short Name", "short_name", "", None, False), @@ -843,7 +841,6 @@ class LeappDataPrinter(LeappDatabase): ] print(format_dict(workflow, format=workflow_fmt)) - def print_workflows(self, log_level=LogLevels.DEBUG, terminal_like_logs=True): """ Print various information about workflows @@ -922,8 +919,7 @@ class SubCommandBaseClass(): if type(self) is SubCommandBaseClass: raise Exception("The base class cannot be instantiated directly.") if not isinstance(leapp_inspector_cli, LeappInspectorCLI): - raise ValueError( - "leapp_inspector_cli must be instance of LeappInspectorCLI") + raise ValueError("leapp_inspector_cli must be instance of LeappInspectorCLI") self.li_cli = leapp_inspector_cli self._register_cmd() @@ -1351,8 +1347,7 @@ class InspectionCLI(SubCommandBaseClass): print("==> {} ({})".format(name, phase_type)) else: print("====> {}".format(name)) - errors = ldp.get_errors( - actor=name, check_logs=True, check_cmd_exit=self.cmdline.is_paranoid) + errors = ldp.get_errors(actor=name, check_logs=True, check_cmd_exit=self.cmdline.is_paranoid) if not errors: continue ldp._print_separator(msg="(Possible) Errors") From da2ce16c6a1929267d6b4b395f4844359f2be554 Mon Sep 17 00:00:00 2001 From: David Kubek Date: Tue, 9 Jul 2024 11:37:14 +0200 Subject: [PATCH 7/9] Add list actor indicators --- scripts/leappinspector/leapp-inspector | 307 ++++++++++++++----------- 1 file changed, 167 insertions(+), 140 deletions(-) diff --git a/scripts/leappinspector/leapp-inspector b/scripts/leappinspector/leapp-inspector index 1e39cba..74137db 100755 --- a/scripts/leappinspector/leapp-inspector +++ b/scripts/leappinspector/leapp-inspector @@ -76,126 +76,6 @@ def print_rows(rows): for row in rows: print_row(row) -class Color(object): - reset = "\033[0m" if sys.stdout.isatty() else "" - bold = "\033[1m" if sys.stdout.isatty() else "" - red = "\033[1;31m" if sys.stdout.isatty() else "" - green = "\033[1;32m" if sys.stdout.isatty() else "" - yellow = "\033[1;33m" if sys.stdout.isatty() else "" - blue = "\033[1;34m" if sys.stdout.isatty() else "" - -def _id(x): - return str(x) - - -def _format(elem, fmt): - """ - Format given element using given format specification - """ - - if fmt is None: - fmt = _id - - if callable(fmt): - return str(fmt(elem)) - - if isinstance(elem, Mapping): - return format_dict(elem, fmt) - - if isinstance(elem, Iterable): - return format_list(elem, fmt) - - assert ValueError("Formatting {} with {} not implemented!".format(type(elem), fmt)) - - -def format_list( - t, - fmt=None, - indent_char=" ", list_bullet="- ", - list_start=None, list_continue=None, list_end=None, - list_empty="---", -): - """ - Format given list - - transform can be: - - function => applies function to all elements of the list - - list => assumes a list of transforms. must be same size az the list - """ - - list_start = list_start or list_bullet - list_continue = list_continue or indent_char - list_end = list_end or list_bullet - - if not t: - return list_empty - - # Validate and normalize format specification - if isinstance(fmt, list): - if len(fmt) != len(t): - msg = "The fmt is of type {} and size {} which does not match the input size {}!".format( - type(fmt), len(fmt), len(t)) - raise ValueError(msg) - - transform = iter(fmt) - else: - transform = itertools.repeat(fmt) - - lines = [] - for elem, tr in zip(t, transform): - result = _format(elem, tr).split('\n') - - lines.append([list_start, result[0]]) - for i in range(1, len(result)): - lines.append([list_continue, result[i]]) - - if lines: - lines[-1][0] = list_end - - return '\n'.join(''.join(line) for line in lines) - - -def format_dict(d, format=None, indent=4, indent_char=" ", empty_field="---", justify="ljust"): - """ - Format given dict - - transform can be: - - function => applies function to all elements of the list - - list => assumes a list of transforms. must be same size az the list - """ - - if not d: - return empty_field - - if isinstance(format, Iterable): - for fmt in format: - if isinstance(fmt, tuple) and len(fmt) != 5: - msg = "Entry {} is not a valid format specification".format(fmt) - raise ValueError(msg) - else: - format = [(key, key, None, format, False) for key in d] - - fieldname_align_width = max(len(f[0]) for f in format) - - lines = [] - - for fieldname, field, default, field_fmt, is_child in format: - offset = indent if is_child else fieldname_align_width + 2 - - result = _format(d.get(field, default), field_fmt).split('\n') - - fieldname = getattr(fieldname + ":", justify)(fieldname_align_width + 1) - for i, line in enumerate(result): - if i == 0: - if not is_child: - lines.append(["{} {}".format(fieldname, line)]) - continue - - lines.append(["{}".format(fieldname)]) - - lines.append([indent_char * offset, line]) - - return '\n'.join(''.join(line) for line in lines) class Database(object): @@ -373,6 +253,7 @@ class LeappDatabase(Database): cmd = ("SELECT name FROM entity" " WHERE kind = '{kind}' AND context = '{context}'" + " ORDER BY id ASC" .format(kind=kind, context=self._context)) results = self.execute(cmd).fetchall() @@ -405,7 +286,7 @@ class LeappDatabase(Database): def get_actors(self): """ - Return a set of discovered actors + Return a set of discovered actors (in alphabetical order) """ return self.get_entities('actor') @@ -588,6 +469,15 @@ class LeappDatabase(Database): return self.get_messages(msg_type="Report") +class Color(object): + reset = "\033[0m" if sys.stdout.isatty() else "" + bold = "\033[1m" if sys.stdout.isatty() else "" + red = "\033[1;31m" if sys.stdout.isatty() else "" + green = "\033[1;32m" if sys.stdout.isatty() else "" + yellow = "\033[1;33m" if sys.stdout.isatty() else "" + blue = "\033[1;34m" if sys.stdout.isatty() else "" + + class LeappDataPrinter(LeappDatabase): """ Print various leapp data in suitable format for reading. @@ -619,6 +509,120 @@ class LeappDataPrinter(LeappDatabase): def _print_tail(self): self._print_separator(full=True) + @staticmethod + def _id(x): + return str(x) + + @staticmethod + def _format(elem, fmt): + """ + Format given element using given format specification + """ + + if fmt is None: + fmt = LeappDataPrinter._id + + if callable(fmt): + return str(fmt(elem)) + + if isinstance(elem, Mapping): + return LeappDataPrinter.format_dict(elem, fmt) + + if isinstance(elem, Iterable): + return LeappDataPrinter.format_list(elem, fmt) + + assert ValueError("Formatting {} with {} not implemented!".format(type(elem), fmt)) + + @staticmethod + def format_list( + t, + fmt=None, + indent_char=" ", list_bullet="- ", + list_start=None, list_continue=None, list_end=None, + list_empty="---", + ): + """ + Format given list + + transform can be: + - function => applies function to all elements of the list + - list => assumes a list of transforms. must be same size az the list + """ + + list_start = list_start or list_bullet + list_continue = list_continue or indent_char + list_end = list_end or list_bullet + + if not t: + return list_empty + + # Validate and normalize format specification + if isinstance(fmt, list): + if len(fmt) != len(t): + msg = "The fmt is of type {} and size {} which does not match the input size {}!".format( + type(fmt), len(fmt), len(t)) + raise ValueError(msg) + + transform = iter(fmt) + else: + transform = itertools.repeat(fmt) + + lines = [] + for elem, tr in zip(t, transform): + result = LeappDataPrinter._format(elem, tr).split('\n') + + lines.append([list_start, result[0]]) + for i in range(1, len(result)): + lines.append([list_continue, result[i]]) + + if lines: + lines[-1][0] = list_end + + return '\n'.join(''.join(line) for line in lines) + + @staticmethod + def format_dict(d, fmt=None, indent=4, indent_char=" ", empty_field="---", justify="ljust"): + """ + Format given dict + + transform can be: + - function => applies function to all elements of the list + - list => assumes a list of transforms. must be same size az the list + """ + + if not d: + return empty_field + + if isinstance(fmt, Iterable): + for entry_fmt in fmt: + if isinstance(entry_fmt, tuple) and len(entry_fmt) != 5: + msg = "Entry {} is not a valid format specification".format(entry_fmt) + raise ValueError(msg) + else: + fmt = [(key, key, None, fmt, False) for key in d] + + fieldname_align_width = max(len(f[0]) for f in fmt) + + lines = [] + + for fieldname, field, default, field_fmt, is_child in fmt: + offset = indent if is_child else fieldname_align_width + 2 + + result = LeappDataPrinter._format(d.get(field, default), field_fmt).split('\n') + + fieldname = getattr(fieldname + ":", justify)(fieldname_align_width + 1) + for i, line in enumerate(result): + if i == 0: + if not is_child: + lines.append(["{} {}".format(fieldname, line)]) + continue + + lines.append(["{}".format(fieldname)]) + + lines.append([indent_char * offset, line]) + + return '\n'.join(''.join(line) for line in lines) + @staticmethod def _fmt_msg_data(data, recursive=False, stack=0): try: @@ -715,22 +719,28 @@ class LeappDataPrinter(LeappDatabase): "indent_char": " ", "list_bullet": "- ", "list_start": "├ ", "list_continue": "│ ", "list_end": "└ ", "list_empty": "---"} + + def pretty_actor_list(t): + return LeappDataPrinter.format_list(t, **list_style) + + def lstrip_lines(s): + return '\n'.join(line.lstrip() for line in s.split('\n')) + actor_fmt = [ - # (field to print, field, default value, fn) ("Actor", "name", "", None, False), ("Class", "class_name", "", None, False), - ("Description", "description", "", None, True), + ("Description", "description", "", lstrip_lines, True), ("Path", "path", "", None, False), ("Exit Status", "exit_status", None, None, False), ("Executed", "stamp", None, lambda _: _ is not None, False), ("Started", "stamp", None, lambda _: _ or "---", False), ("Phase", "phase", "", None, False), - ("Tags", "tags", (), lambda _: format_list(_, **list_style), True), - ("Consumes", "consumes", (), lambda _: format_list(_, **list_style), True), - ("Produces", "produces", (), lambda _: format_list(_, **list_style), True), - ("Produced", "msgs", (), lambda _: format_list(_, **list_style), True), + ("Tags", "tags", (), pretty_actor_list, True), + ("Consumes", "consumes", (), pretty_actor_list, True), + ("Produces", "produces", (), pretty_actor_list, True), + ("Produced", "msgs", (), pretty_actor_list, True), ] - print(format_dict(actor, actor_fmt)) + print(LeappDataPrinter.format_dict(actor, actor_fmt)) print() dialogs = self.get_dialogs(actor_name) @@ -743,20 +753,20 @@ class LeappDataPrinter(LeappDatabase): ("Key", "key", "", None, False), ("Value", "value", "", None, False), ] - return format_dict(comp, format=fmt) + return LeappDataPrinter.format_dict(comp, fmt=fmt) def dialog_fmt(dialog): fmt = [ ("Title", "title", "", None, False), ("Reason", "reason", "", None, False), - ("Components", "components", [], lambda _: format_list( + ("Components", "components", [], lambda _: LeappDataPrinter.format_list( _, fmt=component_fmt, **list_style), True), ] - return format_dict(dialog["data"], format=fmt) + return LeappDataPrinter.format_dict(dialog["data"], fmt=fmt) if dialogs: print("Dialogs:") - print(format_list(dialogs, fmt=dialog_fmt, **list_style)) + print(LeappDataPrinter.format_list(dialogs, fmt=dialog_fmt, **list_style)) print() # NOTE(pstodulk): That regular is terrible, I know @@ -826,20 +836,20 @@ class LeappDataPrinter(LeappDatabase): ("Name", "name", "", None, False), ("Class name", "class_name", "", None, False), ("Index", "index", "", None, False), - ("Policies", "policies", [], format_dict, True), - ("Flags", "flags", [], format_dict, True), - ("Filter", "filter", [], format_dict, True), + ("Policies", "policies", [], LeappDataPrinter.format_dict, True), + ("Flags", "flags", [], LeappDataPrinter.format_dict, True), + ("Filter", "filter", [], LeappDataPrinter.format_dict, True), ] - return format_dict(phase, format=fmt) + return LeappDataPrinter.format_dict(phase, fmt=fmt) workflow_fmt = [ ("Name", "name", "", None, False), ("Short Name", "short_name", "", None, False), ("Tag", "tag", "", None, False), ("Description", "description", "", None, False), - ("Phases", "phases", [], lambda _: format_list(_, fmt=phase_fmt, **list_style), True) + ("Phases", "phases", [], lambda _: LeappDataPrinter.format_list(_, fmt=phase_fmt, **list_style), True) ] - print(format_dict(workflow, format=workflow_fmt)) + print(LeappDataPrinter.format_dict(workflow, fmt=workflow_fmt)) def print_workflows(self, log_level=LogLevels.DEBUG, terminal_like_logs=True): """ @@ -1244,8 +1254,25 @@ class ActorsCLI(SubCommandBaseClass): } if cmdline.list: - for actor in sorted(actors_dict[cmdline.list]()): - print(" {}".format(actor)) + actors = actors_dict[cmdline.list]() + # NOTE(dkubek): uncomment if want to print in alphabetical order + # actors = sorted(actors) + for actor in actors: + exit_status = self.LeappDatabase.get_actor_exit_status(actor) + + symbol = "○" if exit_status is None else "●" + color = Color.blue + if exit_status == 0: + color = Color.green + elif exit_status == 1: + color = Color.red + + indicator = "" + if sys.stdout.isatty(): + indicator = "{color}{symbol}{reset} ".format( + color=color, symbol=symbol, reset=Color.reset) + + print("{}{}".format(indicator, actor)) return if cmdline.actor: self.LeappDataPrinter.print_actor( From 81609b8080167fc85f57be5fef01af01ed30e77a Mon Sep 17 00:00:00 2001 From: David Kubek Date: Mon, 15 Jul 2024 10:42:24 +0200 Subject: [PATCH 8/9] Add docs --- scripts/leappinspector/leapp-inspector | 88 +++++++++++++++++--------- 1 file changed, 58 insertions(+), 30 deletions(-) diff --git a/scripts/leappinspector/leapp-inspector b/scripts/leappinspector/leapp-inspector index 74137db..f91aadd 100755 --- a/scripts/leappinspector/leapp-inspector +++ b/scripts/leappinspector/leapp-inspector @@ -246,9 +246,9 @@ class LeappDatabase(Database): raise NotImplementedError() def get_entities(self, kind): - # TODO: doc what kind of entities """ Get a list of entities of the given kind for the selected execution. + Currently entities are of kind: actor, metadata. """ cmd = ("SELECT name FROM entity" @@ -261,9 +261,7 @@ class LeappDatabase(Database): def get_entity_metadata(self, kind, name): """ - Get information about specific actor including all metadata. - - # TODO: kind is one of actor, workflow .... + Get metadata of the specified entity. """ cmd = ("SELECT metadata.metadata FROM entity" @@ -292,9 +290,12 @@ class LeappDatabase(Database): return self.get_entities('actor') def get_actor_exit_status(self, actor_name): - # TODO: doc """ - Return the exit status of the actor. + Return the exit status of the actor. Can be either: + + - 0: success or handled failure + - 1: unhandled failure + - None: not specified (unexecuted) """ event = next(self._filter_data(actor=actor_name, event="actor-exit-status"), None) @@ -371,7 +372,9 @@ class LeappDatabase(Database): return None def get_dialogs(self, scope=None): - # TODO: doc + """ + Get information about dialogs for the current context. + """ cond = ["context = '{}'".format(self._context)] if scope: @@ -516,7 +519,12 @@ class LeappDataPrinter(LeappDatabase): @staticmethod def _format(elem, fmt): """ - Format given element using given format specification + Format given object `elem` using given format specification `fmt`. The + format specification can be either: + - None: the elem will just be converted to string using __str__ + - function `f`: returns `f(elem)` (should return string) + - format specification for `list` or `dict`: see + `LeappDataPrinter.format_list` and `LeappDataPrinter.format_dict` """ if fmt is None: @@ -542,11 +550,20 @@ class LeappDataPrinter(LeappDatabase): list_empty="---", ): """ - Format given list + Format given list. - transform can be: - - function => applies function to all elements of the list - - list => assumes a list of transforms. must be same size az the list + The `fmt` can be either: + - function: applies the function to all elements of the list + - list: assumes a list of same size where each element is a format + specification for the corresponding element + + The list can be styled using: + - indent_chat: whitespace character + - list_bullet: default bullet for list elements + - list_start: first bullet in the list (default: list_bullet) + - list_continue: character to join subsequent bullets (default: indent char) + - list_end: character of last bullet (default: list_bullet) + - list_empty: string to place when the given list is empty """ list_start = list_start or list_bullet @@ -720,7 +737,7 @@ class LeappDataPrinter(LeappDatabase): "list_start": "├ ", "list_continue": "│ ", "list_end": "└ ", "list_empty": "---"} - def pretty_actor_list(t): + def pretty_list(t): return LeappDataPrinter.format_list(t, **list_style) def lstrip_lines(s): @@ -735,10 +752,10 @@ class LeappDataPrinter(LeappDatabase): ("Executed", "stamp", None, lambda _: _ is not None, False), ("Started", "stamp", None, lambda _: _ or "---", False), ("Phase", "phase", "", None, False), - ("Tags", "tags", (), pretty_actor_list, True), - ("Consumes", "consumes", (), pretty_actor_list, True), - ("Produces", "produces", (), pretty_actor_list, True), - ("Produced", "msgs", (), pretty_actor_list, True), + ("Tags", "tags", (), pretty_list, True), + ("Consumes", "consumes", (), pretty_list, True), + ("Produces", "produces", (), pretty_list, True), + ("Produced", "msgs", (), pretty_list, True), ] print(LeappDataPrinter.format_dict(actor, actor_fmt)) print() @@ -781,8 +798,8 @@ class LeappDataPrinter(LeappDatabase): if cmds: print("Executed shell commands:") - for cmd in cmds: - print(" - {}".format(cmd)) + print(LeappDataPrinter.format_list(cmds)) + print() logs = self.get_logs(actor=actor_name, log_level=log_level) for i, log in enumerate(logs): @@ -794,7 +811,7 @@ class LeappDataPrinter(LeappDatabase): else: print("--- {}".format(log["data"])) - def print_actors(self, log_level=LogLevels.DEBUG, terminal_like_logs=True): + def print_actors(self, log_level=LogLevels.DEBUG, terminal_like_logs=True, phase=None): """ Print various information about actors """ @@ -811,7 +828,7 @@ class LeappDataPrinter(LeappDatabase): print() self._print_tail() - def print_workflow(self, workflow_name): + def print_workflow(self, workflow_name, short=False): """ Print information about the specified workflow. """ @@ -831,7 +848,7 @@ class LeappDataPrinter(LeappDatabase): "list_start": "│\n● ", "list_continue": "│ ", "list_end": "└ ", "list_empty": "---"} - def phase_fmt(phase): + def phase_fmt_long(phase): fmt = [ ("Name", "name", "", None, False), ("Class name", "class_name", "", None, False), @@ -842,6 +859,12 @@ class LeappDataPrinter(LeappDatabase): ] return LeappDataPrinter.format_dict(phase, fmt=fmt) + def phase_fmt_short(phase): + return phase.get("name", "") + + phase_fmt = phase_fmt_short if short else phase_fmt_long + list_style = {} if short else list_style + workflow_fmt = [ ("Name", "name", "", None, False), ("Short Name", "short_name", "", None, False), @@ -851,14 +874,14 @@ class LeappDataPrinter(LeappDatabase): ] print(LeappDataPrinter.format_dict(workflow, fmt=workflow_fmt)) - def print_workflows(self, log_level=LogLevels.DEBUG, terminal_like_logs=True): + def print_workflows(self, short=False): """ - Print various information about workflows + Print various information about workflows. """ workflows = self.get_workflows() self._print_header("DISCOVERED WORKFLOWS") try: - self.print_workflow(workflows.pop(0)) + self.print_workflow(workflows.pop(0), short=short) for workflow_name in workflows: self._print_separator() self.print_workflow(workflow_name) @@ -1145,22 +1168,25 @@ class WorkflowsCLI(SubCommandBaseClass): def set_arguments(self): self.subparser.add_argument("--list", dest="list", action="store_true", - help="List types of all produced messages.") + help="List all discovered workflows.") self.subparser.add_argument("--workflow", dest="workflow", default=None, help="Print information about selected workflow.") + self.subparser.add_argument("--short", dest="short", action="store_true", + help="Print only names of phases.") def process(self): cmdline = self.li_cli.cmdline if cmdline.list: - for actor in sorted(self.LeappDatabase.get_models()): - print(" {}".format(actor)) + for workflow in sorted(self.LeappDatabase.get_workflows()): + print(" {}".format(workflow)) return if cmdline.workflow: - self.LeappDataPrinter.print_workflow(cmdline.workflow) + self.LeappDataPrinter.print_workflow(cmdline.workflow, short=cmdline.short) + return - self.LeappDataPrinter.print_workflows() + self.LeappDataPrinter.print_workflows(short=cmdline.short) class MessagesCLI(SubCommandBaseClass): @@ -1274,6 +1300,7 @@ class ActorsCLI(SubCommandBaseClass): print("{}{}".format(indicator, actor)) return + if cmdline.actor: self.LeappDataPrinter.print_actor( cmdline.actor, @@ -1281,6 +1308,7 @@ class ActorsCLI(SubCommandBaseClass): terminal_like_logs=cmdline.terminal_like ) return + self.LeappDataPrinter.print_actors( log_level=self._log_level_map[cmdline.log_level], terminal_like_logs=cmdline.terminal_like From af466b38a79e71d756232fb3470f63928f6555d3 Mon Sep 17 00:00:00 2001 From: David Kubek Date: Thu, 10 Oct 2024 11:05:13 +0200 Subject: [PATCH 9/9] Add actor configuration support --- scripts/leappinspector/leapp-inspector | 36 ++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/scripts/leappinspector/leapp-inspector b/scripts/leappinspector/leapp-inspector index f91aadd..b2100af 100755 --- a/scripts/leappinspector/leapp-inspector +++ b/scripts/leappinspector/leapp-inspector @@ -733,8 +733,8 @@ class LeappDataPrinter(LeappDatabase): color=color, indicator=indicator, reset=Color.reset, actor_name=actor_name)) list_style = { - "indent_char": " ", "list_bullet": "- ", - "list_start": "├ ", "list_continue": "│ ", "list_end": "└ ", + "indent_char": " ", "list_bullet": "── ", + "list_start": "├─ ", "list_continue": "│ ", "list_end": "└─ ", "list_empty": "---"} def pretty_list(t): @@ -743,6 +743,36 @@ class LeappDataPrinter(LeappDatabase): def lstrip_lines(s): return '\n'.join(line.lstrip() for line in s.split('\n')) + def recursive_format_dict(field): + if not isinstance(field, dict): + if isinstance(field, str): + return lstrip_lines(field) + return field + + ans = {} + for key in field: + ans[key] = recursive_format_dict(field[key]) + + return LeappDataPrinter.format_dict(ans) + + def format_config(config): + config_fmt = [ + ("Class", "class_name", "", None, False), + ("Section", "section", "", None, False), + ("Name", "name", "", None, False), + ("Description", "description", "", lstrip_lines, True), + ("Type", "type", "", recursive_format_dict, False), + ("Default", "default", "", None, False), + ] + + return LeappDataPrinter.format_dict(config, fmt=config_fmt) + + def format_config(config): + return recursive_format_dict(config) + + def format_config_schemas(t): + return LeappDataPrinter.format_list(t, fmt=format_config, **list_style) + actor_fmt = [ ("Actor", "name", "", None, False), ("Class", "class_name", "", None, False), @@ -753,6 +783,8 @@ class LeappDataPrinter(LeappDatabase): ("Started", "stamp", None, lambda _: _ or "---", False), ("Phase", "phase", "", None, False), ("Tags", "tags", (), pretty_list, True), + ("Config Schemas", "config_schemas", (), format_config_schemas, True), + ("Config", "config", {}, format_config, True), ("Consumes", "consumes", (), pretty_list, True), ("Produces", "produces", (), pretty_list, True), ("Produced", "msgs", (), pretty_list, True),