Files
Christian Manivong 2b84ceae3a feat: VM provisioning, and reboot_host on ESXi
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.
2026-09-24 10:00:33 +02:00

159 lines
6.1 KiB
Python

"""What the ESXi and vCenter drivers share: session, VM lookup, common getters."""
from __future__ import annotations
from pathlib import Path
from typing import Any, ClassVar
from napalm.base.exceptions import ConnectionException
from napalm_device_types import HypervisorDriver
from napalm_device_types.models import (
SnapshotDict,
StorageVolumeDict,
VirtualNetworkDict,
VMConfigDict,
)
from napalm_vmware import _session, paths
from napalm_vmware._inventory import Inventory
from napalm_vmware.parse.environment import environment
from napalm_vmware.parse.networks import NetworkIndex, network_index, virtual_networks
from napalm_vmware.parse.snapshots import snapshot_list
from napalm_vmware.parse.storage import storage_pools
from napalm_vmware.parse.vm_config import vm_config
from napalm_vmware.parse.vms import vm_list
from napalm_vmware.parse.warnings import host_warnings
from napalm_vmware.provision.image import DEFAULT_CACHE_DIR
_DEFAULT_PORT = 443
#: ``about.apiType`` -> the driver that handles it, for a helpful refusal.
_DRIVER_FOR_API = {"HostAgent": "vmware_esxi", "VirtualCenter": "vmware_vcenter"}
class VmwareBaseDriver(HypervisorDriver):
"""Shared implementation. Concrete drivers set ``API_TYPE`` and ``STANDALONE``."""
VENDOR = "VMware"
USES_SSH = False
platform = "vmware"
#: ``about.apiType`` this driver accepts.
API_TYPE: ClassVar[str] = ""
#: True when the driver talks to one host directly (ESXi), False for vCenter.
STANDALONE: ClassVar[bool] = True
def __init__(
self,
hostname: str,
username: str,
password: str,
timeout: int = 60,
optional_args: dict[str, Any] | None = None,
) -> None:
self.hostname = hostname
self.username = username
self.password = password
self.timeout = timeout
args = optional_args or {}
self._port = int(args.get("port") or _DEFAULT_PORT)
self._verify_ssl = bool(args.get("verify_ssl", args.get("ssl_verify", True)))
# Where converted cloud images are kept between provisioning jobs.
self._image_cache_dir = Path(args.get("image_cache_dir") or DEFAULT_CACHE_DIR)
self._si: Any = None
self._inventory: Any = None
# -- session -------------------------------------------------------------
def open(self) -> None:
try:
si = _session.connect(
self.hostname,
self._port,
self.username,
self.password,
verify_ssl=self._verify_ssl,
timeout=self.timeout,
)
except Exception as exc:
raise ConnectionException(f"Cannot connect to {self.hostname}: {exc}") from exc
inventory = Inventory(si.RetrieveContent())
about = inventory.about()
if about.get("apiType") != self.API_TYPE:
_session.disconnect(si)
other = _DRIVER_FOR_API.get(about.get("apiType", ""), "another driver")
raise ConnectionException(
f"{self.hostname} is {about.get('fullName', 'not a supported VMware endpoint')}"
f"; use the {other} driver for it"
)
self._si, self._inventory = si, inventory
def close(self) -> None:
if self._si is not None:
_session.disconnect(self._si)
self._si = self._inventory = None
def is_alive(self) -> dict[str, bool]:
return {"is_alive": self._si is not None and _session.alive(self._si)}
def _mo(self, vim_type: Any, moref: str) -> Any:
"""A live managed-object reference for a MoRef id from a plain row."""
return vim_type(moref, self._si._stub)
# -- inventory reads -------------------------------------------------------
def _hosts(self) -> list[dict[str, Any]]:
return self._inventory.collect("HostSystem", paths.HOST)
def _vm_rows(self) -> list[dict[str, Any]]:
return self._inventory.collect("VirtualMachine", paths.VM)
def _index(self, hosts: list[dict[str, Any]]) -> NetworkIndex:
dv_portgroups = self._inventory.collect("DistributedVirtualPortgroup", paths.DV_PORTGROUP)
return network_index(hosts, dv_portgroups)
def _find_vm(self, name: str) -> dict[str, Any]:
"""The VM whose instance UUID, MoRef or name is ``name``."""
vms = [
vm
for vm in self._vm_rows()
if vm.get("config.instanceUuid") and not vm.get("config.template")
]
for key in ("config.instanceUuid", "_moref"):
match = [vm for vm in vms if vm.get(key) == name]
if match:
return match[0]
match = [vm for vm in vms if vm.get("name") == name]
if len(match) > 1:
raise ValueError(f"{len(match)} VMs are named {name!r}; address one by its vmid")
if not match:
raise ValueError(f"There is no VM named or identified by {name!r}")
return match[0]
# -- HypervisorDriver ------------------------------------------------------
def get_vms(self) -> list[dict[str, Any]]:
hosts = self._hosts()
return vm_list(self._vm_rows(), hosts, self._index(hosts))
def get_vm_config(self, name: str) -> VMConfigDict:
return vm_config(self._find_vm(name), self._index(self._hosts()))
def get_vm_snapshots(self, name: str) -> list[SnapshotDict]:
return snapshot_list(self._find_vm(name))
def get_vm_storage_pools(self) -> dict[str, StorageVolumeDict]:
return storage_pools(self._inventory.collect("Datastore", paths.DATASTORE))
def get_virtual_networks(self) -> dict[str, VirtualNetworkDict]:
return virtual_networks( # type: ignore[return-value]
self._hosts(),
self._inventory.collect("DistributedVirtualPortgroup", paths.DV_PORTGROUP),
self._inventory.collect("DistributedVirtualSwitch", paths.DV_SWITCH),
)
def get_environment(self) -> dict[str, Any]:
return environment(self._hosts())
def get_device_warnings(self) -> list[dict[str, Any]]:
return host_warnings(self._hosts(), self._inventory.licenses(), standalone=self.STANDALONE)