At the moment, we only support the v8 API. We'll update the checks accordingly. Newer vSphere versions are expected to continue to provide v8 support.
379 lines
13 KiB
Python
379 lines
13 KiB
Python
# Copyright 2026 Cloudbase Solutions Srl
|
|
# All Rights Reserved.
|
|
|
|
"""Drop-in replacement for ``tests.integration.vixdisklib`` that does not
|
|
use VDDK.
|
|
|
|
Callers can switch with::
|
|
|
|
from openvixdisklib import openvixdisklib as vixdisklib
|
|
|
|
``VixDiskLibHandle.connect`` / ``open`` / ``read`` match the VDDK wrapper
|
|
in ``tests/integration/vixdisklib.py``. VIM login uses pyVmomi; NFC ticket,
|
|
authd, and disk I/O use ``nfc_auth`` and ``nfc_open``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import ctypes
|
|
import logging
|
|
import os
|
|
from collections.abc import Iterator
|
|
|
|
from pyVim.connect import Disconnect
|
|
from pyVmomi import vim
|
|
|
|
from openvixdisklib import nfc_auth, nfc_open
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
VIXDISKLIB_VERSION_MAJOR = 8
|
|
VIXDISKLIB_VERSION_MINOR = 0
|
|
|
|
VIXDISKLIB_SECTOR_SIZE = 512
|
|
|
|
VIXDISKLIB_CRED_UID = 1
|
|
|
|
VIXDISKLIB_FLAG_OPEN_UNBUFFERED = 1
|
|
VIXDISKLIB_FLAG_OPEN_SINGLE_LINK = 2
|
|
VIXDISKLIB_FLAG_OPEN_READ_ONLY = 4
|
|
|
|
VIXDISKLIB_FLAG_OPEN_COMPRESSION_ZLIB = 16
|
|
VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ = 32
|
|
VIXDISKLIB_FLAG_OPEN_COMPRESSION_SKIPZ = 64
|
|
|
|
|
|
def _nfc_compression(flags: int) -> int:
|
|
"""Return the NFC IO compression type for VixDiskLib open ``flags``."""
|
|
alg = flags & (
|
|
VIXDISKLIB_FLAG_OPEN_COMPRESSION_ZLIB
|
|
| VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ
|
|
| VIXDISKLIB_FLAG_OPEN_COMPRESSION_SKIPZ
|
|
)
|
|
if alg == 0:
|
|
return nfc_open.NFC_COMPRESSION_NONE
|
|
if alg == VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ:
|
|
return nfc_open.NFC_COMPRESSION_FASTLZ
|
|
if alg & (alg - 1):
|
|
raise NotImplementedError(
|
|
"Cannot set two or more NBD compression algorithms at the same time"
|
|
)
|
|
raise NotImplementedError(f"NBD compression open flag 0x{alg:x} is not supported")
|
|
|
|
|
|
VIX_SUPPORTED_COMPATIBILITY_MODES = ["8.0"]
|
|
|
|
|
|
def get_buffer(size: int):
|
|
"""Return a ctypes buffer of ``size`` bytes, as the VDDK wrapper did."""
|
|
return ctypes.create_string_buffer(size)
|
|
|
|
|
|
def _parse_vm_moref(vmx_spec: str | None) -> str:
|
|
if not vmx_spec:
|
|
raise ValueError("vmx_spec is required (for example 'moref=vm-13098')")
|
|
if "=" in vmx_spec:
|
|
kind, value = vmx_spec.split("=", 1)
|
|
if kind.lower() != "moref" or not value:
|
|
raise ValueError(f"unsupported vmx_spec: {vmx_spec}")
|
|
return value
|
|
return vmx_spec
|
|
|
|
|
|
def _select_transport(transport_modes: str | None) -> str:
|
|
"""Return the first requested transport this replacement implements.
|
|
|
|
``None`` defaults to ``nbdssl``. A colon-separated list (VDDK
|
|
style, for example ``file:nbdssl:nbd``) picks the first of
|
|
``nbdssl`` or ``nbd``.
|
|
"""
|
|
if transport_modes is None:
|
|
return "nbdssl"
|
|
for mode in transport_modes.split(":"):
|
|
if mode in ("nbdssl", "nbd"):
|
|
return mode
|
|
raise NotImplementedError(
|
|
f"supported transports are nbdssl and nbd, got {transport_modes!r}"
|
|
)
|
|
|
|
|
|
class _Connection:
|
|
"""VIM session plus the VM moref needed to issue an NFC ticket at Open."""
|
|
|
|
def __init__(
|
|
self,
|
|
si: vim.ServiceInstance,
|
|
vm_moref: str,
|
|
snapshot_ref: str | None,
|
|
thumbprint: str | None,
|
|
allow_untrusted: bool,
|
|
read_only: bool,
|
|
transport_mode: str,
|
|
) -> None:
|
|
self.si = si
|
|
self.vm_moref = vm_moref
|
|
self.snapshot_ref = snapshot_ref
|
|
self.thumbprint = thumbprint
|
|
self.allow_untrusted = allow_untrusted
|
|
self.read_only = read_only
|
|
self.transport_mode = transport_mode
|
|
|
|
|
|
class _DiskHandle:
|
|
"""Opened NFC disk plus the authd TLS socket it was taken from."""
|
|
|
|
def __init__(self, disk: nfc_open.NfcDisk, authd_sock, transport_mode: str) -> None:
|
|
self.disk = disk
|
|
self.authd_sock = authd_sock
|
|
self.transport_mode = transport_mode
|
|
|
|
|
|
class VixDiskLibHandle:
|
|
"""VDDK-compatible handle backed by pyVmomi and the NFC replacement."""
|
|
|
|
def __init__(
|
|
self,
|
|
config_path: str | None = None,
|
|
vixdisklib_compatibility_version: str | None = None,
|
|
) -> None:
|
|
"""Accept the VDDK wrapper constructor; no native library is loaded.
|
|
|
|
Args:
|
|
config_path: Ignored. VDDK used this for logging plugins.
|
|
vixdisklib_compatibility_version: Optional ``major.minor`` string
|
|
such as ``8.0``. Must be in
|
|
``VIX_SUPPORTED_COMPATIBILITY_MODES``.
|
|
"""
|
|
del config_path
|
|
target_versions = VIX_SUPPORTED_COMPATIBILITY_MODES
|
|
if vixdisklib_compatibility_version:
|
|
if (
|
|
vixdisklib_compatibility_version
|
|
not in VIX_SUPPORTED_COMPATIBILITY_MODES
|
|
):
|
|
raise ValueError(
|
|
"Unsupported vixDiskLib compatibility version '%s'. "
|
|
"Supported versions: %s"
|
|
% (
|
|
vixdisklib_compatibility_version,
|
|
VIX_SUPPORTED_COMPATIBILITY_MODES,
|
|
)
|
|
)
|
|
target_versions = [vixdisklib_compatibility_version]
|
|
LOG.debug("vixDiskLib versions targeted: %s", target_versions)
|
|
version_used = target_versions[-1]
|
|
LOG.info(
|
|
"Successfully initialized vixDiskLib with target version '%s'", version_used
|
|
)
|
|
|
|
@classmethod
|
|
def get_vix_disklib_name(cls) -> str:
|
|
"""Return the native library name; this replacement does not load it."""
|
|
if os.name == "nt":
|
|
return "vixDiskLib.dll"
|
|
return "libvixDiskLib.so"
|
|
|
|
def get_transport_modes(self) -> list[str]:
|
|
"""Return the transport modes this replacement implements."""
|
|
return ["nbdssl", "nbd"]
|
|
|
|
def get_transport_mode(self, disk_handle: _DiskHandle) -> str:
|
|
"""Return the transport used for ``disk_handle``."""
|
|
return disk_handle.transport_mode
|
|
|
|
@contextlib.contextmanager
|
|
def connect(
|
|
self,
|
|
server_name: str,
|
|
thumbprint: str | None,
|
|
username: str,
|
|
password: str,
|
|
vmx_spec: str | None = None,
|
|
snapshot_ref: str | None = None,
|
|
read_only: bool = True,
|
|
transport_modes: str | None = None,
|
|
port: int = 443,
|
|
allow_untrusted: bool = False,
|
|
) -> Iterator[_Connection]:
|
|
"""Login to vCenter/ESXi. Matches ``VixDiskLib_ConnectEx``.
|
|
|
|
The NFC ticket and authd handshake are deferred to ``open``, as in
|
|
VDDK. Writable opens use ``NfcRandomAccessOpenDisk``; read-only
|
|
opens use ``NfcGetVmFiles``. ``snapshot_ref`` is accepted for API
|
|
compatibility and is not sent on the ticket SOAP call.
|
|
|
|
Args:
|
|
server_name: vCenter or ESXi hostname/IP.
|
|
thumbprint: SHA-1 thumbprint of the management TLS certificate.
|
|
When set, the certificate is pinned and need not be in
|
|
the system CA store.
|
|
username: VIM user name.
|
|
password: VIM password.
|
|
vmx_spec: VM selector, ``moref=vm-…``.
|
|
snapshot_ref: Snapshot moref; unused on the NFC ticket.
|
|
read_only: When False, the disk may be opened for write.
|
|
transport_modes: ``nbdssl``, ``nbd``, or a colon list. The
|
|
first supported mode is used; ``None`` defaults to
|
|
``nbdssl``.
|
|
port: HTTPS port, usually 443.
|
|
allow_untrusted: Skip management TLS verification when True.
|
|
When False with no ``thumbprint``, the system CA store
|
|
is used.
|
|
"""
|
|
LOG.debug(
|
|
"Connecting VixDiskLib: server_name=%s thumbprint=%s "
|
|
"vmx_spec=%s snapshot_ref=%s read_only=%s transport_modes=%s "
|
|
"port=%s allow_untrusted=%s",
|
|
server_name,
|
|
thumbprint,
|
|
vmx_spec,
|
|
snapshot_ref,
|
|
read_only,
|
|
transport_modes,
|
|
port,
|
|
allow_untrusted,
|
|
)
|
|
transport_mode = _select_transport(transport_modes)
|
|
vm_moref = _parse_vm_moref(vmx_spec)
|
|
si = nfc_auth.connect_vim(
|
|
server_name,
|
|
username,
|
|
password,
|
|
port=port,
|
|
thumbprint=thumbprint,
|
|
allow_untrusted=allow_untrusted,
|
|
)
|
|
conn = _Connection(
|
|
si,
|
|
vm_moref,
|
|
snapshot_ref,
|
|
thumbprint,
|
|
allow_untrusted,
|
|
read_only,
|
|
transport_mode,
|
|
)
|
|
try:
|
|
yield conn
|
|
finally:
|
|
self.disconnect(conn)
|
|
|
|
@contextlib.contextmanager
|
|
def open(
|
|
self,
|
|
conn: _Connection,
|
|
disk_path: str,
|
|
flags: int = VIXDISKLIB_FLAG_OPEN_READ_ONLY,
|
|
) -> Iterator[_DiskHandle]:
|
|
"""Open ``disk_path`` over NFC. Matches ``VixDiskLib_Open``.
|
|
|
|
Read-only opens request ``NfcGetVmFiles`` (VM only). The VMDK
|
|
path, including a snapshot parent such as ``…-000007.vmdk``, is
|
|
sent on NFC ``OPEN_FILE``. Writable opens use
|
|
``NfcRandomAccessOpenDisk`` and resolve a device key from the
|
|
disk's backing chain.
|
|
|
|
Args:
|
|
conn: Connection from ``connect``.
|
|
disk_path: Datastore path of the VMDK.
|
|
flags: Open flags. ``VIXDISKLIB_FLAG_OPEN_READ_ONLY`` opens
|
|
the disk read-only; omit it for write.
|
|
``VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ`` compresses
|
|
NFC IO. zlib and skipz are not implemented.
|
|
"""
|
|
LOG.debug("Openning VixDiskLib disk: %s", disk_path)
|
|
compression = _nfc_compression(flags)
|
|
read_only = bool(flags & VIXDISKLIB_FLAG_OPEN_READ_ONLY)
|
|
if not read_only and conn.read_only:
|
|
raise NotImplementedError("ConnectEx was read-only; cannot open for write")
|
|
|
|
vm = vim.VirtualMachine(conn.vm_moref, conn.si._stub)
|
|
nfc_ssl = conn.transport_mode == "nbdssl"
|
|
ticket = nfc_auth.get_nfc_ticket(
|
|
conn.si, vm, read_only=read_only, disk_path=None if read_only else disk_path
|
|
)
|
|
authd_sock = nfc_auth.connect_authd(
|
|
ticket, allow_untrusted=conn.allow_untrusted, nfc_ssl=nfc_ssl
|
|
)
|
|
session = nfc_auth.NfcAuthSession(conn.si, ticket, authd_sock, nfc_ssl=nfc_ssl)
|
|
try:
|
|
disk = nfc_open.open_disk(
|
|
session, disk_path, read_only=read_only, compression=compression
|
|
)
|
|
except Exception:
|
|
authd_sock.close()
|
|
raise
|
|
handle = _DiskHandle(disk, authd_sock, conn.transport_mode)
|
|
try:
|
|
yield handle
|
|
finally:
|
|
self.close(handle)
|
|
|
|
def read(
|
|
self,
|
|
disk_handle: _DiskHandle,
|
|
start_sector: int,
|
|
num_sectors: int,
|
|
buf: ctypes.Array | bytearray | memoryview,
|
|
) -> None:
|
|
"""Read ``num_sectors`` from ``start_sector`` into ``buf``.
|
|
|
|
Args:
|
|
disk_handle: Handle from ``open``.
|
|
start_sector: First sector to read.
|
|
num_sectors: Number of sectors to read.
|
|
buf: Destination buffer (``get_buffer`` or a writable bytes-like).
|
|
Uncompressed NFC extra is received into this buffer.
|
|
"""
|
|
disk_handle.disk.readinto(start_sector, num_sectors, memoryview(buf))
|
|
|
|
def write(
|
|
self,
|
|
disk_handle: _DiskHandle,
|
|
start_sector: int,
|
|
num_sectors: int,
|
|
buf: ctypes.Array | bytes | bytearray | memoryview,
|
|
) -> None:
|
|
"""Write ``num_sectors`` from ``buf`` starting at ``start_sector``.
|
|
|
|
Args:
|
|
disk_handle: Handle from ``open``.
|
|
start_sector: First sector to write.
|
|
num_sectors: Number of sectors to write.
|
|
buf: Source buffer (``get_buffer`` or a bytes-like).
|
|
"""
|
|
length = num_sectors * VIXDISKLIB_SECTOR_SIZE
|
|
if isinstance(buf, (bytes, bytearray, memoryview)):
|
|
data = bytes(buf[:length])
|
|
else:
|
|
data = buf.raw[:length]
|
|
disk_handle.disk.write(start_sector, num_sectors, data)
|
|
|
|
def close(self, disk_handle: _DiskHandle) -> None:
|
|
"""Close the VMDK and the authd socket used for NFC.
|
|
|
|
Args:
|
|
disk_handle: Handle from ``open``.
|
|
"""
|
|
LOG.debug("Closing VixDiskLib disk handle: %s", disk_handle)
|
|
try:
|
|
disk_handle.disk.close()
|
|
finally:
|
|
try:
|
|
disk_handle.authd_sock.close()
|
|
except OSError:
|
|
pass
|
|
|
|
def disconnect(self, conn: _Connection) -> None:
|
|
"""Logout of the VIM session.
|
|
|
|
Args:
|
|
conn: Connection from ``connect``.
|
|
"""
|
|
LOG.debug("Disconnecting VixDiskLib")
|
|
Disconnect(conn.si)
|
|
|
|
def exit(self) -> None:
|
|
"""No-op; there is no native VDDK library to tear down."""
|
|
return
|