Skip to content

Instantly share code, notes, and snippets.

@outwitevil
Created September 19, 2025 12:19
Show Gist options
  • Save outwitevil/06bd46eea22d329fa50583812b2e63f0 to your computer and use it in GitHub Desktop.
Save outwitevil/06bd46eea22d329fa50583812b2e63f0 to your computer and use it in GitHub Desktop.
#!/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