Add openvixdisklib as an open NBD replacement for VMware VDDK.

VDDK is no longer publicly distributed, so this library reverse-engineers
the vSphere NFC path and exposes ConnectEx, Open, Read, and Write without
the proprietary SDK.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Lucian Petrut
2026-09-07 11:41:01 +00:00
co-authored by Cursor
commit 4ddf0c015d
31 changed files with 4089 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
# These are some examples of commonly ignored file patterns.
# You should customize this list as applicable to your project.
# Learn more about .gitignore:
# https://www.atlassian.com/git/tutorials/saving-changes/gitignore
# Compiled Python bytecode
*.py[cod]
# Log files
*.log
# JetBrains IDE
.idea/
# Unit test reports
TEST*.xml
# Generated by MacOS
.DS_Store
# Generated by Windows
Thumbs.db
# Applications
*.app
*.exe
*.war
.egg*
*.swo
*.swp
AUTHORS
ChangeLog
coriolis_provider_libvirt.egg-info/*
*__pycache__*
.coverage
.tox
.venv
.stestr
coverage.xml
cover/*
MANIFEST
nosetests.xml
coverage.html
# Avoid republishing VDDK due to licensing constraints.
# Copy the VDDK files to .vddk for reverse engineering and cross-check
# purposes, which is safe from a licensing point of view.
.vddk
# Lab credentials and VM/snapshot names for integration tests.
.test_config.yaml
+68
View File
@@ -0,0 +1,68 @@
# AI agent guidelines
## Overview
- This is a test project meant to bypass/replace VDDK, which is no longer
publicly available.
- The end goal is to have a Python library that can be used as a VDDK replacement
to retrieve VMware disk contents.
- Integration tests under `tests/integration/` are a good starting point for
interacting with the VMware NBD / NFC APIs. They inherit lab credentials and
VM/disk settings from `tests.integration.base.TestBase`. The base
class creates a temporary empty VM for the run. We can make use of
them to reverse engineer the VMware protocol, for example making various
calls, capturing the request and replies and then trying to determine the
structures used by the protocol.
- tcpdump may be used to intercept the communication with ESXI
- if deemed helpful, we may write a simple service that impersonates ESXI,
capturing the information sent by VDDK
- we should reuse pyVmomi for any operation that it supports. It's publicly
available and safe to use.
- `docs/` contains various documents that describe the reverse engineered
vmware APIs and resulting modules.
- Use `docs/reverse_engineering_procedure.md` to best describe the steps that
were undertaken to reverse engineer the Vmware APIs.
## Architecture
- The project uses Python and must be Python 3.12 compatible.
- Library code lives in the `openvixdisklib` package (`nfc_auth`, `nfc_open`,
`openvixdisklib`).
- The `.vddk` dir contains the VDDK libraries and their dependencies, including
`libvixDiskLib`. These files shouldn't be included in git commits due to
licensing constrains.
- `tests/integration/vixdisklib.py` is a Python wrapper on top of
`libvixDiskLib`, used to cross-check the replacement against native VDDK.
- Integration tests live under `tests/integration/`, use the unittest
framework, and inherit `tests.integration.base.TestBase`. Lab vCenter
credentials, datacenter, and datastore come from repo-root
`.test_config.yaml` (gitignored; sample in `README.md`). Each test
class shares a temporary empty VM with a 10 GiB disk created in
`TestBase.setUpClass` and destroyed in `tearDownClass`. Run them with
`tox -e integration` or
`.venv/bin/python -m unittest discover -s tests/integration`.
## Other rules
- AI agents should ignore folders that start with a dot, e.g. .mypy_cache, .ruff_cache, .tox
- AI agents may use the `.venv/` virtual env, it is expected to have
all project dependencies, including the `pyVmomi` vmware client
- AI agents should not generate unit or integration tests unless asked to.
- When modifying Markdown tables, the columns should be properly aligned.
- If an agent regenerates a file, avoid appending the new content, but instead
replace the file contents. We don't want duplicate definitions.
- Empty __init__.py files should not contain license headers.
- Use Linux style line endings.
- All public methods should include docstrings. Subclasses may reuse the ones
from the parent class.
- Avoid defining new methods for trivial checks such as `server.power_status == "RUNNING"`,
make the checks inline.
- Avoid removing inline comments that are still applicable.
- Agents should use type hints when the argument type can be determined.
- When writing unit tests, assert_has_calls is preferred instead of checking
the call cound and call parameters separately.
- When writing unit tests, mock decorators are preferred instead of context
managers.
- If a folder or file under this directory is inaccessible, ask for permissions.
+113
View File
@@ -0,0 +1,113 @@
# openvixdisklib
A Python replacement for VMware VDDK's `vixDiskLib` NBD path. It reads
and writes VMDK contents over vSphere NFC without the proprietary VDDK
SDK.
VIM login and inventory use [pyVmomi](https://github.com/vmware/pyvmomi).
The NFC ticket, ESXi authd handshake, and disk I/O were reverse-engineered
from VDDK 8 NBD traffic; see `docs/`.
## Status
Implemented against vCenter 8 / ESXi 8, transport `nbd`:
- `VixDiskLib_ConnectEx` (UID credentials)
- `VixDiskLib_Open` (datastore path, read-only or read-write)
- `VixDiskLib_Read`
- `VixDiskLib_Write`
Not implemented: compression open flags, CBT / allocated-block
queries, disk geometry (`DDB_GET`), encrypted disks, and direct ESXi
`ha-nfc` without vCenter `vpxa-nfc`.
## Install
```bash
python3.12 -m venv .venv
.venv/bin/pip install -e .
```
## Usage
```python
from openvixdisklib import nfc_auth
from openvixdisklib import openvixdisklib as vixdisklib
handle = vixdisklib.VixDiskLibHandle(
vixdisklib_compatibility_version="8.0")
buf = vixdisklib.get_buffer(vixdisklib.VIXDISKLIB_SECTOR_SIZE)
thumbprint = nfc_auth.get_ssl_cert_thumbprint("vcenter.example.com")
with handle.connect(
server_name="vcenter.example.com",
thumbprint=thumbprint,
username="[email protected]",
password="secret",
vmx_spec="moref=vm-1234",
transport_modes="nbd",
read_only=False) as conn:
with handle.open(conn, "[datastore] vm/vm.vmdk", flags=0) as disk:
handle.write(disk, 0, 1, buf)
handle.read(disk, 0, 1, buf)
```
Lower-level NFC helpers live in `openvixdisklib.nfc_auth` and
`openvixdisklib.nfc_open` if you need the ticket or socket without the
VDDK-shaped handle.
## Layout
| Path | Role |
| ---------------------------------- | ------------------------------------------------------ |
| `openvixdisklib/openvixdisklib.py` | Drop-in handle (`connect` / `open` / `read` / `write`) |
| `openvixdisklib/nfc_auth.py` | VIM login, NFC ticket, authd on 902 |
| `openvixdisklib/nfc_open.py` | Classic NFC handshake, AIO open, sector read/write |
| `tests/integration/` | Live unittest suite against a lab vCenter |
| `tests/integration/vixdisklib.py` | Native VDDK wrapper used only to cross-check |
| `docs/` | Protocol notes and reverse-engineering steps |
VDDK shared libraries, if present for cross-check, belong in `.vddk/`
(gitignored). They are not required to use `openvixdisklib`.
## Tests
Lab connection settings live in `.test_config.yaml` at the repo root
(gitignored). Copy:
```yaml
host: vcenter.example.com
port: 443
username: [email protected]
password: secret
allow_untrusted: true
datacenter: Datacenter
datastore: datastore0
```
`TestBase.setUpClass` creates an empty VM with a 10 GiB thin disk on
that datastore and tears it down in `tearDownClass`. Tests write known
patterns and read them back.
```bash
tox -e integration
# or
.venv/bin/python -m unittest discover -s tests/integration
```
VDDK cross-check tests skip when `libvixDiskLib` is not loadable from
`.vddk`. `tox -e integration` sets `LD_LIBRARY_PATH` to that directory
and clears `LD_PRELOAD`. For a direct unittest run, do the same.
Lint and typecheck: `tox -e pep8`, `tox -e mypy`.
## Documentation
| Document | Contents |
| --------------------------------------- | -------------------------------- |
| `docs/nfc_auth.md` | Ticket SOAP and authd handshake |
| `docs/nfc_open.md` | Classic NFC and AIO open |
| `docs/nfc_read.md` | AIO IO / `VixDiskLib_Read` |
| `docs/nfc_write.md` | AIO IO / `VixDiskLib_Write` |
| `docs/ssl_hook.md` | TLS intercept used for capture |
| `docs/reverse_engineering_procedure.md` | How the protocol was recovered |
+265
View File
@@ -0,0 +1,265 @@
# VDDK NFC authentication
This document records how VMware VDDK authenticates for NBD/NFC disk
access, and how the Python replacement in `openvixdisklib/nfc_auth.py`
reproduces that path. Findings come from VDDK 8.0.2 libraries
(`libvixDiskLib`, `libvddkVimAccess`, `libvim-types`), live SOAP calls
against vCenter
8.0.1, and a TLS intercept of `VixDiskLib_ConnectEx` / `VixDiskLib_Open`.
The steps used to obtain those findings are in
`docs/reverse_engineering_procedure.md`.
The goal of this stage is authentication only: a logged-in VIM session
plus an authd TLS socket that has completed `200 Connect`. Opening a
VMDK and reading sectors is `docs/nfc_open.md`.
## Mapping from VDDK
The VDDK wrapper in `tests/integration/vixdisklib.py` calls
`VixDiskLib_ConnectEx` with UID credentials and `VixDiskLib_Open` on a
datastore path. VDDK does **not** send the vCenter username and
password to ESXi port 902. It:
1. Logs into vCenter over HTTPS 443 (SOAP / `urn:vim25`).
2. Asks vCenter for a one-time NFC ticket.
3. Connects to the ESXi **authd** daemon on TCP 902, upgrades to TLS,
and presents that ticket.
| VDDK call | What actually happens |
| --------------------------------- | ---------------------------------------------------------- |
| `VixDiskLib_InitEx` | Load plugins, SSL, logging |
| `VixDiskLib_ConnectEx` | SOAP `SessionManager.Login` to vCenter |
| `VixDiskLib_Open` (read-only) | `NfcGetVmFiles` ticket, then authd handshake, then NFC I/O |
| `VixDiskLib_Open` (read-write) | `NfcRandomAccessOpenDisk` ticket (disk key + host) |
| `transport_modes="nbd"` | NBD over NFC (`vpxa-nfc://...@esxi:902`) |
| `vmxSpec=moref=vm-13098` | VM managed object used as the ticket target |
| `snapshot_ref` | Not consumed by the ticket call itself |
| `VIXDISKLIB_CRED_UID` | Username/password for VIM only |
Lab topology used for capture:
- vCenter: `10.8.1.199` (VirtualCenter 8.0.1)
- VM: `vm-13098` on host `host-13001` (`10.8.1.250`)
- NFC service moref on vCenter: `nfcService`
- Authd: `10.8.1.250:902`
## Stage 1: VIM login
This is a public pyVmomi operation. Reuse `pyVim.connect.SmartConnect`
rather than crafting SOAP.
- Endpoint: `https://<vcenter>:443/sdk`
- Cookie: `vmware_soap_session`
- SOAPAction: `"urn:vim25/8.0.1.0"` (negotiated)
VDDK logs this as `Connected to VIM Server` / `Authenticating user` /
`Logged in!`. The Python replacement 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.
## Stage 2: NFC ticket
### Why this is not public pyVmomi
`vim.NfcService` is omitted from the public vim25 WSDL that pyVmomi
ships. vCenter still implements it:
- Version document: `GET /sdk/nfcServiceVersions.xml` → namespace
`urn:nfc`, version `7.0.3.2`
- Methods also accept `urn:vim25` (that is what VDDK uses)
- Well-known moref on this vCenter: `nfcService`
`ServiceManager.QueryServiceList` does **not** list NFC. The moref is
hardcoded in VDDK as `nfcService` (vCenter) or `ha-nfc` (ESXi).
`openvixdisklib/nfc_auth.py` registers the missing type with
`pyVmomi.VmomiSupport.CreateManagedType` and invokes it on the existing
SmartConnect stub, so serialization, cookies, and `HostServiceTicket`
stay in pyVmomi.
### Methods VDDK actually calls
Intercepted SOAP for a **read-only** `VixDiskLib_Open` of a datastore
path:
```xml
<NfcGetVmFiles xmlns="urn:vim25">
<_this type="NfcService">nfcService</_this>
<vm type="VirtualMachine">vm-13098</vm>
</NfcGetVmFiles>
```
No disk path, snapshot, or host is in this request. The path
(`[datastore0] ...-000007.vmdk`) is used later on the NFC channel.
A `GetVmFiles` ticket is **not** writable. Opening the same path with
NFC flags `0x1a` returns AIO error `0x0b` (`VIX_E_FILE_READ_ONLY`).
Writable `ConnectEx(readOnly=FALSE)` uses a disk-scoped ticket instead.
`libvim-types.so` maps vmodl `randomAccessOpen` to WSDL
`NfcRandomAccessOpenDisk` (same arguments as the read-only sibling):
```xml
<NfcRandomAccessOpenDisk xmlns="urn:vim25">
<_this type="NfcService">nfcService</_this>
<vm type="VirtualMachine">vm-13098</vm>
<diskDeviceKey>2000</diskDeviceKey>
<hostForAccess type="HostSystem">host-13001</hostForAccess>
</NfcRandomAccessOpenDisk>
```
A disk-scoped **read** ticket also works and returns the same
`HostServiceTicket` type:
```xml
<NfcRandomAccessOpenReadonly xmlns="urn:nfc">
<_this type="NfcService">nfcService</_this>
<vm type="VirtualMachine">vm-13098</vm>
<diskDeviceKey>2000</diskDeviceKey>
<hostForAccess type="HostSystem">host-13001</hostForAccess>
</NfcRandomAccessOpenReadonly>
```
`diskDeviceKey` is `VirtualDisk.key` from `vm.config.hardware.device`
(2000 for Hard disk 1). The replacement resolves it from the datastore
path when `open` is given a VMDK rather than a key.
### Return value: `vim.HostServiceTicket`
Public pyVmomi type. Example from this lab:
| Field | Example | Role |
| ---------------- | -------------------------------------- | ----------------------------------------- |
| `host` | `10.8.1.250` | ESXi management / NFC address |
| `port` | `902` | authd TCP port |
| `sslThumbprint` | `BE:22:58:...:76:29` | SHA-1 of the ESXi TLS cert |
| `service` | `vpxa-nfc` | authd `PROXY` argument |
| `serviceVersion` | `1.1` | NFC hosted by hostd (ESX 3.0+ convention) |
| `sessionId` | `52cdebc5-b7ee-359a-1dec-76f0bc105ac5` | One-time authd `SESSION` token |
Tickets are single-use. Calling `GetVmFiles` twice issues two tickets;
only the one presented to authd is consumed.
### Other NfcService methods seen in VDDK
WSDL names are prefixed with `Nfc`. The vmodl names (from
`libvim-types.so`) include:
| WSDL name | Parameters (observed / from C++) | Notes |
| ----------------------------- | -------------------------------------- | ------------------------------ |
| `NfcGetVmFiles` | `vm` | VDDK read-only Open path |
| `NfcRandomAccessOpenReadonly` | `vm`, `diskDeviceKey`, `hostForAccess` | Disk-scoped read ticket |
| `NfcRandomAccessOpenDisk` | `vm`, `diskDeviceKey`, `hostForAccess` | Disk-scoped read-write ticket |
| `NfcGetServerNfcLibVersion` | `hostForAccess` | Lab returned `11` |
| `NfcFileManagement` | requires `ds` (datastore) | File copy, not NBD |
| `NfcSystemManagement` | host moref | Not used for disk open |
`NfcGetServerNfcLibVersion` without `hostForAccess` fails with
`A specified parameter was not correct: hostForAccess`. Using moref
`ha-nfc` on vCenter fails with `ManagedObjectNotFound`; `nfcService`
is the correct vCenter object.
## Stage 3: authd handshake (TCP 902)
authd is the VMware Authentication Daemon. Plaintext banner from ESXi
8:
```
220 VMware Authentication Daemon Version 1.10: SSL Required, ServerDaemonProtocol:SOAP, MKSDisplayProtocol:VNC , VMXARGS supported, NFCSSL supported/t, SHA256 supported
```
SSL is required. Sending commands before `wrap_socket` closes the
connection. After TLS there is **no** `USER` / `PASS` when the client
holds a vCenter NFC ticket.
### Sequence captured from VDDK
VDDK log line immediately before the socket:
```
Using proxy/session authentication, sessionId=..., useSSL=0
Plain-text connection is deprecated; use SSL to connect to NFC server
```
`useSSL=0` does **not** mean skip TLS on 902. It means skip a second
NFCSSL wrap after authd TLS (`THUMBPRINT_SHA2 PlainText`). The
management channel is still TLS.
Intercepted writes/reads after the TLS handshake:
```
C -> SESSION <sessionId>\r\n
C -> BANNER \r\n
S -> 220 VMware Authentication Daemon Version 1.10: ...\r\n
C -> THUMBPRINT_SHA2 PlainText\r\n
S -> 200 <SHA-256 thumbprint with colons>\r\n
C -> PROXY vpxa-nfc\r\n
S -> 200 Connect ha-nfc\r\n
```
Notes:
- `SESSION` does not get a reply of its own. Waiting for a line after
`SESSION` looks like a hang.
- `BANNER` is the 7-byte command `BANNER` plus a trailing space. That
space is part of the token; authd strips spaces when matching some
commands, so `THUMBPRINT_SHA2 <colon-thumbprint>` is parsed as one
token and returns `501 Invalid arguments`. `PlainText` has no extra
spaces/colons and is the argument VDDK sends.
- `PROXY` uses `ticket.service` (`vpxa-nfc` via vCenter). The success
line names the host-side NFC endpoint (`ha-nfc`).
- After `200 Connect`, the socket speaks binary NFC (not documented
here).
### Commands that are not used for this ticket type
authd also implements FTP-style `USER` / `PASS` (and `XPAS`). Those
are for local ESXi credentials. With a vCenter ticket:
| Attempt | Result |
| -------------------------------------------- | --------------------------------------- |
| `USER` / `PASS` (vCenter account) | `530 Login incorrect` |
| `USER *` / `PASS <sessionId>` | `530 Login incorrect` |
| `USER <sessionId>` / `PASS <sessionId>` | `530 Login incorrect` |
| `SESSIONID <sessionId>` | `530 Please login with USER and PASS` |
| `CONNECT_VPXA <sessionId>` (after TLS) | `530 Please login with USER and PASS` |
| `SESSION <sessionId>` then wait for a reply | No line until `BANNER` / `PROXY` follow |
`THUMBPRINT` / `THUMBPRINT_SHA2` with the SHA-1 ticket thumbprint as
argument is not what VDDK sends. The SHA-1 value is for verifying the
TLS certificate, not for the `THUMBPRINT_SHA2` command.
## Python replacement
| Piece | Module | Reuses pyVmomi? |
| -------------------- | ---------------------------------------- | ---------------------------------- |
| VIM login | `openvixdisklib.nfc_auth.connect_vim` | Yes — `SmartConnect` |
| VM / host lookup | `vim.VirtualMachine` | Yes |
| `HostServiceTicket` | return type of ticket call | Yes — public data object |
| NFC ticket | `openvixdisklib.nfc_auth.get_nfc_ticket` | Same stub; type registered locally |
| authd TLS + commands | `openvixdisklib.nfc_auth.connect_authd` | No public API |
| End-to-end | `openvixdisklib.nfc_auth.authenticate` | `NfcAuthSession` |
Management SHA-1 thumbprints are read with
`openvixdisklib.nfc_auth.get_ssl_cert_thumbprint` (stdlib `ssl` and
`hashlib`; no pyOpenSSL). Integration tests call that instead of
hard-coding the lab certificate.
Run:
```bash
.venv/bin/python -m unittest tests.integration.test_nfc_auth
```
The test completes VIM login and the authd handshake (`200 Connect`)
and asserts an established TLS socket on `ticket.host:ticket.port`.
## What comes after authentication
Authentication stops at `200 Connect ha-nfc`. Opening the VMDK and
reading or writing sectors is documented in `docs/nfc_open.md` and
implemented in `openvixdisklib/nfc_open.py`. The datastore path is
consumed there (and, for writes, as `diskDeviceKey` on the ticket).
+226
View File
@@ -0,0 +1,226 @@
# VDDK NFC disk open
This document records how VMware VDDK opens a VMDK over NBD/NFC after
the authd handshake in `docs/nfc_auth.md`, and how
`openvixdisklib/nfc_open.py` reproduces that path. Findings come from
VDDK 8.0.2 verbose logs
(`vixDiskLib.nfc.LogLevel=4`) plus an `LD_PRELOAD` intercept of
`write` / `read` on the ESXi:902 file descriptor. Capture method:
`docs/reverse_engineering_procedure.md`.
Authentication is already done: VIM login, NFC ticket (`NfcGetVmFiles`
for read-only, `NfcRandomAccessOpenDisk` for write), TLS to authd,
`SESSION` / `BANNER` / `THUMBPRINT_SHA2 PlainText` / `PROXY`. This
stage starts at `200 Connect ha-nfc` and ends with an open file handle
that can read and write sectors. Flags `0x1a` require the writable
ticket; the same flags on a `GetVmFiles` ticket fail with
`VIX_E_FILE_READ_ONLY`.
## Mapping from VDDK
| VDDK call / log | Wire effect |
| ---------------------------------------------------- | -------------------------------------------------------- |
| `VixDiskLib_Open` | Ticket + authd (see `nfc_auth.md`), then this protocol |
| `NBD_ClientOpen` `vpxa-nfc://[ds] path.vmdk@esxi:902` | Datastore path is the NFC open argument, not the ticket |
| `useSSL=0` | NFC bytes are raw TCP, not `SSL_write` |
| `NfcProcessSessionParams` flags `0x3` | Classic 264-byte session messages |
| `SendConnectionDataMsg` payloadInfo 4 and 7 | Client name `vddk` (4) and opId `nbdmode` (7) |
| Server version 11 | Classic version message; 11 on this ESXi 8 lab |
| `NfcAio_OpenSession` | AIO framing after the classic handshake |
| `NfcUtil_PrintFileInfoOpenFlag` `NFC_DISK` `0x1e` | `NFC_AIO_MSG_OPEN_FILE` (read-only) |
| Open without `VIXDISKLIB_FLAG_OPEN_READ_ONLY` | `OPEN_FILE` flags `0x1a` (read-write) |
| `VixDiskLib_Read` / `VixDiskLib_Write` | `NFC_AIO_MSG_IO` + sector bytes |
`snapshot_ref` is still not on the wire. Integration tests pass the
flat VMDK created with the temporary lab VM.
## After PROXY: plaintext on the TLS fd
`THUMBPRINT_SHA2 PlainText` tells authd not to wrap NFC in a second
TLS session. VDDK logs `useSSL=0` and “Plain-text connection is
deprecated”.
On the wire that means:
1. Authd commands stay inside the original TLS session (`SSL_write` /
`SSL_read`).
2. After `200 Connect ha-nfc`, VDDK calls `write(SSL_get_fd(ssl), …)`
and `read` on that same descriptor. Those buffers are NFC, not TLS
records (`0x17 0x03 …`).
3. ESXi’s `ha-nfc` side does the same: replies are plaintext NFC.
An SSL hook that only interposes `SSL_write` / `SSL_read` therefore
goes silent after PROXY. Interposing `write` / `read` and filtering
`getpeername` port 902 shows the frames.
Python must not use `SSLSocket.send` for this stage: that would
`SSL_write` and encrypt bytes the server now reads as NFC.
`nfc_open.takeover_authd_socket` dups `SSL_get_fd` and uses a raw
`socket.socket`. `unwrap()` / `SSL_shutdown` is not used; VDDK does
not send `close_notify` before NFC.
## Classic 264-byte messages
Before AIO, both peers send a **fixed 264-byte** struct, little-endian:
| Offset | Type | Meaning |
| ------ | --------- | -------------------------------------------- |
| 0 | `uint32` | Message type |
| 4 | remainder | Type-specific fields, zero-padded to 264 |
Types seen in this Open (names from `libvixDiskLib` strings matched to
the first `uint32`):
| Type | Name (inferred) | Body |
| ---- | ---------------------- | ------------------------------------------------- |
| 43 | `NFC_HANDSHAKE` | ASCII `PlainText` at offset 4 |
| 33 | `NFC_SESSION_PARAMS` | zeros |
| 36 | session-params reply | `uint32` 1 at offset 16 |
| 51 | version | `uint32` protocol version (11) at offset 4 |
| 54 | `NFC_CONNECTION_DATA` | `uint32` nameLen, `uint32` opIdLen |
| 55 | session features | `uint32` `0x3` (interruption \| switch) |
| 52 | `NFC_AIO_SESSION_OPEN` | zeros |
| 4 | `NFC_SESSION_COMPLETE` | zeros (sent on close) |
After type 54, VDDK writes the two connection-data payloads as **raw
strings**, not 264-byte frames: `vddk` then `nbdmode`. Lengths 4 and 7
are the `payloadInfo` values in the VDDK log.
Handshake order (client → server unless noted):
```
C: 43 PlainText
C: 33
S: 36
C: 51 version=11
S: 51 version=11
C: 54 nameLen=4 opIdLen=7
C: "vddk"
C: "nbdmode"
C: 55 features=3
C: 52
S: 52
```
Server version 11 is what this lab returned. VDDK logs that connection
info requires version ≥ 3.
## AIO framing
Once type 52 has been acknowledged, I/O uses a 16-byte header:
```
uint32 magic # 0xA100DA7A, wire bytes 7a da 00 a1
uint32 type # NfcAioSendMessage "type ="
uint32 size # payload bytes that follow the header
uint32 opId # monotonic, starting at 0
```
Then `size` bytes of payload. Variable-length extras (VMDK path, DDB
key name, read data) are **separate** `write`/`read` calls after that
payload, not counted in `size`.
The server echoes the same header (`magic`, `type`, `size`, `opId`)
and a payload of `size` bytes.
Magic mismatch is the `invalid msg hdr magic` string in VDDK. Type 1
is `NFC_AIO_MSG_ERROR`.
AIO types used for Open / Read / Close, correlated with the consecutive
`NFC_AIO_MSG_*` string table and VDDK logs:
| Type | Name | Payload size | Extra on the wire |
| ---- | -------------------- | ------------ | --------------------------------------------- |
| 2 | `OPEN_SESSION` | 16 | |
| 9 | `SET_SOCK_OPTS` | 12 | |
| 22 | `SET_RES_POOL` | 4 | |
| 4 | `OPEN_FILE` | 60 | path string |
| 11 | `DDB_GET` | 16 | key name (VDDK only) |
| 7 | `IO` | 44 | sector bytes (read reply / write request) |
| 5 | `CLOSE_FILE` | 8 | |
| 3 | `CLOSE_SESSION` | 4 | |
`opId` increases by one per client message. Replies reuse the request
`opId`.
VDDK Open also issues several `DDB_GET` queries (`resumeConsolidateSector`,
`isDigest`, `iofilters`, …). The server answered “key is not found”
(16 zero bytes) on this unencrypted disk. They are not required to
obtain a file handle or to read sector 0.
### OPEN_SESSION / sockopts / resource pool
VDDK sends 16 zero bytes (`OPEN_SESSION`), 12 zero bytes
(`SET_SOCK_OPTS`; server returns send/recv buffer sizes), then
`uint32` 1 (`SET_RES_POOL`, log: “Setting Resource Pool(1)”).
### OPEN_FILE
60-byte payload, little-endian:
| Offset | Type | Value on a VDDK open |
| ------ | -------- | -------------------------------------------------------------- |
| 0 | `uint32` | Path length in bytes |
| 4 | `uint32` | 0 |
| 8 | `uint32` | 0 |
| 12 | `uint32` | 0 |
| 16 | `uint32` | `2` (`NFC_DISK`) |
| 20 | `uint32` | `0x0000001e` (read-only) or `0x1a` (read-write) |
| 24 | 36 bytes | zeros |
Immediately afterwards the client writes the path, no NUL terminator
(for example `[datastore0] ovdl-test-…/ovdl-test-….vmdk`).
Reply payload (60 bytes), fields that matter:
| Offset | Type | Meaning |
| ------ | -------- | ------------------------------- |
| 8 | `uint64` | File handle (opaque, per open) |
| 16 | `uint32` | File type (`2` = `NFC_DISK`) |
| 20 | `uint32` | Flags echoed (`0x1e` or `0x1a`) |
| 36 | `uint32` | Sector size (`512` on this VM) |
Later AIO messages pass that handle as a `uint64`.
### IO (read / write)
Sector reads and writes are `NFC_AIO_MSG_IO` (type 7). Request layout,
read fragments, and write extras are documented in `docs/nfc_read.md`
and `docs/nfc_write.md`. `NfcDisk.read` / `NfcDisk.write` match
`VixDiskLib_Read` / `VixDiskLib_Write`.
### Close
`CLOSE_FILE` (handle as `uint64`), `CLOSE_SESSION` (`uint32` 0), then
classic type 4 `NFC_SESSION_COMPLETE`.
## Python replacement
| Piece | Module |
| ----------------------------- | ----------------------------------------------- |
| VIM + authd | `openvixdisklib.nfc_auth.authenticate` |
| Dup fd, skip TLS for NFC | `openvixdisklib.nfc_open.takeover_authd_socket` |
| Handshake + AIO + OPEN_FILE | `openvixdisklib.nfc_open.open_disk` |
| Sector read / write / close | `openvixdisklib.nfc_open.NfcDisk` |
Run:
```bash
.venv/bin/python -m unittest tests.integration.test_nfc_open
```
The test opens the temporary lab VMDK, asserts an opaque handle and
`sector_size=512`, writes sector 0, and reads it back. Multi-sector
I/O: `docs/nfc_read.md`, `docs/nfc_write.md`, and
`tests/integration/test_nfc_read_write.py`.
## What is still VDDK-only
- `DDB_GET` / geometry / compression / encryption keys
- `NFC_DELTA_DISK`, change-block tracking
- Host-switch (`NFC_AIO_SWITCH_HOST_*`) and a second NFCSSL wrap
(`useSSL=1`, not what VDDK NBD used here)
- Direct ESXi `ha-nfc` without vCenter `vpxa-nfc`
Reads after open are in `docs/nfc_read.md`. Writes are in
`docs/nfc_write.md`.
+120
View File
@@ -0,0 +1,120 @@
# VDDK NFC disk read
This document records how VMware VDDK reads VMDK sectors over NBD/NFC
after the open in `docs/nfc_open.md`, and how `NfcDisk.read` in
`openvixdisklib/nfc_open.py` reproduces `VixDiskLib_Read`. Capture
method: `docs/reverse_engineering_procedure.md`.
## Mapping from VDDK
`VixDiskLib_Read(handle, startSector, numSectors, buf)` becomes one
`NFC_AIO_MSG_IO` (type 7) on the NFC socket. Units on the wire are
**bytes**, not sectors:
```
offset = startSector * sectorSize
length = numSectors * sectorSize
```
`sectorSize` is 512 from the `OPEN_FILE` reply on this lab disk.
| VDDK call | Wire effect |
| --------------------------------- | ------------------------------------------------ |
| `VixDiskLib_Read(h, 0, 1, buf)` | IO offset 0, length 512, one 512-byte fragment |
| `VixDiskLib_Read(h, 1, 1, buf)` | IO offset 512, length 512 |
| `VixDiskLib_Read(h, 0, 128, buf)` | IO length 65536 (AIO buffer size), one fragment |
| `VixDiskLib_Read(h, 0, 129, buf)` | One request of 66048; **two** reply fragments |
VDDK does **not** split a `Read` larger than 64 KiB into multiple
requests. The client sends one AIO message; the server answers with
one or more same-`opId` replies, each carrying at most
`NFC_AIO_BUFFER_SIZE` (65536) data bytes. `NfcAioInitSession` logged
that buffer size and count 4 during open.
Sparse regions are still transferred as zeros. A read of 8 sectors at
LBA 8 on this disk was 4096 zero bytes on the wire, not a skip.
## Request (44 bytes)
Little-endian, after the usual 16-byte AIO header
(`magic 0xA100DA7A`, type 7, size 44, monotonic `opId`):
| Offset | Type | VDDK `Read(start, n)` |
| ------ | -------- | ---------------------------------------------- |
| 0 | `uint64` | File handle from `OPEN_FILE` |
| 8 | `uint64` | `1` (`NFC_AIO_IO_READ`; write uses `0`) |
| 16 | `uint64` | Byte offset |
| 24 | `uint64` | Byte length |
| 32 | `uint32` | Byte length (same value) |
| 36 | `uint32` | Byte length (same value) |
| 40 | `uint32` | `0` (flags; uncompressed in this capture) |
An earlier guess that offset 36 was `NFC_DISK` (`2`) was wrong: a
1-sector VDDK read puts `512` in both `uint32` length fields. A Python
read that sent `(512, 2, 0)` still worked for one sector; the
replacement now matches VDDK.
## Reply
Each fragment is: 16-byte AIO header (same `type` and `opId`) + 44-byte
payload + `chunkLength` data bytes.
Reply payload (handle is zeroed; lengths describe this fragment):
| Offset | Type | Meaning |
| ------ | -------- | ----------------------------------------------- |
| 0 | `uint64` | `0` |
| 8 | `uint64` | `1` (read) |
| 16 | `uint64` | Byte offset of the **request** |
| 24 | `uint32` | Total request length |
| 28 | `uint32` | Fragment index (`0`, `1`, …) |
| 32 | `uint32` | This fragment’s byte length |
| 36 | `uint32` | Same as offset 32 |
| 40 | `uint32` | `0` |
When there is a single fragment, offsets 24–31 look like a `uint64`
length (index is 0). The 129-sector capture shows why they are two
`uint32`s: fragment 0 has `(66048, 0)` then chunk 65536; fragment 1
has `(66048, 1)` then chunk 512.
Read loop: receive fragments with that `opId` until the concatenated
data length equals the request. Use the `uint32` at payload offset 32
as the extra-data size for that fragment. Do not treat extra data as
part of AIO `size` (that field stays 44).
129-sector example (one client request, two server fragments):
```
C: type=7 opId=18 size=44 offset=0 length=66048
S: type=7 opId=18 size=44 index=0 chunk=65536 + 65536 data
S: type=7 opId=18 size=44 index=1 chunk=512 + 512 data
```
## Lab check
Integration tests create an empty 10 GiB thin disk, write a repeating
pattern at each captured range (including 129 sectors), and read it
back. An unwritten region is zeros.
Writes use the same 44-byte IO payload with opcode `2`; see
`docs/nfc_write.md`.
## Python replacement
`NfcDisk.read(start_sector, num_sectors)` in
`openvixdisklib/nfc_open.py`. Run:
```bash
.venv/bin/python -m unittest tests.integration.test_nfc_read_write
```
The integration test writes and then reads the captured VDDK ranges
(including a 129-sector transfer that must assemble two read
fragments).
## What is still VDDK-only
- Compression flags on the last `uint32`
- `VixDiskLib_ReadAsync` (same IO messages, different client threading)
- `VixDiskLib_QueryAllocatedBlocks` / allocation bitmaps
- `VixDiskLib_GetInfo` capacity (not required to read a known range)
+91
View File
@@ -0,0 +1,91 @@
# VDDK NFC disk write
This document records how `NfcDisk.write` in
`openvixdisklib/nfc_open.py` implements `VixDiskLib_Write` over NFC AIO.
The request layout matches the captured `VixDiskLib_Read` IO message in
`docs/nfc_read.md`. Open flags and the IO direction field were taken
from a `strace` of VDDK 8 writing one sector to a temporary 10 GiB
disk (`docs/reverse_engineering_procedure.md`).
The public `VixDiskLib_Write` prototype is in `.vddk/vixDiskLib.h`:
```
VixError VixDiskLib_Write(VixDiskLibHandle diskHandle,
VixDiskLibSectorType startSector,
VixDiskLibSectorType numSectors,
const uint8 *writeBuffer);
```
`ConnectEx(..., Bool readOnly, ...)` with `readOnly=FALSE` and `Open`
without `VIXDISKLIB_FLAG_OPEN_READ_ONLY` (that flag is `1 << 2` in the
same header) is what produces the writable NFC open below. The ticket
must be `NfcRandomAccessOpenDisk` (`docs/nfc_auth.md`); flags `0x1a`
on a `NfcGetVmFiles` ticket are rejected as `VIX_E_FILE_READ_ONLY`.
## Mapping from VDDK
Units on the wire are **bytes**, as for reads:
```
offset = startSector * sectorSize
length = numSectors * sectorSize
```
| VDDK call | Wire effect |
| ------------------------------------------- | ------------------------------------------------ |
| Open without `VIXDISKLIB_FLAG_OPEN_READ_ONLY` | `OPEN_FILE` flags `0x1a` |
| Open with `VIXDISKLIB_FLAG_OPEN_READ_ONLY` | `OPEN_FILE` flags `0x1e` (read-only) |
| `VixDiskLib_Write(h, start, n, buf)` | IO opcode `0`, then `n * 512` data bytes |
| `VixDiskLib_Read(h, start, n, buf)` | IO opcode `1` |
`0x1e` vs `0x1a` is bit `0x04`, the same value as
`VIXDISKLIB_FLAG_OPEN_READ_ONLY`. Writable opens clear that bit.
VDDK also issues several `DDB_GET` queries and a type-10
`GET_FILE_INFO` (`longContentID`) before the first write. They are not
required to write or read sectors.
## Request (44 bytes + data)
Little-endian, after the usual 16-byte AIO header
(`magic 0xA100DA7A`, type 7, size 44, monotonic `opId`):
| Offset | Type | `Write(start, n)` |
| ------ | -------- | -------------------------------------------------- |
| 0 | `uint64` | File handle from `OPEN_FILE` |
| 8 | `uint64` | `0` (`NFC_AIO_IO_WRITE`; read uses `1`) |
| 16 | `uint64` | Byte offset |
| 24 | `uint64` | Byte length |
| 32 | `uint32` | Byte length (same value) |
| 36 | `uint32` | Byte length (same value) |
| 40 | `uint32` | `0` |
Sector bytes follow the 44-byte payload and are **not** counted in AIO
`size`. VDDK sends header + payload + data in one `write()`. The
replacement may split that into two `sendall`s; TCP does not care.
The server replies with a type-7 header and a 44-byte payload for that
`opId`. There is no extra data on the write reply (unlike reads).
A 1-sector VDDK write was 572 bytes on the wire: 16 + 44 + 512.
## Client-side split
`NfcAioInitSession` advertises a 64 KiB buffer. VDDK splits writes
larger than that into 64 KiB chunks (VDDK programming guide). The
Python client does the same: several IO requests of at most
`NFC_AIO_BUFFER_SIZE` bytes, each with its own `opId`.
## Python replacement
`NfcDisk.write(start_sector, num_sectors, data)` in
`openvixdisklib/nfc_open.py`. `open_disk(..., read_only=False)` selects
flags `0x1a`. The drop-in handle exposes the same shape as VDDK:
`connect(read_only=False)`, `open` without
`VIXDISKLIB_FLAG_OPEN_READ_ONLY`, then `write`.
Integration tests create an empty 10 GiB disk, write known patterns,
and read them back (`tests/integration/test_nfc_read_write.py`,
`tests/integration/test_openvixdisklib.py`). Cross-check tests write
with VDDK and with the replacement and read with both
(`tests/integration/test_crosscheck.py`).
+281
View File
@@ -0,0 +1,281 @@
# Reverse-engineering procedure
This is the working method used to replace VDDK’s NBD path with Python.
Protocol details live in `docs/nfc_auth.md`, `docs/nfc_open.md`,
`docs/nfc_read.md`, and `docs/nfc_write.md`. The capture tool is
described in `docs/ssl_hook.md`.
This file is the **sequence of steps**, including dead ends, so later
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, transport `nbd`. Driver: `tests/integration/` (`TestBase`
creates a temporary empty VM with a 10 GiB disk in `setUpClass` and
destroys it in `tearDownClass`).
Rule from `AGENTS.md`: reuse pyVmomi for every public VIM operation.
Only reimplement what pyVmomi does not expose.
## Loop
Each unknown stage (ticket SOAP, authd, NFC binary) went through:
1. **Name it** from VDDK logs and `strings` on the bundled libraries.
2. **See it** on the wire (or prove that tcpdump cannot).
3. **Replay** the smallest working subset in Python against the lab.
4. **Write** findings into a protocol doc and keep the hook out of the
library path.
Do not skip (2). Log lines such as `SESSIONID` or `useSSL=0` named the
wrong wire command until the intercept existed.
## Lab and artifacts
| Item | Where / value |
| ----------------------- | -------------------------------------------------------------------- |
| VDDK 8.0.2 | `.vddk/` (`libvixDiskLib`, `libvddkVimAccess`, `libvim-types`) |
| pyVmomi | `.venv` |
| Known-good VDDK client | `tests/integration/test_vddk.py` / `tests/integration/vixdisklib.py` |
| Verbose NFC logs | `vixDiskLib.nfc.LogLevel=4` in a temp VDDK config |
| ctypes Open+Read driver | `/tmp/vddk_open_trace.py` (not in the library) |
| SSL / `write` hook | `/tmp/sslhook.c` → `/tmp/sslhook.so` |
Always set `LD_LIBRARY_PATH` to `.vddk/` so VDDK uses its own
`libssl.so.3`. Unset `LD_PRELOAD` before running the Python replacement;
a leftover `write` hook will crash pyVmomi’s TLS.
## Step 1 — Map the public VDDK calls
`tests/integration/test_vddk.py` is the specification of what
“success” looks like: login, open the temporary VM’s VMDK, write a
known pattern, read it back.
Turn on VDDK verbose logging around `InitEx` / `ConnectEx` / `Open`. The
logs split the work that the Python API hides:
- `ConnectEx` → VIM login only.
- `Open` → NFC ticket, authd, NFC handshake, AIO open, then I/O.
- `transport_modes=nbd` → URL form `vpxa-nfc://[ds] path.vmdk@esxi:902`.
- `snapshot_ref` does not appear on the ticket SOAP call.
That mapping is the table at the top of `docs/nfc_auth.md`. It tells you
which stage to reverse next and which arguments belong there (VM moref
on the ticket, VMDK path on NFC `OPEN_FILE`).
## Step 2 — Strings and pyVmomi before any capture
`strings -a` on `.vddk/*.so` produced candidate tokens before a single
packet was decoded:
- SOAP: `NfcService`, `NfcGetVmFiles`, `nfcService`, `ha-nfc`,
`HostServiceTicket`.
- authd: `SESSION`, `BANNER`, `THUMBPRINT_SHA2`, `PROXY`, `USER`,
`PASS`, `SSL Required`.
- NFC: `NFC_HANDSHAKE`, `NFC_CONNECTION_DATA`, `NFC_AIO_MSG_*`,
`NFC_DISK`.
Then check whether pyVmomi already has the type:
```python
from pyVmomi import vim
hasattr(vim, "NfcService") # False
hasattr(vim, "HostServiceTicket") # True
```
`ServiceManager.QueryServiceList` on the live vCenter does **not** list
NFC. `GET /sdk/nfcServiceVersions.xml` does (`urn:nfc` 7.0.3.2). The
moref is hardcoded in VDDK (`nfcService` on vCenter, `ha-nfc` on ESXi).
Anything public (`SmartConnect`, `vim.VirtualMachine`,
`HostServiceTicket`) stays in pyVmomi. Missing managed types are
registered with `CreateManagedType` on the same SOAP stub so cookies
and serialization are not reimplemented.
## Step 3 — Confirm tcpdump is the wrong tool for TLS stages
tcpdump on 443 and 902 shows TLS records only. That is enough to prove
“something talks to vCenter then to ESXi:902”, and not enough for SOAP
bodies, authd lines, or NFC headers.
VDDK logs name functions and AIO `opId` / `type` / `size`. They do not
give magic numbers, path placement, or command spacing (`BANNER \r\n`).
An ESXi-impersonating service was considered (`AGENTS.md`) and not
needed: the lab answers VDDK, so capturing the real client is simpler
than simulating the server.
## Step 4 — Interpose OpenSSL (authd and SOAP)
VDDK 8.0.2 still calls `SSL_write` / `SSL_read`. A small `LD_PRELOAD`
library logs those buffers as hex, tagged with the `SSL *` pointer.
Run a minimal ctypes program (`InitEx`, `ConnectEx`, `Open`, `Read`)
under:
```bash
export LD_LIBRARY_PATH=…/.vddk
export LD_PRELOAD=/tmp/sslhook.so
export SSLHOOK_LOG=/tmp/sslhook-open.log
python /tmp/vddk_open_trace.py
```
Parse offline:
1. Concatenate adjacent same-direction records (authd `SSL_read` is
often one byte).
2. Split by `SSL *`. vCenter HTTPS contains `POST /sdk` and SOAP.
ESXi:902 contains `SESSION` / `PROXY`.
3. An early hook without the pointer mixed both streams; always tag.
The vCenter stream identified the ticket as `NfcGetVmFiles` with moref
`nfcService` and `xmlns="urn:vim25"` (not a guess from strings alone).
The ESXi stream gave the authd command order, including the trailing
space on `BANNER` and `THUMBPRINT_SHA2 PlainText`.
Hook implementation notes: `docs/ssl_hook.md`.
## Step 5 — Probe SOAP, then replay only what VDDK sends
With a SmartConnect session, raw SOAP posts were used to learn
parameter names and which moref vCenter accepts:
- `ha-nfc` on vCenter → `ManagedObjectNotFound`.
- `NfcGetServerNfcLibVersion` without `hostForAccess` → invalid
argument; with a host moref → `11`.
- `NfcGetVmFiles(vm)` → `HostServiceTicket` (VDDK read-only Open).
- `NfcRandomAccessOpenReadonly(vm, diskDeviceKey, host)` → same ticket
type, disk-scoped read.
- `NfcRandomAccessOpenDisk(vm, diskDeviceKey, host)` → writable
ticket. `GetVmFiles` plus `OPEN_FILE` flags `0x1a` fails with
`VIX_E_FILE_READ_ONLY` (`0x0b`).
The replacement registers those methods and calls them through pyVmomi.
It does not ship a hand-rolled SOAP client for login or tickets.
## Step 6 — Probe authd; record dead ends
Plaintext banner (`220 … SSL Required`), then `ssl.wrap_socket`.
Commands before TLS drop the connection.
vCenter UID/password are **not** sent to port 902. Attempts that failed
and must not be retried for this ticket type:
| Attempt | Result |
| ------------------------------------- | ------------------------------------- |
| `USER` / `PASS` (vCenter account) | `530 Login incorrect` |
| `USER` / `PASS` with `sessionId` | `530 Login incorrect` |
| `SESSIONID <sessionId>` | `530 Please login with USER and PASS` |
| `CONNECT_VPXA` after TLS | `530 Please login with USER and PASS` |
| Wait for a reply after `SESSION` | Hang until `BANNER` / `PROXY` follow |
| `THUMBPRINT_SHA2` with SHA-1 digest | `501 Invalid arguments` |
The working sequence is in `docs/nfc_auth.md`. `useSSL=0` in the VDDK
log means skip a **second** NFCSSL wrap, not skip TLS on 902.
Replay: `openvixdisklib/nfc_auth.py` /
`tests/integration/test_nfc_auth.py`. Stop at `200 Connect`.
## Step 7 — NFC binary: extend the hook to `write` / `read`
After `PROXY`, `SSL_write` on the ESXi `SSL *` goes silent. VDDK logs
`useSSL=0` / “plain-text connection is deprecated” and then NFC
function names. The bytes are `write(SSL_get_fd(ssl), …)` / `read` on
peer port 902.
The hook was extended to those syscalls, filtered with `getpeername`
port 902, and mutex-locked (VDDK is multi-threaded). Skip TLS records
(`16 03` / `17 03`) left over from the authd phase.
Correlate each frame with the verbose log line that has the same
`type` and `size` (`NfcAioSendMessage: opId = … type = … size = …`).
Name the types from the consecutive `NFC_AIO_MSG_*` string table in
`libvixDiskLib.so`. Lengths 4 and 7 on the connection-data message are
the ASCII strings `vddk` and `nbdmode` sent in the next two writes.
Classic NFC uses a 264-byte padded struct; AIO uses a 16-byte header
(`magic 0xA100DA7A`) plus payload; path / DDB key / sector data are
extra writes not included in `size`.
## Step 8 — Replay the smallest subset, then compare to VDDK
Python must **dup the authd fd** and send NFC as raw TCP.
`SSLSocket.send` would encrypt; `unwrap()` would `SSL_shutdown`. VDDK
does neither.
`openvixdisklib/nfc_open.py` replays handshake + AIO `OPEN_SESSION` /
sockopts / resource pool / `OPEN_FILE`. VDDK’s extra `DDB_GET` keys
were omitted once a file handle was enough to read. Proof of open:
`tests/integration/test_nfc_open.py`.
Do not copy every VDDK message. Copy what the server requires for the
Python API you are replacing.
## Step 9 — Vary `VixDiskLib_Read` until IO fields stop moving
A single-sector read is not enough to decode `NFC_AIO_MSG_IO`. Drive
VDDK with several `(startSector, numSectors)` pairs in one Open
(including `n=128` = 64 KiB and `n=129`) under the `write`/`read` hook.
What that comparison showed:
- Wire units are bytes (`offset = start * 512`, `length = n * 512`).
- Request size stays 44; data is extra after the payload.
- VDDK sends **one** request even when `length > 65536`. The server
replies with several type-7 messages that share `opId`, each with a
chunk length at payload offset 32 (max 65536).
- Treating offset 36 as `NFC_DISK` (`2`) was a 1-sector coincidence;
VDDK repeats the byte length there.
- Zeros on the wire are real transferred zeros, not a sparse skip.
Replay: `NfcDisk.read` loops on fragments until `length` bytes arrive.
Proof: `tests/integration/test_nfc_read_write.py` writes a known pattern
(including a 129-sector read that must assemble two fragments) and
checks the bytes that came back.
## Step 10 — Writes from the same IO message
`VixDiskLib_Write` uses the same 44-byte `NFC_AIO_MSG_IO` layout as
read. The direction field at offset 8 is `0` instead of `1`, and the
sector bytes are sent after the payload (like the path on
`OPEN_FILE`). Writable `OPEN_FILE` flags are `0x1a` (the captured
read-only flags `0x1e` with bit `0x04` cleared, matching
`VIXDISKLIB_FLAG_OPEN_READ_ONLY` in `.vddk/vixDiskLib.h`).
Writable `OPEN_FILE` still failed with `VIX_E_FILE_READ_ONLY` until
the ticket switched from `NfcGetVmFiles` to `NfcRandomAccessOpenDisk`
(`libvim-types.so`: vmodl `randomAccessOpen` ↔ WSDL
`NfcRandomAccessOpenDisk`). Integration tests create a temporary empty
10 GiB VM for the run so writes cannot land on other lab disks.
The Python client splits writes larger than 64 KiB; it does not send a
single oversized write the way VDDK sends an oversized read. Details:
`docs/nfc_write.md`. Proof: write then read in
`tests/integration/test_nfc_read_write.py` and the VDDK cross-check in
`tests/integration/test_crosscheck.py`.
## What to write down
After a stage works:
| Document | Contents |
| --------------------------------------- | --------------------------------------------- |
| `docs/nfc_auth.md` | Ticket SOAP + authd wire format |
| `docs/nfc_open.md` | Classic NFC + AIO open |
| `docs/nfc_read.md` | AIO IO / `VixDiskLib_Read` |
| `docs/nfc_write.md` | AIO IO / `VixDiskLib_Write` |
| `docs/ssl_hook.md` | Capture tool only |
| `docs/reverse_engineering_procedure.md` | This procedure (update when the method changes) |
Keep the hook and ctypes driver under `/tmp`. They are not part of the
replacement library.
## Next stages (same procedure)
Not yet reversed, same loop as above:
- `DDB_GET` / disk geometry, compression, encrypted disks
- `NFC_DELTA_DISK`, CBT / `QueryAllocatedBlocks`
- `VixDiskLib_GetInfo` capacity
- Host-switch AIO messages
- `useSSL=1` (second NFCSSL wrap)
- Direct ESXi `ha-nfc` without vCenter `vpxa-nfc`
+145
View File
@@ -0,0 +1,145 @@
# SSL hook for VDDK protocol capture
VDDK’s NBD path is TLS end to end: SOAP to vCenter on 443, then authd/NFC
to ESXi on 902. Packet captures on those ports are ciphertext, so they
cannot show command names, tickets, or NFC frames.
This project used a small `LD_PRELOAD` library (`sslhook.c`, built to
`sslhook.so`) to log OpenSSL plaintext while a ctypes wrapper ran
`VixDiskLib_ConnectEx` / `VixDiskLib_Open`. The authd sequence in
`docs/nfc_auth.md` was recovered from that log, not from VDDK source.
The hook is a reverse-engineering aid. It is not part of the Python
NFC client.
## Why not tcpdump or VDDK logs
| Approach | What it shows | Gap |
| -------------------------------- | -------------------------------------------------- | ------------------------------------------------ |
| tcpdump on 443 / 902 | TLS records | No SOAP bodies, no authd lines, no NFC frames |
| `vixDiskLib.nfc.LogLevel=4` | Function names, `opId` / `type` / `size` | Not the bytes on the wire |
| Strings in `libvixDiskLib.so` | Command tokens (`SESSION`, `PROXY`, `BANNER`) | Not order, spacing, or replies |
| SSL hook on `SSL_write`/`read` | Exact buffers before encrypt / after decrypt | Must split connections and reassemble 1-byte I/O |
VDDK logs were still useful to *name* AIO message types after the hex
dump showed `type` and `size`. The hook supplied the actual framing.
## How `LD_PRELOAD` interposition works
The hook exports `SSL_write` and `SSL_read` with OpenSSL’s signatures.
When the process starts with `LD_PRELOAD=/path/sslhook.so`, the dynamic
linker binds VDDK’s calls to those symbols instead of `libssl`.
Each wrapper:
1. Resolves the real OpenSSL function with `dlsym(RTLD_NEXT, ...)`.
2. Logs the plaintext buffer.
3. Calls the real function so the session is unchanged.
```
VixDiskLib --> SSL_write (hook) --> log hex --> SSL_write (libssl)
VixDiskLib <-- SSL_read (hook) <-- log hex <-- SSL_read (libssl)
```
`SSL_write` logs **before** encryption. `SSL_read` calls OpenSSL first,
then logs `n` decrypted bytes when `n > 0`.
## Implementation notes
The working copy lived under `/tmp` during capture (`/tmp/sslhook.c`).
Behavior that mattered for parsing:
- Log path from `SSLHOOK_LOG`, default `/tmp/sslhook-open.log`.
- Unbuffered writes (`_IONBF`) so a crash still leaves a complete file.
- Each record tagged with the `SSL *` pointer so vCenter HTTPS and
ESXi:902 are separable. An earlier version omitted the pointer and
mixed both streams into one timeline.
- Payload stored as hex, not mixed ASCII, so binary NFC frames stay
unambiguous.
Record layout:
```
==== W 0x7f8a1234 46 ====
53455353494f4e2035326364656263352d...0d0a
```
| Field | Meaning |
| ------- | ---------------------------------------------------- |
| `W`/`R` | Write (plaintext to encrypt) or read (decrypted) |
| `%p` | `SSL *` for this socket |
| length | Byte count of this OpenSSL call |
| hex | Buffer contents |
`SSL_read` is often **one byte per call**. A 220 banner is therefore
dozens of `R 1` records. Adjacent records with the same `SSL *` and
direction must be concatenated before parsing lines or NFC headers.
OpenSSL 3 also has `SSL_write_ex` / `SSL_read_ex`. This VDDK 8.0.2
build still used `SSL_write` / `SSL_read`, so those two symbols were
enough. If a later library switches APIs, the hook would need matching
wrappers.
## How it was used for authd
A minimal ctypes program loaded `libvixDiskLib.so`, called
`VixDiskLib_InitEx`, `ConnectEx` (UID to vCenter, `nbd`), and `Open` on
the lab VMDK. The process was started as:
```bash
export LD_LIBRARY_PATH=/home/ubuntu/workspace/vmware_nbd_tests/.vddk
export LD_PRELOAD=/tmp/sslhook.so
export SSLHOOK_LOG=/tmp/sslhook-open.log
python /tmp/vddk_open_trace.py
```
`LD_LIBRARY_PATH` is required so VDDK uses its bundled `libssl.so.3`.
`LD_PRELOAD` still interposes that copy.
After the run, records were grouped by `SSL *`. The ESXi connection is
the one whose writes contain `SESSION ` and `PROXY `. Concatenating
that stream after the TLS handshake produced:
```
C -> SESSION <sessionId>\r\n
C -> BANNER \r\n
S -> 220 VMware Authentication Daemon Version 1.10: ...\r\n
C -> THUMBPRINT_SHA2 PlainText\r\n
S -> 200 <SHA-256 thumbprint>\r\n
C -> PROXY vpxa-nfc\r\n
S -> 200 Connect ha-nfc\r\n
```
The same log also showed the SOAP `NfcGetVmFiles` body on the vCenter
`SSL *` (`xmlns="urn:vim25"`, moref `nfcService`). That is how the
ticket call was identified as `NfcGetVmFiles` rather than guessing
from `libvim-types` strings alone.
Details that only the hex dump made obvious:
- `BANNER` includes a trailing space (`BANNER \r\n`).
- `THUMBPRINT_SHA2` argument is the literal `PlainText`, not the
ticket SHA-1 thumbprint.
- `SESSION` has no reply; waiting for a line after it looks like a hang.
- Ticket `sessionId` is the UUID string on the `SESSION` line.
Those facts are written up in `docs/nfc_auth.md`. The Python client in
`openvixdisklib/nfc_auth.py` replays this sequence; it does not use
the hook.
After `200 Connect`, NFC is **not** on `SSL_write`. VDDK uses
`write`/`read` on `SSL_get_fd` (`useSSL=0`). A later hook that also
interposed those syscalls, filtered to peer port 902, recovered the
264-byte handshake and AIO frames in `docs/nfc_open.md`. TLS record
bytes (`16 03` / `17 03`) on that fd are the authd phase and must be
skipped.
## Limits
- The hook sees every OpenSSL client in the process (VDDK and, if the
same interpreter is used, anything else linked to OpenSSL). Filter by
`SSL *`.
- It does not decode TLS handshakes, certificates, or SOAP envelopes;
that is done offline on the hex log.
- It must not ship in a production VDDK replacement. Keep it out of
the library path used by `openvixdisklib/nfc_auth.py`.
View File
+371
View File
@@ -0,0 +1,371 @@
# Copyright 2026 Cloudbase Solutions Srl
# All Rights Reserved.
"""VDDK-compatible vSphere NFC authentication.
VixDiskLib_ConnectEx / Open authenticate in two stages:
1. SOAP login to vCenter (or ESXi) and an internal NfcService call that
returns a one-time vim.HostServiceTicket.
2. A TLS session to the ESXi authd daemon on TCP 902, completed with the
ticket's sessionId and service name.
pyVim / pyVmomi are used for every public VIM operation (login, inventory,
HostServiceTicket). NfcService is not in the public WSDL, so it is registered
with pyVmomi's type system and invoked through the same SOAP stub.
"""
from __future__ import annotations
import hashlib
import socket
import ssl
from typing import Optional
from pyVim.connect import Disconnect, SmartConnect
from pyVmomi import vim
from pyVmomi.VmomiSupport import CreateManagedType, F_OPTIONAL, GetVmodlType
NFC_SERVICE_MOID = "nfcService"
AUTHD_DEFAULT_PORT = 902
_NFC_TYPES_REGISTERED = False
def _ssl_client_context(verify: bool = True) -> ssl.SSLContext:
"""Return a client TLS context built with public ``ssl`` APIs."""
context = ssl.create_default_context()
if not verify:
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
return context
def _register_nfc_types() -> None:
"""Register internal vim.NfcService methods on the pyVmomi type map."""
global _NFC_TYPES_REGISTERED
if _NFC_TYPES_REGISTERED:
return
try:
GetVmodlType("vim.NfcService")
_NFC_TYPES_REGISTERED = True
return
except Exception:
pass
CreateManagedType(
"vim.NfcService",
"NfcService",
"vmodl.ManagedObject",
"vim.version.version1",
[],
[
("getVmFiles", "NfcGetVmFiles", "vim.version.version1",
(("vm", "vim.VirtualMachine", "vim.version.version1", 0, None),),
(0, "vim.HostServiceTicket", "vim.HostServiceTicket"), None, None),
("randomAccessOpen", "NfcRandomAccessOpenDisk",
"vim.version.version1",
(("vm", "vim.VirtualMachine", "vim.version.version1", 0, None),
("diskDeviceKey", "int", "vim.version.version1", 0, None),
("hostForAccess", "vim.HostSystem", "vim.version.version1",
F_OPTIONAL, None),),
(0, "vim.HostServiceTicket", "vim.HostServiceTicket"), None, None),
("randomAccessOpenReadonly", "NfcRandomAccessOpenReadonly",
"vim.version.version1",
(("vm", "vim.VirtualMachine", "vim.version.version1", 0, None),
("diskDeviceKey", "int", "vim.version.version1", 0, None),
("hostForAccess", "vim.HostSystem", "vim.version.version1",
F_OPTIONAL, None),),
(0, "vim.HostServiceTicket", "vim.HostServiceTicket"), None, None),
("getServerNfcLibVersion", "NfcGetServerNfcLibVersion",
"vim.version.version1",
(("hostForAccess", "vim.HostSystem", "vim.version.version1",
0, None),),
(0, "int", "int"), None, None),
],
)
_NFC_TYPES_REGISTERED = True
def nfc_service(si: vim.ServiceInstance) -> vim.NfcService:
"""Return the vCenter/ESXi NfcService managed object on ``si``'s SOAP stub.
Args:
si: An authenticated ServiceInstance from pyVim.connect.SmartConnect.
"""
_register_nfc_types()
nfc_cls = GetVmodlType("vim.NfcService")
return nfc_cls(NFC_SERVICE_MOID, si._stub)
def connect_vim(
host: str,
username: str,
password: str,
port: int = 443,
thumbprint: Optional[str] = None,
allow_untrusted: bool = False) -> vim.ServiceInstance:
"""Login to vCenter or ESXi using pyVim.connect.SmartConnect.
Args:
host: vCenter or ESXi hostname/IP.
username: VIM user name.
password: VIM password.
port: HTTPS port, usually 443.
thumbprint: Optional SHA-1 SSL thumbprint of the management endpoint.
allow_untrusted: If True, skip certificate validation.
"""
ssl_context = None
if allow_untrusted:
ssl_context = _ssl_client_context(verify=False)
return SmartConnect(
host=host,
user=username,
pwd=password,
port=port,
thumbprint=thumbprint,
sslContext=ssl_context,
disableSslCertValidation=allow_untrusted)
def _virtual_disk_key(vm: vim.VirtualMachine, disk_path: str) -> int:
"""Return the VirtualDisk device key whose backing path is ``disk_path``."""
for device in vm.config.hardware.device:
if isinstance(device, vim.vm.device.VirtualDisk):
backing = getattr(device, "backing", None)
file_name = getattr(backing, "fileName", None)
if file_name == disk_path:
return device.key
raise ValueError(
f"VMDK path {disk_path!r} is not attached to {vm._moId}")
def get_nfc_ticket(
si: vim.ServiceInstance,
vm: vim.VirtualMachine,
disk_device_key: Optional[int] = None,
host_for_access: Optional[vim.HostSystem] = None,
read_only: bool = True,
disk_path: Optional[str] = None) -> vim.HostServiceTicket:
"""Return a one-time NFC HostServiceTicket for ``vm``.
Matches VDDK: ``NfcGetVmFiles`` when only the VM is known (read-only),
``NfcRandomAccessOpenReadonly`` / ``NfcRandomAccessOpenDisk`` when a
virtual disk device key (or datastore path) is supplied.
Args:
si: Authenticated ServiceInstance.
vm: Target virtual machine.
disk_device_key: Optional VirtualDisk.device key (for example 2000).
host_for_access: Host that should serve NFC; defaults to the VM's host.
read_only: When False, request a writable ticket (needs a disk).
disk_path: Datastore path used to resolve ``disk_device_key``.
"""
nfc = nfc_service(si)
if read_only and disk_device_key is None and disk_path is None:
return nfc.GetVmFiles(vm)
if disk_device_key is None:
if disk_path is None:
raise ValueError(
"writable NFC tickets need disk_path or disk_device_key")
disk_device_key = _virtual_disk_key(vm, disk_path)
if host_for_access is None:
host_for_access = vm.runtime.host
if read_only:
return nfc.RandomAccessOpenReadonly(
vm, disk_device_key, host_for_access)
return nfc.RandomAccessOpen(vm, disk_device_key, host_for_access)
def _format_thumbprint(digest: bytes) -> str:
return ":".join(f"{byte:02X}" for byte in digest)
def _sha1_thumbprint(der_cert: bytes) -> str:
return _format_thumbprint(hashlib.sha1(der_cert).digest())
def _normalize_thumbprint(thumbprint: str) -> str:
return thumbprint.replace(":", "").replace(" ", "").upper()
def get_ssl_cert_thumbprint(
host: str,
port: int = 443,
digest_algorithm: str = "sha1",
ssl_context: Optional[ssl.SSLContext] = None,
timeout: float = 30.0) -> str:
"""Return the TLS certificate thumbprint of ``host``:``port``.
Reads the peer certificate in DER form and hashes it with ``hashlib``.
The result is colon-separated uppercase hex (for example
``A5:AF:7D:…``), matching VDDK / pyVmomi SHA-1 thumbprints.
Args:
host: Hostname or IP of the TLS server.
port: TLS port, usually 443.
digest_algorithm: Hash name accepted by ``hashlib.new``. Default
``sha1`` is the format VDDK and pyVmomi expect.
ssl_context: Optional SSL context. When omitted, a default client
context is used with hostname checks and certificate
validation disabled so a self-signed management certificate
can still be read.
timeout: Connect timeout in seconds.
"""
if ssl_context is None:
ssl_context = _ssl_client_context(verify=False)
with socket.create_connection((host, port), timeout=timeout) as sock:
with ssl_context.wrap_socket(
sock, server_hostname=host) as ssock:
cert = ssock.getpeercert(binary_form=True)
if not cert:
raise ConnectionError(
f"no peer certificate from {host}:{port}")
return _format_thumbprint(
hashlib.new(digest_algorithm, cert).digest())
def _readline(sock: socket.socket) -> str:
buf = b""
while not buf.endswith(b"\n"):
chunk = sock.recv(1)
if not chunk:
raise ConnectionError("authd connection closed")
buf += chunk
if len(buf) > 4096:
raise ConnectionError("oversized authd response")
return buf.decode("ascii", "replace").rstrip("\r\n")
def _expect_code(line: str, code: str, what: str) -> str:
if not line.startswith(code):
raise ConnectionError(f"authd {what} failed: {line}")
return line[len(code):].lstrip()
def connect_authd(
ticket: vim.HostServiceTicket,
allow_untrusted: bool = False,
timeout: float = 30.0) -> ssl.SSLSocket:
"""Complete the ESXi authd handshake using an NFC HostServiceTicket.
Wire sequence captured from VDDK against authd on TCP 902:
1. Read the plaintext 220 banner, then wrap the socket with TLS.
2. SESSION <sessionId>
3. BANNER
4. THUMBPRINT_SHA2 PlainText (NFC data stays on this TLS socket)
5. PROXY <ticket.service> (vpxa-nfc when connecting via vCenter)
Args:
ticket: One-time ticket from get_nfc_ticket().
allow_untrusted: If False, require the peer SHA-1 thumbprint to match
ticket.sslThumbprint.
timeout: Socket timeout in seconds.
"""
host = ticket.host
port = ticket.port or AUTHD_DEFAULT_PORT
raw = socket.create_connection((host, port), timeout=timeout)
try:
banner = _readline(raw)
if not banner.startswith("220"):
raise ConnectionError(f"unexpected authd banner: {banner}")
ssl_context = _ssl_client_context(verify=False)
ssock = ssl_context.wrap_socket(raw, server_hostname=host)
except Exception:
raw.close()
raise
try:
if not allow_untrusted and ticket.sslThumbprint:
peer = _sha1_thumbprint(ssock.getpeercert(True))
if _normalize_thumbprint(peer) != _normalize_thumbprint(
ticket.sslThumbprint):
raise ConnectionError(
f"ESXi SSL thumbprint mismatch: got {peer}, "
f"expected {ticket.sslThumbprint}")
ssock.sendall(f"SESSION {ticket.sessionId}\r\n".encode("ascii"))
# Trailing space is part of the BANNER command token used by authd.
ssock.sendall(b"BANNER \r\n")
_expect_code(_readline(ssock), "220", "BANNER")
ssock.sendall(b"THUMBPRINT_SHA2 PlainText\r\n")
_expect_code(_readline(ssock), "200", "THUMBPRINT_SHA2")
service = ticket.service or "vpxa-nfc"
ssock.sendall(f"PROXY {service}\r\n".encode("ascii"))
_expect_code(_readline(ssock), "200", "PROXY")
return ssock
except Exception:
ssock.close()
raise
class NfcAuthSession:
"""Authenticated VIM session plus an authd/NFC TLS socket."""
def __init__(
self,
si: vim.ServiceInstance,
ticket: vim.HostServiceTicket,
authd_sock: ssl.SSLSocket) -> None:
self.si = si
self.ticket = ticket
self.authd_sock = authd_sock
def close(self) -> None:
"""Close the authd socket and logout of the VIM session."""
try:
self.authd_sock.close()
finally:
Disconnect(self.si)
def __enter__(self) -> "NfcAuthSession":
return self
def __exit__(self, exc_type, exc, tb) -> None:
self.close()
def authenticate(
host: str,
username: str,
password: str,
vm_moref: str,
port: int = 443,
thumbprint: Optional[str] = None,
allow_untrusted: bool = False,
disk_device_key: Optional[int] = None,
disk_path: Optional[str] = None,
read_only: bool = True) -> NfcAuthSession:
"""Login to vSphere and complete NFC authd authentication for a VM.
Args:
host: vCenter or ESXi hostname/IP.
username: VIM user name.
password: VIM password.
vm_moref: Virtual machine managed object id (for example ``vm-13098``).
port: HTTPS port for VIM, usually 443.
thumbprint: Optional SHA-1 thumbprint of the management endpoint.
allow_untrusted: Skip TLS certificate checks when True.
disk_device_key: Optional VirtualDisk device key; when omitted with
``read_only``, the VDDK ``NfcGetVmFiles`` ticket is used.
disk_path: Datastore path used to resolve ``disk_device_key``.
read_only: When False, request a writable ``NfcRandomAccessOpenDisk``
ticket.
"""
si = connect_vim(
host, username, password, port=port,
thumbprint=thumbprint, allow_untrusted=allow_untrusted)
try:
vm = vim.VirtualMachine(vm_moref, si._stub)
ticket = get_nfc_ticket(
si, vm, disk_device_key=disk_device_key,
disk_path=disk_path, read_only=read_only)
authd_sock = connect_authd(
ticket, allow_untrusted=allow_untrusted)
except Exception:
Disconnect(si)
raise
return NfcAuthSession(si, ticket, authd_sock)
+425
View File
@@ -0,0 +1,425 @@
# Copyright 2026 Cloudbase Solutions Srl
# All Rights Reserved.
"""VDDK-compatible NFC disk open, sector read, and sector write.
After ``nfc_auth.connect_authd`` returns ``200 Connect``, VDDK stops using
``SSL_write`` on the authd socket. ``THUMBPRINT_SHA2 PlainText`` means the
NFC binary protocol runs as raw TCP on that same file descriptor
(``useSSL=0``). This module dups that fd and speaks:
1. Classic 264-byte NFC messages (handshake, version, connection data,
AIO session open).
2. NFC AIO frames (16-byte header plus payload) to open a VMDK and read
or write sectors.
pyVmomi is not involved here; the ticket and TLS authd handshake already
happened in ``nfc_auth``.
"""
from __future__ import annotations
import os
import socket
import ssl
import struct
from openvixdisklib.nfc_auth import NfcAuthSession
NFC_MSG_SIZE = 264
NFC_AIO_MAGIC = 0xA100DA7A
NFC_AIO_HDR_SIZE = 16
NFC_SECTOR_SIZE = 512
NFC_PROTOCOL_VERSION = 11
# Max data bytes in one AIO IO reply fragment (NfcAioInitSession buffer).
NFC_AIO_BUFFER_SIZE = 65536
# Classic NFC message types observed on the wire (uint32 at offset 0).
NFC_MSG_SESSION_COMPLETE = 4
NFC_MSG_SESSION_PARAMS = 33
NFC_MSG_SESSION_PARAMS_REPLY = 36
NFC_MSG_HANDSHAKE = 43
NFC_MSG_VERSION = 51
NFC_MSG_AIO_SESSION_OPEN = 52
NFC_MSG_CONNECTION_DATA = 54
NFC_MSG_SESSION_FEATURES = 55
# SessionParams / feature bits from VDDK logs (interruption | switch).
NFC_SESSION_FEATURE_INTERRUPTION_SWITCH = 3
# AIO message types (NfcAioSendMessage "type = N").
NFC_AIO_MSG_ERROR = 1
NFC_AIO_MSG_OPEN_SESSION = 2
NFC_AIO_MSG_CLOSE_SESSION = 3
NFC_AIO_MSG_OPEN_FILE = 4
NFC_AIO_MSG_CLOSE_FILE = 5
NFC_AIO_MSG_IO = 7
NFC_AIO_MSG_SET_SOCK_OPTS = 9
NFC_AIO_MSG_DDB_GET = 11
NFC_AIO_MSG_SET_RES_POOL = 22
# Open-file body: file type NFC_DISK. 0x1e is what VDDK sends for
# VIXDISKLIB_FLAG_OPEN_READ_ONLY; writable opens clear bit 0x04 (0x1a).
NFC_DISK = 2
NFC_OPEN_FLAGS_READ_ONLY = 0x1E
NFC_OPEN_FLAGS_READ_WRITE = 0x1A
NFC_AIO_IO_WRITE = 0
NFC_AIO_IO_READ = 1
class NfcProtocolError(ConnectionError):
"""Raised when an NFC message is malformed or reports failure."""
def takeover_authd_socket(ssock: ssl.SSLSocket) -> socket.socket:
"""Return a raw socket on the authd TCP connection.
VDDK writes NFC with ``write(SSL_get_fd(ssl), ...)`` after PROXY, so
those bytes are not TLS records. Duping the fd lets Python do the
same without ``SSLSocket.send`` re-encrypting, and without
``SSL_shutdown``.
Args:
ssock: The TLS socket from ``nfc_auth.connect_authd``.
"""
timeout = ssock.gettimeout()
raw = socket.socket(
family=ssock.family,
type=ssock.type,
proto=ssock.proto,
fileno=os.dup(ssock.fileno()))
raw.settimeout(timeout)
return raw
def _recvn(sock: socket.socket, size: int) -> bytes:
buf = bytearray()
while len(buf) < size:
chunk = sock.recv(size - len(buf))
if not chunk:
raise NfcProtocolError(
f"NFC connection closed, needed {size} bytes, got {len(buf)}")
buf.extend(chunk)
return bytes(buf)
def _send_nfc_msg(
sock: socket.socket, msg_type: int, body: bytes = b"") -> None:
if len(body) > NFC_MSG_SIZE - 4:
raise ValueError("NFC classic message body too large")
frame = struct.pack("<I", msg_type) + body
sock.sendall(frame.ljust(NFC_MSG_SIZE, b"\x00"))
def _recv_nfc_msg(sock: socket.socket) -> tuple[int, bytes]:
frame = _recvn(sock, NFC_MSG_SIZE)
msg_type = struct.unpack_from("<I", frame)[0]
return msg_type, frame[4:]
def _pack_aio_hdr(msg_type: int, payload_size: int, op_id: int) -> bytes:
return struct.pack(
"<IIII", NFC_AIO_MAGIC, msg_type, payload_size, op_id)
def _unpack_aio_hdr(hdr: bytes) -> tuple[int, int, int]:
magic, msg_type, size, op_id = struct.unpack_from("<IIII", hdr)
if magic != NFC_AIO_MAGIC:
raise NfcProtocolError(
f"AIO header magic mismatch: 0x{magic:x}, "
f"expected 0x{NFC_AIO_MAGIC:x}")
if msg_type == NFC_AIO_MSG_ERROR:
raise NfcProtocolError(f"AIO error opId={op_id} size={size}")
return msg_type, size, op_id
class NfcDisk:
"""An NFC AIO session with one VMDK opened for I/O."""
def __init__(
self,
sock: socket.socket,
path: str,
handle: int,
sector_size: int) -> None:
"""Wrap an AIO session that already has ``path`` open.
Args:
sock: Raw NFC socket after handshake.
path: Datastore path that was opened.
handle: Server file handle from OPEN_FILE.
sector_size: Sector size from the OPEN_FILE reply.
"""
self._sock = sock
self._op_id = 0
self.path = path
self.handle = handle
self.sector_size = sector_size
self._closed = False
def _next_op_id(self) -> int:
op_id = self._op_id
self._op_id += 1
return op_id
def _aio_roundtrip(
self,
msg_type: int,
payload: bytes,
extra: bytes = b"",
extra_recv: int = 0) -> bytes:
"""Send one AIO request and return the reply payload (+ extra)."""
op_id = self._next_op_id()
self._sock.sendall(
_pack_aio_hdr(msg_type, len(payload), op_id) + payload)
if extra:
self._sock.sendall(extra)
rhdr = _recvn(self._sock, NFC_AIO_HDR_SIZE)
magic, rtype, rsize, rop = struct.unpack_from("<IIII", rhdr)
if magic != NFC_AIO_MAGIC:
raise NfcProtocolError(
f"AIO header magic mismatch: 0x{magic:x}, "
f"expected 0x{NFC_AIO_MAGIC:x}")
body = _recvn(self._sock, rsize) if rsize else b""
if rtype == NFC_AIO_MSG_ERROR:
raise NfcProtocolError(
f"AIO error opId={rop} size={rsize} {body.hex()}")
if rtype != msg_type or rop != op_id:
raise NfcProtocolError(
f"AIO reply type={rtype} opId={rop}, "
f"expected type={msg_type} opId={op_id}")
if extra_recv:
body += _recvn(self._sock, extra_recv)
return body
def read(self, start_sector: int, num_sectors: int = 1) -> bytes:
"""Read ``num_sectors`` starting at ``start_sector``.
Matches ``VixDiskLib_Read``: one ``NFC_AIO_MSG_IO`` request in
byte units. If the length exceeds the AIO buffer (64 KiB) the
server replies with several same-``opId`` fragments.
Args:
start_sector: Sector offset from the start of the disk.
num_sectors: Number of sectors to read.
"""
if num_sectors < 1:
raise ValueError("num_sectors must be at least 1")
length = num_sectors * self.sector_size
offset = start_sector * self.sector_size
payload = struct.pack(
"<QQQQIII",
self.handle,
NFC_AIO_IO_READ,
offset,
length,
length,
length,
0)
op_id = self._next_op_id()
self._sock.sendall(
_pack_aio_hdr(NFC_AIO_MSG_IO, len(payload), op_id) + payload)
data = bytearray()
while len(data) < length:
rhdr = _recvn(self._sock, NFC_AIO_HDR_SIZE)
rtype, rsize, rop = _unpack_aio_hdr(rhdr)
if rtype != NFC_AIO_MSG_IO or rop != op_id:
raise NfcProtocolError(
f"AIO IO reply type={rtype} opId={rop}, "
f"expected type={NFC_AIO_MSG_IO} opId={op_id}")
body = _recvn(self._sock, rsize)
if rsize < 36:
raise NfcProtocolError(
f"AIO IO reply payload too short: {rsize}")
chunk_len = struct.unpack_from("<I", body, 32)[0]
remaining = length - len(data)
if chunk_len == 0 or chunk_len > remaining:
raise NfcProtocolError(
f"AIO IO chunk length {chunk_len} invalid, "
f"remaining {remaining}")
data.extend(_recvn(self._sock, chunk_len))
return bytes(data)
def write(
self,
start_sector: int,
num_sectors: int,
data: bytes) -> None:
"""Write ``num_sectors`` starting at ``start_sector``.
Matches ``VixDiskLib_Write``: one ``NFC_AIO_MSG_IO`` request per
chunk in byte units, with sector bytes sent after the 44-byte
payload. Chunks larger than the AIO buffer (64 KiB) are split.
Args:
start_sector: Sector offset from the start of the disk.
num_sectors: Number of sectors to write.
data: Bytes to write; length must be ``num_sectors * sector_size``.
"""
if num_sectors < 1:
raise ValueError("num_sectors must be at least 1")
length = num_sectors * self.sector_size
if len(data) != length:
raise ValueError(
f"write data is {len(data)} bytes, need {length}")
max_sectors = NFC_AIO_BUFFER_SIZE // self.sector_size
offset_sectors = start_sector
remaining = data
while remaining:
n_sectors = min(len(remaining) // self.sector_size, max_sectors)
chunk = remaining[:n_sectors * self.sector_size]
self._write_once(offset_sectors, n_sectors, chunk)
offset_sectors += n_sectors
remaining = remaining[n_sectors * self.sector_size:]
def _write_once(
self,
start_sector: int,
num_sectors: int,
data: bytes) -> None:
length = num_sectors * self.sector_size
offset = start_sector * self.sector_size
payload = struct.pack(
"<QQQQIII",
self.handle,
NFC_AIO_IO_WRITE,
offset,
length,
length,
length,
0)
self._aio_roundtrip(NFC_AIO_MSG_IO, payload, extra=data)
def close(self) -> None:
"""Close the VMDK, the AIO session, and the classic NFC session."""
if self._closed:
return
self._closed = True
try:
self._aio_roundtrip(
NFC_AIO_MSG_CLOSE_FILE, struct.pack("<Q", self.handle))
self._aio_roundtrip(
NFC_AIO_MSG_CLOSE_SESSION, struct.pack("<I", 0))
_send_nfc_msg(self._sock, NFC_MSG_SESSION_COMPLETE)
finally:
try:
self._sock.close()
except OSError:
pass
def __enter__(self) -> "NfcDisk":
return self
def __exit__(self, exc_type, exc, tb) -> None:
self.close()
def _handshake(
sock: socket.socket,
client_name: str,
op_id: str,
version: int) -> None:
"""Run the classic NFC session handshake used by VDDK NBD."""
_send_nfc_msg(sock, NFC_MSG_HANDSHAKE, b"PlainText")
_send_nfc_msg(sock, NFC_MSG_SESSION_PARAMS)
reply_type, _ = _recv_nfc_msg(sock)
if reply_type != NFC_MSG_SESSION_PARAMS_REPLY:
raise NfcProtocolError(
f"expected session-params reply {NFC_MSG_SESSION_PARAMS_REPLY}, "
f"got {reply_type}")
_send_nfc_msg(sock, NFC_MSG_VERSION, struct.pack("<I", version))
reply_type, body = _recv_nfc_msg(sock)
if reply_type != NFC_MSG_VERSION:
raise NfcProtocolError(
f"expected version reply {NFC_MSG_VERSION}, got {reply_type}")
remote_version = struct.unpack_from("<I", body)[0]
if remote_version < 3:
raise NfcProtocolError(
f"NFC server version {remote_version} is too old for AIO")
name_b = client_name.encode("ascii")
op_b = op_id.encode("ascii")
_send_nfc_msg(
sock, NFC_MSG_CONNECTION_DATA,
struct.pack("<II", len(name_b), len(op_b)))
sock.sendall(name_b)
sock.sendall(op_b)
_send_nfc_msg(
sock, NFC_MSG_SESSION_FEATURES,
struct.pack("<I", NFC_SESSION_FEATURE_INTERRUPTION_SWITCH))
_send_nfc_msg(sock, NFC_MSG_AIO_SESSION_OPEN)
reply_type, _ = _recv_nfc_msg(sock)
if reply_type != NFC_MSG_AIO_SESSION_OPEN:
raise NfcProtocolError(
f"expected AIO session-open reply "
f"{NFC_MSG_AIO_SESSION_OPEN}, got {reply_type}")
def _aio_prepare(disk: NfcDisk) -> None:
disk._aio_roundtrip(
NFC_AIO_MSG_OPEN_SESSION, bytes(16))
disk._aio_roundtrip(
NFC_AIO_MSG_SET_SOCK_OPTS, bytes(12))
disk._aio_roundtrip(
NFC_AIO_MSG_SET_RES_POOL, struct.pack("<I", 1))
def _parse_open_reply(body: bytes) -> tuple[int, int]:
if len(body) < 40:
raise NfcProtocolError(f"OPEN_FILE reply too short: {len(body)}")
handle, file_type, _flags = struct.unpack_from("<QII", body, 8)
sector_size = struct.unpack_from("<I", body, 36)[0]
if file_type != NFC_DISK:
raise NfcProtocolError(
f"opened file type {file_type}, expected NFC_DISK")
if sector_size == 0:
sector_size = NFC_SECTOR_SIZE
return handle, sector_size
def open_disk(
session: NfcAuthSession,
disk_path: str,
client_name: str = "vddk",
op_id: str = "nbdmode",
version: int = NFC_PROTOCOL_VERSION,
read_only: bool = True) -> NfcDisk:
"""Open ``disk_path`` over the authenticated authd socket.
Matches VDDK ``VixDiskLib_Open`` of a datastore path after the NFC
ticket and authd PROXY handshake: session init, AIO open, then
``NFC_AIO_MSG_OPEN_FILE`` with type ``NFC_DISK``.
Args:
session: Result of ``nfc_auth.authenticate``.
disk_path: Datastore path, for example
``[datastore0] vm/vm.vmdk``.
client_name: NFC client name; VDDK sends ``vddk``.
op_id: NFC operation id; VDDK NBD sends ``nbdmode``.
version: Client NFC protocol version (lab ESXi answered 11).
read_only: When True, open with VDDK's read-only NFC flags.
"""
sock = takeover_authd_socket(session.authd_sock)
try:
_handshake(sock, client_name, op_id, version)
disk = NfcDisk(sock, disk_path, handle=0, sector_size=NFC_SECTOR_SIZE)
_aio_prepare(disk)
path_b = disk_path.encode("utf-8")
open_flags = (
NFC_OPEN_FLAGS_READ_ONLY if read_only
else NFC_OPEN_FLAGS_READ_WRITE)
open_body = struct.pack(
"<IIIIII",
len(path_b), 0, 0, 0, NFC_DISK, open_flags)
open_body = open_body.ljust(60, b"\x00")
reply = disk._aio_roundtrip(
NFC_AIO_MSG_OPEN_FILE, open_body, extra=path_b)
handle, sector_size = _parse_open_reply(reply)
disk.handle = handle
disk.sector_size = sector_size
return disk
except Exception:
sock.close()
raise
+333
View File
@@ -0,0 +1,333 @@
# 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 typing import Iterator, Optional, Union
from pyVim.connect import Disconnect
from pyVmomi import vim
from openvixdisklib import nfc_auth
from openvixdisklib import 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
_COMPRESSION_FLAGS = (
VIXDISKLIB_FLAG_OPEN_COMPRESSION_ZLIB
| VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ
| VIXDISKLIB_FLAG_OPEN_COMPRESSION_SKIPZ)
VIX_SUPPORTED_COMPATIBILITY_MODES = [
"6.0", "6.5", "6.7", "7.0", "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: Optional[str]) -> 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 _require_nbd(transport_modes: Optional[str]) -> None:
if transport_modes is None:
return
modes = [m for m in transport_modes.split(":") if m]
if "nbd" not in modes:
raise NotImplementedError(
f"only nbd transport is supported, 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: Optional[str],
thumbprint: Optional[str],
allow_untrusted: bool,
read_only: bool) -> 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
class _DiskHandle:
"""Opened NFC disk plus the authd TLS socket it was taken from."""
def __init__(
self,
disk: nfc_open.NfcDisk,
authd_sock) -> None:
self.disk = disk
self.authd_sock = authd_sock
class VixDiskLibHandle:
"""VDDK-compatible handle backed by pyVmomi and the NFC replacement."""
def __init__(
self,
config_path: Optional[str] = None,
vixdisklib_compatibility_version: Optional[str] = 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``. Validated for form only.
"""
del config_path
target_versions = VIX_SUPPORTED_COMPATIBILITY_MODES
if vixdisklib_compatibility_version:
target_versions = [vixdisklib_compatibility_version]
LOG.debug("vixDiskLib versions targeted: %s", target_versions)
version_used = None
for version in reversed(target_versions):
try:
major_ver, minor_ver = version.split(".")
int(major_ver)
int(minor_ver)
except ValueError as ex:
raise ValueError(
"Unsupported vixDiskLib version format '%s'. vixDiskLib "
"compatibility mode must be of the form "
"'$major.$minor'" % version) from ex
version_used = version
break
if not version_used:
raise Exception(
"Could not initialize vixDiskLib with any of the following "
"versions: %s" % target_versions)
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 ["nbd"]
def get_transport_mode(self, disk_handle: _DiskHandle) -> str:
"""Return the transport used for ``disk_handle``."""
del disk_handle
return "nbd"
@contextlib.contextmanager
def connect(
self,
server_name: str,
thumbprint: Optional[str],
username: str,
password: str,
vmx_spec: Optional[str] = None,
snapshot_ref: Optional[str] = None,
read_only: bool = True,
transport_modes: Optional[str] = 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.
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: ``nbd`` or a colon list that includes ``nbd``.
port: HTTPS port, usually 443.
allow_untrusted: Skip management TLS verification when True.
"""
LOG.debug("Connecting VixDiskLib: %s", server_name)
_require_nbd(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 or not thumbprint)
conn = _Connection(
si, vm_moref, snapshot_ref, thumbprint,
allow_untrusted or not thumbprint, read_only)
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``.
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. Compression flags
are not implemented.
"""
LOG.debug("Openning VixDiskLib disk: %s", disk_path)
if flags & _COMPRESSION_FLAGS:
raise NotImplementedError(
"NBD compression open flags are not supported")
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)
ticket = nfc_auth.get_nfc_ticket(
conn.si, vm, read_only=read_only, disk_path=disk_path)
authd_sock = nfc_auth.connect_authd(
ticket, allow_untrusted=conn.allow_untrusted)
session = nfc_auth.NfcAuthSession(conn.si, ticket, authd_sock)
try:
disk = nfc_open.open_disk(
session, disk_path, read_only=read_only)
except Exception:
authd_sock.close()
raise
handle = _DiskHandle(disk, authd_sock)
try:
yield handle
finally:
self.close(handle)
def read(
self,
disk_handle: _DiskHandle,
start_sector: int,
num_sectors: int,
buf: Union[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).
"""
data = disk_handle.disk.read(start_sector, num_sectors)
if isinstance(buf, (bytearray, memoryview)):
if len(buf) < len(data):
raise Exception(
f"read buffer is {len(buf)} bytes, need {len(data)}")
buf[:len(data)] = data
return
ctypes.memmove(buf, data, len(data))
def write(
self,
disk_handle: _DiskHandle,
start_sector: int,
num_sectors: int,
buf: Union[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
+5
View File
@@ -0,0 +1,5 @@
pbr
pyOpenSSL
pyVmomi>=7.0
PyYAML
requests
+11
View File
@@ -0,0 +1,11 @@
line-length = 88
target-version = "py312"
[lint]
# UP031: Allow percent formatting for now.
# PIE790: Unnecessary `pass` statement - actually improves readability.
# RUF100: Unused blanket `noqa` directive - used by other linters.
# BLE001: we'll allow catching Exception for now.
# RUF015: enforces next() to be used when getting the first item of a list,
# may be enabled at a later time.
ignore = ["UP031", "PIE790", "RUF100", "BLE001", "RUF015"]
+28
View File
@@ -0,0 +1,28 @@
[metadata]
name = openvixdisklib
summary =
A reverse engineered replacement for the proprietary VMware VDDK vixdisklib
library.
description-file = README.md
author = Cloudbase Solutions SRL
author-email = [email protected]
home-page = http://cloudbase.it
classifier =
Environment :: VMware
Intended Audience :: Information Technology
Intended Audience :: System Administrators
Operating System :: OS Independent
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3.12
[files]
packages =
openvixdisklib
[global]
setup-hooks =
pbr.hooks.setup_hook
[wheel]
universal = 1
+6
View File
@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['pbr>=1.8'],
pbr=True)
+4
View File
@@ -0,0 +1,4 @@
coverage
discover
ddt
stestr
View File
View File
+336
View File
@@ -0,0 +1,336 @@
# Copyright 2026 Cloudbase Solutions Srl
# All Rights Reserved.
"""Test base classes for openvixdisklib integration tests."""
from __future__ import annotations
import ctypes
import os
import time
import unittest
import uuid
from typing import Any, Optional
import yaml
from pyVim.connect import Disconnect
from pyVmomi import vim
from openvixdisklib import nfc_auth
from openvixdisklib.nfc_auth import NfcAuthSession
_REPO_ROOT = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", ".."))
_CONFIG_PATH = os.path.join(_REPO_ROOT, ".test_config.yaml")
_CONFIG_KEYS = (
"host",
"port",
"username",
"password",
"allow_untrusted",
"datacenter",
"datastore",
)
_VDDK_DIR = os.path.join(_REPO_ROOT, ".vddk")
_VDDK_LIB = os.path.join(_VDDK_DIR, "libvixDiskLib.so")
_DISK_CAPACITY_KB = 10 * 1024 * 1024
_TASK_POLL_S = 0.5
_TASK_TIMEOUT_S = 300
_LAB_VM_PREFIX = "ovdl-test-"
class TestBase(unittest.TestCase):
"""Shared lab vSphere settings for live NFC / VDDK integration tests."""
HOST: str
PORT: int
USERNAME: str
PASSWORD: str
ALLOW_UNTRUSTED: bool
DATACENTER: str
DATASTORE: str
THUMBPRINT: str
VM_MOREF: str
VMX_SPEC: str
DISK_PATH: str
SECTOR_SIZE = 512
SECTOR_AT_1GB = (1024 * 1024 * 1024) // SECTOR_SIZE
VDDK_DIR = _VDDK_DIR
_lab_refcount = 0
_lab_vm_moref: Optional[str] = None
_lab_vm_name: Optional[str] = None
@classmethod
def setUpClass(cls) -> None:
"""Prepare process environment and create a temporary lab VM."""
super().setUpClass()
os.environ.pop("LD_PRELOAD", None)
cls._ensure_vddk_library_path()
TestBase._acquire_lab()
@classmethod
def tearDownClass(cls) -> None:
"""Release the temporary lab VM when the last test class finishes."""
TestBase._release_lab()
super().tearDownClass()
def setUp(self) -> None:
"""Reset per-test state; subclasses may reuse this."""
super().setUp()
@classmethod
def _ensure_vddk_library_path(cls) -> None:
current = os.environ.get("LD_LIBRARY_PATH", "")
parts = [p for p in current.split(":") if p]
if cls.VDDK_DIR not in parts:
os.environ["LD_LIBRARY_PATH"] = (
cls.VDDK_DIR if not current else f"{cls.VDDK_DIR}:{current}")
@classmethod
def require_vddk(cls) -> None:
"""Skip when ``libvixDiskLib`` cannot be loaded from ``.vddk``."""
cls._ensure_vddk_library_path()
try:
ctypes.CDLL(_VDDK_LIB)
except OSError as exc:
raise unittest.SkipTest(
f"VDDK library not available at {_VDDK_LIB}: {exc}") from exc
@classmethod
def _load_test_config(cls) -> None:
"""Load lab settings from the repo-root ``.test_config.yaml``."""
if not os.path.isfile(_CONFIG_PATH):
raise unittest.SkipTest(
"integration tests need .test_config.yaml in the repo "
"root; see README.md for a sample")
with open(_CONFIG_PATH, encoding="utf-8") as config_file:
data = yaml.safe_load(config_file) or {}
missing = [key for key in _CONFIG_KEYS if key not in data]
if missing:
raise RuntimeError(
f"{_CONFIG_PATH} is missing keys: {', '.join(missing)}")
TestBase.HOST = str(data["host"])
TestBase.PORT = int(data["port"])
TestBase.USERNAME = str(data["username"])
TestBase.PASSWORD = str(data["password"])
TestBase.ALLOW_UNTRUSTED = bool(data["allow_untrusted"])
TestBase.DATACENTER = str(data["datacenter"])
TestBase.DATASTORE = str(data["datastore"])
@classmethod
def _connect_vim(cls) -> vim.ServiceInstance:
return nfc_auth.connect_vim(
cls.HOST,
cls.USERNAME,
cls.PASSWORD,
port=cls.PORT,
thumbprint=cls.THUMBPRINT,
allow_untrusted=cls.ALLOW_UNTRUSTED)
@classmethod
def _wait_for_task(cls, task: vim.Task) -> Any:
deadline = time.monotonic() + _TASK_TIMEOUT_S
while task.info.state in (
vim.TaskInfo.State.running, vim.TaskInfo.State.queued):
if time.monotonic() > deadline:
raise TimeoutError(
f"timed out waiting for vSphere task {task}")
time.sleep(_TASK_POLL_S)
if task.info.state != vim.TaskInfo.State.success:
raise RuntimeError(f"vSphere task failed: {task.info.error}")
return task.info.result
@classmethod
def _find_datacenter(
cls, content: vim.ServiceInstanceContent) -> vim.Datacenter:
matches = [
entity for entity in content.rootFolder.childEntity
if isinstance(entity, vim.Datacenter)
and entity.name == cls.DATACENTER]
if not matches:
raise RuntimeError(f"datacenter {cls.DATACENTER!r} not found")
return matches[0]
@classmethod
def _find_datastore(cls, datacenter: vim.Datacenter) -> vim.Datastore:
matches = [
datastore for datastore in datacenter.datastore
if datastore.name == cls.DATASTORE]
if not matches:
raise RuntimeError(
f"datastore {cls.DATASTORE!r} not found in "
f"datacenter {cls.DATACENTER!r}")
return matches[0]
@classmethod
def _bind_lab_fields(cls) -> None:
"""Copy shared lab VM fields onto the active test class."""
cls.HOST = TestBase.HOST
cls.PORT = TestBase.PORT
cls.USERNAME = TestBase.USERNAME
cls.PASSWORD = TestBase.PASSWORD
cls.ALLOW_UNTRUSTED = TestBase.ALLOW_UNTRUSTED
cls.DATACENTER = TestBase.DATACENTER
cls.DATASTORE = TestBase.DATASTORE
cls.THUMBPRINT = TestBase.THUMBPRINT
cls.VM_MOREF = TestBase.VM_MOREF
cls.VMX_SPEC = TestBase.VMX_SPEC
cls.DISK_PATH = TestBase.DISK_PATH
@classmethod
def _acquire_lab(cls) -> None:
if TestBase._lab_refcount == 0:
TestBase._load_test_config()
TestBase.THUMBPRINT = nfc_auth.get_ssl_cert_thumbprint(
TestBase.HOST, TestBase.PORT)
TestBase._create_lab_vm()
TestBase._lab_refcount += 1
cls._bind_lab_fields()
@classmethod
def _release_lab(cls) -> None:
if TestBase._lab_refcount == 0:
return
TestBase._lab_refcount -= 1
if TestBase._lab_refcount == 0:
cls._destroy_lab_vm()
@classmethod
def _create_lab_vm(cls) -> None:
"""Create an empty VM with a 10 GiB thin disk for I/O tests."""
si = cls._connect_vim()
vm = None
try:
content = si.RetrieveContent()
datacenter = cls._find_datacenter(content)
datastore = cls._find_datastore(datacenter)
if not datastore.host:
raise RuntimeError(
f"datastore {cls.DATASTORE!r} is not mounted on any host")
host = datastore.host[0].key
pool = host.parent.resourcePool
vm_name = _LAB_VM_PREFIX + uuid.uuid4().hex[:12]
vm = cls._wait_for_task(
datacenter.vmFolder.CreateVM_Task(
config=cls._vm_config_spec(vm_name, datastore.name),
pool=pool,
host=host))
TestBase._lab_vm_moref = vm._moId
TestBase._lab_vm_name = vm_name
TestBase.VM_MOREF = vm._moId
TestBase.VMX_SPEC = f"moref={vm._moId}"
disks = [
device.backing.fileName
for device in vm.config.hardware.device
if isinstance(device, vim.vm.device.VirtualDisk)]
if not disks:
raise RuntimeError(
f"temporary VM {vm_name!r} has no virtual disks")
TestBase.DISK_PATH = disks[0]
except Exception:
if vm is not None:
try:
cls._wait_for_task(vm.Destroy_Task())
except Exception:
pass
TestBase._lab_vm_moref = None
TestBase._lab_vm_name = None
raise
finally:
Disconnect(si)
@classmethod
def _vm_config_spec(
cls, vm_name: str, datastore_name: str) -> vim.vm.ConfigSpec:
config = vim.vm.ConfigSpec()
config.name = vm_name
config.guestId = "otherGuest64"
config.memoryMB = 128
config.numCPUs = 1
config.files = vim.vm.FileInfo(
vmPathName=f"[{datastore_name}]")
controller = vim.vm.device.ParaVirtualSCSIController()
controller.key = 1000
controller.busNumber = 0
controller.sharedBus = (
vim.vm.device.VirtualSCSIController.Sharing.noSharing)
controller_spec = vim.vm.device.VirtualDeviceSpec()
controller_spec.operation = (
vim.vm.device.VirtualDeviceSpec.Operation.add)
controller_spec.device = controller
backing = vim.vm.device.VirtualDisk.FlatVer2BackingInfo()
backing.diskMode = "persistent"
backing.thinProvisioned = True
backing.fileName = f"[{datastore_name}]"
disk = vim.vm.device.VirtualDisk()
disk.key = 2000
disk.controllerKey = 1000
disk.unitNumber = 0
disk.capacityInKB = _DISK_CAPACITY_KB
disk.backing = backing
disk_spec = vim.vm.device.VirtualDeviceSpec()
disk_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
disk_spec.fileOperation = (
vim.vm.device.VirtualDeviceSpec.FileOperation.create)
disk_spec.device = disk
config.deviceChange = [controller_spec, disk_spec]
return config
@classmethod
def _destroy_lab_vm(cls) -> None:
"""Power off and delete the temporary lab VM if it still exists."""
moref = TestBase._lab_vm_moref
TestBase._lab_vm_moref = None
TestBase._lab_vm_name = None
if not moref:
return
si = cls._connect_vim()
try:
vm = vim.VirtualMachine(moref, si._stub)
try:
vm.Reload()
except Exception:
return
if vm.runtime.powerState == vim.VirtualMachinePowerState.poweredOn:
cls._wait_for_task(vm.PowerOffVM_Task())
cls._wait_for_task(vm.Destroy_Task())
finally:
Disconnect(si)
def authenticate(self, read_only: bool = True) -> NfcAuthSession:
"""Login to the lab vCenter and complete NFC authd for the temp VM."""
return nfc_auth.authenticate(
host=self.HOST,
username=self.USERNAME,
password=self.PASSWORD,
vm_moref=self.VM_MOREF,
thumbprint=self.THUMBPRINT,
allow_untrusted=self.ALLOW_UNTRUSTED,
disk_path=None if read_only else self.DISK_PATH,
read_only=read_only)
def vixdisklib_connect_kwargs(
self, extra: Optional[dict[str, Any]] = None) -> dict[str, Any]:
"""Return common ``VixDiskLib_ConnectEx`` arguments for the temp VM."""
kwargs: dict[str, Any] = {
"server_name": self.HOST,
"port": self.PORT,
"thumbprint": self.THUMBPRINT,
"username": self.USERNAME,
"password": self.PASSWORD,
"vmx_spec": self.VMX_SPEC,
"transport_modes": "nbd",
"read_only": False,
}
if extra:
kwargs.update(extra)
return kwargs
def pattern_bytes(self, length: int, seed: bytes) -> bytes:
"""Return ``length`` bytes by repeating ``seed``."""
if not seed:
raise ValueError("seed must be non-empty")
return (seed * ((length // len(seed)) + 1))[:length]
+94
View File
@@ -0,0 +1,94 @@
# Copyright 2026 Cloudbase Solutions Srl
# All Rights Reserved.
"""Compare writes and reads from VDDK with openvixdisklib."""
from typing import Any, Optional
from openvixdisklib import openvixdisklib as open_vix
from tests.integration import vixdisklib
from tests.integration.base import TestBase
class CrosscheckTest(TestBase):
@classmethod
def setUpClass(cls) -> None:
"""Skip when the bundled VDDK shared library is not present."""
cls.require_vddk()
super().setUpClass()
def _connect_extra(self, module: Any) -> Optional[dict[str, Any]]:
"""Return extra ``connect`` kwargs needed by ``module``."""
if module is open_vix:
return {"allow_untrusted": self.ALLOW_UNTRUSTED}
return None
def _write_sectors(
self,
module: Any,
payloads: dict[int, bytes]) -> None:
"""Write one sector at each index using a vixdisklib-compatible module."""
handle = module.VixDiskLibHandle(
vixdisklib_compatibility_version="8.0",
config_path=None)
buf = module.get_buffer(self.SECTOR_SIZE)
kwargs = self.vixdisklib_connect_kwargs(self._connect_extra(module))
with handle.connect(**kwargs) as conn:
with handle.open(conn, self.DISK_PATH, flags=0) as disk:
for start, data in payloads.items():
buf[:self.SECTOR_SIZE] = data
handle.write(disk, start, 1, buf)
def _read_sectors(
self,
module: Any,
sectors: tuple[int, ...]) -> dict[int, bytes]:
"""Read one sector at each index using a vixdisklib-compatible module."""
handle = module.VixDiskLibHandle(
vixdisklib_compatibility_version="8.0",
config_path=None)
buf = module.get_buffer(self.SECTOR_SIZE)
result: dict[int, bytes] = {}
kwargs = self.vixdisklib_connect_kwargs(self._connect_extra(module))
with handle.connect(**kwargs) as conn:
with handle.open(conn, self.DISK_PATH, flags=0) as disk:
for start in sectors:
buf[:self.SECTOR_SIZE] = b"\xa5" * self.SECTOR_SIZE
handle.read(disk, start, 1, buf)
result[start] = buf.raw[:self.SECTOR_SIZE]
return result
def _assert_both_read(
self,
sectors: tuple[int, ...],
expected: dict[int, bytes]) -> None:
vddk = self._read_sectors(vixdisklib, sectors)
replacement = self._read_sectors(open_vix, sectors)
for start in sectors:
self.assertEqual(
vddk[start], expected[start],
f"VDDK mismatch at sector {start}")
self.assertEqual(
replacement[start], expected[start],
f"openvixdisklib mismatch at sector {start}")
def test_openvixdisklib_matches_vddk_sectors(self) -> None:
"""Writes from either library must be visible to both readers."""
sectors = (0, 1, self.SECTOR_AT_1GB)
vddk_payloads = {
0: self.pattern_bytes(self.SECTOR_SIZE, b"XCHK-VDDK-S0"),
1: self.pattern_bytes(self.SECTOR_SIZE, b"XCHK-VDDK-S1"),
self.SECTOR_AT_1GB: self.pattern_bytes(
self.SECTOR_SIZE, b"XCHK-VDDK-1G"),
}
self._write_sectors(vixdisklib, vddk_payloads)
self._assert_both_read(sectors, vddk_payloads)
ovdl_payloads = {
0: self.pattern_bytes(self.SECTOR_SIZE, b"XCHK-OVDL-S0"),
1: self.pattern_bytes(self.SECTOR_SIZE, b"XCHK-OVDL-S1"),
self.SECTOR_AT_1GB: self.pattern_bytes(
self.SECTOR_SIZE, b"XCHK-OVDL-1G"),
}
self._write_sectors(open_vix, ovdl_payloads)
self._assert_both_read(sectors, ovdl_payloads)
+18
View File
@@ -0,0 +1,18 @@
# Copyright 2026 Cloudbase Solutions Srl
# All Rights Reserved.
"""Exercise VDDK-compatible NFC authentication against the lab vCenter."""
from tests.integration.base import TestBase
class NfcAuthTest(TestBase):
def test_authd_handshake_completes(self) -> None:
"""Complete VIM login and authd PROXY through ``200 Connect``."""
with self.authenticate() as session:
ticket = session.ticket
self.assertTrue(ticket.host)
self.assertTrue(ticket.port)
self.assertTrue(ticket.sessionId)
self.assertTrue(session.authd_sock.version())
self.assertTrue(session.authd_sock.cipher())
+23
View File
@@ -0,0 +1,23 @@
# Copyright 2026 Cloudbase Solutions Srl
# All Rights Reserved.
"""Exercise NFC disk open and a one-sector write/read against the lab."""
from openvixdisklib import nfc_open
from tests.integration.base import TestBase
class NfcOpenTest(TestBase):
def test_open_disk_and_read_first_sector(self) -> None:
"""Open the temp VMDK, write sector 0, and read it back."""
expected = self.pattern_bytes(self.SECTOR_SIZE, b"NFC-OPEN-S0")
with self.authenticate(read_only=False) as session:
with nfc_open.open_disk(
session, self.DISK_PATH, read_only=False) as disk:
self.assertEqual(disk.path, self.DISK_PATH)
self.assertGreater(disk.handle, 0)
self.assertEqual(disk.sector_size, self.SECTOR_SIZE)
disk.write(0, 1, expected)
got = disk.read(0, 1)
self.assertIsNot(got, expected)
self.assertEqual(got, expected)
+54
View File
@@ -0,0 +1,54 @@
# Copyright 2026 Cloudbase Solutions Srl
# All Rights Reserved.
"""Exercise NFC sector writes and reads against the lab vCenter."""
from openvixdisklib import nfc_open
from tests.integration.base import TestBase
class NfcReadWriteTest(TestBase):
def test_sector_writes_and_reads(self) -> None:
"""Write known patterns and read them back at several ranges."""
ranges = [
(0, 1),
(0, 2),
(1, 1),
(8, 8),
(0, 128),
(0, 129),
(256, 64),
]
with self.authenticate(read_only=False) as session:
with nfc_open.open_disk(
session, self.DISK_PATH, read_only=False) as disk:
for start, n_sectors in ranges:
length = n_sectors * self.SECTOR_SIZE
seed = f"NFC-R{start}:{n_sectors}-".encode()
to_write = self.pattern_bytes(length, seed)
disk.write(start, n_sectors, to_write)
got = disk.read(start, n_sectors)
self.assertIsNot(got, to_write)
self.assertEqual(len(got), length)
self.assertEqual(got, to_write)
two_seed = b"NFC-TWO-SECTOR"
two_to_write = self.pattern_bytes(
2 * self.SECTOR_SIZE, two_seed)
disk.write(0, 2, two_to_write)
two_got = disk.read(0, 2)
self.assertIsNot(two_got, two_to_write)
self.assertEqual(two_got, two_to_write)
self.assertEqual(
disk.read(1, 1), two_to_write[self.SECTOR_SIZE:])
big_seed = b"NFC-129-SECTOR-WRITE"
big_to_write = self.pattern_bytes(
129 * self.SECTOR_SIZE, big_seed)
disk.write(0, 129, big_to_write)
big_got = disk.read(0, 129)
self.assertIsNot(big_got, big_to_write)
self.assertEqual(big_got, big_to_write)
self.assertEqual(
big_got[self.SECTOR_SIZE:2 * self.SECTOR_SIZE],
big_to_write[self.SECTOR_SIZE:2 * self.SECTOR_SIZE])
+33
View File
@@ -0,0 +1,33 @@
# Copyright 2026 Cloudbase Solutions Srl
# All Rights Reserved.
"""Exercise the VDDK-compatible openvixdisklib handle against the lab."""
from openvixdisklib import openvixdisklib as vixdisklib
from tests.integration.base import TestBase
class OpenVixDiskLibTest(TestBase):
def test_write_and_read_sector_zero_and_one_gib(self) -> None:
"""Write then read sector 0 and the sector at a 1 GiB offset."""
handle = vixdisklib.VixDiskLibHandle(
vixdisklib_compatibility_version="8.0",
config_path=None)
write_buf = vixdisklib.get_buffer(self.SECTOR_SIZE)
read_buf = vixdisklib.get_buffer(self.SECTOR_SIZE)
connect_kwargs = self.vixdisklib_connect_kwargs({
"allow_untrusted": self.ALLOW_UNTRUSTED,
})
patterns = {
0: self.pattern_bytes(self.SECTOR_SIZE, b"OVDL-S0"),
self.SECTOR_AT_1GB: self.pattern_bytes(
self.SECTOR_SIZE, b"OVDL-1GB"),
}
with handle.connect(**connect_kwargs) as conn:
with handle.open(conn, self.DISK_PATH, flags=0) as disk:
for start, expected in patterns.items():
write_buf[:self.SECTOR_SIZE] = expected
handle.write(disk, start, 1, write_buf)
read_buf[:self.SECTOR_SIZE] = b"\xa5" * self.SECTOR_SIZE
handle.read(disk, start, 1, read_buf)
self.assertEqual(read_buf.raw[:self.SECTOR_SIZE], expected)
+31
View File
@@ -0,0 +1,31 @@
# Copyright 2026 Cloudbase Solutions Srl
# All Rights Reserved.
"""Exercise native VDDK via tests.integration.vixdisklib against the lab."""
from tests.integration import vixdisklib
from tests.integration.base import TestBase
class VddkTest(TestBase):
@classmethod
def setUpClass(cls) -> None:
"""Skip when the bundled VDDK shared library is not present."""
cls.require_vddk()
super().setUpClass()
def test_write_and_read_first_sector(self) -> None:
"""Open the temp VMDK with VDDK, write sector 0, and read it back."""
handle = vixdisklib.VixDiskLibHandle(
vixdisklib_compatibility_version="8.0",
config_path=None)
write_buf = vixdisklib.get_buffer(self.SECTOR_SIZE)
read_buf = vixdisklib.get_buffer(self.SECTOR_SIZE)
expected = self.pattern_bytes(self.SECTOR_SIZE, b"VDDK-S0")
write_buf[:self.SECTOR_SIZE] = expected
with handle.connect(**self.vixdisklib_connect_kwargs()) as conn:
with handle.open(conn, self.DISK_PATH, flags=0) as disk:
handle.write(disk, 0, 1, write_buf)
read_buf[:self.SECTOR_SIZE] = b"\xa5" * self.SECTOR_SIZE
handle.read(disk, 0, 1, read_buf)
self.assertEqual(read_buf.raw[:self.SECTOR_SIZE], expected)
+538
View File
@@ -0,0 +1,538 @@
# Copyright 2019 Cloudbase Solutions Srl
# All Rights Reserved.
# Generated from vixDiskLib 6.7.0-8535999 from:
# $VIX_ROOT/doc/errors/errors.html
VIX_OK = 0
VIX_E_FAIL = 1
VIX_E_OUT_OF_MEMORY = 2
VIX_E_INVALID_ARG = 3
VIX_E_FILE_NOT_FOUND = 4
VIX_E_OBJECT_IS_BUSY = 5
VIX_E_NOT_SUPPORTED = 6
VIX_E_FILE_ERROR = 7
VIX_E_DISK_FULL = 8
VIX_E_INCORRECT_FILE_TYPE = 9
VIX_E_CANCELLED = 10
VIX_E_FILE_READ_ONLY = 11
VIX_E_FILE_ALREADY_EXISTS = 12
VIX_E_FILE_ACCESS_ERROR = 13
VIX_E_REQUIRES_LARGE_FILES = 14
VIX_E_FILE_ALREADY_LOCKED = 15
VIX_E_VMDB = 16
VIX_E_NOT_SUPPORTED_ON_REMOTE_OBJECT = 20
VIX_E_FILE_TOO_BIG = 21
VIX_E_FILE_NAME_INVALID = 22
VIX_E_ALREADY_EXISTS = 23
VIX_E_BUFFER_TOOSMALL = 24
VIX_E_OBJECT_NOT_FOUND = 25
VIX_E_HOST_NOT_CONNECTED = 26
VIX_E_INVALID_UTF = 8
VIX_E_OPERATION_ALREADY_IN_PROGRESS = 31
VIX_E_UNFINISHED_JOB = 29
VIX_E_NEED_KEY = 30
VIX_E_LICENSE = 32
VIX_E_VM_HOST_DISCONNECTED = 34
VIX_E_AUTHENTICATION_FAIL = 35
VIX_E_HOST_CONNECTION_LOST = 36
VIX_E_DUPLICATE_NAME = 41
VIX_E_ARGUMENT_TOO_BIG = 44
VIX_E_INVALID_HANDLE = 1000
VIX_E_NOT_SUPPORTED_ON_HANDLE_TYPE = 1001
VIX_E_TOO_MANY_HANDLES = 1002
VIX_E_NOT_FOUND = 2000
VIX_E_TYPE_MISMATCH = 2001
VIX_E_INVALID_XML = 2002
VIX_E_TIMEOUT_WAITING_FOR_TOOLS = 3000
VIX_E_UNRECOGNIZED_COMMAND = 3001
VIX_E_OP_NOT_SUPPORTED_ON_GUEST = 3003
VIX_E_PROGRAM_NOT_STARTED = 3004
VIX_E_CANNOT_START_READ_ONLY_VM = 3005
VIX_E_VM_NOT_RUNNING = 3006
VIX_E_VM_IS_RUNNING = 3007
VIX_E_CANNOT_CONNECT_TO_VM = 3008
VIX_E_POWEROP_SCRIPTS_NOT_AVAILABLE = 3009
VIX_E_NO_GUEST_OS_INSTALLED = 3010
VIX_E_VM_INSUFFICIENT_HOST_MEMORY = 3011
VIX_E_SUSPEND_ERROR = 3012
VIX_E_VM_NOT_ENOUGH_CPUS = 3013
VIX_E_HOST_USER_PERMISSIONS = 3014
VIX_E_GUEST_USER_PERMISSIONS = 3015
VIX_E_TOOLS_NOT_RUNNING = 3016
VIX_E_GUEST_OPERATIONS_PROHIBITED = 3017
VIX_E_ANON_GUEST_OPERATIONS_PROHIBITED = 3018
VIX_E_ROOT_GUEST_OPERATIONS_PROHIBITED = 3019
VIX_E_MISSING_ANON_GUEST_ACCOUNT = 3023
VIX_E_CANNOT_AUTHENTICATE_WITH_GUEST = 3024
VIX_E_UNRECOGNIZED_COMMAND_IN_GUEST = 3025
VIX_E_CONSOLE_GUEST_OPERATIONS_PROHIBITED = 3026
VIX_E_MUST_BE_CONSOLE_USER = 3027
VIX_E_VMX_MSG_DIALOG_AND_NO_UI = 3028
VIX_E_OPERATION_NOT_ALLOWED_FOR_LOGIN_TYPE = 3031
VIX_E_LOGIN_TYPE_NOT_SUPPORTED = 3032
VIX_E_EMPTY_PASSWORD_NOT_ALLOWED_IN_GUEST = 3033
VIX_E_INTERACTIVE_SESSION_NOT_PRESENT = 3034
VIX_E_INTERACTIVE_SESSION_USER_MISMATCH = 3035
VIX_E_CANNOT_POWER_ON_VM = 3041
VIX_E_NO_DISPLAY_SERVER = 3043
VIX_E_TOO_MANY_LOGONS = 3046
VIX_E_INVALID_AUTHENTICATION_SESSION = 3047
VIX_E_VM_NOT_FOUND = 4000
VIX_E_NOT_SUPPORTED_FOR_VM_VERSION = 4001
VIX_E_CANNOT_READ_VM_CONFIG = 4002
VIX_E_TEMPLATE_VM = 4003
VIX_E_VM_ALREADY_LOADED = 4004
VIX_E_VM_ALREADY_UP_TO_DATE = 4006
VIX_E_VM_UNSUPPORTED_GUEST = 4011
VIX_E_UNRECOGNIZED_PROPERTY = 6000
VIX_E_INVALID_PROPERTY_VALUE = 6001
VIX_E_READ_ONLY_PROPERTY = 6002
VIX_E_MISSING_REQUIRED_PROPERTY = 6003
VIX_E_INVALID_SERIALIZED_DATA = 6004
VIX_E_PROPERTY_TYPE_MISMATCH = 6005
VIX_E_BAD_VM_INDEX = 8000
VIX_E_INVALID_MESSAGE_HEADER = 10000
VIX_E_INVALID_MESSAGE_BODY = 10001
VIX_E_SNAPSHOT_INVAL = 13000
VIX_E_SNAPSHOT_DUMPER = 13001
VIX_E_SNAPSHOT_DISKLIB = 13002
VIX_E_SNAPSHOT_NOTFOUND = 13003
VIX_E_SNAPSHOT_EXISTS = 13004
VIX_E_SNAPSHOT_VERSION = 13005
VIX_E_SNAPSHOT_NOPERM = 13006
VIX_E_SNAPSHOT_CONFIG = 13007
VIX_E_SNAPSHOT_NOCHANGE = 13008
VIX_E_SNAPSHOT_CHECKPOINT = 13009
VIX_E_SNAPSHOT_LOCKED = 13010
VIX_E_SNAPSHOT_INCONSISTENT = 13011
VIX_E_SNAPSHOT_NAMETOOLONG = 13012
VIX_E_SNAPSHOT_VIXFILE = 13013
VIX_E_SNAPSHOT_DISKLOCKED = 13014
VIX_E_SNAPSHOT_DUPLICATEDDISK = 13015
VIX_E_SNAPSHOT_INDEPENDENTDISK = 13016
VIX_E_SNAPSHOT_NONUNIQUE_NAME = 13017
VIX_E_SNAPSHOT_MEMORY_ON_INDEPENDENT_DISK = 13018
VIX_E_SNAPSHOT_MAXSNAPSHOTS = 13019
VIX_E_SNAPSHOT_MIN_FREE_SPACE = 13020
VIX_E_SNAPSHOT_HIERARCHY_TOODEEP = 13021
VIX_E_SNAPSHOT_NOT_REVERTABLE = 13024
VIX_E_HOST_DISK_INVALID_VALUE = 14003
VIX_E_HOST_DISK_SECTORSIZE = 14004
VIX_E_HOST_FILE_ERROR_EOF = 14005
VIX_E_HOST_NETBLKDEV_HANDSHAKE = 14006
VIX_E_HOST_SOCKET_CREATION_ERROR = 14007
VIX_E_HOST_SERVER_NOT_FOUND = 14008
VIX_E_HOST_NETWORK_CONN_REFUSED = 14009
VIX_E_HOST_TCP_SOCKET_ERROR = 14010
VIX_E_HOST_TCP_CONN_LOST = 14011
VIX_E_HOST_NBD_HASHFILE_VOLUME = 14012
VIX_E_HOST_NBD_HASHFILE_INIT = 14013
VIX_E_DISK_INVAL = 16000
VIX_E_DISK_NOINIT = 16001
VIX_E_DISK_NOIO = 16002
VIX_E_DISK_PARTIALCHAIN = 16003
VIX_E_DISK_NEEDSREPAIR = 16006
VIX_E_DISK_OUTOFRANGE = 16007
VIX_E_DISK_CID_MISMATCH = 16008
VIX_E_DISK_CANTSHRINK = 16009
VIX_E_DISK_PARTMISMATCH = 16010
VIX_E_DISK_UNSUPPORTEDDISKVERSION = 16011
VIX_E_DISK_OPENPARENT = 16012
VIX_E_DISK_NOTSUPPORTED = 16013
VIX_E_DISK_NEEDKEY = 16014
VIX_E_DISK_NOKEYOVERRIDE = 16015
VIX_E_DISK_NOTENCRYPTED = 16016
VIX_E_DISK_NOKEY = 16017
VIX_E_DISK_INVALIDPARTITIONTABLE = 16018
VIX_E_DISK_NOTNORMAL = 16019
VIX_E_DISK_NOTENCDESC = 16020
VIX_E_DISK_NEEDVMFS = 16022
VIX_E_DISK_RAWTOOBIG = 16024
VIX_E_DISK_TOOMANYOPENFILES = 16027
VIX_E_DISK_TOOMANYREDO = 16028
VIX_E_DISK_RAWTOOSMALL = 16029
VIX_E_DISK_INVALIDCHAIN = 16030
VIX_E_DISK_KEY_NOTFOUND = 16052
VIX_E_DISK_SUBSYSTEM_INIT_FAIL = 16053
VIX_E_DISK_INVALID_CONNECTION = 16054
VIX_E_DISK_ENCODING = 16061
VIX_E_DISK_CANTREPAIR = 16062
VIX_E_DISK_INVALIDDISK = 16063
VIX_E_DISK_NOLICENSE = 16064
VIX_E_DISK_NODEVICE = 16065
VIX_E_DISK_UNSUPPORTEDDEVICE = 16066
VIX_E_DISK_CAPACITY_MISMATCH = 16067
VIX_E_DISK_PARENT_NOTALLOWED = 16068
VIX_E_DISK_ATTACH_ROOTLINK = 16069
VIX_E_CRYPTO_UNKNOWN_ALGORITHM = 17000
VIX_E_CRYPTO_BAD_BUFFER_SIZE = 17001
VIX_E_CRYPTO_INVALID_OPERATION = 17002
VIX_E_CRYPTO_RANDOM_DEVICE = 17003
VIX_E_CRYPTO_NEED_PASSWORD = 17004
VIX_E_CRYPTO_BAD_PASSWORD = 17005
VIX_E_CRYPTO_NOT_IN_DICTIONARY = 17006
VIX_E_CRYPTO_NO_CRYPTO = 17007
VIX_E_CRYPTO_ERROR = 17008
VIX_E_CRYPTO_BAD_FORMAT = 17009
VIX_E_CRYPTO_LOCKED = 17010
VIX_E_CRYPTO_EMPTY = 17011
VIX_E_CRYPTO_KEYSAFE_LOCATOR = 17012
VIX_E_CANNOT_CONNECT_TO_HOST = 18000
VIX_E_NOT_FOR_REMOTE_HOST = 18001
VIX_E_INVALID_HOSTNAME_SPECIFICATION = 18002
VIX_E_SCREEN_CAPTURE_ERROR = 19000
VIX_E_SCREEN_CAPTURE_BAD_FORMAT = 19001
VIX_E_SCREEN_CAPTURE_COMPRESSION_FAIL = 19002
VIX_E_SCREEN_CAPTURE_LARGE_DATA = 19003
VIX_E_GUEST_VOLUMES_NOT_FROZEN = 20000
VIX_E_NOT_A_FILE = 20001
VIX_E_NOT_A_DIRECTORY = 20002
VIX_E_NO_SUCH_PROCESS = 20003
VIX_E_FILE_NAME_TOO_LONG = 20004
VIX_E_OPERATION_DISABLED = 20005
VIX_E_TOOLS_INSTALL_NO_IMAGE = 21000
VIX_E_TOOLS_INSTALL_IMAGE_INACCESIBLE = 21001
VIX_E_TOOLS_INSTALL_NO_DEVICE = 21002
VIX_E_TOOLS_INSTALL_DEVICE_NOT_CONNECTED = 21003
VIX_E_TOOLS_INSTALL_CANCELLED = 21004
VIX_E_TOOLS_INSTALL_INIT_FAILED = 21005
VIX_E_TOOLS_INSTALL_AUTO_NOT_SUPPORTED = 21006
VIX_E_TOOLS_INSTALL_GUEST_NOT_READY = 21007
VIX_E_TOOLS_INSTALL_SIG_CHECK_FAILED = 21008
VIX_E_TOOLS_INSTALL_ERROR = 21009
VIX_E_TOOLS_INSTALL_ALREADY_UP_TO_DATE = 21010
VIX_E_TOOLS_INSTALL_IN_PROGRESS = 21011
VIX_E_TOOLS_INSTALL_IMAGE_COPY_FAILED = 21012
VIX_E_WRAPPER_WORKSTATION_NOT_INSTALLED = 22001
VIX_E_WRAPPER_VERSION_NOT_FOUND = 22002
VIX_E_WRAPPER_SERVICEPROVIDER_NOT_FOUND = 22003
VIX_E_WRAPPER_PLAYER_NOT_INSTALLED = 22004
VIX_E_WRAPPER_RUNTIME_NOT_INSTALLED = 22005
VIX_E_WRAPPER_MULTIPLE_SERVICEPROVIDERS = 22006
VIX_E_MNTAPI_MOUNTPT_NOT_FOUND = 24000
VIX_E_MNTAPI_MOUNTPT_IN_USE = 24001
VIX_E_MNTAPI_DISK_NOT_FOUND = 24002
VIX_E_MNTAPI_DISK_NOT_MOUNTED = 24003
VIX_E_MNTAPI_DISK_IS_MOUNTED = 24004
VIX_E_MNTAPI_DISK_NOT_SAFE = 24005
VIX_E_MNTAPI_DISK_CANT_OPEN = 24006
VIX_E_MNTAPI_CANT_READ_PARTS = 24007
VIX_E_MNTAPI_UMOUNT_APP_NOT_FOUND = 24008
VIX_E_MNTAPI_UMOUNT = 24009
VIX_E_MNTAPI_NO_MOUNTABLE_PARTITONS = 24010
VIX_E_MNTAPI_PARTITION_RANGE = 24011
VIX_E_MNTAPI_PERM = 24012
VIX_E_MNTAPI_DICT = 24013
VIX_E_MNTAPI_DICT_LOCKED = 24014
VIX_E_MNTAPI_OPEN_HANDLES = 24015
VIX_E_MNTAPI_CANT_MAKE_VAR_DIR = 24016
VIX_E_MNTAPI_NO_ROOT = 24017
VIX_E_MNTAPI_LOOP_FAILED = 24018
VIX_E_MNTAPI_DAEMON = 24019
VIX_E_MNTAPI_INTERNAL = 24020
VIX_E_MNTAPI_SYSTEM = 24021
VIX_E_MNTAPI_NO_CONNECTION_DETAILS = 24022
VIX_E_MNTAPI_INCOMPATIBLE_VERSION = 24300
VIX_E_MNTAPI_OS_ERROR = 24301
VIX_E_MNTAPI_DRIVE_LETTER_IN_USE = 24302
VIX_E_MNTAPI_DRIVE_LETTER_ALREADY_ASSIGNED = 24303
VIX_E_MNTAPI_VOLUME_NOT_MOUNTED = 24304
VIX_E_MNTAPI_VOLUME_ALREADY_MOUNTED = 24305
VIX_E_MNTAPI_FORMAT_FAILURE = 24306
VIX_E_MNTAPI_NO_DRIVER = 24307
VIX_E_MNTAPI_ALREADY_OPENED = 24308
VIX_E_MNTAPI_ITEM_NOT_FOUND = 24309
VIX_E_MNTAPI_UNSUPPROTED_BOOT_LOADER = 24310
VIX_E_MNTAPI_UNSUPPROTED_OS = 24311
VIX_E_MNTAPI_CODECONVERSION = 24312
VIX_E_MNTAPI_REGWRITE_ERROR = 24313
VIX_E_MNTAPI_UNSUPPORTED_FT_VOLUME = 24314
VIX_E_MNTAPI_PARTITION_NOT_FOUND = 24315
VIX_E_MNTAPI_PUTFILE_ERROR = 24316
VIX_E_MNTAPI_GETFILE_ERROR = 24317
VIX_E_MNTAPI_REG_NOT_OPENED = 24318
VIX_E_MNTAPI_REGDELKEY_ERROR = 24319
VIX_E_MNTAPI_CREATE_PARTITIONTABLE_ERROR = 24320
VIX_E_MNTAPI_OPEN_FAILURE = 24321
VIX_E_MNTAPI_VOLUME_NOT_WRITABLE = 24322
VIX_E_ASYNC_MIXEDMODE_UNSUPPORTED = 26000
VIX_E_NET_HTTP_UNSUPPORTED_PROTOCOL = 30001
VIX_E_NET_HTTP_URL_MALFORMAT = 30003
VIX_E_NET_HTTP_COULDNT_RESOLVE_PROXY = 30005
VIX_E_NET_HTTP_COULDNT_RESOLVE_HOST = 30006
VIX_E_NET_HTTP_COULDNT_CONNECT = 30007
VIX_E_NET_HTTP_HTTP_RETURNED_ERROR = 30022
VIX_E_NET_HTTP_OPERATION_TIMEDOUT = 30028
VIX_E_NET_HTTP_SSL_CONNECT_ERROR = 30035
VIX_E_NET_HTTP_TOO_MANY_REDIRECTS = 30047
VIX_E_NET_HTTP_TRANSFER = 30200
VIX_E_NET_HTTP_SSL_SECURITY = 30201
VIX_E_NET_HTTP_GENERIC = 30202
VIX_ERROR_CODE_MAP = {
VIX_OK: "The operation was successful.",
VIX_E_FAIL: "Unknown error.",
VIX_E_OUT_OF_MEMORY: "Memory allocation failed. Out of memory.",
VIX_E_INVALID_ARG: "One of the parameters was invalid.",
VIX_E_FILE_NOT_FOUND: "A file was not found.",
VIX_E_OBJECT_IS_BUSY: "This function cannot be performed because the handle is executing another function.",
VIX_E_NOT_SUPPORTED: "The operation is not supported.",
VIX_E_FILE_ERROR: "A file access error occurred on the host or guest operating system.",
VIX_E_DISK_FULL: "An error occurred while writing a file; the disk is full. Data has not been saved. Free some disk space and try again.",
VIX_E_INCORRECT_FILE_TYPE: "An error occurred while accessing a file: wrong file type.",
VIX_E_CANCELLED: "The operation was canceled.",
VIX_E_FILE_READ_ONLY: "The file is write-protected.",
VIX_E_FILE_ALREADY_EXISTS: "The file already exists.",
VIX_E_FILE_ACCESS_ERROR: "You do not have access rights to this file.",
VIX_E_REQUIRES_LARGE_FILES: "The file system does not support large files.",
VIX_E_FILE_ALREADY_LOCKED: "The file is already in use.",
VIX_E_VMDB: "The system returned an error. Communication with the virtual machine might have been interrupted.",
VIX_E_NOT_SUPPORTED_ON_REMOTE_OBJECT: "The command is not supported on remote objects.",
VIX_E_FILE_TOO_BIG: "The file is too large for the file system.",
VIX_E_FILE_NAME_INVALID: "The file name is not valid.",
VIX_E_ALREADY_EXISTS: "Already exists.",
VIX_E_BUFFER_TOOSMALL: "Buffer is too small.",
VIX_E_OBJECT_NOT_FOUND: "The request refers to an object that does not exist.",
VIX_E_HOST_NOT_CONNECTED: "Unable to connect to the host.",
VIX_E_INVALID_UTF: "The string parameter has incorrect encoding.",
VIX_E_OPERATION_ALREADY_IN_PROGRESS: "The operation is already in progress.",
VIX_E_UNFINISHED_JOB: "The job has not finished.",
VIX_E_NEED_KEY: "A decryption key is required to perform the operation.",
VIX_E_LICENSE: "This operation is not supported with the current license.",
VIX_E_VM_HOST_DISCONNECTED: "Unable to communicate with the virtual machine's host because it is disconnected.",
VIX_E_AUTHENTICATION_FAIL: "Authentication for encrypted virtual machine failed.",
VIX_E_HOST_CONNECTION_LOST: "The connection to the host was lost.",
VIX_E_DUPLICATE_NAME: "Another object is using this name.",
VIX_E_ARGUMENT_TOO_BIG: "One of the specified arguments is too large.",
VIX_E_INVALID_HANDLE: "The handle is not a valid VIX object.",
VIX_E_NOT_SUPPORTED_ON_HANDLE_TYPE: "The operation is not supported on this type of handle.",
VIX_E_TOO_MANY_HANDLES: "Too many handles are open.",
VIX_E_NOT_FOUND: "Invalid file. A required section of the file is missing.",
VIX_E_TYPE_MISMATCH: "Invalid file. An object has the wrong type.",
VIX_E_INVALID_XML: "Invalid file. The contents might be corrupt.",
VIX_E_TIMEOUT_WAITING_FOR_TOOLS: "A timeout error occurred while waiting for .",
VIX_E_UNRECOGNIZED_COMMAND: "The command is not recognized by the virtual machine.",
VIX_E_OP_NOT_SUPPORTED_ON_GUEST: "The requested operation is not supported on this guest operating system.",
VIX_E_PROGRAM_NOT_STARTED: "A program could not run on the guest operating system.",
VIX_E_CANNOT_START_READ_ONLY_VM: "Cannot power on a read-only virtual machine.",
VIX_E_VM_NOT_RUNNING: "The virtual machine needs to be powered on.",
VIX_E_VM_IS_RUNNING: "The virtual machine should not be powered on. It is already running.",
VIX_E_CANNOT_CONNECT_TO_VM: "Cannot connect to the virtual machine.",
VIX_E_POWEROP_SCRIPTS_NOT_AVAILABLE: "Cannot execute scripts.",
VIX_E_NO_GUEST_OS_INSTALLED: "There is no operating system installed in the virtual machine.",
VIX_E_VM_INSUFFICIENT_HOST_MEMORY: "Not enough physical memory is available to power on this virtual machine.",
VIX_E_SUSPEND_ERROR: "An error occurred while suspending the virtual machine.",
VIX_E_VM_NOT_ENOUGH_CPUS: "This virtual machine is configured to run with 2 CPUs, but the host has only 1 CPU. The virtual machine cannot be powered on.",
VIX_E_HOST_USER_PERMISSIONS: "Insufficient permissions in the host operating system.",
VIX_E_GUEST_USER_PERMISSIONS: "Authentication failure or insufficient permissions in guest operating system.",
VIX_E_TOOLS_NOT_RUNNING: " are not running in the guest.",
VIX_E_GUEST_OPERATIONS_PROHIBITED: "Guest operations are not allowed on this virtual machine.",
VIX_E_ANON_GUEST_OPERATIONS_PROHIBITED: "Anonymous guest operations are not allowed on this virtual machine. You must call VixVM_LoginInGuest before performing guest operations.",
VIX_E_ROOT_GUEST_OPERATIONS_PROHIBITED: "Guest operations are not allowed for the administrative user on this virtual machine.",
VIX_E_MISSING_ANON_GUEST_ACCOUNT: "The virtual machine configuration must specify the guest account name to be used for anonymous guest operations.",
VIX_E_CANNOT_AUTHENTICATE_WITH_GUEST: "The virtual machine cannot authenticate users with guest.",
VIX_E_UNRECOGNIZED_COMMAND_IN_GUEST: "The command is not recognized by .",
VIX_E_CONSOLE_GUEST_OPERATIONS_PROHIBITED: "Guest operations are not allowed for console users on this virtual machine.",
VIX_E_MUST_BE_CONSOLE_USER: "Only the console user can run the command.",
VIX_E_VMX_MSG_DIALOG_AND_NO_UI: "The virtual machine is blocked waiting for a user operation.",
VIX_E_OPERATION_NOT_ALLOWED_FOR_LOGIN_TYPE: "The command is not allowed by this login type.",
VIX_E_LOGIN_TYPE_NOT_SUPPORTED: "This login type is not supported.",
VIX_E_EMPTY_PASSWORD_NOT_ALLOWED_IN_GUEST: "The guest OS does not support empty passwords.",
VIX_E_INTERACTIVE_SESSION_NOT_PRESENT: "The specified guest user must be logged in interactively to perform this operation.",
VIX_E_INTERACTIVE_SESSION_USER_MISMATCH: "The specified guest user does not match the user currently logged in interactively.",
VIX_E_CANNOT_POWER_ON_VM: "The virtual machine could not start.",
VIX_E_NO_DISPLAY_SERVER: "Cannot launch the UI because no display server is present in the current environment.",
VIX_E_TOO_MANY_LOGONS: "The supported number of active authentication sessions has been exceeded.",
VIX_E_INVALID_AUTHENTICATION_SESSION: "The authenticaton session provided does not exist.",
VIX_E_VM_NOT_FOUND: "The virtual machine cannot be found.",
VIX_E_NOT_SUPPORTED_FOR_VM_VERSION: "The operation is not supported for this virtual machine version.",
VIX_E_CANNOT_READ_VM_CONFIG: "Cannot read the virtual machine configuration file.",
VIX_E_TEMPLATE_VM: "Cannot perform this operation on a template virtual machine.",
VIX_E_VM_ALREADY_LOADED: "The virtual machine has already been loaded.",
VIX_E_VM_ALREADY_UP_TO_DATE: "The virtual machine is already up-to-date.",
VIX_E_VM_UNSUPPORTED_GUEST: "The specified guest operating system is not supported on the host that is the target of the operation.",
VIX_E_UNRECOGNIZED_PROPERTY: "Unrecognized handle property identifier.",
VIX_E_INVALID_PROPERTY_VALUE: "Invalid property value.",
VIX_E_READ_ONLY_PROPERTY: "Cannot change a read-only property.",
VIX_E_MISSING_REQUIRED_PROPERTY: "This handle is missing a required property.",
VIX_E_INVALID_SERIALIZED_DATA: "A serialized object is invalid and cannot be deserialized.",
VIX_E_PROPERTY_TYPE_MISMATCH: "The data provided does not match the property type.",
VIX_E_BAD_VM_INDEX: "The index parameter does not correspond to a result set.",
VIX_E_INVALID_MESSAGE_HEADER: "A message header was corrupted or has the incorrect version.",
VIX_E_INVALID_MESSAGE_BODY: "A message body was corrupted or is missing.",
VIX_E_SNAPSHOT_INVAL: "A snapshot-related error has occurred.",
VIX_E_SNAPSHOT_DUMPER: "Unable to open the snapshot file.",
VIX_E_SNAPSHOT_DISKLIB: "Disk error.",
VIX_E_SNAPSHOT_NOTFOUND: "The snapshot does not exist.",
VIX_E_SNAPSHOT_EXISTS: "The snapshot already exists.",
VIX_E_SNAPSHOT_VERSION: "Snapshots are not allowed on this virtual machine.",
VIX_E_SNAPSHOT_NOPERM: "Insufficient permissions.",
VIX_E_SNAPSHOT_CONFIG: "There is an error in the configuration file.",
VIX_E_SNAPSHOT_NOCHANGE: "The state of the virtual machine has not changed since the last snapshot operation.",
VIX_E_SNAPSHOT_CHECKPOINT: "Unable to save the snapshot file.",
VIX_E_SNAPSHOT_LOCKED: "A snapshot operation is already in progress.",
VIX_E_SNAPSHOT_INCONSISTENT: "The snapshot files are in an inconsistent state.",
VIX_E_SNAPSHOT_NAMETOOLONG: "The filename is too long.",
VIX_E_SNAPSHOT_VIXFILE: "Cannot snapshot all metadata files.",
VIX_E_SNAPSHOT_DISKLOCKED: "One or more of the disks are busy.",
VIX_E_SNAPSHOT_DUPLICATEDDISK: "The virtual disk is used multiple times.",
VIX_E_SNAPSHOT_INDEPENDENTDISK: "Cannot take snapshots of powered on virtual machines with independent disks.",
VIX_E_SNAPSHOT_NONUNIQUE_NAME: "The name does not uniquely identify one snapshot.",
VIX_E_SNAPSHOT_MEMORY_ON_INDEPENDENT_DISK: "Failed to take a memory snapshot because the virtual machine is configured with independent disks.",
VIX_E_SNAPSHOT_MAXSNAPSHOTS: "Exceeded the maximum number of permitted snapshots.",
VIX_E_SNAPSHOT_MIN_FREE_SPACE: "Available free space is less than the configured minimum free space.",
VIX_E_SNAPSHOT_HIERARCHY_TOODEEP: "Snapshot hierarchy is too deep.",
VIX_E_SNAPSHOT_NOT_REVERTABLE: "Cannot revert. The snapshot is .",
VIX_E_HOST_DISK_INVALID_VALUE: "The specified device is not a valid physical disk device.",
VIX_E_HOST_DISK_SECTORSIZE: "The disk sector size check failed.",
VIX_E_HOST_FILE_ERROR_EOF: "Read beyond the end of file.",
VIX_E_HOST_NETBLKDEV_HANDSHAKE: "Error in protocol.",
VIX_E_HOST_SOCKET_CREATION_ERROR: "Unable to create a socket.",
VIX_E_HOST_SERVER_NOT_FOUND: "The specified server could not be contacted.",
VIX_E_HOST_NETWORK_CONN_REFUSED: "The server refused connection.",
VIX_E_HOST_TCP_SOCKET_ERROR: "There was an error in communication.",
VIX_E_HOST_TCP_CONN_LOST: "The connection was lost.",
VIX_E_HOST_NBD_HASHFILE_VOLUME: "NBD_ERR_HASHFILE_VOLUME.",
VIX_E_HOST_NBD_HASHFILE_INIT: "NBD_ERR_HASHFILE_INIT.",
VIX_E_DISK_INVAL: "One of the parameters supplied is invalid.",
VIX_E_DISK_NOINIT: "The disk library has not been initialized.",
VIX_E_DISK_NOIO: "The called function requires the virtual disk to be opened for I/O.",
VIX_E_DISK_PARTIALCHAIN: "The called function cannot be performed on partial chains. Open the parent virtual disk.",
VIX_E_DISK_NEEDSREPAIR: "The specified virtual disk needs repair.",
VIX_E_DISK_OUTOFRANGE: "You have requested access to an area of the virtual disk that is out of bounds.",
VIX_E_DISK_CID_MISMATCH: "The parent virtual disk has been modified since the child was created. Parent virutal disk's content ID does not match with the parent content ID in the child.",
VIX_E_DISK_CANTSHRINK: "The specified virtual disk cannot be shrunk because it is not the parent disk.",
VIX_E_DISK_PARTMISMATCH: "The partition table on the physical disk has changed since the disk was created. Remove the physical disk from the virtual machine, then add it again.",
VIX_E_DISK_UNSUPPORTEDDISKVERSION: "The version of the virtual disk is newer than the version supported by this program.",
VIX_E_DISK_OPENPARENT: "The parent of this virtual disk could not be opened.",
VIX_E_DISK_NOTSUPPORTED: "The specified feature is not supported by this version.",
VIX_E_DISK_NEEDKEY: "One or more required keys were not provided.",
VIX_E_DISK_NOKEYOVERRIDE: "Will not create an unencrypted child of an encrypted disk without explicit request.",
VIX_E_DISK_NOTENCRYPTED: "Not an encrypted disk.",
VIX_E_DISK_NOKEY: "No keys were supplied for encrypting the disk.",
VIX_E_DISK_INVALIDPARTITIONTABLE: "The partition table is invalid.",
VIX_E_DISK_NOTNORMAL: "Only sparse extents with embedded descriptors can be encrypted.",
VIX_E_DISK_NOTENCDESC: "Not an encrypted descriptor file.",
VIX_E_DISK_NEEDVMFS: "The file system is not VMFS.",
VIX_E_DISK_RAWTOOBIG: "The physical disk is too big.",
VIX_E_DISK_TOOMANYOPENFILES: "The host's limit for open files has been exceeded.",
VIX_E_DISK_TOOMANYREDO: "Too many levels of redo logs.",
VIX_E_DISK_RAWTOOSMALL: "The physical disk is too small.",
VIX_E_DISK_INVALIDCHAIN: "Invalid disk chain: cannot mix hosted and managed style disks in the same chain.",
VIX_E_DISK_KEY_NOTFOUND: "The specified key is not found in the disk database.",
VIX_E_DISK_SUBSYSTEM_INIT_FAIL: "One or more required subsystems failed to initialize.",
VIX_E_DISK_INVALID_CONNECTION: "Invalid connection handle.",
VIX_E_DISK_ENCODING: "Disk encoding error.",
VIX_E_DISK_CANTREPAIR: "The disk is corrupted and unrepairable.",
VIX_E_DISK_INVALIDDISK: "The specified file is not a virtual disk.",
VIX_E_DISK_NOLICENSE: "The host is not licensed for this feature.",
VIX_E_DISK_NODEVICE: "The device does not exist.",
VIX_E_DISK_UNSUPPORTEDDEVICE: "The operation is not supported on this type of device.",
VIX_E_DISK_CAPACITY_MISMATCH: "The parent virtual disk's capacity is not the same as child's capacity.",
VIX_E_DISK_PARENT_NOTALLOWED: "Disk type cannot be allowed as parent.",
VIX_E_DISK_ATTACH_ROOTLINK: "Both parent and child virtual disks are root links.",
VIX_E_CRYPTO_UNKNOWN_ALGORITHM: "Security library error.",
VIX_E_CRYPTO_BAD_BUFFER_SIZE: "Security library error.",
VIX_E_CRYPTO_INVALID_OPERATION: "Security library error.",
VIX_E_CRYPTO_RANDOM_DEVICE: "Security library error.",
VIX_E_CRYPTO_NEED_PASSWORD: "A password is required for this operation.",
VIX_E_CRYPTO_BAD_PASSWORD: "Incorrect password.",
VIX_E_CRYPTO_NOT_IN_DICTIONARY: "Security library error.",
VIX_E_CRYPTO_NO_CRYPTO: "Security library error.",
VIX_E_CRYPTO_ERROR: "Security library error.",
VIX_E_CRYPTO_BAD_FORMAT: "Security library error.",
VIX_E_CRYPTO_LOCKED: "Security library error.",
VIX_E_CRYPTO_EMPTY: "Security library error.",
VIX_E_CRYPTO_KEYSAFE_LOCATOR: "Security library error.",
VIX_E_CANNOT_CONNECT_TO_HOST: "Cannot connect to the host.",
VIX_E_NOT_FOR_REMOTE_HOST: "Only a local host can support this feature.",
VIX_E_INVALID_HOSTNAME_SPECIFICATION: "Malformed hostname parameter. For the given service provider, the hostname must be a URL in the form https://<hostname>:<port>/sdk.",
VIX_E_SCREEN_CAPTURE_ERROR: "Could not capture screen.",
VIX_E_SCREEN_CAPTURE_BAD_FORMAT: "Requested unsupported format.",
VIX_E_SCREEN_CAPTURE_COMPRESSION_FAIL: "Could not compress the screen capture.",
VIX_E_SCREEN_CAPTURE_LARGE_DATA: "The screen capture data is larger than the maximum size.",
VIX_E_GUEST_VOLUMES_NOT_FROZEN: "The drives are not frozen.",
VIX_E_NOT_A_FILE: "The object is not a file.",
VIX_E_NOT_A_DIRECTORY: "The object is not a directory.",
VIX_E_NO_SUCH_PROCESS: "No such process.",
VIX_E_FILE_NAME_TOO_LONG: "File name too long.",
VIX_E_OPERATION_DISABLED: "The operation has been disabled by the guest operating system.",
VIX_E_TOOLS_INSTALL_NO_IMAGE: "No .",
VIX_E_TOOLS_INSTALL_IMAGE_INACCESIBLE: "The .",
VIX_E_TOOLS_INSTALL_NO_DEVICE: "The guest operating system does not have a device configured for the .",
VIX_E_TOOLS_INSTALL_DEVICE_NOT_CONNECTED: "The guest operating system device used for installation of .",
VIX_E_TOOLS_INSTALL_CANCELLED: "The .",
VIX_E_TOOLS_INSTALL_INIT_FAILED: "The .",
VIX_E_TOOLS_INSTALL_AUTO_NOT_SUPPORTED: "The .",
VIX_E_TOOLS_INSTALL_GUEST_NOT_READY: " are not running in the guest OS. Automatic upgrade is not possible.",
VIX_E_TOOLS_INSTALL_SIG_CHECK_FAILED: "The .",
VIX_E_TOOLS_INSTALL_ERROR: "The .",
VIX_E_TOOLS_INSTALL_ALREADY_UP_TO_DATE: " are already up to date.",
VIX_E_TOOLS_INSTALL_IN_PROGRESS: "A .",
VIX_E_TOOLS_INSTALL_IMAGE_COPY_FAILED: "Could not copy .",
VIX_E_WRAPPER_WORKSTATION_NOT_INSTALLED: "Service type VIX_SERVICEPROVIDER_VMWARE_WORKSTATION was specified but not installed.",
VIX_E_WRAPPER_VERSION_NOT_FOUND: "The specified version was not found.",
VIX_E_WRAPPER_SERVICEPROVIDER_NOT_FOUND: "The specified service provider was not found.",
VIX_E_WRAPPER_PLAYER_NOT_INSTALLED: "Service type VIX_SERVICEPROVIDER_VMWARE_PLAYER was specified but not installed.",
VIX_E_WRAPPER_RUNTIME_NOT_INSTALLED: "Cannot find support libraries; VIX appears to have not been installed.",
VIX_E_WRAPPER_MULTIPLE_SERVICEPROVIDERS: "Cannot connect with multiple service providers.",
VIX_E_MNTAPI_MOUNTPT_NOT_FOUND: "Could not find the specified mountpoint.",
VIX_E_MNTAPI_MOUNTPT_IN_USE: "The mountpoint is already in use.",
VIX_E_MNTAPI_DISK_NOT_FOUND: "Could not find the specified virtual disk.",
VIX_E_MNTAPI_DISK_NOT_MOUNTED: "The specified disk is not mounted.",
VIX_E_MNTAPI_DISK_IS_MOUNTED: "The specified disk is already mounted.",
VIX_E_MNTAPI_DISK_NOT_SAFE: "It is not safe to mount the virtual disk. It might be attached to a suspended or powered-on virtual machine, or it may be inside a snapshot chain.",
VIX_E_MNTAPI_DISK_CANT_OPEN: "Cannot open the virtual disk.",
VIX_E_MNTAPI_CANT_READ_PARTS: "Cannot read or parse the partition table on the virtual disk.",
VIX_E_MNTAPI_UMOUNT_APP_NOT_FOUND: "Could not find the umount application in a standard system directory such as /bin, /usr/bin, or /sbin.",
VIX_E_MNTAPI_UMOUNT: "The umount command failed.",
VIX_E_MNTAPI_NO_MOUNTABLE_PARTITONS: "The virtual disk does not have any partitions that the host system knows how to mount.",
VIX_E_MNTAPI_PARTITION_RANGE: "An invalid partition number was specified.",
VIX_E_MNTAPI_PERM: "Insufficient permissions to perform this operation.",
VIX_E_MNTAPI_DICT: "Error accessing metadata. You might not have sufficient permission to access this disk or the metadata may be corrupted.",
VIX_E_MNTAPI_DICT_LOCKED: "The metadata for this disk is locked. Check for other running virtual disk mounter applications.",
VIX_E_MNTAPI_OPEN_HANDLES: "Another process is performing an operation on this mounted virtual disk.",
VIX_E_MNTAPI_CANT_MAKE_VAR_DIR: "Cannot create directory '/var/run/vmware/fuse'.",
VIX_E_MNTAPI_NO_ROOT: "This application must be run setuid root.",
VIX_E_MNTAPI_LOOP_FAILED: "A loop device operation failed.",
VIX_E_MNTAPI_DAEMON: "The VMware fuse daemon failed to start.",
VIX_E_MNTAPI_INTERNAL: "An internal error has occurred. Contact VMware support.",
VIX_E_MNTAPI_SYSTEM: "A system call has failed.",
VIX_E_MNTAPI_NO_CONNECTION_DETAILS: "Unable to get vixDiskLib connection details.",
VIX_E_MNTAPI_INCOMPATIBLE_VERSION: "The product version number is lower than the expected version number.",
VIX_E_MNTAPI_OS_ERROR: "There was an operating system error.",
VIX_E_MNTAPI_DRIVE_LETTER_IN_USE: "The specified drive letter is already in use.",
VIX_E_MNTAPI_DRIVE_LETTER_ALREADY_ASSIGNED: "The specified drive letter is already assigned.",
VIX_E_MNTAPI_VOLUME_NOT_MOUNTED: "The specified volume is not mounted.",
VIX_E_MNTAPI_VOLUME_ALREADY_MOUNTED: "The specified volume is already mounted.",
VIX_E_MNTAPI_FORMAT_FAILURE: "Unable to format volume.",
VIX_E_MNTAPI_NO_DRIVER: "Driver not found.",
VIX_E_MNTAPI_ALREADY_OPENED: "A handle to the Volume or DiskSet is already open.",
VIX_E_MNTAPI_ITEM_NOT_FOUND: "Invalid file. A required section of the file is missing.",
VIX_E_MNTAPI_UNSUPPROTED_BOOT_LOADER: "Boot loader not supported.",
VIX_E_MNTAPI_UNSUPPROTED_OS: "The current operating system is not supported.",
VIX_E_MNTAPI_CODECONVERSION: "An error occurred while converting the string.",
VIX_E_MNTAPI_REGWRITE_ERROR: "There was an error writing to the registry.",
VIX_E_MNTAPI_UNSUPPORTED_FT_VOLUME: "Windows NT4 Fault Tolerant volume type is not supported.",
VIX_E_MNTAPI_PARTITION_NOT_FOUND: "The specified partition was not found.",
VIX_E_MNTAPI_PUTFILE_ERROR: "Putfile error.",
VIX_E_MNTAPI_GETFILE_ERROR: "Getfile error.",
VIX_E_MNTAPI_REG_NOT_OPENED: "Unable to open registry key.",
VIX_E_MNTAPI_REGDELKEY_ERROR: "There was an error deleting the registry key.",
VIX_E_MNTAPI_CREATE_PARTITIONTABLE_ERROR: "An error occurred while creating the partition table.",
VIX_E_MNTAPI_OPEN_FAILURE: "Failed to open DiskSet.",
VIX_E_MNTAPI_VOLUME_NOT_WRITABLE: "The volume is write-protected.",
VIX_E_ASYNC_MIXEDMODE_UNSUPPORTED: "Synchronous and asynchronous I/O on the same disk handle is not allowed.",
VIX_E_NET_HTTP_UNSUPPORTED_PROTOCOL: "The URL provided uses an unsupported protocol.",
VIX_E_NET_HTTP_URL_MALFORMAT: "The URL was not properly formatted.",
VIX_E_NET_HTTP_COULDNT_RESOLVE_PROXY: "Failed to resolve proxy.",
VIX_E_NET_HTTP_COULDNT_RESOLVE_HOST: "Failed to resolve host.",
VIX_E_NET_HTTP_COULDNT_CONNECT: "Failed to connect to host or proxy.",
VIX_E_NET_HTTP_HTTP_RETURNED_ERROR: "Server returned HTTP error code >= 400.",
VIX_E_NET_HTTP_OPERATION_TIMEDOUT: "Network operation timed out.",
VIX_E_NET_HTTP_SSL_CONNECT_ERROR: "A problem occurred during the SSL/TLS handshake.",
VIX_E_NET_HTTP_TOO_MANY_REDIRECTS: "Reached the maximum number of redirects.",
VIX_E_NET_HTTP_TRANSFER: "Failure sending/receiving network data.",
VIX_E_NET_HTTP_SSL_SECURITY: "An SSL error occurred.",
VIX_E_NET_HTTP_GENERIC: "A generic HTTP error occurred."
}
+324
View File
@@ -0,0 +1,324 @@
# Copyright 2016 Cloudbase Solutions Srl
# All Rights Reserved.
"""Python bindings for the VDDK vixdisklib library. Superseded by the
openvixdisklib, which avoids the proprietary VDDK SDK.
This module is used by the integration tests in order to cross-check the
openvixdisklib library. ctypes layouts follow `.vddk/vixDiskLib.h`.
"""
import contextlib
import ctypes
import logging
import os
import traceback
from tests.integration import vix_disklib_errors
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
# NBD compression flags
VIXDISKLIB_FLAG_OPEN_COMPRESSION_ZLIB = 16
VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ = 32
VIXDISKLIB_FLAG_OPEN_COMPRESSION_SKIPZ = 64
VIX_SUPPORTED_COMPATIBILITY_MODES = [
"6.0", "6.5", "6.7", "7.0", "8.0"]
class VixDiskLibUidPasswdCreds(ctypes.Structure):
_fields_ = [
("userName", ctypes.c_char_p),
("password", ctypes.c_char_p),
]
class VixDiskLibSessionIdCreds(ctypes.Structure):
_fields_ = [
("cookie", ctypes.c_char_p),
("userName", ctypes.c_char_p),
("key", ctypes.c_char_p),
]
class VixDiskLibCreds(ctypes.Union):
_fields_ = [
("uid", VixDiskLibUidPasswdCreds),
("sessionId", VixDiskLibSessionIdCreds),
]
class VixDiskLibConnectParams(ctypes.Structure):
_fields_ = [
("vmxSpec", ctypes.c_char_p),
("serverName", ctypes.c_char_p),
("thumbPrint", ctypes.c_char_p),
# Note: this is 32bit on Windows
("privateUse", ctypes.c_longlong),
("credType", ctypes.c_uint32),
("creds", VixDiskLibCreds),
("port", ctypes.c_uint32),
("nfcHostPort", ctypes.c_uint32),
("vimApiVer", ctypes.c_char_p),
]
class VixDiskLibConnection(ctypes.Structure):
_fields_ = []
def get_buffer(size):
return ctypes.create_string_buffer(size)
class VixDiskLibHandle(object):
""" Class which acts as a proxy for vixDiskLib-related operations:
"""
def __init__(
self, config_path=None, vixdisklib_compatibility_version=None):
self._vix_disklib = ctypes.cdll.LoadLibrary(
self.get_vix_disklib_name())
self._setup_vix_disklib()
if config_path:
config_path = config_path.encode()
target_versions = VIX_SUPPORTED_COMPATIBILITY_MODES
if vixdisklib_compatibility_version:
target_versions = [vixdisklib_compatibility_version]
LOG.debug("vixDiskLib versions targeted: %s", target_versions)
# NOTE: iterate through all versions and try to initialize using each:
version_used = None
for version in reversed(target_versions):
major_ver = None
minor_ver = None
try:
major_ver, minor_ver = version.split(".")
major_ver = int(major_ver)
minor_ver = int(minor_ver)
except ValueError as ex:
raise ValueError(
"Unsupported vixDiskLib version format '%s'. vixDiskLib "
"compatibility mode must be of the form "
"'$major.$minor'" % version) from ex
try:
self._check_err(self._vix_disklib.VixDiskLib_InitEx(
major_ver, minor_ver, None, None, None, None, config_path))
version_used = version
break
except Exception:
LOG.debug(
"Failed to initialize vixDiskLib using compatibility "
"version '%s'. Trying next version. Error trace: %s",
version, traceback.format_exc())
if not version_used:
raise Exception(
"Could not initialize vixDiskLib with any of the following "
"versions: %s" % target_versions)
LOG.info(
"Successfully initialized vixDiskLib with target version '%s'",
version_used)
@classmethod
def get_vix_disklib_name(cls):
vixDiskLibName = None
if os.name == 'nt':
vixDiskLibName = 'vixDiskLib.dll'
else:
vixDiskLibName = 'libvixDiskLib.so'
return vixDiskLibName
def _setup_vix_disklib(self):
self._vix_disklib.VixDiskLib_InitEx.argtypes = [
ctypes.c_uint32, ctypes.c_uint32, ctypes.c_void_p, ctypes.c_void_p,
ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p]
self._vix_disklib.VixDiskLib_InitEx.restype = ctypes.c_uint64
self._vix_disklib.VixDiskLib_GetErrorText.argtypes = [
ctypes.c_uint64, ctypes.c_char_p]
self._vix_disklib.VixDiskLib_GetErrorText.restype = ctypes.c_void_p
self._vix_disklib.VixDiskLib_FreeErrorText.arg_types = [
ctypes.c_char_p]
self._vix_disklib.VixDiskLib_FreeErrorText.restype = None
self._vix_disklib.VixDiskLib_ListTransportModes.argtypes = []
self._vix_disklib.VixDiskLib_ListTransportModes.restype = (
ctypes.c_char_p)
self._vix_disklib.VixDiskLib_GetTransportMode.argtypes = [
ctypes.c_void_p]
self._vix_disklib.VixDiskLib_GetTransportMode.restype = (
ctypes.c_char_p)
self._vix_disklib.VixDiskLib_ConnectEx.argtypes = [
ctypes.POINTER(VixDiskLibConnectParams), ctypes.c_char,
ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_void_p)]
self._vix_disklib.VixDiskLib_ConnectEx.restype = ctypes.c_uint64
self._vix_disklib.VixDiskLib_Open.argtypes = [
ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint32,
ctypes.POINTER(ctypes.c_void_p)]
self._vix_disklib.VixDiskLib_Open.restype = ctypes.c_uint64
self._vix_disklib.VixDiskLib_Read.argtypes = [
ctypes.c_void_p, ctypes.c_uint64, ctypes.c_uint64, ctypes.c_char_p]
self._vix_disklib.VixDiskLib_Read.restype = ctypes.c_uint64
self._vix_disklib.VixDiskLib_Write.argtypes = [
ctypes.c_void_p, ctypes.c_uint64, ctypes.c_uint64, ctypes.c_char_p]
self._vix_disklib.VixDiskLib_Write.restype = ctypes.c_uint64
self._vix_disklib.VixDiskLib_GetMetadataKeys.argtypes = [
ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint64,
ctypes.POINTER(ctypes.c_uint64)]
self._vix_disklib.VixDiskLib_GetMetadataKeys.restype = ctypes.c_uint64
self._vix_disklib.VixDiskLib_ReadMetadata.argtypes = [
ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_uint64,
ctypes.POINTER(ctypes.c_uint64)]
self._vix_disklib.VixDiskLib_ReadMetadata.restype = ctypes.c_uint64
self._vix_disklib.VixDiskLib_Close.argtypes = [ctypes.c_void_p]
self._vix_disklib.VixDiskLib_Close.restype = ctypes.c_uint64
self._vix_disklib.VixDiskLib_Disconnect.argtypes = [ctypes.c_void_p]
self._vix_disklib.VixDiskLib_Disconnect.restype = ctypes.c_uint64
self._vix_disklib.VixDiskLib_Exit.argtypes = []
self._vix_disklib.VixDiskLib_Exit.restype = None
def _check_err(self, err, allowed_values=[vix_disklib_errors.VIX_OK]):
if err not in allowed_values:
err_msg = self._vix_disklib.VixDiskLib_GetErrorText(err, None)
err_msg_copy = str(ctypes.cast(
err_msg, ctypes.c_char_p).value.decode())
self._vix_disklib.VixDiskLib_FreeErrorText(
ctypes.cast(err_msg, ctypes.c_char_p))
msg = None
if err == vix_disklib_errors.VIX_E_OUT_OF_MEMORY:
msg = (
"The ESXi host performing the CBT export ran out of RAM. "
"The host performing the export is automatically chosen "
"by vCenter, so enough RAM to run the export is required "
"on all hosts. To force the export from the specific host "
"the VM is on, create a Coriolis endpoint with the DNS "
"name/IP address of that host.")
if err == vix_disklib_errors.VIX_E_HOST_NETWORK_CONN_REFUSED:
msg = (
"The ESXi host performing the CBT export refused "
"connection. The host is chosen automatically by vCenter, "
"so please ensure that the Coriolis deployment "
"can dial TCP/902 on all of the ESXi hosts of a vSphere, "
"and that DNS name resolution and firewalls are setup to "
"facilitate this. Alternatively, try connecting Coriolis "
"directly to the specific ESXi host which is running the "
"VM(s) to be migrated by creating a Coriolis endpoint "
"using the DNS name/IP address of the host itself.")
if err == vix_disklib_errors.VIX_E_CANNOT_CONNECT_TO_HOST:
msg = (
"Coriolis lost connection to the ESXi host performing the "
"CBT export. If the Coriolis Endpoint connects to a "
"vSphere host, please try connecting Coriolis to the ESXi "
"host directly. If problem persists, try re-enabling CBT "
"on the VM, or moving it to another ESXi host.")
err_msg = err_msg_copy
if msg:
LOG.debug("Original vixDiskLib error message: %s", err_msg_copy)
err_msg = msg
raise Exception(err_msg)
def get_transport_modes(self):
transport_modes = self._vix_disklib.VixDiskLib_ListTransportModes()
return transport_modes.decode().split(':')
def get_transport_mode(self, disk_handle):
t_mode = self._vix_disklib.VixDiskLib_GetTransportMode(disk_handle)
return t_mode.decode()
@contextlib.contextmanager
def connect(
self, server_name, thumbprint, username, password,
vmx_spec=None, snapshot_ref=None, read_only=True,
transport_modes=None, port=443):
LOG.debug("Connecting VixDiskLib: %s", server_name)
connectParams = VixDiskLibConnectParams()
connectParams.serverName = server_name.encode()
if vmx_spec:
connectParams.vmxSpec = vmx_spec.encode()
if thumbprint:
connectParams.thumbPrint = thumbprint.encode()
connectParams.credType = VIXDISKLIB_CRED_UID
connectParams.creds.uid.userName = username.encode()
connectParams.creds.uid.password = password.encode()
connectParams.port = port
if transport_modes:
transport_modes = transport_modes.encode()
if snapshot_ref:
snapshot_ref = snapshot_ref.encode()
conn = ctypes.c_void_p()
self._check_err(self._vix_disklib.VixDiskLib_ConnectEx(
connectParams, read_only, snapshot_ref, transport_modes,
ctypes.byref(conn)))
try:
yield conn
finally:
self.disconnect(conn)
@contextlib.contextmanager
def open(self, conn, disk_path, flags=VIXDISKLIB_FLAG_OPEN_READ_ONLY):
LOG.debug("Openning VixDiskLib disk: %s", disk_path)
disk_handle = ctypes.c_void_p()
self._check_err(self._vix_disklib.VixDiskLib_Open(
conn, disk_path.encode(), flags, ctypes.byref(disk_handle)))
try:
yield disk_handle
finally:
self.close(disk_handle)
def read(self, disk_handle, start_sector, num_sectors, buf):
self._check_err(self._vix_disklib.VixDiskLib_Read(
disk_handle, start_sector, num_sectors, buf))
def write(self, disk_handle, start_sector, num_sectors, buf):
"""Write ``num_sectors`` from ``buf`` starting at ``start_sector``."""
self._check_err(self._vix_disklib.VixDiskLib_Write(
disk_handle, start_sector, num_sectors, buf))
def close(self, disk_handle):
LOG.debug("Closing VixDiskLib disk handle: %s", disk_handle)
self._check_err(self._vix_disklib.VixDiskLib_Close(disk_handle))
def disconnect(self, conn):
LOG.debug("Disconnecting VixDiskLib")
self._check_err(self._vix_disklib.VixDiskLib_Disconnect(conn))
def exit(self):
self._vix_disklib.VixDiskLib_Exit()
View File
+94
View File
@@ -0,0 +1,94 @@
# Copyright 2026 Cloudbase Solutions Srl
# All Rights Reserved.
[tox]
minversion = 4.0.2
envlist = fmt,flake8,mypy,pep8,py3
skipsdist = True
[vars]
src_path = {toxinidir}/openvixdisklib
tst_path = {toxinidir}/tests
all_path = {[vars]src_path} {[vars]tst_path}
vddk_path = {toxinidir}/.vddk
[testenv]
sitepackages = True
usedevelop = True
setenv =
VIRTUAL_ENV={envdir}
PYTHONDONTWRITEBYTECODE=1
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
allowlist_externals =
bash
[testenv:integration]
description = Run integration tests against a VMware environment.
sitepackages = True
usedevelop = True
setenv =
{[testenv]setenv}
LD_LIBRARY_PATH={[vars]vddk_path}
LD_PRELOAD=
commands =
stestr run --slowest --concurrency 1 \
--test-path tests/integration/ {posargs}
[testenv:integration-coverage]
description = Integration tests with coverage report.
sitepackages = True
usedevelop = True
setenv =
{[testenv:integration]setenv}
deps =
{[testenv]deps}
pytest
pytest-cov
pycobertura
commands =
pytest --cov=openvixdisklib --cov-report=term-missing \
--cov-report=xml -p no:cacheprovider tests/integration/ {posargs}
pycobertura show --format html --source openvixdisklib \
coverage.xml -o coverage.html
[testenv:fmt]
description = Format the code based on the coding style standards.
deps =
ruff
commands =
ruff check --select I --fix {[vars]all_path}
ruff format {[vars]all_path}
[testenv:pep8]
description = Verify the coding style (pep8).
deps =
ruff
commands =
ruff format --diff {[vars]all_path}
ruff check {[vars]all_path}
[testenv:mypy]
description = Type checks (mypy).
deps =
mypy
commands =
mypy {[vars]all_path}
[testenv:flake8]
sitepackages = True
deps =
flake8
commands = flake8 {posargs}
allowlist_externals =
flake8
[flake8]
ignore = E125,E251,W503,W504,E305,E731,E117,W605,F632
exclude = .venv,.git,.tox,dist,build,*.egg
# Same length as ruff.
max-line-length = 88
[stestr]
test_path = ./tests