Fix code formatting - tox -e fmt

This commit is contained in:
Lucian Petrut
2026-09-08 14:59:43 +00:00
parent ffce3a1cd6
commit e595dcd9e7
13 changed files with 615 additions and 492 deletions
+2
View File
@@ -73,3 +73,5 @@
- When writing unit tests, mock decorators are preferred instead of context - When writing unit tests, mock decorators are preferred instead of context
managers. managers.
- If a folder or file under this directory is inaccessible, ask for permissions. - If a folder or file under this directory is inaccessible, ask for permissions.
- Use "tox -e fmt" to apply code formatting, "tox -e pep8" and "tox -e fmt"
and "tox -e mypy" for liniting / static code analysis.
+91 -44
View File
@@ -24,7 +24,7 @@ from typing import Optional
from pyVim.connect import Disconnect, SmartConnect from pyVim.connect import Disconnect, SmartConnect
from pyVmomi import vim from pyVmomi import vim
from pyVmomi.VmomiSupport import CreateManagedType, F_OPTIONAL, GetVmodlType from pyVmomi.VmomiSupport import F_OPTIONAL, CreateManagedType, GetVmodlType
NFC_SERVICE_MOID = "nfcService" NFC_SERVICE_MOID = "nfcService"
AUTHD_DEFAULT_PORT = 902 AUTHD_DEFAULT_PORT = 902
@@ -59,28 +59,62 @@ def _register_nfc_types() -> None:
"vim.version.version1", "vim.version.version1",
[], [],
[ [
("getVmFiles", "NfcGetVmFiles", "vim.version.version1", (
"getVmFiles",
"NfcGetVmFiles",
"vim.version.version1",
(("vm", "vim.VirtualMachine", "vim.version.version1", 0, None),), (("vm", "vim.VirtualMachine", "vim.version.version1", 0, None),),
(0, "vim.HostServiceTicket", "vim.HostServiceTicket"), None, None), (0, "vim.HostServiceTicket", "vim.HostServiceTicket"),
("randomAccessOpen", "NfcRandomAccessOpenDisk", None,
None,
),
(
"randomAccessOpen",
"NfcRandomAccessOpenDisk",
"vim.version.version1", "vim.version.version1",
(("vm", "vim.VirtualMachine", "vim.version.version1", 0, None), (
("vm", "vim.VirtualMachine", "vim.version.version1", 0, None),
("diskDeviceKey", "int", "vim.version.version1", 0, None), ("diskDeviceKey", "int", "vim.version.version1", 0, None),
("hostForAccess", "vim.HostSystem", "vim.version.version1", (
F_OPTIONAL, None),), "hostForAccess",
(0, "vim.HostServiceTicket", "vim.HostServiceTicket"), None, None), "vim.HostSystem",
("randomAccessOpenReadonly", "NfcRandomAccessOpenReadonly",
"vim.version.version1", "vim.version.version1",
(("vm", "vim.VirtualMachine", "vim.version.version1", 0, None), 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), ("diskDeviceKey", "int", "vim.version.version1", 0, None),
("hostForAccess", "vim.HostSystem", "vim.version.version1", (
F_OPTIONAL, None),), "hostForAccess",
(0, "vim.HostServiceTicket", "vim.HostServiceTicket"), None, None), "vim.HostSystem",
("getServerNfcLibVersion", "NfcGetServerNfcLibVersion",
"vim.version.version1", "vim.version.version1",
(("hostForAccess", "vim.HostSystem", "vim.version.version1", F_OPTIONAL,
0, None),), None,
(0, "int", "int"), None, 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 _NFC_TYPES_REGISTERED = True
@@ -103,7 +137,8 @@ def connect_vim(
password: str, password: str,
port: int = 443, port: int = 443,
thumbprint: Optional[str] = None, thumbprint: Optional[str] = None,
allow_untrusted: bool = False) -> vim.ServiceInstance: allow_untrusted: bool = False,
) -> vim.ServiceInstance:
"""Login to vCenter or ESXi using pyVim.connect.SmartConnect. """Login to vCenter or ESXi using pyVim.connect.SmartConnect.
Args: Args:
@@ -124,7 +159,8 @@ def connect_vim(
port=port, port=port,
thumbprint=thumbprint, thumbprint=thumbprint,
sslContext=ssl_context, sslContext=ssl_context,
disableSslCertValidation=allow_untrusted) disableSslCertValidation=allow_untrusted,
)
def _virtual_disk_key(vm: vim.VirtualMachine, disk_path: str) -> int: def _virtual_disk_key(vm: vim.VirtualMachine, disk_path: str) -> int:
@@ -144,8 +180,7 @@ def _virtual_disk_key(vm: vim.VirtualMachine, disk_path: str) -> int:
if getattr(backing, "fileName", None) == disk_path: if getattr(backing, "fileName", None) == disk_path:
return device.key return device.key
backing = getattr(backing, "parent", None) backing = getattr(backing, "parent", None)
raise ValueError( raise ValueError(f"VMDK path {disk_path!r} is not attached to {vm._moId}")
f"VMDK path {disk_path!r} is not attached to {vm._moId}")
def get_nfc_ticket( def get_nfc_ticket(
@@ -154,7 +189,8 @@ def get_nfc_ticket(
disk_device_key: Optional[int] = None, disk_device_key: Optional[int] = None,
host_for_access: Optional[vim.HostSystem] = None, host_for_access: Optional[vim.HostSystem] = None,
read_only: bool = True, read_only: bool = True,
disk_path: Optional[str] = None) -> vim.HostServiceTicket: disk_path: Optional[str] = None,
) -> vim.HostServiceTicket:
"""Return a one-time NFC HostServiceTicket for ``vm``. """Return a one-time NFC HostServiceTicket for ``vm``.
Matches VDDK: ``NfcGetVmFiles`` when only the VM is known (read-only), Matches VDDK: ``NfcGetVmFiles`` when only the VM is known (read-only),
@@ -174,14 +210,12 @@ def get_nfc_ticket(
return nfc.GetVmFiles(vm) return nfc.GetVmFiles(vm)
if disk_device_key is None: if disk_device_key is None:
if disk_path is None: if disk_path is None:
raise ValueError( raise ValueError("writable NFC tickets need disk_path or disk_device_key")
"writable NFC tickets need disk_path or disk_device_key")
disk_device_key = _virtual_disk_key(vm, disk_path) disk_device_key = _virtual_disk_key(vm, disk_path)
if host_for_access is None: if host_for_access is None:
host_for_access = vm.runtime.host host_for_access = vm.runtime.host
if read_only: if read_only:
return nfc.RandomAccessOpenReadonly( return nfc.RandomAccessOpenReadonly(vm, disk_device_key, host_for_access)
vm, disk_device_key, host_for_access)
return nfc.RandomAccessOpen(vm, disk_device_key, host_for_access) return nfc.RandomAccessOpen(vm, disk_device_key, host_for_access)
@@ -202,7 +236,8 @@ def get_ssl_cert_thumbprint(
port: int = 443, port: int = 443,
digest_algorithm: str = "sha1", digest_algorithm: str = "sha1",
ssl_context: Optional[ssl.SSLContext] = None, ssl_context: Optional[ssl.SSLContext] = None,
timeout: float = 30.0) -> str: timeout: float = 30.0,
) -> str:
"""Return the TLS certificate thumbprint of ``host``:``port``. """Return the TLS certificate thumbprint of ``host``:``port``.
Reads the peer certificate in DER form and hashes it with ``hashlib``. Reads the peer certificate in DER form and hashes it with ``hashlib``.
@@ -223,14 +258,11 @@ def get_ssl_cert_thumbprint(
if ssl_context is None: if ssl_context is None:
ssl_context = _ssl_client_context(verify=False) ssl_context = _ssl_client_context(verify=False)
with socket.create_connection((host, port), timeout=timeout) as sock: with socket.create_connection((host, port), timeout=timeout) as sock:
with ssl_context.wrap_socket( with ssl_context.wrap_socket(sock, server_hostname=host) as ssock:
sock, server_hostname=host) as ssock:
cert = ssock.getpeercert(binary_form=True) cert = ssock.getpeercert(binary_form=True)
if not cert: if not cert:
raise ConnectionError( raise ConnectionError(f"no peer certificate from {host}:{port}")
f"no peer certificate from {host}:{port}") return _format_thumbprint(hashlib.new(digest_algorithm, cert).digest())
return _format_thumbprint(
hashlib.new(digest_algorithm, cert).digest())
def _readline(sock: socket.socket) -> str: def _readline(sock: socket.socket) -> str:
@@ -248,7 +280,7 @@ def _readline(sock: socket.socket) -> str:
def _expect_code(line: str, code: str, what: str) -> str: def _expect_code(line: str, code: str, what: str) -> str:
if not line.startswith(code): if not line.startswith(code):
raise ConnectionError(f"authd {what} failed: {line}") raise ConnectionError(f"authd {what} failed: {line}")
return line[len(code):].lstrip() return line[len(code) :].lstrip()
def nfcssl_service_name(service: str) -> str: def nfcssl_service_name(service: str) -> str:
@@ -269,7 +301,8 @@ def connect_authd(
ticket: vim.HostServiceTicket, ticket: vim.HostServiceTicket,
allow_untrusted: bool = False, allow_untrusted: bool = False,
timeout: float = 30.0, timeout: float = 30.0,
nfc_ssl: bool = True) -> ssl.SSLSocket: nfc_ssl: bool = True,
) -> ssl.SSLSocket:
"""Complete the ESXi authd handshake using an NFC HostServiceTicket. """Complete the ESXi authd handshake using an NFC HostServiceTicket.
Wire sequence captured from VDDK against authd on TCP 902: Wire sequence captured from VDDK against authd on TCP 902:
@@ -310,10 +343,12 @@ def connect_authd(
if not allow_untrusted and ticket.sslThumbprint: if not allow_untrusted and ticket.sslThumbprint:
peer = _sha1_thumbprint(ssock.getpeercert(True)) peer = _sha1_thumbprint(ssock.getpeercert(True))
if _normalize_thumbprint(peer) != _normalize_thumbprint( if _normalize_thumbprint(peer) != _normalize_thumbprint(
ticket.sslThumbprint): ticket.sslThumbprint
):
raise ConnectionError( raise ConnectionError(
f"ESXi SSL thumbprint mismatch: got {peer}, " f"ESXi SSL thumbprint mismatch: got {peer}, "
f"expected {ticket.sslThumbprint}") f"expected {ticket.sslThumbprint}"
)
ssock.sendall(f"SESSION {ticket.sessionId}\r\n".encode("ascii")) ssock.sendall(f"SESSION {ticket.sessionId}\r\n".encode("ascii"))
# Trailing space is part of the BANNER command token used by authd. # Trailing space is part of the BANNER command token used by authd.
@@ -342,7 +377,8 @@ class NfcAuthSession:
si: vim.ServiceInstance, si: vim.ServiceInstance,
ticket: vim.HostServiceTicket, ticket: vim.HostServiceTicket,
authd_sock: ssl.SSLSocket, authd_sock: ssl.SSLSocket,
nfc_ssl: bool = True) -> None: nfc_ssl: bool = True,
) -> None:
self.si = si self.si = si
self.ticket = ticket self.ticket = ticket
self.authd_sock = authd_sock self.authd_sock = authd_sock
@@ -373,7 +409,8 @@ def authenticate(
disk_device_key: Optional[int] = None, disk_device_key: Optional[int] = None,
disk_path: Optional[str] = None, disk_path: Optional[str] = None,
read_only: bool = True, read_only: bool = True,
nfc_ssl: bool = True) -> NfcAuthSession: nfc_ssl: bool = True,
) -> NfcAuthSession:
"""Login to vSphere and complete NFC authd authentication for a VM. """Login to vSphere and complete NFC authd authentication for a VM.
Args: Args:
@@ -393,15 +430,25 @@ def authenticate(
PROXY service used by nbdssl. Pass False for nbd. PROXY service used by nbdssl. Pass False for nbd.
""" """
si = connect_vim( si = connect_vim(
host, username, password, port=port, host,
thumbprint=thumbprint, allow_untrusted=allow_untrusted) username,
password,
port=port,
thumbprint=thumbprint,
allow_untrusted=allow_untrusted,
)
try: try:
vm = vim.VirtualMachine(vm_moref, si._stub) vm = vim.VirtualMachine(vm_moref, si._stub)
ticket = get_nfc_ticket( ticket = get_nfc_ticket(
si, vm, disk_device_key=disk_device_key, si,
disk_path=disk_path, read_only=read_only) vm,
disk_device_key=disk_device_key,
disk_path=disk_path,
read_only=read_only,
)
authd_sock = connect_authd( authd_sock = connect_authd(
ticket, allow_untrusted=allow_untrusted, nfc_ssl=nfc_ssl) ticket, allow_untrusted=allow_untrusted, nfc_ssl=nfc_ssl
)
except Exception: except Exception:
Disconnect(si) Disconnect(si)
raise raise
+84 -109
View File
@@ -98,15 +98,14 @@ def takeover_authd_socket(ssock: ssl.SSLSocket) -> socket.socket:
family=ssock.family, family=ssock.family,
type=ssock.type, type=ssock.type,
proto=ssock.proto, proto=ssock.proto,
fileno=os.dup(ssock.fileno())) fileno=os.dup(ssock.fileno()),
)
raw.settimeout(timeout) raw.settimeout(timeout)
_enable_tcp_nodelay(raw) _enable_tcp_nodelay(raw)
return raw return raw
def wrap_nfcssl_socket( def wrap_nfcssl_socket(ssock: ssl.SSLSocket, server_hostname: str) -> ssl.SSLSocket:
ssock: ssl.SSLSocket,
server_hostname: str) -> ssl.SSLSocket:
"""Start the second TLS session used by NBDSSL after PROXY. """Start the second TLS session used by NBDSSL after PROXY.
After ``200 Connect ha-nfcssl``, authd TLS is finished and After ``200 Connect ha-nfcssl``, authd TLS is finished and
@@ -121,8 +120,7 @@ def wrap_nfcssl_socket(
raw = takeover_authd_socket(ssock) raw = takeover_authd_socket(ssock)
ssl_context = _ssl_client_context(verify=False) ssl_context = _ssl_client_context(verify=False)
try: try:
return ssl_context.wrap_socket( return ssl_context.wrap_socket(raw, server_hostname=server_hostname)
raw, server_hostname=server_hostname)
except Exception: except Exception:
raw.close() raw.close()
raise raise
@@ -139,13 +137,13 @@ def _recvn(sock: socket.socket, size: int) -> bytes:
chunk = sock.recv(size - len(buf)) chunk = sock.recv(size - len(buf))
if not chunk: if not chunk:
raise NfcProtocolError( raise NfcProtocolError(
f"NFC connection closed, needed {size} bytes, got {len(buf)}") f"NFC connection closed, needed {size} bytes, got {len(buf)}"
)
buf.extend(chunk) buf.extend(chunk)
return bytes(buf) return bytes(buf)
def _send_nfc_msg( def _send_nfc_msg(sock: socket.socket, msg_type: int, body: bytes = b"") -> None:
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")
frame = struct.pack("<I", msg_type) + body frame = struct.pack("<I", msg_type) + body
@@ -159,16 +157,15 @@ def _recv_nfc_msg(sock: socket.socket) -> tuple[int, bytes]:
def _pack_aio_hdr(msg_type: int, payload_size: int, op_id: int) -> bytes: def _pack_aio_hdr(msg_type: int, payload_size: int, op_id: int) -> bytes:
return struct.pack( return struct.pack("<IIII", NFC_AIO_MAGIC, msg_type, payload_size, op_id)
"<IIII", NFC_AIO_MAGIC, msg_type, payload_size, op_id)
def _unpack_aio_hdr(hdr: bytes) -> tuple[int, int, int]: def _unpack_aio_hdr(hdr: bytes) -> tuple[int, int, int]:
magic, msg_type, size, op_id = struct.unpack_from("<IIII", hdr) magic, msg_type, size, op_id = struct.unpack_from("<IIII", hdr)
if magic != NFC_AIO_MAGIC: if magic != NFC_AIO_MAGIC:
raise NfcProtocolError( raise NfcProtocolError(
f"AIO header magic mismatch: 0x{magic:x}, " f"AIO header magic mismatch: 0x{magic:x}, expected 0x{NFC_AIO_MAGIC:x}"
f"expected 0x{NFC_AIO_MAGIC:x}") )
if msg_type == NFC_AIO_MSG_ERROR: if msg_type == NFC_AIO_MSG_ERROR:
raise NfcProtocolError(f"AIO error opId={op_id} size={size}") raise NfcProtocolError(f"AIO error opId={op_id} size={size}")
return msg_type, size, op_id return msg_type, size, op_id
@@ -183,7 +180,8 @@ class NfcDisk:
path: str, path: str,
handle: int, handle: int,
sector_size: int, sector_size: int,
compression: int = NFC_COMPRESSION_NONE) -> None: compression: int = NFC_COMPRESSION_NONE,
) -> None:
"""Wrap an AIO session that already has ``path`` open. """Wrap an AIO session that already has ``path`` open.
Args: Args:
@@ -207,15 +205,12 @@ class NfcDisk:
self._op_id += 1 self._op_id += 1
return op_id return op_id
def _aio_send( def _aio_send(self, msg_type: int, payload: bytes, extra: bytes = b"") -> int:
self,
msg_type: int,
payload: bytes,
extra: bytes = b"") -> int:
"""Send one AIO request (header, payload, and extra in one write).""" """Send one AIO request (header, payload, and extra in one write)."""
op_id = self._next_op_id() op_id = self._next_op_id()
self._sock.sendall( self._sock.sendall(
_pack_aio_hdr(msg_type, len(payload), op_id) + payload + extra) _pack_aio_hdr(msg_type, len(payload), op_id) + payload + extra
)
return op_id return op_id
def _aio_recv_reply(self) -> tuple[int, int, bytes]: def _aio_recv_reply(self) -> tuple[int, int, bytes]:
@@ -224,27 +219,24 @@ class NfcDisk:
magic, rtype, rsize, rop = struct.unpack_from("<IIII", rhdr) magic, rtype, rsize, rop = struct.unpack_from("<IIII", rhdr)
if magic != NFC_AIO_MAGIC: if magic != NFC_AIO_MAGIC:
raise NfcProtocolError( raise NfcProtocolError(
f"AIO header magic mismatch: 0x{magic:x}, " f"AIO header magic mismatch: 0x{magic:x}, expected 0x{NFC_AIO_MAGIC:x}"
f"expected 0x{NFC_AIO_MAGIC:x}") )
body = _recvn(self._sock, rsize) if rsize else b"" body = _recvn(self._sock, rsize) if rsize else b""
if rtype == NFC_AIO_MSG_ERROR: if rtype == NFC_AIO_MSG_ERROR:
raise NfcProtocolError( raise NfcProtocolError(f"AIO error opId={rop} size={rsize} {body.hex()}")
f"AIO error opId={rop} size={rsize} {body.hex()}")
return rtype, rop, body return rtype, rop, body
def _aio_roundtrip( def _aio_roundtrip(
self, self, msg_type: int, payload: bytes, extra: bytes = b"", extra_recv: int = 0
msg_type: int, ) -> bytes:
payload: bytes,
extra: bytes = b"",
extra_recv: int = 0) -> bytes:
"""Send one AIO request and return the reply payload (+ extra).""" """Send one AIO request and return the reply payload (+ extra)."""
op_id = self._aio_send(msg_type, payload, extra) op_id = self._aio_send(msg_type, payload, extra)
rtype, rop, body = self._aio_recv_reply() rtype, rop, body = self._aio_recv_reply()
if rtype != msg_type or rop != op_id: if rtype != msg_type or rop != op_id:
raise NfcProtocolError( raise NfcProtocolError(
f"AIO reply type={rtype} opId={rop}, " f"AIO reply type={rtype} opId={rop}, "
f"expected type={msg_type} opId={op_id}") f"expected type={msg_type} opId={op_id}"
)
if extra_recv: if extra_recv:
body += _recvn(self._sock, extra_recv) body += _recvn(self._sock, extra_recv)
return body return body
@@ -269,17 +261,10 @@ class NfcDisk:
offset = start_sector * self.sector_size offset = start_sector * self.sector_size
opcode = NFC_AIO_IO_READ | (self.compression << 32) opcode = NFC_AIO_IO_READ | (self.compression << 32)
payload = struct.pack( payload = struct.pack(
"<QQQQIII", "<QQQQIII", self.handle, opcode, offset, length, length, length, 0
self.handle, )
opcode,
offset,
length,
length,
length,
0)
op_id = self._next_op_id() op_id = self._next_op_id()
self._sock.sendall( self._sock.sendall(_pack_aio_hdr(NFC_AIO_MSG_IO, len(payload), op_id) + payload)
_pack_aio_hdr(NFC_AIO_MSG_IO, len(payload), op_id) + payload)
data = bytearray(length) data = bytearray(length)
filled = 0 filled = 0
seen: set[int] = set() seen: set[int] = set()
@@ -289,11 +274,11 @@ class NfcDisk:
if rtype != NFC_AIO_MSG_IO or rop != op_id: if rtype != NFC_AIO_MSG_IO or rop != op_id:
raise NfcProtocolError( raise NfcProtocolError(
f"AIO IO reply type={rtype} opId={rop}, " f"AIO IO reply type={rtype} opId={rop}, "
f"expected type={NFC_AIO_MSG_IO} opId={op_id}") f"expected type={NFC_AIO_MSG_IO} opId={op_id}"
)
body = _recvn(self._sock, rsize) body = _recvn(self._sock, rsize)
if rsize < 36: if rsize < 36:
raise NfcProtocolError( raise NfcProtocolError(f"AIO IO reply payload too short: {rsize}")
f"AIO IO reply payload too short: {rsize}")
# Fragments may arrive out of order. Offset 28 is the byte # Fragments may arrive out of order. Offset 28 is the byte
# offset of this chunk within the request (0, 65536, …), # offset of this chunk within the request (0, 65536, …),
# not a 0-based index. Offset 32 is the uncompressed # not a 0-based index. Offset 32 is the uncompressed
@@ -301,13 +286,11 @@ class NfcDisk:
# (2), extra data is compressed and offset 36 is its size. # (2), extra data is compressed and offset 36 is its size.
opcode = struct.unpack_from("<Q", body, 8)[0] opcode = struct.unpack_from("<Q", body, 8)[0]
dest, chunk_len = struct.unpack_from("<II", body, 28) dest, chunk_len = struct.unpack_from("<II", body, 28)
if ( if dest in seen or chunk_len == 0 or dest + chunk_len > length:
dest in seen
or chunk_len == 0
or dest + chunk_len > length):
raise NfcProtocolError( raise NfcProtocolError(
f"AIO IO chunk offset={dest} length={chunk_len} invalid, " f"AIO IO chunk offset={dest} length={chunk_len} invalid, "
f"request {length}") f"request {length}"
)
seen.add(dest) seen.add(dest)
ctype = opcode >> 32 ctype = opcode >> 32
if ctype == NFC_COMPRESSION_FASTLZ: if ctype == NFC_COMPRESSION_FASTLZ:
@@ -317,25 +300,21 @@ class NfcDisk:
chunk = fastlz.decompress(extra, chunk_len) chunk = fastlz.decompress(extra, chunk_len)
except ValueError as exc: except ValueError as exc:
raise NfcProtocolError( raise NfcProtocolError(
f"FastLZ read fragment failed: {exc}") from exc f"FastLZ read fragment failed: {exc}"
) from exc
if len(chunk) != chunk_len: if len(chunk) != chunk_len:
raise NfcProtocolError( raise NfcProtocolError(
f"FastLZ read got {len(chunk)} bytes, " f"FastLZ read got {len(chunk)} bytes, expected {chunk_len}"
f"expected {chunk_len}") )
elif ctype == NFC_COMPRESSION_NONE: elif ctype == NFC_COMPRESSION_NONE:
chunk = _recvn(self._sock, chunk_len) chunk = _recvn(self._sock, chunk_len)
else: else:
raise NfcProtocolError( raise NfcProtocolError(f"unsupported NFC IO compression type {ctype}")
f"unsupported NFC IO compression type {ctype}") data[dest : dest + chunk_len] = chunk
data[dest:dest + chunk_len] = chunk
filled += chunk_len filled += chunk_len
return bytes(data) return bytes(data)
def write( def write(self, start_sector: int, num_sectors: int, data: bytes) -> None:
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``
@@ -352,19 +331,16 @@ class NfcDisk:
raise ValueError("num_sectors must be at least 1") raise ValueError("num_sectors must be at least 1")
length = num_sectors * self.sector_size length = num_sectors * self.sector_size
if len(data) != length: if len(data) != length:
raise ValueError( raise ValueError(f"write data is {len(data)} bytes, need {length}")
f"write data is {len(data)} bytes, need {length}")
disk_offset = start_sector * self.sector_size disk_offset = start_sector * self.sector_size
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 + NFC_AIO_BUFFER_SIZE]
extra = chunk extra = chunk
extra_len = len(chunk) extra_len = len(chunk)
ctype = NFC_COMPRESSION_NONE ctype = NFC_COMPRESSION_NONE
if ( if self.compression == NFC_COMPRESSION_FASTLZ and extra_len >= 16:
self.compression == NFC_COMPRESSION_FASTLZ
and extra_len >= 16):
compressed = fastlz.compress(chunk) compressed = fastlz.compress(chunk)
if compressed and len(compressed) < extra_len: if compressed and len(compressed) < extra_len:
extra = compressed extra = compressed
@@ -380,16 +356,18 @@ class NfcDisk:
frag_offset, frag_offset,
len(chunk), len(chunk),
extra_len, extra_len,
0) 0,
)
self._sock.sendall( self._sock.sendall(
_pack_aio_hdr(NFC_AIO_MSG_IO, len(payload), op_id) _pack_aio_hdr(NFC_AIO_MSG_IO, len(payload), op_id) + payload + extra
+ payload + extra) )
frag_offset += len(chunk) frag_offset += len(chunk)
rtype, rop, _body = self._aio_recv_reply() rtype, rop, _body = self._aio_recv_reply()
if rtype != NFC_AIO_MSG_IO or rop != op_id: if rtype != NFC_AIO_MSG_IO or rop != op_id:
raise NfcProtocolError( raise NfcProtocolError(
f"AIO IO write reply type={rtype} opId={rop}, " f"AIO IO write reply type={rtype} opId={rop}, "
f"expected type={NFC_AIO_MSG_IO} opId={op_id}") f"expected type={NFC_AIO_MSG_IO} opId={op_id}"
)
def close(self) -> None: def close(self) -> None:
"""Close the VMDK, the AIO session, and the classic NFC session.""" """Close the VMDK, the AIO session, and the classic NFC session."""
@@ -397,10 +375,8 @@ class NfcDisk:
return return
self._closed = True self._closed = True
try: try:
self._aio_roundtrip( self._aio_roundtrip(NFC_AIO_MSG_CLOSE_FILE, struct.pack("<Q", self.handle))
NFC_AIO_MSG_CLOSE_FILE, struct.pack("<Q", self.handle)) self._aio_roundtrip(NFC_AIO_MSG_CLOSE_SESSION, struct.pack("<I", 0))
self._aio_roundtrip(
NFC_AIO_MSG_CLOSE_SESSION, struct.pack("<I", 0))
_send_nfc_msg(self._sock, NFC_MSG_SESSION_COMPLETE) _send_nfc_msg(self._sock, NFC_MSG_SESSION_COMPLETE)
finally: finally:
try: try:
@@ -415,11 +391,7 @@ class NfcDisk:
self.close() self.close()
def _handshake( def _handshake(sock: socket.socket, client_name: str, op_id: str, version: int) -> None:
sock: socket.socket,
client_name: str,
op_id: str,
version: int) -> None:
"""Run the classic NFC session handshake used by VDDK NBD.""" """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_HANDSHAKE, b"PlainText")
_send_nfc_msg(sock, NFC_MSG_SESSION_PARAMS) _send_nfc_msg(sock, NFC_MSG_SESSION_PARAMS)
@@ -427,43 +399,46 @@ def _handshake(
if reply_type != NFC_MSG_SESSION_PARAMS_REPLY: if reply_type != NFC_MSG_SESSION_PARAMS_REPLY:
raise NfcProtocolError( raise NfcProtocolError(
f"expected session-params reply {NFC_MSG_SESSION_PARAMS_REPLY}, " f"expected session-params reply {NFC_MSG_SESSION_PARAMS_REPLY}, "
f"got {reply_type}") f"got {reply_type}"
)
_send_nfc_msg(sock, NFC_MSG_VERSION, struct.pack("<I", version)) _send_nfc_msg(sock, NFC_MSG_VERSION, struct.pack("<I", version))
reply_type, body = _recv_nfc_msg(sock) reply_type, body = _recv_nfc_msg(sock)
if reply_type != NFC_MSG_VERSION: if reply_type != NFC_MSG_VERSION:
raise NfcProtocolError( raise NfcProtocolError(
f"expected version reply {NFC_MSG_VERSION}, got {reply_type}") f"expected version reply {NFC_MSG_VERSION}, got {reply_type}"
)
remote_version = struct.unpack_from("<I", body)[0] remote_version = struct.unpack_from("<I", body)[0]
if remote_version < 3: if remote_version < 3:
raise NfcProtocolError( raise NfcProtocolError(
f"NFC server version {remote_version} is too old for AIO") f"NFC server version {remote_version} is too old for AIO"
)
name_b = client_name.encode("ascii") name_b = client_name.encode("ascii")
op_b = op_id.encode("ascii") op_b = op_id.encode("ascii")
_send_nfc_msg( _send_nfc_msg(
sock, NFC_MSG_CONNECTION_DATA, sock, NFC_MSG_CONNECTION_DATA, struct.pack("<II", len(name_b), len(op_b))
struct.pack("<II", len(name_b), len(op_b))) )
sock.sendall(name_b) sock.sendall(name_b)
sock.sendall(op_b) sock.sendall(op_b)
_send_nfc_msg( _send_nfc_msg(
sock, NFC_MSG_SESSION_FEATURES, sock,
struct.pack("<I", NFC_SESSION_FEATURE_INTERRUPTION_SWITCH)) NFC_MSG_SESSION_FEATURES,
struct.pack("<I", NFC_SESSION_FEATURE_INTERRUPTION_SWITCH),
)
_send_nfc_msg(sock, NFC_MSG_AIO_SESSION_OPEN) _send_nfc_msg(sock, NFC_MSG_AIO_SESSION_OPEN)
reply_type, _ = _recv_nfc_msg(sock) reply_type, _ = _recv_nfc_msg(sock)
if reply_type != NFC_MSG_AIO_SESSION_OPEN: if reply_type != NFC_MSG_AIO_SESSION_OPEN:
raise NfcProtocolError( raise NfcProtocolError(
f"expected AIO session-open reply " f"expected AIO session-open reply "
f"{NFC_MSG_AIO_SESSION_OPEN}, got {reply_type}") f"{NFC_MSG_AIO_SESSION_OPEN}, got {reply_type}"
)
def _aio_prepare(disk: NfcDisk) -> None: def _aio_prepare(disk: NfcDisk) -> None:
disk._aio_roundtrip( disk._aio_roundtrip(NFC_AIO_MSG_OPEN_SESSION, bytes(16))
NFC_AIO_MSG_OPEN_SESSION, bytes(16)) disk._aio_roundtrip(NFC_AIO_MSG_SET_SOCK_OPTS, bytes(12))
disk._aio_roundtrip( disk._aio_roundtrip(NFC_AIO_MSG_SET_RES_POOL, struct.pack("<I", 1))
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]: def _parse_open_reply(body: bytes) -> tuple[int, int]:
@@ -472,8 +447,7 @@ def _parse_open_reply(body: bytes) -> tuple[int, int]:
handle, file_type, _flags = struct.unpack_from("<QII", body, 8) handle, file_type, _flags = struct.unpack_from("<QII", body, 8)
sector_size = struct.unpack_from("<I", body, 36)[0] sector_size = struct.unpack_from("<I", body, 36)[0]
if file_type != NFC_DISK: if file_type != NFC_DISK:
raise NfcProtocolError( raise NfcProtocolError(f"opened file type {file_type}, expected NFC_DISK")
f"opened file type {file_type}, expected NFC_DISK")
if sector_size == 0: if sector_size == 0:
sector_size = NFC_SECTOR_SIZE sector_size = NFC_SECTOR_SIZE
return handle, sector_size return handle, sector_size
@@ -486,7 +460,8 @@ def open_disk(
op_id: str = "nbdmode", op_id: str = "nbdmode",
version: int = NFC_PROTOCOL_VERSION, version: int = NFC_PROTOCOL_VERSION,
read_only: bool = True, read_only: bool = True,
compression: int = NFC_COMPRESSION_NONE) -> NfcDisk: compression: int = NFC_COMPRESSION_NONE,
) -> NfcDisk:
"""Open ``disk_path`` over the authenticated authd socket. """Open ``disk_path`` over the authenticated authd socket.
Matches VDDK ``VixDiskLib_Open`` of a datastore path after the NFC Matches VDDK ``VixDiskLib_Open`` of a datastore path after the NFC
@@ -506,31 +481,31 @@ def open_disk(
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.
""" """
if compression not in ( if compression not in (NFC_COMPRESSION_NONE, NFC_COMPRESSION_FASTLZ):
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"
)
if session.nfc_ssl: if session.nfc_ssl:
sock = wrap_nfcssl_socket( sock = wrap_nfcssl_socket(session.authd_sock, session.ticket.host)
session.authd_sock, session.ticket.host)
else: else:
sock = takeover_authd_socket(session.authd_sock) sock = takeover_authd_socket(session.authd_sock)
try: try:
_handshake(sock, client_name, op_id, version) _handshake(sock, client_name, op_id, version)
disk = NfcDisk( disk = NfcDisk(
sock, disk_path, handle=0, sector_size=NFC_SECTOR_SIZE, sock,
compression=compression) disk_path,
handle=0,
sector_size=NFC_SECTOR_SIZE,
compression=compression,
)
_aio_prepare(disk) _aio_prepare(disk)
path_b = disk_path.encode("utf-8") path_b = disk_path.encode("utf-8")
open_flags = ( open_flags = (
NFC_OPEN_FLAGS_READ_ONLY if read_only NFC_OPEN_FLAGS_READ_ONLY if read_only else NFC_OPEN_FLAGS_READ_WRITE
else NFC_OPEN_FLAGS_READ_WRITE) )
open_body = struct.pack( open_body = struct.pack("<IIIIII", len(path_b), 0, 0, 0, NFC_DISK, open_flags)
"<IIIIII",
len(path_b), 0, 0, 0, NFC_DISK, open_flags)
open_body = open_body.ljust(60, b"\x00") open_body = open_body.ljust(60, b"\x00")
reply = disk._aio_roundtrip( reply = disk._aio_roundtrip(NFC_AIO_MSG_OPEN_FILE, open_body, extra=path_b)
NFC_AIO_MSG_OPEN_FILE, open_body, extra=path_b)
handle, sector_size = _parse_open_reply(reply) handle, sector_size = _parse_open_reply(reply)
disk.handle = handle disk.handle = handle
disk.sector_size = sector_size disk.sector_size = sector_size
+49 -43
View File
@@ -24,8 +24,7 @@ from typing import Iterator, Optional, Union
from pyVim.connect import Disconnect from pyVim.connect import Disconnect
from pyVmomi import vim from pyVmomi import vim
from openvixdisklib import nfc_auth from openvixdisklib import nfc_auth, nfc_open
from openvixdisklib import nfc_open
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@@ -50,21 +49,20 @@ def _nfc_compression(flags: int) -> int:
alg = flags & ( alg = flags & (
VIXDISKLIB_FLAG_OPEN_COMPRESSION_ZLIB VIXDISKLIB_FLAG_OPEN_COMPRESSION_ZLIB
| VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ | VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ
| VIXDISKLIB_FLAG_OPEN_COMPRESSION_SKIPZ) | VIXDISKLIB_FLAG_OPEN_COMPRESSION_SKIPZ
)
if alg == 0: if alg == 0:
return nfc_open.NFC_COMPRESSION_NONE return nfc_open.NFC_COMPRESSION_NONE
if alg == VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ: if alg == VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ:
return nfc_open.NFC_COMPRESSION_FASTLZ return nfc_open.NFC_COMPRESSION_FASTLZ
if alg & (alg - 1): if alg & (alg - 1):
raise NotImplementedError( raise NotImplementedError(
"Cannot set two or more NBD compression algorithms at the " "Cannot set two or more NBD compression algorithms at the same time"
"same time") )
raise NotImplementedError( raise NotImplementedError(f"NBD compression open flag 0x{alg:x} is not supported")
f"NBD compression open flag 0x{alg:x} is not supported")
VIX_SUPPORTED_COMPATIBILITY_MODES = [ VIX_SUPPORTED_COMPATIBILITY_MODES = ["6.0", "6.5", "6.7", "7.0", "8.0"]
"6.0", "6.5", "6.7", "7.0", "8.0"]
def get_buffer(size: int): def get_buffer(size: int):
@@ -74,8 +72,7 @@ def get_buffer(size: int):
def _parse_vm_moref(vmx_spec: Optional[str]) -> str: def _parse_vm_moref(vmx_spec: Optional[str]) -> str:
if not vmx_spec: if not vmx_spec:
raise ValueError( raise ValueError("vmx_spec is required (for example 'moref=vm-13098')")
"vmx_spec is required (for example 'moref=vm-13098')")
if "=" in vmx_spec: if "=" in vmx_spec:
kind, value = vmx_spec.split("=", 1) kind, value = vmx_spec.split("=", 1)
if kind.lower() != "moref" or not value: if kind.lower() != "moref" or not value:
@@ -97,7 +94,8 @@ def _select_transport(transport_modes: Optional[str]) -> str:
if mode in ("nbdssl", "nbd"): if mode in ("nbdssl", "nbd"):
return mode return mode
raise NotImplementedError( raise NotImplementedError(
f"supported transports are nbdssl and nbd, got {transport_modes!r}") f"supported transports are nbdssl and nbd, got {transport_modes!r}"
)
class _Connection: class _Connection:
@@ -111,7 +109,8 @@ class _Connection:
thumbprint: Optional[str], thumbprint: Optional[str],
allow_untrusted: bool, allow_untrusted: bool,
read_only: bool, read_only: bool,
transport_mode: str) -> None: transport_mode: str,
) -> None:
self.si = si self.si = si
self.vm_moref = vm_moref self.vm_moref = vm_moref
self.snapshot_ref = snapshot_ref self.snapshot_ref = snapshot_ref
@@ -124,11 +123,7 @@ class _Connection:
class _DiskHandle: class _DiskHandle:
"""Opened NFC disk plus the authd TLS socket it was taken from.""" """Opened NFC disk plus the authd TLS socket it was taken from."""
def __init__( def __init__(self, disk: nfc_open.NfcDisk, authd_sock, transport_mode: str) -> None:
self,
disk: nfc_open.NfcDisk,
authd_sock,
transport_mode: str) -> None:
self.disk = disk self.disk = disk
self.authd_sock = authd_sock self.authd_sock = authd_sock
self.transport_mode = transport_mode self.transport_mode = transport_mode
@@ -140,7 +135,8 @@ class VixDiskLibHandle:
def __init__( def __init__(
self, self,
config_path: Optional[str] = None, config_path: Optional[str] = None,
vixdisklib_compatibility_version: Optional[str] = None) -> None: vixdisklib_compatibility_version: Optional[str] = None,
) -> None:
"""Accept the VDDK wrapper constructor; no native library is loaded. """Accept the VDDK wrapper constructor; no native library is loaded.
Args: Args:
@@ -164,18 +160,20 @@ class VixDiskLibHandle:
raise ValueError( raise ValueError(
"Unsupported vixDiskLib version format '%s'. vixDiskLib " "Unsupported vixDiskLib version format '%s'. vixDiskLib "
"compatibility mode must be of the form " "compatibility mode must be of the form "
"'$major.$minor'" % version) from ex "'$major.$minor'" % version
) from ex
version_used = version version_used = version
break break
if not version_used: if not version_used:
raise Exception( raise Exception(
"Could not initialize vixDiskLib with any of the following " "Could not initialize vixDiskLib with any of the following "
"versions: %s" % target_versions) "versions: %s" % target_versions
)
LOG.info( LOG.info(
"Successfully initialized vixDiskLib with target version '%s'", "Successfully initialized vixDiskLib with target version '%s'", version_used
version_used) )
@classmethod @classmethod
def get_vix_disklib_name(cls) -> str: def get_vix_disklib_name(cls) -> str:
@@ -204,7 +202,8 @@ class VixDiskLibHandle:
read_only: bool = True, read_only: bool = True,
transport_modes: Optional[str] = None, transport_modes: Optional[str] = None,
port: int = 443, port: int = 443,
allow_untrusted: bool = False) -> Iterator[_Connection]: allow_untrusted: bool = False,
) -> Iterator[_Connection]:
"""Login to vCenter/ESXi. Matches ``VixDiskLib_ConnectEx``. """Login to vCenter/ESXi. Matches ``VixDiskLib_ConnectEx``.
The NFC ticket and authd handshake are deferred to ``open``, as in The NFC ticket and authd handshake are deferred to ``open``, as in
@@ -235,11 +234,17 @@ class VixDiskLibHandle:
password, password,
port=port, port=port,
thumbprint=thumbprint, thumbprint=thumbprint,
allow_untrusted=allow_untrusted or not thumbprint) allow_untrusted=allow_untrusted or not thumbprint,
)
conn = _Connection( conn = _Connection(
si, vm_moref, snapshot_ref, thumbprint, si,
allow_untrusted or not thumbprint, read_only, vm_moref,
transport_mode) snapshot_ref,
thumbprint,
allow_untrusted or not thumbprint,
read_only,
transport_mode,
)
try: try:
yield conn yield conn
finally: finally:
@@ -250,7 +255,8 @@ class VixDiskLibHandle:
self, self,
conn: _Connection, conn: _Connection,
disk_path: str, disk_path: str,
flags: int = VIXDISKLIB_FLAG_OPEN_READ_ONLY) -> Iterator[_DiskHandle]: flags: int = VIXDISKLIB_FLAG_OPEN_READ_ONLY,
) -> Iterator[_DiskHandle]:
"""Open ``disk_path`` over NFC. Matches ``VixDiskLib_Open``. """Open ``disk_path`` over NFC. Matches ``VixDiskLib_Open``.
Read-only opens request ``NfcGetVmFiles`` (VM only). The VMDK Read-only opens request ``NfcGetVmFiles`` (VM only). The VMDK
@@ -271,22 +277,21 @@ class VixDiskLibHandle:
compression = _nfc_compression(flags) compression = _nfc_compression(flags)
read_only = bool(flags & VIXDISKLIB_FLAG_OPEN_READ_ONLY) read_only = bool(flags & VIXDISKLIB_FLAG_OPEN_READ_ONLY)
if not read_only and conn.read_only: if not read_only and conn.read_only:
raise NotImplementedError( raise NotImplementedError("ConnectEx was read-only; cannot open for write")
"ConnectEx was read-only; cannot open for write")
vm = vim.VirtualMachine(conn.vm_moref, conn.si._stub) vm = vim.VirtualMachine(conn.vm_moref, conn.si._stub)
nfc_ssl = conn.transport_mode == "nbdssl" nfc_ssl = conn.transport_mode == "nbdssl"
ticket = nfc_auth.get_nfc_ticket( ticket = nfc_auth.get_nfc_ticket(
conn.si, vm, read_only=read_only, conn.si, vm, read_only=read_only, disk_path=None if read_only else disk_path
disk_path=None if read_only else disk_path) )
authd_sock = nfc_auth.connect_authd( authd_sock = nfc_auth.connect_authd(
ticket, allow_untrusted=conn.allow_untrusted, nfc_ssl=nfc_ssl) ticket, allow_untrusted=conn.allow_untrusted, nfc_ssl=nfc_ssl
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, session, disk_path, read_only=read_only, compression=compression
compression=compression) )
except Exception: except Exception:
authd_sock.close() authd_sock.close()
raise raise
@@ -301,7 +306,8 @@ class VixDiskLibHandle:
disk_handle: _DiskHandle, disk_handle: _DiskHandle,
start_sector: int, start_sector: int,
num_sectors: int, num_sectors: int,
buf: Union[ctypes.Array, bytearray, memoryview]) -> None: buf: Union[ctypes.Array, bytearray, memoryview],
) -> None:
"""Read ``num_sectors`` from ``start_sector`` into ``buf``. """Read ``num_sectors`` from ``start_sector`` into ``buf``.
Args: Args:
@@ -313,9 +319,8 @@ class VixDiskLibHandle:
data = disk_handle.disk.read(start_sector, num_sectors) data = disk_handle.disk.read(start_sector, num_sectors)
if isinstance(buf, (bytearray, memoryview)): if isinstance(buf, (bytearray, memoryview)):
if len(buf) < len(data): if len(buf) < len(data):
raise Exception( raise Exception(f"read buffer is {len(buf)} bytes, need {len(data)}")
f"read buffer is {len(buf)} bytes, need {len(data)}") buf[: len(data)] = data
buf[:len(data)] = data
return return
ctypes.memmove(buf, data, len(data)) ctypes.memmove(buf, data, len(data))
@@ -324,7 +329,8 @@ class VixDiskLibHandle:
disk_handle: _DiskHandle, disk_handle: _DiskHandle,
start_sector: int, start_sector: int,
num_sectors: int, num_sectors: int,
buf: Union[ctypes.Array, bytes, bytearray, memoryview]) -> None: buf: Union[ctypes.Array, bytes, bytearray, memoryview],
) -> None:
"""Write ``num_sectors`` from ``buf`` starting at ``start_sector``. """Write ``num_sectors`` from ``buf`` starting at ``start_sector``.
Args: Args:
+59 -48
View File
@@ -20,8 +20,7 @@ from pyVmomi import vim
from openvixdisklib import nfc_auth from openvixdisklib import nfc_auth
from openvixdisklib.nfc_auth import NfcAuthSession from openvixdisklib.nfc_auth import NfcAuthSession
_REPO_ROOT = os.path.abspath( _REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
os.path.join(os.path.dirname(__file__), "..", ".."))
_CONFIG_PATH = os.path.join(_REPO_ROOT, ".test_config.yaml") _CONFIG_PATH = os.path.join(_REPO_ROOT, ".test_config.yaml")
_CONFIG_KEYS = ( _CONFIG_KEYS = (
"host", "host",
@@ -61,9 +60,8 @@ class LabEnv:
disk_path: str disk_path: str
def authenticate( def authenticate(
self, self, read_only: bool = True, nfc_ssl: bool = True
read_only: bool = True, ) -> NfcAuthSession:
nfc_ssl: bool = True) -> NfcAuthSession:
"""Login to the lab vCenter and complete NFC authd for the temp VM.""" """Login to the lab vCenter and complete NFC authd for the temp VM."""
return nfc_auth.authenticate( return nfc_auth.authenticate(
host=self.host, host=self.host,
@@ -74,10 +72,12 @@ class LabEnv:
allow_untrusted=self.allow_untrusted, allow_untrusted=self.allow_untrusted,
disk_path=None if read_only else self.disk_path, disk_path=None if read_only else self.disk_path,
read_only=read_only, read_only=read_only,
nfc_ssl=nfc_ssl) nfc_ssl=nfc_ssl,
)
def vixdisklib_connect_kwargs( def vixdisklib_connect_kwargs(
self, extra: Optional[dict[str, Any]] = None) -> dict[str, Any]: self, extra: Optional[dict[str, Any]] = None
) -> dict[str, Any]:
"""Return common ``VixDiskLib_ConnectEx`` arguments for the temp VM.""" """Return common ``VixDiskLib_ConnectEx`` arguments for the temp VM."""
kwargs: dict[str, Any] = { kwargs: dict[str, Any] = {
"server_name": self.host, "server_name": self.host,
@@ -107,7 +107,8 @@ def ensure_vddk_library_path() -> None:
parts = [p for p in current.split(":") if p] parts = [p for p in current.split(":") if p]
if VDDK_DIR not in parts: if VDDK_DIR not in parts:
os.environ["LD_LIBRARY_PATH"] = ( os.environ["LD_LIBRARY_PATH"] = (
VDDK_DIR if not current else f"{VDDK_DIR}:{current}") VDDK_DIR if not current else f"{VDDK_DIR}:{current}"
)
def require_vddk() -> None: def require_vddk() -> None:
@@ -123,13 +124,13 @@ def _load_test_config() -> dict[str, Any]:
if not os.path.isfile(_CONFIG_PATH): if not os.path.isfile(_CONFIG_PATH):
pytest.skip( pytest.skip(
"integration tests need .test_config.yaml in the repo " "integration tests need .test_config.yaml in the repo "
"root; see README.md for a sample") "root; see README.md for a sample"
)
with open(_CONFIG_PATH, encoding="utf-8") as config_file: with open(_CONFIG_PATH, encoding="utf-8") as config_file:
data = yaml.safe_load(config_file) or {} data = yaml.safe_load(config_file) or {}
missing = [key for key in _CONFIG_KEYS if key not in data] missing = [key for key in _CONFIG_KEYS if key not in data]
if missing: if missing:
raise RuntimeError( raise RuntimeError(f"{_CONFIG_PATH} is missing keys: {', '.join(missing)}")
f"{_CONFIG_PATH} is missing keys: {', '.join(missing)}")
return { return {
"host": str(data["host"]), "host": str(data["host"]),
"port": int(data["port"]), "port": int(data["port"]),
@@ -147,23 +148,23 @@ def _connect_vim(
password: str, password: str,
port: int, port: int,
thumbprint: str, thumbprint: str,
allow_untrusted: bool) -> vim.ServiceInstance: allow_untrusted: bool,
) -> vim.ServiceInstance:
return nfc_auth.connect_vim( return nfc_auth.connect_vim(
host, host,
username, username,
password, password,
port=port, port=port,
thumbprint=thumbprint, thumbprint=thumbprint,
allow_untrusted=allow_untrusted) allow_untrusted=allow_untrusted,
)
def _wait_for_task(task: vim.Task) -> Any: def _wait_for_task(task: vim.Task) -> Any:
deadline = time.monotonic() + _TASK_TIMEOUT_S deadline = time.monotonic() + _TASK_TIMEOUT_S
while task.info.state in ( while task.info.state in (vim.TaskInfo.State.running, vim.TaskInfo.State.queued):
vim.TaskInfo.State.running, vim.TaskInfo.State.queued):
if time.monotonic() > deadline: if time.monotonic() > deadline:
raise TimeoutError( raise TimeoutError(f"timed out waiting for vSphere task {task}")
f"timed out waiting for vSphere task {task}")
time.sleep(_TASK_POLL_S) time.sleep(_TASK_POLL_S)
if task.info.state != vim.TaskInfo.State.success: if task.info.state != vim.TaskInfo.State.success:
raise RuntimeError(f"vSphere task failed: {task.info.error}") raise RuntimeError(f"vSphere task failed: {task.info.error}")
@@ -171,26 +172,28 @@ def _wait_for_task(task: vim.Task) -> Any:
def _find_datacenter( def _find_datacenter(
content: vim.ServiceInstanceContent, content: vim.ServiceInstanceContent, datacenter_name: str
datacenter_name: str) -> vim.Datacenter: ) -> vim.Datacenter:
matches = [ matches = [
entity for entity in content.rootFolder.childEntity entity
if isinstance(entity, vim.Datacenter) for entity in content.rootFolder.childEntity
and entity.name == datacenter_name] if isinstance(entity, vim.Datacenter) and entity.name == datacenter_name
]
if not matches: if not matches:
raise RuntimeError(f"datacenter {datacenter_name!r} not found") raise RuntimeError(f"datacenter {datacenter_name!r} not found")
return matches[0] return matches[0]
def _find_datastore( def _find_datastore(datacenter: vim.Datacenter, datastore_name: str) -> vim.Datastore:
datacenter: vim.Datacenter, datastore_name: str) -> vim.Datastore:
matches = [ matches = [
datastore for datastore in datacenter.datastore datastore
if datastore.name == datastore_name] for datastore in datacenter.datastore
if datastore.name == datastore_name
]
if not matches: if not matches:
raise RuntimeError( raise RuntimeError(
f"datastore {datastore_name!r} not found in " f"datastore {datastore_name!r} not found in datacenter {datacenter.name!r}"
f"datacenter {datacenter.name!r}") )
return matches[0] return matches[0]
@@ -200,17 +203,14 @@ def _vm_config_spec(vm_name: str, datastore_name: str) -> vim.vm.ConfigSpec:
config.guestId = "otherGuest64" config.guestId = "otherGuest64"
config.memoryMB = 128 config.memoryMB = 128
config.numCPUs = 1 config.numCPUs = 1
config.files = vim.vm.FileInfo( config.files = vim.vm.FileInfo(vmPathName=f"[{datastore_name}]")
vmPathName=f"[{datastore_name}]")
controller = vim.vm.device.ParaVirtualSCSIController() controller = vim.vm.device.ParaVirtualSCSIController()
controller.key = 1000 controller.key = 1000
controller.busNumber = 0 controller.busNumber = 0
controller.sharedBus = ( controller.sharedBus = vim.vm.device.VirtualSCSIController.Sharing.noSharing
vim.vm.device.VirtualSCSIController.Sharing.noSharing)
controller_spec = vim.vm.device.VirtualDeviceSpec() controller_spec = vim.vm.device.VirtualDeviceSpec()
controller_spec.operation = ( controller_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
vim.vm.device.VirtualDeviceSpec.Operation.add)
controller_spec.device = controller controller_spec.device = controller
backing = vim.vm.device.VirtualDisk.FlatVer2BackingInfo() backing = vim.vm.device.VirtualDisk.FlatVer2BackingInfo()
@@ -225,8 +225,7 @@ def _vm_config_spec(vm_name: str, datastore_name: str) -> vim.vm.ConfigSpec:
disk.backing = backing disk.backing = backing
disk_spec = vim.vm.device.VirtualDeviceSpec() disk_spec = vim.vm.device.VirtualDeviceSpec()
disk_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add disk_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
disk_spec.fileOperation = ( disk_spec.fileOperation = vim.vm.device.VirtualDeviceSpec.FileOperation.create
vim.vm.device.VirtualDeviceSpec.FileOperation.create)
disk_spec.device = disk disk_spec.device = disk
config.deviceChange = [controller_spec, disk_spec] config.deviceChange = [controller_spec, disk_spec]
@@ -238,8 +237,13 @@ def create_lab_vm() -> LabEnv:
cfg = _load_test_config() cfg = _load_test_config()
thumbprint = nfc_auth.get_ssl_cert_thumbprint(cfg["host"], cfg["port"]) thumbprint = nfc_auth.get_ssl_cert_thumbprint(cfg["host"], cfg["port"])
si = _connect_vim( si = _connect_vim(
cfg["host"], cfg["username"], cfg["password"], cfg["port"], cfg["host"],
thumbprint, cfg["allow_untrusted"]) cfg["username"],
cfg["password"],
cfg["port"],
thumbprint,
cfg["allow_untrusted"],
)
vm = None vm = None
try: try:
content = si.RetrieveContent() content = si.RetrieveContent()
@@ -247,22 +251,23 @@ def create_lab_vm() -> LabEnv:
datastore = _find_datastore(datacenter, cfg["datastore"]) datastore = _find_datastore(datacenter, cfg["datastore"])
if not datastore.host: if not datastore.host:
raise RuntimeError( raise RuntimeError(
f"datastore {cfg['datastore']!r} is not mounted on any host") f"datastore {cfg['datastore']!r} is not mounted on any host"
)
host = datastore.host[0].key host = datastore.host[0].key
pool = host.parent.resourcePool pool = host.parent.resourcePool
vm_name = _LAB_VM_PREFIX + uuid.uuid4().hex[:12] vm_name = _LAB_VM_PREFIX + uuid.uuid4().hex[:12]
vm = _wait_for_task( vm = _wait_for_task(
datacenter.vmFolder.CreateVM_Task( datacenter.vmFolder.CreateVM_Task(
config=_vm_config_spec(vm_name, datastore.name), config=_vm_config_spec(vm_name, datastore.name), pool=pool, host=host
pool=pool, )
host=host)) )
disks = [ disks = [
device.backing.fileName device.backing.fileName
for device in vm.config.hardware.device for device in vm.config.hardware.device
if isinstance(device, vim.vm.device.VirtualDisk)] if isinstance(device, vim.vm.device.VirtualDisk)
]
if not disks: if not disks:
raise RuntimeError( raise RuntimeError(f"temporary VM {vm_name!r} has no virtual disks")
f"temporary VM {vm_name!r} has no virtual disks")
return LabEnv( return LabEnv(
host=cfg["host"], host=cfg["host"],
port=cfg["port"], port=cfg["port"],
@@ -274,7 +279,8 @@ def create_lab_vm() -> LabEnv:
thumbprint=thumbprint, thumbprint=thumbprint,
vm_moref=vm._moId, vm_moref=vm._moId,
vmx_spec=f"moref={vm._moId}", vmx_spec=f"moref={vm._moId}",
disk_path=disks[0]) disk_path=disks[0],
)
except Exception: except Exception:
if vm is not None: if vm is not None:
try: try:
@@ -289,8 +295,13 @@ def create_lab_vm() -> LabEnv:
def destroy_lab_vm(lab: LabEnv) -> None: def destroy_lab_vm(lab: LabEnv) -> None:
"""Power off and delete the temporary lab VM if it still exists.""" """Power off and delete the temporary lab VM if it still exists."""
si = _connect_vim( si = _connect_vim(
lab.host, lab.username, lab.password, lab.port, lab.host,
lab.thumbprint, lab.allow_untrusted) lab.username,
lab.password,
lab.port,
lab.thumbprint,
lab.allow_untrusted,
)
try: try:
vm = vim.VirtualMachine(lab.vm_moref, si._stub) vm = vim.VirtualMachine(lab.vm_moref, si._stub)
try: try:
+18 -23
View File
@@ -9,8 +9,7 @@ import pytest
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 ( from tests.integration.base import SECTOR_AT_1GB, SECTOR_SIZE, LabEnv, pattern_bytes
LabEnv, SECTOR_AT_1GB, SECTOR_SIZE, pattern_bytes)
def _connect_extra(lab: LabEnv, module: Any) -> Optional[dict[str, Any]]: def _connect_extra(lab: LabEnv, module: Any) -> Optional[dict[str, Any]]:
@@ -21,14 +20,12 @@ def _connect_extra(lab: LabEnv, module: Any) -> Optional[dict[str, Any]]:
def _write_sectors( def _write_sectors(
lab: LabEnv, lab: LabEnv, module: Any, payloads: dict[int, bytes], flags: int = 0
module: Any, ) -> None:
payloads: dict[int, bytes],
flags: int = 0) -> None:
"""Write one sector at each index using a vixdisklib-compatible module.""" """Write one sector at each index using a vixdisklib-compatible module."""
handle = module.VixDiskLibHandle( handle = module.VixDiskLibHandle(
vixdisklib_compatibility_version="8.0", vixdisklib_compatibility_version="8.0", config_path=None
config_path=None) )
buf = module.get_buffer(SECTOR_SIZE) buf = module.get_buffer(SECTOR_SIZE)
kwargs = lab.vixdisklib_connect_kwargs(_connect_extra(lab, module)) kwargs = lab.vixdisklib_connect_kwargs(_connect_extra(lab, module))
with handle.connect(**kwargs) as conn: with handle.connect(**kwargs) as conn:
@@ -39,14 +36,12 @@ def _write_sectors(
def _read_sectors( def _read_sectors(
lab: LabEnv, lab: LabEnv, module: Any, sectors: tuple[int, ...], flags: int = 0
module: Any, ) -> dict[int, bytes]:
sectors: tuple[int, ...],
flags: int = 0) -> dict[int, bytes]:
"""Read one sector at each index using a vixdisklib-compatible module.""" """Read one sector at each index using a vixdisklib-compatible module."""
handle = module.VixDiskLibHandle( handle = module.VixDiskLibHandle(
vixdisklib_compatibility_version="8.0", vixdisklib_compatibility_version="8.0", config_path=None
config_path=None) )
buf = module.get_buffer(SECTOR_SIZE) buf = module.get_buffer(SECTOR_SIZE)
result: dict[int, bytes] = {} result: dict[int, bytes] = {}
kwargs = lab.vixdisklib_connect_kwargs(_connect_extra(lab, module)) kwargs = lab.vixdisklib_connect_kwargs(_connect_extra(lab, module))
@@ -60,26 +55,26 @@ def _read_sectors(
def _assert_both_read( def _assert_both_read(
lab: LabEnv, lab: LabEnv, sectors: tuple[int, ...], expected: dict[int, bytes], flags: int = 0
sectors: tuple[int, ...], ) -> None:
expected: dict[int, bytes],
flags: int = 0) -> None:
vddk_data = _read_sectors(lab, vixdisklib, sectors, flags=flags) vddk_data = _read_sectors(lab, vixdisklib, sectors, flags=flags)
replacement = _read_sectors(lab, open_vix, sectors, flags=flags) replacement = _read_sectors(lab, open_vix, sectors, flags=flags)
for start in sectors: for start in sectors:
assert vddk_data[start] == expected[start], ( assert vddk_data[start] == expected[start], f"VDDK mismatch at sector {start}"
f"VDDK mismatch at sector {start}")
assert replacement[start] == expected[start], ( assert replacement[start] == expected[start], (
f"openvixdisklib mismatch at sector {start}") f"openvixdisklib mismatch at sector {start}"
)
class TestCrosscheck: class TestCrosscheck:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"open_flags", "open_flags",
[0, vixdisklib.VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ], [0, vixdisklib.VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ],
ids=["plain", "fastlz"]) ids=["plain", "fastlz"],
)
def test_openvixdisklib_matches_vddk_sectors( def test_openvixdisklib_matches_vddk_sectors(
self, lab: LabEnv, vddk: None, open_flags: int) -> None: self, lab: LabEnv, vddk: None, open_flags: int
) -> None:
"""Writes from either library must be visible to both readers.""" """Writes from either library must be visible to both readers."""
sectors = (0, 1, SECTOR_AT_1GB) sectors = (0, 1, SECTOR_AT_1GB)
vddk_payloads = { vddk_payloads = {
+8 -7
View File
@@ -6,24 +6,25 @@
import pytest import pytest
from openvixdisklib import nfc_open from openvixdisklib import nfc_open
from tests.integration.base import LabEnv, SECTOR_SIZE, pattern_bytes from tests.integration.base import SECTOR_SIZE, LabEnv, pattern_bytes
class TestNfcOpen: class TestNfcOpen:
@pytest.mark.parametrize( @pytest.mark.parametrize("nfc_ssl", [True, False], ids=["nbdssl", "nbd"])
"nfc_ssl", [True, False], ids=["nbdssl", "nbd"])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"compression", "compression",
[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_open_disk_and_read_first_sector( def test_open_disk_and_read_first_sector(
self, lab: LabEnv, nfc_ssl: bool, compression: int) -> None: self, lab: LabEnv, nfc_ssl: bool, compression: int
) -> None:
"""Open the temp VMDK, write sector 0, and read it back.""" """Open the temp VMDK, write sector 0, and read it back."""
expected = pattern_bytes(SECTOR_SIZE, b"NFC-OPEN-S0") expected = pattern_bytes(SECTOR_SIZE, b"NFC-OPEN-S0")
with lab.authenticate(read_only=False, nfc_ssl=nfc_ssl) as session: with lab.authenticate(read_only=False, nfc_ssl=nfc_ssl) as session:
with nfc_open.open_disk( with nfc_open.open_disk(
session, lab.disk_path, read_only=False, session, lab.disk_path, read_only=False, compression=compression
compression=compression) as disk: ) as disk:
assert disk.path == lab.disk_path assert disk.path == lab.disk_path
assert disk.handle > 0 assert disk.handle > 0
assert disk.sector_size == SECTOR_SIZE assert disk.sector_size == SECTOR_SIZE
+15 -15
View File
@@ -8,7 +8,7 @@ import os
import pytest import pytest
from openvixdisklib import nfc_open from openvixdisklib import nfc_open
from tests.integration.base import LabEnv, SECTOR_SIZE, pattern_bytes from tests.integration.base import SECTOR_SIZE, LabEnv, pattern_bytes
_32MIB = 32 * 1024 * 1024 _32MIB = 32 * 1024 * 1024
@@ -17,9 +17,9 @@ class TestNfcReadWrite:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"compression", "compression",
[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_sector_writes_and_reads( )
self, lab: LabEnv, compression: int) -> None: def test_sector_writes_and_reads(self, lab: LabEnv, compression: int) -> None:
"""Write known patterns and read them back at several ranges.""" """Write known patterns and read them back at several ranges."""
ranges = [ ranges = [
(0, 1), (0, 1),
@@ -32,8 +32,8 @@ class TestNfcReadWrite:
] ]
with lab.authenticate(read_only=False) as session: with lab.authenticate(read_only=False) as session:
with nfc_open.open_disk( with nfc_open.open_disk(
session, lab.disk_path, read_only=False, session, lab.disk_path, read_only=False, compression=compression
compression=compression) as disk: ) as disk:
for start, n_sectors in ranges: for start, n_sectors in ranges:
length = n_sectors * SECTOR_SIZE length = n_sectors * SECTOR_SIZE
seed = f"NFC-R{start}:{n_sectors}-".encode() seed = f"NFC-R{start}:{n_sectors}-".encode()
@@ -53,29 +53,29 @@ class TestNfcReadWrite:
assert disk.read(1, 1) == two_to_write[SECTOR_SIZE:] assert disk.read(1, 1) == two_to_write[SECTOR_SIZE:]
big_seed = b"NFC-129-SECTOR-WRITE" big_seed = b"NFC-129-SECTOR-WRITE"
big_to_write = pattern_bytes( big_to_write = pattern_bytes(129 * SECTOR_SIZE, big_seed)
129 * SECTOR_SIZE, big_seed)
disk.write(0, 129, big_to_write) disk.write(0, 129, big_to_write)
big_got = disk.read(0, 129) big_got = disk.read(0, 129)
assert big_got is not big_to_write assert big_got is not big_to_write
assert big_got == big_to_write assert big_got == big_to_write
assert ( assert (
big_got[SECTOR_SIZE:2 * SECTOR_SIZE] big_got[SECTOR_SIZE : 2 * SECTOR_SIZE]
== big_to_write[SECTOR_SIZE:2 * SECTOR_SIZE]) == big_to_write[SECTOR_SIZE : 2 * SECTOR_SIZE]
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"compression", "compression",
[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: def test_write_and_read_32mb(self, lab: LabEnv, compression: int) -> None:
"""Write 32 MiB (512 AIO chunks) and read it back in one request.""" """Write 32 MiB (512 AIO chunks) and read it back in one request."""
n_sectors = _32MIB // SECTOR_SIZE n_sectors = _32MIB // SECTOR_SIZE
to_write = os.urandom(_32MIB) to_write = os.urandom(_32MIB)
with lab.authenticate(read_only=False) as session: with lab.authenticate(read_only=False) as session:
with nfc_open.open_disk( with nfc_open.open_disk(
session, lab.disk_path, read_only=False, session, lab.disk_path, read_only=False, compression=compression
compression=compression) as disk: ) as disk:
disk.write(0, n_sectors, to_write) disk.write(0, n_sectors, to_write)
got = disk.read(0, n_sectors) got = disk.read(0, n_sectors)
assert got is not to_write assert got is not to_write
+37 -20
View File
@@ -3,18 +3,24 @@
"""Exercise the VDDK-compatible openvixdisklib handle against the lab.""" """Exercise the VDDK-compatible openvixdisklib handle against the lab."""
import pytest
from pyVim.connect import Disconnect from pyVim.connect import Disconnect
from pyVmomi import vim from pyVmomi import vim
import pytest
from openvixdisklib import openvixdisklib as vixdisklib from openvixdisklib import openvixdisklib as vixdisklib
from tests.integration.base import ( from tests.integration.base import (
LabEnv, SECTOR_AT_1GB, SECTOR_SIZE, _connect_vim, _wait_for_task, SECTOR_AT_1GB,
pattern_bytes) SECTOR_SIZE,
LabEnv,
_connect_vim,
_wait_for_task,
pattern_bytes,
)
def _virtual_disk_backing( def _virtual_disk_backing(
vm: vim.VirtualMachine) -> vim.vm.device.VirtualDevice.BackingInfo: vm: vim.VirtualMachine,
) -> vim.vm.device.VirtualDevice.BackingInfo:
"""Return the lab VM's first virtual disk backing.""" """Return the lab VM's first virtual disk backing."""
for device in vm.config.hardware.device: for device in vm.config.hardware.device:
if isinstance(device, vim.vm.device.VirtualDisk): if isinstance(device, vim.vm.device.VirtualDisk):
@@ -27,19 +33,23 @@ class TestOpenvixdisklib:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"open_flags", "open_flags",
[0, vixdisklib.VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ], [0, vixdisklib.VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ],
ids=["plain", "fastlz"]) ids=["plain", "fastlz"],
)
def test_write_and_read_sector_zero_and_one_gib( def test_write_and_read_sector_zero_and_one_gib(
self, lab: LabEnv, transport_mode: str, open_flags: int) -> None: self, lab: LabEnv, transport_mode: str, open_flags: int
) -> None:
"""Write then read sector 0 and the sector at a 1 GiB offset.""" """Write then read sector 0 and the sector at a 1 GiB offset."""
handle = vixdisklib.VixDiskLibHandle( handle = vixdisklib.VixDiskLibHandle(
vixdisklib_compatibility_version="8.0", vixdisklib_compatibility_version="8.0", config_path=None
config_path=None) )
write_buf = vixdisklib.get_buffer(SECTOR_SIZE) write_buf = vixdisklib.get_buffer(SECTOR_SIZE)
read_buf = vixdisklib.get_buffer(SECTOR_SIZE) read_buf = vixdisklib.get_buffer(SECTOR_SIZE)
connect_kwargs = lab.vixdisklib_connect_kwargs({ connect_kwargs = lab.vixdisklib_connect_kwargs(
{
"allow_untrusted": lab.allow_untrusted, "allow_untrusted": lab.allow_untrusted,
"transport_modes": transport_mode, "transport_modes": transport_mode,
}) }
)
patterns = { patterns = {
0: pattern_bytes(SECTOR_SIZE, b"OVDL-S0"), 0: pattern_bytes(SECTOR_SIZE, b"OVDL-S0"),
SECTOR_AT_1GB: pattern_bytes(SECTOR_SIZE, b"OVDL-1GB"), SECTOR_AT_1GB: pattern_bytes(SECTOR_SIZE, b"OVDL-1GB"),
@@ -63,19 +73,22 @@ class TestOpenvixdisklib:
``device.backing.fileName``; VDDK still opens it with a VM-only ``device.backing.fileName``; VDDK still opens it with a VM-only
ticket and NFC ``OPEN_FILE``. ticket and NFC ``OPEN_FILE``.
""" """
handle = vixdisklib.VixDiskLibHandle( handle = vixdisklib.VixDiskLibHandle(vixdisklib_compatibility_version="8.0")
vixdisklib_compatibility_version="8.0")
expected = pattern_bytes(SECTOR_SIZE, b"OVDL-RO") expected = pattern_bytes(SECTOR_SIZE, b"OVDL-RO")
write_buf = vixdisklib.get_buffer(SECTOR_SIZE) write_buf = vixdisklib.get_buffer(SECTOR_SIZE)
read_buf = vixdisklib.get_buffer(SECTOR_SIZE) read_buf = vixdisklib.get_buffer(SECTOR_SIZE)
write_buf[:SECTOR_SIZE] = expected write_buf[:SECTOR_SIZE] = expected
write_kwargs = lab.vixdisklib_connect_kwargs({ write_kwargs = lab.vixdisklib_connect_kwargs(
{
"allow_untrusted": lab.allow_untrusted, "allow_untrusted": lab.allow_untrusted,
}) }
read_kwargs = lab.vixdisklib_connect_kwargs({ )
read_kwargs = lab.vixdisklib_connect_kwargs(
{
"allow_untrusted": lab.allow_untrusted, "allow_untrusted": lab.allow_untrusted,
"read_only": True, "read_only": True,
}) }
)
read_flags = vixdisklib.VIXDISKLIB_FLAG_OPEN_READ_ONLY read_flags = vixdisklib.VIXDISKLIB_FLAG_OPEN_READ_ONLY
def read_sector(path: str) -> bytes: def read_sector(path: str) -> bytes:
@@ -92,12 +105,16 @@ class TestOpenvixdisklib:
assert read_sector(lab.disk_path) == expected assert read_sector(lab.disk_path) == expected
si = _connect_vim( si = _connect_vim(
lab.host, lab.username, lab.password, lab.port, lab.host,
lab.thumbprint, lab.allow_untrusted) lab.username,
lab.password,
lab.port,
lab.thumbprint,
lab.allow_untrusted,
)
try: try:
vm = vim.VirtualMachine(lab.vm_moref, si._stub) vm = vim.VirtualMachine(lab.vm_moref, si._stub)
_wait_for_task( _wait_for_task(vm.CreateSnapshot_Task("ovdl-readonly", "", False, False))
vm.CreateSnapshot_Task("ovdl-readonly", "", False, False))
backing = _virtual_disk_backing(vm) backing = _virtual_disk_backing(vm)
parent = getattr(backing, "parent", None) parent = getattr(backing, "parent", None)
assert parent is not None assert parent is not None
+4 -5
View File
@@ -4,16 +4,15 @@
"""Exercise native VDDK via tests.integration.vixdisklib against the lab.""" """Exercise native VDDK via tests.integration.vixdisklib against the lab."""
from tests.integration import vixdisklib from tests.integration import vixdisklib
from tests.integration.base import LabEnv, SECTOR_SIZE, pattern_bytes from tests.integration.base import SECTOR_SIZE, LabEnv, pattern_bytes
class TestVddk: class TestVddk:
def test_write_and_read_first_sector( def test_write_and_read_first_sector(self, lab: LabEnv, vddk: None) -> None:
self, lab: LabEnv, vddk: None) -> None:
"""Open the temp VMDK with VDDK, write sector 0, and read it back.""" """Open the temp VMDK with VDDK, write sector 0, and read it back."""
handle = vixdisklib.VixDiskLibHandle( handle = vixdisklib.VixDiskLibHandle(
vixdisklib_compatibility_version="8.0", vixdisklib_compatibility_version="8.0", config_path=None
config_path=None) )
write_buf = vixdisklib.get_buffer(SECTOR_SIZE) write_buf = vixdisklib.get_buffer(SECTOR_SIZE)
read_buf = vixdisklib.get_buffer(SECTOR_SIZE) read_buf = vixdisklib.get_buffer(SECTOR_SIZE)
expected = pattern_bytes(SECTOR_SIZE, b"VDDK-S0") expected = pattern_bytes(SECTOR_SIZE, b"VDDK-S0")
+1 -1
View File
@@ -534,5 +534,5 @@ VIX_ERROR_CODE_MAP = {
VIX_E_NET_HTTP_TOO_MANY_REDIRECTS: "Reached the maximum number of redirects.", VIX_E_NET_HTTP_TOO_MANY_REDIRECTS: "Reached the maximum number of redirects.",
VIX_E_NET_HTTP_TRANSFER: "Failure sending/receiving network data.", VIX_E_NET_HTTP_TRANSFER: "Failure sending/receiving network data.",
VIX_E_NET_HTTP_SSL_SECURITY: "An SSL error occurred.", VIX_E_NET_HTTP_SSL_SECURITY: "An SSL error occurred.",
VIX_E_NET_HTTP_GENERIC: "A generic HTTP error occurred." VIX_E_NET_HTTP_GENERIC: "A generic HTTP error occurred.",
} }
+114 -58
View File
@@ -34,8 +34,7 @@ VIXDISKLIB_FLAG_OPEN_COMPRESSION_ZLIB = 16
VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ = 32 VIXDISKLIB_FLAG_OPEN_COMPRESSION_FASTLZ = 32
VIXDISKLIB_FLAG_OPEN_COMPRESSION_SKIPZ = 64 VIXDISKLIB_FLAG_OPEN_COMPRESSION_SKIPZ = 64
VIX_SUPPORTED_COMPATIBILITY_MODES = [ VIX_SUPPORTED_COMPATIBILITY_MODES = ["6.0", "6.5", "6.7", "7.0", "8.0"]
"6.0", "6.5", "6.7", "7.0", "8.0"]
class VixDiskLibUidPasswdCreds(ctypes.Structure): class VixDiskLibUidPasswdCreds(ctypes.Structure):
@@ -84,12 +83,10 @@ def get_buffer(size):
class VixDiskLibHandle(object): class VixDiskLibHandle(object):
""" Class which acts as a proxy for vixDiskLib-related operations: """Class which acts as a proxy for vixDiskLib-related operations:"""
"""
def __init__( def __init__(self, config_path=None, vixdisklib_compatibility_version=None):
self, config_path=None, vixdisklib_compatibility_version=None): self._vix_disklib = ctypes.cdll.LoadLibrary(self.get_vix_disklib_name())
self._vix_disklib = ctypes.cdll.LoadLibrary(
self.get_vix_disklib_name())
self._setup_vix_disklib() self._setup_vix_disklib()
if config_path: if config_path:
@@ -113,86 +110,119 @@ class VixDiskLibHandle(object):
raise ValueError( raise ValueError(
"Unsupported vixDiskLib version format '%s'. vixDiskLib " "Unsupported vixDiskLib version format '%s'. vixDiskLib "
"compatibility mode must be of the form " "compatibility mode must be of the form "
"'$major.$minor'" % version) from ex "'$major.$minor'" % version
) from ex
try: try:
self._check_err(self._vix_disklib.VixDiskLib_InitEx( self._check_err(
major_ver, minor_ver, None, None, None, None, config_path)) self._vix_disklib.VixDiskLib_InitEx(
major_ver, minor_ver, None, None, None, None, config_path
)
)
version_used = version version_used = version
break break
except Exception: except Exception:
LOG.debug( LOG.debug(
"Failed to initialize vixDiskLib using compatibility " "Failed to initialize vixDiskLib using compatibility "
"version '%s'. Trying next version. Error trace: %s", "version '%s'. Trying next version. Error trace: %s",
version, traceback.format_exc()) version,
traceback.format_exc(),
)
if not version_used: if not version_used:
raise Exception( raise Exception(
"Could not initialize vixDiskLib with any of the following " "Could not initialize vixDiskLib with any of the following "
"versions: %s" % target_versions) "versions: %s" % target_versions
)
LOG.info( LOG.info(
"Successfully initialized vixDiskLib with target version '%s'", "Successfully initialized vixDiskLib with target version '%s'", version_used
version_used) )
@classmethod @classmethod
def get_vix_disklib_name(cls): def get_vix_disklib_name(cls):
vixDiskLibName = None vixDiskLibName = None
if os.name == 'nt': if os.name == "nt":
vixDiskLibName = 'vixDiskLib.dll' vixDiskLibName = "vixDiskLib.dll"
else: else:
vixDiskLibName = 'libvixDiskLib.so' vixDiskLibName = "libvixDiskLib.so"
return vixDiskLibName return vixDiskLibName
def _setup_vix_disklib(self): def _setup_vix_disklib(self):
self._vix_disklib.VixDiskLib_InitEx.argtypes = [ self._vix_disklib.VixDiskLib_InitEx.argtypes = [
ctypes.c_uint32, ctypes.c_uint32, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32,
ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p] ctypes.c_uint32,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_char_p,
ctypes.c_char_p,
]
self._vix_disklib.VixDiskLib_InitEx.restype = ctypes.c_uint64 self._vix_disklib.VixDiskLib_InitEx.restype = ctypes.c_uint64
self._vix_disklib.VixDiskLib_GetErrorText.argtypes = [ self._vix_disklib.VixDiskLib_GetErrorText.argtypes = [
ctypes.c_uint64, ctypes.c_char_p] ctypes.c_uint64,
ctypes.c_char_p,
]
self._vix_disklib.VixDiskLib_GetErrorText.restype = ctypes.c_void_p self._vix_disklib.VixDiskLib_GetErrorText.restype = ctypes.c_void_p
self._vix_disklib.VixDiskLib_FreeErrorText.arg_types = [ self._vix_disklib.VixDiskLib_FreeErrorText.arg_types = [ctypes.c_char_p]
ctypes.c_char_p]
self._vix_disklib.VixDiskLib_FreeErrorText.restype = None self._vix_disklib.VixDiskLib_FreeErrorText.restype = None
self._vix_disklib.VixDiskLib_ListTransportModes.argtypes = [] self._vix_disklib.VixDiskLib_ListTransportModes.argtypes = []
self._vix_disklib.VixDiskLib_ListTransportModes.restype = ( self._vix_disklib.VixDiskLib_ListTransportModes.restype = ctypes.c_char_p
ctypes.c_char_p)
self._vix_disklib.VixDiskLib_GetTransportMode.argtypes = [ self._vix_disklib.VixDiskLib_GetTransportMode.argtypes = [ctypes.c_void_p]
ctypes.c_void_p] self._vix_disklib.VixDiskLib_GetTransportMode.restype = ctypes.c_char_p
self._vix_disklib.VixDiskLib_GetTransportMode.restype = (
ctypes.c_char_p)
self._vix_disklib.VixDiskLib_ConnectEx.argtypes = [ self._vix_disklib.VixDiskLib_ConnectEx.argtypes = [
ctypes.POINTER(VixDiskLibConnectParams), ctypes.c_char, ctypes.POINTER(VixDiskLibConnectParams),
ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_void_p)] ctypes.c_char,
ctypes.c_char_p,
ctypes.c_char_p,
ctypes.POINTER(ctypes.c_void_p),
]
self._vix_disklib.VixDiskLib_ConnectEx.restype = ctypes.c_uint64 self._vix_disklib.VixDiskLib_ConnectEx.restype = ctypes.c_uint64
self._vix_disklib.VixDiskLib_Open.argtypes = [ self._vix_disklib.VixDiskLib_Open.argtypes = [
ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint32, ctypes.c_void_p,
ctypes.POINTER(ctypes.c_void_p)] ctypes.c_char_p,
ctypes.c_uint32,
ctypes.POINTER(ctypes.c_void_p),
]
self._vix_disklib.VixDiskLib_Open.restype = ctypes.c_uint64 self._vix_disklib.VixDiskLib_Open.restype = ctypes.c_uint64
self._vix_disklib.VixDiskLib_Read.argtypes = [ self._vix_disklib.VixDiskLib_Read.argtypes = [
ctypes.c_void_p, ctypes.c_uint64, ctypes.c_uint64, ctypes.c_char_p] ctypes.c_void_p,
ctypes.c_uint64,
ctypes.c_uint64,
ctypes.c_char_p,
]
self._vix_disklib.VixDiskLib_Read.restype = ctypes.c_uint64 self._vix_disklib.VixDiskLib_Read.restype = ctypes.c_uint64
self._vix_disklib.VixDiskLib_Write.argtypes = [ self._vix_disklib.VixDiskLib_Write.argtypes = [
ctypes.c_void_p, ctypes.c_uint64, ctypes.c_uint64, ctypes.c_char_p] ctypes.c_void_p,
ctypes.c_uint64,
ctypes.c_uint64,
ctypes.c_char_p,
]
self._vix_disklib.VixDiskLib_Write.restype = ctypes.c_uint64 self._vix_disklib.VixDiskLib_Write.restype = ctypes.c_uint64
self._vix_disklib.VixDiskLib_GetMetadataKeys.argtypes = [ self._vix_disklib.VixDiskLib_GetMetadataKeys.argtypes = [
ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint64, ctypes.c_void_p,
ctypes.POINTER(ctypes.c_uint64)] ctypes.c_char_p,
ctypes.c_uint64,
ctypes.POINTER(ctypes.c_uint64),
]
self._vix_disklib.VixDiskLib_GetMetadataKeys.restype = ctypes.c_uint64 self._vix_disklib.VixDiskLib_GetMetadataKeys.restype = ctypes.c_uint64
self._vix_disklib.VixDiskLib_ReadMetadata.argtypes = [ self._vix_disklib.VixDiskLib_ReadMetadata.argtypes = [
ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_uint64, ctypes.c_void_p,
ctypes.POINTER(ctypes.c_uint64)] ctypes.c_char_p,
ctypes.c_char_p,
ctypes.c_uint64,
ctypes.POINTER(ctypes.c_uint64),
]
self._vix_disklib.VixDiskLib_ReadMetadata.restype = ctypes.c_uint64 self._vix_disklib.VixDiskLib_ReadMetadata.restype = ctypes.c_uint64
self._vix_disklib.VixDiskLib_Close.argtypes = [ctypes.c_void_p] self._vix_disklib.VixDiskLib_Close.argtypes = [ctypes.c_void_p]
@@ -207,10 +237,10 @@ class VixDiskLibHandle(object):
def _check_err(self, err, allowed_values=[vix_disklib_errors.VIX_OK]): def _check_err(self, err, allowed_values=[vix_disklib_errors.VIX_OK]):
if err not in allowed_values: if err not in allowed_values:
err_msg = self._vix_disklib.VixDiskLib_GetErrorText(err, None) err_msg = self._vix_disklib.VixDiskLib_GetErrorText(err, None)
err_msg_copy = str(ctypes.cast( err_msg_copy = str(ctypes.cast(err_msg, ctypes.c_char_p).value.decode())
err_msg, ctypes.c_char_p).value.decode())
self._vix_disklib.VixDiskLib_FreeErrorText( self._vix_disklib.VixDiskLib_FreeErrorText(
ctypes.cast(err_msg, ctypes.c_char_p)) ctypes.cast(err_msg, ctypes.c_char_p)
)
msg = None msg = None
if err == vix_disklib_errors.VIX_E_OUT_OF_MEMORY: if err == vix_disklib_errors.VIX_E_OUT_OF_MEMORY:
@@ -220,7 +250,8 @@ class VixDiskLibHandle(object):
"by vCenter, so enough RAM to run the export is required " "by vCenter, so enough RAM to run the export is required "
"on all hosts. To force the export from the specific host " "on all hosts. To force the export from the specific host "
"the VM is on, create a Coriolis endpoint with the DNS " "the VM is on, create a Coriolis endpoint with the DNS "
"name/IP address of that host.") "name/IP address of that host."
)
if err == vix_disklib_errors.VIX_E_HOST_NETWORK_CONN_REFUSED: if err == vix_disklib_errors.VIX_E_HOST_NETWORK_CONN_REFUSED:
msg = ( msg = (
"The ESXi host performing the CBT export refused " "The ESXi host performing the CBT export refused "
@@ -231,7 +262,8 @@ class VixDiskLibHandle(object):
"facilitate this. Alternatively, try connecting Coriolis " "facilitate this. Alternatively, try connecting Coriolis "
"directly to the specific ESXi host which is running the " "directly to the specific ESXi host which is running the "
"VM(s) to be migrated by creating a Coriolis endpoint " "VM(s) to be migrated by creating a Coriolis endpoint "
"using the DNS name/IP address of the host itself.") "using the DNS name/IP address of the host itself."
)
if err == vix_disklib_errors.VIX_E_CANNOT_CONNECT_TO_HOST: if err == vix_disklib_errors.VIX_E_CANNOT_CONNECT_TO_HOST:
msg = ( msg = (
@@ -239,7 +271,8 @@ class VixDiskLibHandle(object):
"CBT export. If the Coriolis Endpoint connects to a " "CBT export. If the Coriolis Endpoint connects to a "
"vSphere host, please try connecting Coriolis to the ESXi " "vSphere host, please try connecting Coriolis to the ESXi "
"host directly. If problem persists, try re-enabling CBT " "host directly. If problem persists, try re-enabling CBT "
"on the VM, or moving it to another ESXi host.") "on the VM, or moving it to another ESXi host."
)
err_msg = err_msg_copy err_msg = err_msg_copy
if msg: if msg:
@@ -250,7 +283,7 @@ class VixDiskLibHandle(object):
def get_transport_modes(self): def get_transport_modes(self):
transport_modes = self._vix_disklib.VixDiskLib_ListTransportModes() transport_modes = self._vix_disklib.VixDiskLib_ListTransportModes()
return transport_modes.decode().split(':') return transport_modes.decode().split(":")
def get_transport_mode(self, disk_handle): def get_transport_mode(self, disk_handle):
t_mode = self._vix_disklib.VixDiskLib_GetTransportMode(disk_handle) t_mode = self._vix_disklib.VixDiskLib_GetTransportMode(disk_handle)
@@ -258,9 +291,17 @@ class VixDiskLibHandle(object):
@contextlib.contextmanager @contextlib.contextmanager
def connect( def connect(
self, server_name, thumbprint, username, password, self,
vmx_spec=None, snapshot_ref=None, read_only=True, server_name,
transport_modes=None, port=443): thumbprint,
username,
password,
vmx_spec=None,
snapshot_ref=None,
read_only=True,
transport_modes=None,
port=443,
):
LOG.debug("Connecting VixDiskLib: %s", server_name) LOG.debug("Connecting VixDiskLib: %s", server_name)
connectParams = VixDiskLibConnectParams() connectParams = VixDiskLibConnectParams()
@@ -283,9 +324,15 @@ class VixDiskLibHandle(object):
snapshot_ref = snapshot_ref.encode() snapshot_ref = snapshot_ref.encode()
conn = ctypes.c_void_p() conn = ctypes.c_void_p()
self._check_err(self._vix_disklib.VixDiskLib_ConnectEx( self._check_err(
connectParams, read_only, snapshot_ref, transport_modes, self._vix_disklib.VixDiskLib_ConnectEx(
ctypes.byref(conn))) connectParams,
read_only,
snapshot_ref,
transport_modes,
ctypes.byref(conn),
)
)
try: try:
yield conn yield conn
finally: finally:
@@ -296,21 +343,30 @@ class VixDiskLibHandle(object):
LOG.debug("Openning VixDiskLib disk: %s", disk_path) LOG.debug("Openning VixDiskLib disk: %s", disk_path)
disk_handle = ctypes.c_void_p() disk_handle = ctypes.c_void_p()
self._check_err(self._vix_disklib.VixDiskLib_Open( self._check_err(
conn, disk_path.encode(), flags, ctypes.byref(disk_handle))) self._vix_disklib.VixDiskLib_Open(
conn, disk_path.encode(), flags, ctypes.byref(disk_handle)
)
)
try: try:
yield disk_handle yield disk_handle
finally: finally:
self.close(disk_handle) self.close(disk_handle)
def read(self, disk_handle, start_sector, num_sectors, buf): def read(self, disk_handle, start_sector, num_sectors, buf):
self._check_err(self._vix_disklib.VixDiskLib_Read( self._check_err(
disk_handle, start_sector, num_sectors, buf)) self._vix_disklib.VixDiskLib_Read(
disk_handle, start_sector, num_sectors, buf
)
)
def write(self, disk_handle, start_sector, num_sectors, buf): def write(self, disk_handle, start_sector, num_sectors, buf):
"""Write ``num_sectors`` from ``buf`` starting at ``start_sector``.""" """Write ``num_sectors`` from ``buf`` starting at ``start_sector``."""
self._check_err(self._vix_disklib.VixDiskLib_Write( self._check_err(
disk_handle, start_sector, num_sectors, buf)) self._vix_disklib.VixDiskLib_Write(
disk_handle, start_sector, num_sectors, buf
)
)
def close(self, disk_handle): def close(self, disk_handle):
LOG.debug("Closing VixDiskLib disk handle: %s", disk_handle) LOG.debug("Closing VixDiskLib disk handle: %s", disk_handle)
+33 -19
View File
@@ -10,7 +10,7 @@ from typing import Any
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 LabEnv, SECTOR_SIZE, pattern_bytes from tests.integration.base import SECTOR_SIZE, LabEnv, pattern_bytes
_SIZES = ( _SIZES = (
("64KiB", 64 * 1024), ("64KiB", 64 * 1024),
@@ -19,8 +19,7 @@ _SIZES = (
) )
def _connect_extra( def _connect_extra(lab: LabEnv, module: Any, transport_mode: str) -> dict[str, Any]:
lab: LabEnv, module: Any, transport_mode: str) -> dict[str, Any]:
"""Return extra ``connect`` kwargs needed by ``module``.""" """Return extra ``connect`` kwargs needed by ``module``."""
extra: dict[str, Any] = {"transport_modes": transport_mode} extra: dict[str, Any] = {"transport_modes": transport_mode}
if module is open_vix: if module is open_vix:
@@ -33,27 +32,27 @@ def _time_write_read(
module: Any, module: Any,
payload: bytes, payload: bytes,
flags: int = 0, flags: int = 0,
transport_mode: str = "nbdssl") -> tuple[float, float]: transport_mode: str = "nbdssl",
) -> 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."""
n_sectors = len(payload) // SECTOR_SIZE n_sectors = len(payload) // SECTOR_SIZE
handle = module.VixDiskLibHandle( handle = module.VixDiskLibHandle(
vixdisklib_compatibility_version="8.0", vixdisklib_compatibility_version="8.0", config_path=None
config_path=None) )
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( kwargs = lab.vixdisklib_connect_kwargs(_connect_extra(lab, module, transport_mode))
_connect_extra(lab, module, transport_mode))
with handle.connect(**kwargs) as conn: with handle.connect(**kwargs) as conn:
with handle.open(conn, lab.disk_path, flags=flags) as disk: with handle.open(conn, lab.disk_path, flags=flags) 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)
started = time.perf_counter() started = time.perf_counter()
handle.read(disk, 0, n_sectors, read_buf) handle.read(disk, 0, n_sectors, read_buf)
read_s = time.perf_counter() - started read_s = time.perf_counter() - started
assert read_buf.raw[:len(payload)] == payload assert read_buf.raw[: len(payload)] == payload
return write_s, read_s return write_s, read_s
@@ -82,9 +81,14 @@ class TestCompare:
for mode_name, flags in open_modes: for mode_name, flags in open_modes:
for name, module in libraries: for name, module in libraries:
write_s, read_s = _time_write_read( write_s, read_s = _time_write_read(
lab, module, payload, flags=flags, lab,
transport_mode=transport_mode) module,
rows.append(( payload,
flags=flags,
transport_mode=transport_mode,
)
rows.append(
(
label, label,
transport_mode, transport_mode,
mode_name, mode_name,
@@ -93,16 +97,26 @@ class TestCompare:
read_s, read_s,
_mib_per_s(nbytes, write_s), _mib_per_s(nbytes, write_s),
_mib_per_s(nbytes, read_s), _mib_per_s(nbytes, read_s),
)) )
)
print() print()
print( print(
f"{'size':<14} {'transport':<10} {'flags':<8} {'library':<16} " f"{'size':<14} {'transport':<10} {'flags':<8} {'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, transport_mode, mode_name, name, label,
write_s, read_s, write_r, read_r) in rows: transport_mode,
mode_name,
name,
write_s,
read_s,
write_r,
read_r,
) in rows:
print( print(
f"{label:<14} {transport_mode:<10} {mode_name:<8} {name:<16} " f"{label:<14} {transport_mode:<10} {mode_name:<8} {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}"
)