206 lines
7.4 KiB
Python
206 lines
7.4 KiB
Python
"""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",
|
|
]
|