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,95 @@
|
||||
"""get_facts for an ESXi host and for a vCenter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from napalm_vmware.parse.facts import esxi_facts, host_serial, vcenter_facts
|
||||
|
||||
|
||||
class TestEsxiFacts:
|
||||
def test_from_vcsim(self, esxi_data, esxi_host):
|
||||
facts = esxi_facts(esxi_data["about"], esxi_host)
|
||||
assert facts == {
|
||||
"hostname": "localhost",
|
||||
"fqdn": "localhost.localdomain",
|
||||
"vendor": "VMware, Inc.",
|
||||
"model": "VMware Virtual Platform",
|
||||
"serial_number": "VMware-56 4d 8d e8 1e 9f a1 3e-71 fa 13 a8 e1 a7 fd 70",
|
||||
"os_version": "VMware ESXi 8.0.2 build-21997540",
|
||||
"uptime": 77229.0,
|
||||
"interface_list": ["vmk0", "vmnic0", "vmnic1"],
|
||||
}
|
||||
|
||||
def test_falls_back_to_the_inventory_name_without_dns_config(self, esxi_data, esxi_host):
|
||||
del esxi_host["config.network.dnsConfig"]
|
||||
facts = esxi_facts(esxi_data["about"], esxi_host)
|
||||
assert facts["hostname"] == "localhost"
|
||||
assert facts["fqdn"] == "localhost.localdomain"
|
||||
|
||||
def test_hostname_without_domain(self, esxi_data, esxi_host):
|
||||
esxi_host["config.network.dnsConfig"]["domainName"] = ""
|
||||
assert esxi_facts(esxi_data["about"], esxi_host)["fqdn"] == "localhost"
|
||||
|
||||
def test_unknown_uptime_is_minus_one(self, esxi_data, esxi_host):
|
||||
del esxi_host["summary.quickStats"]
|
||||
assert esxi_facts(esxi_data["about"], esxi_host)["uptime"] == -1.0
|
||||
|
||||
def test_os_version_falls_back_to_about(self, esxi_data, esxi_host):
|
||||
del esxi_host["config.product"]
|
||||
facts = esxi_facts(esxi_data["about"], esxi_host)
|
||||
assert facts["os_version"] == esxi_data["about"]["fullName"]
|
||||
|
||||
|
||||
class TestHostSerial:
|
||||
def _info(self, *pairs):
|
||||
return {
|
||||
"otherIdentifyingInfo": [
|
||||
{"identifierType": {"key": k}, "identifierValue": v} for k, v in pairs
|
||||
]
|
||||
}
|
||||
|
||||
def test_system_info_serial_wins(self):
|
||||
host = {
|
||||
"hardware.systemInfo": {"serialNumber": "ABC123", **self._info(("ServiceTag", "x"))}
|
||||
}
|
||||
assert host_serial(host) == "ABC123"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("pairs", "expected"),
|
||||
[
|
||||
((("ServiceTag", "SVC"), ("SerialNumberTag", "SER")), "SER"),
|
||||
((("EnclosureSerialNumberTag", "ENC"), ("ServiceTag", "SVC")), "ENC"),
|
||||
((("AssetTag", "No Asset Tag"),), ""),
|
||||
],
|
||||
)
|
||||
def test_identifying_info_by_preference(self, pairs, expected):
|
||||
assert host_serial({"hardware.systemInfo": self._info(*pairs)}) == expected
|
||||
|
||||
def test_falls_back_to_summary_hardware(self):
|
||||
host = {"summary.hardware": self._info(("SerialNumberTag", "OLD"))}
|
||||
assert host_serial(host) == "OLD"
|
||||
|
||||
def test_blank_values_are_ignored(self):
|
||||
host = {"hardware.systemInfo": self._info(("SerialNumberTag", " "), ("ServiceTag", "S"))}
|
||||
assert host_serial(host) == "S"
|
||||
|
||||
|
||||
class TestVcenterFacts:
|
||||
def test_from_vcsim(self, vcenter_data):
|
||||
facts = vcenter_facts(vcenter_data["about"], "vc01.example.lan")
|
||||
assert facts == {
|
||||
"hostname": "vc01",
|
||||
"fqdn": "vc01.example.lan",
|
||||
"vendor": "VMware, Inc.",
|
||||
"model": "VMware vCenter Server",
|
||||
"serial_number": "35bb7c82-8526-5aa7-a886-cd7f8e6be786",
|
||||
"os_version": "VMware vCenter Server 6.5.0 build-5973321 (govmomi simulator)",
|
||||
"uptime": -1.0,
|
||||
"interface_list": [],
|
||||
}
|
||||
|
||||
def test_ip_address_is_not_split_into_a_hostname(self, vcenter_data):
|
||||
facts = vcenter_facts(vcenter_data["about"], "10.0.0.5")
|
||||
assert facts["hostname"] == "10.0.0.5"
|
||||
assert facts["fqdn"] == "10.0.0.5"
|
||||
Reference in New Issue
Block a user