From b1b4d05c7c6fc8a2b55cfd40464dbe408d9b39c7 Mon Sep 17 00:00:00 2001 From: doccaz Date: Sat, 19 Sep 2026 10:35:17 -0300 Subject: [PATCH 1/3] Fix direct-ESXi (no vCenter) connectivity nfc_service() hardcoded the NfcService moref as "nfcService", which is correct for vCenter but wrong for a bare ESXi host (where the moref is "ha-nfc-service"). Connecting directly to ESXi therefore failed with vmodl.fault.ManagedObjectNotFound. Fixed by resolving the moref dynamically via an internal RetrieveInternalContent SOAP call, the same way real VDDK does it (confirmed by an SSL-hook capture of native VDDK connecting to a bare ESXi 8.0.3 host). Also fixes connect_authd() for tickets that omit `host`: on a direct-ESXi ticket the field is absent entirely (the authd endpoint is implicitly the same host already logged into), which previously caused a plain None passed to socket.create_connection. Validated end-to-end (ConnectEx + Open + Read, both nbd and nbdssl transports) against a live standalone ESXi 8.0.3 host with no vCenter in the topology. Full protocol details and the wire-level differences from the vCenter-mediated path are in docs/nfc_auth.md. --- README.md | 8 +-- docs/nfc_auth.md | 90 ++++++++++++++++++++++++++- docs/nfc_open.md | 1 - docs/reverse_engineering_procedure.md | 33 ++++++++-- openvixdisklib/nfc_auth.py | 52 ++++++++++++++-- openvixdisklib/openvixdisklib.py | 5 +- tests/integration/test_nfc_auth.py | 6 +- 7 files changed, 177 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 4866ece..b9b8ff5 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ from VDDK 8 NBD traffic; see `docs/`. ## Status -Supported and tested on **vCenter 8 / ESXi 8** (lab: 8.0.1). The +Supported and tested on **vCenter 8 / ESXi 8** (lab: 8.0.1), including a +standalone ESXi host with no vCenter. The VixDiskLib compatibility mode is `8.0` only. VIM login requests pyVmomi's vim25 **8.x** versions, so a newer host such as vSphere 9 stays on 8.x SOAP instead of 9.x types. @@ -27,14 +28,13 @@ moment. Default transport is `nbdssl` (`nbd` is still available): -- `VixDiskLib_ConnectEx` (UID credentials) +- `VixDiskLib_ConnectEx` (UID credentials; vCenter or direct ESXi) - `VixDiskLib_Open` (datastore path, read-only or read-write) - `VixDiskLib_Read` (optional ``skip_decompression`` packs FastLZ extras) - `VixDiskLib_Write` Not implemented: compression open flags other than FastLZ, CBT / -allocated-block queries, disk geometry (`DDB_GET`), encrypted disks, -and direct ESXi `ha-nfc` without vCenter `vpxa-nfc`. +allocated-block queries, disk geometry (`DDB_GET`), and encrypted disks. Requires Python 3.10 or later. diff --git a/docs/nfc_auth.md b/docs/nfc_auth.md index 36e89ed..0941b90 100644 --- a/docs/nfc_auth.md +++ b/docs/nfc_auth.md @@ -59,9 +59,9 @@ VDDK logs this as `Connected to VIM Server` / `Authenticating user` / `Logged in!`. OpenVixDiskLib keeps that `ServiceInstance` and its stub for the ticket call. -Direct ESXi login is the same SOAP login against hostd, but the NFC -moref and service name differ (`ha-nfc` instead of `nfcService` / -`vpxa-nfc`). The lab path is vCenter-mediated. +Direct ESXi login is the same SOAP login, this time against hostd +instead of vCenter. The NFC moref and service/PROXY name differ; see +"Direct ESXi (no vCenter)" below for the verified values. ## Stage 2: NFC ticket @@ -255,6 +255,90 @@ are for local ESXi credentials. With a vCenter ticket: argument is not what VDDK sends. The SHA-1 value is for verifying the TLS certificate, not for the `THUMBPRINT_SHA2` command. +## Direct ESXi (no vCenter) + +Captured against a standalone ESXi 8.0.3 host (`apiType: HostAgent`, +no vCenter in the picture at all) with the same SSL-hook technique +from `docs/ssl_hook.md`, using real VDDK 8.0.3 pointed straight at the +host (`vmxSpec=moref=`, `serverName=`). This corrects an +earlier guess in this file that assumed the moref would be `ha-nfc`. + +Differences from the vCenter-mediated path above: + +| Item | vCenter-mediated | Direct ESXi (verified) | +| ------------------------------- | ---------------------- | -------------------------- | +| `NfcService` moref | `nfcService` | `ha-nfc-service` | +| `NfcGetVmFilesResponse.service` | `vpxa-nfc` | `nfc` | +| `NfcGetVmFilesResponse.host` | present (ESXi address) | **absent** (omitted field) | +| authd `PROXY` line | `PROXY vpxa-nfc` | `PROXY nfc` | +| authd success line | `200 Connect ha-nfc` | `200 Connect ha-nfc` | + +The `NfcGetVmFiles` SOAP call itself is unchanged (`vm` argument only); +only the `_this` moref and the response fields differ: + +```xml + + <_this type="NfcService">ha-nfc-service + 1 + +``` + +```xml + + + 902 + ... + nfc + 1.1 + ... + + +``` + +Since `host` is absent, the client must already know where to dial +authd: the same ESXi host it just logged into over VIM. A vCenter +ticket always fills `host` because that ESXi address is not otherwise +known to the client. + +### Finding the `ha-nfc-service` moref + +VDDK does not hardcode this moref either. Before the `NfcGetVmFiles` +call, it issues an undocumented `RetrieveInternalContent` call on the +same `ServiceInstance` moref used for the public +`RetrieveServiceContent`: + +```xml + + <_this type="ServiceInstance">ServiceInstance + +``` + +The response carries ~20 undocumented managed-object refs +(`agentManager`, `llProvisioningManager`, `diskManager`, +`nfcService`, `proxyService`, ...); only `nfcService` matters here. +Its value was `nfcService` in the earlier vCenter capture and +`ha-nfc-service` on this bare ESXi host — VDDK reads it from this +response rather than assuming either name. + +### OpenVixDiskLib fix + +`openvixdisklib/nfc_auth.py` previously hardcoded +`NFC_SERVICE_MOID = "nfcService"`, which fails outright against a bare +ESXi host with `vmodl.fault.ManagedObjectNotFound`. It now resolves +the moref the same way VDDK does: `_nfc_service_moid()` issues the +`RetrieveInternalContent` SOAP call as raw XML over the existing +authenticated stub connection (registering pyVmomi types for the full +undocumented response schema wasn't worth it for one field) and +regex-extracts `nfcService` from the reply. + +`connect_authd()` also gained a `fallback_host` parameter: when +`ticket.host` is unset (the direct-ESXi case above), it dials the VIM +connection's own host instead. `openvixdisklib.py` passes +`conn.si._stub.host` for this. + +Validated end-to-end (`ConnectEx` + `Open` + `Read`, both `nbd` and +`nbdssl` transports) against a live standalone ESXi 8.0.3 host. + ## OpenVixDiskLib | Piece | Module | Reuses pyVmomi? | diff --git a/docs/nfc_open.md b/docs/nfc_open.md index bf61550..2aeffc2 100644 --- a/docs/nfc_open.md +++ b/docs/nfc_open.md @@ -249,7 +249,6 @@ I/O: `docs/nfc_read.md`, `docs/nfc_write.md`, and - `DDB_GET` / geometry / zlib and skipz compression / encryption keys - `NFC_DELTA_DISK`, change-block tracking - Host-switch (`NFC_AIO_SWITCH_HOST_*`) -- Direct ESXi `ha-nfc` without vCenter `vpxa-nfc` Reads after open are in `docs/nfc_read.md`. Writes are in `docs/nfc_write.md`. diff --git a/docs/reverse_engineering_procedure.md b/docs/reverse_engineering_procedure.md index b988f5c..6ed8273 100644 --- a/docs/reverse_engineering_procedure.md +++ b/docs/reverse_engineering_procedure.md @@ -9,7 +9,8 @@ NFC work can follow the same loop instead of rediscovering it. Scope so far: `VixDiskLib_ConnectEx` + `VixDiskLib_Open` + `VixDiskLib_Read` + `VixDiskLib_Write` against lab vCenter 8.0.1 / -ESXi 8, transports `nbd` and `nbdssl`. Validation method: +ESXi 8, transports `nbd` and `nbdssl`, plus a standalone ESXi 8.0.3 +host with no vCenter (Step 13). Validation method: `tests/integration/` (the session-scoped `lab` fixture creates a temporary empty VM with a 10 GiB disk and destroys it when the pytest session ends). @@ -381,8 +382,33 @@ not an OPEN_FILE bit. Capture VDDK with that flag (NBD + the port-902 Replay: pip `pyfastlz` via `openvixdisklib/fastlz.py` (NFC extra is raw FastLZ, without the wrapper's 4-byte length prefix) plus `NfcDisk` compression on each IO. Proof: -`tests/integration/test_nfc_read_write.py` (`fastlz`) and -`tests/perf/test_compare.py`. +## Step 13 — Direct ESXi (`ha-nfc`) without vCenter + +Same SSL-hook technique (Step 4), this time pointing VDDK 8.0.3 +straight at a standalone ESXi 8.0.3 host (`vmxSpec=moref=`, +`serverName=`, no vCenter in the topology). Confirmed +OpenVixDiskLib's hardcoded `NFC_SERVICE_MOID = "nfcService"` fails on +this host with `vmodl.fault.ManagedObjectNotFound` *before* touching +the capture — reproduced with plain `openvixdisklib` calls, no hook +needed to see that failure. + +The capture showed VDDK does not hardcode the moref either: it calls +an undocumented `RetrieveInternalContent` on the `ServiceInstance` +moref first, and reads `nfcService` from the reply (`ha-nfc-service` +on this host, vs. `nfcService` in the earlier vCenter capture). +`NfcGetVmFilesResponse.service` was `nfc` (not `vpxa-nfc`), and its +`host` field was **absent** — the authd endpoint is implicitly the +same host already logged into. Full detail in `docs/nfc_auth.md` +("Direct ESXi (no vCenter)"). + +Fix: `_nfc_service_moid()` in `openvixdisklib/nfc_auth.py` issues the +`RetrieveInternalContent` call as raw SOAP over the existing stub +connection (its response schema has ~20 other undocumented morefs not +worth registering with pyVmomi's type system for one field), and +`connect_authd()` takes a `fallback_host` used when `ticket.host` is +unset. Validated end-to-end (`ConnectEx`/`Open`/`Read`, `nbd` and +`nbdssl`) against the live host. + ## What to write down @@ -408,4 +434,3 @@ Not yet reversed, same loop as above: - `NFC_DELTA_DISK`, CBT / `QueryAllocatedBlocks` - `VixDiskLib_GetInfo` capacity - Host-switch AIO messages -- Direct ESXi `ha-nfc` without vCenter `vpxa-nfc` diff --git a/openvixdisklib/nfc_auth.py b/openvixdisklib/nfc_auth.py index d50744a..7b4ee5c 100644 --- a/openvixdisklib/nfc_auth.py +++ b/openvixdisklib/nfc_auth.py @@ -19,8 +19,10 @@ import contextlib import hashlib +import re import socket import ssl +import types from pyVim.connect import Disconnect, SmartConnect from pyVmomi import vim @@ -31,9 +33,9 @@ GetVmodlType, ) -NFC_SERVICE_MOID = "nfcService" AUTHD_DEFAULT_PORT = 902 _NFC_TYPES_REGISTERED = False +_NFC_SERVICE_MOID_RE = re.compile(r"]*>([^<]+)") def _ssl_client_context(verify: bool = True) -> ssl.SSLContext: @@ -126,6 +128,43 @@ def _register_nfc_types() -> None: _NFC_TYPES_REGISTERED = True +def _nfc_service_moid(si: vim.ServiceInstance) -> str: + """Return the NfcService moref via the internal ``RetrieveInternalContent`` call. + + vCenter and a bare ESXi host disagree on this moref (``nfcService`` vs. + ``ha-nfc-service``); VDDK resolves it dynamically instead of assuming + vCenter's name, which is why OpenVixDiskLib must too. The response also + carries ~20 other undocumented managed-object refs (agent manager, disk + manager, and so on) that aren't worth registering with pyVmomi's type + system just to read one field, so the call is issued as raw SOAP over + the existing authenticated connection and only ``nfcService`` is pulled + out of the XML. + """ + stub = si._stub + info = types.SimpleNamespace( + wsdlName="RetrieveInternalContent", version=stub.version, params=() + ) + request = stub.SerializeRequest(si, info, ()) + headers = { + "Cookie": stub.cookie, + "SOAPAction": stub.versionId, + "Content-Type": "text/xml; charset=utf-8", + } + conn = stub.GetConnection() + try: + conn.request("POST", stub.path, request, headers) + response = conn.getresponse() + body = response.read().decode("utf-8") + finally: + stub.ReturnConnection(conn) + match = _NFC_SERVICE_MOID_RE.search(body) + if response.status != 200 or not match: + raise RuntimeError( + f"RetrieveInternalContent (status {response.status}) had no nfcService moref" + ) + return match.group(1) + + def nfc_service(si: vim.ServiceInstance) -> vim.NfcService: """Return the vCenter/ESXi NfcService managed object on ``si``'s SOAP stub. @@ -134,7 +173,7 @@ def nfc_service(si: vim.ServiceInstance) -> vim.NfcService: """ _register_nfc_types() nfc_cls = GetVmodlType("vim.NfcService") - return nfc_cls(NFC_SERVICE_MOID, si._stub) + return nfc_cls(_nfc_service_moid(si), si._stub) def _vim_preferred_api_versions() -> list[str]: @@ -339,6 +378,7 @@ def connect_authd( allow_untrusted: bool = False, timeout: float = 30.0, nfc_ssl: bool = True, + fallback_host: str | None = None, ) -> ssl.SSLSocket: """Complete the ESXi authd handshake using an NFC HostServiceTicket. @@ -361,8 +401,12 @@ def connect_authd( timeout: Socket timeout in seconds. nfc_ssl: When True (the default), PROXY to the NFCSSL service used by nbdssl. Pass False for plaintext NFC (nbd). + fallback_host: Host to dial when ``ticket.host`` is unset. A ticket + issued directly by a bare ESXi host (no vCenter) omits ``host`` + entirely, since the authd endpoint is that same host; pass the + VIM connection's host in that case. """ - host = ticket.host + host = ticket.host or fallback_host port = ticket.port or AUTHD_DEFAULT_PORT raw = socket.create_connection((host, port), timeout=timeout) try: @@ -487,7 +531,7 @@ def authenticate( read_only=read_only, ) authd_sock = connect_authd( - ticket, allow_untrusted=allow_untrusted, nfc_ssl=nfc_ssl + ticket, allow_untrusted=allow_untrusted, nfc_ssl=nfc_ssl, fallback_host=host ) except Exception: Disconnect(si) diff --git a/openvixdisklib/openvixdisklib.py b/openvixdisklib/openvixdisklib.py index cafb7f9..fce8275 100644 --- a/openvixdisklib/openvixdisklib.py +++ b/openvixdisklib/openvixdisklib.py @@ -305,7 +305,10 @@ def open( 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 + ticket, + allow_untrusted=conn.allow_untrusted, + nfc_ssl=nfc_ssl, + fallback_host=conn.si._stub.host.rsplit(":", 1)[0], ) session = nfc_auth.NfcAuthSession(conn.si, ticket, authd_sock, nfc_ssl=nfc_ssl) try: diff --git a/tests/integration/test_nfc_auth.py b/tests/integration/test_nfc_auth.py index e074f1e..d6c32e1 100644 --- a/tests/integration/test_nfc_auth.py +++ b/tests/integration/test_nfc_auth.py @@ -11,7 +11,11 @@ def test_authd_handshake_completes(self, lab: LabEnv) -> None: """Complete VIM login and authd PROXY through ``200 Connect``.""" with lab.authenticate() as session: ticket = session.ticket - assert ticket.host + # ticket.host is unset on a direct-ESXi ticket (no vCenter): + # the authd endpoint is implicitly the host already logged + # into. connect_authd() falls back to that host, so the + # socket's peer address is the reliable check here. + assert session.authd_sock.getpeername()[0] assert ticket.port assert ticket.sessionId assert session.nfc_ssl is True From cb3b6577156344a871a9ba8c0d8ec1935125603b Mon Sep 17 00:00:00 2001 From: Lucian Petrut Date: Wed, 23 Sep 2026 08:33:14 +0000 Subject: [PATCH 2/3] Add direct ESXi integration tests The OpenVixDiskLib integration tests currently expect vSphere credentials. We'll add a test class that specifically covers direct ESXi connections. An ESXi node will be picked from vSphere, using credentials from `.test_config.yaml`. If no ESXi credentials are provided, we'll try to use the default "root" user and the vSphere password. --- README.md | 8 +- docs/nfc_auth.md | 7 +- tests/conftest.py | 7 ++ tests/integration/base.py | 118 ++++++++++++++++++++++++++ tests/integration/test_direct_esxi.py | 83 ++++++++++++++++++ 5 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 tests/integration/test_direct_esxi.py diff --git a/README.md b/README.md index b9b8ff5..de1d4e2 100644 --- a/README.md +++ b/README.md @@ -103,11 +103,17 @@ password: secret allow_untrusted: true datacenter: Datacenter datastore: datastore0 +esxi: + username: root + password: secret ``` A session-scoped pytest fixture creates an empty VM with a 10 GiB thin disk on that datastore and tears it down when the session ends. Tests -write known patterns and read them back. +write known patterns and read them back. Direct-ESXi tests pick the lab +VM's host from vCenter and log into hostd (default ``root`` and the +vCenter password) so NFC uses ``ha-nfc-service`` instead of +``nfcService``. They skip when lockdown is on or hostd login fails. ```bash tox -e integration diff --git a/docs/nfc_auth.md b/docs/nfc_auth.md index 0941b90..f8d762e 100644 --- a/docs/nfc_auth.md +++ b/docs/nfc_auth.md @@ -359,10 +359,13 @@ Run: ```bash .venv/bin/pytest tests/integration/test_nfc_auth.py +.venv/bin/pytest tests/integration/test_direct_esxi.py ``` -The test completes VIM login and the authd handshake (`200 Connect`) -and asserts an established TLS socket on `ticket.host:ticket.port`. +`test_nfc_auth.py` completes VIM login and the authd handshake against +vCenter. `test_direct_esxi.py` picks the lab VM's ESXi host from +inventory and repeats ConnectEx / Open / Read on hostd, where the +ticket omits `host` and NfcService is `ha-nfc-service`. ## What comes after authentication diff --git a/tests/conftest.py b/tests/conftest.py index a972d1e..4cf6402 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,7 @@ destroy_lab_vm, ensure_vddk_library_path, require_vddk, + resolve_direct_esxi_lab, ) @@ -31,6 +32,12 @@ def lab() -> Iterator[LabEnv]: destroy_lab_vm(env) +@pytest.fixture(scope="session") +def esxi_lab(lab: LabEnv) -> LabEnv: + """The session lab VM addressed through its ESXi host, not vCenter.""" + return resolve_direct_esxi_lab(lab) + + @pytest.fixture(scope="session") def vddk() -> None: """Skip VDDK-backed tests when ``libvixDiskLib`` cannot be loaded.""" diff --git a/tests/integration/base.py b/tests/integration/base.py index 57157fc..0e3d382 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -162,6 +162,124 @@ def _connect_vim( ) +def _esxi_direct_credentials(vc_password: str) -> tuple[str, str]: + """Return ESXi hostd credentials for direct-connect tests. + + Optional ``esxi.username`` / ``esxi.password`` in ``.test_config.yaml`` + override the defaults (``root`` and the vCenter password). + """ + if not os.path.isfile(_CONFIG_PATH): + return "root", vc_password + with open(_CONFIG_PATH, encoding="utf-8") as config_file: + data = yaml.safe_load(config_file) or {} + section = data.get("esxi") + if not isinstance(section, dict): + section = {} + username = str(section["username"]) if section.get("username") else "root" + password = str(section["password"]) if section.get("password") else vc_password + return username, password + + +def _host_management_ip(host: vim.HostSystem) -> str: + vnics = list(host.config.network.vnic or []) + for vnic in vnics: + ip = vnic.spec.ip.ipAddress if vnic.spec.ip else None + if ip and vnic.device == "vmk0": + return str(ip) + for vnic in vnics: + ip = vnic.spec.ip.ipAddress if vnic.spec.ip else None + if ip: + return str(ip) + if host.name: + return str(host.name) + raise RuntimeError(f"no management IPv4 on host {host._moId}") + + +def resolve_direct_esxi_lab(lab: LabEnv) -> LabEnv: + """Return a ``LabEnv`` that talks to the lab VM's ESXi host, not vCenter. + + The host is the one ``lab``'s VM is registered on. Hostd credentials + default to ``root`` plus the vCenter password. Tests should skip when + lockdown is on or hostd login fails. + """ + si = _connect_vim( + lab.host, + lab.username, + lab.password, + lab.port, + lab.thumbprint, + lab.allow_untrusted, + ) + esxi_host = "" + instance_uuid = "" + try: + if si.content.about.apiType == "HostAgent": + return lab + vm = vim.VirtualMachine(lab.vm_moref, si._stub) + host = vm.runtime.host + lockdown = str(getattr(host.config, "lockdownMode", "") or "") + if lockdown and not lockdown.endswith("lockdownDisabled"): + pytest.skip(f"ESXi {host.name} is in lockdown ({lockdown})") + esxi_host = _host_management_ip(host) + instance_uuid = vm.config.instanceUuid + if not instance_uuid: + pytest.skip(f"lab VM {lab.vm_moref} has no instanceUuid") + finally: + Disconnect(si) + + username, password = _esxi_direct_credentials(lab.password) + try: + thumbprint = nfc_auth.get_ssl_cert_thumbprint(esxi_host, lab.port) + except OSError as exc: + pytest.skip(f"cannot reach ESXi {esxi_host}: {exc}") + try: + esxi_si = _connect_vim( + esxi_host, + username, + password, + lab.port, + thumbprint, + lab.allow_untrusted, + ) + except vim.fault.InvalidLogin: + pytest.skip( + f"direct ESXi login to {esxi_host} failed; set esxi.username / " + "esxi.password in .test_config.yaml" + ) + except vim.fault.NoPermission: + pytest.skip( + f"ESXi user has no hostd privileges on {esxi_host}; set " + "esxi.username / esxi.password in .test_config.yaml" + ) + except OSError as exc: + pytest.skip(f"cannot connect to ESXi {esxi_host}: {exc}") + try: + if esxi_si.content.about.apiType != "HostAgent": + pytest.skip( + f"{esxi_host} apiType is {esxi_si.content.about.apiType}, not HostAgent" + ) + found = esxi_si.content.searchIndex.FindByUuid(None, instance_uuid, True, True) + if found is None: + pytest.skip( + f"lab VM instanceUuid {instance_uuid} not found on ESXi {esxi_host}" + ) + return LabEnv( + host=esxi_host, + port=lab.port, + username=username, + password=password, + allow_untrusted=lab.allow_untrusted, + datacenter=lab.datacenter, + datastore=lab.datastore, + thumbprint=thumbprint, + vm_moref=found._moId, + vmx_spec=f"moref={found._moId}", + disk_path=lab.disk_path, + ) + finally: + Disconnect(esxi_si) + + def _wait_for_task(task: vim.Task) -> Any: deadline = time.monotonic() + _TASK_TIMEOUT_S while task.info.state in (vim.TaskInfo.State.running, vim.TaskInfo.State.queued): diff --git a/tests/integration/test_direct_esxi.py b/tests/integration/test_direct_esxi.py new file mode 100644 index 0000000..e9a59eb --- /dev/null +++ b/tests/integration/test_direct_esxi.py @@ -0,0 +1,83 @@ +# Copyright 2026 Cloudbase Solutions Srl +# All Rights Reserved. + +"""Direct ESXi (HostAgent) NFC path, using the lab VM's host from vCenter.""" + +import pytest +from pyVim.connect import Disconnect + +from openvixdisklib import nfc_auth +from openvixdisklib import openvixdisklib as vixdisklib +from tests.integration.base import ( + SECTOR_AT_1GB, + SECTOR_SIZE, + LabEnv, + _connect_vim, + pattern_bytes, +) + + +class TestDirectEsxi: + def test_nfc_service_moref_is_host_agent(self, esxi_lab: LabEnv) -> None: + """RetrieveInternalContent must yield the ESXi NfcService moref.""" + si = _connect_vim( + esxi_lab.host, + esxi_lab.username, + esxi_lab.password, + esxi_lab.port, + esxi_lab.thumbprint, + esxi_lab.allow_untrusted, + ) + try: + assert si.content.about.apiType == "HostAgent" + moref = nfc_auth.nfc_service(si)._moId + assert moref != "nfcService" + assert moref == "ha-nfc-service" + finally: + Disconnect(si) + + @pytest.mark.parametrize("nfc_ssl", [True, False], ids=["nbdssl", "nbd"]) + def test_authd_handshake_omits_ticket_host( + self, esxi_lab: LabEnv, nfc_ssl: bool + ) -> None: + """Direct-ESXi tickets omit ``host`` and PROXY ``nfc``, not ``vpxa-nfc``.""" + with esxi_lab.authenticate(nfc_ssl=nfc_ssl) as session: + assert not session.ticket.host + assert session.ticket.service == "nfc" + assert session.ticket.sessionId + assert session.nfc_ssl is nfc_ssl + assert session.authd_sock.getpeername()[0] + assert session.authd_sock.version() + assert session.authd_sock.cipher() + + @pytest.mark.parametrize("transport_mode", ["nbdssl", "nbd"]) + def test_write_and_read_sector_zero_and_one_gib( + self, esxi_lab: LabEnv, transport_mode: str + ) -> None: + """ConnectEx + Open + Write/Read against hostd, not vCenter.""" + handle = vixdisklib.VixDiskLibHandle( + vixdisklib_compatibility_version="8.0", config_path=None + ) + write_buf = vixdisklib.get_buffer(SECTOR_SIZE) + read_buf = vixdisklib.get_buffer(SECTOR_SIZE) + connect_kwargs = esxi_lab.vixdisklib_connect_kwargs( + { + "allow_untrusted": esxi_lab.allow_untrusted, + "transport_modes": transport_mode, + } + ) + patterns = { + 0: pattern_bytes(SECTOR_SIZE, b"OVDL-ESX0"), + SECTOR_AT_1GB: pattern_bytes(SECTOR_SIZE, b"OVDL-ESX1"), + } + with ( + handle.connect(**connect_kwargs) as conn, + handle.open(conn, esxi_lab.disk_path, flags=0) as disk, + ): + assert handle.get_transport_mode(disk) == transport_mode + for start, expected in patterns.items(): + write_buf[:SECTOR_SIZE] = expected + handle.write(disk, start, 1, write_buf) + read_buf[:SECTOR_SIZE] = b"\xa5" * SECTOR_SIZE + handle.read(disk, start, 1, read_buf) + assert read_buf.raw[:SECTOR_SIZE] == expected From 172d565fb520e9369c092afab1c4587f94192ad6 Mon Sep 17 00:00:00 2001 From: Lucian Petrut Date: Wed, 23 Sep 2026 08:39:46 +0000 Subject: [PATCH 3/3] Fix flake8 error, complaining about line length --- openvixdisklib/nfc_auth.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openvixdisklib/nfc_auth.py b/openvixdisklib/nfc_auth.py index 7b4ee5c..cacffe0 100644 --- a/openvixdisklib/nfc_auth.py +++ b/openvixdisklib/nfc_auth.py @@ -160,7 +160,8 @@ def _nfc_service_moid(si: vim.ServiceInstance) -> str: match = _NFC_SERVICE_MOID_RE.search(body) if response.status != 200 or not match: raise RuntimeError( - f"RetrieveInternalContent (status {response.status}) had no nfcService moref" + f"RetrieveInternalContent (status {response.status}) " + "had no nfcService moref" ) return match.group(1)