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.
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
"""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, props_json
|
||||
from app.vsphere.security.session import ensure_default_credentials
|
||||
|
||||
|
||||
async def _seed_platform(database: Database) -> dict[str, Any]:
|
||||
"""Libraries/tags/files + full Automation API surface state (all profiles)."""
|
||||
|
||||
extras: dict[str, Any] = {}
|
||||
try:
|
||||
await seed_platform_extras(database)
|
||||
extras["platform_extras"] = True
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
return {
|
||||
"hosts": by_type.get("HostSystem", 0),
|
||||
"vms": by_type.get("VirtualMachine", 0),
|
||||
"datastores": by_type.get("Datastore", 0),
|
||||
"datacenters": by_type.get("Datacenter", 0),
|
||||
"clusters": by_type.get("ClusterComputeResource", 0),
|
||||
"objects": sum(by_type.values()),
|
||||
"by_type": by_type,
|
||||
"profile_hint": default_profile_name(),
|
||||
}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"default_profile_name",
|
||||
"props_json",
|
||||
"seed_vsphere_inventory",
|
||||
"vsphere_state_summary",
|
||||
]
|
||||
Reference in New Issue
Block a user