diff --git a/README.md b/README.md index 7704864..925cee9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ openstack-lb-info - A command-line tool for displaying OpenStack Load Balancer r # About -This Python script is designed to interact with an OpenStack cloud infrastructure and retrieve information about +This Python script interacts with an OpenStack cloud infrastructure and retrieve information about load balancers and their components such as listeners, pools, health monitors, members, and amphorae. It displays the information in a visually appealing and user-friendly way and provide a clear representation of the load balancer resources. @@ -37,10 +37,11 @@ about amphoras associated with load balancers. Amphoras are responsible for hand information includes amphora IDs, roles, status, load balancer network IP addresses, associated images, server information, and optional details. If no amphoras match the filter criteria, it will indicate that no amphoras were found. -## Example +## CLI Options ```bash -$ usage: openstack-lb-info [-h] [-d] [--os-cloud OS_CLOUD] -t {lb,amphora} +$ openstack-lb-info --help +usage: openstack-lb-info [-h] [-d] [--os-cloud OS_CLOUD] -t {lb,amphora} [-o {plain,rich,json}] [--name NAME] [--id ID] [--tags TAGS] [--flavor-id FLAVOR_ID] [--vip-address VIP_ADDRESS] @@ -87,31 +88,44 @@ options: openstack-lb-info --type amphora --id load_balancer_id --details ``` + +## Example + ![example](img/example.png) ## Authentication Methods ##### Environment Variables -You can manually set the required environment variables or use an OpenStack RC file to simplify the process. +You can manually set the required environment variables or source an OpenStack RC file. ##### clouds.yaml Configuration -Alternatively, you can use a *clouds.yaml* and export "*OS_CLOUD*" variable to pass the cloud name. +Alternatively, you can use a *clouds.yaml* and export "*OS_CLOUD*" environment variable to pass the cloud name, +or specify it directly using the `--os-cloud` option. For more information: https://docs.openstack.org/python-openstackclient/latest/cli/man/openstack.html ## Installation -Clone or download the repository to your local machine. +Install from PyPI: -#### Development mode using pip ```bash -$ pip install -e . +pip install openstack-lb-info ``` +Or, clone the repository and install from source in development mode: -#### Development mode using pipx ```bash -$ pipx install -e . +git clone https://github.com/thobiast/openstack-loadbalancer-info.git +cd openstack-loadbalancer-info +python3 -m venv venv +source venv/bin/activate +pip install -e . ``` +You can also use **pipx** to install it in an isolated environment automatically: + +```bash +pipx install -e . +``` + ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. diff --git a/pyproject.toml b/pyproject.toml index bdc0d78..313a18c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,8 @@ name = "openstack-lb-info" description = "A script to display OpenStack Load Balancer resource details." readme = "README.md" -license = {file = "LICENSE"} +license = "MIT" +license-files = ["LICENSE"] dynamic = ["version", "dependencies"] requires-python = ">=3.7" authors = [ diff --git a/src/openstack_lb_info/__init__.py b/src/openstack_lb_info/__init__.py index 96389a9..11104fb 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.2.1" +__version__ = "0.2.2" diff --git a/src/openstack_lb_info/formatters.py b/src/openstack_lb_info/formatters.py index ea649c3..0b2fb3a 100644 --- a/src/openstack_lb_info/formatters.py +++ b/src/openstack_lb_info/formatters.py @@ -81,6 +81,14 @@ def add_listener_to_tree(self, parent_tree, listener): def add_pool_to_tree(self, parent_tree, pool): """Add a formatted pool node to a parent tree.""" + @abstractmethod + def add_l7policy_to_tree(self, parent_tree, l7policy): + """Add a formatted L7 Policy node to a parent tree.""" + + @abstractmethod + def add_l7rule_to_tree(self, parent_tree, l7rule): + """Add a formatted L7 Rule node to a parent tree.""" + @abstractmethod def add_health_monitor_to_tree(self, parent_tree, hm): """Add a formatted health monitor node to a parent tree.""" @@ -195,7 +203,8 @@ def add_listener_to_tree(self, parent_tree, listener): 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)}" + f"oper_status:{self.format_status(listener.operating_status)} " + f"default_pool_id:[b white]{listener.default_pool_id}[/]" ) return self._add_to_tree(parent_tree, message) @@ -211,6 +220,37 @@ def add_pool_to_tree(self, parent_tree, pool): ) return self._add_to_tree(parent_tree, message) + def add_l7policy_to_tree(self, parent_tree, l7policy): + """Add a styled L7 Policy node to the tree.""" + message = ( + f"[b green]L7 Policy:[/] [b white]{l7policy.id}[/] " + f"([blue b]{l7policy.name}[/]) " + f"position:[magenta]{l7policy.position}[/magenta] " + f"action:[magenta]{l7policy.action}[/magenta] " + f"prov_status:{self.format_status(l7policy.provisioning_status)} " + f"oper_status:{self.format_status(l7policy.operating_status)}" + ) + redir_attrs = ["redirect_pool_id", "redirect_prefix", "redirect_url"] + for attr in redir_attrs: + value = getattr(l7policy, attr, None) + if value is not None: + message += f" {attr}:{value}" + return self._add_to_tree(parent_tree, message) + + def add_l7rule_to_tree(self, parent_tree, l7rule): + """Add a styled L7 Rule node to the tree.""" + message = ( + f"[b green]L7 Rule:[/] [b white]{l7rule.id}[/] " + f"compare_type:[magenta]{l7rule.compare_type}[/magenta] " + f"invert:[magenta]{l7rule.invert}[/magenta] " + f"key:[magenta]{l7rule.key}[/magenta] " + f"type:[magenta]{l7rule.type}[/magenta] " + f"rule_value:[magenta]{l7rule.rule_value}[/magenta] " + f"prov_status:{self.format_status(l7rule.provisioning_status)} " + f"oper_status:{self.format_status(l7rule.operating_status)}" + ) + 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 = ( @@ -342,6 +382,23 @@ def add_pool_to_tree(self, parent_tree, pool): ) return self._add_to_tree(parent_tree, message) + def add_l7policy_to_tree(self, parent_tree, l7policy): + message = f"L7 Policy: {l7policy.id} " + return self._add_to_tree(parent_tree, message) + + def add_l7rule_to_tree(self, parent_tree, l7rule): + message = ( + f"L7 Rule: {l7rule.id} " + f"compare_type:{l7rule.compare_type} " + f"invert:{l7rule.invert} " + f"key:{l7rule.key} " + f"type:{l7rule.type} " + f"rule_value:{l7rule.rule_value} " + f"prov_status:{self.format_status(l7rule.provisioning_status)} " + f"oper_status:{self.format_status(l7rule.operating_status)}" + ) + return self._add_to_tree(parent_tree, message) + def add_health_monitor_to_tree(self, parent_tree, hm): message = ( f"Health Monitor: {hm.id} " @@ -437,6 +494,12 @@ def add_listener_to_tree(self, parent_tree, listener): def add_pool_to_tree(self, parent_tree, pool): return self._add_node_from_obj(parent_tree, "pool", pool) + def add_l7policy_to_tree(self, parent_tree, l7policy): + return self._add_node_from_obj(parent_tree, "l7policy", l7policy) + + def add_l7rule_to_tree(self, parent_tree, l7rule): + return self._add_node_from_obj(parent_tree, "l7rule", l7rule) + def add_health_monitor_to_tree(self, parent_tree, hm): return self._add_node_from_obj(parent_tree, "health_monitor", hm) diff --git a/src/openstack_lb_info/loadbalancer_info.py b/src/openstack_lb_info/loadbalancer_info.py index 0fd0d8d..a5e8926 100644 --- a/src/openstack_lb_info/loadbalancer_info.py +++ b/src/openstack_lb_info/loadbalancer_info.py @@ -80,7 +80,7 @@ def create_lb_tree(self): if self.details: self.formatter.add_details_to_tree(self.lb_tree, self.lb.to_dict()) - def add_listener_info(self, lb_tree, listener_id): + def add_listener_info(self, lb_tree, listener_id, listener_pool_ids): """ Add information about the Listener to the Load Balancer's tree. @@ -88,6 +88,8 @@ def add_listener_info(self, lb_tree, listener_id): 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. + listener_pool_ids (set[str]): A set of pool IDs associated with this + listener. """ with self.formatter.status(f"Getting Listener details id [b]{listener_id}[/b]"): listener = self.openstack_api.retrieve_listener(listener_id) @@ -97,13 +99,62 @@ def add_listener_info(self, lb_tree, listener_id): if self.details: self.formatter.add_details_to_tree(listener_tree, listener.to_dict()) - if listener.default_pool_id: - self.add_pool_info(listener_tree, listener.default_pool_id) + if listener.l7_policies: + for l7policy in listener.l7_policies: + self.add_l7policy_info(listener_tree, l7policy["id"]) else: - self.formatter.add_empty_node(listener_tree, "Pool") + self.formatter.add_empty_node(listener_tree, "L7 Policy") + + for pool_id in listener_pool_ids: + self.add_pool_info(listener_tree, pool_id) else: self.formatter.add_empty_node(lb_tree, "Listener") + def add_l7policy_info(self, listener_tree, l7policy_id): + """ + Add information about the L7 Policy to the listener's tree. + + Args: + listener_tree (object): The tree representing the listener. + l7policy_id (str): The ID of the L7 Policy for which to retrieve and display. + """ + with self.formatter.status(f"Getting L7 Policy details id [b]{l7policy_id}[/b]"): + l7policy = self.openstack_api.retrieve_l7_policy(l7policy_id) + + if l7policy: + l7_tree = self.formatter.add_l7policy_to_tree(listener_tree, l7policy) + + if self.details: + self.formatter.add_details_to_tree(l7_tree, l7policy.to_dict()) + + self.add_l7rules_info(l7_tree, l7policy) + + def add_l7rules_info(self, l7_tree, l7policy): + """ + Add information about the L7 Rules to the L7 Policy's tree. + + Args: + l7_tree (object): The tree representing the l7 policy. + l7policy (openstack.load_balancer.v2.l7_policy.L7Policy): The L7 Policy + for which to retrieve and display the rules. + """ + rule_ids = [rule["id"] for rule in l7policy.rules if "id" in rule] + if not rule_ids: + self.formatter.add_empty_node(l7_tree, "L7 Rule") + return + + for rule_id in rule_ids: + with self.formatter.status(f"Getting L7 Rule details id [b]{rule_id}[/b]"): + l7rule = self.openstack_api.retrieve_l7_rule(rule_id, l7policy.id) + + if not l7rule: + self.formatter.add_empty_node(l7_tree, f"L7 Rule ({rule_id})") + continue + + l7rule_tree = self.formatter.add_l7rule_to_tree(l7_tree, l7rule) + if self.details: + self.formatter.add_details_to_tree(l7rule_tree, l7rule.to_dict()) + def add_pool_info(self, listener_tree, pool_id): """ Add information about the Pool to the listener's tree. @@ -213,8 +264,16 @@ def display_lb_info(self): if not self.lb.listeners: self.formatter.add_empty_node(self.lb_tree, "Listener") else: + # Get all pools associated with this load balancer + lb_pools = list(self.openstack_api.retrieve_pools(loadbalancer_id=self.lb.id)) for listener in self.lb.listeners: - self.add_listener_info(self.lb_tree, listener["id"]) + # Filter the ids of the pools attached to this specific listener + listener_pool_ids = { + pool.id + for pool in lb_pools + if any(lstn.get("id") == listener["id"] for lstn in (pool.listeners or [])) + } + self.add_listener_info(self.lb_tree, listener["id"], listener_pool_ids) self.formatter.rule( f"[b]Loadbalancer ID: {self.lb.id} [bright_blue]({self.lb.name})[/]", diff --git a/src/openstack_lb_info/openstack_api.py b/src/openstack_lb_info/openstack_api.py index 6f41b5e..e8a30b5 100644 --- a/src/openstack_lb_info/openstack_api.py +++ b/src/openstack_lb_info/openstack_api.py @@ -86,6 +86,22 @@ def retrieve_pool(self, pool_id): log.debug("Retrieving pool with ID: %s", pool_id) return self.os_conn.load_balancer.find_pool(pool_id) + def retrieve_pools(self, loadbalancer_id): + """ + Retrieve pools associated with an OpenStack load balancer. + + Args: + loadbalancer_id (str): The ID of the load balancer for which pools are + to be retrieved. + + Returns: + Generator[openstack.load_balancer.v2.pool.Pool]: A generator of OpenStack + pool objects representing the pools associated with the specified + load balancer. + """ + log.debug("Retrieving pools for load balancer ID: %s", loadbalancer_id) + return self.os_conn.load_balancer.pools(loadbalancer_id=loadbalancer_id) + def retrieve_health_monitor(self, health_monitor_id): """ Retrieve details of an OpenStack load balancer health monitor. @@ -101,6 +117,40 @@ def retrieve_health_monitor(self, health_monitor_id): log.debug("Retrieving health monitor with ID: %s", health_monitor_id) return self.os_conn.load_balancer.find_health_monitor(health_monitor_id) + def retrieve_l7_policy(self, l7_policy_id): + """ + Retrieve details of an L7 Policy. + + Args: + l7_policy_id (str): The ID of the L7 Policy to retrieve. + + Returns: + openstack.load_balancer.v2.l7_policy.L7Policy | None: + The L7 Policy object if found, otherwise None. + """ + log.debug("Retrieving L7 Policy with ID: %s", l7_policy_id) + return self.os_conn.load_balancer.find_l7_policy(l7_policy_id) + + def retrieve_l7_rule(self, l7_rule_id, l7_policy_id, ignore_missing=True): + """ + Retrieve details of an L7 Rule. + + Args: + l7_rule_id (str): The ID of the L7 Rule to retrieve. + l7_policy_id (str): The ID of the L7 Policy that the l7rule belongs to. + ignore_missing (bool, optional): If True, returns None if the l7rule + is not found. If False, raises an exception if the rule + does not exist. Defaults to True. + + Returns: + openstack.load_balancer.v2.l7_rule.L7Rule | None: + The L7 Rule object if found, otherwise None. + """ + log.debug("Retrieving L7 Rule %s for L7 Policy %s", l7_rule_id, l7_policy_id) + return self.os_conn.load_balancer.find_l7_rule( + l7_rule_id, l7_policy_id, ignore_missing=ignore_missing + ) + def retrieve_member(self, member_id, pool_id): """ Retrieve details of an load balancer member by its ID and associated pool.