Merge pull request #1 from petrutlucian94/io_improvements
Allow retrieving compressed chunks
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
- This is a test project meant to bypass/replace VDDK, which is no longer
|
- This is a project meant to bypass/replace VDDK, which is no longer
|
||||||
publicly available.
|
publicly available.
|
||||||
- The end goal is to have a Python library that can be used as a VDDK replacement
|
- The end goal is to have a Python library that can be used as a VDDK replacement
|
||||||
to retrieve VMware disk contents.
|
to retrieve VMware disk contents.
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ OpenVixDiskLib is an open-source Python replacement for VMware VDDK's
|
|||||||
`VixDiskLib` NBD path. It reads and writes VMDK contents over vSphere
|
`VixDiskLib` NBD path. It reads and writes VMDK contents over vSphere
|
||||||
NFC without the proprietary VDDK SDK.
|
NFC without the proprietary VDDK SDK.
|
||||||
|
|
||||||
|
AI tools (Cursor + Grok 4.6) have been heavily used to reverse engineer the
|
||||||
|
NBD and NFC protocols, obtaining a working VDDK replacement in a few hours and
|
||||||
|
comprehensive testing in a matter of days.
|
||||||
|
|
||||||
The Python package is `openvixdisklib` (lowercase, following usual
|
The Python package is `openvixdisklib` (lowercase, following usual
|
||||||
Python naming).
|
Python naming).
|
||||||
|
|
||||||
@@ -18,7 +22,7 @@ Implemented against vCenter 8 / ESXi 8. Default transport is `nbdssl`
|
|||||||
|
|
||||||
- `VixDiskLib_ConnectEx` (UID credentials)
|
- `VixDiskLib_ConnectEx` (UID credentials)
|
||||||
- `VixDiskLib_Open` (datastore path, read-only or read-write)
|
- `VixDiskLib_Open` (datastore path, read-only or read-write)
|
||||||
- `VixDiskLib_Read`
|
- `VixDiskLib_Read` (optional ``skip_decompression`` packs FastLZ extras)
|
||||||
- `VixDiskLib_Write`
|
- `VixDiskLib_Write`
|
||||||
|
|
||||||
Not implemented: compression open flags other than FastLZ, CBT /
|
Not implemented: compression open flags other than FastLZ, CBT /
|
||||||
@@ -112,7 +116,8 @@ tox -e integration -- --runslow
|
|||||||
|
|
||||||
Compare write/read throughput of OpenVixDiskLib and native VDDK
|
Compare write/read throughput of OpenVixDiskLib and native VDDK
|
||||||
(`64KiB`, 129-sector, and `32MiB` transfers; `nbdssl` and `nbd`;
|
(`64KiB`, 129-sector, and `32MiB` transfers; `nbdssl` and `nbd`;
|
||||||
plain and FastLZ):
|
plain, FastLZ, and OpenVixDiskLib FastLZ ``skip_decompression``;
|
||||||
|
AIO sessions 64 KiB×1, 1 MiB×1, 2 MiB×1, and 2 MiB×4).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tox -e perf
|
tox -e perf
|
||||||
|
|||||||
+22
-3
@@ -158,8 +158,26 @@ obtain a file handle or to read sector 0.
|
|||||||
|
|
||||||
### OPEN_SESSION / sockopts / resource pool
|
### OPEN_SESSION / sockopts / resource pool
|
||||||
|
|
||||||
VDDK sends 16 zero bytes (`OPEN_SESSION`; server replies with 16 zeros),
|
`OPEN_SESSION` payload is 16 bytes, little-endian:
|
||||||
12 zero bytes (`SET_SOCK_OPTS`; server returns send/recv buffer sizes
|
|
||||||
|
| Offset | Type | Meaning |
|
||||||
|
| ------ | -------- | ---------------------------------------------------- |
|
||||||
|
| 0 | `uint32` | 0 (unused in captures) |
|
||||||
|
| 4 | `uint32` | AIO buffer size in **bytes** (VDDK default 65536) |
|
||||||
|
| 8 | `uint32` | Buffer count (VDDK ``nfcAio.Session.BufCount``) |
|
||||||
|
| 12 | `uint32` | 0 |
|
||||||
|
|
||||||
|
VDDK config `vixDiskLib.nfcAio.Session.BufSizeIn64KB` is that byte size
|
||||||
|
divided by 64 KiB (`1` → 65536, `32` → 2097152). The server replies
|
||||||
|
with 16 zeros; it still **uses** the requested size for IO extras.
|
||||||
|
A 129-sector read is two fragments at 64 KiB, and one 66048-byte
|
||||||
|
fragment at 2 MiB. Lab ESXi 8 accepted 2 MiB (`BufCount` 1 and 4) and
|
||||||
|
rejected 16 MiB and 32 MiB (`OPEN_SESSION` AIO error). Broadcom's 16 MiB
|
||||||
|
figure is session memory (`size × count`), not a larger extra; the
|
||||||
|
per-buffer max on the wire is 2 MiB. Probe:
|
||||||
|
`docs/probing_samples/vddk_aio_bufsize_probe.py`.
|
||||||
|
|
||||||
|
`SET_SOCK_OPTS` is 12 zero bytes (server returns send/recv buffer sizes
|
||||||
and a `uint32` flag), then `uint32` 1 (`SET_RES_POOL`, log: “Setting
|
and a `uint32` flag), then `uint32` 1 (`SET_RES_POOL`, log: “Setting
|
||||||
Resource Pool(1)”).
|
Resource Pool(1)”).
|
||||||
|
|
||||||
@@ -206,12 +224,13 @@ classic type 4 `NFC_SESSION_COMPLETE`.
|
|||||||
## OpenVixDiskLib
|
## OpenVixDiskLib
|
||||||
|
|
||||||
| Piece | Module |
|
| Piece | Module |
|
||||||
| ----------------------------- | ----------------------------------------------- |
|
| ------------------------------- | --------------------------------------------------------- |
|
||||||
| VIM + authd | `openvixdisklib.nfc_auth.authenticate` |
|
| VIM + authd | `openvixdisklib.nfc_auth.authenticate` |
|
||||||
| Dup fd, skip TLS for NFC | `openvixdisklib.nfc_open.takeover_authd_socket` |
|
| Dup fd, skip TLS for NFC | `openvixdisklib.nfc_open.takeover_authd_socket` |
|
||||||
| Second TLS for nbdssl | `openvixdisklib.nfc_open.wrap_nfcssl_socket` |
|
| Second TLS for nbdssl | `openvixdisklib.nfc_open.wrap_nfcssl_socket` |
|
||||||
| FastLZ for NBD compression | `openvixdisklib.fastlz` (pip `pyfastlz`) |
|
| FastLZ for NBD compression | `openvixdisklib.fastlz` (pip `pyfastlz`) |
|
||||||
| Handshake + AIO + OPEN_FILE | `openvixdisklib.nfc_open.open_disk` |
|
| Handshake + AIO + OPEN_FILE | `openvixdisklib.nfc_open.open_disk` |
|
||||||
|
| AIO extra size / pool count | `open_disk(..., aio_buffer_size=, aio_buffer_count=)` |
|
||||||
| Sector read / write / close | `openvixdisklib.nfc_open.NfcDisk` |
|
| Sector read / write / close | `openvixdisklib.nfc_open.NfcDisk` |
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|||||||
+65
-11
@@ -26,11 +26,13 @@ length = numSectors * sectorSize
|
|||||||
| `VixDiskLib_Read(h, 0, 128, buf)` | IO length 65536 (AIO buffer size), one fragment |
|
| `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 |
|
| `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
|
VDDK does **not** split a `Read` larger than the AIO buffer into
|
||||||
requests. The client sends one AIO message; the server answers with
|
multiple requests. The client sends one AIO message; the server
|
||||||
one or more same-`opId` replies, each carrying at most
|
answers with one or more same-`opId` replies, each carrying at most
|
||||||
`NFC_AIO_BUFFER_SIZE` (65536) data bytes. `NfcAioInitSession` logged
|
the OPEN_SESSION buffer size (VDDK default 65536).
|
||||||
that buffer size and count 4 during open.
|
`vixDiskLib.nfcAio.Session.BufSizeIn64KB=32` advertises 2 MiB; a
|
||||||
|
129-sector read then returns **one** 66048-byte extra, and a 2 MiB +
|
||||||
|
512 read returns 2097152 + 512. See `docs/nfc_open.md` (OPEN_SESSION).
|
||||||
|
|
||||||
Sparse regions are still transferred as zeros. A read of 8 sectors at
|
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.
|
LBA 8 on this disk was 4096 zero bytes on the wire, not a skip.
|
||||||
@@ -82,21 +84,22 @@ payload + `chunkLength` data bytes.
|
|||||||
Reply payload (handle is zeroed; lengths describe this fragment):
|
Reply payload (handle is zeroed; lengths describe this fragment):
|
||||||
|
|
||||||
| Offset | Type | Meaning |
|
| Offset | Type | Meaning |
|
||||||
| ------ | -------- | ----------------------------------------------- |
|
| ------ | -------- | -------------------------------------------------------------------- |
|
||||||
| 0 | `uint64` | `0` |
|
| 0 | `uint64` | `0` |
|
||||||
| 8 | `uint64` | `1` (read) |
|
| 8 | `uint64` | `1` (read) |
|
||||||
| 16 | `uint64` | Byte offset of the **request** |
|
| 16 | `uint64` | Byte offset of the **request** on disk |
|
||||||
| 24 | `uint32` | Total request length |
|
| 24 | `uint32` | Total request length |
|
||||||
| 28 | `uint32` | Byte offset of this fragment (`0`, `65536`, …) |
|
| 28 | `uint32` | Fragment byte offset **in this request** (`0`, `65536`, …), not disk |
|
||||||
| 32 | `uint32` | This fragment’s byte length |
|
| 32 | `uint32` | This fragment’s uncompressed byte length |
|
||||||
| 36 | `uint32` | Same as offset 32 |
|
| 36 | `uint32` | Same as offset 32, or compressed extra size when type is FastLZ |
|
||||||
| 40 | `uint32` | `0` |
|
| 40 | `uint32` | `0` |
|
||||||
|
|
||||||
When there is a single fragment, offsets 24–31 look like a `uint64`
|
When there is a single fragment, offsets 24–31 look like a `uint64`
|
||||||
length (the fragment offset is 0). The 129-sector capture shows why
|
length (the fragment offset is 0). The 129-sector capture shows why
|
||||||
they are two `uint32`s: fragment 0 has `(66048, 0)` then chunk 65536;
|
they are two `uint32`s: fragment 0 has `(66048, 0)` then chunk 65536;
|
||||||
fragment 1 has `(66048, 65536)` then chunk 512. `0x00010000` at offset
|
fragment 1 has `(66048, 65536)` then chunk 512. `0x00010000` at offset
|
||||||
28 is the byte offset, not a 0-based index.
|
28 is the byte offset, not a 0-based index. Disk byte address of a
|
||||||
|
fragment is request offset (payload 16) plus payload 28.
|
||||||
|
|
||||||
Read loop: receive fragments with that `opId` until the concatenated
|
Read loop: receive fragments with that `opId` until the concatenated
|
||||||
data length equals the request. Use the `uint32` at payload offset 32
|
data length equals the request. Use the `uint32` at payload offset 32
|
||||||
@@ -113,6 +116,9 @@ S: type=7 opId=18 size=44 dest=0 chunk=65536 + 65536 data
|
|||||||
S: type=7 opId=18 size=44 dest=65536 chunk=512 + 512 data
|
S: type=7 opId=18 size=44 dest=65536 chunk=512 + 512 data
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`dest` in that dump is payload offset 28 (`ReadFragment.dest`): 0 and
|
||||||
|
65536 are positions in this 66048-byte read, not sector numbers.
|
||||||
|
|
||||||
## Lab check
|
## Lab check
|
||||||
|
|
||||||
Integration tests create an empty 10 GiB thin disk, write a repeating
|
Integration tests create an empty 10 GiB thin disk, write a repeating
|
||||||
@@ -135,6 +141,54 @@ The integration test writes and then reads the captured VDDK ranges
|
|||||||
(including a 129-sector transfer that must assemble two read
|
(including a 129-sector transfer that must assemble two read
|
||||||
fragments).
|
fragments).
|
||||||
|
|
||||||
|
## Skip decompression (OpenVixDiskLib extension)
|
||||||
|
|
||||||
|
`VixDiskLib_Read` always fills `buf` with uncompressed sector bytes.
|
||||||
|
OpenVixDiskLib can skip FastLZ decode so a backup application can
|
||||||
|
forward the compressed data as-is, avoiding unnecessary re-compression.
|
||||||
|
|
||||||
|
`NfcDisk.readinto(..., skip_decompression=True)` and
|
||||||
|
`VixDiskLibHandle.read(..., skip_decompression=True)` still send one
|
||||||
|
IO request and wait until uncompressed `filled == length`. They do
|
||||||
|
**not** decompress. Extras are packed densely from offset 0 of `buf`.
|
||||||
|
`ReadResult.fragments` describes each extra. Type `2` extras are
|
||||||
|
FastLZ; type `0` fallbacks are raw. Concatenating extras is not a
|
||||||
|
valid FastLZ stream; the caller must use the table to split them.
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| ---------------------- | ------------------------------------------------------------------------------------------------ |
|
||||||
|
| `dest` | Byte offset **in this uncompressed read** (NFC payload 28). Not a disk LBA or VMDK file offset. |
|
||||||
|
| `uncompressed_length` | Uncompressed fragment size (NFC payload 32). |
|
||||||
|
| `compression_type` | `NFC_COMPRESSION_NONE` (0) or `NFC_COMPRESSION_FASTLZ` (2). |
|
||||||
|
| `offset` | Start of this extra in packed `buf` (receive order, densely from 0). |
|
||||||
|
| `length` | Extra size on the wire. |
|
||||||
|
|
||||||
|
Disk byte address of a fragment is `start_sector * 512 + dest`. A
|
||||||
|
129-sector `read` from sector 0 or from sector 1000 still reports
|
||||||
|
`dest=0` and `dest=65536` when extras are 64 KiB.
|
||||||
|
|
||||||
|
`buf` is sized for the uncompressed request, so it is always large
|
||||||
|
enough. Default `read` still decompresses; `fragments` is empty and
|
||||||
|
`compressed_length` is still the extra bytes on the wire.
|
||||||
|
`skip_decompression` with a plain (no FASTLZ) open only records raw
|
||||||
|
extras (`compressed_length == uncompressed_length`).
|
||||||
|
|
||||||
|
This is not `VixDiskLib_Read`. Do not add an open flag for it;
|
||||||
|
compression on the wire is already the FASTLZ open flag.
|
||||||
|
|
||||||
|
A 32 MiB read at 64 KiB extras is 512 fragments in **one** result. A
|
||||||
|
2 MiB OPEN_SESSION extra (`aio_buffer_size=2097152`) is 16 fragments
|
||||||
|
for the same read. One dest PUT per extra is not viable.
|
||||||
|
|
||||||
|
```
|
||||||
|
uncompressed request (offsets in this read, not on disk)
|
||||||
|
|---------------- 64KiB --|-- 64KiB --|-- ... --|
|
||||||
|
dest=0 dest=65536
|
||||||
|
extra (FastLZ or raw) extra (FastLZ or raw)
|
||||||
|
|
||||||
|
buf when skip_decompression=True: extras packed densely from offset 0
|
||||||
|
```
|
||||||
|
|
||||||
## What is still VDDK-only
|
## What is still VDDK-only
|
||||||
|
|
||||||
- zlib and skipz NBD compression flags
|
- zlib and skipz NBD compression flags
|
||||||
|
|||||||
+6
-5
@@ -85,8 +85,9 @@ A 1-sector VDDK write was 572 bytes on the wire: 16 + 44 + 512.
|
|||||||
|
|
||||||
## Fragments and the single reply
|
## Fragments and the single reply
|
||||||
|
|
||||||
`NfcAioInitSession` advertises a 64 KiB buffer. Extra per type-7
|
`OPEN_SESSION` advertises the AIO buffer size (VDDK default 64 KiB;
|
||||||
message is at most that size. VDDK does **not** issue a new `opId` per
|
`BufSizeIn64KB` can raise it). Extra per type-7 message is at most
|
||||||
|
that size. VDDK does **not** issue a new `opId` per
|
||||||
chunk, and it does **not** coalesce separate `VixDiskLib_Write` calls
|
chunk, and it does **not** coalesce separate `VixDiskLib_Write` calls
|
||||||
(eight 8 KiB writes stayed eight IOs). One public write becomes N
|
(eight 8 KiB writes stayed eight IOs). One public write becomes N
|
||||||
client type-7 messages with the **same** `opId`, then **one** 44-byte
|
client type-7 messages with the **same** `opId`, then **one** 44-byte
|
||||||
@@ -105,9 +106,9 @@ for one reply per chunk; raising the window did not match VDDK
|
|||||||
throughput because VDDK pays one RTT per `Write`, not per fragment.
|
throughput because VDDK pays one RTT per `Write`, not per fragment.
|
||||||
|
|
||||||
`NfcAioFlushCoalescedWrites` is server-side (`nfcAioServer.c`), not a
|
`NfcAioFlushCoalescedWrites` is server-side (`nfcAioServer.c`), not a
|
||||||
client merge of API writes. OPEN_SESSION is 16 zero bytes both ways, so
|
client merge of API writes. Buffer count is
|
||||||
the logged AIO buffer count of 4 is a VDDK client default
|
`vixDiskLib.nfcAio.Session.BufCount` (OPEN_SESSION offset 8); size is
|
||||||
(`vixDiskLib.nfcAio.Session.BufCount`), not a server cap.
|
`BufSizeIn64KB` (offset 4, in bytes). `docs/nfc_open.md`.
|
||||||
|
|
||||||
## OpenVixDiskLib
|
## OpenVixDiskLib
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Probe VDDK ``vixDiskLib.nfcAio.Session.BufSizeIn64KB`` vs NFC read extras.
|
||||||
|
|
||||||
|
Not part of the library. Creates a temp lab VM, runs native VDDK over ``nbd``
|
||||||
|
under ``strace``, and prints OPEN_SESSION payloads plus IO reply chunk
|
||||||
|
lengths. BufSizeIn64KB=1 is 64 KiB; 32 is 2 MiB.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
|
||||||
|
os.environ.pop("LD_PRELOAD", None)
|
||||||
|
|
||||||
|
from tests.integration import vixdisklib # noqa: E402
|
||||||
|
from tests.integration.base import ( # noqa: E402
|
||||||
|
SECTOR_SIZE,
|
||||||
|
create_lab_vm,
|
||||||
|
destroy_lab_vm,
|
||||||
|
ensure_vddk_library_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
|
||||||
|
_VDDK = os.path.join(_REPO, ".vddk")
|
||||||
|
_AIO_MAGIC = 0xA100DA7A
|
||||||
|
_AIO_HDR = 16
|
||||||
|
_NFC_AIO_MSG_OPEN_SESSION = 2
|
||||||
|
_NFC_AIO_MSG_IO = 7
|
||||||
|
_NFC_AIO_IO_READ = 1
|
||||||
|
# 129 sectors: two 64 KiB-class fragments today. 4097: 2 MiB + 512.
|
||||||
|
_READS = ((129, "129s"), (4097, "2MiB+512"))
|
||||||
|
|
||||||
|
|
||||||
|
def _vddk_config(directory: str, buf_size_in_64kb: int, buf_count: int) -> str:
|
||||||
|
path = os.path.join(directory, "vddk.config")
|
||||||
|
log = os.path.join(directory, "vddk.log")
|
||||||
|
with open(path, "w", encoding="utf-8") as config:
|
||||||
|
config.write(f"tmpDirectory={directory}\n")
|
||||||
|
config.write(f"log.fileName={log}\n")
|
||||||
|
config.write("log.fileLevel=verbose\n")
|
||||||
|
config.write("vixDiskLib.nfc.LogLevel=4\n")
|
||||||
|
config.write("vixDiskLib.transport.LogLevel=4\n")
|
||||||
|
config.write(f"vixDiskLib.nfcAio.Session.BufSizeIn64KB={buf_size_in_64kb}\n")
|
||||||
|
config.write(f"vixDiskLib.nfcAio.Session.BufCount={buf_count}\n")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _worker(lab_pkl: str, work_dir: str, buf_size_in_64kb: int, buf_count: int) -> None:
|
||||||
|
ensure_vddk_library_path()
|
||||||
|
with open(lab_pkl, "rb") as pickle_file:
|
||||||
|
lab = pickle.load(pickle_file)
|
||||||
|
config_path = _vddk_config(work_dir, buf_size_in_64kb, buf_count)
|
||||||
|
handle = vixdisklib.VixDiskLibHandle(
|
||||||
|
vixdisklib_compatibility_version="8.0", config_path=config_path
|
||||||
|
)
|
||||||
|
kwargs = {
|
||||||
|
"server_name": lab.host,
|
||||||
|
"port": lab.port,
|
||||||
|
"thumbprint": lab.thumbprint,
|
||||||
|
"username": lab.username,
|
||||||
|
"password": lab.password,
|
||||||
|
"vmx_spec": lab.vmx_spec,
|
||||||
|
"transport_modes": "nbd",
|
||||||
|
"read_only": True,
|
||||||
|
}
|
||||||
|
with (
|
||||||
|
handle.connect(**kwargs) as conn,
|
||||||
|
handle.open(
|
||||||
|
conn, lab.disk_path, flags=vixdisklib.VIXDISKLIB_FLAG_OPEN_READ_ONLY
|
||||||
|
) as disk,
|
||||||
|
):
|
||||||
|
print("transport", handle.get_transport_mode(disk), flush=True)
|
||||||
|
for n_sectors, label in _READS:
|
||||||
|
buf = vixdisklib.get_buffer(n_sectors * SECTOR_SIZE)
|
||||||
|
handle.read(disk, 0, n_sectors, buf)
|
||||||
|
print(f"read {label} ok", flush=True)
|
||||||
|
handle.exit()
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_strace_hex(quoted: str) -> bytes:
|
||||||
|
parts = re.findall(r"\\x([0-9a-fA-F]{2})", quoted)
|
||||||
|
return bytes(int(part, 16) for part in parts)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_strace(path: str) -> tuple[list[bytes], list[tuple[int, int, int]]]:
|
||||||
|
"""Return client OPEN_SESSION payloads and **server** read-reply chunks.
|
||||||
|
|
||||||
|
VDDK reads the 16-byte AIO header in one syscall and the payload in
|
||||||
|
the next, so bytes are concatenated per fd before parsing.
|
||||||
|
"""
|
||||||
|
syscall_re = re.compile(r'(read|write|recv|send)\((\d+),\s*"(.*?)"')
|
||||||
|
writes: dict[int, bytearray] = {}
|
||||||
|
reads: dict[int, bytearray] = {}
|
||||||
|
with open(path, encoding="utf-8", errors="replace") as strace_file:
|
||||||
|
for line in strace_file:
|
||||||
|
match = syscall_re.search(line)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
op, fd_s, quoted = match.group(1), match.group(2), match.group(3)
|
||||||
|
buf = _decode_strace_hex(quoted)
|
||||||
|
if not buf:
|
||||||
|
continue
|
||||||
|
fd = int(fd_s)
|
||||||
|
bucket = writes if op in ("write", "send") else reads
|
||||||
|
bucket.setdefault(fd, bytearray()).extend(buf)
|
||||||
|
|
||||||
|
def walk(buf: bytes, collect_open: bool, collect_io: bool) -> None:
|
||||||
|
offset = 0
|
||||||
|
while offset + _AIO_HDR <= len(buf):
|
||||||
|
magic, msg_type, size, _opid = struct.unpack_from("<IIII", buf, offset)
|
||||||
|
if magic != _AIO_MAGIC:
|
||||||
|
offset += 1
|
||||||
|
continue
|
||||||
|
payload = buf[offset + _AIO_HDR : offset + _AIO_HDR + size]
|
||||||
|
if collect_open and msg_type == _NFC_AIO_MSG_OPEN_SESSION and len(payload) >= 16:
|
||||||
|
open_sessions.append(payload[:16])
|
||||||
|
if collect_io and msg_type == _NFC_AIO_MSG_IO and len(payload) >= 40:
|
||||||
|
opcode = struct.unpack_from("<Q", payload, 8)[0]
|
||||||
|
if opcode & 0xFFFFFFFF == _NFC_AIO_IO_READ:
|
||||||
|
dest, chunk_len, extra_len = struct.unpack_from("<III", payload, 28)
|
||||||
|
io_reads.append((dest, chunk_len, extra_len))
|
||||||
|
offset += _AIO_HDR + size
|
||||||
|
|
||||||
|
open_sessions: list[bytes] = []
|
||||||
|
io_reads: list[tuple[int, int, int]] = []
|
||||||
|
for buf in writes.values():
|
||||||
|
walk(bytes(buf), collect_open=True, collect_io=False)
|
||||||
|
for buf in reads.values():
|
||||||
|
walk(bytes(buf), collect_open=False, collect_io=True)
|
||||||
|
return open_sessions, io_reads
|
||||||
|
|
||||||
|
|
||||||
|
def _interesting_log_lines(log_path: str) -> list[str]:
|
||||||
|
keys = (
|
||||||
|
"Buffer Size",
|
||||||
|
"BufCount",
|
||||||
|
"BufSize",
|
||||||
|
"AIO session",
|
||||||
|
"Aio Session",
|
||||||
|
"maximum session",
|
||||||
|
"Req. buffer",
|
||||||
|
)
|
||||||
|
lines: list[str] = []
|
||||||
|
if not os.path.isfile(log_path):
|
||||||
|
return lines
|
||||||
|
with open(log_path, encoding="utf-8", errors="replace") as log_file:
|
||||||
|
for line in log_file:
|
||||||
|
if any(key in line for key in keys):
|
||||||
|
lines.append(line.rstrip())
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def _run_traced(lab_pkl: str, buf_size_in_64kb: int, buf_count: int) -> None:
|
||||||
|
work_dir = tempfile.mkdtemp(prefix=f"vddk-aio-bufsize-{buf_size_in_64kb}-")
|
||||||
|
strace_path = os.path.join(work_dir, "nfc.strace")
|
||||||
|
python = sys.executable
|
||||||
|
cmd = [
|
||||||
|
"strace",
|
||||||
|
"-f",
|
||||||
|
"-x",
|
||||||
|
"-s",
|
||||||
|
"96",
|
||||||
|
"-e",
|
||||||
|
"trace=read,write,readv,writev,send,recv,sendto,recvfrom",
|
||||||
|
"-o",
|
||||||
|
strace_path,
|
||||||
|
python,
|
||||||
|
__file__,
|
||||||
|
"--worker",
|
||||||
|
lab_pkl,
|
||||||
|
work_dir,
|
||||||
|
str(buf_size_in_64kb),
|
||||||
|
str(buf_count),
|
||||||
|
]
|
||||||
|
env = os.environ.copy()
|
||||||
|
env.pop("LD_PRELOAD", None)
|
||||||
|
lib_path = env.get("LD_LIBRARY_PATH", "")
|
||||||
|
env["LD_LIBRARY_PATH"] = _VDDK if not lib_path else f"{_VDDK}:{lib_path}"
|
||||||
|
print(f"\n=== BufSizeIn64KB={buf_size_in_64kb} BufCount={buf_count} ===")
|
||||||
|
print("work_dir", work_dir)
|
||||||
|
proc = subprocess.run(cmd, env=env, check=False, text=True, capture_output=True)
|
||||||
|
sys.stdout.write(proc.stdout)
|
||||||
|
sys.stderr.write(proc.stderr)
|
||||||
|
print("worker exit", proc.returncode)
|
||||||
|
for line in _interesting_log_lines(os.path.join(work_dir, "vddk.log")):
|
||||||
|
print("LOG", line)
|
||||||
|
open_sessions, io_reads = parse_strace(strace_path)
|
||||||
|
for payload in open_sessions:
|
||||||
|
ints = struct.unpack("<IIII", payload)
|
||||||
|
print("OPEN_SESSION hex", payload.hex(), "u32", ints)
|
||||||
|
print("read fragments (dest, chunk_len, extra_len):")
|
||||||
|
for dest, chunk_len, extra_len in io_reads:
|
||||||
|
print(f" dest={dest} chunk_len={chunk_len} extra_len={extra_len}")
|
||||||
|
if io_reads:
|
||||||
|
print("max chunk_len", max(item[1] for item in io_reads))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
if "--worker" in sys.argv:
|
||||||
|
_, lab_pkl, work_dir, buf_size, buf_count = sys.argv[1:]
|
||||||
|
_worker(lab_pkl, work_dir, int(buf_size), int(buf_count))
|
||||||
|
return
|
||||||
|
ensure_vddk_library_path()
|
||||||
|
lab = create_lab_vm()
|
||||||
|
lab_pkl = "/tmp/vddk-aio-bufsize-lab.pkl"
|
||||||
|
try:
|
||||||
|
with open(lab_pkl, "wb") as pickle_file:
|
||||||
|
pickle.dump(lab, pickle_file)
|
||||||
|
print("lab", lab.disk_path, lab.vm_moref)
|
||||||
|
for buf_size, buf_count in ((1, 1), (32, 1)):
|
||||||
|
_run_traced(lab_pkl, buf_size, buf_count)
|
||||||
|
finally:
|
||||||
|
destroy_lab_vm(lab)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -250,7 +250,9 @@ What that comparison showed:
|
|||||||
- Request size stays 44; data is extra after the payload.
|
- Request size stays 44; data is extra after the payload.
|
||||||
- VDDK sends **one** request even when `length > 65536`. The server
|
- VDDK sends **one** request even when `length > 65536`. The server
|
||||||
replies with several type-7 messages that share `opId`, each with a
|
replies with several type-7 messages that share `opId`, each with a
|
||||||
chunk length at payload offset 32 (max 65536).
|
chunk length at payload offset 32 (max = OPEN_SESSION bufSize;
|
||||||
|
default 65536). `vixDiskLib.nfcAio.Session.BufSizeIn64KB=32` makes
|
||||||
|
that 2 MiB (`docs/probing_samples/vddk_aio_bufsize_probe.py`).
|
||||||
- Treating offset 36 as `NFC_DISK` (`2`) was a 1-sector coincidence;
|
- Treating offset 36 as `NFC_DISK` (`2`) was a 1-sector coincidence;
|
||||||
VDDK repeats the byte length there.
|
VDDK repeats the byte length there.
|
||||||
- Zeros on the wire are real transferred zeros, not a sparse skip.
|
- Zeros on the wire are real transferred zeros, not a sparse skip.
|
||||||
|
|||||||
+130
-18
@@ -25,6 +25,7 @@ import os
|
|||||||
import socket
|
import socket
|
||||||
import ssl
|
import ssl
|
||||||
import struct
|
import struct
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from openvixdisklib import fastlz
|
from openvixdisklib import fastlz
|
||||||
from openvixdisklib.nfc_auth import NfcAuthSession, _ssl_client_context
|
from openvixdisklib.nfc_auth import NfcAuthSession, _ssl_client_context
|
||||||
@@ -34,9 +35,12 @@ NFC_AIO_MAGIC = 0xA100DA7A
|
|||||||
NFC_AIO_HDR_SIZE = 16
|
NFC_AIO_HDR_SIZE = 16
|
||||||
NFC_SECTOR_SIZE = 512
|
NFC_SECTOR_SIZE = 512
|
||||||
NFC_PROTOCOL_VERSION = 11
|
NFC_PROTOCOL_VERSION = 11
|
||||||
# Max data bytes in one AIO IO request/reply fragment
|
# Max data bytes in one AIO IO request/reply fragment. Sent as
|
||||||
# (NfcAioInitSession buffer).
|
# OPEN_SESSION ``bufSize`` (VDDK ``vixDiskLib.nfcAio.Session.BufSizeIn64KB``
|
||||||
|
# times 64 KiB). ESXi read extras use this size; 2 MiB (32) works on
|
||||||
|
# ESXi 8, 16 MiB and 32 MiB do not.
|
||||||
NFC_AIO_BUFFER_SIZE = 65536
|
NFC_AIO_BUFFER_SIZE = 65536
|
||||||
|
NFC_AIO_BUFFER_COUNT = 1
|
||||||
|
|
||||||
# Classic NFC message types observed on the wire (uint32 at offset 0).
|
# Classic NFC message types observed on the wire (uint32 at offset 0).
|
||||||
NFC_MSG_SESSION_COMPLETE = 4
|
NFC_MSG_SESSION_COMPLETE = 4
|
||||||
@@ -78,6 +82,46 @@ NFC_COMPRESSION_NONE = 0
|
|||||||
NFC_COMPRESSION_FASTLZ = 2
|
NFC_COMPRESSION_FASTLZ = 2
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ReadFragment:
|
||||||
|
"""One NFC AIO extra in a packed skip-decompression ``buf``.
|
||||||
|
|
||||||
|
Views into ``buf`` (``buf[offset:offset + length]``) are valid
|
||||||
|
until the next ``read`` into the same buffer.
|
||||||
|
|
||||||
|
``dest`` is NFC payload offset 28: the byte offset of this fragment
|
||||||
|
**inside this uncompressed read**, starting at 0. It is not a disk
|
||||||
|
LBA and not a byte offset from the start of the VMDK. The disk byte
|
||||||
|
address is ``start_sector * sector_size + dest``.
|
||||||
|
|
||||||
|
``offset`` is where this extra sits in packed ``buf`` (densely from
|
||||||
|
0 in receive order). ``length`` is the extra on the wire.
|
||||||
|
``uncompressed_length`` is NFC payload offset 32.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dest: int
|
||||||
|
uncompressed_length: int
|
||||||
|
compression_type: int
|
||||||
|
offset: int
|
||||||
|
length: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ReadResult:
|
||||||
|
"""Outcome of ``NfcDisk.readinto`` / ``VixDiskLibHandle.read``.
|
||||||
|
|
||||||
|
``compressed_length`` is bytes of extra on the wire. With
|
||||||
|
``skip_decompression=True``, extras are packed in ``buf`` from
|
||||||
|
offset 0 and ``fragments`` describes them. The decompressing path
|
||||||
|
sets ``fragments`` to empty so callers can still use the lengths
|
||||||
|
for metrics.
|
||||||
|
"""
|
||||||
|
|
||||||
|
uncompressed_length: int
|
||||||
|
compressed_length: int
|
||||||
|
fragments: tuple[ReadFragment, ...]
|
||||||
|
|
||||||
|
|
||||||
class NfcProtocolError(ConnectionError):
|
class NfcProtocolError(ConnectionError):
|
||||||
"""Raised when an NFC message is malformed or reports failure."""
|
"""Raised when an NFC message is malformed or reports failure."""
|
||||||
|
|
||||||
@@ -162,6 +206,20 @@ def _writable_bytes(buf: bytearray | memoryview, length: int) -> memoryview:
|
|||||||
return raw[:length]
|
return raw[:length]
|
||||||
|
|
||||||
|
|
||||||
|
def _aio_extra_len(ctype: int, body: bytes, chunk_len: int) -> int:
|
||||||
|
"""Return this fragment's extra size on the wire."""
|
||||||
|
if ctype == NFC_COMPRESSION_FASTLZ:
|
||||||
|
extra_len = struct.unpack_from("<I", body, 36)[0]
|
||||||
|
if extra_len < 1:
|
||||||
|
raise NfcProtocolError(
|
||||||
|
f"FastLZ extra length {extra_len} is invalid, chunk {chunk_len}"
|
||||||
|
)
|
||||||
|
return extra_len
|
||||||
|
if ctype == NFC_COMPRESSION_NONE:
|
||||||
|
return chunk_len
|
||||||
|
raise NfcProtocolError(f"unsupported NFC IO compression type {ctype}")
|
||||||
|
|
||||||
|
|
||||||
def _send_nfc_msg(sock: socket.socket, msg_type: int, body: bytes = b"") -> None:
|
def _send_nfc_msg(sock: socket.socket, msg_type: int, body: bytes = b"") -> None:
|
||||||
if len(body) > NFC_MSG_SIZE - 4:
|
if len(body) > NFC_MSG_SIZE - 4:
|
||||||
raise ValueError("NFC classic message body too large")
|
raise ValueError("NFC classic message body too large")
|
||||||
@@ -200,6 +258,8 @@ class NfcDisk:
|
|||||||
handle: int,
|
handle: int,
|
||||||
sector_size: int,
|
sector_size: int,
|
||||||
compression: int = NFC_COMPRESSION_NONE,
|
compression: int = NFC_COMPRESSION_NONE,
|
||||||
|
aio_buffer_size: int = NFC_AIO_BUFFER_SIZE,
|
||||||
|
aio_buffer_count: int = NFC_AIO_BUFFER_COUNT,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Wrap an AIO session that already has ``path`` open.
|
"""Wrap an AIO session that already has ``path`` open.
|
||||||
|
|
||||||
@@ -210,6 +270,11 @@ class NfcDisk:
|
|||||||
sector_size: Sector size from the OPEN_FILE reply.
|
sector_size: Sector size from the OPEN_FILE reply.
|
||||||
compression: NFC IO compression type (``NFC_COMPRESSION_NONE``
|
compression: NFC IO compression type (``NFC_COMPRESSION_NONE``
|
||||||
or ``NFC_COMPRESSION_FASTLZ``).
|
or ``NFC_COMPRESSION_FASTLZ``).
|
||||||
|
aio_buffer_size: OPEN_SESSION extra size in bytes (default
|
||||||
|
``NFC_AIO_BUFFER_SIZE``, 64 KiB). ESXi read extras are
|
||||||
|
at most this large.
|
||||||
|
aio_buffer_count: OPEN_SESSION buffer pool count (default
|
||||||
|
``NFC_AIO_BUFFER_COUNT``).
|
||||||
"""
|
"""
|
||||||
self._sock = sock
|
self._sock = sock
|
||||||
self._op_id = 0
|
self._op_id = 0
|
||||||
@@ -217,6 +282,8 @@ class NfcDisk:
|
|||||||
self.handle = handle
|
self.handle = handle
|
||||||
self.sector_size = sector_size
|
self.sector_size = sector_size
|
||||||
self.compression = compression
|
self.compression = compression
|
||||||
|
self.aio_buffer_size = aio_buffer_size
|
||||||
|
self.aio_buffer_count = aio_buffer_count
|
||||||
self._closed = False
|
self._closed = False
|
||||||
|
|
||||||
def _next_op_id(self) -> int:
|
def _next_op_id(self) -> int:
|
||||||
@@ -264,7 +331,7 @@ class NfcDisk:
|
|||||||
"""Read ``num_sectors`` starting at ``start_sector``.
|
"""Read ``num_sectors`` starting at ``start_sector``.
|
||||||
|
|
||||||
Matches ``VixDiskLib_Read``: one ``NFC_AIO_MSG_IO`` request in
|
Matches ``VixDiskLib_Read``: one ``NFC_AIO_MSG_IO`` request in
|
||||||
byte units. If the length exceeds the AIO buffer (64 KiB) the
|
byte units. If the length exceeds the session AIO buffer the
|
||||||
server replies with several same-``opId`` fragments, which are
|
server replies with several same-``opId`` fragments, which are
|
||||||
placed by the fragment byte offset in the reply (they may arrive
|
placed by the fragment byte offset in the reply (they may arrive
|
||||||
out of order). FASTLZ open requests compression in the opcode;
|
out of order). FASTLZ open requests compression in the opcode;
|
||||||
@@ -285,22 +352,30 @@ class NfcDisk:
|
|||||||
start_sector: int,
|
start_sector: int,
|
||||||
num_sectors: int,
|
num_sectors: int,
|
||||||
buf: bytearray | memoryview,
|
buf: bytearray | memoryview,
|
||||||
) -> int:
|
skip_decompression: bool = False,
|
||||||
|
) -> ReadResult:
|
||||||
"""Read ``num_sectors`` into ``buf`` starting at ``start_sector``.
|
"""Read ``num_sectors`` into ``buf`` starting at ``start_sector``.
|
||||||
|
|
||||||
Uncompressed fragments are received directly into ``buf``. FastLZ
|
Uncompressed fragments are received directly into ``buf``. FastLZ
|
||||||
still decompresses into a temporary buffer, then copies the
|
still decompresses into a temporary buffer, then copies the
|
||||||
result. ``buf`` must be writable and at least
|
result, unless ``skip_decompression`` is set. ``buf`` must be
|
||||||
``num_sectors * sector_size`` bytes (a ``get_buffer`` ctypes
|
writable and at least ``num_sectors * sector_size`` bytes (a
|
||||||
array is wrapped with ``memoryview`` by the VDDK-shaped handle).
|
``get_buffer`` ctypes array is wrapped with ``memoryview`` by
|
||||||
|
the VDDK-shaped handle).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
start_sector: Sector offset from the start of the disk.
|
start_sector: Sector offset from the start of the disk.
|
||||||
num_sectors: Number of sectors to read.
|
num_sectors: Number of sectors to read.
|
||||||
buf: Destination buffer.
|
buf: Destination buffer.
|
||||||
|
skip_decompression: When True, pack NFC extras densely from
|
||||||
|
offset 0 without FastLZ decompress. Fragment metadata
|
||||||
|
is in the returned ``ReadResult``. Completion still
|
||||||
|
uses uncompressed chunk lengths.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The number of bytes written to ``buf``.
|
Lengths of the uncompressed request and of extras on the
|
||||||
|
wire. ``fragments`` is populated only when skipping
|
||||||
|
decompression.
|
||||||
"""
|
"""
|
||||||
if num_sectors < 1:
|
if num_sectors < 1:
|
||||||
raise ValueError("num_sectors must be at least 1")
|
raise ValueError("num_sectors must be at least 1")
|
||||||
@@ -314,6 +389,9 @@ class NfcDisk:
|
|||||||
op_id = self._next_op_id()
|
op_id = self._next_op_id()
|
||||||
self._sock.sendall(_pack_aio_hdr(NFC_AIO_MSG_IO, len(payload), op_id) + payload)
|
self._sock.sendall(_pack_aio_hdr(NFC_AIO_MSG_IO, len(payload), op_id) + payload)
|
||||||
filled = 0
|
filled = 0
|
||||||
|
wire_bytes = 0
|
||||||
|
packed_offset = 0
|
||||||
|
fragments: list[ReadFragment] = []
|
||||||
seen: set[int] = set()
|
seen: set[int] = set()
|
||||||
while filled < length:
|
while filled < length:
|
||||||
rhdr = _recvn(self._sock, NFC_AIO_HDR_SIZE)
|
rhdr = _recvn(self._sock, NFC_AIO_HDR_SIZE)
|
||||||
@@ -340,10 +418,26 @@ class NfcDisk:
|
|||||||
)
|
)
|
||||||
seen.add(dest)
|
seen.add(dest)
|
||||||
ctype = opcode >> 32
|
ctype = opcode >> 32
|
||||||
chunk_view = data[dest : dest + chunk_len]
|
extra_len = _aio_extra_len(ctype, body, chunk_len)
|
||||||
if ctype == NFC_COMPRESSION_FASTLZ:
|
if skip_decompression:
|
||||||
comp_len = struct.unpack_from("<I", body, 36)[0]
|
end = packed_offset + extra_len
|
||||||
extra = _recvn(self._sock, comp_len)
|
if end > length:
|
||||||
|
raise NfcProtocolError(
|
||||||
|
f"packed extras {end} bytes exceed request {length}"
|
||||||
|
)
|
||||||
|
_recvn_into(self._sock, data[packed_offset:end])
|
||||||
|
fragments.append(
|
||||||
|
ReadFragment(
|
||||||
|
dest=dest,
|
||||||
|
uncompressed_length=chunk_len,
|
||||||
|
compression_type=ctype,
|
||||||
|
offset=packed_offset,
|
||||||
|
length=extra_len,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
packed_offset = end
|
||||||
|
elif ctype == NFC_COMPRESSION_FASTLZ:
|
||||||
|
extra = _recvn(self._sock, extra_len)
|
||||||
try:
|
try:
|
||||||
chunk = fastlz.decompress(extra, chunk_len)
|
chunk = fastlz.decompress(extra, chunk_len)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
@@ -354,19 +448,24 @@ class NfcDisk:
|
|||||||
raise NfcProtocolError(
|
raise NfcProtocolError(
|
||||||
f"FastLZ read got {len(chunk)} bytes, expected {chunk_len}"
|
f"FastLZ read got {len(chunk)} bytes, expected {chunk_len}"
|
||||||
)
|
)
|
||||||
chunk_view[:] = chunk
|
data[dest : dest + chunk_len] = chunk
|
||||||
elif ctype == NFC_COMPRESSION_NONE:
|
elif ctype == NFC_COMPRESSION_NONE:
|
||||||
_recvn_into(self._sock, chunk_view)
|
_recvn_into(self._sock, data[dest : dest + chunk_len])
|
||||||
else:
|
else:
|
||||||
raise NfcProtocolError(f"unsupported NFC IO compression type {ctype}")
|
raise NfcProtocolError(f"unsupported NFC IO compression type {ctype}")
|
||||||
|
wire_bytes += extra_len
|
||||||
filled += chunk_len
|
filled += chunk_len
|
||||||
return length
|
return ReadResult(
|
||||||
|
uncompressed_length=length,
|
||||||
|
compressed_length=wire_bytes,
|
||||||
|
fragments=tuple(fragments) if skip_decompression else (),
|
||||||
|
)
|
||||||
|
|
||||||
def write(self, start_sector: int, num_sectors: int, data: bytes) -> None:
|
def write(self, start_sector: int, num_sectors: int, data: bytes) -> None:
|
||||||
"""Write ``num_sectors`` starting at ``start_sector``.
|
"""Write ``num_sectors`` starting at ``start_sector``.
|
||||||
|
|
||||||
Matches ``VixDiskLib_Write``: one ``NFC_AIO_MSG_IO`` ``opId``
|
Matches ``VixDiskLib_Write``: one ``NFC_AIO_MSG_IO`` ``opId``
|
||||||
for the whole call. Chunks larger than the AIO buffer (64 KiB)
|
for the whole call. Chunks larger than the session AIO buffer
|
||||||
are extra fragments with that same ``opId``; the server replies
|
are extra fragments with that same ``opId``; the server replies
|
||||||
once. FASTLZ open compresses each fragment when that shrinks it.
|
once. FASTLZ open compresses each fragment when that shrinks it.
|
||||||
|
|
||||||
@@ -384,7 +483,7 @@ class NfcDisk:
|
|||||||
op_id = self._next_op_id()
|
op_id = self._next_op_id()
|
||||||
frag_offset = 0
|
frag_offset = 0
|
||||||
while frag_offset < length:
|
while frag_offset < length:
|
||||||
chunk = data[frag_offset : frag_offset + NFC_AIO_BUFFER_SIZE]
|
chunk = data[frag_offset : frag_offset + self.aio_buffer_size]
|
||||||
extra = chunk
|
extra = chunk
|
||||||
extra_len = len(chunk)
|
extra_len = len(chunk)
|
||||||
ctype = NFC_COMPRESSION_NONE
|
ctype = NFC_COMPRESSION_NONE
|
||||||
@@ -484,7 +583,10 @@ def _handshake(sock: socket.socket, client_name: str, op_id: str, version: int)
|
|||||||
|
|
||||||
|
|
||||||
def _aio_prepare(disk: NfcDisk) -> None:
|
def _aio_prepare(disk: NfcDisk) -> None:
|
||||||
disk._aio_roundtrip(NFC_AIO_MSG_OPEN_SESSION, bytes(16))
|
open_session = struct.pack(
|
||||||
|
"<IIII", 0, disk.aio_buffer_size, disk.aio_buffer_count, 0
|
||||||
|
)
|
||||||
|
disk._aio_roundtrip(NFC_AIO_MSG_OPEN_SESSION, open_session)
|
||||||
disk._aio_roundtrip(NFC_AIO_MSG_SET_SOCK_OPTS, bytes(12))
|
disk._aio_roundtrip(NFC_AIO_MSG_SET_SOCK_OPTS, bytes(12))
|
||||||
disk._aio_roundtrip(NFC_AIO_MSG_SET_RES_POOL, struct.pack("<I", 1))
|
disk._aio_roundtrip(NFC_AIO_MSG_SET_RES_POOL, struct.pack("<I", 1))
|
||||||
|
|
||||||
@@ -509,6 +611,8 @@ def open_disk(
|
|||||||
version: int = NFC_PROTOCOL_VERSION,
|
version: int = NFC_PROTOCOL_VERSION,
|
||||||
read_only: bool = True,
|
read_only: bool = True,
|
||||||
compression: int = NFC_COMPRESSION_NONE,
|
compression: int = NFC_COMPRESSION_NONE,
|
||||||
|
aio_buffer_size: int = NFC_AIO_BUFFER_SIZE,
|
||||||
|
aio_buffer_count: int = NFC_AIO_BUFFER_COUNT,
|
||||||
) -> NfcDisk:
|
) -> NfcDisk:
|
||||||
"""Open ``disk_path`` over the authenticated authd socket.
|
"""Open ``disk_path`` over the authenticated authd socket.
|
||||||
|
|
||||||
@@ -528,7 +632,13 @@ def open_disk(
|
|||||||
read_only: When True, open with VDDK's read-only NFC flags.
|
read_only: When True, open with VDDK's read-only NFC flags.
|
||||||
compression: ``NFC_COMPRESSION_NONE`` or ``NFC_COMPRESSION_FASTLZ``.
|
compression: ``NFC_COMPRESSION_NONE`` or ``NFC_COMPRESSION_FASTLZ``.
|
||||||
OPEN_FILE flags are unchanged; compression is per IO message.
|
OPEN_FILE flags are unchanged; compression is per IO message.
|
||||||
|
aio_buffer_size: Extra size advertised in OPEN_SESSION (bytes).
|
||||||
|
aio_buffer_count: Buffer pool count advertised in OPEN_SESSION.
|
||||||
"""
|
"""
|
||||||
|
if aio_buffer_size < 1:
|
||||||
|
raise ValueError("aio_buffer_size must be at least 1")
|
||||||
|
if aio_buffer_count < 1:
|
||||||
|
raise ValueError("aio_buffer_count must be at least 1")
|
||||||
if compression not in (NFC_COMPRESSION_NONE, NFC_COMPRESSION_FASTLZ):
|
if compression not in (NFC_COMPRESSION_NONE, NFC_COMPRESSION_FASTLZ):
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
f"NFC compression type {compression} is not supported"
|
f"NFC compression type {compression} is not supported"
|
||||||
@@ -546,6 +656,8 @@ def open_disk(
|
|||||||
handle=0,
|
handle=0,
|
||||||
sector_size=NFC_SECTOR_SIZE,
|
sector_size=NFC_SECTOR_SIZE,
|
||||||
compression=compression,
|
compression=compression,
|
||||||
|
aio_buffer_size=aio_buffer_size,
|
||||||
|
aio_buffer_count=aio_buffer_count,
|
||||||
)
|
)
|
||||||
_aio_prepare(disk)
|
_aio_prepare(disk)
|
||||||
path_b = disk_path.encode("utf-8")
|
path_b = disk_path.encode("utf-8")
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ from pyVmomi import vim
|
|||||||
|
|
||||||
from openvixdisklib import nfc_auth, nfc_open
|
from openvixdisklib import nfc_auth, nfc_open
|
||||||
|
|
||||||
|
ReadResult = nfc_open.ReadResult
|
||||||
|
ReadFragment = nfc_open.ReadFragment
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
VIXDISKLIB_VERSION_MAJOR = 8
|
VIXDISKLIB_VERSION_MAJOR = 8
|
||||||
@@ -264,6 +267,8 @@ class VixDiskLibHandle:
|
|||||||
conn: _Connection,
|
conn: _Connection,
|
||||||
disk_path: str,
|
disk_path: str,
|
||||||
flags: int = VIXDISKLIB_FLAG_OPEN_READ_ONLY,
|
flags: int = VIXDISKLIB_FLAG_OPEN_READ_ONLY,
|
||||||
|
aio_buffer_size: int = nfc_open.NFC_AIO_BUFFER_SIZE,
|
||||||
|
aio_buffer_count: int = nfc_open.NFC_AIO_BUFFER_COUNT,
|
||||||
) -> Iterator[_DiskHandle]:
|
) -> Iterator[_DiskHandle]:
|
||||||
"""Open ``disk_path`` over NFC. Matches ``VixDiskLib_Open``.
|
"""Open ``disk_path`` over NFC. Matches ``VixDiskLib_Open``.
|
||||||
|
|
||||||
@@ -280,6 +285,13 @@ class VixDiskLibHandle:
|
|||||||
the disk read-only; omit it for write.
|
the disk read-only; omit it for write.
|
||||||
``VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ`` compresses
|
``VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ`` compresses
|
||||||
NFC IO. zlib and skipz are not implemented.
|
NFC IO. zlib and skipz are not implemented.
|
||||||
|
aio_buffer_size: NFC AIO extra size in bytes, advertised in
|
||||||
|
OPEN_SESSION. Default 64 KiB. ESXi 8 accepts 2 MiB
|
||||||
|
(``2097152``) and rejects 16 MiB and 32 MiB. This is an
|
||||||
|
OpenVixDiskLib extension (VDDK uses
|
||||||
|
``vixDiskLib.nfcAio.Session.BufSizeIn64KB``).
|
||||||
|
aio_buffer_count: NFC AIO buffer pool count. Default 1.
|
||||||
|
VDDK's default is 4.
|
||||||
"""
|
"""
|
||||||
LOG.debug("Openning VixDiskLib disk: %s", disk_path)
|
LOG.debug("Openning VixDiskLib disk: %s", disk_path)
|
||||||
compression = _nfc_compression(flags)
|
compression = _nfc_compression(flags)
|
||||||
@@ -298,7 +310,12 @@ class VixDiskLibHandle:
|
|||||||
session = nfc_auth.NfcAuthSession(conn.si, ticket, authd_sock, nfc_ssl=nfc_ssl)
|
session = nfc_auth.NfcAuthSession(conn.si, ticket, authd_sock, nfc_ssl=nfc_ssl)
|
||||||
try:
|
try:
|
||||||
disk = nfc_open.open_disk(
|
disk = nfc_open.open_disk(
|
||||||
session, disk_path, read_only=read_only, compression=compression
|
session,
|
||||||
|
disk_path,
|
||||||
|
read_only=read_only,
|
||||||
|
compression=compression,
|
||||||
|
aio_buffer_size=aio_buffer_size,
|
||||||
|
aio_buffer_count=aio_buffer_count,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
authd_sock.close()
|
authd_sock.close()
|
||||||
@@ -315,7 +332,8 @@ class VixDiskLibHandle:
|
|||||||
start_sector: int,
|
start_sector: int,
|
||||||
num_sectors: int,
|
num_sectors: int,
|
||||||
buf: ctypes.Array | bytearray | memoryview,
|
buf: ctypes.Array | bytearray | memoryview,
|
||||||
) -> None:
|
skip_decompression: bool = False,
|
||||||
|
) -> ReadResult:
|
||||||
"""Read ``num_sectors`` from ``start_sector`` into ``buf``.
|
"""Read ``num_sectors`` from ``start_sector`` into ``buf``.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -323,9 +341,27 @@ class VixDiskLibHandle:
|
|||||||
start_sector: First sector to read.
|
start_sector: First sector to read.
|
||||||
num_sectors: Number of sectors to read.
|
num_sectors: Number of sectors to read.
|
||||||
buf: Destination buffer (``get_buffer`` or a writable bytes-like).
|
buf: Destination buffer (``get_buffer`` or a writable bytes-like).
|
||||||
Uncompressed NFC extra is received into this buffer.
|
Uncompressed NFC extra is received into this buffer
|
||||||
|
unless ``skip_decompression`` is True.
|
||||||
|
skip_decompression: OpenVixDiskLib extension. When True,
|
||||||
|
pack NFC extras densely from offset 0 without FastLZ
|
||||||
|
decompress. ``ReadResult.fragments`` lists each extra
|
||||||
|
(``ReadFragment``). ``ReadFragment.dest`` is the byte
|
||||||
|
offset inside this uncompressed read, not a disk
|
||||||
|
offset. Completion still uses uncompressed
|
||||||
|
lengths. With no FASTLZ open flag this only records
|
||||||
|
raw extras (``compressed_length == uncompressed_length``).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Uncompressed and wire lengths. ``fragments`` is empty unless
|
||||||
|
``skip_decompression`` is True.
|
||||||
"""
|
"""
|
||||||
disk_handle.disk.readinto(start_sector, num_sectors, memoryview(buf))
|
return disk_handle.disk.readinto(
|
||||||
|
start_sector,
|
||||||
|
num_sectors,
|
||||||
|
memoryview(buf),
|
||||||
|
skip_decompression=skip_decompression,
|
||||||
|
)
|
||||||
|
|
||||||
def write(
|
def write(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import pytest
|
|||||||
from openvixdisklib import nfc_open
|
from openvixdisklib import nfc_open
|
||||||
from tests.integration.base import SECTOR_SIZE, LabEnv, pattern_bytes
|
from tests.integration.base import SECTOR_SIZE, LabEnv, pattern_bytes
|
||||||
|
|
||||||
|
_1MIB = 1024 * 1024
|
||||||
|
_2MIB = 2 * 1024 * 1024
|
||||||
|
_16MIB = 16 * 1024 * 1024
|
||||||
_32MIB = 32 * 1024 * 1024
|
_32MIB = 32 * 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
@@ -70,14 +73,60 @@ class TestNfcReadWrite:
|
|||||||
[nfc_open.NFC_COMPRESSION_NONE, nfc_open.NFC_COMPRESSION_FASTLZ],
|
[nfc_open.NFC_COMPRESSION_NONE, nfc_open.NFC_COMPRESSION_FASTLZ],
|
||||||
ids=["plain", "fastlz"],
|
ids=["plain", "fastlz"],
|
||||||
)
|
)
|
||||||
def test_write_and_read_32mb(self, lab: LabEnv, compression: int) -> None:
|
@pytest.mark.parametrize(
|
||||||
"""Write 32 MiB (512 AIO chunks) and read it back in one request."""
|
"aio_buffer_count, aio_buffer_size",
|
||||||
|
[
|
||||||
|
(1, nfc_open.NFC_AIO_BUFFER_SIZE),
|
||||||
|
(1, _1MIB),
|
||||||
|
(1, _2MIB),
|
||||||
|
(4, _2MIB),
|
||||||
|
pytest.param(
|
||||||
|
1,
|
||||||
|
_16MIB,
|
||||||
|
marks=pytest.mark.xfail(
|
||||||
|
raises=nfc_open.NfcProtocolError,
|
||||||
|
reason="ESXi 8 rejects OPEN_SESSION bufSize 16 MiB",
|
||||||
|
strict=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
1,
|
||||||
|
_32MIB,
|
||||||
|
marks=pytest.mark.xfail(
|
||||||
|
raises=nfc_open.NfcProtocolError,
|
||||||
|
reason="ESXi 8 rejects OPEN_SESSION bufSize 32 MiB",
|
||||||
|
strict=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
ids=[
|
||||||
|
"count1-64kib",
|
||||||
|
"count1-1mib",
|
||||||
|
"count1-2mib",
|
||||||
|
"count4-2mib",
|
||||||
|
"count1-16mib",
|
||||||
|
"count1-32mib",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_write_and_read_32mb(
|
||||||
|
self,
|
||||||
|
lab: LabEnv,
|
||||||
|
compression: int,
|
||||||
|
aio_buffer_count: int,
|
||||||
|
aio_buffer_size: int,
|
||||||
|
) -> None:
|
||||||
|
"""Write 32 MiB and read it back for several OPEN_SESSION sizes."""
|
||||||
n_sectors = _32MIB // SECTOR_SIZE
|
n_sectors = _32MIB // SECTOR_SIZE
|
||||||
to_write = os.urandom(_32MIB)
|
to_write = os.urandom(_32MIB)
|
||||||
with (
|
with (
|
||||||
lab.authenticate(read_only=False) as session,
|
lab.authenticate(read_only=False) as session,
|
||||||
nfc_open.open_disk(
|
nfc_open.open_disk(
|
||||||
session, lab.disk_path, read_only=False, compression=compression
|
session,
|
||||||
|
lab.disk_path,
|
||||||
|
read_only=False,
|
||||||
|
compression=compression,
|
||||||
|
aio_buffer_size=aio_buffer_size,
|
||||||
|
aio_buffer_count=aio_buffer_count,
|
||||||
) as disk,
|
) as disk,
|
||||||
):
|
):
|
||||||
disk.write(0, n_sectors, to_write)
|
disk.write(0, n_sectors, to_write)
|
||||||
|
|||||||
@@ -3,11 +3,15 @@
|
|||||||
|
|
||||||
"""Exercise the VDDK-compatible openvixdisklib handle against the lab."""
|
"""Exercise the VDDK-compatible openvixdisklib handle against the lab."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pyVim.connect import Disconnect
|
from pyVim.connect import Disconnect
|
||||||
from pyVmomi import vim
|
from pyVmomi import vim
|
||||||
|
|
||||||
|
from openvixdisklib import fastlz, nfc_open
|
||||||
from openvixdisklib import openvixdisklib as vixdisklib
|
from openvixdisklib import openvixdisklib as vixdisklib
|
||||||
|
from openvixdisklib.openvixdisklib import ReadResult
|
||||||
from tests.integration.base import (
|
from tests.integration.base import (
|
||||||
SECTOR_AT_1GB,
|
SECTOR_AT_1GB,
|
||||||
SECTOR_SIZE,
|
SECTOR_SIZE,
|
||||||
@@ -28,6 +32,29 @@ def _virtual_disk_backing(
|
|||||||
raise AssertionError(f"{vm._moId} has no virtual disk")
|
raise AssertionError(f"{vm._moId} has no virtual disk")
|
||||||
|
|
||||||
|
|
||||||
|
_2MIB = 2 * 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
def _rebuild_skip(buf: Any, result: ReadResult) -> bytes:
|
||||||
|
"""Decompress packed skip-decompression extras into uncompressed bytes."""
|
||||||
|
view = buf.raw if hasattr(buf, "raw") else buf
|
||||||
|
out = bytearray(result.uncompressed_length)
|
||||||
|
packed = 0
|
||||||
|
for frag in result.fragments:
|
||||||
|
extra = bytes(view[frag.offset : frag.offset + frag.length])
|
||||||
|
packed += frag.length
|
||||||
|
if frag.compression_type == nfc_open.NFC_COMPRESSION_FASTLZ:
|
||||||
|
chunk = fastlz.decompress(extra, frag.uncompressed_length)
|
||||||
|
elif frag.compression_type == nfc_open.NFC_COMPRESSION_NONE:
|
||||||
|
chunk = extra
|
||||||
|
else:
|
||||||
|
raise AssertionError(f"unexpected compression_type {frag.compression_type}")
|
||||||
|
assert len(chunk) == frag.uncompressed_length
|
||||||
|
out[frag.dest : frag.dest + frag.uncompressed_length] = chunk
|
||||||
|
assert packed == result.compressed_length
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
class TestOpenvixdisklib:
|
class TestOpenvixdisklib:
|
||||||
@pytest.mark.parametrize("transport_mode", ["nbdssl", "nbd"])
|
@pytest.mark.parametrize("transport_mode", ["nbdssl", "nbd"])
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -134,3 +161,62 @@ class TestOpenvixdisklib:
|
|||||||
_wait_for_task(vm.RemoveAllSnapshots_Task())
|
_wait_for_task(vm.RemoveAllSnapshots_Task())
|
||||||
finally:
|
finally:
|
||||||
Disconnect(si)
|
Disconnect(si)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"aio_buffer_size, n_sectors, n_fragments",
|
||||||
|
[
|
||||||
|
(nfc_open.NFC_AIO_BUFFER_SIZE, 128, 1),
|
||||||
|
(nfc_open.NFC_AIO_BUFFER_SIZE, 129, 2),
|
||||||
|
(_2MIB, 129, 1),
|
||||||
|
],
|
||||||
|
ids=["64kib-128s", "64kib-129s", "2mib-129s"],
|
||||||
|
)
|
||||||
|
def test_skip_decompression_fastlz(
|
||||||
|
self,
|
||||||
|
lab: LabEnv,
|
||||||
|
aio_buffer_size: int,
|
||||||
|
n_sectors: int,
|
||||||
|
n_fragments: int,
|
||||||
|
) -> None:
|
||||||
|
"""Pack FastLZ extras and rebuild the same bytes as a normal read."""
|
||||||
|
length = n_sectors * SECTOR_SIZE
|
||||||
|
expected = pattern_bytes(length, b"OVDL-SKIP-")
|
||||||
|
handle = vixdisklib.VixDiskLibHandle(
|
||||||
|
vixdisklib_compatibility_version="8.0", config_path=None
|
||||||
|
)
|
||||||
|
write_buf = vixdisklib.get_buffer(length)
|
||||||
|
plain_buf = vixdisklib.get_buffer(length)
|
||||||
|
skip_buf = vixdisklib.get_buffer(length)
|
||||||
|
write_buf[:length] = expected
|
||||||
|
connect_kwargs = lab.vixdisklib_connect_kwargs(
|
||||||
|
{"allow_untrusted": lab.allow_untrusted, "transport_modes": "nbd"}
|
||||||
|
)
|
||||||
|
flags = vixdisklib.VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ
|
||||||
|
with (
|
||||||
|
handle.connect(**connect_kwargs) as conn,
|
||||||
|
handle.open(
|
||||||
|
conn,
|
||||||
|
lab.disk_path,
|
||||||
|
flags=flags,
|
||||||
|
aio_buffer_size=aio_buffer_size,
|
||||||
|
aio_buffer_count=1,
|
||||||
|
) as disk,
|
||||||
|
):
|
||||||
|
handle.write(disk, 0, n_sectors, write_buf)
|
||||||
|
plain = handle.read(disk, 0, n_sectors, plain_buf)
|
||||||
|
skip = handle.read(disk, 0, n_sectors, skip_buf, skip_decompression=True)
|
||||||
|
assert isinstance(plain, ReadResult)
|
||||||
|
assert plain.fragments == ()
|
||||||
|
assert plain.uncompressed_length == length
|
||||||
|
assert plain.compressed_length <= length
|
||||||
|
assert skip.uncompressed_length == length
|
||||||
|
assert skip.compressed_length <= length
|
||||||
|
assert skip.compressed_length == plain.compressed_length
|
||||||
|
assert len(skip.fragments) == n_fragments
|
||||||
|
dests = {frag.dest for frag in skip.fragments}
|
||||||
|
if n_fragments == 1:
|
||||||
|
assert dests == {0}
|
||||||
|
else:
|
||||||
|
assert dests == {0, nfc_open.NFC_AIO_BUFFER_SIZE}
|
||||||
|
assert plain_buf.raw[:length] == expected
|
||||||
|
assert _rebuild_skip(skip_buf, skip) == expected
|
||||||
|
|||||||
+247
-15
@@ -5,18 +5,71 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from openvixdisklib import nfc_open
|
||||||
from openvixdisklib import openvixdisklib as open_vix
|
from openvixdisklib import openvixdisklib as open_vix
|
||||||
from tests.integration import vixdisklib
|
from tests.integration import vixdisklib
|
||||||
from tests.integration.base import SECTOR_SIZE, LabEnv, pattern_bytes
|
from tests.integration.base import (
|
||||||
|
SECTOR_SIZE,
|
||||||
|
LabEnv,
|
||||||
|
ensure_vddk_library_path,
|
||||||
|
pattern_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
|
||||||
_SIZES = (
|
_SIZES = (
|
||||||
("64KiB", 64 * 1024),
|
("64KiB", 64 * 1024),
|
||||||
("129 sectors", 129 * SECTOR_SIZE),
|
("129 sectors", 129 * SECTOR_SIZE),
|
||||||
("32MiB", 32 * 1024 * 1024),
|
("32MiB", 32 * 1024 * 1024),
|
||||||
)
|
)
|
||||||
|
_1MIB = 1024 * 1024
|
||||||
|
_2MIB = 2 * 1024 * 1024
|
||||||
|
# ESXi 8 accepts 64 KiB, 1 MiB, and 2 MiB extras. 16 MiB and 32 MiB
|
||||||
|
# OPEN_SESSION are rejected (AIO error). Broadcom's 16 MiB cap is
|
||||||
|
# size×count session memory, not a larger extra; VDDK's per-buffer max
|
||||||
|
# is 2 MiB (``BufSizeIn64KB=16`` is 1 MiB).
|
||||||
|
_AIO_SESSIONS = (
|
||||||
|
(nfc_open.NFC_AIO_BUFFER_COUNT, nfc_open.NFC_AIO_BUFFER_SIZE),
|
||||||
|
(1, _1MIB),
|
||||||
|
(1, _2MIB),
|
||||||
|
(4, _2MIB),
|
||||||
|
)
|
||||||
|
_64KIB = 64 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
def _aio_size_label(nbytes: int) -> str:
|
||||||
|
"""Return a short label for an AIO extra size."""
|
||||||
|
if nbytes % (1024 * 1024) == 0:
|
||||||
|
return f"{nbytes // (1024 * 1024)}MiB"
|
||||||
|
if nbytes % 1024 == 0:
|
||||||
|
return f"{nbytes // 1024}KiB"
|
||||||
|
return str(nbytes)
|
||||||
|
|
||||||
|
|
||||||
|
def _vddk_aio_config(
|
||||||
|
directory: str, aio_buffer_size: int, aio_buffer_count: int
|
||||||
|
) -> str:
|
||||||
|
"""Write a temp VDDK config for ``BufSizeIn64KB`` and ``BufCount``."""
|
||||||
|
if aio_buffer_size % _64KIB:
|
||||||
|
raise ValueError(
|
||||||
|
f"VDDK BufSizeIn64KB needs a 64 KiB multiple, got {aio_buffer_size}"
|
||||||
|
)
|
||||||
|
path = os.path.join(directory, "vddk.config")
|
||||||
|
with open(path, "w", encoding="utf-8") as config:
|
||||||
|
config.write(f"tmpDirectory={directory}\n")
|
||||||
|
config.write(
|
||||||
|
f"vixDiskLib.nfcAio.Session.BufSizeIn64KB={aio_buffer_size // _64KIB}\n"
|
||||||
|
)
|
||||||
|
config.write(f"vixDiskLib.nfcAio.Session.BufCount={aio_buffer_count}\n")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
def _connect_extra(lab: LabEnv, module: Any, transport_mode: str) -> dict[str, Any]:
|
def _connect_extra(lab: LabEnv, module: Any, transport_mode: str) -> dict[str, Any]:
|
||||||
@@ -27,37 +80,182 @@ def _connect_extra(lab: LabEnv, module: Any, transport_mode: str) -> dict[str, A
|
|||||||
return extra
|
return extra
|
||||||
|
|
||||||
|
|
||||||
def _time_write_read(
|
def _time_write_read_once(
|
||||||
lab: LabEnv,
|
lab: LabEnv,
|
||||||
module: Any,
|
module: Any,
|
||||||
payload: bytes,
|
payload: bytes,
|
||||||
flags: int = 0,
|
flags: int = 0,
|
||||||
transport_mode: str = "nbdssl",
|
transport_mode: str = "nbdssl",
|
||||||
|
aio_buffer_size: int = nfc_open.NFC_AIO_BUFFER_SIZE,
|
||||||
|
aio_buffer_count: int = nfc_open.NFC_AIO_BUFFER_COUNT,
|
||||||
|
config_dir: str | None = None,
|
||||||
|
skip_decompression: bool = False,
|
||||||
) -> tuple[float, float]:
|
) -> tuple[float, float]:
|
||||||
"""Write ``payload`` at sector 0, read it back, and return durations."""
|
"""Write ``payload`` at sector 0, read it back, and return durations.
|
||||||
|
|
||||||
|
Does not call ``VixDiskLib_Exit``. Native VDDK double-frees if
|
||||||
|
``InitEx``/``Exit`` are paired more than once in the same process.
|
||||||
|
``skip_decompression`` is OpenVixDiskLib FastLZ skip; ``buf`` then
|
||||||
|
holds packed extras, not sector bytes.
|
||||||
|
"""
|
||||||
n_sectors = len(payload) // SECTOR_SIZE
|
n_sectors = len(payload) // SECTOR_SIZE
|
||||||
|
config_path = None
|
||||||
|
if module is vixdisklib:
|
||||||
|
if config_dir is None:
|
||||||
|
raise ValueError("VDDK timings need a config_dir")
|
||||||
|
config_path = _vddk_aio_config(config_dir, aio_buffer_size, aio_buffer_count)
|
||||||
handle = module.VixDiskLibHandle(
|
handle = module.VixDiskLibHandle(
|
||||||
vixdisklib_compatibility_version="8.0", config_path=None
|
vixdisklib_compatibility_version="8.0", config_path=config_path
|
||||||
)
|
)
|
||||||
write_buf = module.get_buffer(len(payload))
|
write_buf = module.get_buffer(len(payload))
|
||||||
read_buf = module.get_buffer(len(payload))
|
read_buf = module.get_buffer(len(payload))
|
||||||
write_buf[: len(payload)] = payload
|
write_buf[: len(payload)] = payload
|
||||||
kwargs = lab.vixdisklib_connect_kwargs(_connect_extra(lab, module, transport_mode))
|
kwargs = lab.vixdisklib_connect_kwargs(_connect_extra(lab, module, transport_mode))
|
||||||
|
open_kwargs: dict[str, Any] = {"flags": flags}
|
||||||
|
if module is open_vix:
|
||||||
|
open_kwargs["aio_buffer_size"] = aio_buffer_size
|
||||||
|
open_kwargs["aio_buffer_count"] = aio_buffer_count
|
||||||
with (
|
with (
|
||||||
handle.connect(**kwargs) as conn,
|
handle.connect(**kwargs) as conn,
|
||||||
handle.open(conn, lab.disk_path, flags=flags) as disk,
|
handle.open(conn, lab.disk_path, **open_kwargs) as disk,
|
||||||
):
|
):
|
||||||
started = time.perf_counter()
|
started = time.perf_counter()
|
||||||
handle.write(disk, 0, n_sectors, write_buf)
|
handle.write(disk, 0, n_sectors, write_buf)
|
||||||
write_s = time.perf_counter() - started
|
write_s = time.perf_counter() - started
|
||||||
read_buf[: len(payload)] = b"\xa5" * len(payload)
|
read_buf[: len(payload)] = b"\xa5" * len(payload)
|
||||||
|
read_kwargs: dict[str, Any] = {}
|
||||||
|
if skip_decompression:
|
||||||
|
read_kwargs["skip_decompression"] = True
|
||||||
started = time.perf_counter()
|
started = time.perf_counter()
|
||||||
handle.read(disk, 0, n_sectors, read_buf)
|
result = handle.read(disk, 0, n_sectors, read_buf, **read_kwargs)
|
||||||
read_s = time.perf_counter() - started
|
read_s = time.perf_counter() - started
|
||||||
|
if skip_decompression:
|
||||||
|
assert result.uncompressed_length == len(payload)
|
||||||
|
assert result.compressed_length <= len(payload)
|
||||||
|
assert result.fragments
|
||||||
|
else:
|
||||||
assert read_buf.raw[: len(payload)] == payload
|
assert read_buf.raw[: len(payload)] == payload
|
||||||
return write_s, read_s
|
return write_s, read_s
|
||||||
|
|
||||||
|
|
||||||
|
def _run_vddk_worker(lab_pkl: str, job_pkl: str, work_dir: str) -> None:
|
||||||
|
"""InitEx once in this process, time one write/read, write result.json."""
|
||||||
|
os.environ.pop("LD_PRELOAD", None)
|
||||||
|
ensure_vddk_library_path()
|
||||||
|
with open(lab_pkl, "rb") as pickle_file:
|
||||||
|
lab = pickle.load(pickle_file)
|
||||||
|
with open(job_pkl, "rb") as pickle_file:
|
||||||
|
job = pickle.load(pickle_file)
|
||||||
|
payload = pattern_bytes(job["nbytes"], f"PERF-{job['label']}-".encode())
|
||||||
|
write_s, read_s = _time_write_read_once(
|
||||||
|
lab,
|
||||||
|
vixdisklib,
|
||||||
|
payload,
|
||||||
|
flags=job["flags"],
|
||||||
|
transport_mode=job["transport_mode"],
|
||||||
|
aio_buffer_size=job["aio_buffer_size"],
|
||||||
|
aio_buffer_count=job["aio_buffer_count"],
|
||||||
|
config_dir=work_dir,
|
||||||
|
)
|
||||||
|
result_path = os.path.join(work_dir, "result.json")
|
||||||
|
with open(result_path, "w", encoding="utf-8") as result_file:
|
||||||
|
json.dump({"write_s": write_s, "read_s": read_s}, result_file)
|
||||||
|
|
||||||
|
|
||||||
|
def _time_vddk_subprocess(
|
||||||
|
lab: LabEnv,
|
||||||
|
label: str,
|
||||||
|
nbytes: int,
|
||||||
|
flags: int,
|
||||||
|
transport_mode: str,
|
||||||
|
aio_buffer_size: int,
|
||||||
|
aio_buffer_count: int,
|
||||||
|
) -> tuple[float, float]:
|
||||||
|
"""Time native VDDK in a child process so InitEx sees this AIO config."""
|
||||||
|
with tempfile.TemporaryDirectory(prefix="vddk-perf-") as work_dir:
|
||||||
|
lab_pkl = os.path.join(work_dir, "lab.pkl")
|
||||||
|
job_pkl = os.path.join(work_dir, "job.pkl")
|
||||||
|
result_path = os.path.join(work_dir, "result.json")
|
||||||
|
with open(lab_pkl, "wb") as pickle_file:
|
||||||
|
pickle.dump(lab, pickle_file)
|
||||||
|
with open(job_pkl, "wb") as pickle_file:
|
||||||
|
pickle.dump(
|
||||||
|
{
|
||||||
|
"label": label,
|
||||||
|
"nbytes": nbytes,
|
||||||
|
"flags": flags,
|
||||||
|
"transport_mode": transport_mode,
|
||||||
|
"aio_buffer_size": aio_buffer_size,
|
||||||
|
"aio_buffer_count": aio_buffer_count,
|
||||||
|
},
|
||||||
|
pickle_file,
|
||||||
|
)
|
||||||
|
env = os.environ.copy()
|
||||||
|
env.pop("LD_PRELOAD", None)
|
||||||
|
pythonpath = env.get("PYTHONPATH", "")
|
||||||
|
env["PYTHONPATH"] = _REPO if not pythonpath else f"{_REPO}:{pythonpath}"
|
||||||
|
proc = subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
os.path.abspath(__file__),
|
||||||
|
"--vddk-worker",
|
||||||
|
lab_pkl,
|
||||||
|
job_pkl,
|
||||||
|
work_dir,
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=env,
|
||||||
|
cwd=_REPO,
|
||||||
|
)
|
||||||
|
if proc.returncode != 0 or not os.path.exists(result_path):
|
||||||
|
raise RuntimeError(
|
||||||
|
"VDDK perf worker failed "
|
||||||
|
f"(exit {proc.returncode}): {proc.stderr}\n{proc.stdout}"
|
||||||
|
)
|
||||||
|
with open(result_path, encoding="utf-8") as result_file:
|
||||||
|
result = json.load(result_file)
|
||||||
|
return float(result["write_s"]), float(result["read_s"])
|
||||||
|
|
||||||
|
|
||||||
|
def _time_write_read(
|
||||||
|
lab: LabEnv,
|
||||||
|
module: Any,
|
||||||
|
label: str,
|
||||||
|
nbytes: int,
|
||||||
|
flags: int = 0,
|
||||||
|
transport_mode: str = "nbdssl",
|
||||||
|
aio_buffer_size: int = nfc_open.NFC_AIO_BUFFER_SIZE,
|
||||||
|
aio_buffer_count: int = nfc_open.NFC_AIO_BUFFER_COUNT,
|
||||||
|
skip_decompression: bool = False,
|
||||||
|
) -> tuple[float, float]:
|
||||||
|
"""Time one write/read; native VDDK runs in a subprocess."""
|
||||||
|
if skip_decompression and module is vixdisklib:
|
||||||
|
raise ValueError("skip_decompression is OpenVixDiskLib-only")
|
||||||
|
if module is vixdisklib:
|
||||||
|
return _time_vddk_subprocess(
|
||||||
|
lab,
|
||||||
|
label,
|
||||||
|
nbytes,
|
||||||
|
flags,
|
||||||
|
transport_mode,
|
||||||
|
aio_buffer_size,
|
||||||
|
aio_buffer_count,
|
||||||
|
)
|
||||||
|
payload = pattern_bytes(nbytes, f"PERF-{label}-".encode())
|
||||||
|
return _time_write_read_once(
|
||||||
|
lab,
|
||||||
|
module,
|
||||||
|
payload,
|
||||||
|
flags=flags,
|
||||||
|
transport_mode=transport_mode,
|
||||||
|
aio_buffer_size=aio_buffer_size,
|
||||||
|
aio_buffer_count=aio_buffer_count,
|
||||||
|
skip_decompression=skip_decompression,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _mib_per_s(nbytes: int, seconds: float) -> float:
|
def _mib_per_s(nbytes: int, seconds: float) -> float:
|
||||||
if seconds <= 0:
|
if seconds <= 0:
|
||||||
return float("inf")
|
return float("inf")
|
||||||
@@ -66,32 +264,54 @@ def _mib_per_s(nbytes: int, seconds: float) -> float:
|
|||||||
|
|
||||||
class TestCompare:
|
class TestCompare:
|
||||||
def test_write_read_throughput(self, lab: LabEnv, vddk: None) -> None:
|
def test_write_read_throughput(self, lab: LabEnv, vddk: None) -> None:
|
||||||
"""Time matching write/read sizes on VDDK and openvixdisklib."""
|
"""Time matching write/read sizes on VDDK and openvixdisklib.
|
||||||
|
|
||||||
|
Prints ``aio_size`` / ``aio_count`` for each OPEN_SESSION
|
||||||
|
(64 KiB×1, 1 MiB×1, 2 MiB×1, 2 MiB×4). VDDK gets those via
|
||||||
|
``vixDiskLib.nfcAio.Session.BufSizeIn64KB`` / ``BufCount`` in a
|
||||||
|
fresh process per row (``VixDiskLib_Exit`` is not loop-safe).
|
||||||
|
``fastlz-skip`` is OpenVixDiskLib ``skip_decompression`` (packed
|
||||||
|
extras, no FastLZ decode); VDDK has no equivalent.
|
||||||
|
"""
|
||||||
libraries = (
|
libraries = (
|
||||||
("vddk", vixdisklib),
|
("vddk", vixdisklib),
|
||||||
("openvixdisklib", open_vix),
|
("openvixdisklib", open_vix),
|
||||||
)
|
)
|
||||||
transports = ("nbdssl", "nbd")
|
transports = ("nbdssl", "nbd")
|
||||||
open_modes = (
|
open_modes = (
|
||||||
("plain", 0),
|
("plain", 0, False),
|
||||||
("fastlz", vixdisklib.VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ),
|
("fastlz", vixdisklib.VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ, False),
|
||||||
|
(
|
||||||
|
"fastlz-skip",
|
||||||
|
vixdisklib.VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ,
|
||||||
|
True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
rows: list[tuple[str, str, str, str, float, float, float, float]] = []
|
rows: list[tuple[str, str, int, str, str, str, float, float, float, float]] = []
|
||||||
for label, nbytes in _SIZES:
|
for label, nbytes in _SIZES:
|
||||||
payload = pattern_bytes(nbytes, f"PERF-{label}-".encode())
|
for aio_buffer_count, aio_buffer_size in _AIO_SESSIONS:
|
||||||
|
aio_label = _aio_size_label(aio_buffer_size)
|
||||||
for transport_mode in transports:
|
for transport_mode in transports:
|
||||||
for mode_name, flags in open_modes:
|
for mode_name, flags, skip_decompression in open_modes:
|
||||||
for name, module in libraries:
|
for name, module in libraries:
|
||||||
|
if skip_decompression and module is vixdisklib:
|
||||||
|
continue
|
||||||
write_s, read_s = _time_write_read(
|
write_s, read_s = _time_write_read(
|
||||||
lab,
|
lab,
|
||||||
module,
|
module,
|
||||||
payload,
|
label,
|
||||||
|
nbytes,
|
||||||
flags=flags,
|
flags=flags,
|
||||||
transport_mode=transport_mode,
|
transport_mode=transport_mode,
|
||||||
|
aio_buffer_size=aio_buffer_size,
|
||||||
|
aio_buffer_count=aio_buffer_count,
|
||||||
|
skip_decompression=skip_decompression,
|
||||||
)
|
)
|
||||||
rows.append(
|
rows.append(
|
||||||
(
|
(
|
||||||
label,
|
label,
|
||||||
|
aio_label,
|
||||||
|
aio_buffer_count,
|
||||||
transport_mode,
|
transport_mode,
|
||||||
mode_name,
|
mode_name,
|
||||||
name,
|
name,
|
||||||
@@ -103,12 +323,15 @@ class TestCompare:
|
|||||||
)
|
)
|
||||||
print()
|
print()
|
||||||
print(
|
print(
|
||||||
f"{'size':<14} {'transport':<10} {'flags':<8} {'library':<16} "
|
f"{'size':<14} {'aio_size':<8} {'aio_count':>9} "
|
||||||
|
f"{'transport':<10} {'flags':<12} {'library':<16} "
|
||||||
f"{'write_s':>10} {'read_s':>10} "
|
f"{'write_s':>10} {'read_s':>10} "
|
||||||
f"{'write_MiB/s':>12} {'read_MiB/s':>12}"
|
f"{'write_MiB/s':>12} {'read_MiB/s':>12}"
|
||||||
)
|
)
|
||||||
for (
|
for (
|
||||||
label,
|
label,
|
||||||
|
aio_label,
|
||||||
|
aio_buffer_count,
|
||||||
transport_mode,
|
transport_mode,
|
||||||
mode_name,
|
mode_name,
|
||||||
name,
|
name,
|
||||||
@@ -118,7 +341,16 @@ class TestCompare:
|
|||||||
read_r,
|
read_r,
|
||||||
) in rows:
|
) in rows:
|
||||||
print(
|
print(
|
||||||
f"{label:<14} {transport_mode:<10} {mode_name:<8} {name:<16} "
|
f"{label:<14} {aio_label:<8} {aio_buffer_count:>9} "
|
||||||
|
f"{transport_mode:<10} {mode_name:<12} {name:<16} "
|
||||||
f"{write_s:10.3f} {read_s:10.3f} "
|
f"{write_s:10.3f} {read_s:10.3f} "
|
||||||
f"{write_r:12.1f} {read_r:12.1f}"
|
f"{write_r:12.1f} {read_r:12.1f}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if sys.argv[1:2] == ["--vddk-worker"]:
|
||||||
|
_, _, lab_pkl, job_pkl, work_dir = sys.argv
|
||||||
|
_run_vddk_worker(lab_pkl, job_pkl, work_dir)
|
||||||
|
else:
|
||||||
|
raise SystemExit("usage: test_compare.py --vddk-worker LAB JOB DIR")
|
||||||
|
|||||||
Reference in New Issue
Block a user