feat: NAPALM drivers for VMware ESXi and vCenter
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.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
"""Inventory: the single seam between the drivers and the vSphere API."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from pyVmomi import vim
|
||||
|
||||
from napalm_vmware._inventory import Inventory
|
||||
|
||||
|
||||
def _content(obj, props, missing=()):
|
||||
return SimpleNamespace(
|
||||
obj=obj,
|
||||
propSet=[SimpleNamespace(name=k, val=v) for k, v in props.items()],
|
||||
missingSet=[SimpleNamespace(path=p) for p in missing],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def content():
|
||||
"""pyVmomi type-checks spec fields, so the objects put into them are real."""
|
||||
c = MagicMock()
|
||||
c.about = vim.AboutInfo(apiType="HostAgent", fullName="VMware ESXi 8.0.2")
|
||||
view = vim.view.ContainerView("session[1]view-1", None)
|
||||
view.Destroy = MagicMock()
|
||||
c.viewManager.CreateContainerView.return_value = view
|
||||
c.licenseManager = vim.LicenseManager("LicenseManager", None)
|
||||
return c
|
||||
|
||||
|
||||
class TestCollect:
|
||||
def test_returns_plain_dicts_keyed_by_path_with_moref(self, content):
|
||||
pc = content.propertyCollector
|
||||
pc.RetrievePropertiesEx.return_value = SimpleNamespace(
|
||||
objects=[
|
||||
_content(
|
||||
vim.VirtualMachine("vm-1", None),
|
||||
{"name": "web01", "runtime.powerState": "poweredOn"},
|
||||
)
|
||||
],
|
||||
token=None,
|
||||
)
|
||||
rows = Inventory(content).collect("VirtualMachine", ["name", "runtime.powerState"])
|
||||
assert rows == [{"_moref": "vm-1", "name": "web01", "runtime.powerState": "poweredOn"}]
|
||||
|
||||
def test_follows_continuation_tokens(self, content):
|
||||
pc = content.propertyCollector
|
||||
pc.RetrievePropertiesEx.return_value = SimpleNamespace(
|
||||
objects=[_content(vim.VirtualMachine("vm-1", None), {"name": "a"})], token="t1"
|
||||
)
|
||||
pc.ContinueRetrievePropertiesEx.return_value = SimpleNamespace(
|
||||
objects=[_content(vim.VirtualMachine("vm-2", None), {"name": "b"})], token=None
|
||||
)
|
||||
rows = Inventory(content).collect("VirtualMachine", ["name"])
|
||||
assert [r["_moref"] for r in rows] == ["vm-1", "vm-2"]
|
||||
pc.ContinueRetrievePropertiesEx.assert_called_once_with(token="t1")
|
||||
|
||||
def test_empty_result(self, content):
|
||||
content.propertyCollector.RetrievePropertiesEx.return_value = None
|
||||
assert Inventory(content).collect("Datastore", ["name"]) == []
|
||||
|
||||
def test_missing_properties_are_simply_absent(self, content):
|
||||
"""An inaccessible VM has no config; the row must still come back."""
|
||||
content.propertyCollector.RetrievePropertiesEx.return_value = SimpleNamespace(
|
||||
objects=[
|
||||
_content(vim.VirtualMachine("vm-9", None), {"name": "ghost"}, ["config.template"])
|
||||
],
|
||||
token=None,
|
||||
)
|
||||
rows = Inventory(content).collect("VirtualMachine", ["name", "config.template"])
|
||||
assert rows == [{"_moref": "vm-9", "name": "ghost"}]
|
||||
|
||||
def test_the_container_view_is_destroyed(self, content):
|
||||
content.propertyCollector.RetrievePropertiesEx.return_value = None
|
||||
Inventory(content).collect("HostSystem", ["name"])
|
||||
content.viewManager.CreateContainerView.return_value.Destroy.assert_called_once()
|
||||
|
||||
def test_unknown_type_name_is_rejected(self, content):
|
||||
with pytest.raises(ValueError, match="NoSuchThing"):
|
||||
Inventory(content).collect("NoSuchThing", ["name"])
|
||||
|
||||
|
||||
def test_about_is_plain(content):
|
||||
assert Inventory(content).about()["apiType"] == "HostAgent"
|
||||
|
||||
|
||||
def test_licenses_are_read_from_the_license_manager(content):
|
||||
content.propertyCollector.RetrievePropertiesEx.return_value = SimpleNamespace(
|
||||
objects=[
|
||||
_content(
|
||||
vim.LicenseManager("LicenseManager", None),
|
||||
{"licenses": [vim.LicenseManager.LicenseInfo(editionKey="esxBasic", name="Free")]},
|
||||
)
|
||||
],
|
||||
token=None,
|
||||
)
|
||||
assert Inventory(content).licenses()[0]["editionKey"] == "esxBasic"
|
||||
|
||||
|
||||
def test_licenses_empty_without_a_license_manager(content):
|
||||
content.licenseManager = None
|
||||
assert Inventory(content).licenses() == []
|
||||
|
||||
|
||||
def test_properties_of_a_single_object(content):
|
||||
content.propertyCollector.RetrievePropertiesEx.return_value = SimpleNamespace(
|
||||
objects=[_content(vim.Task("task-7", None), {"info.state": "success"})], token=None
|
||||
)
|
||||
task = vim.Task("task-7", None)
|
||||
assert Inventory(content).properties(task, ["info.state"]) == {
|
||||
"_moref": "task-7",
|
||||
"info.state": "success",
|
||||
}
|
||||
|
||||
|
||||
def test_properties_of_a_vanished_object(content):
|
||||
content.propertyCollector.RetrievePropertiesEx.return_value = None
|
||||
assert Inventory(content).properties(vim.Task("task-7", None), ["info.state"]) == {}
|
||||
Reference in New Issue
Block a user