Skip to content

Instantly share code, notes, and snippets.

@outwitevil
Created August 26, 2025 18:49
Show Gist options
  • Save outwitevil/2369cce21ccd6fe2331397c1df7d4763 to your computer and use it in GitHub Desktop.
Save outwitevil/2369cce21ccd6fe2331397c1df7d4763 to your computer and use it in GitHub Desktop.
#!/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