create_vm_from_cloud_init takes the same qcow2/raw cloud images netOrk offers for Proxmox. ESXi can neither boot nor download them, so the driver does both: download with checksum check and one retry, convert with qemu-img to a streamOptimized VMDK (cached by URL), import through a minimal OVF descriptor over NFC, pin requested MACs, grow the disk, and attach a NoCloud seed ISO placed next to the VM's files. NoCloud rather than guestinfo because it needs nothing in the guest; the user-data installs open-vm-tools, which the driver declares as its guest agent. A failure after the import removes the VM again. Placement is a pure decision over inventory rows: a connected host outside maintenance mode that sees the datastore and every port group, with the resource pool and VM folder found by walking up to the datacenter -- one path for a standalone host and for a vCenter. Two faults vcsim surfaced and the tests now pin: a chunked upload body next to a Content-Length is refused with 500, so the disk goes up as a sized file object that also reports lease progress; and a device edit replaces the device as sent, so disk and NIC edits start from the live objects, backing included (vcsim panicked on a disk without one). destroy_vm, get_vm_status, get_network_targets (port groups with their fixed VLAN) and get_image_storages complete the contract. reboot_host on ESXi uses RebootHost_Task and refuses outside maintenance mode: force=True would cut power to running VMs. Tested against vcsim in ESXi and vCenter mode, end to end.
76 lines
3.0 KiB
Python
76 lines
3.0 KiB
Python
"""NAPALM driver for a VMware ESXi host, addressed directly."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from napalm_device_types import FingerprintRule
|
|
from pyVmomi import vim
|
|
|
|
from napalm_vmware._plain import to_plain
|
|
from napalm_vmware._tasks import invoke
|
|
from napalm_vmware.actions import VmwareActionsMixin
|
|
from napalm_vmware.base import VmwareBaseDriver
|
|
from napalm_vmware.parse.facts import esxi_facts
|
|
from napalm_vmware.parse.interfaces import host_interfaces, host_interfaces_ip
|
|
from napalm_vmware.parse.lldp import lldp_neighbors
|
|
from napalm_vmware.provisioning import VmwareProvisioningMixin
|
|
|
|
|
|
class VmwareEsxiDriver(VmwareProvisioningMixin, VmwareActionsMixin, VmwareBaseDriver):
|
|
"""One ESXi host. Its VMs, vmnics, vmkernel NICs, datastores and port groups."""
|
|
|
|
DRIVER_NAME = "vmware_esxi"
|
|
API_TYPE = "HostAgent"
|
|
STANDALONE = True
|
|
# A host reboot runs through hardware POST before ESXi starts its services.
|
|
REBOOT_SETTLE_SECONDS = 300
|
|
# Unverified against real hardware: the Host Client landing page says
|
|
# "VMware ESXi"; a vCenter's does not, and it says "vCenter".
|
|
HTTP_FINGERPRINT = [
|
|
FingerprintRule("vmware esxi", weight=9.0, mandatory=True),
|
|
FingerprintRule("vcenter", weight=9.0, negative=True),
|
|
]
|
|
|
|
def _host(self) -> dict[str, Any]:
|
|
hosts = self._hosts()
|
|
if not hosts:
|
|
raise RuntimeError(f"{self.hostname} shows no host in its inventory")
|
|
return hosts[0]
|
|
|
|
def get_facts(self) -> dict[str, Any]:
|
|
return esxi_facts(self._inventory.about(), self._host())
|
|
|
|
def get_interfaces(self) -> dict[str, dict[str, Any]]:
|
|
return host_interfaces(self._host())
|
|
|
|
def get_interfaces_ip(self) -> dict[str, dict[str, Any]]:
|
|
return host_interfaces_ip(self._host())
|
|
|
|
def reboot_host(self) -> None:
|
|
"""Restart the host, which has to be in maintenance mode.
|
|
|
|
``force=True`` would restart a host with running VMs by cutting their
|
|
power. Evacuating them -- or deciding not to -- is the operator's call,
|
|
made by entering maintenance mode in vSphere first.
|
|
"""
|
|
host = self._host()
|
|
if not host.get("runtime.inMaintenanceMode"):
|
|
raise RuntimeError(
|
|
f"{host.get('name', self.hostname)} is not in maintenance mode; "
|
|
"enter it in vSphere first so running VMs are not powered off"
|
|
)
|
|
# The task finishes as the host goes down; there is nothing to wait for.
|
|
invoke(self._mo(vim.HostSystem, host["_moref"]).RebootHost_Task, force=False)
|
|
|
|
def get_lldp_neighbors(self) -> dict[str, list[dict[str, str]]]:
|
|
network_system = self._host().get("configManager.networkSystem")
|
|
if not network_system:
|
|
return {}
|
|
mo = self._mo(vim.host.NetworkSystem, network_system)
|
|
try:
|
|
hints = invoke(mo.QueryNetworkHint)
|
|
except RuntimeError:
|
|
return {}
|
|
return lldp_neighbors(to_plain(hints) or [])
|