diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a4a76de..8066053 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: end-of-file-fixer - id: check-yaml @@ -8,18 +8,18 @@ repos: - id: debug-statements - repo: https://github.com/psf/black - rev: 24.8.0 + rev: 25.1.0 hooks: - id: black - repo: https://github.com/PyCQA/bandit - rev: 1.7.9 + rev: 1.8.6 hooks: - id: bandit exclude: ^tests/ - repo: https://github.com/PyCQA/flake8 - rev: 7.1.1 + rev: 7.3.0 hooks: - id: flake8 additional_dependencies: [flake8-bugbear, pep8-naming] @@ -32,7 +32,7 @@ repos: args: ["--profile", "black"] - repo: https://github.com/PyCQA/pylint - rev: v3.2.7 + rev: v3.3.8 hooks: - id: pylint additional_dependencies: diff --git a/README.md b/README.md index 2f9fbec..7704864 100644 --- a/README.md +++ b/README.md @@ -40,19 +40,25 @@ and optional details. If no amphoras match the filter criteria, it will indicate ## Example ```bash -$ 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] [--max-workers MAX_WORKERS] +$ 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] + [--availability-zone AVAILABILITY_ZONE] + [--vip-network-id VIP_NETWORK_ID] + [--vip-subnet-id VIP_SUBNET_ID] [--details] + [--no-members] [--max-workers MAX_WORKERS] A script to show OpenStack load balancers information. options: -h, --help show this help message and exit - -o {plain,rich,json}, --output-format {plain,rich,json} - Output format: 'plain', 'rich' or 'json' + --os-cloud OS_CLOUD Name of the cloud to load from clouds.yaml. + (Default 'envvars', which uses OS_* env vars) -t {lb,amphora}, --type {lb,amphora} Show information about load balancers or amphoras + -o {plain,rich,json}, --output-format {plain,rich,json} + Output format. (default: rich) --name NAME Filter load balancers name --id ID Filter load balancers id (UUID) --tags TAGS Filter load balancers tags @@ -66,9 +72,12 @@ options: Filter load balancers network id (UUID) --vip-subnet-id VIP_SUBNET_ID Filter load balancers subnet id (UUID) - --details Show all load balancers/amphora details + --details Show all load balancers/amphora details. (default: False) + --no-members Do not show load balancers pool members information. + (default: False) --max-workers MAX_WORKERS - Max number of concurrent threads to fetch members details (1-32). (default: 4) + 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/formatters.py b/src/openstack_lb_info/formatters.py index 828ab2c..ea649c3 100644 --- a/src/openstack_lb_info/formatters.py +++ b/src/openstack_lb_info/formatters.py @@ -206,7 +206,8 @@ def add_pool_to_tree(self, parent_tree, pool): 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)}" + f"oper_status:{self.format_status(pool.operating_status)} " + f"number_members:[cyan]{len(pool.members)}[/]" ) return self._add_to_tree(parent_tree, message) @@ -336,7 +337,8 @@ def add_pool_to_tree(self, parent_tree, pool): 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)}" + f"oper_status:{self.format_status(pool.operating_status)} " + f"number_members:{len(pool.members)}" ) return self._add_to_tree(parent_tree, message) diff --git a/src/openstack_lb_info/loadbalancer_info.py b/src/openstack_lb_info/loadbalancer_info.py index e1fc59e..0fd0d8d 100644 --- a/src/openstack_lb_info/loadbalancer_info.py +++ b/src/openstack_lb_info/loadbalancer_info.py @@ -17,11 +17,14 @@ class for interacting with the OpenStack environment and uses `OutputFormatter` about the amphorae associated with a Load Balancer. """ import concurrent.futures +import logging from dataclasses import dataclass from .formatters import OutputFormatter from .openstack_api import OpenStackAPI +log = logging.getLogger(__name__) + @dataclass class ProcessingContext: @@ -33,11 +36,13 @@ class ProcessingContext: 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. + no_members (bool): Do not show load balancer pool members information. """ openstack_api: OpenStackAPI details: bool max_workers: int + no_members: bool formatter: OutputFormatter @@ -59,8 +64,10 @@ def __init__(self, lb, context): self.formatter = context.formatter self.openstack_api = context.openstack_api self.max_workers = context.max_workers + self.no_members = context.no_members # The root of the display tree for the formatter self.lb_tree = None + log.info("Processing info for Load Balancer ID: %s (Name: %s)", self.lb.id, self.lb.name) def create_lb_tree(self): """ @@ -118,10 +125,11 @@ def add_pool_info(self, listener_tree, pool_id): else: self.formatter.add_empty_node(pool_tree, "Health Monitor") - if pool.members: - self.add_pool_members(pool_tree, pool.id, pool.members) - else: - self.formatter.add_empty_node(pool_tree, "Member") + if not self.no_members: + if pool.members: + self.add_pool_members(pool_tree, pool.id, pool.members) + else: + self.formatter.add_empty_node(pool_tree, "Member") else: self.formatter.add_empty_node(listener_tree, "Pool") @@ -155,6 +163,12 @@ def add_pool_members(self, pool_tree, pool_id, pool_members): """ # Avoid spinning up extra idle threads max_workers = min(self.max_workers, len(pool_members)) + log.debug( + "Using %s workers to fetch details of %s members (pool_id=%s)", + max_workers, + len(pool_members), + pool_id, + ) with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: # Create future for each member IDs @@ -206,6 +220,7 @@ def display_lb_info(self): f"[b]Loadbalancer ID: {self.lb.id} [bright_blue]({self.lb.name})[/]", align="center", ) + log.info("Displaying final tree for Load Balancer ID: %s", self.lb.id) self.formatter.print_tree(self.lb_tree) self.formatter.print("") diff --git a/src/openstack_lb_info/main.py b/src/openstack_lb_info/main.py index 3e6cc54..1ed6dfd 100644 --- a/src/openstack_lb_info/main.py +++ b/src/openstack_lb_info/main.py @@ -32,6 +32,7 @@ import argparse import ipaddress +import logging import sys import uuid @@ -44,6 +45,8 @@ from .loadbalancer_info import AmphoraInfo, LoadBalancerInfo, ProcessingContext from .openstack_api import OpenStackAPI +log = logging.getLogger(__name__) + # Max allowed threads for --max-workers MAX_WORKERS_LIMIT = 32 @@ -73,11 +76,20 @@ def parse_parameters(): ) parser.add_argument( - "-o", - "--output-format", - help="Output format: 'plain', 'rich' or 'json'", - choices=("plain", "rich", "json"), - default="rich", + "-d", + "--debug", + help="Enable debug log messages. (default: %(default)s)", + action="store_true", + required=False, + ) + parser.add_argument( + "--os-cloud", + help=( + "Name of the cloud to load from clouds.yaml. " + "(Default '%(default)s', which uses OS_* env vars)" + ), + type=str, + default="envvars", required=False, ) parser.add_argument( @@ -87,6 +99,14 @@ def parse_parameters(): choices=("lb", "amphora"), required=True, ) + parser.add_argument( + "-o", + "--output-format", + help="Output format. (default: %(default)s)", + choices=("plain", "rich", "json"), + default="rich", + required=False, + ) parser.add_argument("--name", help="Filter load balancers name", type=str, required=False) parser.add_argument( "--id", help="Filter load balancers id (UUID)", type=validate_uuid, required=False @@ -121,7 +141,13 @@ def parse_parameters(): ) parser.add_argument( "--details", - help="Show all load balancers/amphora details", + help="Show all load balancers/amphora details. (default: %(default)s)", + action="store_true", + required=False, + ) + parser.add_argument( + "--no-members", + help="Do not show load balancers pool members information. (default: %(default)s)", action="store_true", required=False, ) @@ -211,6 +237,23 @@ def validate_ip_address(value_str): raise argparse.ArgumentTypeError(f"Invalid IP address: {value_str!r}") from exc +def setup_logging(log_level): + """Setup logging configuration.""" + datefmt = "%Y-%m-%d %H:%M:%S" + msg_fmt = "%(asctime)s - %(module)s.%(funcName)s - [%(levelname)s] - %(message)s" + + formatter = logging.Formatter( + fmt=msg_fmt, + datefmt=datefmt, + ) + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(formatter) + + root_logger = logging.getLogger() + root_logger.setLevel(log_level) + root_logger.addHandler(handler) + + def query_openstack_lbs(openstackapi, args, formatter): """ Query OpenStack Load Balancers based on user-defined filters. @@ -238,6 +281,7 @@ def query_openstack_lbs(openstackapi, args, formatter): }.items() if v is not None } + log.debug("Retrieve load balancers filter: %s", filter_criteria) with formatter.status("Querying load balancers and applying filters..."): filtered_lbs_tmp = openstackapi.retrieve_load_balancers(filter_criteria) @@ -283,6 +327,10 @@ def main(): args = parse_parameters() + log_level = logging.DEBUG if args.debug else logging.WARNING + setup_logging(log_level) + log.debug("CMD line args: %s", args) + if args.output_format == "rich" and not RICH_AVAILABLE: sys.exit( "Error: 'rich' library is not installed. " @@ -293,9 +341,18 @@ def main(): formatter = get_formatter(args.output_format) # Create an instance of OpenStackAPI - openstackapi = OpenStackAPI() + try: + openstackapi = OpenStackAPI(args.os_cloud) + except RuntimeError as exc: + sys.exit(f"Error: {exc}") + + try: + filtered_lbs = query_openstack_lbs(openstackapi, args, formatter) + except Exception as exc: # pylint: disable=broad-exception-caught + log.debug("Error to query openstack:", exc_info=True) + sys.exit(f"Error: {exc}") - filtered_lbs = query_openstack_lbs(openstackapi, args, formatter) + log.info("Found %d load balancer(s) to process.", len(filtered_lbs)) if not filtered_lbs: formatter.print("No load balancer(s) found.") @@ -305,8 +362,10 @@ def main(): openstack_api=openstackapi, details=args.details, max_workers=args.max_workers, + no_members=args.no_members, formatter=formatter, ) + log.debug("Process context: %s", context) for lb in filtered_lbs: if args.type == "amphora": diff --git a/src/openstack_lb_info/openstack_api.py b/src/openstack_lb_info/openstack_api.py index ce2fe23..6f41b5e 100644 --- a/src/openstack_lb_info/openstack_api.py +++ b/src/openstack_lb_info/openstack_api.py @@ -9,23 +9,34 @@ listeners, pools, health monitors, members, amphorae, servers, and images. """ +import logging + import openstack +log = logging.getLogger(__name__) + class OpenStackAPI: """ Provides an interface for querying OpenStack load balancer resources. """ - def __init__(self, debug=False): + def __init__(self, os_cloud, debug=False): """ Initialize the OpenStackAPI instance and establish a connection. Args: - debug (bool): Whether to enable debug logging. + debug (bool): Whether to enable debug logging. + os_cloud (str): The name of the configuration to load from clouds.yaml. + If 'envvars', it loads config from environment variables """ + log.debug("Create openstack connect to cloud: '%s'", os_cloud) openstack.enable_logging(debug=debug) - self.os_conn = openstack.connect() + try: + self.os_conn = openstack.connect(cloud=os_cloud) + except Exception as exc: + log.debug("Openstack connection configuration failed:", exc_info=True) + raise RuntimeError(f"Failed to connect to OpenStack: {exc}") from exc def retrieve_load_balancers(self, filter_criteria): """ @@ -41,6 +52,7 @@ def retrieve_load_balancers(self, filter_criteria): filter criteria, or an empty list if no load balancers match the criteria. """ + log.debug("Retrieving load balancers with filters: %s", filter_criteria) filtered_lbs = self.os_conn.load_balancer.load_balancers(**filter_criteria) return filtered_lbs @@ -56,6 +68,7 @@ def retrieve_listener(self, listener_id): listener object representing the listener with the specified ID, or None if the listener is not found. """ + log.debug("Retrieving listener with ID: %s", listener_id) return self.os_conn.load_balancer.find_listener(listener_id) def retrieve_pool(self, pool_id): @@ -70,6 +83,7 @@ def retrieve_pool(self, pool_id): representing the pool with the specified ID, or None if the pool is not found. """ + log.debug("Retrieving pool with ID: %s", pool_id) return self.os_conn.load_balancer.find_pool(pool_id) def retrieve_health_monitor(self, health_monitor_id): @@ -84,6 +98,7 @@ def retrieve_health_monitor(self, health_monitor_id): balancer health monitor object representing the health monitor with the specified ID, or None if the health monitor is not found. """ + 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_member(self, member_id, pool_id): @@ -99,6 +114,7 @@ def retrieve_member(self, member_id, pool_id): object representing the member with the specified ID and associated pool, or None if the member is not found. """ + log.debug("Retrieving member with ID: %s from pool ID: %s", member_id, pool_id) return self.os_conn.load_balancer.find_member(member_id, pool_id) def retrieve_amphoraes(self, loadbalancer_id): @@ -114,6 +130,7 @@ def retrieve_amphoraes(self, loadbalancer_id): OpenStack amphora objects representing the amphorae associated with the specified load balancer. """ + log.debug("Retrieving amphoraes from LB ID: %s", loadbalancer_id) return self.os_conn.load_balancer.amphorae(loadbalancer_id=loadbalancer_id) def retrieve_server(self, server_id): @@ -128,6 +145,7 @@ def retrieve_server(self, server_id): openstack.compute.v2.server.Server: An OpenStack server object representing the specified server. """ + log.debug("Retrieving compute server with ID: %s", server_id) return self.os_conn.compute.find_server(server_id) def retrieve_images(self, image_ids): @@ -141,6 +159,7 @@ def retrieve_images(self, image_ids): Returns: list: A list of OpenStack image objects representing the specified images. """ + log.debug("Retrieving image with IDs: %s", image_ids) return self.os_conn.image.images(id=image_ids)