Files
vmware-api-simulator/app/vsphere/soap/property_collector.py
T
inecs f8d3cbdd59 Initial commit: VMware vSphere API simulator scaffold.
Add the FastAPI app, PostgreSQL migrations, Docker/Helm packaging, API
contracts, docs, client examples, and the unit/integration/compatibility
test suite for local client and tooling labs without a real vCenter.
2026-07-18 04:42:11 +03:00

1640 lines
67 KiB
Python

"""PropertyCollector helpers: filter specs, updates, folder traversal props."""
from __future__ import annotations
import json
import re
import secrets
from typing import Any
from xml.sax.saxutils import escape
from app.db.pool import Database
from app.vsphere import inventory
from app.vsphere.inventory import ManagedObject
# Types that inherit ManagedEntity (govmomi Ancestors / Finder propSet Type=ManagedEntity).
_MANAGED_ENTITY_TYPES = frozenset(
{
"Folder",
"Datacenter",
"VirtualMachine",
"HostSystem",
"ClusterComputeResource",
"ComputeResource",
"ResourcePool",
"VirtualApp",
"StoragePod",
"Datastore",
"Network",
"DistributedVirtualPortgroup",
"VmwareDistributedVirtualSwitch",
"DistributedVirtualSwitch",
"OpaqueNetwork",
}
)
def _decode(value: Any) -> Any:
current = value
while isinstance(current, str):
try:
current = json.loads(current)
except json.JSONDecodeError:
break
return current
async def clear_pc_state(database: Database | None = None) -> None:
"""Drop durable PC views/tokens/versions (and orphan ContainerView inventory rows)."""
if database is None:
return
pool = database.pool # type: ignore[attr-defined]
async with pool.acquire() as conn:
await conn.execute(
"""
DO $$ BEGIN
IF to_regclass('public.vsphere_pc_state') IS NOT NULL THEN
DELETE FROM vsphere_pc_state;
END IF;
END $$;
"""
)
await conn.execute("DELETE FROM vsphere_objects WHERE type = 'ContainerView'")
async def _pc_put(database: Database, kind: str, key: str, payload: Any) -> None:
pool = database.pool # type: ignore[attr-defined]
async with pool.acquire() as conn:
await conn.execute(
"""
INSERT INTO vsphere_pc_state (kind, key, payload, updated_at)
VALUES ($1, $2, $3::jsonb, now())
ON CONFLICT (kind, key) DO UPDATE SET
payload = EXCLUDED.payload,
updated_at = now()
""",
kind,
key,
json.dumps(payload),
)
async def _pc_get(database: Database, kind: str, key: str) -> Any | None:
pool = database.pool # type: ignore[attr-defined]
async with pool.acquire() as conn:
row = await conn.fetchrow(
"SELECT payload FROM vsphere_pc_state WHERE kind = $1 AND key = $2",
kind,
key,
)
if row is None:
return None
return _decode(row["payload"])
async def _pc_pop(database: Database, kind: str, key: str) -> Any | None:
pool = database.pool # type: ignore[attr-defined]
async with pool.acquire() as conn:
row = await conn.fetchrow(
"""
DELETE FROM vsphere_pc_state
WHERE kind = $1 AND key = $2
RETURNING payload
""",
kind,
key,
)
if row is None:
return None
return _decode(row["payload"])
def parse_path_sets(body: str) -> list[str]:
return re.findall(r"<(?:\w+:)?pathSet[^>]*>([^<]+)</(?:\w+:)?pathSet>", body)
def parse_obj_refs(body: str) -> list[tuple[str, str]]:
return [
(m.group(1), m.group(2))
for m in re.finditer(
r'<(?:\w+:)?obj[^>]*type="([^"]+)"[^>]*>([^<]+)</(?:\w+:)?obj>',
body,
)
]
def parse_view_types(body: str) -> list[str]:
return re.findall(r"<(?:\w+:)?type[^>]*>([^<]+)</(?:\w+:)?type>", body)
def parse_prop_types(body: str) -> list[str]:
"""Types listed under propSet (what the client wants returned)."""
return re.findall(
r"<(?:\w+:)?propSet\b[^>]*>\s*<(?:\w+:)?type[^>]*>([^<]+)</(?:\w+:)?type>",
body,
flags=re.DOTALL,
)
def parse_continue_token(body: str) -> str | None:
match = re.search(r"<(?:\w+:)?token[^>]*>([^<]+)</(?:\w+:)?token>", body)
return match.group(1).strip() if match else None
async def next_view_id(database: Database) -> str:
pool = database.pool # type: ignore[attr-defined]
async with pool.acquire() as conn:
row = await conn.fetchrow(
"""
INSERT INTO vsphere_pc_state (kind, key, payload, updated_at)
VALUES ('meta', 'view_seq', '{"n": 1}'::jsonb, now())
ON CONFLICT (kind, key) DO UPDATE SET
payload = jsonb_build_object(
'n', COALESCE((vsphere_pc_state.payload->>'n')::int, 0) + 1
),
updated_at = now()
RETURNING payload
"""
)
n = int((_decode(row["payload"]) or {}).get("n") or 1)
return f"view-{n}"
async def register_container_view(database: Database, view_id: str, moids: list[str]) -> None:
payload = {"moids": list(moids)}
await _pc_put(database, "view", view_id, payload)
await inventory.upsert_object(
database,
moid=view_id,
type_name="ContainerView",
name=view_id,
parent_moid=None,
props={"view_moids": list(moids)},
)
def view_moids_from_object(obj: ManagedObject) -> list[str]:
props = obj.props or {}
return [str(m) for m in (props.get("view_moids") or [])]
async def store_page_token(
database: Database,
remaining_moids: list[str],
path_sets: list[str] | None = None,
) -> str:
token = f"token-{secrets.token_hex(8)}"
await _pc_put(
database,
"token",
token,
{"moids": list(remaining_moids), "path_sets": list(path_sets or [])},
)
return token
async def take_page_token(database: Database, token: str) -> list[str] | None:
payload = await _pc_pop(database, "token", token)
if payload is None:
return None
if isinstance(payload, list):
return payload
return list(payload.get("moids") or [])
async def take_page_token_full(
database: Database, token: str
) -> tuple[list[str], list[str]] | None:
payload = await _pc_pop(database, "token", token)
if payload is None:
return None
if isinstance(payload, list):
return payload, []
return list(payload.get("moids") or []), list(payload.get("path_sets") or [])
def _xsi(type_name: str) -> str:
return f'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="{escape(type_name)}"'
def _prop(name: str, value: str, *, xsi_type: str | None = None) -> str:
# govmomi SOAP AnyType decode requires xsi:type; bare <val> leaves Name empty → Ancestors panic.
type_name = xsi_type or "xsd:string"
return (
f"<propSet><name>{escape(name)}</name>"
f"<val {_xsi(type_name)}>{escape(value)}</val></propSet>"
)
def _prop_bool(name: str, value: bool) -> str:
return _prop(name, "true" if value else "false", xsi_type="xsd:boolean")
def _prop_int(name: str, value: int) -> str:
return _prop(name, str(value), xsi_type="xsd:int")
def _prop_long(name: str, value: int) -> str:
return _prop(name, str(value), xsi_type="xsd:long")
def _prop_raw(name: str, inner_xml: str, *, xsi_type: str) -> str:
return f"<propSet><name>{escape(name)}</name><val {_xsi(xsi_type)}>{inner_xml}</val></propSet>"
def _prop_mor(name: str, type_name: str, moid: str) -> str:
return (
f"<propSet><name>{escape(name)}</name>"
f'<val {_xsi("ManagedObjectReference")} type="{escape(type_name)}">'
f"{escape(moid)}</val></propSet>"
)
def _prop_array_mor(name: str, items: list[tuple[str, str]]) -> str:
if not items:
return (
f"<propSet><name>{escape(name)}</name>"
f"<val {_xsi('ArrayOfManagedObjectReference')}></val></propSet>"
)
inner = "".join(
f'<ManagedObjectReference type="{escape(t)}">{escape(m)}</ManagedObjectReference>'
for t, m in items
)
return (
f"<propSet><name>{escape(name)}</name>"
f"<val {_xsi('ArrayOfManagedObjectReference')}>{inner}</val></propSet>"
)
def _vim_power(state: str) -> str:
return {
"POWERED_ON": "poweredOn",
"POWERED_OFF": "poweredOff",
"SUSPENDED": "suspended",
}.get(state, "poweredOff")
def _parent_type(moid: str) -> str:
if moid.startswith("group-"):
return "Folder"
if moid.startswith("datacenter-"):
return "Datacenter"
if moid.startswith("domain-"):
return "ClusterComputeResource"
if moid.startswith("host-"):
return "HostSystem"
if moid.startswith("resgroup-"):
return "ResourcePool"
if moid.startswith("network-"):
return "Network"
if moid.startswith("dvportgroup-"):
return "DistributedVirtualPortgroup"
if moid.startswith("dvs-"):
return "VmwareDistributedVirtualSwitch"
if moid.startswith("datastore-"):
return "Datastore"
return "ManagedEntity"
def _tools_status(props: dict[str, Any]) -> str:
raw = str(props.get("tools_status") or "")
mapping = {
"GUEST_TOOLS_RUNNING": "toolsOk",
"GUEST_TOOLS_NOT_RUNNING": "toolsNotRunning",
"GUEST_TOOLS_OLD": "toolsOld",
}
return mapping.get(
raw, "toolsOk" if props.get("power_state") == "POWERED_ON" else "toolsNotRunning"
)
def _guest_id(props: dict[str, Any]) -> str:
guest = str(props.get("guest_OS") or "OTHER_GUEST_64")
# REST enums often use UBUNTU_64_GUEST; SOAP guestId uses ubuntu64Guest.
mapping = {
"UBUNTU_64_GUEST": "ubuntu64Guest",
"CENTOS_64_GUEST": "centos64Guest",
"RHEL_8_64_GUEST": "rhel8_64Guest",
"WINDOWS_2019_64_GUEST": "windows2019srv_64Guest",
"OTHER_GUEST_64": "otherGuest64",
"OTHER_GUEST": "otherGuest",
}
if guest in mapping:
return mapping[guest]
if guest.endswith("_GUEST"):
return guest.lower().replace("_guest", "Guest").replace("_64", "64")
return guest
def _instance_uuid(obj: ManagedObject) -> str:
identity = obj.props.get("identity") or {}
if identity.get("instance_uuid"):
return str(identity["instance_uuid"])
# Stable synthetic UUID from moid digits.
digits = "".join(ch for ch in obj.moid if ch.isdigit()) or "0"
n = int(digits) % 10_000_000_000_000
return f"5029aaaa-bbbb-cccc-dddd-{n:012d}"
def _bios_uuid(obj: ManagedObject) -> str:
identity = obj.props.get("identity") or {}
if identity.get("bios_uuid"):
return str(identity["bios_uuid"])
digits = "".join(ch for ch in obj.moid if ch.isdigit()) or "0"
n = int(digits) % 10_000_000_000_000
return f"4200aaaa-bbbb-cccc-dddd-{n:012d}"
def _folder_child_types(obj: ManagedObject, children: list[ManagedObject]) -> list[str]:
"""Declare Folder.childType the way vCenter does (not ManagedEntity)."""
from_children = sorted({c.type for c in children})
name = (obj.name or "").lower()
moid = obj.moid or ""
if name in {"datacenters", "datacenter"} or moid.startswith("group-d"):
return ["Folder", "Datacenter"]
if name == "vm" or moid.startswith("group-v"):
return ["Folder", "VirtualMachine", "VirtualApp"]
if name == "host" or moid.startswith("group-h"):
return ["Folder", "ComputeResource", "ClusterComputeResource", "HostSystem"]
if name in {"datastore", "datastores"} or moid.startswith("group-s"):
return ["Folder", "Datastore", "StoragePod"]
if name == "network" or moid.startswith("group-n"):
return [
"Folder",
"Network",
"DistributedVirtualPortgroup",
"VmwareDistributedVirtualSwitch",
"DistributedVirtualSwitch",
]
if from_children:
return ["Folder", *from_children]
return ["Folder", "VirtualMachine"]
def _vm_devices_xml(props: dict[str, Any]) -> str:
"""Minimal ArrayOfVirtualDevice suitable for govmomi / Terraform device reads."""
xsi = 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
datastore = str(props.get("datastore") or "datastore-31")
devices: list[str] = []
# PCI root + SCSI — Terraform device sorter requires controller assignment.
devices.append(
f'<VirtualDevice {xsi} xsi:type="VirtualPCIController">'
"<key>100</key><deviceInfo><label>PCI controller 0</label>"
"<summary>PCI controller 0</summary></deviceInfo>"
"<busNumber>0</busNumber></VirtualDevice>"
)
devices.append(
f'<VirtualDevice {xsi} xsi:type="VirtualLsiLogicController">'
"<key>1000</key><deviceInfo><label>SCSI controller 0</label>"
"<summary>LSI Logic</summary></deviceInfo>"
"<controllerKey>100</controllerKey><unitNumber>3</unitNumber>"
"<busNumber>0</busNumber><sharedBus>noSharing</sharedBus>"
"<scsiCtlrUnitNumber>7</scsiCtlrUnitNumber>"
"</VirtualDevice>"
)
devices.append(
f'<VirtualDevice {xsi} xsi:type="VirtualIDEController">'
"<key>200</key><deviceInfo><label>IDE 0</label><summary>IDE 0</summary></deviceInfo>"
"<busNumber>0</busNumber></VirtualDevice>"
)
# Terraform/pulumi-vsphere getVirtualMachine requires a video card device.
devices.append(
f'<VirtualDevice {xsi} xsi:type="VirtualMachineVideoCard">'
"<key>500</key><deviceInfo><label>Video card</label>"
"<summary>Video card</summary></deviceInfo>"
"<controllerKey>100</controllerKey><unitNumber>0</unitNumber>"
"<videoRamSizeInKB>4096</videoRamSizeInKB>"
"<numDisplays>1</numDisplays><useAutoDetect>false</useAutoDetect>"
"<enable3DSupport>false</enable3DSupport>"
"</VirtualDevice>"
)
disks = list(props.get("disks") or [])
if not disks:
disks = [{"key": "2000", "value": {"label": "Hard disk 1", "capacity": 42949672960}}]
for disk in disks:
key = int(str(disk.get("key") or 2000))
value = disk.get("value") or disk
capacity = int(value.get("capacity") or 42949672960)
capacity_kb = max(capacity // 1024, 1)
label = escape(str(value.get("label") or f"Hard disk {key}"))
uuid = escape(str(value.get("uuid") or f"6000C29{key:08x}-0000-0000-0000-000000000001"))
file_name = escape(str(value.get("file_name") or f"[datastore1] disk-{key}.vmdk"))
devices.append(
f'<VirtualDevice {xsi} xsi:type="VirtualDisk">'
f"<key>{key}</key><deviceInfo><label>{label}</label>"
f"<summary>{capacity_kb} KB</summary></deviceInfo>"
f"<controllerKey>1000</controllerKey><unitNumber>{max(key - 2000, 0)}</unitNumber>"
f"<capacityInKB>{capacity_kb}</capacityInKB>"
f"<capacityInBytes>{capacity}</capacityInBytes>"
f'<backing {xsi} xsi:type="VirtualDiskFlatVer2BackingInfo">'
f"<fileName>{file_name}</fileName>"
f'<datastore type="Datastore">{escape(datastore)}</datastore>'
f"<diskMode>persistent</diskMode>"
f"<thinProvisioned>true</thinProvisioned>"
f"<eagerlyScrub>false</eagerlyScrub>"
f"<writeThrough>false</writeThrough>"
f"<uuid>{uuid}</uuid>"
f"</backing></VirtualDevice>"
)
nics = list(props.get("nics") or [])
if not nics:
nics = [
{
"key": "4000",
"value": {
"label": "Network adapter 1",
"mac": "00:50:56:01:00:01",
"state": "CONNECTED",
"backing": {"network": "network-41"},
},
}
]
for nic in nics:
key = int(str(nic.get("key") or 4000))
value = nic.get("value") or nic
backing = value.get("backing") or {}
network = str(backing.get("network") or "network-41")
mac = escape(str(value.get("mac") or "00:50:56:00:00:01"))
label = escape(str(value.get("label") or f"Network adapter {key}"))
connected = "true" if str(value.get("state") or "").upper() == "CONNECTED" else "false"
devices.append(
f'<VirtualDevice {xsi} xsi:type="VirtualVmxnet3">'
f"<key>{key}</key><deviceInfo><label>{label}</label>"
f"<summary>VMXNET3</summary></deviceInfo>"
f"<controllerKey>100</controllerKey><unitNumber>7</unitNumber>"
f"<macAddress>{mac}</macAddress>"
f"<addressType>assigned</addressType>"
f"<connectable><connected>{connected}</connected>"
f"<startConnected>true</startConnected><allowGuestControl>true</allowGuestControl>"
f"</connectable>"
"<resourceAllocation><reservation>0</reservation><limit>-1</limit>"
"<share><shares>50</shares><level>normal</level></share>"
"</resourceAllocation>"
f'<backing {xsi} xsi:type="VirtualEthernetCardNetworkBackingInfo">'
f"<deviceName>VM Network</deviceName>"
f'<network type="Network">{escape(network)}</network>'
f"</backing></VirtualDevice>"
)
for cdrom in props.get("cdroms") or []:
key = int(str(cdrom.get("cdrom") or cdrom.get("key") or 3000))
label = escape(str(cdrom.get("label") or "CD/DVD drive 1"))
devices.append(
f'<VirtualDevice {xsi} xsi:type="VirtualCdrom">'
f"<key>{key}</key><deviceInfo><label>{label}</label>"
f"<summary>CD/DVD</summary></deviceInfo>"
f"<controllerKey>200</controllerKey><unitNumber>0</unitNumber>"
f'<backing {xsi} xsi:type="VirtualCdromIsoBackingInfo">'
f"<fileName>[datastore1] ISO/ubuntu.iso</fileName>"
f"</backing></VirtualDevice>"
)
return "".join(devices)
def build_prop_map(
obj: ManagedObject,
*,
children: list[ManagedObject],
all_by_moid: dict[str, ManagedObject],
) -> dict[str, str]:
"""Map property path -> already-escaped XML fragment for <propSet>…."""
props = obj.props
out: dict[str, str] = {
"name": _prop("name", obj.name),
"overallStatus": _prop("overallStatus", "green"),
}
if obj.parent_moid:
out["parent"] = _prop_mor("parent", _parent_type(obj.parent_moid), obj.parent_moid)
child_items = [(c.type, c.moid) for c in children]
if obj.type == "Folder":
out["childEntity"] = _prop_array_mor("childEntity", child_items)
# Folder.childType is ArrayOfString (govmomi folder helpers reject scalar "ManagedEntity").
child_types = _folder_child_types(obj, children)
out["childType"] = _prop_raw(
"childType",
"".join(f"<string>{escape(t)}</string>" for t in child_types),
xsi_type="ArrayOfString",
)
if obj.type == "Datacenter":
out["hostFolder"] = _prop_mor(
"hostFolder", "Folder", str(props.get("host_folder") or "group-h23")
)
out["vmFolder"] = _prop_mor(
"vmFolder", "Folder", str(props.get("vm_folder") or "group-v23")
)
out["datastoreFolder"] = _prop_mor(
"datastoreFolder", "Folder", str(props.get("datastore_folder") or "group-s23")
)
out["networkFolder"] = _prop_mor(
"networkFolder", "Folder", str(props.get("network_folder") or "group-n23")
)
datastores = [("Datastore", o.moid) for o in all_by_moid.values() if o.type == "Datastore"]
networks = [
(o.type, o.moid)
for o in all_by_moid.values()
if o.type in {"Network", "DistributedVirtualPortgroup"}
]
out["datastore"] = _prop_array_mor("datastore", datastores)
out["network"] = _prop_array_mor("network", networks)
if obj.type == "ClusterComputeResource":
hosts = [
("HostSystem", c.moid)
for c in all_by_moid.values()
if c.type == "HostSystem" and c.parent_moid == obj.moid
]
out["host"] = _prop_array_mor("host", hosts)
out["resourcePool"] = _prop_mor(
"resourcePool", "ResourcePool", str(props.get("resource_pool") or "resgroup-22")
)
# Terraform CreateVM loads default devices via EnvironmentBrowser.
env_browser = str(props.get("environment_browser") or f"envbrowser-{obj.moid}")
out["environmentBrowser"] = _prop_mor(
"environmentBrowser", "EnvironmentBrowser", env_browser
)
out["datastore"] = _prop_array_mor(
"datastore",
[("Datastore", o.moid) for o in all_by_moid.values() if o.type == "Datastore"][:8],
)
out["network"] = _prop_array_mor(
"network",
[
(o.type, o.moid)
for o in all_by_moid.values()
if o.type in {"Network", "DistributedVirtualPortgroup"}
][:20],
)
out["name"] = _prop("name", obj.name)
out["summary.effectiveCpu"] = _prop_int("summary.effectiveCpu", 48000)
out["summary.effectiveMemory"] = _prop_long("summary.effectiveMemory", 256 * 1024)
out["summary.numHosts"] = _prop_int("summary.numHosts", len(hosts))
out["configuration.drsConfig.enabled"] = _prop_bool("configuration.drsConfig.enabled", True)
out["configuration.dasConfig.enabled"] = _prop_bool(
"configuration.dasConfig.enabled", False
)
if obj.type == "ResourcePool":
out["owner"] = _prop_mor("owner", "ClusterComputeResource", obj.parent_moid or "domain-c21")
vms = [
("VirtualMachine", o.moid)
for o in all_by_moid.values()
if o.type == "VirtualMachine" and str(o.props.get("resource_pool") or "") == obj.moid
]
if not vms:
vms = [
("VirtualMachine", o.moid)
for o in all_by_moid.values()
if o.type == "VirtualMachine"
and o.parent_moid
and "group-v" in (o.parent_moid or "")
][:50]
out["vm"] = _prop_array_mor("vm", vms)
out["resourcePool"] = _prop_array_mor("resourcePool", [])
out["config.cpuAllocation.limit"] = _prop_long("config.cpuAllocation.limit", -1)
out["config.memoryAllocation.limit"] = _prop_long("config.memoryAllocation.limit", -1)
if obj.type == "VirtualMachine":
power = _vim_power(str(props.get("power_state", "POWERED_OFF")))
cpu = int(props.get("cpu_count") or 1)
mem = int(props.get("memory_size_mib") or 1024)
guest_id = _guest_id(props)
inst_uuid = _instance_uuid(obj)
bios_uuid = _bios_uuid(obj)
host = str(props.get("host") or "")
datastore = str(props.get("datastore") or "datastore-31")
ds_obj = all_by_moid.get(datastore)
datastore_name = ds_obj.name if ds_obj is not None else "datastore1"
pool = str(props.get("resource_pool") or "resgroup-22")
networks = [str(n) for n in (props.get("networks") or ["network-41"])]
guest_ip = str(props.get("guest_ip") or props.get("ip_address") or "")
hostname = str((props.get("identity") or {}).get("name") or obj.name)
template = bool(props.get("template"))
tools = _tools_status(props)
out["runtime.powerState"] = _prop("runtime.powerState", power)
out["summary.runtime.powerState"] = _prop("summary.runtime.powerState", power)
out["config.hardware.numCPU"] = _prop_int("config.hardware.numCPU", cpu)
out["config.hardware.memoryMB"] = _prop_int("config.hardware.memoryMB", mem)
out["config.hardware.numCoresPerSocket"] = _prop_int("config.hardware.numCoresPerSocket", 1)
out["config.uuid"] = _prop("config.uuid", bios_uuid)
out["config.instanceUuid"] = _prop("config.instanceUuid", inst_uuid)
out["config.guestId"] = _prop("config.guestId", guest_id)
out["config.guestFullName"] = _prop(
"config.guestFullName", str(props.get("guest_OS") or guest_id)
)
out["config.name"] = _prop("config.name", obj.name)
out["config.version"] = _prop(
"config.version", str(props.get("hardware_version") or "vmx-19").lower()
)
out["config.template"] = _prop_bool("config.template", template)
out["config.files.vmPathName"] = _prop(
"config.files.vmPathName", f"[{datastore_name}] {obj.name}/{obj.name}.vmx"
)
out["config.hardware.device"] = _prop_raw(
"config.hardware.device", _vm_devices_xml(props), xsi_type="ArrayOfVirtualDevice"
)
# Terraform flattenVirtualMachineConfigInfo requires non-nil Tools / allocations / bootOptions.
out["config.tools"] = _prop_raw(
"config.tools",
"<toolsUpgradePolicy>manual</toolsUpgradePolicy>"
"<afterPowerOn>true</afterPowerOn>"
"<afterResume>true</afterResume>"
"<beforeGuestStandby>true</beforeGuestStandby>"
"<beforeGuestShutdown>true</beforeGuestShutdown>"
"<beforeGuestReboot>true</beforeGuestReboot>"
"<syncTimeWithHost>false</syncTimeWithHost>"
"<syncTimeWithHostAllowed>true</syncTimeWithHostAllowed>",
xsi_type="ToolsConfigInfo",
)
out["config.firmware"] = _prop("config.firmware", str(props.get("firmware") or "bios"))
out["config.changeVersion"] = _prop("config.changeVersion", "1")
out["config.annotation"] = _prop("config.annotation", str(props.get("annotation") or ""))
out["config.alternateGuestName"] = _prop("config.alternateGuestName", "")
out["config.memoryHotAddEnabled"] = _prop_bool("config.memoryHotAddEnabled", False)
out["config.cpuHotAddEnabled"] = _prop_bool("config.cpuHotAddEnabled", False)
out["config.cpuHotRemoveEnabled"] = _prop_bool("config.cpuHotRemoveEnabled", False)
out["config.memoryReservationLockedToMax"] = _prop_bool(
"config.memoryReservationLockedToMax", False
)
out["config.nestedHVEnabled"] = _prop_bool("config.nestedHVEnabled", False)
out["config.vPMCEnabled"] = _prop_bool("config.vPMCEnabled", False)
out["config.swapPlacement"] = _prop("config.swapPlacement", "inherit")
out["config.flags"] = _prop_raw(
"config.flags",
"<enableLogging>true</enableLogging>"
"<diskUuidEnabled>false</diskUuidEnabled>"
"<virtualMmuUsage>automatic</virtualMmuUsage>"
"<virtualExecUsage>hvAuto</virtualExecUsage>"
"<snapshotLocked>false</snapshotLocked>"
"<monitorType>release</monitorType>"
"<vbsEnabled>false</vbsEnabled>"
"<vvtdEnabled>false</vvtdEnabled>",
xsi_type="VirtualMachineFlagInfo",
)
alloc = (
"<reservation>0</reservation><limit>-1</limit>"
"<shares><shares>1000</shares><level>normal</level></shares>"
)
out["config.cpuAllocation"] = _prop_raw(
"config.cpuAllocation", alloc, xsi_type="ResourceAllocationInfo"
)
out["config.memoryAllocation"] = _prop_raw(
"config.memoryAllocation",
"<reservation>0</reservation><limit>-1</limit>"
"<shares><shares>20480</shares><level>normal</level></shares>",
xsi_type="ResourceAllocationInfo",
)
out["config.bootOptions"] = _prop_raw(
"config.bootOptions",
"<bootDelay>0</bootDelay>"
"<enterBIOSSetup>false</enterBIOSSetup>"
"<bootRetryEnabled>false</bootRetryEnabled>"
"<bootRetryDelay>10000</bootRetryDelay>"
"<efiSecureBootEnabled>false</efiSecureBootEnabled>",
xsi_type="VirtualMachineBootOptions",
)
out["summary.config.numCpu"] = _prop_int("summary.config.numCpu", cpu)
out["summary.config.memorySizeMB"] = _prop_int("summary.config.memorySizeMB", mem)
out["summary.config.name"] = _prop("summary.config.name", obj.name)
out["summary.config.uuid"] = _prop("summary.config.uuid", bios_uuid)
out["summary.config.instanceUuid"] = _prop("summary.config.instanceUuid", inst_uuid)
out["summary.config.guestId"] = _prop("summary.config.guestId", guest_id)
out["summary.guest.ipAddress"] = _prop("summary.guest.ipAddress", guest_ip)
out["summary.guest.hostName"] = _prop("summary.guest.hostName", hostname)
out["summary.guest.toolsStatus"] = _prop("summary.guest.toolsStatus", tools)
out["guest.ipAddress"] = _prop("guest.ipAddress", guest_ip)
out["guest.hostName"] = _prop("guest.hostName", hostname)
out["guest.toolsStatus"] = _prop("guest.toolsStatus", tools)
out["guest.guestId"] = _prop("guest.guestId", guest_id)
out["guest.guestState"] = _prop(
"guest.guestState", "running" if power == "poweredOn" else "notRunning"
)
if host:
out["runtime.host"] = _prop_mor("runtime.host", "HostSystem", host)
out["summary.runtime.host"] = _prop_mor("summary.runtime.host", "HostSystem", host)
out["resourcePool"] = _prop_mor("resourcePool", "ResourcePool", pool)
out["datastore"] = _prop_array_mor("datastore", [("Datastore", datastore)])
net_items: list[tuple[str, str]] = []
for net_id in networks:
net_obj = all_by_moid.get(net_id)
net_items.append((net_obj.type if net_obj else "Network", net_id))
out["network"] = _prop_array_mor("network", net_items)
out["layoutEx.file"] = _prop_raw(
"layoutEx.file",
f"<VirtualMachineFileLayoutExFileInfo><key>0</key>"
f"<name>[{escape(datastore_name)}] {escape(obj.name)}/{escape(obj.name)}.vmx</name>"
f"<type>config</type><size>4096</size></VirtualMachineFileLayoutExFileInfo>",
xsi_type="ArrayOfVirtualMachineFileLayoutExFileInfo",
)
if obj.type == "HostSystem":
cpu_mhz = int(props.get("cpu_mhz") or 2400)
cpu_cores = int(props.get("cpu_cores") or props.get("num_cpu_cores") or 16)
mem_bytes = int(props.get("memory_bytes") or props.get("memory_size") or 137438953472)
out["runtime.connectionState"] = _prop("runtime.connectionState", "connected")
out["runtime.powerState"] = _prop("runtime.powerState", "poweredOn")
out["runtime.inMaintenanceMode"] = _prop_bool(
"runtime.inMaintenanceMode", bool(props.get("maintenance_mode"))
)
out["summary.config.name"] = _prop("summary.config.name", obj.name)
out["summary.hardware.uuid"] = _prop(
"summary.hardware.uuid", str(props.get("uuid") or f"host-uuid-{obj.moid}")
)
out["summary.hardware.memorySize"] = _prop_long("summary.hardware.memorySize", mem_bytes)
out["summary.hardware.numCpuCores"] = _prop_int("summary.hardware.numCpuCores", cpu_cores)
out["summary.hardware.cpuMhz"] = _prop_int("summary.hardware.cpuMhz", cpu_mhz)
out["hardware.memorySize"] = _prop_long("hardware.memorySize", mem_bytes)
out["hardware.cpuInfo.numCpuCores"] = _prop_int("hardware.cpuInfo.numCpuCores", cpu_cores)
out["hardware.cpuInfo.hz"] = _prop_long("hardware.cpuInfo.hz", cpu_mhz * 1_000_000)
out["config.product.version"] = _prop(
"config.product.version", str(props.get("version") or "8.0.0")
)
out["config.product.fullName"] = _prop(
"config.product.fullName", f"VMware ESXi {props.get('version') or '8.0.0'}"
)
ds_ids = (
props.get("datastores")
or [o.moid for o in all_by_moid.values() if o.type == "Datastore"][:4]
)
out["datastore"] = _prop_array_mor("datastore", [("Datastore", str(d)) for d in ds_ids])
net_ids = (
props.get("networks")
or [o.moid for o in all_by_moid.values() if o.type == "Network"][:4]
)
out["network"] = _prop_array_mor("network", [("Network", str(n)) for n in net_ids])
vms = [
("VirtualMachine", o.moid)
for o in all_by_moid.values()
if o.type == "VirtualMachine" and str(o.props.get("host") or "") == obj.moid
]
out["vm"] = _prop_array_mor("vm", vms)
out["environmentBrowser"] = _prop_mor(
"environmentBrowser",
"EnvironmentBrowser",
str(props.get("environment_browser") or f"envbrowser-{obj.moid}"),
)
if obj.type == "EnvironmentBrowser" or obj.moid.startswith("envbrowser-"):
out["name"] = _prop("name", obj.name)
if obj.type == "Datastore":
capacity = int(props.get("capacity") or 0)
free = int(props.get("free_space") or 0)
out["summary.capacity"] = _prop_long("summary.capacity", capacity)
out["summary.freeSpace"] = _prop_long("summary.freeSpace", free)
out["summary.type"] = _prop("summary.type", str(props.get("type") or "VMFS"))
out["summary.name"] = _prop("summary.name", obj.name)
out["summary.url"] = _prop("summary.url", f"ds:///vmfs/volumes/{obj.moid}/")
out["summary.accessible"] = _prop_bool("summary.accessible", True)
out["summary.multipleHostAccess"] = _prop_bool("summary.multipleHostAccess", True)
out["info.name"] = _prop("info.name", obj.name)
out["info.url"] = _prop("info.url", f"ds:///vmfs/volumes/{obj.moid}/")
out["info.freeSpace"] = _prop_long("info.freeSpace", free)
out["info.maxFileSize"] = _prop_long("info.maxFileSize", 62 * 1024**4)
hosts = [o.moid for o in all_by_moid.values() if o.type == "HostSystem"][:20]
# Datastore.host is ArrayOfDatastoreHostMount — not ArrayOfManagedObjectReference.
host_mounts = "".join(
"<DatastoreHostMount>"
f'<key type="HostSystem">{escape(hid)}</key>'
"<mountInfo>"
f"<path>/vmfs/volumes/{escape(obj.moid)}</path>"
"<accessMode>readWrite</accessMode>"
"<mounted>true</mounted>"
"<accessible>true</accessible>"
"</mountInfo>"
"</DatastoreHostMount>"
for hid in hosts
)
out["host"] = _prop_raw("host", host_mounts, xsi_type="ArrayOfDatastoreHostMount")
vms = [
("VirtualMachine", o.moid)
for o in all_by_moid.values()
if o.type == "VirtualMachine" and str(o.props.get("datastore") or "") == obj.moid
][:100]
out["vm"] = _prop_array_mor("vm", vms)
if obj.type in {"Network", "DistributedVirtualPortgroup", "VmwareDistributedVirtualSwitch"}:
out["summary.name"] = _prop("summary.name", obj.name)
out["summary.accessible"] = _prop_bool("summary.accessible", True)
hosts = [("HostSystem", o.moid) for o in all_by_moid.values() if o.type == "HostSystem"][
:20
]
out["host"] = _prop_array_mor("host", hosts)
vms = [
("VirtualMachine", o.moid)
for o in all_by_moid.values()
if o.type == "VirtualMachine"
and (
obj.moid in (o.props.get("networks") or [])
or obj.name in (o.props.get("networks") or [])
)
][:100]
out["vm"] = _prop_array_mor("vm", vms)
if obj.type == "DistributedVirtualPortgroup":
key = str(props.get("key") or obj.moid)
out["key"] = _prop("key", key)
out["config.name"] = _prop("config.name", obj.name)
out["config.key"] = _prop("config.key", key)
dvs = str(props.get("dvs") or props.get("distributed_switch") or "dvs-41")
out["config.distributedVirtualSwitch"] = _prop_mor(
"config.distributedVirtualSwitch", "VmwareDistributedVirtualSwitch", dvs
)
if obj.type == "VmwareDistributedVirtualSwitch":
out["uuid"] = _prop("uuid", str(props.get("uuid") or f"dvs-uuid-{obj.moid}"))
out["summary.name"] = _prop("summary.name", obj.name)
out["config.uuid"] = _prop(
"config.uuid", str(props.get("uuid") or f"dvs-uuid-{obj.moid}")
)
pgs = [
("DistributedVirtualPortgroup", o.moid)
for o in all_by_moid.values()
if o.type == "DistributedVirtualPortgroup"
]
out["portgroup"] = _prop_array_mor("portgroup", pgs)
if obj.type == "ContainerView" or obj.moid.startswith("view-"):
moids = view_moids_from_object(obj)
items = []
for moid in moids:
child = all_by_moid.get(moid)
if child:
items.append((child.type, child.moid))
out["view"] = _prop_array_mor("view", items)
if obj.type == "Task":
state = str(props.get("state") or "success")
progress = 100 if state == "success" else 50
xsi = 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
result_xml = ""
entity_xml = ""
if props.get("result_moid"):
rtype = str(props.get("result_type") or "VirtualMachine")
rmoid = escape(str(props["result_moid"]))
result_xml = (
f'<result {xsi} xsi:type="ManagedObjectReference" type="{escape(rtype)}">'
f"{rmoid}</result>"
)
entity_xml = (
f'<entity {xsi} xsi:type="ManagedObjectReference" type="{escape(rtype)}">'
f"{rmoid}</entity><entityName>{escape(rtype)}</entityName>"
)
info_inner = (
f"<key>{escape(obj.moid)}</key>"
f'<task type="Task">{escape(obj.moid)}</task>'
f"<state>{escape(state)}</state>"
"<cancelled>false</cancelled><cancelable>false</cancelable>"
f"<descriptionId>{escape(str(props.get('operation') or 'task'))}</descriptionId>"
f"<progress>{progress}</progress>"
f"{entity_xml}{result_xml}"
)
out["info"] = _prop_raw("info", info_inner, xsi_type="TaskInfo")
out["info.state"] = _prop("info.state", state)
out["info.descriptionId"] = _prop(
"info.descriptionId", str(props.get("operation") or "task")
)
if props.get("entity"):
out["info.entity"] = _prop_mor("info.entity", "VirtualMachine", str(props["entity"]))
out["info.progress"] = _prop_int("info.progress", progress)
if props.get("result_moid"):
out["info.result"] = _prop_mor(
"info.result",
str(props.get("result_type") or "VirtualMachine"),
str(props["result_moid"]),
)
if obj.type == "HttpNfcLease" or obj.moid.startswith("lease-"):
# Lease document is durable in vsphere_objects.props (+ vsphere_nfc_leases).
lease = props if isinstance(props, dict) else {}
state = str(lease.get("state") or "ready")
out["state"] = _prop("state", state)
out["info.state"] = _prop("info.state", state)
out["info.entity"] = _prop_mor(
"info.entity", "VirtualMachine", str(lease.get("entity") or "vm-101")
)
out["info.initializeProgress"] = _prop_int(
"info.initializeProgress", int(lease.get("initializeProgress") or 100)
)
out["info.transferProgress"] = _prop_int(
"info.transferProgress", int(lease.get("transferProgress") or 0)
)
urls = (lease.get("info") or {}).get("deviceUrl") or []
url_xml = "".join(
f"<HttpNfcLeaseDeviceUrl><key>{escape(str(u.get('key')))}</key>"
f"<importKey>{escape(str(u.get('importKey')))}</importKey>"
f"<url>{escape(str(u.get('url')))}</url>"
f"<sslThumbprint>{escape(str(u.get('sslThumbprint') or ''))}</sslThumbprint>"
f"</HttpNfcLeaseDeviceUrl>"
for u in urls
)
out["info.deviceUrl"] = _prop_raw(
"info.deviceUrl", url_xml, xsi_type="ArrayOfHttpNfcLeaseDeviceUrl"
)
return out
def object_content_xml(
obj: ManagedObject,
*,
children: list[ManagedObject],
all_by_moid: dict[str, ManagedObject],
path_sets: list[str] | None = None,
) -> str:
if obj.props.get("_not_found"):
xsi = 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
# Shape must decode to types.ManagedObjectNotFound so terraform
# viapi.IsManagedObjectNotFoundError is true (CreateVM vs CreateChildVM).
# MissingProperty.fault is the MethodFault itself (no nested <fault>).
path = (path_sets[0] if path_sets else None) or "name"
return (
f'<returnval><obj type="{escape(obj.type)}">{escape(obj.moid)}</obj>'
f"<missingSet><path>{escape(path)}</path>"
f'<fault {xsi} xsi:type="ManagedObjectNotFound">'
f'<obj type="{escape(obj.type)}">{escape(obj.moid)}</obj>'
f"</fault>"
f"</missingSet></returnval>"
)
prop_map = build_prop_map(obj, children=children, all_by_moid=all_by_moid)
if path_sets:
selected = []
for path in path_sets:
if path in prop_map:
selected.append(prop_map[path])
else:
# prefix match e.g. runtime / config
for key, frag in prop_map.items():
if key == path or key.startswith(path + "."):
selected.append(frag)
prop_xml = selected or list(prop_map.values())
else:
prop_xml = list(prop_map.values())
return (
f'<returnval><obj type="{escape(obj.type)}">{escape(obj.moid)}</obj>'
+ "".join(prop_xml)
+ "</returnval>"
)
def resolve_inventory_path(path: str, objects: list[ManagedObject]) -> ManagedObject | None:
"""Resolve inventory paths (govmomi omits root ``Datacenters`` folder)."""
parts = [p for p in path.strip("/").split("/") if p]
if not parts:
return None
by_moid = {o.moid: o for o in objects}
def _walk(start_parent: str | None) -> ManagedObject | None:
current_parent = start_parent
match: ManagedObject | None = None
for part in parts:
found = None
if current_parent:
found = find_child(current_parent, part, objects)
if found is None:
found = next(
(o for o in objects if o.parent_moid == current_parent and o.name == part),
None,
)
if found is None and current_parent is None:
hits = [o for o in objects if o.name == part]
if len(hits) == 1:
found = hits[0]
# Standard DC folder names even if the Folder.name was renamed in lab.
if found is None and current_parent and current_parent in by_moid:
parent = by_moid[current_parent]
if parent.type == "Datacenter":
key = {
"vm": "vm_folder",
"host": "host_folder",
"datastore": "datastore_folder",
"network": "network_folder",
}.get(part.lower())
if key and parent.props.get(key):
found = by_moid.get(str(parent.props[key]))
if found is None:
return None
match = found
current_parent = found.moid
return match
hit = _walk(None)
if hit is not None:
return hit
for root in (o for o in objects if o.parent_moid is None):
hit = _walk(root.moid)
if hit is not None:
return hit
return None
def find_child(parent_moid: str, name: str, objects: list[ManagedObject]) -> ManagedObject | None:
"""SearchIndex.FindChild — direct child by name."""
hits = [o for o in objects if o.parent_moid == parent_moid and o.name == name]
if len(hits) == 1:
return hits[0]
# Datacenter folder props (vmFolder etc.) are not parent_moid edges for DC itself.
parent = next((o for o in objects if o.moid == parent_moid), None)
if parent and parent.type == "Datacenter":
for key in ("host_folder", "vm_folder", "datastore_folder", "network_folder"):
folder_moid = parent.props.get(key)
if not folder_moid:
continue
folder = next((o for o in objects if o.moid == folder_moid and o.name == name), None)
if folder:
return folder
if hits:
return hits[0]
return None
def _descendants(
roots: list[ManagedObject],
all_objects: list[ManagedObject],
*,
max_depth: int = 32,
) -> list[ManagedObject]:
"""BFS descendants by parent_moid (folder/datacenter/cluster inventory walk)."""
by_parent: dict[str | None, list[ManagedObject]] = {}
for obj in all_objects:
by_parent.setdefault(obj.parent_moid, []).append(obj)
seen: dict[str, ManagedObject] = {o.moid: o for o in roots}
frontier = list(roots)
depth = 0
while frontier and depth < max_depth:
nxt: list[ManagedObject] = []
for obj in frontier:
for child in by_parent.get(obj.moid, []):
if child.moid not in seen:
seen[child.moid] = child
nxt.append(child)
# Datacenter folders live as props, not parent_moid edges.
if obj.type == "Datacenter":
for key in ("host_folder", "vm_folder", "datastore_folder", "network_folder"):
folder_moid = obj.props.get(key)
if not folder_moid:
continue
folder = next((o for o in all_objects if o.moid == folder_moid), None)
if folder and folder.moid not in seen:
seen[folder.moid] = folder
nxt.append(folder)
if obj.type == "ClusterComputeResource":
rp = obj.props.get("resource_pool")
if rp:
pool = next((o for o in all_objects if o.moid == rp), None)
if pool and pool.moid not in seen:
seen[pool.moid] = pool
nxt.append(pool)
frontier = nxt
depth += 1
return list(seen.values())
async def select_objects_for_retrieve(
database: Any,
body: str,
) -> tuple[list[ManagedObject], list[str]]:
path_sets = parse_path_sets(body)
prop_types = parse_prop_types(body)
continue_token = parse_continue_token(body)
all_objects = await inventory.list_objects(database)
by_moid = {o.moid: o for o in all_objects}
if continue_token:
remaining = await take_page_token(database, continue_token)
if remaining is None:
return [], path_sets
selected = [by_moid[m] for m in remaining if m in by_moid]
return selected, path_sets
refs = parse_obj_refs(body)
selected: list[ManagedObject] = []
if refs:
for type_name, moid in refs:
if type_name == "ContainerView" or moid.startswith("view-"):
view_obj = by_moid.get(moid)
if view_obj is not None:
selected.append(view_obj)
else:
stored = await _pc_get(database, "view", moid)
moids = (
list((stored or {}).get("moids") or []) if isinstance(stored, dict) else []
)
selected.append(
ManagedObject(
moid=moid,
type="ContainerView",
name=moid,
parent_moid=None,
props={"view_moids": moids},
)
)
elif type_name == "Task" or moid.startswith("task-"):
selected.append(
ManagedObject(
moid=moid,
type="Task",
name=moid,
parent_moid=None,
props={"task": True},
)
)
elif type_name == "HttpNfcLease" or moid.startswith("lease-"):
selected.append(
ManagedObject(
moid=moid,
type="HttpNfcLease",
name=moid,
parent_moid=None,
props={"lease": True},
)
)
elif type_name == "EnvironmentBrowser" or moid.startswith("envbrowser-"):
selected.append(
ManagedObject(
moid=moid,
type="EnvironmentBrowser",
name=moid,
parent_moid=None,
props={"environment_browser": True},
)
)
elif moid in by_moid:
obj = by_moid[moid]
# Honor requested MOR type — VirtualApp:resgroup-X must NOT resolve a ResourcePool.
if type_name and not _type_compatible(type_name, obj.type):
# Return the underlying ResourcePool for VirtualApp probes so the
# provider proceeds to CreateChildVM_Task (which we implement) instead
# of failing/hanging on ManagedObjectNotFound fault decoding.
if type_name == "VirtualApp" and obj.type == "ResourcePool":
selected.append(obj)
continue
selected.append(
ManagedObject(
moid=moid,
type=type_name,
name=moid,
parent_moid=None,
props={"_not_found": True},
)
)
continue
selected.append(obj)
elif moid in {"propertyCollector", "TaskManager", "SearchIndex", "ViewManager"}:
continue
else:
pass
wants_traversal = "selectSet" in body or "TraversalSpec" in body
if wants_traversal and selected:
# Include ContainerView — govmomi view.Retrieve traverses Path=view from the view MOR.
inventory_roots = [o for o in selected if o.type != "Task"]
if inventory_roots:
if _wants_parent_traversal(body):
expanded = _ancestors(inventory_roots, all_objects)
elif _is_recursive_traversal(body):
expanded = _descendants(inventory_roots, all_objects)
else:
# govmomi list.Lister TraversalSpec is one hop (e.g. Folder.childEntity).
expanded = _one_level_traverse(inventory_roots, all_objects, body)
skip_root = _object_set_skip(body)
if prop_types:
type_set = set(prop_types)
filtered = [o for o in expanded if _type_matches(o.type, type_set)]
if not skip_root:
for root in inventory_roots:
if root.type == "ContainerView":
continue
if root.moid not in {o.moid for o in filtered}:
if _type_matches(root.type, type_set) or not type_set:
filtered.append(root)
selected = (
filtered if filtered else [o for o in expanded if o.type != "ContainerView"]
)
else:
selected = [o for o in expanded if o.type != "ContainerView" or not skip_root]
if not skip_root:
for root in inventory_roots:
if root.type == "ContainerView":
continue
if root.moid not in {o.moid for o in selected}:
selected.append(root)
else:
selected = all_objects
if prop_types:
type_set = set(prop_types)
selected = [o for o in selected if _type_matches(o.type, type_set)]
return selected, path_sets
def children_of(moid: str, all_objects: list[ManagedObject]) -> list[ManagedObject]:
return [o for o in all_objects if o.parent_moid == moid]
def _type_compatible(requested: str, actual: str) -> bool:
"""Whether an inventory object may satisfy a client MOR of ``requested`` type."""
if not requested or requested == actual:
return True
if requested == "ManagedEntity":
return actual in _MANAGED_ENTITY_TYPES
if requested == "ComputeResource" and actual in {"ComputeResource", "ClusterComputeResource"}:
return True
if requested == "ResourcePool" and actual in {"ResourcePool", "VirtualApp"}:
return True
# Plain ResourcePool must NOT satisfy VirtualApp (CreateChildVM vs CreateVM).
# Exception: none — keep strict. CreateChildVM path is handled separately.
if requested == "VirtualApp":
return actual == "VirtualApp"
if requested == "DistributedVirtualSwitch" and actual in {
"VmwareDistributedVirtualSwitch",
"DistributedVirtualSwitch",
}:
return True
if requested == "Network" and actual in {
"Network",
"DistributedVirtualPortgroup",
"OpaqueNetwork",
}:
return True
return False
def _type_matches(obj_type: str, type_set: set[str]) -> bool:
if obj_type in type_set:
return True
return any(_type_compatible(t, obj_type) for t in type_set)
def _traversal_paths(body: str) -> list[str]:
"""Paths from TraversalSpec (<path>), not PropertySpec (<pathSet>)."""
return re.findall(r"<(?:\w+:)?path(?:\s[^>]*)?>([^<]+)</(?:\w+:)?path>", body)
def _wants_parent_traversal(body: str) -> bool:
"""govmomi mo.Ancestors walks parent/parentVApp — not inventory descendants."""
paths = {p.strip() for p in _traversal_paths(body)}
if not paths:
return "traverseParent" in body
parentish = {"parent", "parentVApp"}
childish = {
"childEntity",
"hostFolder",
"vmFolder",
"datastoreFolder",
"networkFolder",
"host",
"resourcePool",
"vm",
"datastore",
"network",
"view",
"portgroup",
}
return bool(paths & parentish) and not bool(paths & childish)
def _is_recursive_traversal(body: str) -> bool:
"""True when a TraversalSpec nests a SelectionSpec (name-only selectSet) to recurse."""
if _wants_parent_traversal(body):
return True
# Nested SelectionSpec: <selectSet><name>…</name></selectSet> inside TraversalSpec.
# Do not match the TraversalSpec's own <name> sibling of <path>.
return bool(
re.search(
r'xsi:type="TraversalSpec"[^>]*>[\s\S]*?'
r"<(?:\w+:)?selectSet(?:\s[^>]*)?>\s*"
r"<(?:\w+:)?name(?:\s[^>]*)?>[^<]+</(?:\w+:)?name>\s*"
r"</(?:\w+:)?selectSet>",
body,
flags=re.IGNORECASE,
)
)
def _object_set_skip(body: str) -> bool:
"""objectSet/@skip — ListFolder uses skip=true so the folder itself is omitted."""
match = re.search(
r"<(?:\w+:)?objectSet\b[^>]*>.*?<(?:\w+:)?skip[^>]*>([^<]+)</(?:\w+:)?skip>",
body,
flags=re.DOTALL | re.IGNORECASE,
)
if not match:
return False
return match.group(1).strip().lower() == "true"
def _ancestors(roots: list[ManagedObject], all_objects: list[ManagedObject]) -> list[ManagedObject]:
by_moid = {o.moid: o for o in all_objects}
seen: dict[str, ManagedObject] = {}
for root in roots:
cur: ManagedObject | None = root
while cur is not None:
if cur.moid in seen:
break
seen[cur.moid] = cur
parent_moid = cur.parent_moid
if not parent_moid:
break
cur = by_moid.get(parent_moid)
return list(seen.values())
def _follow_path(
root: ManagedObject,
path: str,
all_objects: list[ManagedObject],
by_moid: dict[str, ManagedObject],
) -> list[ManagedObject]:
"""One PropertyCollector TraversalSpec hop (govmomi list.Lister)."""
if path == "childEntity":
return children_of(root.moid, all_objects)
if path in {"parent", "parentVApp"}:
if not root.parent_moid:
return []
parent = by_moid.get(root.parent_moid)
return [parent] if parent else []
folder_keys = {
"vmFolder": "vm_folder",
"hostFolder": "host_folder",
"datastoreFolder": "datastore_folder",
"networkFolder": "network_folder",
}
if path in folder_keys:
folder_moid = root.props.get(folder_keys[path])
if not folder_moid:
return []
folder = by_moid.get(str(folder_moid))
return [folder] if folder else []
if path == "resourcePool":
rp = root.props.get("resource_pool") or root.props.get("resourcePool")
if rp and str(rp) in by_moid:
return [by_moid[str(rp)]]
return [o for o in all_objects if o.type == "ResourcePool" and o.parent_moid == root.moid]
if path == "host":
return [o for o in all_objects if o.type == "HostSystem" and o.parent_moid == root.moid]
if path == "vm":
if root.type == "ResourcePool":
return [
o
for o in all_objects
if o.type == "VirtualMachine"
and str(o.props.get("resource_pool") or "") == root.moid
]
if root.type == "HostSystem":
return [
o
for o in all_objects
if o.type == "VirtualMachine" and str(o.props.get("host") or "") == root.moid
]
return children_of(root.moid, all_objects)
if path == "datastore":
if root.type == "Datacenter":
return [o for o in all_objects if o.type == "Datastore"]
ds_ids = root.props.get("datastores") or root.props.get("datastore") or []
if isinstance(ds_ids, str):
ds_ids = [ds_ids]
out = [by_moid[str(d)] for d in ds_ids if str(d) in by_moid]
if out:
return out
return [o for o in all_objects if o.type == "Datastore"][:20]
if path == "network":
return [
o
for o in all_objects
if o.type in {"Network", "DistributedVirtualPortgroup", "OpaqueNetwork"}
][:50]
if path == "portgroup":
return [o for o in all_objects if o.type == "DistributedVirtualPortgroup"]
if path == "view":
moids = view_moids_from_object(root)
return [by_moid[m] for m in moids if m in by_moid]
return []
def _one_level_traverse(
roots: list[ManagedObject],
all_objects: list[ManagedObject],
body: str,
) -> list[ManagedObject]:
paths = [p.strip() for p in _traversal_paths(body)] or ["childEntity"]
by_moid = {o.moid: o for o in all_objects}
seen: dict[str, ManagedObject] = {}
for root in roots:
for path in paths:
for hit in _follow_path(root, path, all_objects, by_moid):
seen[hit.moid] = hit
return list(seen.values())
def _vim_task_state(status: str) -> str:
return {
"SUCCEEDED": "success",
"FAILED": "error",
"RUNNING": "running",
"PENDING": "queued",
}.get(status, "error")
def _parse_max_wait_seconds(body: str) -> float:
match = re.search(
r"<(?:\w+:)?maxWaitSeconds[^>]*>([^<]*)</(?:\w+:)?maxWaitSeconds>",
body,
)
if not match:
return 0.0
try:
return max(0.0, float(match.group(1).strip() or "0"))
except ValueError:
return 0.0
def _task_object_set_xml(task: dict[str, Any]) -> str:
"""PropertyCollector update so govmomi ``task.Wait`` observes completion.
govmomi waits on the whole ``info`` property (TaskInfo), not ``info.state``.
"""
task_id = str(task.get("task") or "")
state = _vim_task_state(str(task.get("status") or "FAILED"))
progress = 100 if state in {"success", "error"} else int(task.get("progress") or 50)
result = task.get("result") if isinstance(task.get("result"), dict) else {}
vm_moid = str((result or {}).get("vm") or "")
xsi = 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
result_xml = ""
entity_xml = ""
if vm_moid:
result_xml = (
f'<result {xsi} xsi:type="ManagedObjectReference" type="VirtualMachine">'
f"{escape(vm_moid)}</result>"
)
entity_xml = (
f'<entity {xsi} xsi:type="ManagedObjectReference" type="VirtualMachine">'
f"{escape(vm_moid)}</entity>"
"<entityName>VirtualMachine</entityName>"
)
info_xml = (
f'<val {xsi} xsi:type="TaskInfo">'
f"<key>{escape(task_id)}</key>"
f'<task type="Task">{escape(task_id)}</task>'
f"<state>{escape(state)}</state>"
f"<cancelled>false</cancelled>"
f"<cancelable>false</cancelable>"
f"<descriptionId>{escape(str(task.get('operation') or 'task'))}</descriptionId>"
f"<progress>{progress}</progress>"
f"{entity_xml}{result_xml}"
f"</val>"
)
return (
"<objectSet>"
f'<obj type="Task">{escape(task_id)}</obj>'
"<kind>modify</kind>"
f"<changeSet><name>info</name><op>assign</op>{info_xml}</changeSet>"
"</objectSet>"
)
def _vim_power_state(raw: str) -> str:
return {
"POWERED_ON": "poweredOn",
"POWERED_OFF": "poweredOff",
"SUSPENDED": "suspended",
}.get(raw, "poweredOff")
def _vm_runtime_object_set_xml(obj: ManagedObject) -> str:
"""Push VM runtime/guest props so post-CreateVM waiters can finish."""
props = obj.props if isinstance(obj.props, dict) else {}
power = _vim_power_state(str(props.get("power_state") or "POWERED_OFF"))
identity = props.get("identity") if isinstance(props.get("identity"), dict) else {}
uuid = str(identity.get("instance_uuid") or props.get("uuid") or f"uuid-{obj.moid}")
bios = str(identity.get("bios_uuid") or f"bios-{obj.moid}")
xsi = 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
changes = [
f"<changeSet><name>name</name><op>assign</op><val>{escape(obj.name)}</val></changeSet>",
f"<changeSet><name>runtime.powerState</name><op>assign</op>"
f'<val {xsi} xsi:type="VirtualMachinePowerState">{escape(power)}</val></changeSet>',
f"<changeSet><name>summary.runtime.powerState</name><op>assign</op>"
f'<val {xsi} xsi:type="VirtualMachinePowerState">{escape(power)}</val></changeSet>',
f"<changeSet><name>config.uuid</name><op>assign</op><val>{escape(uuid)}</val></changeSet>",
f"<changeSet><name>config.instanceUuid</name><op>assign</op>"
f"<val>{escape(uuid)}</val></changeSet>",
f"<changeSet><name>summary.config.uuid</name><op>assign</op>"
f"<val>{escape(uuid)}</val></changeSet>",
f"<changeSet><name>summary.config.instanceUuid</name><op>assign</op>"
f"<val>{escape(uuid)}</val></changeSet>",
f"<changeSet><name>summary.config.guestId</name><op>assign</op>"
f"<val>otherGuest64</val></changeSet>",
f"<changeSet><name>guest.toolsStatus</name><op>assign</op>"
f'<val {xsi} xsi:type="VirtualMachineToolsStatus">toolsOk</val></changeSet>',
f"<changeSet><name>guest.toolsRunningStatus</name><op>assign</op>"
f"<val>guestToolsRunning</val></changeSet>",
f"<changeSet><name>guest.ipAddress</name><op>assign</op>"
f"<val>192.168.1.50</val></changeSet>",
f"<changeSet><name>summary.guest.ipAddress</name><op>assign</op>"
f"<val>192.168.1.50</val></changeSet>",
f"<changeSet><name>config.hardware.deviceChange</name><op>assign</op>"
f'<val {xsi} xsi:type="xsd:boolean">false</val></changeSet>',
]
del bios # reserved for future BIOS UUID prop parity
return (
"<objectSet>"
f'<obj type="VirtualMachine">{escape(obj.moid)}</obj>'
"<kind>modify</kind>" + "".join(changes) + "</objectSet>"
)
async def wait_updates_xml(
database: Database,
*,
session_key: str,
body: str,
objects: list[ManagedObject],
) -> str:
from app.vsphere.domain import tasks as task_store
version_match = re.search(
r"<(?:\w+:)?version[^>]*>([^<]*)</(?:\w+:)?version>",
body,
)
client_version = (version_match.group(1).strip() if version_match else "") or ""
stored = await _pc_get(database, "version", session_key)
last = int((stored or {}).get("n") or 0) if isinstance(stored, dict) else int(stored or 0)
notified_raw = await _pc_get(database, "meta", f"tasks_notified:{session_key}")
notified: set[str] = set()
if isinstance(notified_raw, dict):
notified = {str(x) for x in (notified_raw.get("ids") or [])}
elif isinstance(notified_raw, list):
notified = {str(x) for x in notified_raw}
all_tasks = await task_store.list_tasks(database)
new_tasks = [t for t in all_tasks if str(t.get("task") or "") not in notified]
synced = bool(client_version) and int(client_version or "0") >= last and last > 0
max_wait = _parse_max_wait_seconds(body)
# Late task.Wait callers often subscribe after CreateVM already SUCCEEDED and after
# an earlier WaitForUpdates marked that task "notified". Re-assert the newest task
# once while the client is blocking (maxWaitSeconds > 0).
replay_tasks: list[dict[str, Any]] = []
if synced and not new_tasks and max_wait > 0:
newest = next(
(t for t in all_tasks if str(t.get("status") or "") in {"SUCCEEDED", "FAILED"}),
None,
)
if newest is None:
return f"<returnval><version>{last}</version></returnval>"
# Cap replay storms: only bump when client still has maxWait (blocking wait).
replay_tasks = [newest]
elif synced and not new_tasks:
return f"<returnval><version>{last}</version></returnval>"
next_ver = max(last, 0) + 1
await _pc_put(database, "version", session_key, {"n": next_ver})
objects_xml: list[str] = []
by_moid = {o.moid: o for o in objects}
# First update for a session: seed inventory name props (Finder / ContainerView).
if last == 0:
limited: list[ManagedObject] = []
vm_count = 0
for obj in objects:
if obj.type == "VirtualMachine":
if vm_count >= 200:
continue
vm_count += 1
limited.append(obj)
for obj in limited:
objects_xml.append(
"<objectSet>"
f'<obj type="{escape(obj.type)}">{escape(obj.moid)}</obj>'
"<kind>enter</kind>"
f"<changeSet><name>name</name><op>assign</op>"
f"<val>{escape(obj.name)}</val></changeSet>"
"</objectSet>"
)
emit_tasks = new_tasks or replay_tasks
vm_ids: set[str] = set()
for task in emit_tasks:
objects_xml.append(_task_object_set_xml(task))
result = task.get("result") if isinstance(task.get("result"), dict) else {}
vm_moid = str((result or {}).get("vm") or "")
if vm_moid:
vm_ids.add(vm_moid)
for vm_moid in sorted(vm_ids):
obj = by_moid.get(vm_moid)
if obj is not None:
objects_xml.append(_vm_runtime_object_set_xml(obj))
if new_tasks:
notified.update(str(t.get("task") or "") for t in new_tasks)
await _pc_put(database, "meta", f"tasks_notified:{session_key}", {"ids": sorted(notified)})
return (
"<returnval>"
f"<version>{next_ver}</version>"
'<filterSet><filter type="PropertyFilter">filter-1</filter>'
+ "".join(objects_xml)
+ "</filterSet></returnval>"
)