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,90 @@
|
||||
"""Bulk reads through the vSphere PropertyCollector.
|
||||
|
||||
Every read in this package asks for an explicit list of property paths
|
||||
(``napalm_vmware.paths``) instead of touching attributes on managed objects.
|
||||
One round trip per object type rather than one per attribute, and it keeps
|
||||
working where lazy attribute access does not: pyVmomi fails to deserialize
|
||||
whole ``summary`` objects from some servers while the individual paths are
|
||||
fine.
|
||||
|
||||
This class is the one seam the tests replace. Its output is already plain
|
||||
(see :mod:`napalm_vmware._plain`), so a fake only has to return dicts.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from pyVmomi import vim, vmodl
|
||||
|
||||
from napalm_vmware._plain import to_plain
|
||||
|
||||
_PC = vmodl.query.PropertyCollector
|
||||
_PAGE_SIZE = 500
|
||||
|
||||
|
||||
def _vim_type(type_name: str) -> Any:
|
||||
vim_type = getattr(vim, type_name, None) or getattr(vim.dvs, type_name, None)
|
||||
if vim_type is None:
|
||||
raise ValueError(f"Unknown vSphere managed object type {type_name!r}")
|
||||
return vim_type
|
||||
|
||||
|
||||
class Inventory:
|
||||
"""Read-only access to a connected ServiceInstance's content."""
|
||||
|
||||
def __init__(self, content: Any) -> None:
|
||||
self._content = content
|
||||
|
||||
def about(self) -> dict[str, Any]:
|
||||
"""``ServiceContent.about``: product, version and API type of the endpoint."""
|
||||
return to_plain(self._content.about)
|
||||
|
||||
def collect(self, type_name: str, paths: Sequence[str]) -> list[dict[str, Any]]:
|
||||
"""Every object of ``type_name`` in the inventory, with the given properties."""
|
||||
vim_type = _vim_type(type_name)
|
||||
view = self._content.viewManager.CreateContainerView(
|
||||
self._content.rootFolder, [vim_type], True
|
||||
)
|
||||
try:
|
||||
traversal = _PC.TraversalSpec(
|
||||
name="view", path="view", skip=False, type=vim.view.ContainerView
|
||||
)
|
||||
obj_spec = _PC.ObjectSpec(obj=view, skip=True, selectSet=[traversal])
|
||||
return self._retrieve(obj_spec, vim_type, paths)
|
||||
finally:
|
||||
view.Destroy()
|
||||
|
||||
def properties(self, obj: Any, paths: Sequence[str]) -> dict[str, Any]:
|
||||
"""The given properties of one managed object; ``{}`` if it is gone."""
|
||||
rows = self._retrieve(_PC.ObjectSpec(obj=obj), type(obj), paths)
|
||||
return rows[0] if rows else {}
|
||||
|
||||
def licenses(self) -> list[dict[str, Any]]:
|
||||
"""Licenses assigned on this endpoint (edition key, name, used/total)."""
|
||||
manager = getattr(self._content, "licenseManager", None)
|
||||
if manager is None:
|
||||
return []
|
||||
return self.properties(manager, ["licenses"]).get("licenses", [])
|
||||
|
||||
def _retrieve(self, obj_spec: Any, vim_type: Any, paths: Sequence[str]) -> list[dict[str, Any]]:
|
||||
spec = _PC.FilterSpec(
|
||||
objectSet=[obj_spec], propSet=[_PC.PropertySpec(type=vim_type, pathSet=list(paths))]
|
||||
)
|
||||
pc = self._content.propertyCollector
|
||||
result = pc.RetrievePropertiesEx([spec], _PC.RetrieveOptions(maxObjects=_PAGE_SIZE))
|
||||
rows: list[dict[str, Any]] = []
|
||||
while result is not None:
|
||||
rows.extend(_row(obj) for obj in result.objects)
|
||||
if not result.token:
|
||||
break
|
||||
result = pc.ContinueRetrievePropertiesEx(token=result.token)
|
||||
return rows
|
||||
|
||||
|
||||
def _row(obj_content: Any) -> dict[str, Any]:
|
||||
row: dict[str, Any] = {"_moref": obj_content.obj._moId}
|
||||
for prop in obj_content.propSet or []:
|
||||
row[prop.name] = to_plain(prop.val)
|
||||
return row
|
||||
Reference in New Issue
Block a user