Created
August 26, 2025 18:49
-
-
Save outwitevil/2369cce21ccd6fe2331397c1df7d4763 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 | |
""" | |
list_hypervisor_tenants_parallel.py | |
Show which OpenStack projects (tenants) have instances running on a given | |
hypervisor (or on all hypervisors). The script now fetches the server list | |
*per hypervisor* in parallel, using the Nova ``host`` filter. | |
Requirements | |
------------ | |
* Python 3.8+ | |
* openstacksdk (pip install openstacksdk) | |
The script reads credentials from clouds.yaml **or** the usual OS_* | |
environment variables – exactly like the official openstack CLI. | |
""" | |
import argparse | |
import sys | |
import concurrent.futures | |
from collections import defaultdict | |
from typing import List, Tuple | |
# ---------------------------------------------------------------------- | |
# 3rd‑party SDK | |
# ---------------------------------------------------------------------- | |
import openstack | |
from openstack import exceptions as os_exc | |
# ---------------------------------------------------------------------- | |
# Tiny table printer (no external deps) | |
# ---------------------------------------------------------------------- | |
def _print_table(rows: List[List[str]], headers: List[str]) -> None: | |
"""Print a simple fixed‑width table.""" | |
# column widths = max width of each column (including header) | |
col_widths = [len(h) for h in headers] | |
for r in rows: | |
for i, cell in enumerate(r): | |
col_widths[i] = max(col_widths[i], len(str(cell))) | |
fmt = "| " + " | ".join(f"{{:{w}}}" for w in col_widths) + " |" | |
sep = "+-" + "-+-".join("-" * w for w in col_widths) + "-+" | |
print(sep) | |
print(fmt.format(*headers)) | |
print(sep) | |
for r in rows: | |
print(fmt.format(*r)) | |
print(sep) | |
# ---------------------------------------------------------------------- | |
# OpenStack connection helpers | |
# ---------------------------------------------------------------------- | |
def build_connection(cloud_name: str | None = None) -> openstack.connection.Connection: | |
"""Create an SDK connection (cloud name is optional).""" | |
try: | |
return openstack.connect(cloud=cloud_name) | |
except os_exc.SDKException as exc: | |
sys.stderr.write(f"[ERROR] Could not create OpenStack connection: {exc}\n") | |
sys.exit(1) | |
def get_hypervisors( | |
conn: openstack.connection.Connection, | |
name_or_id: str | None = None, | |
) -> List[openstack.compute.v2.hypervisor.Hypervisor]: | |
"""Return a list of hypervisor objects (filtered by name/ID if supplied).""" | |
hypervisors = list(conn.compute.hypervisors()) | |
if not hypervisors: | |
sys.stderr.write("[ERROR] No hypervisors found in this cloud.\n") | |
sys.exit(1) | |
if name_or_id is None: | |
return hypervisors | |
# exact ID match first | |
for hv in hypervisors: | |
if hv.id == name_or_id: | |
return [hv] | |
# then name (case‑insensitive) | |
for hv in hypervisors: | |
if hv.name.lower() == name_or_id.lower(): | |
return [hv] | |
sys.stderr.write(f"[ERROR] No hypervisor matching '{name_or_id}'.\n") | |
sys.stderr.write("Available hypervisors:\n") | |
for hv in hypervisors: | |
sys.stderr.write(f" - {hv.name} (id={hv.id})\n") | |
sys.exit(1) | |
def map_project_names(conn: openstack.connection.Connection) -> dict: | |
"""Build {project_id: project_name} for fast lookup.""" | |
proj_map = {} | |
for proj in conn.identity.projects(): | |
proj_map[proj.id] = proj.name | |
return proj_map | |
# ---------------------------------------------------------------------- | |
# Parallel fetch of instances per hypervisor | |
# ---------------------------------------------------------------------- | |
def fetch_instances_for_hypervisor( | |
conn: openstack.connection.Connection, | |
hypervisor_name: str, | |
) -> List[Tuple[openstack.compute.v2.server.Server, str]]: | |
""" | |
Return a list of (server, project_id) for *this* hypervisor. | |
Nova supports the ``host`` filter on the ``servers/detail`` call, so we | |
ask for exactly the servers that belong to the given compute node. | |
""" | |
instances = [] | |
try: | |
# The SDK forwards unknown kwargs to the REST call, so `host=` works. | |
for srv in conn.compute.servers( | |
details=True, | |
all_projects=True, | |
host=hypervisor_name, | |
): | |
# The server object already carries the project_id. | |
instances.append((srv, srv.project_id)) | |
except os_exc.SDKException as exc: | |
sys.stderr.write( | |
f"[WARN] Could not fetch servers for hypervisor " | |
f"'{hypervisor_name}': {exc}\n" | |
) | |
return instances | |
# ---------------------------------------------------------------------- | |
# Main driver | |
# ---------------------------------------------------------------------- | |
def main() -> None: | |
parser = argparse.ArgumentParser( | |
description=( | |
"Show which OpenStack projects (tenants) have instances on a " | |
"given hypervisor (or on all hypervisors). Server queries are " | |
"performed in parallel for speed." | |
) | |
) | |
parser.add_argument( | |
"hypervisor", | |
nargs="?", | |
default=None, | |
help="Hypervisor name or ID. Omit to scan every hypervisor.", | |
) | |
parser.add_argument( | |
"--cloud", | |
help="Name of the cloud entry in clouds.yaml (defaults to the first entry).", | |
) | |
parser.add_argument( | |
"--max-workers", | |
type=int, | |
default=None, | |
help=( | |
"Maximum number of threads for the parallel fetch. " | |
"If omitted, defaults to min(32, number_of_hypervisors)." | |
), | |
) | |
args = parser.parse_args() | |
# ------------------------------------------------------------------ | |
# 1️⃣ Connect | |
# ------------------------------------------------------------------ | |
conn = build_connection(cloud_name=args.cloud) | |
# ------------------------------------------------------------------ | |
# 2️⃣ Resolve hypervisors (single or all) | |
# ------------------------------------------------------------------ | |
hypervisors = get_hypervisors(conn, args.hypervisor) | |
# ------------------------------------------------------------------ | |
# 3️⃣ Project‑id → name map (Keystone) | |
# ------------------------------------------------------------------ | |
project_names = map_project_names(conn) | |
# ------------------------------------------------------------------ | |
# 4️⃣ Parallel fetch | |
# ------------------------------------------------------------------ | |
# Determine pool size | |
max_workers = ( | |
args.max_workers | |
if args.max_workers is not None | |
else min(32, len(hypervisors)) | |
) | |
rows: List[List[str]] = [] | |
# We use a dict to keep results keyed by hypervisor name (useful for ordering) | |
hv_to_instances: dict[str, List[Tuple[openstack.compute.v2.server.Server, str]]] = {} | |
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: | |
# Submit one task per hypervisor | |
future_to_hv = { | |
executor.submit(fetch_instances_for_hypervisor, conn, hv.name): hv | |
for hv in hypervisors | |
} | |
for future in concurrent.futures.as_completed(future_to_hv): | |
hv = future_to_hv[future] | |
try: | |
instances = future.result() | |
hv_to_instances[hv.name] = instances | |
except Exception as exc: # pragma: no cover (should not happen) | |
sys.stderr.write( | |
f"[ERROR] Unexpected error while processing hypervisor " | |
f"'{hv.name}': {exc}\n" | |
) | |
hv_to_instances[hv.name] = [] | |
# ------------------------------------------------------------------ | |
# 5️⃣ Build rows for the table | |
# ------------------------------------------------------------------ | |
for hv in hypervisors: # preserve the order returned by the API | |
instances = hv_to_instances.get(hv.name, []) | |
if not instances: | |
rows.append([hv.name, "(none)", "(none)"]) | |
continue | |
for srv, proj_id in instances: | |
proj_name = project_names.get(proj_id, f"<unknown:{proj_id}>") | |
rows.append([hv.name, srv.name, proj_name]) | |
# ------------------------------------------------------------------ | |
# 6️⃣ Output | |
# ------------------------------------------------------------------ | |
if rows: | |
_print_table( | |
rows, | |
headers=["Hypervisor", "Server (Instance)", "Project (Tenant)"], | |
) | |
else: | |
print("No instances found on the selected hypervisor(s).") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment