Created
September 19, 2025 12:19
-
-
Save outwitevil/06bd46eea22d329fa50583812b2e63f0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Show which OpenStack projects (tenants) have instances on one or more | |
| hypervisors, optionally displaying: | |
| * instance UUID (--show-id) | |
| * instance status (--show-status) | |
| * host‑aggregate name (--show-aggregate) | |
| The “aggregate” column now shows **only the first aggregate name** a | |
| hypervisor belongs to, even if the host is part of several aggregates. | |
| A new optional flag **--project** limits the output to a single project | |
| (name or ID). Hypervisors that have **no servers matching that project** | |
| are omitted entirely (no “(none)” line). | |
| **New flag** `--all` forces all optional columns (id, status, aggregate) to be shown. | |
| Results can be printed as a pretty table (default) or as plain | |
| tab‑separated values, and the header row can be omitted. | |
| """ | |
| import argparse | |
| import sys | |
| import concurrent.futures | |
| from typing import List, Tuple | |
| import openstack | |
| from openstack import exceptions as os_exc | |
| # … (printing helpers, OpenStack helpers, sorting helpers unchanged) … | |
| def main() -> None: | |
| parser = argparse.ArgumentParser( | |
| description=( | |
| "Show which OpenStack projects (tenants) have instances on one or more " | |
| "hypervisors. Optional columns: instance UUID (--show-id), " | |
| "instance status (--show-status) and host‑aggregate(s) (--show-aggregate). " | |
| "Use --project to limit the output to a single project (name or ID). " | |
| "Use --all to display all optional columns. " | |
| "Results can be printed as a pretty table (default) or as plain " | |
| "tab‑separated values, and the header row can be omitted." | |
| ) | |
| ) | |
| parser.add_argument( | |
| "hypervisors", | |
| nargs="*", | |
| help=( | |
| "One or more hypervisor names or IDs. Separate multiple values with " | |
| "spaces or commas. Omit to scan every hypervisor." | |
| ), | |
| ) | |
| parser.add_argument("--cloud", help="Name of the cloud entry in clouds.yaml.") | |
| parser.add_argument( | |
| "--max-workers", | |
| type=int, | |
| default=None, | |
| help="Maximum number of threads for the parallel fetch." | |
| ) | |
| parser.add_argument( | |
| "--show-id", | |
| action="store_true", | |
| help="Add a column with the instance (server) UUID." | |
| ) | |
| parser.add_argument( | |
| "--show-status", | |
| action="store_true", | |
| help="Add a column with the instance operational status." | |
| ) | |
| parser.add_argument( | |
| "--show-aggregate", | |
| action="store_true", | |
| help="Add a column with the **first** host‑aggregate name the hypervisor belongs to." | |
| ) | |
| # ---------- NEW OPTION ---------- | |
| parser.add_argument( | |
| "--all", | |
| action="store_true", | |
| help="Show all optional columns (id, status, aggregate)." | |
| ) | |
| # ---------- END NEW OPTION ---------- | |
| parser.add_argument( | |
| "--project", | |
| help="Filter results to a single project (tenant) – specify name or UUID." | |
| ) | |
| parser.add_argument( | |
| "--sort-by", | |
| dest="sort_by", | |
| default=None, | |
| help=( | |
| "Comma‑separated list of columns to sort by. Valid values are: " | |
| "hypervisor, server, project, id, status, aggregate." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--reverse", | |
| action="store_true", | |
| help="Reverse the sort order." | |
| ) | |
| parser.add_argument( | |
| "--no-header", | |
| action="store_true", | |
| help="Omit the header row from the output." | |
| ) | |
| parser.add_argument( | |
| "--plain", | |
| action="store_true", | |
| help="Emit raw tab‑separated rows (no table borders)." | |
| ) | |
| args = parser.parse_args() | |
| # If --all is set, enable every optional column | |
| if args.all: | |
| args.show_id = True | |
| args.show_status = True | |
| args.show_aggregate = True | |
| # ------ Connect, resolve hypervisors, project map, etc. (unchanged) ------ | |
| conn = build_connection(cloud_name=args.cloud) | |
| all_hvs = get_all_hypervisors(conn) | |
| # Accept both space‑ and comma‑separated specifications | |
| user_tokens = [] | |
| for token in args.hypervisors: | |
| user_tokens.extend([t.strip() for t in token.split(",") if t.strip()]) | |
| selected_hvs = resolve_hypervisor_names(all_hvs, user_tokens) | |
| project_names = map_project_names(conn) | |
| # Resolve optional project filter (unchanged) | |
| filter_proj_id: str | None = None | |
| if args.project: | |
| if args.project in project_names: | |
| filter_proj_id = args.project | |
| else: | |
| matches = [pid for pid, name in project_names.items() | |
| if name.lower() == args.project.lower()] | |
| if len(matches) == 1: | |
| filter_proj_id = matches[0] | |
| elif len(matches) > 1: | |
| sys.stderr.write( | |
| f"[ERROR] Ambiguous project name '{args.project}'. " | |
| f"Multiple projects share this name.\n" | |
| ) | |
| sys.exit(1) | |
| else: | |
| sys.stderr.write( | |
| f"[ERROR] No project matches '{args.project}'.\n" | |
| ) | |
| sys.exit(1) | |
| host_to_agg = build_aggregate_map(conn) | |
| # Parallel fetch (unchanged) | |
| max_workers = ( | |
| args.max_workers | |
| if args.max_workers is not None | |
| else min(32, len(selected_hvs)) | |
| ) | |
| hv_to_instances: dict[ | |
| str, List[Tuple[openstack.compute.v2.server.Server, str]] | |
| ] = {} | |
| with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: | |
| future_to_hv = { | |
| executor.submit(fetch_instances_for_hypervisor, conn, hv.name): hv | |
| for hv in selected_hvs | |
| } | |
| for future in concurrent.futures.as_completed(future_to_hv): | |
| hv = future_to_hv[future] | |
| try: | |
| hv_to_instances[hv.name] = future.result() | |
| except Exception as exc: # pragma: no cover | |
| sys.stderr.write( | |
| f"[ERROR] Unexpected error while processing hypervisor " | |
| f"'{hv.name}': {exc}\n" | |
| ) | |
| hv_to_instances[hv.name] = [] | |
| # ------ Build rows (with optional project filter) ------ | |
| rows: List[List[str]] = [] | |
| for hv in selected_hvs: | |
| instances = hv_to_instances.get(hv.name, []) | |
| agg_names = host_to_agg.get(hv.name, []) | |
| agg_cell = agg_names[0] if agg_names else "(none)" | |
| filtered_rows: List[List[str]] = [] | |
| for srv, proj_id in instances: | |
| if filter_proj_id and proj_id != filter_proj_id: | |
| continue | |
| proj_name = project_names.get(proj_id, f"<unknown:{proj_id}>") | |
| filtered_rows.append([ | |
| hv.name, # 0 | |
| srv.name, # 1 | |
| proj_name, # 2 | |
| srv.id, # 3 | |
| srv.status or "(unknown)", # 4 | |
| agg_cell # 5 | |
| ]) | |
| if not filtered_rows: | |
| if not filter_proj_id: | |
| rows.append([hv.name, "(none)", "(none)", "(none)", "(none)", "(none)"]) | |
| else: | |
| rows.extend(filtered_rows) | |
| # ------ Sorting (unchanged) ------ | |
| if args.sort_by: | |
| sort_keys = _parse_sort_keys(args.sort_by) | |
| col_index = { | |
| "hypervisor": 0, | |
| "server": 1, | |
| "project": 2, | |
| "id": 3, | |
| "status": 4, | |
| "aggregate": 5, | |
| } | |
| rows = sort_rows(rows, sort_keys, col_index, args.reverse) | |
| # ------ Trim columns according to requested output format ------ | |
| # Build header list based on which optional columns are enabled | |
| headers = ["hypervisor", "server", "project", "id", "status", "aggregate"] | |
| if not args.show_id: | |
| headers.remove("id") | |
| if not args.show_status: | |
| headers.remove("status") | |
| if not args.show_aggregate: | |
| headers.remove("aggregate") | |
| # Determine which internal columns we need to keep | |
| internal_to_output = { | |
| 0: "hypervisor", | |
| 1: "server", | |
| 2: "project", | |
| 3: "id", | |
| 4: "status", | |
| 5: "aggregate", | |
| } | |
| keep_indices = [i for i, name in internal_to_output.items() if name in headers] | |
| # Trim each row to only the selected columns | |
| rows = [[row[i] for i in keep_indices] for row in rows] | |
| # ------ Print the result ------ | |
| if args.plain: | |
| _print_plain(rows, headers, args.no_header) | |
| else: | |
| _print_pretty_table(rows, headers) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment