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,164 @@
|
||||
"""Mutable vCenter appliance networking / timesync state (lab persistence)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.db.pool import Database
|
||||
from app.vsphere.domain import api_state
|
||||
from app.vsphere.errors import invalid_argument
|
||||
|
||||
_NETWORKING_KEY = "/api/appliance/networking"
|
||||
_TIMESYNC_KEY = "/api/appliance/timesync"
|
||||
|
||||
_DEFAULT_NETWORKING: dict[str, Any] = {
|
||||
"hostname": "vcenter.lab.local",
|
||||
"node_name": "vcenter.lab.local",
|
||||
"default_gateway": "192.168.1.1",
|
||||
"dns": {
|
||||
"mode": "DHCP",
|
||||
"servers": ["8.8.8.8", "1.1.1.1"],
|
||||
"domains": ["lab.local"],
|
||||
},
|
||||
"interfaces": [
|
||||
{
|
||||
"name": "nic0",
|
||||
"status": "up",
|
||||
"mac": "00:50:56:aa:bb:cc",
|
||||
"ipv4": {"address": "192.168.1.50", "prefix": 24, "configurable": True},
|
||||
}
|
||||
],
|
||||
"proxy": {"enabled": False, "server": "", "port": 0, "username": ""},
|
||||
"no_proxy": ["localhost", "127.0.0.1", ".lab.local"],
|
||||
}
|
||||
|
||||
_DEFAULT_TIMESYNC: dict[str, Any] = {
|
||||
"mode": "NTP",
|
||||
"servers": ["time.lab.local"],
|
||||
"current_time": "2026-01-01T00:00:00.000Z",
|
||||
}
|
||||
|
||||
|
||||
def _merge_networking(payload: Any) -> dict[str, Any]:
|
||||
"""Overlay stored fields onto the full lab default shape."""
|
||||
|
||||
base = dict(_DEFAULT_NETWORKING)
|
||||
if not isinstance(payload, dict):
|
||||
return base
|
||||
merged = {**base, **payload}
|
||||
dns_base = dict(_DEFAULT_NETWORKING["dns"])
|
||||
dns_stored = payload.get("dns") if isinstance(payload.get("dns"), dict) else {}
|
||||
merged["dns"] = {**dns_base, **dns_stored}
|
||||
if not merged.get("interfaces"):
|
||||
merged["interfaces"] = list(_DEFAULT_NETWORKING["interfaces"])
|
||||
return merged
|
||||
|
||||
|
||||
async def get_networking(database: Database) -> dict[str, Any]:
|
||||
payload = await api_state.get_payload_or_seed(database, "GET", _NETWORKING_KEY)
|
||||
if not isinstance(payload, dict):
|
||||
return {}
|
||||
dns = payload.get("dns") if isinstance(payload.get("dns"), dict) else {}
|
||||
if not list(dns.get("domains") or []) or not list(dns.get("servers") or []):
|
||||
restored = await api_state.restore_seed_payload(database, "GET", _NETWORKING_KEY)
|
||||
if isinstance(restored, dict):
|
||||
await save_networking(database, restored)
|
||||
return restored
|
||||
return payload
|
||||
|
||||
|
||||
async def save_networking(database: Database, networking: dict[str, Any]) -> None:
|
||||
await api_state.put_payload(database, "GET", _NETWORKING_KEY, networking)
|
||||
# Keep Automation API GET mirrors in sync for stub/matrix probes.
|
||||
hostname = str(networking.get("hostname") or "vcenter.lab.local")
|
||||
dns = networking.get("dns") or {}
|
||||
await api_state.put_payload(
|
||||
database, "GET", "/api/appliance/networking/dns/hostname", {"name": hostname}
|
||||
)
|
||||
await api_state.put_payload(
|
||||
database,
|
||||
"GET",
|
||||
"/api/appliance/networking/dns/servers",
|
||||
{"mode": dns.get("mode") or "DHCP", "servers": list(dns.get("servers") or [])},
|
||||
)
|
||||
await api_state.put_payload(
|
||||
database,
|
||||
"GET",
|
||||
"/api/appliance/networking/dns/domains",
|
||||
list(dns.get("domains") or []),
|
||||
)
|
||||
|
||||
|
||||
async def set_hostname(database: Database, name: str) -> str:
|
||||
hostname = name.strip()
|
||||
if not hostname:
|
||||
raise invalid_argument("name is required")
|
||||
networking = await get_networking(database)
|
||||
networking["hostname"] = hostname
|
||||
networking["node_name"] = hostname
|
||||
await save_networking(database, networking)
|
||||
return hostname
|
||||
|
||||
|
||||
async def get_hostname(database: Database) -> str:
|
||||
networking = await get_networking(database)
|
||||
return str(networking.get("hostname") or "vcenter.lab.local")
|
||||
|
||||
|
||||
async def set_dns_servers(
|
||||
database: Database,
|
||||
*,
|
||||
mode: str | None = None,
|
||||
servers: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
networking = await get_networking(database)
|
||||
dns = dict(networking.get("dns") or {})
|
||||
if mode is not None:
|
||||
dns["mode"] = mode
|
||||
if servers is not None:
|
||||
dns["servers"] = [str(s) for s in servers]
|
||||
networking["dns"] = dns
|
||||
await save_networking(database, networking)
|
||||
return {"mode": dns.get("mode") or "DHCP", "servers": list(dns.get("servers") or [])}
|
||||
|
||||
|
||||
async def set_dns_domains(database: Database, domains: list[str]) -> list[str]:
|
||||
networking = await get_networking(database)
|
||||
dns = dict(networking.get("dns") or {})
|
||||
dns["domains"] = [str(d) for d in domains]
|
||||
networking["dns"] = dns
|
||||
await save_networking(database, networking)
|
||||
return list(dns["domains"])
|
||||
|
||||
|
||||
async def get_timesync(database: Database) -> dict[str, Any]:
|
||||
payload = await api_state.get_payload_or_seed(database, "GET", _TIMESYNC_KEY)
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
|
||||
|
||||
async def seed_appliance_state(database: Database) -> None:
|
||||
"""Write appliance networking/timesync seed baselines into vsphere_api_state."""
|
||||
|
||||
networking = dict(_DEFAULT_NETWORKING)
|
||||
await api_state.put_seed_payload(database, "GET", _NETWORKING_KEY, networking)
|
||||
await api_state.put_seed_payload(database, "GET", _TIMESYNC_KEY, dict(_DEFAULT_TIMESYNC))
|
||||
await save_networking(database, networking)
|
||||
dns = networking.get("dns") or {}
|
||||
await api_state.put_seed_payload(
|
||||
database,
|
||||
"GET",
|
||||
"/api/appliance/networking/dns/hostname",
|
||||
{"name": networking["hostname"]},
|
||||
)
|
||||
await api_state.put_seed_payload(
|
||||
database,
|
||||
"GET",
|
||||
"/api/appliance/networking/dns/servers",
|
||||
{"mode": dns.get("mode") or "DHCP", "servers": list(dns.get("servers") or [])},
|
||||
)
|
||||
await api_state.put_seed_payload(
|
||||
database,
|
||||
"GET",
|
||||
"/api/appliance/networking/dns/domains",
|
||||
list(dns.get("domains") or []),
|
||||
)
|
||||
Reference in New Issue
Block a user