Commit 29ed9606ba for qemu.org
commit 29ed9606ba15aa346eaec349d169f90eff56fd01
Author: Denis V. Lunev <den@openvz.org>
Date: Thu Aug 27 18:10:02 2026 +0200
iotests/nbd-commands: cover the command flags and sparse replies
No test sends the NBD command flags, so the server paths behind them
go unexercised. Send them from the new client.
A structured read answers a hole with an offset and a length rather
than a cluster of zeroes. Lay out data, a hole and data again, and
read the three arrangements a hole can appear in, since a leading hole
and a hole between two extents take different turns through
nbd_co_send_sparse_read(). NBD_CMD_FLAG_DF asks for one chunk instead,
which is the same layout sent as real zeroes. Check the chunk
boundaries in both forms, and that the data still reads back.
NBD_CMD_FLAG_REQ_ONE caps the extent array at one entry, so the block
status reply covers only the first cluster of the three asked about.
That a reply may describe less than was requested is also how the
server keeps NBD_MAX_BLOCK_STATUS_EXTENTS from being exceeded on a
long fragmented range, and a client assuming full coverage believes
stale status.
NBD_CMD_FLAG_FAST_ZERO becomes BDRV_REQ_NO_FALLBACK. A cluster aligned
zero can be done by marking the cluster, while zeroing part of a
cluster over a backing file needs the read modify write the flag
forbids, so check both the success and the ENOTSUP.
NBD_CMD_FLAG_FUA is added to a write and a trim.
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Eric Blake <eblake@redhat.com>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Reviewed-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Message-ID: <20260827161002.310688-6-den@openvz.org>
Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
diff --git a/tests/qemu-iotests/tests/nbd-commands b/tests/qemu-iotests/tests/nbd-commands
index 09e811724b..adfd4a495c 100755
--- a/tests/qemu-iotests/tests/nbd-commands
+++ b/tests/qemu-iotests/tests/nbd-commands
@@ -25,6 +25,7 @@ nbd: ModuleType
DEPTH_LOCAL = 1
DEPTH_BACKING = 2
+CLUSTER = 65536
class TestNbdCommands(iotests.QMPTestCase):
@@ -90,7 +91,7 @@ class TestNbdCommands(iotests.QMPTestCase):
self.h.shutdown()
self.h = None
- def block_status(self, count=size, wanted=None):
+ def block_status(self, count=size, wanted=None, flags=0):
"""Map each meta context in the reply to its list of extents."""
reply = {}
@@ -99,9 +100,9 @@ class TestNbdCommands(iotests.QMPTestCase):
entries[1::2]))
if wanted is None:
- self.h.block_status(count, 0, cb)
+ self.h.block_status(count, 0, cb, flags)
else:
- self.h.block_status_filter(count, 0, wanted, cb)
+ self.h.block_status_filter(count, 0, wanted, cb, flags)
return reply
def top_extents(self):
@@ -158,6 +159,71 @@ class TestNbdCommands(iotests.QMPTestCase):
for wanted in (['base:allocation'], ['qemu:allocation-depth']):
self.assertEqual(sorted(self.block_status(wanted=wanted)), wanted)
+ def read_chunks(self, count, offset, flags=0):
+ chunks = []
+
+ def cb(subbuf, off, status, _err):
+ chunks.append((off, len(subbuf), status))
+
+ self.h.pread_structured(count, offset, cb, flags)
+ return chunks
+
+ def make_sparse(self):
+ """Lay out data, a hole and data again, one cluster each."""
+ self.h.pwrite(b'z' * CLUSTER, 0)
+ self.h.zero(CLUSTER, CLUSTER)
+ self.h.pwrite(b'z' * CLUSTER, 2 * CLUSTER)
+
+ def test_read_sparse_chunks(self):
+ self.make_sparse()
+
+ first = (0, CLUSTER, nbd.READ_DATA)
+ hole = (CLUSTER, CLUSTER, nbd.READ_HOLE)
+ second = (2 * CLUSTER, CLUSTER, nbd.READ_DATA)
+
+ # A hole is an offset and a length, not a cluster of zeroes,
+ # wherever it falls in the reply
+ self.assertEqual(self.read_chunks(2 * CLUSTER, 0), [first, hole])
+ self.assertEqual(self.read_chunks(2 * CLUSTER, CLUSTER),
+ [hole, second])
+ self.assertEqual(self.read_chunks(3 * CLUSTER, 0),
+ [first, hole, second])
+
+ self.assertEqual(self.h.pread(CLUSTER, CLUSTER), bytes(CLUSTER))
+ self.assertEqual(self.h.pread(CLUSTER, 0), b'z' * CLUSTER)
+
+ def test_read_dont_fragment(self):
+ self.make_sparse()
+
+ self.assertEqual(self.read_chunks(3 * CLUSTER, 0, nbd.CMD_FLAG_DF),
+ [(0, 3 * CLUSTER, nbd.READ_DATA)])
+
+ def test_block_status_req_one(self):
+ hole = nbd.STATE_HOLE | nbd.STATE_ZERO
+ self.make_sparse()
+
+ alloc = self.block_status(3 * CLUSTER)['base:allocation']
+ self.assertEqual(alloc, [(CLUSTER, 0), (CLUSTER, hole), (CLUSTER, 0)])
+
+ # One extent, so the reply covers less than was asked for and the
+ # client has to come back for the rest
+ alloc = self.block_status(3 * CLUSTER, flags=nbd.CMD_FLAG_REQ_ONE)
+ self.assertEqual(alloc['base:allocation'], [(CLUSTER, 0)])
+
+ def test_write_and_trim_fua(self):
+ self.h.pwrite(b'y' * 4096, 4096, nbd.CMD_FLAG_FUA)
+ self.assertEqual(self.h.pread(4096, 4096), b'y' * 4096)
+ self.h.trim(4096, 4096, nbd.CMD_FLAG_FUA)
+
+ def test_fast_zero(self):
+ self.h.zero(CLUSTER, CLUSTER, nbd.CMD_FLAG_FAST_ZERO)
+ self.assertEqual(self.h.pread(CLUSTER, CLUSTER), bytes(CLUSTER))
+
+ # Zeroing part of a cluster needs the fallback the flag forbids
+ with self.assertRaises(nbd.Error) as caught:
+ self.h.zero(4096, 4096, nbd.CMD_FLAG_FAST_ZERO)
+ self.assertEqual(caught.exception.errno, 'ENOTSUP')
+
def test_cache_past_end_of_export(self):
self.assertRaises(nbd.Error, self.h.cache, size + 1, 0)
diff --git a/tests/qemu-iotests/tests/nbd-commands.out b/tests/qemu-iotests/tests/nbd-commands.out
index 2f7d3902f2..281b69efea 100644
--- a/tests/qemu-iotests/tests/nbd-commands.out
+++ b/tests/qemu-iotests/tests/nbd-commands.out
@@ -1,5 +1,5 @@
-.......
+............
----------------------------------------------------------------------
-Ran 7 tests
+Ran 12 tests
OK