Add openvixdisklib as an open NBD replacement for VMware VDDK.
VDDK is no longer publicly distributed, so this library reverse-engineers the vSphere NFC path and exposes ConnectEx, Open, Read, and Write without the proprietary SDK. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,371 @@
|
||||
# Copyright 2026 Cloudbase Solutions Srl
|
||||
# All Rights Reserved.
|
||||
|
||||
"""VDDK-compatible vSphere NFC authentication.
|
||||
|
||||
VixDiskLib_ConnectEx / Open authenticate in two stages:
|
||||
|
||||
1. SOAP login to vCenter (or ESXi) and an internal NfcService call that
|
||||
returns a one-time vim.HostServiceTicket.
|
||||
2. A TLS session to the ESXi authd daemon on TCP 902, completed with the
|
||||
ticket's sessionId and service name.
|
||||
|
||||
pyVim / pyVmomi are used for every public VIM operation (login, inventory,
|
||||
HostServiceTicket). NfcService is not in the public WSDL, so it is registered
|
||||
with pyVmomi's type system and invoked through the same SOAP stub.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import socket
|
||||
import ssl
|
||||
from typing import Optional
|
||||
|
||||
from pyVim.connect import Disconnect, SmartConnect
|
||||
from pyVmomi import vim
|
||||
from pyVmomi.VmomiSupport import CreateManagedType, F_OPTIONAL, GetVmodlType
|
||||
|
||||
NFC_SERVICE_MOID = "nfcService"
|
||||
AUTHD_DEFAULT_PORT = 902
|
||||
_NFC_TYPES_REGISTERED = False
|
||||
|
||||
|
||||
def _ssl_client_context(verify: bool = True) -> ssl.SSLContext:
|
||||
"""Return a client TLS context built with public ``ssl`` APIs."""
|
||||
context = ssl.create_default_context()
|
||||
if not verify:
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
return context
|
||||
|
||||
|
||||
def _register_nfc_types() -> None:
|
||||
"""Register internal vim.NfcService methods on the pyVmomi type map."""
|
||||
global _NFC_TYPES_REGISTERED
|
||||
if _NFC_TYPES_REGISTERED:
|
||||
return
|
||||
try:
|
||||
GetVmodlType("vim.NfcService")
|
||||
_NFC_TYPES_REGISTERED = True
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
CreateManagedType(
|
||||
"vim.NfcService",
|
||||
"NfcService",
|
||||
"vmodl.ManagedObject",
|
||||
"vim.version.version1",
|
||||
[],
|
||||
[
|
||||
("getVmFiles", "NfcGetVmFiles", "vim.version.version1",
|
||||
(("vm", "vim.VirtualMachine", "vim.version.version1", 0, None),),
|
||||
(0, "vim.HostServiceTicket", "vim.HostServiceTicket"), None, None),
|
||||
("randomAccessOpen", "NfcRandomAccessOpenDisk",
|
||||
"vim.version.version1",
|
||||
(("vm", "vim.VirtualMachine", "vim.version.version1", 0, None),
|
||||
("diskDeviceKey", "int", "vim.version.version1", 0, None),
|
||||
("hostForAccess", "vim.HostSystem", "vim.version.version1",
|
||||
F_OPTIONAL, None),),
|
||||
(0, "vim.HostServiceTicket", "vim.HostServiceTicket"), None, None),
|
||||
("randomAccessOpenReadonly", "NfcRandomAccessOpenReadonly",
|
||||
"vim.version.version1",
|
||||
(("vm", "vim.VirtualMachine", "vim.version.version1", 0, None),
|
||||
("diskDeviceKey", "int", "vim.version.version1", 0, None),
|
||||
("hostForAccess", "vim.HostSystem", "vim.version.version1",
|
||||
F_OPTIONAL, None),),
|
||||
(0, "vim.HostServiceTicket", "vim.HostServiceTicket"), None, None),
|
||||
("getServerNfcLibVersion", "NfcGetServerNfcLibVersion",
|
||||
"vim.version.version1",
|
||||
(("hostForAccess", "vim.HostSystem", "vim.version.version1",
|
||||
0, None),),
|
||||
(0, "int", "int"), None, None),
|
||||
],
|
||||
)
|
||||
_NFC_TYPES_REGISTERED = True
|
||||
|
||||
|
||||
def nfc_service(si: vim.ServiceInstance) -> vim.NfcService:
|
||||
"""Return the vCenter/ESXi NfcService managed object on ``si``'s SOAP stub.
|
||||
|
||||
Args:
|
||||
si: An authenticated ServiceInstance from pyVim.connect.SmartConnect.
|
||||
"""
|
||||
_register_nfc_types()
|
||||
nfc_cls = GetVmodlType("vim.NfcService")
|
||||
return nfc_cls(NFC_SERVICE_MOID, si._stub)
|
||||
|
||||
|
||||
def connect_vim(
|
||||
host: str,
|
||||
username: str,
|
||||
password: str,
|
||||
port: int = 443,
|
||||
thumbprint: Optional[str] = None,
|
||||
allow_untrusted: bool = False) -> vim.ServiceInstance:
|
||||
"""Login to vCenter or ESXi using pyVim.connect.SmartConnect.
|
||||
|
||||
Args:
|
||||
host: vCenter or ESXi hostname/IP.
|
||||
username: VIM user name.
|
||||
password: VIM password.
|
||||
port: HTTPS port, usually 443.
|
||||
thumbprint: Optional SHA-1 SSL thumbprint of the management endpoint.
|
||||
allow_untrusted: If True, skip certificate validation.
|
||||
"""
|
||||
ssl_context = None
|
||||
if allow_untrusted:
|
||||
ssl_context = _ssl_client_context(verify=False)
|
||||
return SmartConnect(
|
||||
host=host,
|
||||
user=username,
|
||||
pwd=password,
|
||||
port=port,
|
||||
thumbprint=thumbprint,
|
||||
sslContext=ssl_context,
|
||||
disableSslCertValidation=allow_untrusted)
|
||||
|
||||
|
||||
def _virtual_disk_key(vm: vim.VirtualMachine, disk_path: str) -> int:
|
||||
"""Return the VirtualDisk device key whose backing path is ``disk_path``."""
|
||||
for device in vm.config.hardware.device:
|
||||
if isinstance(device, vim.vm.device.VirtualDisk):
|
||||
backing = getattr(device, "backing", None)
|
||||
file_name = getattr(backing, "fileName", None)
|
||||
if file_name == disk_path:
|
||||
return device.key
|
||||
raise ValueError(
|
||||
f"VMDK path {disk_path!r} is not attached to {vm._moId}")
|
||||
|
||||
|
||||
def get_nfc_ticket(
|
||||
si: vim.ServiceInstance,
|
||||
vm: vim.VirtualMachine,
|
||||
disk_device_key: Optional[int] = None,
|
||||
host_for_access: Optional[vim.HostSystem] = None,
|
||||
read_only: bool = True,
|
||||
disk_path: Optional[str] = None) -> vim.HostServiceTicket:
|
||||
"""Return a one-time NFC HostServiceTicket for ``vm``.
|
||||
|
||||
Matches VDDK: ``NfcGetVmFiles`` when only the VM is known (read-only),
|
||||
``NfcRandomAccessOpenReadonly`` / ``NfcRandomAccessOpenDisk`` when a
|
||||
virtual disk device key (or datastore path) is supplied.
|
||||
|
||||
Args:
|
||||
si: Authenticated ServiceInstance.
|
||||
vm: Target virtual machine.
|
||||
disk_device_key: Optional VirtualDisk.device key (for example 2000).
|
||||
host_for_access: Host that should serve NFC; defaults to the VM's host.
|
||||
read_only: When False, request a writable ticket (needs a disk).
|
||||
disk_path: Datastore path used to resolve ``disk_device_key``.
|
||||
"""
|
||||
nfc = nfc_service(si)
|
||||
if read_only and disk_device_key is None and disk_path is None:
|
||||
return nfc.GetVmFiles(vm)
|
||||
if disk_device_key is None:
|
||||
if disk_path is None:
|
||||
raise ValueError(
|
||||
"writable NFC tickets need disk_path or disk_device_key")
|
||||
disk_device_key = _virtual_disk_key(vm, disk_path)
|
||||
if host_for_access is None:
|
||||
host_for_access = vm.runtime.host
|
||||
if read_only:
|
||||
return nfc.RandomAccessOpenReadonly(
|
||||
vm, disk_device_key, host_for_access)
|
||||
return nfc.RandomAccessOpen(vm, disk_device_key, host_for_access)
|
||||
|
||||
|
||||
def _format_thumbprint(digest: bytes) -> str:
|
||||
return ":".join(f"{byte:02X}" for byte in digest)
|
||||
|
||||
|
||||
def _sha1_thumbprint(der_cert: bytes) -> str:
|
||||
return _format_thumbprint(hashlib.sha1(der_cert).digest())
|
||||
|
||||
|
||||
def _normalize_thumbprint(thumbprint: str) -> str:
|
||||
return thumbprint.replace(":", "").replace(" ", "").upper()
|
||||
|
||||
|
||||
def get_ssl_cert_thumbprint(
|
||||
host: str,
|
||||
port: int = 443,
|
||||
digest_algorithm: str = "sha1",
|
||||
ssl_context: Optional[ssl.SSLContext] = None,
|
||||
timeout: float = 30.0) -> str:
|
||||
"""Return the TLS certificate thumbprint of ``host``:``port``.
|
||||
|
||||
Reads the peer certificate in DER form and hashes it with ``hashlib``.
|
||||
The result is colon-separated uppercase hex (for example
|
||||
``A5:AF:7D:…``), matching VDDK / pyVmomi SHA-1 thumbprints.
|
||||
|
||||
Args:
|
||||
host: Hostname or IP of the TLS server.
|
||||
port: TLS port, usually 443.
|
||||
digest_algorithm: Hash name accepted by ``hashlib.new``. Default
|
||||
``sha1`` is the format VDDK and pyVmomi expect.
|
||||
ssl_context: Optional SSL context. When omitted, a default client
|
||||
context is used with hostname checks and certificate
|
||||
validation disabled so a self-signed management certificate
|
||||
can still be read.
|
||||
timeout: Connect timeout in seconds.
|
||||
"""
|
||||
if ssl_context is None:
|
||||
ssl_context = _ssl_client_context(verify=False)
|
||||
with socket.create_connection((host, port), timeout=timeout) as sock:
|
||||
with ssl_context.wrap_socket(
|
||||
sock, server_hostname=host) as ssock:
|
||||
cert = ssock.getpeercert(binary_form=True)
|
||||
if not cert:
|
||||
raise ConnectionError(
|
||||
f"no peer certificate from {host}:{port}")
|
||||
return _format_thumbprint(
|
||||
hashlib.new(digest_algorithm, cert).digest())
|
||||
|
||||
|
||||
def _readline(sock: socket.socket) -> str:
|
||||
buf = b""
|
||||
while not buf.endswith(b"\n"):
|
||||
chunk = sock.recv(1)
|
||||
if not chunk:
|
||||
raise ConnectionError("authd connection closed")
|
||||
buf += chunk
|
||||
if len(buf) > 4096:
|
||||
raise ConnectionError("oversized authd response")
|
||||
return buf.decode("ascii", "replace").rstrip("\r\n")
|
||||
|
||||
|
||||
def _expect_code(line: str, code: str, what: str) -> str:
|
||||
if not line.startswith(code):
|
||||
raise ConnectionError(f"authd {what} failed: {line}")
|
||||
return line[len(code):].lstrip()
|
||||
|
||||
|
||||
def connect_authd(
|
||||
ticket: vim.HostServiceTicket,
|
||||
allow_untrusted: bool = False,
|
||||
timeout: float = 30.0) -> ssl.SSLSocket:
|
||||
"""Complete the ESXi authd handshake using an NFC HostServiceTicket.
|
||||
|
||||
Wire sequence captured from VDDK against authd on TCP 902:
|
||||
|
||||
1. Read the plaintext 220 banner, then wrap the socket with TLS.
|
||||
2. SESSION <sessionId>
|
||||
3. BANNER
|
||||
4. THUMBPRINT_SHA2 PlainText (NFC data stays on this TLS socket)
|
||||
5. PROXY <ticket.service> (vpxa-nfc when connecting via vCenter)
|
||||
|
||||
Args:
|
||||
ticket: One-time ticket from get_nfc_ticket().
|
||||
allow_untrusted: If False, require the peer SHA-1 thumbprint to match
|
||||
ticket.sslThumbprint.
|
||||
timeout: Socket timeout in seconds.
|
||||
"""
|
||||
host = ticket.host
|
||||
port = ticket.port or AUTHD_DEFAULT_PORT
|
||||
raw = socket.create_connection((host, port), timeout=timeout)
|
||||
try:
|
||||
banner = _readline(raw)
|
||||
if not banner.startswith("220"):
|
||||
raise ConnectionError(f"unexpected authd banner: {banner}")
|
||||
|
||||
ssl_context = _ssl_client_context(verify=False)
|
||||
ssock = ssl_context.wrap_socket(raw, server_hostname=host)
|
||||
except Exception:
|
||||
raw.close()
|
||||
raise
|
||||
|
||||
try:
|
||||
if not allow_untrusted and ticket.sslThumbprint:
|
||||
peer = _sha1_thumbprint(ssock.getpeercert(True))
|
||||
if _normalize_thumbprint(peer) != _normalize_thumbprint(
|
||||
ticket.sslThumbprint):
|
||||
raise ConnectionError(
|
||||
f"ESXi SSL thumbprint mismatch: got {peer}, "
|
||||
f"expected {ticket.sslThumbprint}")
|
||||
|
||||
ssock.sendall(f"SESSION {ticket.sessionId}\r\n".encode("ascii"))
|
||||
# Trailing space is part of the BANNER command token used by authd.
|
||||
ssock.sendall(b"BANNER \r\n")
|
||||
_expect_code(_readline(ssock), "220", "BANNER")
|
||||
|
||||
ssock.sendall(b"THUMBPRINT_SHA2 PlainText\r\n")
|
||||
_expect_code(_readline(ssock), "200", "THUMBPRINT_SHA2")
|
||||
|
||||
service = ticket.service or "vpxa-nfc"
|
||||
ssock.sendall(f"PROXY {service}\r\n".encode("ascii"))
|
||||
_expect_code(_readline(ssock), "200", "PROXY")
|
||||
return ssock
|
||||
except Exception:
|
||||
ssock.close()
|
||||
raise
|
||||
|
||||
|
||||
class NfcAuthSession:
|
||||
"""Authenticated VIM session plus an authd/NFC TLS socket."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
si: vim.ServiceInstance,
|
||||
ticket: vim.HostServiceTicket,
|
||||
authd_sock: ssl.SSLSocket) -> None:
|
||||
self.si = si
|
||||
self.ticket = ticket
|
||||
self.authd_sock = authd_sock
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the authd socket and logout of the VIM session."""
|
||||
try:
|
||||
self.authd_sock.close()
|
||||
finally:
|
||||
Disconnect(self.si)
|
||||
|
||||
def __enter__(self) -> "NfcAuthSession":
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb) -> None:
|
||||
self.close()
|
||||
|
||||
|
||||
def authenticate(
|
||||
host: str,
|
||||
username: str,
|
||||
password: str,
|
||||
vm_moref: str,
|
||||
port: int = 443,
|
||||
thumbprint: Optional[str] = None,
|
||||
allow_untrusted: bool = False,
|
||||
disk_device_key: Optional[int] = None,
|
||||
disk_path: Optional[str] = None,
|
||||
read_only: bool = True) -> NfcAuthSession:
|
||||
"""Login to vSphere and complete NFC authd authentication for a VM.
|
||||
|
||||
Args:
|
||||
host: vCenter or ESXi hostname/IP.
|
||||
username: VIM user name.
|
||||
password: VIM password.
|
||||
vm_moref: Virtual machine managed object id (for example ``vm-13098``).
|
||||
port: HTTPS port for VIM, usually 443.
|
||||
thumbprint: Optional SHA-1 thumbprint of the management endpoint.
|
||||
allow_untrusted: Skip TLS certificate checks when True.
|
||||
disk_device_key: Optional VirtualDisk device key; when omitted with
|
||||
``read_only``, the VDDK ``NfcGetVmFiles`` ticket is used.
|
||||
disk_path: Datastore path used to resolve ``disk_device_key``.
|
||||
read_only: When False, request a writable ``NfcRandomAccessOpenDisk``
|
||||
ticket.
|
||||
"""
|
||||
si = connect_vim(
|
||||
host, username, password, port=port,
|
||||
thumbprint=thumbprint, allow_untrusted=allow_untrusted)
|
||||
try:
|
||||
vm = vim.VirtualMachine(vm_moref, si._stub)
|
||||
ticket = get_nfc_ticket(
|
||||
si, vm, disk_device_key=disk_device_key,
|
||||
disk_path=disk_path, read_only=read_only)
|
||||
authd_sock = connect_authd(
|
||||
ticket, allow_untrusted=allow_untrusted)
|
||||
except Exception:
|
||||
Disconnect(si)
|
||||
raise
|
||||
return NfcAuthSession(si, ticket, authd_sock)
|
||||
@@ -0,0 +1,425 @@
|
||||
# Copyright 2026 Cloudbase Solutions Srl
|
||||
# All Rights Reserved.
|
||||
|
||||
"""VDDK-compatible NFC disk open, sector read, and sector write.
|
||||
|
||||
After ``nfc_auth.connect_authd`` returns ``200 Connect``, VDDK stops using
|
||||
``SSL_write`` on the authd socket. ``THUMBPRINT_SHA2 PlainText`` means the
|
||||
NFC binary protocol runs as raw TCP on that same file descriptor
|
||||
(``useSSL=0``). This module dups that fd and speaks:
|
||||
|
||||
1. Classic 264-byte NFC messages (handshake, version, connection data,
|
||||
AIO session open).
|
||||
2. NFC AIO frames (16-byte header plus payload) to open a VMDK and read
|
||||
or write sectors.
|
||||
|
||||
pyVmomi is not involved here; the ticket and TLS authd handshake already
|
||||
happened in ``nfc_auth``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
import struct
|
||||
|
||||
from openvixdisklib.nfc_auth import NfcAuthSession
|
||||
|
||||
NFC_MSG_SIZE = 264
|
||||
NFC_AIO_MAGIC = 0xA100DA7A
|
||||
NFC_AIO_HDR_SIZE = 16
|
||||
NFC_SECTOR_SIZE = 512
|
||||
NFC_PROTOCOL_VERSION = 11
|
||||
# Max data bytes in one AIO IO reply fragment (NfcAioInitSession buffer).
|
||||
NFC_AIO_BUFFER_SIZE = 65536
|
||||
|
||||
# Classic NFC message types observed on the wire (uint32 at offset 0).
|
||||
NFC_MSG_SESSION_COMPLETE = 4
|
||||
NFC_MSG_SESSION_PARAMS = 33
|
||||
NFC_MSG_SESSION_PARAMS_REPLY = 36
|
||||
NFC_MSG_HANDSHAKE = 43
|
||||
NFC_MSG_VERSION = 51
|
||||
NFC_MSG_AIO_SESSION_OPEN = 52
|
||||
NFC_MSG_CONNECTION_DATA = 54
|
||||
NFC_MSG_SESSION_FEATURES = 55
|
||||
|
||||
# SessionParams / feature bits from VDDK logs (interruption | switch).
|
||||
NFC_SESSION_FEATURE_INTERRUPTION_SWITCH = 3
|
||||
|
||||
# AIO message types (NfcAioSendMessage "type = N").
|
||||
NFC_AIO_MSG_ERROR = 1
|
||||
NFC_AIO_MSG_OPEN_SESSION = 2
|
||||
NFC_AIO_MSG_CLOSE_SESSION = 3
|
||||
NFC_AIO_MSG_OPEN_FILE = 4
|
||||
NFC_AIO_MSG_CLOSE_FILE = 5
|
||||
NFC_AIO_MSG_IO = 7
|
||||
NFC_AIO_MSG_SET_SOCK_OPTS = 9
|
||||
NFC_AIO_MSG_DDB_GET = 11
|
||||
NFC_AIO_MSG_SET_RES_POOL = 22
|
||||
|
||||
# Open-file body: file type NFC_DISK. 0x1e is what VDDK sends for
|
||||
# VIXDISKLIB_FLAG_OPEN_READ_ONLY; writable opens clear bit 0x04 (0x1a).
|
||||
NFC_DISK = 2
|
||||
NFC_OPEN_FLAGS_READ_ONLY = 0x1E
|
||||
NFC_OPEN_FLAGS_READ_WRITE = 0x1A
|
||||
|
||||
NFC_AIO_IO_WRITE = 0
|
||||
NFC_AIO_IO_READ = 1
|
||||
|
||||
|
||||
class NfcProtocolError(ConnectionError):
|
||||
"""Raised when an NFC message is malformed or reports failure."""
|
||||
|
||||
|
||||
def takeover_authd_socket(ssock: ssl.SSLSocket) -> socket.socket:
|
||||
"""Return a raw socket on the authd TCP connection.
|
||||
|
||||
VDDK writes NFC with ``write(SSL_get_fd(ssl), ...)`` after PROXY, so
|
||||
those bytes are not TLS records. Duping the fd lets Python do the
|
||||
same without ``SSLSocket.send`` re-encrypting, and without
|
||||
``SSL_shutdown``.
|
||||
|
||||
Args:
|
||||
ssock: The TLS socket from ``nfc_auth.connect_authd``.
|
||||
"""
|
||||
timeout = ssock.gettimeout()
|
||||
raw = socket.socket(
|
||||
family=ssock.family,
|
||||
type=ssock.type,
|
||||
proto=ssock.proto,
|
||||
fileno=os.dup(ssock.fileno()))
|
||||
raw.settimeout(timeout)
|
||||
return raw
|
||||
|
||||
|
||||
def _recvn(sock: socket.socket, size: int) -> bytes:
|
||||
buf = bytearray()
|
||||
while len(buf) < size:
|
||||
chunk = sock.recv(size - len(buf))
|
||||
if not chunk:
|
||||
raise NfcProtocolError(
|
||||
f"NFC connection closed, needed {size} bytes, got {len(buf)}")
|
||||
buf.extend(chunk)
|
||||
return bytes(buf)
|
||||
|
||||
|
||||
def _send_nfc_msg(
|
||||
sock: socket.socket, msg_type: int, body: bytes = b"") -> None:
|
||||
if len(body) > NFC_MSG_SIZE - 4:
|
||||
raise ValueError("NFC classic message body too large")
|
||||
frame = struct.pack("<I", msg_type) + body
|
||||
sock.sendall(frame.ljust(NFC_MSG_SIZE, b"\x00"))
|
||||
|
||||
|
||||
def _recv_nfc_msg(sock: socket.socket) -> tuple[int, bytes]:
|
||||
frame = _recvn(sock, NFC_MSG_SIZE)
|
||||
msg_type = struct.unpack_from("<I", frame)[0]
|
||||
return msg_type, frame[4:]
|
||||
|
||||
|
||||
def _pack_aio_hdr(msg_type: int, payload_size: int, op_id: int) -> bytes:
|
||||
return struct.pack(
|
||||
"<IIII", NFC_AIO_MAGIC, msg_type, payload_size, op_id)
|
||||
|
||||
|
||||
def _unpack_aio_hdr(hdr: bytes) -> tuple[int, int, int]:
|
||||
magic, msg_type, size, op_id = struct.unpack_from("<IIII", hdr)
|
||||
if magic != NFC_AIO_MAGIC:
|
||||
raise NfcProtocolError(
|
||||
f"AIO header magic mismatch: 0x{magic:x}, "
|
||||
f"expected 0x{NFC_AIO_MAGIC:x}")
|
||||
if msg_type == NFC_AIO_MSG_ERROR:
|
||||
raise NfcProtocolError(f"AIO error opId={op_id} size={size}")
|
||||
return msg_type, size, op_id
|
||||
|
||||
|
||||
class NfcDisk:
|
||||
"""An NFC AIO session with one VMDK opened for I/O."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sock: socket.socket,
|
||||
path: str,
|
||||
handle: int,
|
||||
sector_size: int) -> None:
|
||||
"""Wrap an AIO session that already has ``path`` open.
|
||||
|
||||
Args:
|
||||
sock: Raw NFC socket after handshake.
|
||||
path: Datastore path that was opened.
|
||||
handle: Server file handle from OPEN_FILE.
|
||||
sector_size: Sector size from the OPEN_FILE reply.
|
||||
"""
|
||||
self._sock = sock
|
||||
self._op_id = 0
|
||||
self.path = path
|
||||
self.handle = handle
|
||||
self.sector_size = sector_size
|
||||
self._closed = False
|
||||
|
||||
def _next_op_id(self) -> int:
|
||||
op_id = self._op_id
|
||||
self._op_id += 1
|
||||
return op_id
|
||||
|
||||
def _aio_roundtrip(
|
||||
self,
|
||||
msg_type: int,
|
||||
payload: bytes,
|
||||
extra: bytes = b"",
|
||||
extra_recv: int = 0) -> bytes:
|
||||
"""Send one AIO request and return the reply payload (+ extra)."""
|
||||
op_id = self._next_op_id()
|
||||
self._sock.sendall(
|
||||
_pack_aio_hdr(msg_type, len(payload), op_id) + payload)
|
||||
if extra:
|
||||
self._sock.sendall(extra)
|
||||
rhdr = _recvn(self._sock, NFC_AIO_HDR_SIZE)
|
||||
magic, rtype, rsize, rop = struct.unpack_from("<IIII", rhdr)
|
||||
if magic != NFC_AIO_MAGIC:
|
||||
raise NfcProtocolError(
|
||||
f"AIO header magic mismatch: 0x{magic:x}, "
|
||||
f"expected 0x{NFC_AIO_MAGIC:x}")
|
||||
body = _recvn(self._sock, rsize) if rsize else b""
|
||||
if rtype == NFC_AIO_MSG_ERROR:
|
||||
raise NfcProtocolError(
|
||||
f"AIO error opId={rop} size={rsize} {body.hex()}")
|
||||
if rtype != msg_type or rop != op_id:
|
||||
raise NfcProtocolError(
|
||||
f"AIO reply type={rtype} opId={rop}, "
|
||||
f"expected type={msg_type} opId={op_id}")
|
||||
if extra_recv:
|
||||
body += _recvn(self._sock, extra_recv)
|
||||
return body
|
||||
|
||||
def read(self, start_sector: int, num_sectors: int = 1) -> bytes:
|
||||
"""Read ``num_sectors`` starting at ``start_sector``.
|
||||
|
||||
Matches ``VixDiskLib_Read``: one ``NFC_AIO_MSG_IO`` request in
|
||||
byte units. If the length exceeds the AIO buffer (64 KiB) the
|
||||
server replies with several same-``opId`` fragments.
|
||||
|
||||
Args:
|
||||
start_sector: Sector offset from the start of the disk.
|
||||
num_sectors: Number of sectors to read.
|
||||
"""
|
||||
if num_sectors < 1:
|
||||
raise ValueError("num_sectors must be at least 1")
|
||||
length = num_sectors * self.sector_size
|
||||
offset = start_sector * self.sector_size
|
||||
payload = struct.pack(
|
||||
"<QQQQIII",
|
||||
self.handle,
|
||||
NFC_AIO_IO_READ,
|
||||
offset,
|
||||
length,
|
||||
length,
|
||||
length,
|
||||
0)
|
||||
op_id = self._next_op_id()
|
||||
self._sock.sendall(
|
||||
_pack_aio_hdr(NFC_AIO_MSG_IO, len(payload), op_id) + payload)
|
||||
data = bytearray()
|
||||
while len(data) < length:
|
||||
rhdr = _recvn(self._sock, NFC_AIO_HDR_SIZE)
|
||||
rtype, rsize, rop = _unpack_aio_hdr(rhdr)
|
||||
if rtype != NFC_AIO_MSG_IO or rop != op_id:
|
||||
raise NfcProtocolError(
|
||||
f"AIO IO reply type={rtype} opId={rop}, "
|
||||
f"expected type={NFC_AIO_MSG_IO} opId={op_id}")
|
||||
body = _recvn(self._sock, rsize)
|
||||
if rsize < 36:
|
||||
raise NfcProtocolError(
|
||||
f"AIO IO reply payload too short: {rsize}")
|
||||
chunk_len = struct.unpack_from("<I", body, 32)[0]
|
||||
remaining = length - len(data)
|
||||
if chunk_len == 0 or chunk_len > remaining:
|
||||
raise NfcProtocolError(
|
||||
f"AIO IO chunk length {chunk_len} invalid, "
|
||||
f"remaining {remaining}")
|
||||
data.extend(_recvn(self._sock, chunk_len))
|
||||
return bytes(data)
|
||||
|
||||
def write(
|
||||
self,
|
||||
start_sector: int,
|
||||
num_sectors: int,
|
||||
data: bytes) -> None:
|
||||
"""Write ``num_sectors`` starting at ``start_sector``.
|
||||
|
||||
Matches ``VixDiskLib_Write``: one ``NFC_AIO_MSG_IO`` request per
|
||||
chunk in byte units, with sector bytes sent after the 44-byte
|
||||
payload. Chunks larger than the AIO buffer (64 KiB) are split.
|
||||
|
||||
Args:
|
||||
start_sector: Sector offset from the start of the disk.
|
||||
num_sectors: Number of sectors to write.
|
||||
data: Bytes to write; length must be ``num_sectors * sector_size``.
|
||||
"""
|
||||
if num_sectors < 1:
|
||||
raise ValueError("num_sectors must be at least 1")
|
||||
length = num_sectors * self.sector_size
|
||||
if len(data) != length:
|
||||
raise ValueError(
|
||||
f"write data is {len(data)} bytes, need {length}")
|
||||
max_sectors = NFC_AIO_BUFFER_SIZE // self.sector_size
|
||||
offset_sectors = start_sector
|
||||
remaining = data
|
||||
while remaining:
|
||||
n_sectors = min(len(remaining) // self.sector_size, max_sectors)
|
||||
chunk = remaining[:n_sectors * self.sector_size]
|
||||
self._write_once(offset_sectors, n_sectors, chunk)
|
||||
offset_sectors += n_sectors
|
||||
remaining = remaining[n_sectors * self.sector_size:]
|
||||
|
||||
def _write_once(
|
||||
self,
|
||||
start_sector: int,
|
||||
num_sectors: int,
|
||||
data: bytes) -> None:
|
||||
length = num_sectors * self.sector_size
|
||||
offset = start_sector * self.sector_size
|
||||
payload = struct.pack(
|
||||
"<QQQQIII",
|
||||
self.handle,
|
||||
NFC_AIO_IO_WRITE,
|
||||
offset,
|
||||
length,
|
||||
length,
|
||||
length,
|
||||
0)
|
||||
self._aio_roundtrip(NFC_AIO_MSG_IO, payload, extra=data)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the VMDK, the AIO session, and the classic NFC session."""
|
||||
if self._closed:
|
||||
return
|
||||
self._closed = True
|
||||
try:
|
||||
self._aio_roundtrip(
|
||||
NFC_AIO_MSG_CLOSE_FILE, struct.pack("<Q", self.handle))
|
||||
self._aio_roundtrip(
|
||||
NFC_AIO_MSG_CLOSE_SESSION, struct.pack("<I", 0))
|
||||
_send_nfc_msg(self._sock, NFC_MSG_SESSION_COMPLETE)
|
||||
finally:
|
||||
try:
|
||||
self._sock.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def __enter__(self) -> "NfcDisk":
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb) -> None:
|
||||
self.close()
|
||||
|
||||
|
||||
def _handshake(
|
||||
sock: socket.socket,
|
||||
client_name: str,
|
||||
op_id: str,
|
||||
version: int) -> None:
|
||||
"""Run the classic NFC session handshake used by VDDK NBD."""
|
||||
_send_nfc_msg(sock, NFC_MSG_HANDSHAKE, b"PlainText")
|
||||
_send_nfc_msg(sock, NFC_MSG_SESSION_PARAMS)
|
||||
reply_type, _ = _recv_nfc_msg(sock)
|
||||
if reply_type != NFC_MSG_SESSION_PARAMS_REPLY:
|
||||
raise NfcProtocolError(
|
||||
f"expected session-params reply {NFC_MSG_SESSION_PARAMS_REPLY}, "
|
||||
f"got {reply_type}")
|
||||
|
||||
_send_nfc_msg(sock, NFC_MSG_VERSION, struct.pack("<I", version))
|
||||
reply_type, body = _recv_nfc_msg(sock)
|
||||
if reply_type != NFC_MSG_VERSION:
|
||||
raise NfcProtocolError(
|
||||
f"expected version reply {NFC_MSG_VERSION}, got {reply_type}")
|
||||
remote_version = struct.unpack_from("<I", body)[0]
|
||||
if remote_version < 3:
|
||||
raise NfcProtocolError(
|
||||
f"NFC server version {remote_version} is too old for AIO")
|
||||
|
||||
name_b = client_name.encode("ascii")
|
||||
op_b = op_id.encode("ascii")
|
||||
_send_nfc_msg(
|
||||
sock, NFC_MSG_CONNECTION_DATA,
|
||||
struct.pack("<II", len(name_b), len(op_b)))
|
||||
sock.sendall(name_b)
|
||||
sock.sendall(op_b)
|
||||
_send_nfc_msg(
|
||||
sock, NFC_MSG_SESSION_FEATURES,
|
||||
struct.pack("<I", NFC_SESSION_FEATURE_INTERRUPTION_SWITCH))
|
||||
_send_nfc_msg(sock, NFC_MSG_AIO_SESSION_OPEN)
|
||||
reply_type, _ = _recv_nfc_msg(sock)
|
||||
if reply_type != NFC_MSG_AIO_SESSION_OPEN:
|
||||
raise NfcProtocolError(
|
||||
f"expected AIO session-open reply "
|
||||
f"{NFC_MSG_AIO_SESSION_OPEN}, got {reply_type}")
|
||||
|
||||
|
||||
def _aio_prepare(disk: NfcDisk) -> None:
|
||||
disk._aio_roundtrip(
|
||||
NFC_AIO_MSG_OPEN_SESSION, bytes(16))
|
||||
disk._aio_roundtrip(
|
||||
NFC_AIO_MSG_SET_SOCK_OPTS, bytes(12))
|
||||
disk._aio_roundtrip(
|
||||
NFC_AIO_MSG_SET_RES_POOL, struct.pack("<I", 1))
|
||||
|
||||
|
||||
def _parse_open_reply(body: bytes) -> tuple[int, int]:
|
||||
if len(body) < 40:
|
||||
raise NfcProtocolError(f"OPEN_FILE reply too short: {len(body)}")
|
||||
handle, file_type, _flags = struct.unpack_from("<QII", body, 8)
|
||||
sector_size = struct.unpack_from("<I", body, 36)[0]
|
||||
if file_type != NFC_DISK:
|
||||
raise NfcProtocolError(
|
||||
f"opened file type {file_type}, expected NFC_DISK")
|
||||
if sector_size == 0:
|
||||
sector_size = NFC_SECTOR_SIZE
|
||||
return handle, sector_size
|
||||
|
||||
|
||||
def open_disk(
|
||||
session: NfcAuthSession,
|
||||
disk_path: str,
|
||||
client_name: str = "vddk",
|
||||
op_id: str = "nbdmode",
|
||||
version: int = NFC_PROTOCOL_VERSION,
|
||||
read_only: bool = True) -> NfcDisk:
|
||||
"""Open ``disk_path`` over the authenticated authd socket.
|
||||
|
||||
Matches VDDK ``VixDiskLib_Open`` of a datastore path after the NFC
|
||||
ticket and authd PROXY handshake: session init, AIO open, then
|
||||
``NFC_AIO_MSG_OPEN_FILE`` with type ``NFC_DISK``.
|
||||
|
||||
Args:
|
||||
session: Result of ``nfc_auth.authenticate``.
|
||||
disk_path: Datastore path, for example
|
||||
``[datastore0] vm/vm.vmdk``.
|
||||
client_name: NFC client name; VDDK sends ``vddk``.
|
||||
op_id: NFC operation id; VDDK NBD sends ``nbdmode``.
|
||||
version: Client NFC protocol version (lab ESXi answered 11).
|
||||
read_only: When True, open with VDDK's read-only NFC flags.
|
||||
"""
|
||||
sock = takeover_authd_socket(session.authd_sock)
|
||||
try:
|
||||
_handshake(sock, client_name, op_id, version)
|
||||
disk = NfcDisk(sock, disk_path, handle=0, sector_size=NFC_SECTOR_SIZE)
|
||||
_aio_prepare(disk)
|
||||
path_b = disk_path.encode("utf-8")
|
||||
open_flags = (
|
||||
NFC_OPEN_FLAGS_READ_ONLY if read_only
|
||||
else NFC_OPEN_FLAGS_READ_WRITE)
|
||||
open_body = struct.pack(
|
||||
"<IIIIII",
|
||||
len(path_b), 0, 0, 0, NFC_DISK, open_flags)
|
||||
open_body = open_body.ljust(60, b"\x00")
|
||||
reply = disk._aio_roundtrip(
|
||||
NFC_AIO_MSG_OPEN_FILE, open_body, extra=path_b)
|
||||
handle, sector_size = _parse_open_reply(reply)
|
||||
disk.handle = handle
|
||||
disk.sector_size = sector_size
|
||||
return disk
|
||||
except Exception:
|
||||
sock.close()
|
||||
raise
|
||||
@@ -0,0 +1,333 @@
|
||||
# Copyright 2026 Cloudbase Solutions Srl
|
||||
# All Rights Reserved.
|
||||
|
||||
"""Drop-in replacement for ``tests.integration.vixdisklib`` that does not
|
||||
use VDDK.
|
||||
|
||||
Callers can switch with::
|
||||
|
||||
from openvixdisklib import openvixdisklib as vixdisklib
|
||||
|
||||
``VixDiskLibHandle.connect`` / ``open`` / ``read`` match the VDDK wrapper
|
||||
in ``tests/integration/vixdisklib.py``. VIM login uses pyVmomi; NFC ticket,
|
||||
authd, and disk I/O use ``nfc_auth`` and ``nfc_open``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import ctypes
|
||||
import logging
|
||||
import os
|
||||
from typing import Iterator, Optional, Union
|
||||
|
||||
from pyVim.connect import Disconnect
|
||||
from pyVmomi import vim
|
||||
|
||||
from openvixdisklib import nfc_auth
|
||||
from openvixdisklib import nfc_open
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
VIXDISKLIB_VERSION_MAJOR = 8
|
||||
VIXDISKLIB_VERSION_MINOR = 0
|
||||
|
||||
VIXDISKLIB_SECTOR_SIZE = 512
|
||||
|
||||
VIXDISKLIB_CRED_UID = 1
|
||||
|
||||
VIXDISKLIB_FLAG_OPEN_UNBUFFERED = 1
|
||||
VIXDISKLIB_FLAG_OPEN_SINGLE_LINK = 2
|
||||
VIXDISKLIB_FLAG_OPEN_READ_ONLY = 4
|
||||
|
||||
VIXDISKLIB_FLAG_OPEN_COMPRESSION_ZLIB = 16
|
||||
VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ = 32
|
||||
VIXDISKLIB_FLAG_OPEN_COMPRESSION_SKIPZ = 64
|
||||
|
||||
_COMPRESSION_FLAGS = (
|
||||
VIXDISKLIB_FLAG_OPEN_COMPRESSION_ZLIB
|
||||
| VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ
|
||||
| VIXDISKLIB_FLAG_OPEN_COMPRESSION_SKIPZ)
|
||||
|
||||
VIX_SUPPORTED_COMPATIBILITY_MODES = [
|
||||
"6.0", "6.5", "6.7", "7.0", "8.0"]
|
||||
|
||||
|
||||
def get_buffer(size: int):
|
||||
"""Return a ctypes buffer of ``size`` bytes, as the VDDK wrapper did."""
|
||||
return ctypes.create_string_buffer(size)
|
||||
|
||||
|
||||
def _parse_vm_moref(vmx_spec: Optional[str]) -> str:
|
||||
if not vmx_spec:
|
||||
raise ValueError(
|
||||
"vmx_spec is required (for example 'moref=vm-13098')")
|
||||
if "=" in vmx_spec:
|
||||
kind, value = vmx_spec.split("=", 1)
|
||||
if kind.lower() != "moref" or not value:
|
||||
raise ValueError(f"unsupported vmx_spec: {vmx_spec}")
|
||||
return value
|
||||
return vmx_spec
|
||||
|
||||
|
||||
def _require_nbd(transport_modes: Optional[str]) -> None:
|
||||
if transport_modes is None:
|
||||
return
|
||||
modes = [m for m in transport_modes.split(":") if m]
|
||||
if "nbd" not in modes:
|
||||
raise NotImplementedError(
|
||||
f"only nbd transport is supported, got {transport_modes!r}")
|
||||
|
||||
|
||||
class _Connection:
|
||||
"""VIM session plus the VM moref needed to issue an NFC ticket at Open."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
si: vim.ServiceInstance,
|
||||
vm_moref: str,
|
||||
snapshot_ref: Optional[str],
|
||||
thumbprint: Optional[str],
|
||||
allow_untrusted: bool,
|
||||
read_only: bool) -> None:
|
||||
self.si = si
|
||||
self.vm_moref = vm_moref
|
||||
self.snapshot_ref = snapshot_ref
|
||||
self.thumbprint = thumbprint
|
||||
self.allow_untrusted = allow_untrusted
|
||||
self.read_only = read_only
|
||||
|
||||
|
||||
class _DiskHandle:
|
||||
"""Opened NFC disk plus the authd TLS socket it was taken from."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
disk: nfc_open.NfcDisk,
|
||||
authd_sock) -> None:
|
||||
self.disk = disk
|
||||
self.authd_sock = authd_sock
|
||||
|
||||
|
||||
class VixDiskLibHandle:
|
||||
"""VDDK-compatible handle backed by pyVmomi and the NFC replacement."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_path: Optional[str] = None,
|
||||
vixdisklib_compatibility_version: Optional[str] = None) -> None:
|
||||
"""Accept the VDDK wrapper constructor; no native library is loaded.
|
||||
|
||||
Args:
|
||||
config_path: Ignored. VDDK used this for logging plugins.
|
||||
vixdisklib_compatibility_version: Optional ``major.minor`` string
|
||||
such as ``8.0``. Validated for form only.
|
||||
"""
|
||||
del config_path
|
||||
target_versions = VIX_SUPPORTED_COMPATIBILITY_MODES
|
||||
if vixdisklib_compatibility_version:
|
||||
target_versions = [vixdisklib_compatibility_version]
|
||||
LOG.debug("vixDiskLib versions targeted: %s", target_versions)
|
||||
|
||||
version_used = None
|
||||
for version in reversed(target_versions):
|
||||
try:
|
||||
major_ver, minor_ver = version.split(".")
|
||||
int(major_ver)
|
||||
int(minor_ver)
|
||||
except ValueError as ex:
|
||||
raise ValueError(
|
||||
"Unsupported vixDiskLib version format '%s'. vixDiskLib "
|
||||
"compatibility mode must be of the form "
|
||||
"'$major.$minor'" % version) from ex
|
||||
version_used = version
|
||||
break
|
||||
|
||||
if not version_used:
|
||||
raise Exception(
|
||||
"Could not initialize vixDiskLib with any of the following "
|
||||
"versions: %s" % target_versions)
|
||||
|
||||
LOG.info(
|
||||
"Successfully initialized vixDiskLib with target version '%s'",
|
||||
version_used)
|
||||
|
||||
@classmethod
|
||||
def get_vix_disklib_name(cls) -> str:
|
||||
"""Return the native library name; this replacement does not load it."""
|
||||
if os.name == "nt":
|
||||
return "vixDiskLib.dll"
|
||||
return "libvixDiskLib.so"
|
||||
|
||||
def get_transport_modes(self) -> list[str]:
|
||||
"""Return the transport modes this replacement implements."""
|
||||
return ["nbd"]
|
||||
|
||||
def get_transport_mode(self, disk_handle: _DiskHandle) -> str:
|
||||
"""Return the transport used for ``disk_handle``."""
|
||||
del disk_handle
|
||||
return "nbd"
|
||||
|
||||
@contextlib.contextmanager
|
||||
def connect(
|
||||
self,
|
||||
server_name: str,
|
||||
thumbprint: Optional[str],
|
||||
username: str,
|
||||
password: str,
|
||||
vmx_spec: Optional[str] = None,
|
||||
snapshot_ref: Optional[str] = None,
|
||||
read_only: bool = True,
|
||||
transport_modes: Optional[str] = None,
|
||||
port: int = 443,
|
||||
allow_untrusted: bool = False) -> Iterator[_Connection]:
|
||||
"""Login to vCenter/ESXi. Matches ``VixDiskLib_ConnectEx``.
|
||||
|
||||
The NFC ticket and authd handshake are deferred to ``open``, as in
|
||||
VDDK. Writable opens use ``NfcRandomAccessOpenDisk``; read-only
|
||||
opens use ``NfcGetVmFiles``. ``snapshot_ref`` is accepted for API
|
||||
compatibility and is not sent on the ticket SOAP call.
|
||||
|
||||
Args:
|
||||
server_name: vCenter or ESXi hostname/IP.
|
||||
thumbprint: SHA-1 thumbprint of the management TLS certificate.
|
||||
username: VIM user name.
|
||||
password: VIM password.
|
||||
vmx_spec: VM selector, ``moref=vm-…``.
|
||||
snapshot_ref: Snapshot moref; unused on the NFC ticket.
|
||||
read_only: When False, the disk may be opened for write.
|
||||
transport_modes: ``nbd`` or a colon list that includes ``nbd``.
|
||||
port: HTTPS port, usually 443.
|
||||
allow_untrusted: Skip management TLS verification when True.
|
||||
"""
|
||||
LOG.debug("Connecting VixDiskLib: %s", server_name)
|
||||
_require_nbd(transport_modes)
|
||||
vm_moref = _parse_vm_moref(vmx_spec)
|
||||
si = nfc_auth.connect_vim(
|
||||
server_name,
|
||||
username,
|
||||
password,
|
||||
port=port,
|
||||
thumbprint=thumbprint,
|
||||
allow_untrusted=allow_untrusted or not thumbprint)
|
||||
conn = _Connection(
|
||||
si, vm_moref, snapshot_ref, thumbprint,
|
||||
allow_untrusted or not thumbprint, read_only)
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
self.disconnect(conn)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def open(
|
||||
self,
|
||||
conn: _Connection,
|
||||
disk_path: str,
|
||||
flags: int = VIXDISKLIB_FLAG_OPEN_READ_ONLY) -> Iterator[_DiskHandle]:
|
||||
"""Open ``disk_path`` over NFC. Matches ``VixDiskLib_Open``.
|
||||
|
||||
Args:
|
||||
conn: Connection from ``connect``.
|
||||
disk_path: Datastore path of the VMDK.
|
||||
flags: Open flags. ``VIXDISKLIB_FLAG_OPEN_READ_ONLY`` opens
|
||||
the disk read-only; omit it for write. Compression flags
|
||||
are not implemented.
|
||||
"""
|
||||
LOG.debug("Openning VixDiskLib disk: %s", disk_path)
|
||||
if flags & _COMPRESSION_FLAGS:
|
||||
raise NotImplementedError(
|
||||
"NBD compression open flags are not supported")
|
||||
read_only = bool(flags & VIXDISKLIB_FLAG_OPEN_READ_ONLY)
|
||||
if not read_only and conn.read_only:
|
||||
raise NotImplementedError(
|
||||
"ConnectEx was read-only; cannot open for write")
|
||||
|
||||
vm = vim.VirtualMachine(conn.vm_moref, conn.si._stub)
|
||||
ticket = nfc_auth.get_nfc_ticket(
|
||||
conn.si, vm, read_only=read_only, disk_path=disk_path)
|
||||
authd_sock = nfc_auth.connect_authd(
|
||||
ticket, allow_untrusted=conn.allow_untrusted)
|
||||
session = nfc_auth.NfcAuthSession(conn.si, ticket, authd_sock)
|
||||
try:
|
||||
disk = nfc_open.open_disk(
|
||||
session, disk_path, read_only=read_only)
|
||||
except Exception:
|
||||
authd_sock.close()
|
||||
raise
|
||||
handle = _DiskHandle(disk, authd_sock)
|
||||
try:
|
||||
yield handle
|
||||
finally:
|
||||
self.close(handle)
|
||||
|
||||
def read(
|
||||
self,
|
||||
disk_handle: _DiskHandle,
|
||||
start_sector: int,
|
||||
num_sectors: int,
|
||||
buf: Union[ctypes.Array, bytearray, memoryview]) -> None:
|
||||
"""Read ``num_sectors`` from ``start_sector`` into ``buf``.
|
||||
|
||||
Args:
|
||||
disk_handle: Handle from ``open``.
|
||||
start_sector: First sector to read.
|
||||
num_sectors: Number of sectors to read.
|
||||
buf: Destination buffer (``get_buffer`` or a writable bytes-like).
|
||||
"""
|
||||
data = disk_handle.disk.read(start_sector, num_sectors)
|
||||
if isinstance(buf, (bytearray, memoryview)):
|
||||
if len(buf) < len(data):
|
||||
raise Exception(
|
||||
f"read buffer is {len(buf)} bytes, need {len(data)}")
|
||||
buf[:len(data)] = data
|
||||
return
|
||||
ctypes.memmove(buf, data, len(data))
|
||||
|
||||
def write(
|
||||
self,
|
||||
disk_handle: _DiskHandle,
|
||||
start_sector: int,
|
||||
num_sectors: int,
|
||||
buf: Union[ctypes.Array, bytes, bytearray, memoryview]) -> None:
|
||||
"""Write ``num_sectors`` from ``buf`` starting at ``start_sector``.
|
||||
|
||||
Args:
|
||||
disk_handle: Handle from ``open``.
|
||||
start_sector: First sector to write.
|
||||
num_sectors: Number of sectors to write.
|
||||
buf: Source buffer (``get_buffer`` or a bytes-like).
|
||||
"""
|
||||
length = num_sectors * VIXDISKLIB_SECTOR_SIZE
|
||||
if isinstance(buf, (bytes, bytearray, memoryview)):
|
||||
data = bytes(buf[:length])
|
||||
else:
|
||||
data = buf.raw[:length]
|
||||
disk_handle.disk.write(start_sector, num_sectors, data)
|
||||
|
||||
def close(self, disk_handle: _DiskHandle) -> None:
|
||||
"""Close the VMDK and the authd socket used for NFC.
|
||||
|
||||
Args:
|
||||
disk_handle: Handle from ``open``.
|
||||
"""
|
||||
LOG.debug("Closing VixDiskLib disk handle: %s", disk_handle)
|
||||
try:
|
||||
disk_handle.disk.close()
|
||||
finally:
|
||||
try:
|
||||
disk_handle.authd_sock.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def disconnect(self, conn: _Connection) -> None:
|
||||
"""Logout of the VIM session.
|
||||
|
||||
Args:
|
||||
conn: Connection from ``connect``.
|
||||
"""
|
||||
LOG.debug("Disconnecting VixDiskLib")
|
||||
Disconnect(conn.si)
|
||||
|
||||
def exit(self) -> None:
|
||||
"""No-op; there is no native VDDK library to tear down."""
|
||||
return
|
||||
Reference in New Issue
Block a user