"""Deterministic vSphere inventory seed for labs.""" from __future__ import annotations import os from typing import Any from app.db.pool import Database from app.security.auth import hash_secret from app.vsphere import inventory from app.vsphere.domain.api_state import seed_api_surface from app.vsphere.domain.appliance import seed_appliance_state from app.vsphere.domain.content import seed_platform_extras from app.vsphere.domain.platform_surface import seed_platform_surface from app.vsphere.profiles import VsphereSeedProfile, build_vsphere_profile, infer_profile_hint, props_json from app.vsphere.security.session import ensure_default_credentials async def _seed_platform(database: Database, *, extras_scale: int = 1) -> dict[str, Any]: """Libraries/tags/files + full Automation API surface state (all profiles).""" extras: dict[str, Any] = {} try: await seed_platform_extras(database, scale=extras_scale) extras["platform_extras"] = True extras["extras_scale"] = extras_scale except Exception as error: extras["platform_extras_error"] = str(error) try: extras["api_surface"] = await seed_api_surface(database) except Exception as error: extras["api_surface_error"] = str(error) try: await seed_appliance_state(database) extras["appliance"] = True except Exception as error: extras["appliance_error"] = str(error) try: extras["platform_surface"] = await seed_platform_surface(database) except Exception as error: extras["platform_surface_error"] = str(error) return extras async def seed_vsphere_inventory( database: Database, *, force: bool = False, profile: str | None = None, large_hosts: int | None = None, large_vms: int | None = None, ) -> dict[str, Any]: """Populate vCenter-like inventory from a named profile (default: large / 1000 VMs).""" resolved = build_vsphere_profile(profile, large_hosts=large_hosts, large_vms=large_vms) existing = await inventory.count_objects(database) if existing and not force: await ensure_default_credentials(database) platform = await _seed_platform(database, extras_scale=resolved.extras_scale) by_type = await inventory.count_by_type(database) return { "seeded": False, "profile": resolved.name, "objects": existing, "by_type": by_type, "vms": by_type.get("VirtualMachine", 0), "hosts": by_type.get("HostSystem", 0), **platform, } await _wipe(database) await _apply_profile(database, resolved) await ensure_default_credentials(database) platform = await _seed_platform(database, extras_scale=resolved.extras_scale) by_type = await inventory.count_by_type(database) return { "seeded": True, "profile": resolved.name, "objects": await inventory.count_objects(database), "by_type": by_type, "vms": by_type.get("VirtualMachine", 0), "hosts": by_type.get("HostSystem", 0), **platform, } async def _wipe(database: Database) -> None: from app.vsphere.domain.content import clear_transfer_sessions from app.vsphere.soap.property_collector import clear_pc_state await clear_pc_state(database) await clear_transfer_sessions(database) pool = database.pool # type: ignore[attr-defined] async with pool.acquire() as conn: await conn.execute( """ DO $$ BEGIN IF to_regclass('public.vsphere_nfc_leases') IS NOT NULL THEN DELETE FROM vsphere_nfc_leases; END IF; IF to_regclass('public.vsphere_transfer_sessions') IS NOT NULL THEN DELETE FROM vsphere_transfer_sessions; END IF; IF to_regclass('public.vsphere_console_tickets') IS NOT NULL THEN DELETE FROM vsphere_console_tickets; END IF; 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_permissions") await conn.execute("DELETE FROM vsphere_datastore_files") await conn.execute("DELETE FROM vsphere_snapshots") await conn.execute("DELETE FROM vsphere_tag_associations") await conn.execute("DELETE FROM vsphere_tags") await conn.execute("DELETE FROM vsphere_tag_categories") await conn.execute("DELETE FROM vsphere_library_items") await conn.execute("DELETE FROM vsphere_libraries") await conn.execute("DELETE FROM vsphere_tasks") await conn.execute( """ DO $$ BEGIN IF to_regclass('public.vsphere_api_state') IS NOT NULL THEN DELETE FROM vsphere_api_state; END IF; END $$; """ ) await conn.execute("UPDATE vsphere_objects SET parent_moid = NULL") await conn.execute("DELETE FROM vsphere_objects") async def _apply_profile(database: Database, profile: VsphereSeedProfile) -> None: rows = [ { "moid": obj.moid, "type": obj.type, "name": obj.name, "parent_moid": obj.parent_moid, "props": obj.props, } for obj in profile.objects ] # Parents first: folders → dc → cluster → hosts/vms. Spec order already topological. await inventory.upsert_objects_batch(database, rows) pool = database.pool # type: ignore[attr-defined] async with pool.acquire() as conn: for cred in profile.credentials: await conn.execute( """ INSERT INTO vsphere_credentials (username, password_hash, roles) VALUES ($1, $2, $3) ON CONFLICT (username) DO UPDATE SET password_hash = EXCLUDED.password_hash, roles = EXCLUDED.roles """, cred.username, hash_secret(cred.password), list(cred.roles), ) for perm in profile.permissions: await conn.execute( """ INSERT INTO vsphere_permissions (principal, role, entity_moid, propagate) VALUES ($1, $2, $3, $4) """, perm.principal, perm.role, perm.entity_moid, perm.propagate, ) def default_profile_name() -> str: return os.getenv("SEED_VSPHERE_PROFILE", "large") async def vsphere_state_summary(database: Database) -> dict[str, Any]: by_type = await inventory.count_by_type(database) hosts = by_type.get("HostSystem", 0) vms = by_type.get("VirtualMachine", 0) datastores = by_type.get("Datastore", 0) networks = by_type.get("Network", 0) + by_type.get("DistributedVirtualPortgroup", 0) return { "hosts": hosts, "vms": vms, "datastores": datastores, "networks": networks, "datacenters": by_type.get("Datacenter", 0), "clusters": by_type.get("ClusterComputeResource", 0), "objects": sum(by_type.values()), "by_type": by_type, "profile_hint": infer_profile_hint(hosts=hosts, vms=vms, datastores=datastores), } __all__ = [ "default_profile_name", "props_json", "seed_vsphere_inventory", "vsphere_state_summary", ]