Source code for saltext.vcf.utils.vim

"""
vSphere SOAP/VMODL connection helpers (pyVmomi).

Used for capabilities that vSphere REST doesn't yet expose — vSAN
imperative ops, Auto Deploy, Fault Tolerance, PerfManager historical
counters, PropertyCollector real-time subscriptions, AlarmManager
authoring, ExtensionManager, LicenseAssignmentManager, PBM policy
authoring.

The ``pyvmomi`` package is a hard dependency of saltext-vcf. Connect
to vCenter (or directly to an ESXi host) once per (host, username) pair
and cache the ``ServiceInstance``. Modules call :func:`get_service_instance`
and operate on ``si.content`` to reach the various managers.

Config is read from Salt opts/pillar under the same ``saltext.vcf.vcenter``
key as ``utils/vcenter`` (REST), so REST and SOAP modules share one
credential block::

    saltext.vcf:
      vcenter:
        host: mgmt-vc.vcf.nimbus.internal
        username: administrator@vsphere.local
        password: VMware123!VMware123!
        verify_ssl: false
"""

import atexit
import logging
import ssl
import time

from pyVim.connect import Disconnect
from pyVim.connect import SmartConnect
from pyVmomi import vim as _vim

from saltext.vcf.utils import vcenter as vc_rest

log = logging.getLogger(__name__)

# Cached ServiceInstance per (host, username). pyVmomi sessions are
# heavier than REST tokens; reusing them across calls saves ~150ms each.
_SI_CACHE: dict[str, object] = {}


[docs] def get_config(opts, profile=None): """SOAP shares the REST vCenter config — no separate pillar key.""" return vc_rest.get_config(opts, profile=profile)
[docs] def get_service_instance(opts, profile=None): """Return a connected pyVmomi ``ServiceInstance``. Cached per ``(host, username)``. Use ``invalidate_service_instance`` to force a fresh connection (e.g. after a session timeout). """ cfg = get_config(opts, profile=profile) host, port, username = _connection_target(cfg) cache_key = f"{host}:{port or 443}:{username}" if cache_key in _SI_CACHE: return _SI_CACHE[cache_key] sslContext = None # pylint: disable=invalid-name if not cfg["verify_ssl"]: sslContext = ssl._create_unverified_context() # pylint: disable=invalid-name smart_kwargs = { "host": host, "user": username, "pwd": cfg["password"], "sslContext": sslContext, } if port is not None: smart_kwargs["port"] = int(port) si = SmartConnect(**smart_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 target.""" cfg = get_config(opts, profile=profile) host, port, username = _connection_target(cfg) cache_key = f"{host}:{port or 443}:{username}" si = _SI_CACHE.pop(cache_key, None) if si is not None: _safe_disconnect(si)
def _connection_target(cfg): """Return ``(host, port, username)`` parsed from a pillar ``cfg`` block. Allows ``host: localhost:25443`` style or an explicit ``port:`` key in the pillar config — useful when reaching the target through an SSH tunnel or other port-forward. """ host = cfg["host"] username = cfg["username"] port = cfg.get("port") if ":" in host and port is None: host, _, port_str = host.rpartition(":") port = int(port_str) return host, port, username def _safe_disconnect(si): try: Disconnect(si) except Exception as exc: # pylint: disable=broad-except log.debug("pyVmomi disconnect raised %s; ignoring", exc)
[docs] def content(opts, profile=None): """Shortcut: ``get_service_instance(opts).RetrieveContent()``.""" return get_service_instance(opts, profile=profile).RetrieveContent()
# --------------------------------------------------------------------------- # Manager accessors — convenience for callers # --------------------------------------------------------------------------- def alarm_manager(opts, profile=None): return content(opts, profile=profile).alarmManager def perf_manager(opts, profile=None): return content(opts, profile=profile).perfManager def extension_manager(opts, profile=None): return content(opts, profile=profile).extensionManager def license_manager(opts, profile=None): return content(opts, profile=profile).licenseManager def license_assignment_manager(opts, profile=None): return license_manager(opts, profile=profile).licenseAssignmentManager def custom_fields_manager(opts, profile=None): return content(opts, profile=profile).customFieldsManager def property_collector(opts, profile=None): return content(opts, profile=profile).propertyCollector def view_manager(opts, profile=None): return content(opts, profile=profile).viewManager def root_folder(opts, profile=None): return content(opts, profile=profile).rootFolder def authorization_manager(opts, profile=None): return content(opts, profile=profile).authorizationManager
[docs] def wait_for_task(task, *, timeout=300, poll_interval=0.5): """Block until a pyVmomi ``*_Task`` finishes, then return ``task.info.result``. pyVmomi's ``*_Task`` methods are async: they return as soon as vCenter accepts the request, before the operation has actually taken effect. Callers that immediately query the resulting state race the task; wrap every mutating SOAP call with this helper. Polls ``task.info.state`` every ``poll_interval`` seconds (default 0.5) up to ``timeout`` seconds (default 300). Raises ``RuntimeError`` on task error and ``TimeoutError`` if the task hasn't finished in time. """ deadline = time.monotonic() + timeout while task.info.state in (_vim.TaskInfo.State.running, _vim.TaskInfo.State.queued): if time.monotonic() > deadline: raise TimeoutError( f"task {task._moId!r} did not finish within {timeout}s" # noqa: SLF001 ) time.sleep(poll_interval) if task.info.state == _vim.TaskInfo.State.error: msg = task.info.error.msg if task.info.error else "task failed" raise RuntimeError(f"task {task._moId!r} failed: {msg}") # noqa: SLF001 return task.info.result