Source code for saltext.vcf.utils.esxi

"""
ESXi host connection helpers — **standalone/unmanaged ESXi only**.

All communication uses the pyVmomi SOAP/VMODL stack (``/sdk``). This works on
any ESXi host regardless of whether the vAPI endpoint (``/api/*``) is present,
which is important because shuttle-deployed test hosts omit that service.

On hosts that have been joined to a vCenter, the REST ``/api/session`` endpoint
is blocked (``400`` on real VCF 9.2). For hosts managed by vCenter, cluster-
level configuration is done via the Configuration Profile API; see
:mod:`saltext.vcf.clients.cluster_config`.

Config is read from Salt opts/pillar under ``saltext.vcf.esxi``::

    saltext.vcf:
      esxi:
        host: esxi-test.example.com
        username: root
        password: VMware123!
        verify_ssl: false
"""

import atexit
import logging
import ssl

from pyVim.connect import Disconnect
from pyVim.connect import SmartConnect

log = logging.getLogger(__name__)

# Cached ServiceInstance per cache key (avoids repeat handshakes).
_SI_CACHE: dict[str, object] = {}


[docs] def get_config(opts, profile=None): """Extract ESXi connection config from Salt opts/pillar.""" pillar = opts.get("pillar", {}) root = pillar.get("saltext.vcf", {}) or opts.get("saltext.vcf", {}) cfg = root.get("esxi", {}) if profile: cfg = root.get("profiles", {}).get(profile, {}).get("esxi", cfg) return { "host": cfg.get("host") or cfg.get("hostname"), "username": cfg.get("username") or cfg.get("user"), "password": cfg.get("password"), "verify_ssl": cfg.get("verify_ssl", True), }
[docs] def get_service_instance(opts, profile=None): """Return a connected pyVmomi ``ServiceInstance`` for the ESXi host. Cached per ``(host, port, username)``. Use :func:`invalidate_service_instance` to force a fresh connection. """ cfg = get_config(opts, profile=profile) host = cfg["host"] port = None # Support ``host: esxi.example.com:8443`` or a separate ``port:`` key. if ":" in host: host, _, port_str = host.rpartition(":") port = int(port_str) cache_key = f"soap:{host}:{port or 443}:{cfg['username']}" if cache_key in _SI_CACHE: return _SI_CACHE[cache_key] ssl_context = None if not cfg["verify_ssl"]: ssl_context = ssl._create_unverified_context() # noqa: SLF001 connect_kwargs = { "host": host, "user": cfg["username"], "pwd": cfg["password"], "sslContext": ssl_context, } if port is not None: connect_kwargs["port"] = port si = SmartConnect(**connect_kwargs) _SI_CACHE[cache_key] = si atexit.register(_safe_disconnect, si) return si
[docs] def invalidate_service_instance(opts, profile=None): """Disconnect and drop the cached ServiceInstance for this host.""" cfg = get_config(opts, profile=profile) host = cfg["host"] port = None if ":" in host: host, _, port_str = host.rpartition(":") port = int(port_str) cache_key = f"soap:{host}:{port or 443}:{cfg['username']}" si = _SI_CACHE.pop(cache_key, None) if si is not None: _safe_disconnect(si)
[docs] def get_host_system(opts, profile=None): """Return the ``vim.HostSystem`` for a standalone ESXi host. When connecting directly to an ESXi host (not vCenter), the SOAP tree contains exactly one datacenter with one host. """ si = get_service_instance(opts, profile=profile) content = si.RetrieveContent() return content.rootFolder.childEntity[0].host[0]
def _safe_disconnect(si): try: Disconnect(si) except Exception as exc: # pylint: disable=broad-except log.debug("pyVmomi ESXi disconnect raised %s; ignoring", exc)