This is an automated email from the ASF dual-hosted git repository.
weizhouapache pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/main by this push:
new a6b31286f06 [Veeam] Fix for dirty context not being returned on el9
a6b31286f06 is described below
commit a6b31286f061d0e2b71233ac9136730b6ce5285e
Author: Nicolas Vazquez <[email protected]>
AuthorDate: Tue Jul 14 11:41:50 2026 -0300
[Veeam] Fix for dirty context not being returned on el9
The dirty extents were not ready correctly for the block_status_64 return
and as a result image server always reported the whole image range as dirty.
This commit fixes by normalizing the value returned by both block_status
and block_status_64. It also adds warning whenever there is an issue getting
the dirty extents and the code falls back to returning the full range as dirty.
Co-authored-by: Abhisar Sinha <[email protected]>
---
.../vm/hypervisor/kvm/imageserver/backends/nbd.py | 45 +++--
scripts/vm/hypervisor/kvm/imageserver/handler.py | 5 +
.../kvm/imageserver/tests/test_nbd_entries.py | 194 +++++++++++++++++++++
3 files changed, 228 insertions(+), 16 deletions(-)
diff --git a/scripts/vm/hypervisor/kvm/imageserver/backends/nbd.py
b/scripts/vm/hypervisor/kvm/imageserver/backends/nbd.py
index c68a3b5188b..002d777080e 100644
--- a/scripts/vm/hypervisor/kvm/imageserver/backends/nbd.py
+++ b/scripts/vm/hypervisor/kvm/imageserver/backends/nbd.py
@@ -32,6 +32,24 @@ from ..util import coalesce_allocation_extents,
merge_dirty_zero_extents
from .base import BackendSession, ImageBackend
+def _entries_to_pairs(entries: Any) -> List[Tuple[int, int]]:
+ """
+ Normalize block status callback entries to (length, flags) pairs.
+ block_status delivers a flat list [length, flags, ...] while
+ block_status_64 delivers a list of (length, flags) tuples.
+ """
+ raw = list(entries)
+ if not raw:
+ return []
+ if isinstance(raw[0], (tuple, list)):
+ return [(int(length), int(flags)) for length, flags in raw]
+ if isinstance(raw[0], int):
+ return [(int(raw[i]), int(raw[i + 1])) for i in range(0, len(raw) - 1,
2)]
+ raise TypeError(
+ f"unrecognized block status entry type: {type(raw[0]).__name__}"
+ )
+
+
class NbdConnection:
"""
Low-level helper to connect to an NBD server over a Unix socket.
@@ -190,19 +208,16 @@ class NbdConnection:
return 0
current = off
try:
- flat = list(entries)
- for i in range(0, len(flat), 2):
- if i + 1 >= len(flat):
- break
- length = int(flat[i])
- flags = int(flat[i + 1])
+ for length, flags in _entries_to_pairs(entries):
zero = (flags & (NBD_STATE_HOLE | NBD_STATE_ZERO)) != 0
allocation_extents.append(
{"start": current, "length": length, "zero": zero}
)
current += length
- except (TypeError, ValueError, IndexError):
- pass
+ except (TypeError, ValueError, IndexError) as e:
+ logging.warning(
+ "get_allocation_extents: unparseable block status entries:
%r", e
+ )
return 0
block_status_fn = getattr(
@@ -261,12 +276,7 @@ class NbdConnection:
return 0
current = off
try:
- flat = list(entries)
- for i in range(0, len(flat), 2):
- if i + 1 >= len(flat):
- break
- length = int(flat[i])
- flags = int(flat[i + 1])
+ for length, flags in _entries_to_pairs(entries):
if metacontext == "base:allocation":
zero = (flags & (NBD_STATE_HOLE | NBD_STATE_ZERO)) != 0
allocation_extents.append((current, length, zero))
@@ -274,8 +284,11 @@ class NbdConnection:
dirty = (flags & NBD_STATE_DIRTY) != 0
dirty_extents.append((current, length, dirty))
current += length
- except (TypeError, ValueError, IndexError):
- pass
+ except (TypeError, ValueError, IndexError) as e:
+ logging.warning(
+ "get_extents_dirty_and_zero: unparseable block status
entries: %r",
+ e,
+ )
return 0
block_status_fn = getattr(
diff --git a/scripts/vm/hypervisor/kvm/imageserver/handler.py
b/scripts/vm/hypervisor/kvm/imageserver/handler.py
index 53bef34b95e..fcdc0b68077 100644
--- a/scripts/vm/hypervisor/kvm/imageserver/handler.py
+++ b/scripts/vm/hypervisor/kvm/imageserver/handler.py
@@ -746,6 +746,11 @@ class Handler(BaseHTTPRequestHandler):
dirty_bitmap_ctx = f"qemu:dirty-bitmap:{export_bitmap}"
extents = backend.get_dirty_extents(dirty_bitmap_ctx)
if is_fallback_dirty_response(extents):
+ logging.warning(
+ "EXTENTS image_id=%s: no usable dirty bitmap data
for "
+ "context %r; reporting all allocated extents as
dirty",
+ image_id, dirty_bitmap_ctx,
+ )
allocation = backend.get_allocation_extents()
extents = [
{
diff --git a/scripts/vm/hypervisor/kvm/imageserver/tests/test_nbd_entries.py
b/scripts/vm/hypervisor/kvm/imageserver/tests/test_nbd_entries.py
new file mode 100644
index 00000000000..8654aa5dad6
--- /dev/null
+++ b/scripts/vm/hypervisor/kvm/imageserver/tests/test_nbd_entries.py
@@ -0,0 +1,194 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+Tests for NBD block status entry parsing.
+"""
+
+import os
+import shutil
+import subprocess
+import tempfile
+import unittest
+
+from imageserver.backends.nbd import NbdConnection, _entries_to_pairs
+from imageserver.util import is_fallback_dirty_response
+
+class TestRealLibnbdBlockStatus(unittest.TestCase):
+ """
+ Creates a 1 MiB qcow2 with a persistent dirty bitmap, dirties two
+ 64 KiB regions, exports it via qemu-nbd --bitmap, and asserts both the
+ raw callback entry format and the parsed extents. Fails loudly if a
+ new libnbd version changes the block status response.
+ """
+
+ REAL_BITMAP = "bm0"
+ KB64 = 65536
+ REAL_SIZE = 16 * KB64 # 1 MiB
+
+ @classmethod
+ def setUpClass(cls):
+ cls._tmp = tempfile.mkdtemp(prefix="nbd_entries_test_")
+ cls._img = os.path.join(cls._tmp, "img.qcow2")
+ cls._sock = os.path.join(cls._tmp, "nbd.sock")
+ cls._proc = None
+
+ def run(*cmd):
+ subprocess.run(cmd, check=True, capture_output=True)
+
+ try:
+ run("qemu-img", "create", "-f", "qcow2", cls._img,
str(cls.REAL_SIZE))
+ # allocate 0-192 KiB before the bitmap exists (clean, allocated)
+ run("qemu-io", "-f", "qcow2", "-c", "write -P 0xaa 0 192k",
cls._img)
+ run("qemu-img", "bitmap", "--add", cls._img, cls.REAL_BITMAP)
+ # dirty two 64 KiB regions while the bitmap is recording
+ run("qemu-io", "-f", "qcow2", "-c", "write -P 0xbb 128k 64k",
cls._img)
+ run("qemu-io", "-f", "qcow2", "-c", "write -P 0xcc 512k 64k",
cls._img)
+
+ cls._proc = subprocess.Popen(
+ [
+ "qemu-nbd",
+ "--socket", cls._sock,
+ "--format", "qcow2",
+ "--persistent",
+ "--shared=0",
+ "--read-only",
+ f"--bitmap={cls.REAL_BITMAP}",
+ cls._img,
+ ],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ from .test_base import _wait_for_nbd_socket
+
+ _wait_for_nbd_socket(cls._sock)
+ except BaseException:
+ cls._teardown()
+ raise
+
+ @classmethod
+ def tearDownClass(cls):
+ cls._teardown()
+
+ @classmethod
+ def _teardown(cls):
+ if cls._proc is not None:
+ for pipe in (cls._proc.stdout, cls._proc.stderr):
+ if pipe:
+ pipe.close()
+ cls._proc.terminate()
+ try:
+ cls._proc.wait(timeout=5)
+ except subprocess.TimeoutExpired:
+ cls._proc.kill()
+ cls._proc.wait(timeout=5)
+ cls._proc = None
+ shutil.rmtree(cls._tmp, ignore_errors=True)
+
+ def _connect(self):
+ return NbdConnection(
+ self._sock,
+ None,
+ need_block_status=True,
+ extra_meta_contexts=[f"qemu:dirty-bitmap:{self.REAL_BITMAP}"],
+ )
+
+ def test_raw_entries_format_is_known(self):
+ """The raw callback entries must be flat ints or (length, flags)
pairs."""
+ with self._connect() as conn:
+ size = conn.size()
+ captured = []
+
+ def cb(*args, **kwargs):
+ self.assertGreaterEqual(len(args), 3)
+ captured.append((args[0], list(args[2])))
+ return 0
+
+ fn = getattr(
+ conn._nbd,
+ "block_status_64",
+ getattr(conn._nbd, "block_status", None),
+ )
+ self.assertIsNotNone(
+ fn, "libnbd no longer exposes block_status/block_status_64"
+ )
+ fn(size, 0, cb)
+
+ self.assertTrue(captured, "block status delivered no callbacks")
+ contexts = {ctx for ctx, _ in captured}
+ self.assertIn("base:allocation", contexts)
+ self.assertIn(f"qemu:dirty-bitmap:{self.REAL_BITMAP}", contexts)
+
+ for ctx, entries in captured:
+ self.assertTrue(entries, f"empty entries for context {ctx}")
+ if isinstance(entries[0], (tuple, list)):
+ for item in entries:
+ self.assertIsInstance(item, (tuple, list))
+ self.assertEqual(len(item), 2)
+ self.assertIsInstance(item[0], int)
+ self.assertIsInstance(item[1], int)
+ else:
+ self.assertTrue(all(isinstance(v, int) for v in entries))
+ self.assertEqual(len(entries) % 2, 0)
+ # entries must parse and tile part of the queried range
+ pairs = _entries_to_pairs(entries)
+ self.assertTrue(pairs)
+ covered = sum(length for length, _ in pairs)
+ self.assertGreater(covered, 0)
+ self.assertLessEqual(covered, size)
+
+ def test_allocation_extents_tile_image(self):
+ with self._connect() as conn:
+ size = conn.size()
+ extents = conn.get_allocation_extents()
+ self.assertEqual(size, self.REAL_SIZE)
+ pos = 0
+ for e in extents:
+ self.assertEqual(e["start"], pos)
+ self.assertGreater(e["length"], 0)
+ pos += e["length"]
+ self.assertEqual(pos, size)
+ self.assertEqual(
+ extents,
+ [
+ {"start": 0, "length": 3 * self.KB64, "zero": False},
+ {"start": 3 * self.KB64, "length": 5 * self.KB64, "zero":
True},
+ {"start": 8 * self.KB64, "length": self.KB64, "zero": False},
+ {"start": 9 * self.KB64, "length": 7 * self.KB64, "zero":
True},
+ ],
+ )
+
+ def test_dirty_extents_match_writes(self):
+ with self._connect() as conn:
+ extents = conn.get_extents_dirty_and_zero(
+ f"qemu:dirty-bitmap:{self.REAL_BITMAP}"
+ )
+ self.assertEqual(
+ extents,
+ [
+ {"start": 0, "length": 2 * self.KB64, "dirty": False, "zero":
False},
+ {"start": 2 * self.KB64, "length": self.KB64, "dirty": True,
"zero": False},
+ {"start": 3 * self.KB64, "length": 5 * self.KB64, "dirty":
False, "zero": True},
+ {"start": 8 * self.KB64, "length": self.KB64, "dirty": True,
"zero": False},
+ {"start": 9 * self.KB64, "length": 7 * self.KB64, "dirty":
False, "zero": True},
+ ],
+ )
+ self.assertFalse(is_fallback_dirty_response(extents))
+
+
+if __name__ == "__main__":
+ unittest.main()