"""Where a new VM goes: a pure decision over plain inventory rows. A host qualifies when it is connected, not in maintenance mode, and sees both a usable datastore and every requested network. Among those, the pair with the most free space wins -- or the named datastore, when one is given. The resource pool comes from the host's compute resource and the VM folder from the datacenter above it; a standalone ESXi host has the same shape (``ha-compute-res`` under ``ha-datacenter``), so one path serves both. """ from __future__ import annotations from dataclasses import dataclass, field from typing import Any Row = dict[str, Any] @dataclass class Inventory: hosts: list[Row] datastores: list[Row] networks: list[Row] compute_resources: list[Row] folders: list[Row] = field(default_factory=list) datacenters: list[Row] = field(default_factory=list) @dataclass class Placement: host: str host_name: str datastore: str datastore_name: str resource_pool: str folder: str datacenter_name: str networks: list[str] def _usable_datastores(inv: Inventory, storage: str | None) -> dict[str, Row]: usable = { ds["_moref"]: ds for ds in inv.datastores if (ds.get("summary") or {}).get("accessible") and (ds.get("summary") or {}).get("maintenanceMode", "normal") == "normal" and (storage is None or ds.get("name") == storage) } if storage is not None and not usable: raise ValueError(f"There is no usable datastore named {storage!r}") return usable def _host_networks( host: Row, names_by_moref: dict[str, str], wanted: list[str] ) -> list[str] | None: """The host's network moref for each wanted name, or None if one is missing.""" by_name = {names_by_moref.get(m): m for m in host.get("network") or []} found = [by_name.get(name) for name in wanted] return None if None in found else [m for m in found if m] def _datacenter(inv: Inventory, compute_resource: str) -> Row: parents = {row["_moref"]: row.get("parent") for row in inv.compute_resources + inv.folders} datacenters = {dc["_moref"]: dc for dc in inv.datacenters} node: str | None = compute_resource while node is not None and node not in datacenters: node = parents.get(node) if node is None: raise RuntimeError(f"No datacenter above {compute_resource!r}") return datacenters[node] def choose_placement(inv: Inventory, storage: str | None, networks: list[str]) -> Placement: usable = _usable_datastores(inv, storage) names = {n["_moref"]: n.get("name", "") for n in inv.networks} best: tuple[int, Row, Row, list[str]] | None = None for host in sorted(inv.hosts, key=lambda h: h.get("name", "")): if host.get("runtime.connectionState") != "connected" or host.get( "runtime.inMaintenanceMode" ): continue host_nets = _host_networks(host, names, networks) if host_nets is None: continue for ds_ref in host.get("datastore") or []: ds = usable.get(ds_ref) free = int((ds or {}).get("summary", {}).get("freeSpace", 0)) if ds is not None and (best is None or free > best[0]): best = (free, host, ds, host_nets) if best is None: raise ValueError( f"There is no host that sees datastore {storage or '(any)'} and networks {networks}" ) _, host, ds, host_nets = best compute = next(c for c in inv.compute_resources if c["_moref"] == host.get("parent")) dc = _datacenter(inv, compute["_moref"]) return Placement( host=host["_moref"], host_name=host.get("name", ""), datastore=ds["_moref"], datastore_name=ds.get("name", ""), resource_pool=compute["resourcePool"], folder=dc["vmFolder"], datacenter_name=dc.get("name", ""), networks=host_nets, )