Two drivers from one package, both in the hypervisor role: - vmware_esxi talks to one host directly: facts, vmnics and vmkernel NICs, CDP/LLDP neighbours, sensors, VMs, datastores and port groups. - vmware_vcenter talks to a vCenter: every VM of every host it manages, with the host as the VM's node, plus distributed port groups. It reports no interfaces of its own; the hosts' NICs belong to the hosts. Both implement the HypervisorDriver VM contract: get_vms, get_vm_config, start/stop/reboot/suspend_vm and the four snapshot methods, and emit raw device warnings (maintenance mode, disconnected host, config issues, host managed by a vCenter, free license making the API read-only). Every read is a PropertyCollector query for the explicit paths in paths.py, converted by to_plain() into dicts and lists; the parsers only ever see that. tools/harvest.py dumps exactly those paths to JSON and tools/sanitize.py scrubs the dump, so a real host can become a test fixture without code changes. A VM's vmid is its instance UUID, which survives vMotion and re-registration; a MoRef does not. Tested against govmomi's vcsim in ESXi and vCenter mode, including real power and snapshot tasks. Not yet tested against real hardware.
69 lines
2.1 KiB
Python
69 lines
2.1 KiB
Python
"""Shared fixtures: harvested inventories and a fake Inventory that serves them.
|
|
|
|
The JSON files under ``fixtures/`` were produced by ``tools/harvest.py``
|
|
against govmomi's vcsim simulator (``vmware/vcsim`` and ``vmware/vcsim -esx``).
|
|
They have the real vSphere shapes but simulator values: vcsim labels devices
|
|
``disk-202-0`` where a real host says ``Hard disk 1``, reports no sensors to
|
|
speak of, and sets ``managementServerIp`` to ``0.0.0.0``. Tests that need a
|
|
branch vcsim does not produce copy a fixture and edit it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import json
|
|
from collections.abc import Sequence
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
FIXTURES = Path(__file__).parent / "fixtures"
|
|
_CACHE: dict[str, dict[str, Any]] = {}
|
|
|
|
|
|
def load(name: str) -> dict[str, Any]:
|
|
"""A private deep copy of fixture ``name``; tests may mutate it freely."""
|
|
if name not in _CACHE:
|
|
_CACHE[name] = json.loads((FIXTURES / f"{name}.json").read_text())
|
|
return copy.deepcopy(_CACHE[name])
|
|
|
|
|
|
class FakeInventory:
|
|
"""Serves a harvest dump the way ``Inventory`` serves a live endpoint."""
|
|
|
|
def __init__(self, data: dict[str, Any]) -> None:
|
|
self.data = data
|
|
self.calls: list[tuple[str, tuple[str, ...]]] = []
|
|
|
|
def about(self) -> dict[str, Any]:
|
|
return self.data["about"]
|
|
|
|
def licenses(self) -> list[dict[str, Any]]:
|
|
return self.data.get("licenses", [])
|
|
|
|
def collect(self, type_name: str, paths: Sequence[str]) -> list[dict[str, Any]]:
|
|
self.calls.append((type_name, tuple(paths)))
|
|
rows = self.data["objects"].get(type_name, [])
|
|
wanted = set(paths) | {"_moref"}
|
|
return [{k: v for k, v in row.items() if k in wanted} for row in rows]
|
|
|
|
# Convenience for tests that tweak a fixture.
|
|
def objects(self, type_name: str) -> list[dict[str, Any]]:
|
|
return self.data["objects"][type_name]
|
|
|
|
|
|
@pytest.fixture
|
|
def esxi_data() -> dict[str, Any]:
|
|
return load("vcsim-esxi")
|
|
|
|
|
|
@pytest.fixture
|
|
def vcenter_data() -> dict[str, Any]:
|
|
return load("vcsim-vcenter")
|
|
|
|
|
|
@pytest.fixture
|
|
def esxi_host(esxi_data) -> dict[str, Any]:
|
|
return esxi_data["objects"]["HostSystem"][0]
|