Add nbdssl support
This commit is contained in:
@@ -10,7 +10,8 @@ from VDDK 8 NBD traffic; see `docs/`.
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Implemented against vCenter 8 / ESXi 8, transport `nbd`:
|
Implemented against vCenter 8 / ESXi 8. Default transport is `nbdssl`
|
||||||
|
(`nbd` is still available):
|
||||||
|
|
||||||
- `VixDiskLib_ConnectEx` (UID credentials)
|
- `VixDiskLib_ConnectEx` (UID credentials)
|
||||||
- `VixDiskLib_Open` (datastore path, read-only or read-write)
|
- `VixDiskLib_Open` (datastore path, read-only or read-write)
|
||||||
@@ -45,7 +46,7 @@ with handle.connect(
|
|||||||
username="[email protected]",
|
username="[email protected]",
|
||||||
password="secret",
|
password="secret",
|
||||||
vmx_spec="moref=vm-1234",
|
vmx_spec="moref=vm-1234",
|
||||||
transport_modes="nbd",
|
transport_modes="nbdssl",
|
||||||
read_only=False) as conn:
|
read_only=False) as conn:
|
||||||
with handle.open(conn, "[datastore] vm/vm.vmdk", flags=0) as disk:
|
with handle.open(conn, "[datastore] vm/vm.vmdk", flags=0) as disk:
|
||||||
handle.write(disk, 0, 1, buf)
|
handle.write(disk, 0, 1, buf)
|
||||||
|
|||||||
+38
-20
@@ -25,16 +25,17 @@ password to ESXi port 902. It:
|
|||||||
3. Connects to the ESXi **authd** daemon on TCP 902, upgrades to TLS,
|
3. Connects to the ESXi **authd** daemon on TCP 902, upgrades to TLS,
|
||||||
and presents that ticket.
|
and presents that ticket.
|
||||||
|
|
||||||
| VDDK call | What actually happens |
|
| VDDK call | What actually happens |
|
||||||
| --------------------------------- | ---------------------------------------------------------- |
|
| ------------------------------------ | ---------------------------------------------------------- |
|
||||||
| `VixDiskLib_InitEx` | Load plugins, SSL, logging |
|
| `VixDiskLib_InitEx` | Load plugins, SSL, logging |
|
||||||
| `VixDiskLib_ConnectEx` | SOAP `SessionManager.Login` to vCenter |
|
| `VixDiskLib_ConnectEx` | SOAP `SessionManager.Login` to vCenter |
|
||||||
| `VixDiskLib_Open` (read-only) | `NfcGetVmFiles` ticket, then authd handshake, then NFC I/O |
|
| `VixDiskLib_Open` (read-only) | `NfcGetVmFiles` ticket, then authd handshake, then NFC I/O |
|
||||||
| `VixDiskLib_Open` (read-write) | `NfcRandomAccessOpenDisk` ticket (disk key + host) |
|
| `VixDiskLib_Open` (read-write) | `NfcRandomAccessOpenDisk` ticket (disk key + host) |
|
||||||
| `transport_modes="nbd"` | NBD over NFC (`vpxa-nfc://...@esxi:902`) |
|
| `transport_modes="nbdssl"` (default) | NBDSSL (`vpxa-nfcssl://...@esxi:902`, second TLS wrap) |
|
||||||
| `vmxSpec=moref=vm-13098` | VM managed object used as the ticket target |
|
| `transport_modes="nbd"` | NBD over NFC (`vpxa-nfc://...@esxi:902`) |
|
||||||
| `snapshot_ref` | Not consumed by the ticket call itself |
|
| `vmxSpec=moref=vm-13098` | VM managed object used as the ticket target |
|
||||||
| `VIXDISKLIB_CRED_UID` | Username/password for VIM only |
|
| `snapshot_ref` | Not consumed by the ticket call itself |
|
||||||
|
| `VIXDISKLIB_CRED_UID` | Username/password for VIM only |
|
||||||
|
|
||||||
Lab topology used for capture:
|
Lab topology used for capture:
|
||||||
|
|
||||||
@@ -185,8 +186,9 @@ Plain-text connection is deprecated; use SSL to connect to NFC server
|
|||||||
```
|
```
|
||||||
|
|
||||||
`useSSL=0` does **not** mean skip TLS on 902. It means skip a second
|
`useSSL=0` does **not** mean skip TLS on 902. It means skip a second
|
||||||
NFCSSL wrap after authd TLS (`THUMBPRINT_SHA2 PlainText`). The
|
NFCSSL wrap after authd TLS. The management channel is still TLS.
|
||||||
management channel is still TLS.
|
`useSSL=1` (nbdssl) is the same authd commands with `PROXY vpxa-nfcssl`
|
||||||
|
and a second TLS handshake after `200 Connect`.
|
||||||
|
|
||||||
Intercepted writes/reads after the TLS handshake:
|
Intercepted writes/reads after the TLS handshake:
|
||||||
|
|
||||||
@@ -200,6 +202,20 @@ C -> PROXY vpxa-nfc\r\n
|
|||||||
S -> 200 Connect ha-nfc\r\n
|
S -> 200 Connect ha-nfc\r\n
|
||||||
```
|
```
|
||||||
|
|
||||||
|
NBDSSL uses the same `SESSION` / `BANNER` / `THUMBPRINT_SHA2 PlainText`
|
||||||
|
sequence. The ticket still has `service=vpxa-nfc`; the client rewrites
|
||||||
|
the PROXY argument:
|
||||||
|
|
||||||
|
```
|
||||||
|
C -> PROXY vpxa-nfcssl\r\n
|
||||||
|
S -> 200 Connect ha-nfcssl\r\n
|
||||||
|
```
|
||||||
|
|
||||||
|
After that reply, authd TLS is finished and `ha-nfcssl` expects a **new**
|
||||||
|
TLS ClientHello on the same TCP connection (`useSSL=1`). NFC frames then
|
||||||
|
travel as TLS application data of that second session. NBD (`useSSL=0`)
|
||||||
|
skips the second wrap and sends NFC as raw TCP instead.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- `SESSION` does not get a reply of its own. Waiting for a line after
|
- `SESSION` does not get a reply of its own. Waiting for a line after
|
||||||
@@ -209,10 +225,11 @@ Notes:
|
|||||||
commands, so `THUMBPRINT_SHA2 <colon-thumbprint>` is parsed as one
|
commands, so `THUMBPRINT_SHA2 <colon-thumbprint>` is parsed as one
|
||||||
token and returns `501 Invalid arguments`. `PlainText` has no extra
|
token and returns `501 Invalid arguments`. `PlainText` has no extra
|
||||||
spaces/colons and is the argument VDDK sends.
|
spaces/colons and is the argument VDDK sends.
|
||||||
- `PROXY` uses `ticket.service` (`vpxa-nfc` via vCenter). The success
|
- `PROXY` uses `ticket.service` (`vpxa-nfc` via vCenter) for NBD. NBDSSL
|
||||||
line names the host-side NFC endpoint (`ha-nfc`).
|
appends `ssl` (`vpxa-nfcssl`). The success line names the host-side
|
||||||
- After `200 Connect`, the socket speaks binary NFC (not documented
|
endpoint (`ha-nfc` or `ha-nfcssl`).
|
||||||
here).
|
- After `200 Connect`, NBD speaks binary NFC on the raw fd. NBDSSL
|
||||||
|
starts a second TLS handshake, then the same NFC protocol.
|
||||||
|
|
||||||
### Commands that are not used for this ticket type
|
### Commands that are not used for this ticket type
|
||||||
|
|
||||||
@@ -259,7 +276,8 @@ and asserts an established TLS socket on `ticket.host:ticket.port`.
|
|||||||
|
|
||||||
## What comes after authentication
|
## What comes after authentication
|
||||||
|
|
||||||
Authentication stops at `200 Connect ha-nfc`. Opening the VMDK and
|
Authentication stops at `200 Connect ha-nfc` (NBD) or `200 Connect
|
||||||
reading or writing sectors is documented in `docs/nfc_open.md` and
|
ha-nfcssl` (NBDSSL). Opening the VMDK and reading or writing sectors is
|
||||||
implemented in `openvixdisklib/nfc_open.py`. The datastore path is
|
documented in `docs/nfc_open.md` and implemented in
|
||||||
consumed there (and, for writes, as `diskDeviceKey` on the ticket).
|
`openvixdisklib/nfc_open.py`. The datastore path is consumed there
|
||||||
|
(and, for writes, as `diskDeviceKey` on the ticket).
|
||||||
|
|||||||
+42
-35
@@ -11,53 +11,60 @@ VDDK 8.0.2 verbose logs
|
|||||||
Authentication is already done: VIM login, NFC ticket (`NfcGetVmFiles`
|
Authentication is already done: VIM login, NFC ticket (`NfcGetVmFiles`
|
||||||
for read-only, `NfcRandomAccessOpenDisk` for write), TLS to authd,
|
for read-only, `NfcRandomAccessOpenDisk` for write), TLS to authd,
|
||||||
`SESSION` / `BANNER` / `THUMBPRINT_SHA2 PlainText` / `PROXY`. This
|
`SESSION` / `BANNER` / `THUMBPRINT_SHA2 PlainText` / `PROXY`. This
|
||||||
stage starts at `200 Connect ha-nfc` and ends with an open file handle
|
stage starts at `200 Connect ha-nfc` (NBD) or `200 Connect ha-nfcssl`
|
||||||
that can read and write sectors. Flags `0x1a` require the writable
|
(NBDSSL) and ends with an open file handle that can read and write
|
||||||
ticket; the same flags on a `GetVmFiles` ticket fail with
|
sectors. Flags `0x1a` require the writable ticket; the same flags on a
|
||||||
`VIX_E_FILE_READ_ONLY`.
|
`GetVmFiles` ticket fail with `VIX_E_FILE_READ_ONLY`.
|
||||||
|
|
||||||
## Mapping from VDDK
|
## Mapping from VDDK
|
||||||
|
|
||||||
| VDDK call / log | Wire effect |
|
| VDDK call / log | Wire effect |
|
||||||
| ---------------------------------------------------- | -------------------------------------------------------- |
|
| ------------------------------------------------------------- | ---------------------------------------------------------- |
|
||||||
| `VixDiskLib_Open` | Ticket + authd (see `nfc_auth.md`), then this protocol |
|
| `VixDiskLib_Open` | Ticket + authd (see `nfc_auth.md`), then this protocol |
|
||||||
| `NBD_ClientOpen` `vpxa-nfc://[ds] path.vmdk@esxi:902` | Datastore path is the NFC open argument, not the ticket |
|
| `NBD_ClientOpen` `vpxa-nfc://[ds] path.vmdk@esxi:902` | Datastore path is the NFC open argument, not the ticket |
|
||||||
| `useSSL=0` | NFC bytes are raw TCP, not `SSL_write` |
|
| `NBD_ClientOpen` `vpxa-nfcssl://…` / `useSSL=1` | Same NFC after a second TLS handshake on the authd fd |
|
||||||
| `NfcProcessSessionParams` flags `0x3` | Classic 264-byte session messages |
|
| `useSSL=0` | NFC bytes are raw TCP, not `SSL_write` |
|
||||||
| `SendConnectionDataMsg` payloadInfo 4 and 7 | Client name `vddk` (4) and opId `nbdmode` (7) |
|
| `NfcProcessSessionParams` flags `0x3` | Classic 264-byte session messages |
|
||||||
| Server version 11 | Classic version message; 11 on this ESXi 8 lab |
|
| `SendConnectionDataMsg` payloadInfo 4 and 7 | Client name `vddk` (4) and opId `nbdmode` (7) |
|
||||||
| `NfcAio_OpenSession` | AIO framing after the classic handshake |
|
| Server version 11 | Classic version message; 11 on this ESXi 8 lab |
|
||||||
| `NfcUtil_PrintFileInfoOpenFlag` `NFC_DISK` `0x1e` | `NFC_AIO_MSG_OPEN_FILE` (read-only) |
|
| `NfcAio_OpenSession` | AIO framing after the classic handshake |
|
||||||
| Open without `VIXDISKLIB_FLAG_OPEN_READ_ONLY` | `OPEN_FILE` flags `0x1a` (read-write) |
|
| `NfcUtil_PrintFileInfoOpenFlag` `NFC_DISK` `0x1e` | `NFC_AIO_MSG_OPEN_FILE` (read-only) |
|
||||||
| `VixDiskLib_Read` / `VixDiskLib_Write` | `NFC_AIO_MSG_IO` + sector bytes |
|
| Open without `VIXDISKLIB_FLAG_OPEN_READ_ONLY` | `OPEN_FILE` flags `0x1a` (read-write) |
|
||||||
|
| `VixDiskLib_Read` / `VixDiskLib_Write` | `NFC_AIO_MSG_IO` + sector bytes |
|
||||||
|
|
||||||
`snapshot_ref` is still not on the wire. Integration tests pass the
|
`snapshot_ref` is still not on the wire. Integration tests pass the
|
||||||
flat VMDK created with the temporary lab VM.
|
flat VMDK created with the temporary lab VM.
|
||||||
|
|
||||||
## After PROXY: plaintext on the TLS fd
|
## After PROXY: NBD plaintext vs NBDSSL wrap
|
||||||
|
|
||||||
`THUMBPRINT_SHA2 PlainText` tells authd not to wrap NFC in a second
|
`THUMBPRINT_SHA2 PlainText` is used for both transports. The PROXY
|
||||||
TLS session. VDDK logs `useSSL=0` and “Plain-text connection is
|
service name selects whether NFC gets a second TLS session.
|
||||||
deprecated”.
|
|
||||||
|
|
||||||
On the wire that means:
|
NBD (`PROXY vpxa-nfc` → `200 Connect ha-nfc`, VDDK `useSSL=0`):
|
||||||
|
|
||||||
1. Authd commands stay inside the original TLS session (`SSL_write` /
|
1. Authd commands stay inside the original TLS session (`SSL_write` /
|
||||||
`SSL_read`).
|
`SSL_read`).
|
||||||
2. After `200 Connect ha-nfc`, VDDK calls `write(SSL_get_fd(ssl), …)`
|
2. After `200 Connect ha-nfc`, NFC is `write(SSL_get_fd(ssl), …)` /
|
||||||
and `read` on that same descriptor. Those buffers are NFC, not TLS
|
`read` on that descriptor. Those buffers are not TLS records
|
||||||
records (`0x17 0x03 …`).
|
(`0x17 0x03 …`).
|
||||||
3. ESXi’s `ha-nfc` side does the same: replies are plaintext NFC.
|
3. An SSL hook that only interposes `SSL_write` / `SSL_read` goes
|
||||||
|
silent after PROXY; a `write` / `read` hook on port 902 shows the
|
||||||
|
frames.
|
||||||
|
4. Python must not use `SSLSocket.send` here: that would encrypt bytes
|
||||||
|
the server now reads as NFC. `nfc_open.takeover_authd_socket` dups
|
||||||
|
the fd. `unwrap()` / `SSL_shutdown` is not used.
|
||||||
|
|
||||||
An SSL hook that only interposes `SSL_write` / `SSL_read` therefore
|
NBDSSL (`PROXY vpxa-nfcssl` → `200 Connect ha-nfcssl`, `useSSL=1`):
|
||||||
goes silent after PROXY. Interposing `write` / `read` and filtering
|
|
||||||
`getpeername` port 902 shows the frames.
|
|
||||||
|
|
||||||
Python must not use `SSLSocket.send` for this stage: that would
|
1. Authd commands are the same, including `THUMBPRINT_SHA2 PlainText`.
|
||||||
`SSL_write` and encrypt bytes the server now reads as NFC.
|
2. After `200 Connect ha-nfcssl`, both sides abandon the authd TLS
|
||||||
`nfc_open.takeover_authd_socket` dups `SSL_get_fd` and uses a raw
|
session. `ha-nfcssl` expects a new ClientHello on the same TCP
|
||||||
`socket.socket`. `unwrap()` / `SSL_shutdown` is not used; VDDK does
|
connection.
|
||||||
not send `close_notify` before NFC.
|
3. `nfc_open.wrap_nfcssl_socket` dups the fd and
|
||||||
|
`SSLContext.wrap_socket`s it. NFC then uses `SSLSocket.sendall` /
|
||||||
|
`recv` (TLS application data). The classic 264-byte handshake still
|
||||||
|
sends the ASCII body `PlainText`; that is NFC's own encoding, not
|
||||||
|
the authd transport.
|
||||||
|
|
||||||
## Classic 264-byte messages
|
## Classic 264-byte messages
|
||||||
|
|
||||||
@@ -200,6 +207,7 @@ classic type 4 `NFC_SESSION_COMPLETE`.
|
|||||||
| ----------------------------- | ----------------------------------------------- |
|
| ----------------------------- | ----------------------------------------------- |
|
||||||
| VIM + authd | `openvixdisklib.nfc_auth.authenticate` |
|
| VIM + authd | `openvixdisklib.nfc_auth.authenticate` |
|
||||||
| Dup fd, skip TLS for NFC | `openvixdisklib.nfc_open.takeover_authd_socket` |
|
| Dup fd, skip TLS for NFC | `openvixdisklib.nfc_open.takeover_authd_socket` |
|
||||||
|
| Second TLS for nbdssl | `openvixdisklib.nfc_open.wrap_nfcssl_socket` |
|
||||||
| Handshake + AIO + OPEN_FILE | `openvixdisklib.nfc_open.open_disk` |
|
| Handshake + AIO + OPEN_FILE | `openvixdisklib.nfc_open.open_disk` |
|
||||||
| Sector read / write / close | `openvixdisklib.nfc_open.NfcDisk` |
|
| Sector read / write / close | `openvixdisklib.nfc_open.NfcDisk` |
|
||||||
|
|
||||||
@@ -218,8 +226,7 @@ I/O: `docs/nfc_read.md`, `docs/nfc_write.md`, and
|
|||||||
|
|
||||||
- `DDB_GET` / geometry / compression / encryption keys
|
- `DDB_GET` / geometry / compression / encryption keys
|
||||||
- `NFC_DELTA_DISK`, change-block tracking
|
- `NFC_DELTA_DISK`, change-block tracking
|
||||||
- Host-switch (`NFC_AIO_SWITCH_HOST_*`) and a second NFCSSL wrap
|
- Host-switch (`NFC_AIO_SWITCH_HOST_*`)
|
||||||
(`useSSL=1`, not what VDDK NBD used here)
|
|
||||||
- Direct ESXi `ha-nfc` without vCenter `vpxa-nfc`
|
- Direct ESXi `ha-nfc` without vCenter `vpxa-nfc`
|
||||||
|
|
||||||
Reads after open are in `docs/nfc_read.md`. Writes are in
|
Reads after open are in `docs/nfc_read.md`. Writes are in
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ NFC work can follow the same loop instead of rediscovering it.
|
|||||||
|
|
||||||
Scope so far: `VixDiskLib_ConnectEx` + `VixDiskLib_Open` +
|
Scope so far: `VixDiskLib_ConnectEx` + `VixDiskLib_Open` +
|
||||||
`VixDiskLib_Read` + `VixDiskLib_Write` against lab vCenter 8.0.1 /
|
`VixDiskLib_Read` + `VixDiskLib_Write` against lab vCenter 8.0.1 /
|
||||||
ESXi 8, transport `nbd`. Driver: `tests/integration/` (the session-scoped
|
ESXi 8, transports `nbd` and `nbdssl`. Driver: `tests/integration/` (the
|
||||||
`lab` fixture creates a temporary empty VM with a 10 GiB disk and
|
session-scoped `lab` fixture creates a temporary empty VM with a 10 GiB
|
||||||
destroys it when the pytest session ends).
|
disk and destroys it when the pytest session ends).
|
||||||
|
|
||||||
Rule from `AGENTS.md`: reuse pyVmomi for every public VIM operation.
|
Rule from `AGENTS.md`: reuse pyVmomi for every public VIM operation.
|
||||||
Only reimplement what pyVmomi does not expose.
|
Only reimplement what pyVmomi does not expose.
|
||||||
@@ -254,6 +254,32 @@ single oversized write the way VDDK sends an oversized read. Details:
|
|||||||
`tests/integration/test_nfc_read_write.py` and the VDDK cross-check in
|
`tests/integration/test_nfc_read_write.py` and the VDDK cross-check in
|
||||||
`tests/integration/test_crosscheck.py`.
|
`tests/integration/test_crosscheck.py`.
|
||||||
|
|
||||||
|
## Step 11 — NBDSSL: second TLS after `PROXY vpxa-nfcssl`
|
||||||
|
|
||||||
|
VDDK strings name `nbdssl`, `vpxa-nfcssl://`, and `ha-nfcssl`. The NFC
|
||||||
|
ticket SOAP call is unchanged (`service` stays `vpxa-nfc`). Transport is
|
||||||
|
an authd/client choice:
|
||||||
|
|
||||||
|
1. Same `SESSION` / `BANNER` / `THUMBPRINT_SHA2 PlainText` as NBD.
|
||||||
|
2. `PROXY vpxa-nfcssl` → `200 Connect ha-nfcssl`.
|
||||||
|
3. A new TLS handshake on the **same TCP connection** (not TLS-in-TLS
|
||||||
|
and not `THUMBPRINT_SHA2 <sha256>`). The colon thumbprint is still
|
||||||
|
`501 Invalid arguments`.
|
||||||
|
4. Classic NFC handshake type 43 still sends ASCII `PlainText`. I/O
|
||||||
|
framing is unchanged.
|
||||||
|
|
||||||
|
Replay: `connect_authd(..., nfc_ssl=True)` plus
|
||||||
|
`nfc_open.wrap_nfcssl_socket`. Sending NFC on the first authd
|
||||||
|
`SSLSocket` after `ha-nfcssl` fails (`BAD_RECORD_TYPE`); sending
|
||||||
|
plaintext NFC on the dup'd fd gets EOF. Dup + `wrap_socket` is the
|
||||||
|
working subset. Proof: `tests/integration/test_nfc_open.py` (`nbdssl`)
|
||||||
|
and `test_openvixdisklib.py` with `transport_modes="nbdssl"`.
|
||||||
|
|
||||||
|
Native `VixDiskLib_ConnectEx(..., transport_modes="nbdssl")` through
|
||||||
|
the old `VixDiskLibConnectParams` ctypes struct can still log nbdssl
|
||||||
|
and then fall back to `vpxa-nfc` / `useSSL=0`. Do not treat that log
|
||||||
|
line as a wire capture of NFCSSL.
|
||||||
|
|
||||||
## What to write down
|
## What to write down
|
||||||
|
|
||||||
After a stage works:
|
After a stage works:
|
||||||
@@ -278,5 +304,4 @@ Not yet reversed, same loop as above:
|
|||||||
- `NFC_DELTA_DISK`, CBT / `QueryAllocatedBlocks`
|
- `NFC_DELTA_DISK`, CBT / `QueryAllocatedBlocks`
|
||||||
- `VixDiskLib_GetInfo` capacity
|
- `VixDiskLib_GetInfo` capacity
|
||||||
- Host-switch AIO messages
|
- Host-switch AIO messages
|
||||||
- `useSSL=1` (second NFCSSL wrap)
|
|
||||||
- Direct ESXi `ha-nfc` without vCenter `vpxa-nfc`
|
- Direct ESXi `ha-nfc` without vCenter `vpxa-nfc`
|
||||||
|
|||||||
@@ -242,10 +242,25 @@ def _expect_code(line: str, code: str, what: str) -> str:
|
|||||||
return line[len(code):].lstrip()
|
return line[len(code):].lstrip()
|
||||||
|
|
||||||
|
|
||||||
|
def nfcssl_service_name(service: str) -> str:
|
||||||
|
"""Return the NFCSSL authd PROXY service for an NFC service name.
|
||||||
|
|
||||||
|
VCenter tickets still report ``vpxa-nfc``. NBDSSL uses
|
||||||
|
``PROXY vpxa-nfcssl`` (or ``ha-nfcssl`` on a direct ESXi ticket).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
service: Ticket ``service`` field, for example ``vpxa-nfc``.
|
||||||
|
"""
|
||||||
|
if service.endswith("ssl"):
|
||||||
|
return service
|
||||||
|
return f"{service}ssl"
|
||||||
|
|
||||||
|
|
||||||
def connect_authd(
|
def connect_authd(
|
||||||
ticket: vim.HostServiceTicket,
|
ticket: vim.HostServiceTicket,
|
||||||
allow_untrusted: bool = False,
|
allow_untrusted: bool = False,
|
||||||
timeout: float = 30.0) -> ssl.SSLSocket:
|
timeout: float = 30.0,
|
||||||
|
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:
|
||||||
@@ -253,14 +268,20 @@ def connect_authd(
|
|||||||
1. Read the plaintext 220 banner, then wrap the socket with TLS.
|
1. Read the plaintext 220 banner, then wrap the socket with TLS.
|
||||||
2. SESSION <sessionId>
|
2. SESSION <sessionId>
|
||||||
3. BANNER
|
3. BANNER
|
||||||
4. THUMBPRINT_SHA2 PlainText (NFC data stays on this TLS socket)
|
4. THUMBPRINT_SHA2 PlainText
|
||||||
5. PROXY <ticket.service> (vpxa-nfc when connecting via vCenter)
|
5. PROXY <ticket.service> (vpxa-nfc / nbd) or vpxa-nfcssl (nbdssl)
|
||||||
|
|
||||||
|
``THUMBPRINT_SHA2 PlainText`` is used for both transports. NBDSSL
|
||||||
|
is selected by the PROXY service name; after ``200 Connect
|
||||||
|
ha-nfcssl`` a second TLS handshake is started in ``nfc_open``.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ticket: One-time ticket from get_nfc_ticket().
|
ticket: One-time ticket from get_nfc_ticket().
|
||||||
allow_untrusted: If False, require the peer SHA-1 thumbprint to match
|
allow_untrusted: If False, require the peer SHA-1 thumbprint to match
|
||||||
ticket.sslThumbprint.
|
ticket.sslThumbprint.
|
||||||
timeout: Socket timeout in seconds.
|
timeout: Socket timeout in seconds.
|
||||||
|
nfc_ssl: When True (the default), PROXY to the NFCSSL service
|
||||||
|
used by nbdssl. Pass False for plaintext NFC (nbd).
|
||||||
"""
|
"""
|
||||||
host = ticket.host
|
host = ticket.host
|
||||||
port = ticket.port or AUTHD_DEFAULT_PORT
|
port = ticket.port or AUTHD_DEFAULT_PORT
|
||||||
@@ -294,6 +315,8 @@ def connect_authd(
|
|||||||
_expect_code(_readline(ssock), "200", "THUMBPRINT_SHA2")
|
_expect_code(_readline(ssock), "200", "THUMBPRINT_SHA2")
|
||||||
|
|
||||||
service = ticket.service or "vpxa-nfc"
|
service = ticket.service or "vpxa-nfc"
|
||||||
|
if nfc_ssl:
|
||||||
|
service = nfcssl_service_name(service)
|
||||||
ssock.sendall(f"PROXY {service}\r\n".encode("ascii"))
|
ssock.sendall(f"PROXY {service}\r\n".encode("ascii"))
|
||||||
_expect_code(_readline(ssock), "200", "PROXY")
|
_expect_code(_readline(ssock), "200", "PROXY")
|
||||||
return ssock
|
return ssock
|
||||||
@@ -309,10 +332,12 @@ class NfcAuthSession:
|
|||||||
self,
|
self,
|
||||||
si: vim.ServiceInstance,
|
si: vim.ServiceInstance,
|
||||||
ticket: vim.HostServiceTicket,
|
ticket: vim.HostServiceTicket,
|
||||||
authd_sock: ssl.SSLSocket) -> None:
|
authd_sock: ssl.SSLSocket,
|
||||||
|
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
|
||||||
|
self.nfc_ssl = nfc_ssl
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
"""Close the authd socket and logout of the VIM session."""
|
"""Close the authd socket and logout of the VIM session."""
|
||||||
@@ -338,7 +363,8 @@ def authenticate(
|
|||||||
allow_untrusted: bool = False,
|
allow_untrusted: bool = False,
|
||||||
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) -> NfcAuthSession:
|
read_only: bool = True,
|
||||||
|
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:
|
||||||
@@ -354,6 +380,8 @@ def authenticate(
|
|||||||
disk_path: Datastore path used to resolve ``disk_device_key``.
|
disk_path: Datastore path used to resolve ``disk_device_key``.
|
||||||
read_only: When False, request a writable ``NfcRandomAccessOpenDisk``
|
read_only: When False, request a writable ``NfcRandomAccessOpenDisk``
|
||||||
ticket.
|
ticket.
|
||||||
|
nfc_ssl: When True (the default), complete authd with the NFCSSL
|
||||||
|
PROXY service used by nbdssl. Pass False for nbd.
|
||||||
"""
|
"""
|
||||||
si = connect_vim(
|
si = connect_vim(
|
||||||
host, username, password, port=port,
|
host, username, password, port=port,
|
||||||
@@ -364,8 +392,8 @@ def authenticate(
|
|||||||
si, vm, disk_device_key=disk_device_key,
|
si, vm, disk_device_key=disk_device_key,
|
||||||
disk_path=disk_path, read_only=read_only)
|
disk_path=disk_path, read_only=read_only)
|
||||||
authd_sock = connect_authd(
|
authd_sock = connect_authd(
|
||||||
ticket, allow_untrusted=allow_untrusted)
|
ticket, allow_untrusted=allow_untrusted, nfc_ssl=nfc_ssl)
|
||||||
except Exception:
|
except Exception:
|
||||||
Disconnect(si)
|
Disconnect(si)
|
||||||
raise
|
raise
|
||||||
return NfcAuthSession(si, ticket, authd_sock)
|
return NfcAuthSession(si, ticket, authd_sock, nfc_ssl=nfc_ssl)
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
|
|
||||||
"""VDDK-compatible NFC disk open, sector read, and sector write.
|
"""VDDK-compatible NFC disk open, sector read, and sector write.
|
||||||
|
|
||||||
After ``nfc_auth.connect_authd`` returns ``200 Connect``, VDDK stops using
|
After ``nfc_auth.connect_authd`` returns ``200 Connect``, NBD
|
||||||
``SSL_write`` on the authd socket. ``THUMBPRINT_SHA2 PlainText`` means the
|
(``useSSL=0``) stops using ``SSL_write`` and speaks NFC as raw TCP on
|
||||||
NFC binary protocol runs as raw TCP on that same file descriptor
|
that file descriptor. NBDSSL (``useSSL=1``) starts a second TLS
|
||||||
(``useSSL=0``). This module dups that fd and speaks:
|
handshake on the same TCP connection (``200 Connect ha-nfcssl``) and
|
||||||
|
speaks the same NFC frames as TLS application data. This module dups
|
||||||
|
the authd fd and speaks:
|
||||||
|
|
||||||
1. Classic 264-byte NFC messages (handshake, version, connection data,
|
1. Classic 264-byte NFC messages (handshake, version, connection data,
|
||||||
AIO session open).
|
AIO session open).
|
||||||
@@ -24,7 +26,7 @@ import socket
|
|||||||
import ssl
|
import ssl
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
from openvixdisklib.nfc_auth import NfcAuthSession
|
from openvixdisklib.nfc_auth import NfcAuthSession, _ssl_client_context
|
||||||
|
|
||||||
NFC_MSG_SIZE = 264
|
NFC_MSG_SIZE = 264
|
||||||
NFC_AIO_MAGIC = 0xA100DA7A
|
NFC_AIO_MAGIC = 0xA100DA7A
|
||||||
@@ -93,6 +95,30 @@ def takeover_authd_socket(ssock: ssl.SSLSocket) -> socket.socket:
|
|||||||
return raw
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_nfcssl_socket(
|
||||||
|
ssock: ssl.SSLSocket,
|
||||||
|
server_hostname: str) -> ssl.SSLSocket:
|
||||||
|
"""Start the second TLS session used by NBDSSL after PROXY.
|
||||||
|
|
||||||
|
After ``200 Connect ha-nfcssl``, authd TLS is finished and
|
||||||
|
``ha-nfcssl`` expects a new ClientHello on the same TCP connection.
|
||||||
|
The fd is dup'd so the original authd ``SSLSocket`` can be closed
|
||||||
|
later without ``SSL_shutdown`` of this NFCSSL session.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ssock: The TLS socket from ``nfc_auth.connect_authd``.
|
||||||
|
server_hostname: Host name passed to ``SSLContext.wrap_socket``.
|
||||||
|
"""
|
||||||
|
raw = takeover_authd_socket(ssock)
|
||||||
|
ssl_context = _ssl_client_context(verify=False)
|
||||||
|
try:
|
||||||
|
return ssl_context.wrap_socket(
|
||||||
|
raw, server_hostname=server_hostname)
|
||||||
|
except Exception:
|
||||||
|
raw.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def _recvn(sock: socket.socket, size: int) -> bytes:
|
def _recvn(sock: socket.socket, size: int) -> bytes:
|
||||||
buf = bytearray()
|
buf = bytearray()
|
||||||
while len(buf) < size:
|
while len(buf) < size:
|
||||||
@@ -146,7 +172,7 @@ class NfcDisk:
|
|||||||
"""Wrap an AIO session that already has ``path`` open.
|
"""Wrap an AIO session that already has ``path`` open.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
sock: Raw NFC socket after handshake.
|
sock: NFC socket after handshake (raw TCP for nbd, TLS for nbdssl).
|
||||||
path: Datastore path that was opened.
|
path: Datastore path that was opened.
|
||||||
handle: Server file handle from OPEN_FILE.
|
handle: Server file handle from OPEN_FILE.
|
||||||
sector_size: Sector size from the OPEN_FILE reply.
|
sector_size: Sector size from the OPEN_FILE reply.
|
||||||
@@ -401,7 +427,9 @@ def open_disk(
|
|||||||
|
|
||||||
Matches VDDK ``VixDiskLib_Open`` of a datastore path after the NFC
|
Matches VDDK ``VixDiskLib_Open`` of a datastore path after the NFC
|
||||||
ticket and authd PROXY handshake: session init, AIO open, then
|
ticket and authd PROXY handshake: session init, AIO open, then
|
||||||
``NFC_AIO_MSG_OPEN_FILE`` with type ``NFC_DISK``.
|
``NFC_AIO_MSG_OPEN_FILE`` with type ``NFC_DISK``. NBD dups the
|
||||||
|
authd fd and sends plaintext NFC. NBDSSL wraps that dup in a
|
||||||
|
second TLS session (``session.nfc_ssl``).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session: Result of ``nfc_auth.authenticate``.
|
session: Result of ``nfc_auth.authenticate``.
|
||||||
@@ -412,7 +440,11 @@ def open_disk(
|
|||||||
version: Client NFC protocol version (lab ESXi answered 11).
|
version: Client NFC protocol version (lab ESXi answered 11).
|
||||||
read_only: When True, open with VDDK's read-only NFC flags.
|
read_only: When True, open with VDDK's read-only NFC flags.
|
||||||
"""
|
"""
|
||||||
sock = takeover_authd_socket(session.authd_sock)
|
if session.nfc_ssl:
|
||||||
|
sock = wrap_nfcssl_socket(
|
||||||
|
session.authd_sock, session.ticket.host)
|
||||||
|
else:
|
||||||
|
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(sock, disk_path, handle=0, sector_size=NFC_SECTOR_SIZE)
|
disk = NfcDisk(sock, disk_path, handle=0, sector_size=NFC_SECTOR_SIZE)
|
||||||
|
|||||||
@@ -70,13 +70,20 @@ def _parse_vm_moref(vmx_spec: Optional[str]) -> str:
|
|||||||
return vmx_spec
|
return vmx_spec
|
||||||
|
|
||||||
|
|
||||||
def _require_nbd(transport_modes: Optional[str]) -> None:
|
def _select_transport(transport_modes: Optional[str]) -> str:
|
||||||
|
"""Return the first requested transport this replacement implements.
|
||||||
|
|
||||||
|
``None`` defaults to ``nbdssl``. A colon-separated list (VDDK
|
||||||
|
style, for example ``file:nbdssl:nbd``) picks the first of
|
||||||
|
``nbdssl`` or ``nbd``.
|
||||||
|
"""
|
||||||
if transport_modes is None:
|
if transport_modes is None:
|
||||||
return
|
return "nbdssl"
|
||||||
modes = [m for m in transport_modes.split(":") if m]
|
for mode in transport_modes.split(":"):
|
||||||
if "nbd" not in modes:
|
if mode in ("nbdssl", "nbd"):
|
||||||
raise NotImplementedError(
|
return mode
|
||||||
f"only nbd transport is supported, got {transport_modes!r}")
|
raise NotImplementedError(
|
||||||
|
f"supported transports are nbdssl and nbd, got {transport_modes!r}")
|
||||||
|
|
||||||
|
|
||||||
class _Connection:
|
class _Connection:
|
||||||
@@ -89,13 +96,15 @@ class _Connection:
|
|||||||
snapshot_ref: Optional[str],
|
snapshot_ref: Optional[str],
|
||||||
thumbprint: Optional[str],
|
thumbprint: Optional[str],
|
||||||
allow_untrusted: bool,
|
allow_untrusted: bool,
|
||||||
read_only: bool) -> None:
|
read_only: bool,
|
||||||
|
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
|
||||||
self.thumbprint = thumbprint
|
self.thumbprint = thumbprint
|
||||||
self.allow_untrusted = allow_untrusted
|
self.allow_untrusted = allow_untrusted
|
||||||
self.read_only = read_only
|
self.read_only = read_only
|
||||||
|
self.transport_mode = transport_mode
|
||||||
|
|
||||||
|
|
||||||
class _DiskHandle:
|
class _DiskHandle:
|
||||||
@@ -104,9 +113,11 @@ class _DiskHandle:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
disk: nfc_open.NfcDisk,
|
disk: nfc_open.NfcDisk,
|
||||||
authd_sock) -> None:
|
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
|
||||||
|
|
||||||
|
|
||||||
class VixDiskLibHandle:
|
class VixDiskLibHandle:
|
||||||
@@ -161,12 +172,11 @@ class VixDiskLibHandle:
|
|||||||
|
|
||||||
def get_transport_modes(self) -> list[str]:
|
def get_transport_modes(self) -> list[str]:
|
||||||
"""Return the transport modes this replacement implements."""
|
"""Return the transport modes this replacement implements."""
|
||||||
return ["nbd"]
|
return ["nbdssl", "nbd"]
|
||||||
|
|
||||||
def get_transport_mode(self, disk_handle: _DiskHandle) -> str:
|
def get_transport_mode(self, disk_handle: _DiskHandle) -> str:
|
||||||
"""Return the transport used for ``disk_handle``."""
|
"""Return the transport used for ``disk_handle``."""
|
||||||
del disk_handle
|
return disk_handle.transport_mode
|
||||||
return "nbd"
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def connect(
|
def connect(
|
||||||
@@ -196,12 +206,14 @@ class VixDiskLibHandle:
|
|||||||
vmx_spec: VM selector, ``moref=vm-…``.
|
vmx_spec: VM selector, ``moref=vm-…``.
|
||||||
snapshot_ref: Snapshot moref; unused on the NFC ticket.
|
snapshot_ref: Snapshot moref; unused on the NFC ticket.
|
||||||
read_only: When False, the disk may be opened for write.
|
read_only: When False, the disk may be opened for write.
|
||||||
transport_modes: ``nbd`` or a colon list that includes ``nbd``.
|
transport_modes: ``nbdssl``, ``nbd``, or a colon list. The
|
||||||
|
first supported mode is used; ``None`` defaults to
|
||||||
|
``nbdssl``.
|
||||||
port: HTTPS port, usually 443.
|
port: HTTPS port, usually 443.
|
||||||
allow_untrusted: Skip management TLS verification when True.
|
allow_untrusted: Skip management TLS verification when True.
|
||||||
"""
|
"""
|
||||||
LOG.debug("Connecting VixDiskLib: %s", server_name)
|
LOG.debug("Connecting VixDiskLib: %s", server_name)
|
||||||
_require_nbd(transport_modes)
|
transport_mode = _select_transport(transport_modes)
|
||||||
vm_moref = _parse_vm_moref(vmx_spec)
|
vm_moref = _parse_vm_moref(vmx_spec)
|
||||||
si = nfc_auth.connect_vim(
|
si = nfc_auth.connect_vim(
|
||||||
server_name,
|
server_name,
|
||||||
@@ -212,7 +224,8 @@ class VixDiskLibHandle:
|
|||||||
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, vm_moref, snapshot_ref, thumbprint,
|
||||||
allow_untrusted or not thumbprint, read_only)
|
allow_untrusted or not thumbprint, read_only,
|
||||||
|
transport_mode)
|
||||||
try:
|
try:
|
||||||
yield conn
|
yield conn
|
||||||
finally:
|
finally:
|
||||||
@@ -243,18 +256,20 @@ class VixDiskLibHandle:
|
|||||||
"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"
|
||||||
ticket = nfc_auth.get_nfc_ticket(
|
ticket = nfc_auth.get_nfc_ticket(
|
||||||
conn.si, vm, read_only=read_only, disk_path=disk_path)
|
conn.si, vm, read_only=read_only, disk_path=disk_path)
|
||||||
authd_sock = nfc_auth.connect_authd(
|
authd_sock = nfc_auth.connect_authd(
|
||||||
ticket, allow_untrusted=conn.allow_untrusted)
|
ticket, allow_untrusted=conn.allow_untrusted, nfc_ssl=nfc_ssl)
|
||||||
session = nfc_auth.NfcAuthSession(conn.si, ticket, authd_sock)
|
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)
|
||||||
except Exception:
|
except Exception:
|
||||||
authd_sock.close()
|
authd_sock.close()
|
||||||
raise
|
raise
|
||||||
handle = _DiskHandle(disk, authd_sock)
|
handle = _DiskHandle(disk, authd_sock, conn.transport_mode)
|
||||||
try:
|
try:
|
||||||
yield handle
|
yield handle
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Copyright 2026 Cloudbase Solutions Srl
|
||||||
|
# All Rights Reserved.
|
||||||
|
|
||||||
|
"""Session-scoped lab fixtures for live vSphere tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests.integration.base import (
|
||||||
|
LabEnv,
|
||||||
|
create_lab_vm,
|
||||||
|
destroy_lab_vm,
|
||||||
|
ensure_vddk_library_path,
|
||||||
|
require_vddk,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def lab() -> Iterator[LabEnv]:
|
||||||
|
"""Create one temporary lab VM for the whole pytest session."""
|
||||||
|
os.environ.pop("LD_PRELOAD", None)
|
||||||
|
ensure_vddk_library_path()
|
||||||
|
env = create_lab_vm()
|
||||||
|
try:
|
||||||
|
yield env
|
||||||
|
finally:
|
||||||
|
destroy_lab_vm(env)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def vddk() -> None:
|
||||||
|
"""Skip VDDK-backed tests when ``libvixDiskLib`` cannot be loaded."""
|
||||||
|
require_vddk()
|
||||||
@@ -60,7 +60,10 @@ class LabEnv:
|
|||||||
vmx_spec: str
|
vmx_spec: str
|
||||||
disk_path: str
|
disk_path: str
|
||||||
|
|
||||||
def authenticate(self, read_only: bool = True) -> NfcAuthSession:
|
def authenticate(
|
||||||
|
self,
|
||||||
|
read_only: bool = True,
|
||||||
|
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,
|
||||||
@@ -70,7 +73,8 @@ class LabEnv:
|
|||||||
thumbprint=self.thumbprint,
|
thumbprint=self.thumbprint,
|
||||||
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)
|
||||||
|
|
||||||
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]:
|
||||||
@@ -82,7 +86,7 @@ class LabEnv:
|
|||||||
"username": self.username,
|
"username": self.username,
|
||||||
"password": self.password,
|
"password": self.password,
|
||||||
"vmx_spec": self.vmx_spec,
|
"vmx_spec": self.vmx_spec,
|
||||||
"transport_modes": "nbd",
|
"transport_modes": "nbdssl",
|
||||||
"read_only": False,
|
"read_only": False,
|
||||||
}
|
}
|
||||||
if extra:
|
if extra:
|
||||||
|
|||||||
@@ -14,5 +14,14 @@ class TestNfcAuth:
|
|||||||
assert ticket.host
|
assert ticket.host
|
||||||
assert ticket.port
|
assert ticket.port
|
||||||
assert ticket.sessionId
|
assert ticket.sessionId
|
||||||
|
assert session.nfc_ssl is True
|
||||||
|
assert session.authd_sock.version()
|
||||||
|
assert session.authd_sock.cipher()
|
||||||
|
|
||||||
|
def test_authd_nbd_handshake_completes(self, lab: LabEnv) -> None:
|
||||||
|
"""Complete authd with plaintext NFC after PROXY (nbd)."""
|
||||||
|
with lab.authenticate(nfc_ssl=False) as session:
|
||||||
|
assert session.nfc_ssl is False
|
||||||
|
assert session.ticket.sessionId
|
||||||
assert session.authd_sock.version()
|
assert session.authd_sock.version()
|
||||||
assert session.authd_sock.cipher()
|
assert session.authd_sock.cipher()
|
||||||
|
|||||||
@@ -3,15 +3,20 @@
|
|||||||
|
|
||||||
"""Exercise NFC disk open and a one-sector write/read against the lab."""
|
"""Exercise NFC disk open and a one-sector write/read against the lab."""
|
||||||
|
|
||||||
|
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 LabEnv, SECTOR_SIZE, pattern_bytes
|
||||||
|
|
||||||
|
|
||||||
class TestNfcOpen:
|
class TestNfcOpen:
|
||||||
def test_open_disk_and_read_first_sector(self, lab: LabEnv) -> None:
|
@pytest.mark.parametrize(
|
||||||
|
"nfc_ssl", [True, False], ids=["nbdssl", "nbd"])
|
||||||
|
def test_open_disk_and_read_first_sector(
|
||||||
|
self, lab: LabEnv, nfc_ssl: bool) -> 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) 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) as disk:
|
session, lab.disk_path, read_only=False) as disk:
|
||||||
assert disk.path == lab.disk_path
|
assert disk.path == lab.disk_path
|
||||||
|
|||||||
@@ -3,13 +3,17 @@
|
|||||||
|
|
||||||
"""Exercise the VDDK-compatible openvixdisklib handle against the lab."""
|
"""Exercise the VDDK-compatible openvixdisklib handle against the lab."""
|
||||||
|
|
||||||
|
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, pattern_bytes)
|
LabEnv, SECTOR_AT_1GB, SECTOR_SIZE, pattern_bytes)
|
||||||
|
|
||||||
|
|
||||||
class TestOpenvixdisklib:
|
class TestOpenvixdisklib:
|
||||||
def test_write_and_read_sector_zero_and_one_gib(self, lab: LabEnv) -> None:
|
@pytest.mark.parametrize("transport_mode", ["nbdssl", "nbd"])
|
||||||
|
def test_write_and_read_sector_zero_and_one_gib(
|
||||||
|
self, lab: LabEnv, transport_mode: str) -> 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",
|
||||||
@@ -18,13 +22,16 @@ class TestOpenvixdisklib:
|
|||||||
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,
|
||||||
})
|
})
|
||||||
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"),
|
||||||
}
|
}
|
||||||
|
assert handle.get_transport_modes() == ["nbdssl", "nbd"]
|
||||||
with handle.connect(**connect_kwargs) as conn:
|
with handle.connect(**connect_kwargs) as conn:
|
||||||
with handle.open(conn, lab.disk_path, flags=0) as disk:
|
with handle.open(conn, lab.disk_path, flags=0) as disk:
|
||||||
|
assert handle.get_transport_mode(disk) == transport_mode
|
||||||
for start, expected in patterns.items():
|
for start, expected in patterns.items():
|
||||||
write_buf[:SECTOR_SIZE] = expected
|
write_buf[:SECTOR_SIZE] = expected
|
||||||
handle.write(disk, start, 1, write_buf)
|
handle.write(disk, start, 1, write_buf)
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Copyright 2026 Cloudbase Solutions Srl
|
||||||
|
# All Rights Reserved.
|
||||||
|
|
||||||
|
"""Compare openvixdisklib and native VDDK I/O throughput against the lab."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from openvixdisklib import openvixdisklib as open_vix
|
||||||
|
from tests.integration import vixdisklib
|
||||||
|
from tests.integration.base import LabEnv, SECTOR_SIZE, pattern_bytes
|
||||||
|
|
||||||
|
_SIZES = (
|
||||||
|
("64KiB", 64 * 1024),
|
||||||
|
("129 sectors", 129 * SECTOR_SIZE),
|
||||||
|
("32MiB", 32 * 1024 * 1024),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _connect_extra(lab: LabEnv, module: Any) -> Optional[dict[str, Any]]:
|
||||||
|
"""Return extra ``connect`` kwargs needed by ``module``."""
|
||||||
|
if module is open_vix:
|
||||||
|
return {"allow_untrusted": lab.allow_untrusted}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _time_write_read(
|
||||||
|
lab: LabEnv,
|
||||||
|
module: Any,
|
||||||
|
payload: bytes) -> tuple[float, float]:
|
||||||
|
"""Write ``payload`` at sector 0, read it back, and return durations."""
|
||||||
|
n_sectors = len(payload) // SECTOR_SIZE
|
||||||
|
handle = module.VixDiskLibHandle(
|
||||||
|
vixdisklib_compatibility_version="8.0",
|
||||||
|
config_path=None)
|
||||||
|
write_buf = module.get_buffer(len(payload))
|
||||||
|
read_buf = module.get_buffer(len(payload))
|
||||||
|
write_buf[:len(payload)] = payload
|
||||||
|
kwargs = lab.vixdisklib_connect_kwargs(_connect_extra(lab, module))
|
||||||
|
with handle.connect(**kwargs) as conn:
|
||||||
|
with handle.open(conn, lab.disk_path, flags=0) as disk:
|
||||||
|
started = time.perf_counter()
|
||||||
|
handle.write(disk, 0, n_sectors, write_buf)
|
||||||
|
write_s = time.perf_counter() - started
|
||||||
|
read_buf[:len(payload)] = b"\xa5" * len(payload)
|
||||||
|
started = time.perf_counter()
|
||||||
|
handle.read(disk, 0, n_sectors, read_buf)
|
||||||
|
read_s = time.perf_counter() - started
|
||||||
|
assert read_buf.raw[:len(payload)] == payload
|
||||||
|
return write_s, read_s
|
||||||
|
|
||||||
|
|
||||||
|
def _mib_per_s(nbytes: int, seconds: float) -> float:
|
||||||
|
if seconds <= 0:
|
||||||
|
return float("inf")
|
||||||
|
return (nbytes / (1024 * 1024)) / seconds
|
||||||
|
|
||||||
|
|
||||||
|
class TestCompare:
|
||||||
|
def test_write_read_throughput(self, lab: LabEnv, vddk: None) -> None:
|
||||||
|
"""Time matching write/read sizes on VDDK and openvixdisklib."""
|
||||||
|
libraries = (
|
||||||
|
("vddk", vixdisklib),
|
||||||
|
("openvixdisklib", open_vix),
|
||||||
|
)
|
||||||
|
rows: list[tuple[str, str, float, float, float, float]] = []
|
||||||
|
for label, nbytes in _SIZES:
|
||||||
|
payload = pattern_bytes(nbytes, f"PERF-{label}-".encode())
|
||||||
|
for name, module in libraries:
|
||||||
|
write_s, read_s = _time_write_read(lab, module, payload)
|
||||||
|
rows.append((
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
write_s,
|
||||||
|
read_s,
|
||||||
|
_mib_per_s(nbytes, write_s),
|
||||||
|
_mib_per_s(nbytes, read_s),
|
||||||
|
))
|
||||||
|
print()
|
||||||
|
print(
|
||||||
|
f"{'size':<14} {'library':<16} {'write_s':>10} {'read_s':>10} "
|
||||||
|
f"{'write_MiB/s':>12} {'read_MiB/s':>12}")
|
||||||
|
for label, name, write_s, read_s, write_r, read_r in rows:
|
||||||
|
print(
|
||||||
|
f"{label:<14} {name:<16} {write_s:10.3f} {read_s:10.3f} "
|
||||||
|
f"{write_r:12.1f} {read_r:12.1f}")
|
||||||
Reference in New Issue
Block a user