From d548d3e41d748f88c1caf13031863372ec4aa88c Mon Sep 17 00:00:00 2001 From: Jacob Callahan Date: Sat, 6 Dec 2025 10:33:13 -0500 Subject: [PATCH] Enhance provider help output with Rich formatting Features: - Beaker provider help: - Display individual job XML with syntax highlighting using `rich.syntax.Syntax`. - Format lists of available jobs into `rich` tables. - Container provider help: - Present detailed image information in `rich` tables. - Display image configuration as syntax-highlighted YAML. - Convert lists of available host and app images into `rich` tables. - Add support for the `container_app` parameter to retrieve details for a specific container application. - Foreman provider help: - Render lists of hostgroups and individual hostgroup details in `rich` tables. Refactoring: - Replaced direct `logging.info` calls for user-facing output in `provider_help` methods with `rich.console.Console` for improved presentation. - Removed the `results_limit` parameter, as `rich` tables manage display and filtering more effectively. Configuration: - Integrated `_settings.less_colors` to control color output in the `rich` console, allowing users to disable colors. --- broker/providers/beaker.py | 21 +++++++++--- broker/providers/container.py | 64 +++++++++++++++++++++++++++++------ broker/providers/foreman.py | 55 ++++++++++++++++++++++-------- 3 files changed, 111 insertions(+), 29 deletions(-) diff --git a/broker/providers/beaker.py b/broker/providers/beaker.py index a461827..95b1905 100644 --- a/broker/providers/beaker.py +++ b/broker/providers/beaker.py @@ -5,7 +5,9 @@ import click from dynaconf import Validator +from rich.console import Console from rich.progress import track +from rich.syntax import Syntax logger = logging.getLogger(__name__) @@ -129,17 +131,28 @@ def submit_job(self, max_wait=None, **kwargs): def provider_help(self, jobs=False, job=None, **kwargs): """Print useful information from the Beaker provider.""" - results_limit = kwargs.get("results_limit", self._settings.container.results_limit) + rich_console = Console(no_color=self._settings.less_colors) if job: if not job.startswith("J:"): job = f"J:{job}" - logger.info(self.runtime.job_clone(job, prettyxml=True, dryrun=True).stdout) + job_xml = self.runtime.job_clone(job, prettyxml=True, dryrun=True).stdout + syntax = Syntax(job_xml, "xml", theme="monokai", line_numbers=True) + rich_console.print(syntax) elif jobs: result = self.runtime.job_list(**kwargs).stdout.splitlines() if res_filter := kwargs.get("results_filter"): result = helpers.eval_filter(result, res_filter, "res") - result = "\n".join(result[:results_limit]) - logger.info(f"Available jobs:\n{result}") + result = result if isinstance(result, list) else [result] + if not result: + logger.warning("No jobs found!") + return + job_table = helpers.dictlist_to_table( + [{"name": j} for j in result], + title="Available Jobs", + _id=False, + headers=False, + ) + rich_console.print(job_table) def release(self, host_name, job_id): """Release a hosts reserved from Beaker by cancelling the job.""" diff --git a/broker/providers/container.py b/broker/providers/container.py index 60fbba7..d3779d5 100644 --- a/broker/providers/container.py +++ b/broker/providers/container.py @@ -8,7 +8,9 @@ import click from dynaconf import Validator +from rich.console import Console from rich.progress import track +from rich.syntax import Syntax logger = logging.getLogger(__name__) @@ -231,15 +233,41 @@ def construct_host(self, provider_params, host_classes, **kwargs): return host_inst def provider_help( - self, container_hosts=False, container_host=None, container_apps=False, **kwargs + self, + container_hosts=False, + container_host=None, + container_apps=False, + container_app=None, + **kwargs, ): """Return useful information about container images.""" - results_limit = kwargs.get("results_limit", self._settings.Container.results_limit) - if container_host: - logger.info( - f"Information for {container_host} container-host:\n" - f"{helpers.yaml_format(self.runtime.image_info(container_host))}" + rich_console = Console(no_color=self._settings.less_colors) + if container_host or container_app: + image_name = container_host or container_app + image_info = self.runtime.image_info(image_name) + if not image_info: + logger.warning(f"Image {image_name} not found!") + return + # Extract config separately for special formatting + config = image_info.pop("config", {}) + # Display basic info in a table + info_table = helpers.dict_to_table( + image_info, + title=f"{image_name} Information", ) + rich_console.print(info_table) + # Display config as syntax-highlighted YAML + if config: + config_yaml = helpers.yaml_format(config) + syntax = Syntax( + config_yaml, + "yaml", + theme="monokai", + line_numbers=False, + background_color="default", + ) + rich_console.print("\n[bold]Image Configuration[/bold]") + rich_console.print(syntax) elif container_hosts: images = [ img.tags[0] @@ -249,15 +277,31 @@ def provider_help( if res_filter := kwargs.get("results_filter"): images = helpers.eval_filter(images, res_filter, "res") images = images if isinstance(images, list) else [images] - images = "\n".join(images[:results_limit]) - logger.info(f"Available host images:\n{images}") + if not images: + logger.warning("No host images found!") + return + image_table = helpers.dictlist_to_table( + [{"name": img} for img in images], + title="Available Host Images", + _id=False, + headers=False, + ) + rich_console.print(image_table) elif container_apps: images = [img.tags[0] for img in self.runtime.images if img.tags] if res_filter := kwargs.get("results_filter"): images = helpers.eval_filter(images, res_filter, "res") images = images if isinstance(images, list) else [images] - images = "\n".join(images[:results_limit]) - logger.info(f"Available app images:\n{images}") + if not images: + logger.warning("No app images found!") + return + image_table = helpers.dictlist_to_table( + [{"name": img} for img in images], + title="Available App Images", + _id=False, + headers=False, + ) + rich_console.print(image_table) def get_inventory(self, name_prefix): """Get all containers that have a matching name prefix.""" diff --git a/broker/providers/foreman.py b/broker/providers/foreman.py index 13ddf85..dbb6d73 100644 --- a/broker/providers/foreman.py +++ b/broker/providers/foreman.py @@ -6,10 +6,12 @@ import click from dynaconf import Validator +from rich.console import Console from rich.progress import track logger = logging.getLogger(__name__) +from broker import helpers from broker.binds import foreman from broker.helpers import Result from broker.providers import Provider @@ -161,27 +163,50 @@ def provider_help( **kwargs, ): """Return useful information about Foreman provider.""" + rich_console = Console(no_color=self._settings.less_colors) if hostgroups: all_hostgroups = self.runtime.hostgroups() - logger.info(f"On Foreman {self.instance} you have the following hostgroups:") - for hg in all_hostgroups["results"]: - logger.info(f"- {hg['title']}") - elif hostgroup: - logger.info( - f"On Foreman {self.instance} the hostgroup {hostgroup} has the following properties:" + if not all_hostgroups.get("results"): + logger.warning("No hostgroups found!") + return + hostgroup_names = [hg["title"] for hg in all_hostgroups["results"]] + if res_filter := kwargs.get("results_filter"): + hostgroup_names = helpers.eval_filter(hostgroup_names, res_filter, "res") + hostgroup_names = ( + hostgroup_names if isinstance(hostgroup_names, list) else [hostgroup_names] + ) + if not hostgroup_names: + logger.warning("No hostgroups found!") + return + hostgroup_table = helpers.dictlist_to_table( + [{"name": hg} for hg in hostgroup_names], + title=f"Available Hostgroups on {self.instance}", + _id=False, + headers=False, ) + rich_console.print(hostgroup_table) + elif hostgroup: data = self.runtime.hostgroup(name=hostgroup) + if not data: + logger.warning(f"Hostgroup {hostgroup} not found!") + return fields_of_interest = { - "description": "description", - "operating_system": "operatingsystem_name", - "domain": "domain_name", - "subnet": "subnet_name", - "subnet6": "subnet6_name", + "Description": "description", + "Operating System": "operatingsystem_name", + "Domain": "domain_name", + "Subnet": "subnet_name", + "Subnet6": "subnet6_name", } - for name, field in fields_of_interest.items(): - value = data.get(field, False) - if value: - logger.info(f" {name}: {value}") + display_data = { + name: data.get(field, "N/A") + for name, field in fields_of_interest.items() + if data.get(field) + } + hostgroup_table = helpers.dict_to_table( + display_data, + title=f"{hostgroup} Information", + ) + rich_console.print(hostgroup_table) def _compile_host_info(self, host): return {