Files
Lucian Petrut 2099591245 Allow retrieving compressed chunks
VDDK always decompresses the chunks it retrieves. However, some
callers may be interested in the compressed chunks, which may
be forwarded to another service involved in the backup process.

We'll add a read flag (skip_decompression). If set, the read
operation will return a "ReadResult" container, containing
a list of fragments and their compressed / decompressed lengths.

If the returned buffer size matches the AIO buffer size (which
now becomes configurable), at most one fragment will be returned.

While at it, we're adding perf tests that check various AIO buffer
sizes, cross checking against VDDK.
2026-09-17 15:29:11 +00:00

677 lines
25 KiB
Python

# 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``, NBD
(``useSSL=0``) stops using ``SSL_write`` and speaks NFC as raw TCP on
that file descriptor. NBDSSL (``useSSL=1``) starts a second TLS
handshake on the same TCP connection (``200 Connect ha-nfcssl``) and
speaks the same NFC frames as TLS application data. This module dups
the authd 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 dataclasses import dataclass
from openvixdisklib import fastlz
from openvixdisklib.nfc_auth import NfcAuthSession, _ssl_client_context
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 request/reply fragment. Sent as
# 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_COUNT = 1
# 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
# High 32 bits of the IO opcode uint64. Captured from VDDK FASTLZ:
# writes that shrink go on the wire as type 2; incompressible writes
# fall back to type 0 with raw extra data.
NFC_COMPRESSION_NONE = 0
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):
"""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)
_enable_tcp_nodelay(raw)
return raw
def wrap_nfcssl_socket(ssock: ssl.SSLSocket, server_hostname: str) -> ssl.SSLSocket:
"""Start the second TLS session used by NBDSSL after PROXY.
After ``200 Connect ha-nfcssl``, authd TLS is finished and
``ha-nfcssl`` expects a new ClientHello on the same TCP connection.
The fd is dup'd so the original authd ``SSLSocket`` can be closed
later without ``SSL_shutdown`` of this NFCSSL session.
Args:
ssock: The TLS socket from ``nfc_auth.connect_authd``.
server_hostname: Host name passed to ``SSLContext.wrap_socket``.
"""
raw = takeover_authd_socket(ssock)
ssl_context = _ssl_client_context(verify=False)
try:
return ssl_context.wrap_socket(raw, server_hostname=server_hostname)
except Exception:
raw.close()
raise
def _enable_tcp_nodelay(sock: socket.socket) -> None:
"""Disable Nagle so a small AIO header is not held back from its extra."""
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
def _recvn(sock: socket.socket, size: int) -> bytes:
buf = bytearray(size)
_recvn_into(sock, memoryview(buf))
return bytes(buf)
def _recvn_into(sock: socket.socket, buf: memoryview) -> None:
"""Read exactly ``len(buf)`` bytes into ``buf``."""
view = buf.cast("B") if buf.format != "B" else buf
filled = 0
n = len(view)
while filled < n:
got = sock.recv_into(view[filled:n])
if not got:
raise NfcProtocolError(
f"NFC connection closed, needed {n} bytes, got {filled}"
)
filled += got
def _writable_bytes(buf: bytearray | memoryview, length: int) -> memoryview:
"""Return a writable ``B`` view of the first ``length`` bytes of ``buf``."""
view = buf if isinstance(buf, memoryview) else memoryview(buf)
if view.readonly:
raise TypeError("read buffer is read-only")
raw = view.cast("B") if view.format != "B" else view
if len(raw) < length:
raise RuntimeError(f"read buffer is {len(raw)} bytes, need {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:
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}, 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,
compression: int = NFC_COMPRESSION_NONE,
aio_buffer_size: int = NFC_AIO_BUFFER_SIZE,
aio_buffer_count: int = NFC_AIO_BUFFER_COUNT,
) -> None:
"""Wrap an AIO session that already has ``path`` open.
Args:
sock: NFC socket after handshake (raw TCP for nbd, TLS for nbdssl).
path: Datastore path that was opened.
handle: Server file handle from OPEN_FILE.
sector_size: Sector size from the OPEN_FILE reply.
compression: NFC IO compression type (``NFC_COMPRESSION_NONE``
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._op_id = 0
self.path = path
self.handle = handle
self.sector_size = sector_size
self.compression = compression
self.aio_buffer_size = aio_buffer_size
self.aio_buffer_count = aio_buffer_count
self._closed = False
def _next_op_id(self) -> int:
op_id = self._op_id
self._op_id += 1
return op_id
def _aio_send(self, msg_type: int, payload: bytes, extra: bytes = b"") -> int:
"""Send one AIO request (header, payload, and extra in one write)."""
op_id = self._next_op_id()
self._sock.sendall(
_pack_aio_hdr(msg_type, len(payload), op_id) + payload + extra
)
return op_id
def _aio_recv_reply(self) -> tuple[int, int, bytes]:
"""Read the next AIO reply. Returns ``(type, op_id, payload)``."""
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}, 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()}")
return rtype, rop, body
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._aio_send(msg_type, payload, extra)
rtype, rop, body = self._aio_recv_reply()
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 session AIO buffer the
server replies with several same-``opId`` fragments, which are
placed by the fragment byte offset in the reply (they may arrive
out of order). FASTLZ open requests compression in the opcode;
each reply fragment may be compressed (type 2) or raw (type 0).
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")
buf = bytearray(num_sectors * self.sector_size)
self.readinto(start_sector, num_sectors, buf)
return bytes(buf)
def readinto(
self,
start_sector: int,
num_sectors: int,
buf: bytearray | memoryview,
skip_decompression: bool = False,
) -> ReadResult:
"""Read ``num_sectors`` into ``buf`` starting at ``start_sector``.
Uncompressed fragments are received directly into ``buf``. FastLZ
still decompresses into a temporary buffer, then copies the
result, unless ``skip_decompression`` is set. ``buf`` must be
writable and at least ``num_sectors * sector_size`` bytes (a
``get_buffer`` ctypes array is wrapped with ``memoryview`` by
the VDDK-shaped handle).
Args:
start_sector: Sector offset from the start of the disk.
num_sectors: Number of sectors to read.
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:
Lengths of the uncompressed request and of extras on the
wire. ``fragments`` is populated only when skipping
decompression.
"""
if num_sectors < 1:
raise ValueError("num_sectors must be at least 1")
length = num_sectors * self.sector_size
data = _writable_bytes(buf, length)
offset = start_sector * self.sector_size
opcode = NFC_AIO_IO_READ | (self.compression << 32)
payload = struct.pack(
"<QQQQIII", self.handle, opcode, 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)
filled = 0
wire_bytes = 0
packed_offset = 0
fragments: list[ReadFragment] = []
seen: set[int] = set()
while filled < 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}")
# Fragments may arrive out of order. Offset 28 is the byte
# offset of this chunk within the request (0, 65536, …),
# not a 0-based index. Offset 32 is the uncompressed
# fragment length. When the opcode high bits are FastLZ
# (2), extra data is compressed and offset 36 is its size.
opcode = struct.unpack_from("<Q", body, 8)[0]
dest, chunk_len = struct.unpack_from("<II", body, 28)
if dest in seen or chunk_len == 0 or dest + chunk_len > length:
raise NfcProtocolError(
f"AIO IO chunk offset={dest} length={chunk_len} invalid, "
f"request {length}"
)
seen.add(dest)
ctype = opcode >> 32
extra_len = _aio_extra_len(ctype, body, chunk_len)
if skip_decompression:
end = packed_offset + extra_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:
chunk = fastlz.decompress(extra, chunk_len)
except ValueError as exc:
raise NfcProtocolError(
f"FastLZ read fragment failed: {exc}"
) from exc
if len(chunk) != chunk_len:
raise NfcProtocolError(
f"FastLZ read got {len(chunk)} bytes, expected {chunk_len}"
)
data[dest : dest + chunk_len] = chunk
elif ctype == NFC_COMPRESSION_NONE:
_recvn_into(self._sock, data[dest : dest + chunk_len])
else:
raise NfcProtocolError(f"unsupported NFC IO compression type {ctype}")
wire_bytes += extra_len
filled += chunk_len
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:
"""Write ``num_sectors`` starting at ``start_sector``.
Matches ``VixDiskLib_Write``: one ``NFC_AIO_MSG_IO`` ``opId``
for the whole call. Chunks larger than the session AIO buffer
are extra fragments with that same ``opId``; the server replies
once. FASTLZ open compresses each fragment when that shrinks it.
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}")
disk_offset = start_sector * self.sector_size
op_id = self._next_op_id()
frag_offset = 0
while frag_offset < length:
chunk = data[frag_offset : frag_offset + self.aio_buffer_size]
extra = chunk
extra_len = len(chunk)
ctype = NFC_COMPRESSION_NONE
if self.compression == NFC_COMPRESSION_FASTLZ and extra_len >= 16:
compressed = fastlz.compress(chunk)
if compressed and len(compressed) < extra_len:
extra = compressed
extra_len = len(compressed)
ctype = NFC_COMPRESSION_FASTLZ
opcode = NFC_AIO_IO_WRITE | (ctype << 32)
payload = struct.pack(
"<QQQIIIII",
self.handle,
opcode,
disk_offset,
length,
frag_offset,
len(chunk),
extra_len,
0,
)
self._sock.sendall(
_pack_aio_hdr(NFC_AIO_MSG_IO, len(payload), op_id) + payload + extra
)
frag_offset += len(chunk)
rtype, rop, _body = self._aio_recv_reply()
if rtype != NFC_AIO_MSG_IO or rop != op_id:
raise NfcProtocolError(
f"AIO IO write reply type={rtype} opId={rop}, "
f"expected type={NFC_AIO_MSG_IO} opId={op_id}"
)
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:
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_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,
compression: int = NFC_COMPRESSION_NONE,
aio_buffer_size: int = NFC_AIO_BUFFER_SIZE,
aio_buffer_count: int = NFC_AIO_BUFFER_COUNT,
) -> 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``. NBD dups the
authd fd and sends plaintext NFC. NBDSSL wraps that dup in a
second TLS session (``session.nfc_ssl``).
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.
compression: ``NFC_COMPRESSION_NONE`` or ``NFC_COMPRESSION_FASTLZ``.
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):
raise NotImplementedError(
f"NFC compression type {compression} is not supported"
)
sock: socket.socket
if session.nfc_ssl:
sock = wrap_nfcssl_socket(session.authd_sock, session.ticket.host)
else:
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,
compression=compression,
aio_buffer_size=aio_buffer_size,
aio_buffer_count=aio_buffer_count,
)
_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