From 707fca772e9265d95fa5d5b9a8fbe3b4b6023d18 Mon Sep 17 00:00:00 2001 From: Thobias Salazar Trevisan Date: Fri, 8 Aug 2025 15:05:00 -0300 Subject: [PATCH 1/9] refactor - new formatters methods --- src/openstack_lb_info/formatters.py | 114 +++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/src/openstack_lb_info/formatters.py b/src/openstack_lb_info/formatters.py index c3c7935..95af4a8 100644 --- a/src/openstack_lb_info/formatters.py +++ b/src/openstack_lb_info/formatters.py @@ -11,7 +11,7 @@ import contextlib import json -import re +import re # delete after from abc import ABC, abstractmethod try: @@ -59,6 +59,46 @@ def rule(self, title, align="center"): def format_status(self, status): """Format status text.""" + @abstractmethod + def add_details_to_tree(self, tree, details_dict): + """Adds all attributes from an object to the tree.""" + pass # pylint: disable=unnecessary-pass + + @abstractmethod + def add_empty_node(self, tree, resource_name): + """Adds a placeholder for a missing resource.""" + pass # pylint: disable=unnecessary-pass + + @abstractmethod + def add_lb_to_tree(self, lb): + """Create and return the root tree for the Load Balancer.""" + pass # pylint: disable=unnecessary-pass + + @abstractmethod + def add_listener_to_tree(self, parent_tree, listener): + """Adds a formatted listener node to the tree.""" + pass # pylint: disable=unnecessary-pass + + @abstractmethod + def add_pool_to_tree(self, parent_tree, pool): + """Adds a formatted pool node to the tree.""" + pass # pylint: disable=unnecessary-pass + + @abstractmethod + def add_health_monitor_to_tree(self, parent_tree, hm): + """Adds a formatted health monitor node to the tree.""" + pass # pylint: disable=unnecessary-pass + + @abstractmethod + def add_member_to_tree(self, parent_tree, member): + """Adds a formatted member node to the tree.""" + pass # pylint: disable=unnecessary-pass + + @abstractmethod + def add_amphora_to_tree(self, parent_tree, amphora, server, image_name): + """Adds a formatted amphora node to the tree.""" + pass # pylint: disable=unnecessary-pass + class RichOutputFormatter(OutputFormatter): """Formatter using the Rich library.""" @@ -103,6 +143,30 @@ def format_status(self, status): color = status_colors.get(status, "red") return f"[{color}]{status}[/{color}]" + def add_details_to_tree(self, tree, details_dict): + pass + + def add_empty_node(self, tree, resource_name): + pass + + def add_lb_to_tree(self, lb): + pass + + def add_listener_to_tree(self, parent_tree, listener): + pass + + def add_pool_to_tree(self, parent_tree, pool): + pass + + def add_health_monitor_to_tree(self, parent_tree, hm): + pass + + def add_member_to_tree(self, parent_tree, member): + pass + + def add_amphora_to_tree(self, parent_tree, amphora, server, image_name): + pass + class PlainOutputFormatter(OutputFormatter): """Formatter for plain text output.""" @@ -154,6 +218,30 @@ def format_message(self, message): def format_status(self, status): return status + def add_details_to_tree(self, tree, details_dict): + pass + + def add_empty_node(self, tree, resource_name): + pass + + def add_lb_to_tree(self, lb): + pass + + def add_listener_to_tree(self, parent_tree, listener): + pass + + def add_pool_to_tree(self, parent_tree, pool): + pass + + def add_health_monitor_to_tree(self, parent_tree, hm): + pass + + def add_member_to_tree(self, parent_tree, member): + pass + + def add_amphora_to_tree(self, parent_tree, amphora, server, image_name): + pass + class JSONOutputFormatter(OutputFormatter): """Formatter for JSON output.""" @@ -208,5 +296,29 @@ def format_message(self, message): return clean_message return message + def add_details_to_tree(self, tree, details_dict): + pass + + def add_empty_node(self, tree, resource_name): + pass + + def add_lb_to_tree(self, lb): + pass + + def add_listener_to_tree(self, parent_tree, listener): + pass + + def add_pool_to_tree(self, parent_tree, pool): + pass + + def add_health_monitor_to_tree(self, parent_tree, hm): + pass + + def add_member_to_tree(self, parent_tree, member): + pass + + def add_amphora_to_tree(self, parent_tree, amphora, server, image_name): + pass + # vim: ts=4 From a24d7e5386ace3f9ca2e22d3eade8d2e16bca3ee Mon Sep 17 00:00:00 2001 From: Thobias Salazar Trevisan Date: Fri, 8 Aug 2025 15:39:44 -0300 Subject: [PATCH 2/9] refactor LoadBalancerInfo class to use new fmt methods --- src/openstack_lb_info/formatters.py | 192 +++++++++++++++++---- src/openstack_lb_info/loadbalancer_info.py | 173 ++++++------------- 2 files changed, 210 insertions(+), 155 deletions(-) diff --git a/src/openstack_lb_info/formatters.py b/src/openstack_lb_info/formatters.py index 95af4a8..35fda46 100644 --- a/src/openstack_lb_info/formatters.py +++ b/src/openstack_lb_info/formatters.py @@ -62,37 +62,30 @@ def format_status(self, status): @abstractmethod def add_details_to_tree(self, tree, details_dict): """Adds all attributes from an object to the tree.""" - pass # pylint: disable=unnecessary-pass @abstractmethod def add_empty_node(self, tree, resource_name): """Adds a placeholder for a missing resource.""" - pass # pylint: disable=unnecessary-pass @abstractmethod def add_lb_to_tree(self, lb): """Create and return the root tree for the Load Balancer.""" - pass # pylint: disable=unnecessary-pass @abstractmethod def add_listener_to_tree(self, parent_tree, listener): """Adds a formatted listener node to the tree.""" - pass # pylint: disable=unnecessary-pass @abstractmethod def add_pool_to_tree(self, parent_tree, pool): """Adds a formatted pool node to the tree.""" - pass # pylint: disable=unnecessary-pass @abstractmethod def add_health_monitor_to_tree(self, parent_tree, hm): """Adds a formatted health monitor node to the tree.""" - pass # pylint: disable=unnecessary-pass @abstractmethod def add_member_to_tree(self, parent_tree, member): """Adds a formatted member node to the tree.""" - pass # pylint: disable=unnecessary-pass @abstractmethod def add_amphora_to_tree(self, parent_tree, amphora, server, image_name): @@ -130,9 +123,9 @@ def line(self): def rule(self, title, align="center"): self.console.rule(title, align=align) - def format_message(self, message): - """Return the message as-is, preserving Rich formatting.""" - return message + # def format_message(self, message): + # """Return the message as-is, preserving Rich formatting.""" + # return message def format_status(self, status): status_colors = { @@ -144,28 +137,86 @@ def format_status(self, status): return f"[{color}]{status}[/{color}]" def add_details_to_tree(self, tree, details_dict): - pass + for attr in sorted(details_dict): + value = details_dict[attr] + content = f"{attr}: {value}" + self.add_to_tree(tree, content, highlight=True) def add_empty_node(self, tree, resource_name): - pass + self.add_to_tree(tree, f"[b green]{resource_name}:[/] None") def add_lb_to_tree(self, lb): - pass + message = ( + f"LB:[bright_yellow] {lb.id}[/] " + f"vip:[bright_cyan]{lb.vip_address}[/] " + f"prov_status:{self.format_status(lb.provisioning_status)} " + f"oper_status:{self.format_status(lb.operating_status)} " + f"tags:[magenta]{lb.tags}[/]" + ) + return self.create_tree(message) def add_listener_to_tree(self, parent_tree, listener): - pass + message = ( + f"[b green]Listener:[/] [b white]{listener.id}[/] " + f"([blue b]{listener.name}[/]) " + f"port:[cyan]{listener.protocol}/{listener.protocol_port}[/] " + f"prov_status:{self.format_status(listener.provisioning_status)} " + f"oper_status:{self.format_status(listener.operating_status)}" + ) + return self.add_to_tree(parent_tree, message) def add_pool_to_tree(self, parent_tree, pool): - pass + message = ( + f"[b green]Pool:[/] [b white]{pool.id}[/] " + f"protocol:[magenta]{pool.protocol}[/magenta] " + f"algorithm:[magenta]{pool.lb_algorithm}[/magenta] " + f"prov_status:{self.format_status(pool.provisioning_status)} " + f"oper_status:{self.format_status(pool.operating_status)}" + ) + return self.add_to_tree(parent_tree, message) def add_health_monitor_to_tree(self, parent_tree, hm): - pass + message = ( + f"[b green]Health Monitor:[/] [b white]{hm.id}[/] " + f"type:[magenta]{hm.type}[/magenta] " + f"http_method:[magenta]{hm.http_method}[/magenta] " + f"http_codes:[magenta]{hm.expected_codes}[/magenta] " + f"url_path:[magenta]{hm.url_path}[/magenta] " + f"prov_status:{self.format_status(hm.provisioning_status)} " + f"oper_status:{self.format_status(hm.operating_status)}" + ) + return self.add_to_tree(parent_tree, message) def add_member_to_tree(self, parent_tree, member): - pass + message = ( + f"[b green]Member:[/] [b white]{member.id}[/] " + f"IP:[magenta]{member.address}[/magenta] " + f"port:[magenta]{member.protocol_port}[/magenta] " + f"weight:[magenta]{member.weight}[/magenta] " + f"backup:[magenta]{member.backup}[/magenta] " + f"prov_status:{self.format_status(member.provisioning_status)} " + f"oper_status:{self.format_status(member.operating_status)}" + ) + return self.add_to_tree(parent_tree, message) def add_amphora_to_tree(self, parent_tree, amphora, server, image_name): - pass + server_id = server.id if server else "N/A" + server_flavor_name = server.flavor.name if server and server.flavor else "N/A" + server_compute_host = server.compute_host if server else "N/A" + + # pylint: disable=duplicate-code + message = ( + f"[b green]amphora: [/]" + f"[b white]{amphora.id} [/]" + f"{amphora.role} " + f"{amphora.status} " + f"lb_network_ip:[green]{amphora.lb_network_ip} [/]" + f"img:[magenta]{image_name}[/] " + f"server:[magenta]{server_id}[/] " + f"vm_flavor:[magenta]{server_flavor_name}[/] " + f"compute host:([magenta]{server_compute_host}[/])" + ) + return self.add_to_tree(parent_tree, message) class PlainOutputFormatter(OutputFormatter): @@ -219,28 +270,80 @@ def format_status(self, status): return status def add_details_to_tree(self, tree, details_dict): - pass + for attr in sorted(details_dict): + value = details_dict[attr] + content = f"{attr}: {value}" + self.add_to_tree(tree, content) def add_empty_node(self, tree, resource_name): - pass + self.add_to_tree(tree, f"{resource_name}: None") def add_lb_to_tree(self, lb): - pass + message = ( + f"LB: {lb.id} " + f"vip:{lb.vip_address} " + f"prov_status:{self.format_status(lb.provisioning_status)} " + f"oper_status:{self.format_status(lb.operating_status)} " + f"tags:{lb.tags}" + ) + return self.create_tree(message) def add_listener_to_tree(self, parent_tree, listener): - pass + message = ( + f"Listener: {listener.id} ({listener.name}) " + f"port:{listener.protocol}/{listener.protocol_port} " + f"prov_status:{self.format_status(listener.provisioning_status)} " + f"oper_status:{self.format_status(listener.operating_status)}" + ) + return self.add_to_tree(parent_tree, message) def add_pool_to_tree(self, parent_tree, pool): - pass + message = ( + f"Pool: {pool.id} " + f"protocol:{pool.protocol} " + f"algorithm:{pool.lb_algorithm} " + f"prov_status:{self.format_status(pool.provisioning_status)} " + f"oper_status:{self.format_status(pool.operating_status)}" + ) + return self.add_to_tree(parent_tree, message) def add_health_monitor_to_tree(self, parent_tree, hm): - pass + message = ( + f"Health Monitor: {hm.id} " + f"type:{hm.type} " + f"http_method:{hm.http_method} " + f"http_codes:{hm.expected_codes} " + f"url_path:{hm.url_path} " + f"prov_status:{self.format_status(hm.provisioning_status)} " + f"oper_status:{self.format_status(hm.operating_status)}" + ) + return self.add_to_tree(parent_tree, message) def add_member_to_tree(self, parent_tree, member): - pass + message = ( + f"Member: {member.id} " + f"IP:{member.address} " + f"port:{member.protocol_port} " + f"weight:{member.weight} " + f"backup:{member.backup} " + f"prov_status:{self.format_status(member.provisioning_status)} " + f"oper_status:{self.format_status(member.operating_status)}" + ) + return self.add_to_tree(parent_tree, message) def add_amphora_to_tree(self, parent_tree, amphora, server, image_name): - pass + server_id = server.id if server else "N/A" + server_flavor_name = server.flavor.name if server and server.flavor else "N/A" + server_compute_host = server.compute_host if server else "N/A" + message = ( + f"amphora: {amphora.id} {amphora.role} {amphora.status} " + f"lb_network_ip:{amphora.lb_network_ip} " + f"img:{image_name} " + f"server:{server_id} " + f"vm_flavor:{server_flavor_name} " + f"compute host:({server_compute_host})" + ) + return self.add_to_tree(parent_tree, message) class JSONOutputFormatter(OutputFormatter): @@ -296,29 +399,52 @@ def format_message(self, message): return clean_message return message + def _add_node_from_obj(self, parent_node, node_type, resource_obj): + node = resource_obj.to_dict() + node["type"] = node_type + if "children" not in node: + node["children"] = [] + parent_node["children"].append(node) + return node + def add_details_to_tree(self, tree, details_dict): pass def add_empty_node(self, tree, resource_name): - pass + tree["children"].append({f"{resource_name.lower().replace(' ', '_')}": None}) def add_lb_to_tree(self, lb): - pass + root_node = lb.to_dict() + root_node["type"] = "loadbalancer" + root_node["children"] = [] + return root_node def add_listener_to_tree(self, parent_tree, listener): - pass + return self._add_node_from_obj(parent_tree, "listener", listener) def add_pool_to_tree(self, parent_tree, pool): - pass + return self._add_node_from_obj(parent_tree, "pool", pool) def add_health_monitor_to_tree(self, parent_tree, hm): - pass + return self._add_node_from_obj(parent_tree, "health_monitor", hm) def add_member_to_tree(self, parent_tree, member): - pass + return self._add_node_from_obj(parent_tree, "member", member) def add_amphora_to_tree(self, parent_tree, amphora, server, image_name): - pass + node = amphora.to_dict() + node["type"] = "amphora" + node["image_name"] = image_name + if server: + node["server_details"] = { + "id": server.id, + "flavor": server.flavor.name if server.flavor else "N/A", + "compute_host": server.compute_host, + } + else: + node["server_details"] = None + parent_tree["children"].append(node) + return node # vim: ts=4 diff --git a/src/openstack_lb_info/loadbalancer_info.py b/src/openstack_lb_info/loadbalancer_info.py index 807e7fb..169afeb 100644 --- a/src/openstack_lb_info/loadbalancer_info.py +++ b/src/openstack_lb_info/loadbalancer_info.py @@ -58,38 +58,6 @@ def _add_all_attr_to_tree(self, obj, tree): content = f"{attr}: {value}" self.formatter.add_to_tree(tree, content, highlight=True) - # pylint: disable=too-many-arguments - def _retrieve_and_add_to_tree(self, label, resource_id, retrieve_method, tree, format_fn): - """ - Generic helper to retrieve a resource, add its formatted information to a tree. - - This method displays a status message while retrieving a resource via the provided API call. - If the resource is found, its formatted information is added to the specified tree node. In - detailed mode, all resource attributes are appended to the tree node as well. - - Args: - label (str): The resource label (e.g., "Listener, "Health Monitor", ...). - resource_id (str): The ID of the resource to retrieve. - retrieve_method (Callable): The API method used to retrieve the resource. - tree: The tree node to which the resource's info will be added. - format_fn (Callable): A function that takes the resource and returns a formatted string. - - Returns: - The retrieved resource object if found; otherwise, returns None. - """ - with self.formatter.status(f"Getting {label} details id [b]{resource_id}[/b]"): - resource = retrieve_method(resource_id) - - if resource: - resource_tree = self.formatter.add_to_tree(tree, format_fn(resource)) - if self.details: - self._add_all_attr_to_tree(resource, resource_tree) - return resource - - self.formatter.add_to_tree(tree, f"[b green]{label}:[/] None") - - return None - def create_lb_tree(self): """ Create a tree representing Load Balancer information. @@ -97,19 +65,13 @@ def create_lb_tree(self): Returns: Tree: A tree object representing Load Balancer information. """ - self.lb_tree = self.formatter.create_tree( - f"LB:[bright_yellow] {self.lb.id}[/] " - f"vip:[bright_cyan]{self.lb.vip_address}[/] " - f"prov_status:{self.formatter.format_status(self.lb.provisioning_status)} " - f"oper_status:{self.formatter.format_status(self.lb.operating_status)} " - f"tags:[magenta]{self.lb.tags}[/]" - ) + self.lb_tree = self.formatter.add_lb_to_tree(self.lb) if self.details: - self._add_all_attr_to_tree(self.lb, self.lb_tree) + self.formatter.add_details_to_tree(self.lb_tree, self.lb.to_dict()) return self.lb_tree - def add_listener_info(self, listener_id): + def add_listener_info(self, parent_tree, listener_id): """ Add information about a Listener to the Load Balancer tree. @@ -120,30 +82,22 @@ def add_listener_info(self, listener_id): Returns: None """ + with self.formatter.status(f"Getting Listener details id [b]{listener_id}[/b]"): + listener = self.openstack_api.retrieve_listener(listener_id) - def format_listener(listener): - return ( - f"[b green]Listener:[/] [b white]{listener.id}[/] " - f"([blue b]{listener.name}[/]) " - f"port:[cyan]{listener.protocol}/{listener.protocol_port}[/] " - f"prov_status:{self.formatter.format_status(listener.provisioning_status)} " - f"oper_status:{self.formatter.format_status(listener.operating_status)}" - ) - - listener = self._retrieve_and_add_to_tree( - "Listener", - listener_id, - self.openstack_api.retrieve_listener, - self.lb_tree, - format_listener, - ) if listener: + listener_tree = self.formatter.add_listener_to_tree(parent_tree, listener) + if self.details: + self.formatter.add_details_to_tree(listener_tree, listener.to_dict()) + if listener.default_pool_id: - self.add_pool_info(self.lb_tree, listener.default_pool_id) + self.add_pool_info(listener_tree, listener.default_pool_id) else: - self.formatter.add_to_tree(self.lb_tree, "[b green]Pool:[/] None") + self.formatter.add_empty_node(listener_tree, "Pool") + else: + self.formatter.add_empty_node(parent_tree, "Listener") - def add_pool_info(self, tree, pool_id): + def add_pool_info(self, parent_tree, pool_id): """ Add information about a Pool to the Load Balancer tree. @@ -154,31 +108,27 @@ def add_pool_info(self, tree, pool_id): Returns: None """ + with self.formatter.status(f"Getting Pool details id [b]{pool_id}[/b]"): + pool = self.openstack_api.retrieve_pool(pool_id) - def format_pool(pool): - return ( - f"[b green]Pool:[/] [b white]{pool.id}[/] " - f"protocol:[magenta]{pool.protocol}[/magenta] " - f"algorithm:[magenta]{pool.lb_algorithm}[/magenta] " - f"prov_status:{self.formatter.format_status(pool.provisioning_status)} " - f"oper_status:{self.formatter.format_status(pool.operating_status)}" - ) - - pool = self._retrieve_and_add_to_tree( - "Pool", pool_id, self.openstack_api.retrieve_pool, tree, format_pool - ) if pool: + pool_tree = self.formatter.add_pool_to_tree(parent_tree, pool) + if self.details: + self.formatter.add_details_to_tree(pool_tree, pool.to_dict()) + if pool.health_monitor_id: - self.add_health_monitor_info(tree, pool.health_monitor_id) + self.add_health_monitor_info(pool_tree, pool.health_monitor_id) else: - self.formatter.add_to_tree(tree, "[b green]Health Monitor:[/] None") + self.formatter.add_empty_node(pool_tree, "Health Monitor") if pool.members: - self.add_pool_members(tree, pool.id, pool.members) + self.add_pool_members(pool_tree, pool.id, pool.members) else: - self.formatter.add_to_tree(tree, "[b green]Member:[/] None") + self.formatter.add_empty_node(pool_tree, "Member") + else: + self.formatter.add_empty_node(parent_tree, "Pool") - def add_health_monitor_info(self, pool_tree, health_monitor_id): + def add_health_monitor_info(self, parent_tree, health_monitor_id): """ Add information about a Health Monitor to a Pool tree. @@ -189,27 +139,17 @@ def add_health_monitor_info(self, pool_tree, health_monitor_id): Returns: None """ + with self.formatter.status(f"Getting Health Monitor details id [b]{health_monitor_id}[/b]"): + hm = self.openstack_api.retrieve_health_monitor(health_monitor_id) - def format_health_monitor(hm): - return ( - f"[b green]Health Monitor:[/] [b white]{hm.id}[/] " - f"type:[magenta]{hm.type}[/magenta] " - f"http_method:[magenta]{hm.http_method}[/magenta] " - f"http_codes:[magenta]{hm.expected_codes}[/magenta] " - f"url_path:[magenta]{hm.url_path}[/magenta] " - f"prov_status:{self.formatter.format_status(hm.provisioning_status)} " - f"oper_status:{self.formatter.format_status(hm.operating_status)}" - ) - - self._retrieve_and_add_to_tree( - "Health Monitor", - health_monitor_id, - self.openstack_api.retrieve_health_monitor, - pool_tree, - format_health_monitor, - ) + if hm: + hm_tree = self.formatter.add_health_monitor_to_tree(parent_tree, hm) + if self.details: + self.formatter.add_details_to_tree(hm_tree, hm.to_dict()) + else: + self.formatter.add_empty_node(parent_tree, "Health Monitor") - def add_pool_members(self, pool_tree, pool_id, pool_members): + def add_pool_members(self, parent_tree, pool_id, pool_members): """ Add information about Members of a Pool to the Pool tree. @@ -222,28 +162,17 @@ def add_pool_members(self, pool_tree, pool_id, pool_members): Returns: None """ - for member in pool_members: - with self.formatter.status(f"Getting member details id [b]{member['id']}[/b]"): - os_m = self.openstack_api.retrieve_member(member["id"], pool_id) - - def format_member(m): - return ( - f"[b green]Member:[/] [b white]{m.id}[/] " - f"IP:[magenta]{m.address}[/magenta] " - f"port:[magenta]{m.protocol_port}[/magenta] " - f"weight:[magenta]{m.weight}[/magenta] " - f"backup:[magenta]{m.backup}[/magenta] " - f"prov_status:{self.formatter.format_status(m.provisioning_status)} " - f"oper_status:{self.formatter.format_status(m.operating_status)}" - ) - - def return_member(_, os_m=os_m): - # Simply return the already retrieved member. - return os_m - - self._retrieve_and_add_to_tree( - "Member", member["id"], return_member, pool_tree, format_member - ) + for member_ref in pool_members: + member_id = member_ref["id"] + with self.formatter.status(f"Getting member details id [b]{member_id}[/b]"): + member = self.openstack_api.retrieve_member(member_id, pool_id) + + if member: + member_tree = self.formatter.add_member_to_tree(parent_tree, member) + if self.details: + self.formatter.add_details_to_tree(member_tree, member.to_dict()) + else: + self.formatter.add_empty_node(parent_tree, f"Member ({member_id})") def display_lb_info(self): """ @@ -255,10 +184,10 @@ def display_lb_info(self): self.create_lb_tree() if not self.lb.listeners: - self.formatter.add_to_tree(self.lb_tree, "[b green]Listener:[/] None") + self.formatter.add_empty_node(self.lb_tree, "Listener") else: for listener in self.lb.listeners: - self.add_listener_info(listener["id"]) + self.add_listener_info(self.lb_tree, listener["id"]) self.formatter.rule( f"[b]Loadbalancer ID: {self.lb.id} [bright_blue]({self.lb.name})[/]", @@ -345,8 +274,8 @@ def add_amphora_to_tree(self, amphora): server_compute_host = "N/A" # Add amphora to the load balancer tree - amphora_tree = self.formatter.add_to_tree( - self.lb_tree, + amphora_tree = self.formatter.add_to_tree( # pylint: disable=duplicate-code + self.lb_tree, # pylint: disable=duplicate-code f"[b green]amphora: [/]" f"[b white]{amphora.id} [/]" f"{amphora.role} " From 7bf4fe30427648c6956b87974fb1a9c8e854c696 Mon Sep 17 00:00:00 2001 From: Thobias Salazar Trevisan Date: Fri, 8 Aug 2025 16:40:29 -0300 Subject: [PATCH 3/9] refactor AmphoraInfo class to new new fmt methods --- src/openstack_lb_info/formatters.py | 110 ++++++--------------- src/openstack_lb_info/loadbalancer_info.py | 59 +---------- 2 files changed, 36 insertions(+), 133 deletions(-) diff --git a/src/openstack_lb_info/formatters.py b/src/openstack_lb_info/formatters.py index 35fda46..c73d361 100644 --- a/src/openstack_lb_info/formatters.py +++ b/src/openstack_lb_info/formatters.py @@ -11,7 +11,7 @@ import contextlib import json -import re # delete after +import re from abc import ABC, abstractmethod try: @@ -27,14 +27,6 @@ class OutputFormatter(ABC): """Abstract base class for output formatters.""" - @abstractmethod - def create_tree(self, name): - """Create a tree structure for the output.""" - - @abstractmethod - def add_to_tree(self, tree, content): - """Add content to the tree structure.""" - @abstractmethod def print_tree(self, tree): """Print the tree structure.""" @@ -90,7 +82,6 @@ def add_member_to_tree(self, parent_tree, member): @abstractmethod def add_amphora_to_tree(self, parent_tree, amphora, server, image_name): """Adds a formatted amphora node to the tree.""" - pass # pylint: disable=unnecessary-pass class RichOutputFormatter(OutputFormatter): @@ -100,10 +91,10 @@ def __init__(self): self.console = Console() self.highlighter = ReprHighlighter() - def create_tree(self, name): + def _create_tree(self, name): return Tree(name) - def add_to_tree(self, tree, content, highlight=False): + def _add_to_tree(self, tree, content, highlight=False): if highlight: content = self.highlighter(content) return tree.add(content) @@ -123,10 +114,6 @@ def line(self): def rule(self, title, align="center"): self.console.rule(title, align=align) - # def format_message(self, message): - # """Return the message as-is, preserving Rich formatting.""" - # return message - def format_status(self, status): status_colors = { "ACTIVE": "green", @@ -140,10 +127,10 @@ def add_details_to_tree(self, tree, details_dict): for attr in sorted(details_dict): value = details_dict[attr] content = f"{attr}: {value}" - self.add_to_tree(tree, content, highlight=True) + self._add_to_tree(tree, content, highlight=True) def add_empty_node(self, tree, resource_name): - self.add_to_tree(tree, f"[b green]{resource_name}:[/] None") + self._add_to_tree(tree, f"[b green]{resource_name}:[/] None") def add_lb_to_tree(self, lb): message = ( @@ -153,7 +140,7 @@ def add_lb_to_tree(self, lb): f"oper_status:{self.format_status(lb.operating_status)} " f"tags:[magenta]{lb.tags}[/]" ) - return self.create_tree(message) + return self._create_tree(message) def add_listener_to_tree(self, parent_tree, listener): message = ( @@ -163,7 +150,7 @@ def add_listener_to_tree(self, parent_tree, listener): f"prov_status:{self.format_status(listener.provisioning_status)} " f"oper_status:{self.format_status(listener.operating_status)}" ) - return self.add_to_tree(parent_tree, message) + return self._add_to_tree(parent_tree, message) def add_pool_to_tree(self, parent_tree, pool): message = ( @@ -173,7 +160,7 @@ def add_pool_to_tree(self, parent_tree, pool): f"prov_status:{self.format_status(pool.provisioning_status)} " f"oper_status:{self.format_status(pool.operating_status)}" ) - return self.add_to_tree(parent_tree, message) + return self._add_to_tree(parent_tree, message) def add_health_monitor_to_tree(self, parent_tree, hm): message = ( @@ -185,7 +172,7 @@ def add_health_monitor_to_tree(self, parent_tree, hm): f"prov_status:{self.format_status(hm.provisioning_status)} " f"oper_status:{self.format_status(hm.operating_status)}" ) - return self.add_to_tree(parent_tree, message) + return self._add_to_tree(parent_tree, message) def add_member_to_tree(self, parent_tree, member): message = ( @@ -197,14 +184,13 @@ def add_member_to_tree(self, parent_tree, member): f"prov_status:{self.format_status(member.provisioning_status)} " f"oper_status:{self.format_status(member.operating_status)}" ) - return self.add_to_tree(parent_tree, message) + return self._add_to_tree(parent_tree, message) def add_amphora_to_tree(self, parent_tree, amphora, server, image_name): server_id = server.id if server else "N/A" server_flavor_name = server.flavor.name if server and server.flavor else "N/A" server_compute_host = server.compute_host if server else "N/A" - # pylint: disable=duplicate-code message = ( f"[b green]amphora: [/]" f"[b white]{amphora.id} [/]" @@ -216,40 +202,37 @@ def add_amphora_to_tree(self, parent_tree, amphora, server, image_name): f"vm_flavor:[magenta]{server_flavor_name}[/] " f"compute host:([magenta]{server_compute_host}[/])" ) - return self.add_to_tree(parent_tree, message) + return self._add_to_tree(parent_tree, message) class PlainOutputFormatter(OutputFormatter): """Formatter for plain text output.""" - def create_tree(self, name): + def _create_tree(self, name): return {"name": name, "children": []} - def add_to_tree(self, tree, content, highlight=False): - _ = highlight - child_tree = {"name": self.format_message(content), "children": []} + def _add_to_tree(self, tree, content): + child_tree = {"name": content, "children": []} tree["children"].append(child_tree) return child_tree def print_tree(self, tree, level=0): indent = " " * level - print(f"{indent}{self.format_message(tree['name'])}") + print(f"{indent}{tree['name']}") for child in tree.get("children", []): self.print_tree(child, level + 1) def print(self, message): - print(self.format_message(message)) + print(message) def status(self, message): @contextlib.contextmanager def plain_status(): - # Remove Rich formatting codes from the message - clean_message = self.format_message(message) - print(f"[STATUS] {clean_message}") + print(f"[STATUS] {message}") try: yield finally: - print(f"[STATUS] Completed: {clean_message}") + print(f"[STATUS] Completed: {message}") return plain_status() @@ -257,14 +240,9 @@ def line(self): print() def rule(self, title, align="center"): - title = self.format_message(title) - print(f"{title}") - print("-" * len(title)) - - def format_message(self, message): - """Remove Rich text formatting tags from a message.""" - clean_message = re.sub(r"\[\/?[^\]]+\]", "", message) - return clean_message + clean_title = re.sub(r"\[\/?[^\]]+\]", "", title) + print(f"{clean_title}") + print("-" * len(clean_title)) def format_status(self, status): return status @@ -273,10 +251,10 @@ def add_details_to_tree(self, tree, details_dict): for attr in sorted(details_dict): value = details_dict[attr] content = f"{attr}: {value}" - self.add_to_tree(tree, content) + self._add_to_tree(tree, content) def add_empty_node(self, tree, resource_name): - self.add_to_tree(tree, f"{resource_name}: None") + self._add_to_tree(tree, f"{resource_name}: None") def add_lb_to_tree(self, lb): message = ( @@ -286,7 +264,7 @@ def add_lb_to_tree(self, lb): f"oper_status:{self.format_status(lb.operating_status)} " f"tags:{lb.tags}" ) - return self.create_tree(message) + return self._create_tree(message) def add_listener_to_tree(self, parent_tree, listener): message = ( @@ -295,7 +273,7 @@ def add_listener_to_tree(self, parent_tree, listener): f"prov_status:{self.format_status(listener.provisioning_status)} " f"oper_status:{self.format_status(listener.operating_status)}" ) - return self.add_to_tree(parent_tree, message) + return self._add_to_tree(parent_tree, message) def add_pool_to_tree(self, parent_tree, pool): message = ( @@ -305,7 +283,7 @@ def add_pool_to_tree(self, parent_tree, pool): f"prov_status:{self.format_status(pool.provisioning_status)} " f"oper_status:{self.format_status(pool.operating_status)}" ) - return self.add_to_tree(parent_tree, message) + return self._add_to_tree(parent_tree, message) def add_health_monitor_to_tree(self, parent_tree, hm): message = ( @@ -317,7 +295,7 @@ def add_health_monitor_to_tree(self, parent_tree, hm): f"prov_status:{self.format_status(hm.provisioning_status)} " f"oper_status:{self.format_status(hm.operating_status)}" ) - return self.add_to_tree(parent_tree, message) + return self._add_to_tree(parent_tree, message) def add_member_to_tree(self, parent_tree, member): message = ( @@ -329,7 +307,7 @@ def add_member_to_tree(self, parent_tree, member): f"prov_status:{self.format_status(member.provisioning_status)} " f"oper_status:{self.format_status(member.operating_status)}" ) - return self.add_to_tree(parent_tree, message) + return self._add_to_tree(parent_tree, message) def add_amphora_to_tree(self, parent_tree, amphora, server, image_name): server_id = server.id if server else "N/A" @@ -343,7 +321,7 @@ def add_amphora_to_tree(self, parent_tree, amphora, server, image_name): f"vm_flavor:{server_flavor_name} " f"compute host:({server_compute_host})" ) - return self.add_to_tree(parent_tree, message) + return self._add_to_tree(parent_tree, message) class JSONOutputFormatter(OutputFormatter): @@ -352,32 +330,13 @@ class JSONOutputFormatter(OutputFormatter): def __init__(self): self.data = None - def create_tree(self, name): - # Remove Rich codes - clean_name = self.format_message(name) - self.data = {"name": clean_name, "children": []} - return self.data - - def add_to_tree(self, tree, content, highlight=False): - _ = highlight - # Remove Rich codes - clean_content = self.format_message(content) - # Create a new node and add it to the tree's children - child = {"name": clean_content, "children": []} - tree["children"].append(child) - return child - def print_tree(self, tree): print(json.dumps(tree, indent=4)) def print(self, message): - # Remove Rich codes - clean_message = self.format_message(message) - # Not show empty prints - if not clean_message: + if not message: return - # For consistency, wrap messages in a dict - output = {"message": clean_message} + output = {"message": message} print(json.dumps(output, indent=4)) def status(self, message): @@ -392,13 +351,6 @@ def rule(self, title, align="center"): def format_status(self, status): return status - def format_message(self, message): - """Remove Rich text formatting tags from a message.""" - if isinstance(message, str): - clean_message = re.sub(r"\[\/?[^\]]+\]", "", message) - return clean_message - return message - def _add_node_from_obj(self, parent_node, node_type, resource_obj): node = resource_obj.to_dict() node["type"] = node_type diff --git a/src/openstack_lb_info/loadbalancer_info.py b/src/openstack_lb_info/loadbalancer_info.py index 169afeb..375aa48 100644 --- a/src/openstack_lb_info/loadbalancer_info.py +++ b/src/openstack_lb_info/loadbalancer_info.py @@ -40,24 +40,6 @@ def __init__(self, openstack_api, lb, details, formatter): self.lb_tree = None self.openstack_api = openstack_api - def _add_all_attr_to_tree(self, obj, tree): - """ - Add all attributes of an object to a tree. - - This function iterates through all the attributes of a given Python object and - adds them to a Rich tree. Each attribute is displayed in the - format "attribute_name: value". - - Args: - obj (object): The object whose attributes are to be added. - tree: The tree to which the attributes will be added. - """ - obj_dict = obj.to_dict() - for attr in sorted(obj_dict): - value = obj_dict[attr] - content = f"{attr}: {value}" - self.formatter.add_to_tree(tree, content, highlight=True) - def create_lb_tree(self): """ Create a tree representing Load Balancer information. @@ -211,19 +193,6 @@ class AmphoraInfo(LoadBalancerInfo): images_name = {} - def __init__(self, openstack_api, lb, details, formatter): - """ - Initialize an AmphoraInfo instance. - - Args: - openstack_api (OpenStackAPI): An instance of `OpenStackAPI` for OpenStack interactions. - lb (openstack.load_balancer.v2.load_balancer.LoadBalancer): The Load Balancer object. - details (bool): If True, displays detailed attributes of the Amphorae. - formatter (OutputFormatter): An instance of a formatter class for output formatting. - """ - super().__init__(openstack_api, lb, details, formatter) - self.lb_tree = self.create_lb_tree() - def get_images_name(self, image_ids): """ Retrieve image names for a list of image IDs and cache the results. @@ -260,34 +229,15 @@ def add_amphora_to_tree(self, amphora): """ # Get image name for the image ID self.get_images_name([amphora.image_id]) + image_name = AmphoraInfo.images_name.get(amphora.image_id, "N/A") + # Get amphora server (instance) details with self.formatter.status(f"Getting server details [b]{amphora.compute_id}[/b]"): server = self.openstack_api.retrieve_server(amphora.compute_id) - if server: - server_id = server.id - server_flavor_name = server.flavor.name if server.flavor else "N/A" - server_compute_host = server.compute_host - else: - server_id = "N/A" - server_flavor_name = "N/A" - server_compute_host = "N/A" - - # Add amphora to the load balancer tree - amphora_tree = self.formatter.add_to_tree( # pylint: disable=duplicate-code - self.lb_tree, # pylint: disable=duplicate-code - f"[b green]amphora: [/]" - f"[b white]{amphora.id} [/]" - f"{amphora.role} " - f"{amphora.status} " - f"lb_network_ip:[green]{amphora.lb_network_ip} [/]" - f"img:[magenta]{AmphoraInfo.images_name.get(amphora.image_id, 'N/A')}[/] " - f"server:[magenta]{server_id}[/] " - f"vm_flavor:[magenta]{server_flavor_name}[/] " - f"compute host:([magenta]{server_compute_host}[/])", - ) + amphora_tree = self.formatter.add_amphora_to_tree(self.lb_tree, amphora, server, image_name) if self.details: - self._add_all_attr_to_tree(amphora, amphora_tree) + self.formatter.add_details_to_tree(amphora_tree, amphora.to_dict()) def display_amp_info(self): """ @@ -296,6 +246,7 @@ def display_amp_info(self): Returns: None """ + self.lb_tree = self.create_lb_tree() with self.formatter.status( f"Getting amphora details for load balancer [b]{self.lb.id}[/b]" From a4cef074ce2ed6f6496bba682dd559a8efe9612b Mon Sep 17 00:00:00 2001 From: Thobias Salazar Trevisan Date: Mon, 11 Aug 2025 11:31:35 -0300 Subject: [PATCH 4/9] Update docstrings and rename tree variables for clarity --- src/openstack_lb_info/__init__.py | 2 +- src/openstack_lb_info/formatters.py | 37 ++++++-- src/openstack_lb_info/loadbalancer_info.py | 103 +++++++++------------ src/openstack_lb_info/main.py | 10 +- 4 files changed, 75 insertions(+), 77 deletions(-) diff --git a/src/openstack_lb_info/__init__.py b/src/openstack_lb_info/__init__.py index ddfff52..f661f41 100644 --- a/src/openstack_lb_info/__init__.py +++ b/src/openstack_lb_info/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- """openstack-lb-info module.""" -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/src/openstack_lb_info/formatters.py b/src/openstack_lb_info/formatters.py index c73d361..ba3fe71 100644 --- a/src/openstack_lb_info/formatters.py +++ b/src/openstack_lb_info/formatters.py @@ -45,76 +45,85 @@ def line(self): @abstractmethod def rule(self, title, align="center"): - """Print a rule with a title.""" + """Print a horizontal rule with a title.""" @abstractmethod def format_status(self, status): - """Format status text.""" + """Format a status string (e.g., 'ACTIVE') for display.""" @abstractmethod def add_details_to_tree(self, tree, details_dict): - """Adds all attributes from an object to the tree.""" + """Add a dictionary of detailed attributes to a tree node.""" @abstractmethod def add_empty_node(self, tree, resource_name): - """Adds a placeholder for a missing resource.""" + """Add a placeholder node for a resource that was not found.""" @abstractmethod def add_lb_to_tree(self, lb): - """Create and return the root tree for the Load Balancer.""" + """Create and return the root tree for a Load Balancer.""" @abstractmethod def add_listener_to_tree(self, parent_tree, listener): - """Adds a formatted listener node to the tree.""" + """Add a formatted listener node to a parent tree.""" @abstractmethod def add_pool_to_tree(self, parent_tree, pool): - """Adds a formatted pool node to the tree.""" + """Add a formatted pool node to a parent tree.""" @abstractmethod def add_health_monitor_to_tree(self, parent_tree, hm): - """Adds a formatted health monitor node to the tree.""" + """Add a formatted health monitor node to a parent tree.""" @abstractmethod def add_member_to_tree(self, parent_tree, member): - """Adds a formatted member node to the tree.""" + """Add a formatted member node to a parent tree.""" @abstractmethod def add_amphora_to_tree(self, parent_tree, amphora, server, image_name): - """Adds a formatted amphora node to the tree.""" + """Add a formatted amphora node to a parent tree.""" class RichOutputFormatter(OutputFormatter): """Formatter using the Rich library.""" def __init__(self): + """Initialize the Rich console and highlighter.""" self.console = Console() self.highlighter = ReprHighlighter() def _create_tree(self, name): + """Create a Rich Tree instance.""" return Tree(name) def _add_to_tree(self, tree, content, highlight=False): + """Add a node to a Rich Tree.""" if highlight: content = self.highlighter(content) return tree.add(content) def print_tree(self, tree): + """Print a Rich Tree to the console.""" self.console.print(tree) def print(self, message): + """Print a message using the Rich console.""" self.console.print(message) def status(self, message): + """Display a status indicator using the Rich console.""" return self.console.status(message) def line(self): + """Print a line using the Rich console.""" self.console.line() def rule(self, title, align="center"): + """Print a Rich rule to the console.""" self.console.rule(title, align=align) def format_status(self, status): + """Format a status string with appropriate colors.""" status_colors = { "ACTIVE": "green", "ONLINE": "green", @@ -124,15 +133,18 @@ def format_status(self, status): return f"[{color}]{status}[/{color}]" def add_details_to_tree(self, tree, details_dict): + """Add highlighted key-value pairs to the tree.""" for attr in sorted(details_dict): value = details_dict[attr] content = f"{attr}: {value}" self._add_to_tree(tree, content, highlight=True) def add_empty_node(self, tree, resource_name): + """Add a styled placeholder for a missing resource.""" self._add_to_tree(tree, f"[b green]{resource_name}:[/] None") def add_lb_to_tree(self, lb): + """Create a styled root tree node for the Load Balancer.""" message = ( f"LB:[bright_yellow] {lb.id}[/] " f"vip:[bright_cyan]{lb.vip_address}[/] " @@ -143,6 +155,7 @@ def add_lb_to_tree(self, lb): return self._create_tree(message) def add_listener_to_tree(self, parent_tree, listener): + """Add a styled listener node to the tree.""" message = ( f"[b green]Listener:[/] [b white]{listener.id}[/] " f"([blue b]{listener.name}[/]) " @@ -153,6 +166,7 @@ def add_listener_to_tree(self, parent_tree, listener): return self._add_to_tree(parent_tree, message) def add_pool_to_tree(self, parent_tree, pool): + """Add a styled pool node to the tree.""" message = ( f"[b green]Pool:[/] [b white]{pool.id}[/] " f"protocol:[magenta]{pool.protocol}[/magenta] " @@ -163,6 +177,7 @@ def add_pool_to_tree(self, parent_tree, pool): return self._add_to_tree(parent_tree, message) def add_health_monitor_to_tree(self, parent_tree, hm): + """Add a styled health monitor node to the tree.""" message = ( f"[b green]Health Monitor:[/] [b white]{hm.id}[/] " f"type:[magenta]{hm.type}[/magenta] " @@ -175,6 +190,7 @@ def add_health_monitor_to_tree(self, parent_tree, hm): return self._add_to_tree(parent_tree, message) def add_member_to_tree(self, parent_tree, member): + """Add a styled member node to the tree.""" message = ( f"[b green]Member:[/] [b white]{member.id}[/] " f"IP:[magenta]{member.address}[/magenta] " @@ -187,6 +203,7 @@ def add_member_to_tree(self, parent_tree, member): return self._add_to_tree(parent_tree, message) def add_amphora_to_tree(self, parent_tree, amphora, server, image_name): + """Add a styled amphora node to the tree.""" server_id = server.id if server else "N/A" server_flavor_name = server.flavor.name if server and server.flavor else "N/A" server_compute_host = server.compute_host if server else "N/A" diff --git a/src/openstack_lb_info/loadbalancer_info.py b/src/openstack_lb_info/loadbalancer_info.py index 375aa48..392c3b8 100644 --- a/src/openstack_lb_info/loadbalancer_info.py +++ b/src/openstack_lb_info/loadbalancer_info.py @@ -4,10 +4,9 @@ -------------------------------- This module provides classes for retrieving, organizing, and displaying detailed information -about OpenStack Load Balancers and their associated resources, such as listeners, pools, -health monitors, members, and amphorae. It uses the `OpenStackAPI` class for interacting -with the OpenStack environment and uses `OutputFormatter` instances to present the information -in various output formats (e.g., Rich text, plain text, JSON). +about OpenStack Load Balancers and their associated resources. It uses the `OpenStackAPI` +class for interacting with the OpenStack environment and uses `OutputFormatter` instances +to present the information in various output formats (e.g., Rich text, plain text, JSON). Classes: @@ -21,7 +20,7 @@ class LoadBalancerInfo: """ - Provides information and structured display of OpenStack Load Balancers. + Retrieves and displays information for a single OpenStack Load Balancer. """ def __init__(self, openstack_api, lb, details, formatter): @@ -37,38 +36,35 @@ def __init__(self, openstack_api, lb, details, formatter): self.lb = lb self.details = details self.formatter = formatter - self.lb_tree = None self.openstack_api = openstack_api + # The root of the display tree for the formatter + self.lb_tree = None def create_lb_tree(self): """ - Create a tree representing Load Balancer information. + Create the root of the display tree for the load balancer. - Returns: - Tree: A tree object representing Load Balancer information. + This method instructs the formatter to create the main tree node + and adds detailed attributes if requested. """ self.lb_tree = self.formatter.add_lb_to_tree(self.lb) if self.details: self.formatter.add_details_to_tree(self.lb_tree, self.lb.to_dict()) - return self.lb_tree - - def add_listener_info(self, parent_tree, listener_id): + def add_listener_info(self, lb_tree, listener_id): """ - Add information about a Listener to the Load Balancer tree. + Add information about the Listener to the Load Balancer's tree. Args: + lb_tree (object): The root tree node for the load balancer. listener_id (str): The ID of the Listener for which to retrieve and display information. - - Returns: - None """ with self.formatter.status(f"Getting Listener details id [b]{listener_id}[/b]"): listener = self.openstack_api.retrieve_listener(listener_id) if listener: - listener_tree = self.formatter.add_listener_to_tree(parent_tree, listener) + listener_tree = self.formatter.add_listener_to_tree(lb_tree, listener) if self.details: self.formatter.add_details_to_tree(listener_tree, listener.to_dict()) @@ -77,24 +73,21 @@ def add_listener_info(self, parent_tree, listener_id): else: self.formatter.add_empty_node(listener_tree, "Pool") else: - self.formatter.add_empty_node(parent_tree, "Listener") + self.formatter.add_empty_node(lb_tree, "Listener") - def add_pool_info(self, parent_tree, pool_id): + def add_pool_info(self, listener_tree, pool_id): """ - Add information about a Pool to the Load Balancer tree. + Add information about the Pool to the listener's tree. Args: - tree: The tree representing the Load Balancer. + listener_tree (object): The tree representing the listener. pool_id (str): The ID of the Pool for which to retrieve and display. - - Returns: - None """ with self.formatter.status(f"Getting Pool details id [b]{pool_id}[/b]"): pool = self.openstack_api.retrieve_pool(pool_id) if pool: - pool_tree = self.formatter.add_pool_to_tree(parent_tree, pool) + pool_tree = self.formatter.add_pool_to_tree(listener_tree, pool) if self.details: self.formatter.add_details_to_tree(pool_tree, pool.to_dict()) @@ -108,60 +101,54 @@ def add_pool_info(self, parent_tree, pool_id): else: self.formatter.add_empty_node(pool_tree, "Member") else: - self.formatter.add_empty_node(parent_tree, "Pool") + self.formatter.add_empty_node(listener_tree, "Pool") - def add_health_monitor_info(self, parent_tree, health_monitor_id): + def add_health_monitor_info(self, pool_tree, health_monitor_id): """ - Add information about a Health Monitor to a Pool tree. + Add information about the Health Monitor to the pool's tree. Args: - pool_tree: The tree representing the Pool. + pool_tree: The tree representing the pool. health_monitor_id (str): The ID of the Health Monitor. - - Returns: - None """ with self.formatter.status(f"Getting Health Monitor details id [b]{health_monitor_id}[/b]"): hm = self.openstack_api.retrieve_health_monitor(health_monitor_id) if hm: - hm_tree = self.formatter.add_health_monitor_to_tree(parent_tree, hm) + hm_tree = self.formatter.add_health_monitor_to_tree(pool_tree, hm) if self.details: self.formatter.add_details_to_tree(hm_tree, hm.to_dict()) else: - self.formatter.add_empty_node(parent_tree, "Health Monitor") + self.formatter.add_empty_node(pool_tree, "Health Monitor") - def add_pool_members(self, parent_tree, pool_id, pool_members): + def add_pool_members(self, pool_tree, pool_id, pool_members): """ - Add information about Members of a Pool to the Pool tree. + Add information about Members to the pool's tree. Args: pool_tree: The tree representing the Pool. pool_id (str): The ID of the Pool for which to retrieve Member information. pool_members (list): A list of dictionaries containing Member information, where each dictionary includes the Member's ID and additional details. - - Returns: - None """ - for member_ref in pool_members: - member_id = member_ref["id"] + for member in pool_members: + member_id = member["id"] with self.formatter.status(f"Getting member details id [b]{member_id}[/b]"): member = self.openstack_api.retrieve_member(member_id, pool_id) if member: - member_tree = self.formatter.add_member_to_tree(parent_tree, member) + member_tree = self.formatter.add_member_to_tree(pool_tree, member) if self.details: self.formatter.add_details_to_tree(member_tree, member.to_dict()) else: - self.formatter.add_empty_node(parent_tree, f"Member ({member_id})") + self.formatter.add_empty_node(pool_tree, f"Member ({member_id})") def display_lb_info(self): """ - Display information about the Load Balancer. + Fetch and display information about the Load Balancer. - Returns: - None + This is the main entry point for this class. It builds the display tree + and prints it. """ self.create_lb_tree() @@ -181,14 +168,13 @@ def display_lb_info(self): class AmphoraInfo(LoadBalancerInfo): """ - Provides information about Amphorae associated with an OpenStack Load Balancer. + Retrieves and displays Amphora information for a Load Balancer. - This class extends the LoadBalancerInfo class and adds functionality to retrieve - and display information about Amphorae associated with an OpenStack - Load Balancer. + This class extends LoadBalancerInfo to provide specific functionality for + displaying information about Amphorae associated with a Load Balancer. Class Attributes: - images_name (dict): A dictionary to cache image names for Amphorae. + images_name (dict): A class-level cache for image names. """ images_name = {} @@ -203,10 +189,8 @@ def get_images_name(self, image_ids): Note: The retrieved image names are stored in the 'images_name' class attribute for future reference, avoiding redundant queries to the OpenStack. - - Returns: - None """ + # Checks the cache before making an API call new_img_ids = [i for i in image_ids if i not in AmphoraInfo.images_name] if new_img_ids: with self.formatter.status(f"Getting image details [b]{new_img_ids}[/b]"): @@ -223,9 +207,6 @@ def add_amphora_to_tree(self, amphora): Args: amphora (openstack.load_balancer.v2.amphora.Amphora): The amphora for which to display detailed information. - - Returns: - None """ # Get image name for the image ID self.get_images_name([amphora.image_id]) @@ -241,12 +222,12 @@ def add_amphora_to_tree(self, amphora): def display_amp_info(self): """ - Display information about amphorae associated with a load balancer. + Fetch and display information about amphorae associated with a load balancer. - Returns: - None + This is the main entry point for this class. It builds the display tree + and prints it. """ - self.lb_tree = self.create_lb_tree() + self.create_lb_tree() with self.formatter.status( f"Getting amphora details for load balancer [b]{self.lb.id}[/b]" diff --git a/src/openstack_lb_info/main.py b/src/openstack_lb_info/main.py index 6fa3976..91cfce6 100644 --- a/src/openstack_lb_info/main.py +++ b/src/openstack_lb_info/main.py @@ -120,7 +120,7 @@ def parse_parameters(): if len(sys.argv) < 2: parser.print_help() - sys.exit(1) + sys.exit(0) args = parser.parse_args() @@ -213,7 +213,7 @@ def query_openstack_lbs(openstackapi, args, formatter): if v is not None } - with formatter.status("Quering load balancers..."): + with formatter.status("Querying load balancers..."): filtered_lbs_tmp = openstackapi.retrieve_load_balancers(filter_criteria) # Perform name filtering here rather than adding it to filter_criteria @@ -257,9 +257,6 @@ def main(): args = parse_parameters() - # Create an instance of OpenStackAPI - openstackapi = OpenStackAPI() - if args.output_format == "rich" and not RICH_AVAILABLE: sys.exit( "Error: 'rich' library is not installed. " @@ -269,6 +266,9 @@ def main(): # Initialize the formatter formatter = get_formatter(args.output_format) + # Create an instance of OpenStackAPI + openstackapi = OpenStackAPI() + filtered_lbs = query_openstack_lbs(openstackapi, args, formatter) if not filtered_lbs: From a08abc782716faba9fc7a52c420274945e6ed3bf Mon Sep 17 00:00:00 2001 From: Thobias Salazar Trevisan Date: Mon, 11 Aug 2025 18:31:38 -0300 Subject: [PATCH 5/9] feat: add parallel member fetching with progress bar support --- src/openstack_lb_info/formatters.py | 40 +++++++++++ src/openstack_lb_info/loadbalancer_info.py | 80 +++++++++++++++++----- src/openstack_lb_info/main.py | 20 +++++- 3 files changed, 119 insertions(+), 21 deletions(-) diff --git a/src/openstack_lb_info/formatters.py b/src/openstack_lb_info/formatters.py index ba3fe71..48aa9b3 100644 --- a/src/openstack_lb_info/formatters.py +++ b/src/openstack_lb_info/formatters.py @@ -15,6 +15,7 @@ from abc import ABC, abstractmethod try: + from rich import progress from rich.console import Console from rich.highlighter import ReprHighlighter from rich.tree import Tree @@ -39,6 +40,15 @@ def print(self, message): def status(self, message): """Display a status message.""" + @abstractmethod + def track_progress(self, sequence, description, total): + """ + Track progress of an iterable. + + Yields: + The items from the sequence. + """ + @abstractmethod def line(self): """Print a line separator.""" @@ -114,6 +124,29 @@ def status(self, message): """Display a status indicator using the Rich console.""" return self.console.status(message) + def track_progress(self, sequence, description, total=None): + """Track progress with a customized Rich progress bar.""" + progress_bar = progress.Progress( + progress.TextColumn("[progress.description]{task.description}"), + progress.BarColumn(), + progress.TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), + progress.TextColumn("({task.completed} of {task.total})"), + progress.TimeRemainingColumn(), + console=self.console, + ) + + if total is None: + try: + total = len(sequence) + except TypeError: + total = None + + with progress_bar: + task_id = progress_bar.add_task(description, total=total) + for item in sequence: + progress_bar.update(task_id, advance=1) + yield item + def line(self): """Print a line using the Rich console.""" self.console.line() @@ -253,6 +286,10 @@ def plain_status(): return plain_status() + def track_progress(self, sequence, description, total=None): + print(f"[STATUS] {description}...") + return sequence + def line(self): print() @@ -359,6 +396,9 @@ def print(self, message): def status(self, message): return contextlib.nullcontext() + def track_progress(self, sequence, description, total=None): + return sequence + def line(self): pass diff --git a/src/openstack_lb_info/loadbalancer_info.py b/src/openstack_lb_info/loadbalancer_info.py index 392c3b8..e1fc59e 100644 --- a/src/openstack_lb_info/loadbalancer_info.py +++ b/src/openstack_lb_info/loadbalancer_info.py @@ -16,6 +16,29 @@ class for interacting with the OpenStack environment and uses `OutputFormatter` - `AmphoraInfo`: Extends `LoadBalancerInfo` to focus on retrieving and displaying information about the amphorae associated with a Load Balancer. """ +import concurrent.futures +from dataclasses import dataclass + +from .formatters import OutputFormatter +from .openstack_api import OpenStackAPI + + +@dataclass +class ProcessingContext: + """ + Holds shared objects and configuration used during load balancer processing. + + Attributes: + openstack_api (OpenStackAPI): An instance of `OpenStackAPI` for OpenStack interactions. + details (bool): If True, displays detailed attributes of the Load Balancer. + formatter (OutputFormatter): An instance of a formatter class for output formatting. + max_workers (int): Max number of concurrent threads to fetch members details. + """ + + openstack_api: OpenStackAPI + details: bool + max_workers: int + formatter: OutputFormatter class LoadBalancerInfo: @@ -23,20 +46,19 @@ class LoadBalancerInfo: Retrieves and displays information for a single OpenStack Load Balancer. """ - def __init__(self, openstack_api, lb, details, formatter): + def __init__(self, lb, context): """ Initialize a LoadBalancerInfo instance. Args: - openstack_api (OpenStackAPI): An instance of `OpenStackAPI` for OpenStack interactions. lb (openstack.load_balancer.v2.load_balancer.LoadBalancer): The Load Balancer object. - details (bool): If True, displays detailed attributes of the Load Balancer. - formatter (OutputFormatter): An instance of a formatter class for output formatting. + context (ProcessingContext): A instance of ProcessingContext class. """ self.lb = lb - self.details = details - self.formatter = formatter - self.openstack_api = openstack_api + self.details = context.details + self.formatter = context.formatter + self.openstack_api = context.openstack_api + self.max_workers = context.max_workers # The root of the display tree for the formatter self.lb_tree = None @@ -131,17 +153,39 @@ def add_pool_members(self, pool_tree, pool_id, pool_members): pool_members (list): A list of dictionaries containing Member information, where each dictionary includes the Member's ID and additional details. """ - for member in pool_members: - member_id = member["id"] - with self.formatter.status(f"Getting member details id [b]{member_id}[/b]"): - member = self.openstack_api.retrieve_member(member_id, pool_id) - - if member: - member_tree = self.formatter.add_member_to_tree(pool_tree, member) - if self.details: - self.formatter.add_details_to_tree(member_tree, member.to_dict()) - else: - self.formatter.add_empty_node(pool_tree, f"Member ({member_id})") + # Avoid spinning up extra idle threads + max_workers = min(self.max_workers, len(pool_members)) + + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + # Create future for each member IDs + futures_to_member_id = { + executor.submit(self.openstack_api.retrieve_member, member["id"], pool_id): member[ + "id" + ] + for member in pool_members + } + + # Use the formatter's progress bar to track completion + description = f"Getting members information for pool id: {pool_id}" + + progress_iterator = self.formatter.track_progress( + sequence=concurrent.futures.as_completed(futures_to_member_id), + description=description, + total=len(futures_to_member_id), + ) + + for future in progress_iterator: + member_id = futures_to_member_id[future] + try: + member = future.result() + if member: + member_tree = self.formatter.add_member_to_tree(pool_tree, member) + if self.details: + self.formatter.add_details_to_tree(member_tree, member.to_dict()) + else: + self.formatter.add_empty_node(pool_tree, f"Member ({member_id})") + except Exception as exc: # pylint: disable=broad-exception-caught + self.formatter.add_empty_node(pool_tree, f"Member ({member_id} - Error: {exc})") def display_lb_info(self): """ diff --git a/src/openstack_lb_info/main.py b/src/openstack_lb_info/main.py index 91cfce6..3320708 100644 --- a/src/openstack_lb_info/main.py +++ b/src/openstack_lb_info/main.py @@ -41,7 +41,7 @@ PlainOutputFormatter, RichOutputFormatter, ) -from .loadbalancer_info import AmphoraInfo, LoadBalancerInfo +from .loadbalancer_info import AmphoraInfo, LoadBalancerInfo, ProcessingContext from .openstack_api import OpenStackAPI @@ -117,6 +117,13 @@ def parse_parameters(): action="store_true", required=False, ) + parser.add_argument( + "--max-workers", + help="Max number of concurrent threads to fetch members details (default: %(default)s)", + type=int, + default=4, + required=False, + ) if len(sys.argv) < 2: parser.print_help() @@ -275,12 +282,19 @@ def main(): formatter.print("No load balancer(s) found.") sys.exit(1) + context = ProcessingContext( + openstack_api=openstackapi, + details=args.details, + max_workers=args.max_workers, + formatter=formatter, + ) + for lb in filtered_lbs: if args.type == "amphora": - amphora_info = AmphoraInfo(openstackapi, lb, args.details, formatter) + amphora_info = AmphoraInfo(lb, context) amphora_info.display_amp_info() else: - lb_info = LoadBalancerInfo(openstackapi, lb, args.details, formatter) + lb_info = LoadBalancerInfo(lb, context) lb_info.display_lb_info() From 5286bede87b0ab0f2a8e2473147d03de2e0342a1 Mon Sep 17 00:00:00 2001 From: Thobias Salazar Trevisan Date: Tue, 12 Aug 2025 09:59:20 -0300 Subject: [PATCH 6/9] remove Rich progress bar from output after completion --- src/openstack_lb_info/formatters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/openstack_lb_info/formatters.py b/src/openstack_lb_info/formatters.py index 48aa9b3..828ab2c 100644 --- a/src/openstack_lb_info/formatters.py +++ b/src/openstack_lb_info/formatters.py @@ -133,6 +133,7 @@ def track_progress(self, sequence, description, total=None): progress.TextColumn("({task.completed} of {task.total})"), progress.TimeRemainingColumn(), console=self.console, + transient=True, ) if total is None: From 2f75ae2295d44812b2ca2e305e6b67ad09402b26 Mon Sep 17 00:00:00 2001 From: Thobias Salazar Trevisan Date: Tue, 12 Aug 2025 11:40:10 -0300 Subject: [PATCH 7/9] add MAX_WORKERS_LIMIT cap and move argument validations to argparse types --- README.md | 12 ++-- src/openstack_lb_info/main.py | 101 ++++++++++++++++++++-------------- 2 files changed, 67 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 7ae3202..2f9fbec 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ and optional details. If no amphoras match the filter criteria, it will indicate $ usage: openstack-lb-info [-h] [-o {plain,rich,json}] -t {lb,amphora} [--name NAME] [--id ID] [--tags TAGS] [--flavor-id FLAVOR_ID] [--vip-address VIP_ADDRESS] [--availability-zone AVAILABILITY_ZONE] [--vip-network-id VIP_NETWORK_ID] - [--vip-subnet-id VIP_SUBNET_ID] [--details] + [--vip-subnet-id VIP_SUBNET_ID] [--details] [--max-workers MAX_WORKERS] A script to show OpenStack load balancers information. @@ -54,19 +54,21 @@ options: -t {lb,amphora}, --type {lb,amphora} Show information about load balancers or amphoras --name NAME Filter load balancers name - --id ID Filter load balancers id + --id ID Filter load balancers id (UUID) --tags TAGS Filter load balancers tags --flavor-id FLAVOR_ID - Filter load balancers flavor id + Filter load balancers flavor id (UUID) --vip-address VIP_ADDRESS Filter load balancers VIP address --availability-zone AVAILABILITY_ZONE Filter load balancers AZ --vip-network-id VIP_NETWORK_ID - Filter load balancers network id + Filter load balancers network id (UUID) --vip-subnet-id VIP_SUBNET_ID - Filter load balancers subnet id + Filter load balancers subnet id (UUID) --details Show all load balancers/amphora details + --max-workers MAX_WORKERS + Max number of concurrent threads to fetch members details (1-32). (default: 4) Example of use: openstack-lb-info diff --git a/src/openstack_lb_info/main.py b/src/openstack_lb_info/main.py index 3320708..17030c6 100644 --- a/src/openstack_lb_info/main.py +++ b/src/openstack_lb_info/main.py @@ -3,7 +3,7 @@ """ A Python script to display OpenStack Load Balancer details. -This script query an OpenStack environment to present detailed information +This script queries an OpenStack environment to present detailed information about load balancers, including their components such as listeners, pools, health monitors, members, and amphorae. It connects to an OpenStack environment using the OpenStack SDK and presents the information in a @@ -44,6 +44,9 @@ from .loadbalancer_info import AmphoraInfo, LoadBalancerInfo, ProcessingContext from .openstack_api import OpenStackAPI +# Max allowed threads for --max-workers +MAX_WORKERS_LIMIT = 32 + ########################################################################### # Parses the command line arguments @@ -85,15 +88,20 @@ def parse_parameters(): required=True, ) parser.add_argument("--name", help="Filter load balancers name", type=str, required=False) - parser.add_argument("--id", help="Filter load balancers id", type=str, required=False) + parser.add_argument( + "--id", help="Filter load balancers id (UUID)", type=validate_uuid, required=False + ) parser.add_argument("--tags", help="Filter load balancers tags", type=str, required=False) parser.add_argument( - "--flavor-id", help="Filter load balancers flavor id", type=str, required=False + "--flavor-id", + help="Filter load balancers flavor id (UUID)", + type=validate_uuid, + required=False, ) parser.add_argument( "--vip-address", help="Filter load balancers VIP address", - type=str, + type=validate_ip_address, required=False, ) parser.add_argument( @@ -101,14 +109,14 @@ def parse_parameters(): ) parser.add_argument( "--vip-network-id", - help="Filter load balancers network id", - type=str, + help="Filter load balancers network id (UUID)", + type=validate_uuid, required=False, ) parser.add_argument( "--vip-subnet-id", - help="Filter load balancers subnet id", - type=str, + help="Filter load balancers subnet id (UUID)", + type=validate_uuid, required=False, ) parser.add_argument( @@ -119,8 +127,11 @@ def parse_parameters(): ) parser.add_argument( "--max-workers", - help="Max number of concurrent threads to fetch members details (default: %(default)s)", - type=int, + help=( + f"Max number of concurrent threads to fetch members details " + f"(1-{MAX_WORKERS_LIMIT}). (default: %(default)s)" + ), + type=validate_int_range(1, MAX_WORKERS_LIMIT), default=4, required=False, ) @@ -131,65 +142,73 @@ def parse_parameters(): args = parser.parse_args() - validate_arguments(args) - return args -def validate_arguments(args): +def validate_int_range(min_val, max_val): """ - Validate command-line arguments. + Argparse type function that validates integer input within a given range. Args: - args (argparse.Namespace): Parsed command-line arguments. + min_val (int): Minimum allowed integer value (inclusive). + max_val (int): Maximum allowed integer value (inclusive). Raises: - SystemExit: If any validation fails, the script exits. + argparse.ArgumentTypeError: If the string cannot be converted to an integer + or if the value is out of range. """ - # Validate UUIDs parameters - uuid_args = ["id", "vip_network_id", "vip_subnet_id", "flavor_id"] - for arg_name in uuid_args: - arg_value = getattr(args, arg_name) - if arg_value and not is_valid_uuid(arg_value): - sys.exit(f"Error: Invalid {arg_name.replace('_', '-')} format. Expected a UUID.") - # Validate IP address - if args.vip_address and not is_valid_ip_address(args.vip_address): - sys.exit("Error: Invalid VIP address format. Expected a valid IP address.") + def _check_value(value_str): + try: + value = int(value_str) + except ValueError as exc: + raise argparse.ArgumentTypeError(f"Invalid integer: '{value_str!r}'") from exc + if value < min_val or value > max_val: + raise argparse.ArgumentTypeError(f"Value must be between {min_val} and {max_val}") + return value -def is_valid_uuid(uuid_str): + return _check_value + + +def validate_uuid(value_str): """ - Check if uuid_str parameter is a valid UUID. + Argparse type function that checks whether a string is a valid UUID. Args: - uuid_str (str): The value to check. + value_str (str): The value to validate. Returns: - bool: True if valid UUID, False otherwise. + str: The UUID string if valid. + + Raises: + argparse.ArgumentTypeError: If the value is not a valid UUID. """ try: - uuid.UUID(str(uuid_str)) - return True - except ValueError: - return False + uuid.UUID(str(value_str)) + return value_str + except ValueError as exc: + raise argparse.ArgumentTypeError(f"invalid UUID: {value_str!r}") from exc -def is_valid_ip_address(address): +def validate_ip_address(value_str): """ - Check if the address parameter is a valid IP address. + Argparse type function that checks whether a string is a valid IP address. Args: - address (str): The IP address to validate. + value_str (str): The IP address to validate. Returns: - bool: True if valid IP address, False otherwise. + str: The string if it is a valid IPv4/IPv6 address. + + Raises: + argparse.ArgumentTypeError: If the string is not valid IP address. """ try: - ipaddress.ip_address(address) - return True - except ValueError: - return False + ipaddress.ip_address(value_str) + return value_str + except ValueError as exc: + raise argparse.ArgumentTypeError(f"Invalid IP address: {value_str!r}") from exc def query_openstack_lbs(openstackapi, args, formatter): From 6ed94c41949a5058379ba9b61eb93dbbff316d2a Mon Sep 17 00:00:00 2001 From: Thobias Salazar Trevisan Date: Tue, 12 Aug 2025 13:41:01 -0300 Subject: [PATCH 8/9] add status messages for filtering in load balancer query --- src/openstack_lb_info/main.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/openstack_lb_info/main.py b/src/openstack_lb_info/main.py index 17030c6..3e6cc54 100644 --- a/src/openstack_lb_info/main.py +++ b/src/openstack_lb_info/main.py @@ -239,15 +239,15 @@ def query_openstack_lbs(openstackapi, args, formatter): if v is not None } - with formatter.status("Querying load balancers..."): + with formatter.status("Querying load balancers and applying filters..."): filtered_lbs_tmp = openstackapi.retrieve_load_balancers(filter_criteria) - # Perform name filtering here rather than adding it to filter_criteria - # because this allows for partial matching of the lb name - if args.name: - filtered_lbs = [lb for lb in filtered_lbs_tmp if args.name in lb.name] - else: - filtered_lbs = list(filtered_lbs_tmp) + # Perform name filtering here rather than adding it to filter_criteria + # because this allows for partial matching of the lb name + if args.name: + filtered_lbs = [lb for lb in filtered_lbs_tmp if args.name in lb.name] + else: + filtered_lbs = list(filtered_lbs_tmp) return filtered_lbs From f8f3a6ae2aa2885072cdf3d39e8988f6af0611b0 Mon Sep 17 00:00:00 2001 From: Thobias Salazar Trevisan Date: Tue, 12 Aug 2025 15:41:49 -0300 Subject: [PATCH 9/9] Bump version to 0.2.0 --- src/openstack_lb_info/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openstack_lb_info/__init__.py b/src/openstack_lb_info/__init__.py index f661f41..3f1e72f 100644 --- a/src/openstack_lb_info/__init__.py +++ b/src/openstack_lb_info/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- """openstack-lb-info module.""" -__version__ = "0.1.1" +__version__ = "0.2.0"