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.
97 lines
3.7 KiB
Python
97 lines
3.7 KiB
Python
"""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], *, raw: bool = False) -> dict[str, Any]:
|
|
"""The given properties of one managed object; ``{}`` if it is gone.
|
|
|
|
``raw=True`` keeps the pyVmomi objects -- for a caller that has to hand
|
|
a device back to vSphere whole, backing and all, in a reconfigure.
|
|
"""
|
|
rows = self._retrieve(_PC.ObjectSpec(obj=obj), type(obj), paths, raw=raw)
|
|
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], *, raw: bool = False
|
|
) -> 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, raw) for obj in result.objects)
|
|
if not result.token:
|
|
break
|
|
result = pc.ContinueRetrievePropertiesEx(token=result.token)
|
|
return rows
|
|
|
|
|
|
def _row(obj_content: Any, raw: bool = False) -> dict[str, Any]:
|
|
row: dict[str, Any] = {"_moref": obj_content.obj._moId}
|
|
for prop in obj_content.propSet or []:
|
|
row[prop.name] = prop.val if raw else to_plain(prop.val)
|
|
return row
|