From 80af360d5b6a3a0ff9ad4bcf73ba72c2faf68c5b Mon Sep 17 00:00:00 2001 From: Goulven Riou Date: Wed, 28 Jan 2026 16:09:29 +0100 Subject: [PATCH 1/8] implement CBT helpers in lib/vdi.py and tests/storage/storage.py Signed-off-by: Goulven Riou --- lib/vdi.py | 27 ++++ tests/storage/__init__.py | 16 ++ tests/storage/storage.py | 326 +++++++++++++++++++++++++++++++++++++- 3 files changed, 368 insertions(+), 1 deletion(-) diff --git a/lib/vdi.py b/lib/vdi.py index 0ad83a580..5d749e4ff 100644 --- a/lib/vdi.py +++ b/lib/vdi.py @@ -23,6 +23,7 @@ ImageFormat = Literal['qcow2', 'raw', 'vhd'] + class VDI: xe_prefix = "vdi" sr: SR @@ -53,6 +54,10 @@ def destroy(self) -> None: logging.info("Destroy %s", self) self.sr.pool.master.xe('vdi-destroy', {'uuid': self.uuid}) + def data_destroy(self) -> None: + logging.info("Data-destroy %s", self) + self.sr.pool.master.xe('vdi-data-destroy', {'uuid': self.uuid}) + def clone(self) -> VDI: uuid = self.sr.pool.master.xe('vdi-clone', {'uuid': self.uuid}) return VDI(uuid, sr=self.sr) @@ -124,3 +129,25 @@ def wait_for_coalesce(self, fn: Callable[[], R] | None = None) -> R | None: wait_for(lambda: self.get_parent() != previous_parent, msg="Waiting for coalesce", timeout_secs=10 * 60) logging.info("Coalesce done") return ret + + def enable_cbt(self) -> None: + logging.info(f"Enabling CBT on VDI {self.uuid}") + self.sr.pool.master.xe('vdi-enable-cbt', {'uuid': self.uuid}) + + def disable_cbt(self) -> None: + logging.info(f"Disabling CBT on VDI {self.uuid}") + self.sr.pool.master.xe('vdi-disable-cbt', {'uuid': self.uuid}) + + def get_cbt_enabled(self) -> str: + return self.param_get('cbt-enabled') + + def is_cbt_enabled(self) -> bool: + return self.get_cbt_enabled() == 'true' + + def list_changed_blocks(self, vdi_to: 'VDI') -> str: + + logging.info(f"Listing changed blocks from VDI {self.uuid} to {vdi_to.uuid}") + return self.sr.pool.master.xe('vdi-list-changed-blocks', { + 'vdi-from-uuid': self.uuid, + 'vdi-to-uuid': vdi_to.uuid + }) diff --git a/tests/storage/__init__.py b/tests/storage/__init__.py index 5e34c4882..280d3afaa 100644 --- a/tests/storage/__init__.py +++ b/tests/storage/__init__.py @@ -1,16 +1,32 @@ from .storage import ( MAX_VDI_SIZE, + CBTTest, CoalesceOperation, ImageFormat, XVACompression, + assert_cbt_disabled, + assert_cbt_enabled, + assert_cbt_log_does_not_exist_file_sr, + assert_cbt_log_does_not_exist_lvm_sr, + assert_cbt_log_exists_file_sr, + assert_cbt_log_exists_lvm_sr, + assert_changed_blocks_exist, + assert_no_changed_blocks, + cbt_enabled, coalesce_integrity, cold_migration_then_come_back, + disable_cbt_with_wait, + enable_cbt_with_wait, full_vdi_write, + get_vdi_from_vm, install_randstream, live_storage_migration_then_come_back, randstream, try_to_create_sr_with_missing_device, vdi_export_import, vdi_is_open, + verify_changed_blocks_detected, + wait_for_cbt_disabled, + wait_for_cbt_enabled, xva_export_import, ) diff --git a/tests/storage/storage.py b/tests/storage/storage.py index 80cedd8ba..742e7e748 100644 --- a/tests/storage/storage.py +++ b/tests/storage/storage.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +from contextlib import ExitStack, contextmanager from dataclasses import dataclass from lib import config @@ -11,7 +12,7 @@ from lib.vdi import VDI, ImageFormat from lib.vm import VM -from typing import Literal +from typing import Generator, Literal MAX_VDI_SIZE: dict[ImageFormat, int] = {'qcow2': QCOW2_MAX, 'vhd': VHD_MAX} @@ -26,6 +27,7 @@ def try_to_create_sr_with_missing_device(sr_type, label, host) -> None: return assert False, 'SR creation should not have succeeded!' + def cold_migration_then_come_back(vm: VM, prov_host: Host, dest_host: Host, dest_sr: SR) -> None: """ Storage migration of a shutdown VM, then migrate it back. """ prov_sr = vm.get_sr() @@ -489,3 +491,325 @@ def validate_partially_populated_device(vm: VM, dev: str, spans: list[StreamSpan for span in spans: if span.checksum is not None: span.validate(vm, dev) + +# ---- CBT helpers ------------------------------------------------------------- + +def wait_for_cbt_enabled(vdi: VDI, timeout: int = 60) -> None: + wait_for( + lambda: vdi.is_cbt_enabled(), + msg=f"Waiting for CBT to be enabled on VDI {vdi.uuid}", + timeout_secs=timeout + ) + + +def wait_for_cbt_disabled(vdi: VDI, timeout: int = 60) -> None: + wait_for( + lambda: not vdi.is_cbt_enabled(), + msg=f"Waiting for CBT to be disabled on VDI {vdi.uuid}", + timeout_secs=timeout + ) + + +def assert_cbt_enabled(vdi: VDI) -> None: + assert vdi.is_cbt_enabled(), f"CBT should be enabled on VDI {vdi.uuid}" + + +def assert_cbt_disabled(vdi: VDI) -> None: + assert not vdi.is_cbt_enabled(), f"CBT should be disabled on VDI {vdi.uuid}" + + +def list_changed_blocks(vdi_from: VDI, vdi_to: VDI) -> str: + """List changed blocks between two VDIs using the correct XAPI parameter name.""" + logging.info(f"Listing changed blocks from VDI {vdi_from.uuid} to {vdi_to.uuid}") + return vdi_from.sr.pool.master.xe('vdi-list-changed-blocks', { + 'vdi-from-uuid': vdi_from.uuid, + 'vdi-to-uuid': vdi_to.uuid + }) + + +def verify_changed_blocks_detected(vdi_from: VDI, vdi_to: VDI) -> bool: + changed = list_changed_blocks(vdi_from, vdi_to) + return bool(changed and changed.strip()) + + +def assert_changed_blocks_exist(vdi_from: VDI, vdi_to: VDI) -> None: + changed = list_changed_blocks(vdi_from, vdi_to) + assert changed and changed.strip(), \ + f"Expected changed blocks between {vdi_from.uuid} and {vdi_to.uuid}" + + +def assert_no_changed_blocks(vdi_from: VDI, vdi_to: VDI) -> None: + changed = list_changed_blocks(vdi_from, vdi_to) + assert not changed or not changed.strip(), \ + f"Expected no changed blocks between {vdi_from.uuid} and {vdi_to.uuid}" + + +def get_vdi_from_vm(vm: VM, sr: SR) -> VDI: + """Return the first VDI of a VM, bound to the given SR.""" + vdi_uuids = vm.vdi_uuids() + assert vdi_uuids, f"VM {vm.uuid} has no VDIs" + return VDI(vdi_uuids[0], sr=sr) + + +def enable_cbt_with_wait(vdi: VDI, timeout: int = 60) -> None: + vdi.enable_cbt() + wait_for_cbt_enabled(vdi, timeout) + + +def disable_cbt_with_wait(vdi: VDI, timeout: int = 60) -> None: + vdi.disable_cbt() + wait_for_cbt_disabled(vdi, timeout) + + +def assert_cbt_log_exists_file_sr(host: Host, sr: SR, vdi: VDI) -> None: + log_path = f"/var/run/sr-mount/{sr.uuid}/{vdi.uuid}.cbtlog" + result = host.ssh(f'test -f {log_path}', check=False, simple_output=False) + assert result.returncode == 0, f"CBT log not found at {log_path}" + result = host.ssh(f'stat -c %s {log_path}', simple_output=False) + log_size = int(result.stdout.strip()) + assert log_size > 0, f"CBT log at {log_path} is empty" + logging.info(f"CBT log exists: {log_path} ({log_size} bytes)") + + +def assert_cbt_log_exists_lvm_sr(host: Host, sr: SR, vdi: VDI) -> None: + vg_name = f"VG_XenStorage-{sr.uuid}" + cbt_log_name = f"{vdi.uuid}.cbtlog" + result = host.ssh(f'lvs --noheadings -o lv_name {vg_name}', simple_output=False) + assert cbt_log_name in result.stdout, \ + f"CBT log LV {cbt_log_name} not found in VG {vg_name}" + logging.info(f"CBT log LV exists: {vg_name}/{cbt_log_name}") + + +def assert_cbt_log_does_not_exist_file_sr(host: Host, sr: SR, vdi: VDI) -> None: + log_path = f"/var/run/sr-mount/{sr.uuid}/{vdi.uuid}.cbtlog" + result = host.ssh(f'test -f {log_path}', check=False, simple_output=False) + assert result.returncode != 0, f"CBT log should not exist at {log_path}" + logging.info(f"CBT log correctly absent: {log_path}") + + +def assert_cbt_log_does_not_exist_lvm_sr(host: Host, sr: SR, vdi: VDI) -> None: + vg_name = f"VG_XenStorage-{sr.uuid}" + cbt_log_name = f"{vdi.uuid}.cbtlog" + result = host.ssh(f'lvs --noheadings -o lv_name {vg_name}', simple_output=False) + assert cbt_log_name not in result.stdout, \ + f"CBT log LV {cbt_log_name} should not exist in VG {vg_name}" + logging.info(f"CBT log LV correctly absent: {vg_name}/{cbt_log_name}") + + +@contextmanager +def cbt_enabled(vdi: VDI) -> Generator[VDI, None, None]: + enable_cbt_with_wait(vdi) + try: + yield vdi + finally: + disable_cbt_with_wait(vdi) + + +@contextmanager +def vm_halted(vm: VM) -> Generator[VM, None, None]: + """Ensure the VM is halted on entry, yield, then ensure it is halted again on exit.""" + if vm.is_paused() or vm.is_suspended(): + vm.shutdown(force=True, verify=True) + elif vm.is_running(): + vm.shutdown(verify=True) + wait_for(vm.is_halted, "Wait for VM halted") + try: + yield vm + finally: + if vm.is_paused() or vm.is_suspended(): + vm.shutdown(force=True, verify=True) + elif vm.is_running(): + vm.shutdown(verify=True) + wait_for(vm.is_halted, "Wait for VM halted") + + +@contextmanager +def vm_started(vm: VM) -> Generator[VM, None, None]: + """Ensure the VM is started, booted, and SSH-reachable on entry, yield, then shut it down on exit.""" + if vm.is_paused() or vm.is_suspended(): + vm.shutdown(force=True, verify=True) + wait_for(vm.is_halted, "Wait for VM halted before start") + vm.start() + vm.wait_for_vm_running_and_ssh_up() + try: + yield vm + finally: + if vm.is_running(): + vm.shutdown(verify=True, force_if_fails=True) + + +class CBTTest: + """ + Base class for CBT tests on a given SR. + + Subclasses should expose one `test_*` wrapper per `_test_*` method below, + declaring the SR and VM fixtures explicitly so that fixture-dependent + parametrization (e.g. `image_format`) is correctly resolved by pytest. + """ + + @staticmethod + def assert_cbt_log_exists(host: Host, sr: SR, vdi: VDI) -> None: + raise NotImplementedError + + @staticmethod + def assert_cbt_log_does_not_exist(host: Host, sr: SR, vdi: VDI) -> None: + raise NotImplementedError + + def _test_enable_disable_cbt(self, host: Host, sr: SR, vdi: VDI) -> None: + with cbt_enabled(vdi): + assert_cbt_enabled(vdi) + assert_cbt_disabled(vdi) + + def _test_cbt_log_creation(self, host: Host, sr: SR, vdi: VDI) -> None: + with cbt_enabled(vdi): + self.assert_cbt_log_exists(host, sr, vdi) + + def _test_snapshot_with_cbt(self, host: Host, sr: SR, vdi: VDI) -> None: + with cbt_enabled(vdi), ExitStack() as stack: + snapshot_vdi = vdi.snapshot() + stack.callback(lambda: vdi.wait_for_coalesce(snapshot_vdi.destroy)) + assert_cbt_enabled(snapshot_vdi) + self.assert_cbt_log_exists(host, sr, snapshot_vdi) + logging.info(f"Snapshot with CBT: {snapshot_vdi.uuid}") + + def _test_changed_blocks_tracking(self, host: Host, sr: SR, vdi: VDI, vm: VM) -> None: + with cbt_enabled(vdi), ExitStack() as stack: + baseline = vdi.snapshot() + stack.callback(baseline.destroy) + vm.start() + vm.wait_for_os_booted() + vbd = vm.connect_vdi(vdi) + dev = f'/dev/{vbd.param_get("device")}' + install_randstream(vm) + vm.ssh(f'randstream generate --size 10485760 {dev}') + vm.ssh('sync') + vm.disconnect_vdi(vdi) + vm.shutdown(verify=True) + second = vdi.snapshot() + stack.callback(second.destroy) + assert_changed_blocks_exist(baseline, second) + logging.info("Changed blocks detected successfully") + + def _test_cbt_after_coalesce(self, host: Host, sr: SR, vdi: VDI, vm: VM) -> None: + enable_cbt_with_wait(vdi) + try: + vm.start() + vm.wait_for_os_booted() + vbd = vm.connect_vdi(vdi) + dev = f'/dev/{vbd.param_get("device")}' + install_randstream(vm) + vm.ssh(f'randstream generate --size 5242880 {dev}') + vm.ssh('sync') + vm.disconnect_vdi(vdi) + vm.shutdown(verify=True) + snap1 = vdi.snapshot() + vdi.wait_for_coalesce(snap1.destroy) + assert_cbt_enabled(vdi) + logging.info("CBT survived coalesce") + finally: + disable_cbt_with_wait(vdi) + + def _test_incremental_snap_scenario(self, host: Host, sr: SR, vdi: VDI, vm: VM) -> None: + enable_cbt_with_wait(vdi) + snapshots = [] + try: + vm.start() + vm.wait_for_os_booted() + vbd = vm.connect_vdi(vdi) + dev = f'/dev/{vbd.param_get("device")}' + install_randstream(vm) + for i in range(3): + snap = vdi.snapshot() + snapshots.append(snap) + assert_cbt_enabled(snap) + if i < 2: + vm.ssh(f'randstream generate --size 5242880 {dev}') + vm.ssh('sync') + vm.disconnect_vdi(vdi) + vm.shutdown(verify=True) + for i in range(len(snapshots) - 1): + assert verify_changed_blocks_detected(snapshots[i], snapshots[i + 1]) + logging.info(f"Changes detected: snap{i} -> snap{i + 1}") + finally: + for snap in snapshots: + snap.destroy() + disable_cbt_with_wait(vdi) + + def _test_disable_cbt_removes_log(self, host: Host, sr: SR, vdi: VDI) -> None: + enable_cbt_with_wait(vdi) + self.assert_cbt_log_exists(host, sr, vdi) + disable_cbt_with_wait(vdi) + self.assert_cbt_log_does_not_exist(host, sr, vdi) + + def _test_destroy_vdi_removes_cbt_log(self, host: Host, sr: SR, vdi: VDI) -> None: + with cbt_enabled(vdi): + snapshot_vdi = vdi.snapshot() + with ExitStack() as stack: + stack.callback(lambda: vdi.wait_for_coalesce(snapshot_vdi.destroy)) + self.assert_cbt_log_exists(host, sr, snapshot_vdi) + self.assert_cbt_log_does_not_exist(host, sr, snapshot_vdi) + + def _test_cbt_persist_after_sr_reboot(self, host: Host, sr: SR, vdi: VDI) -> None: + with cbt_enabled(vdi): + assert_cbt_enabled(vdi) + sr.unplug_pbds() + sr.plug_pbds(verify=True) + assert_cbt_enabled(vdi) + logging.info("CBT persisted after SR unplug/replug") + + def _test_cbt_on_snapshot_chain(self, host: Host, sr: SR, vdi: VDI) -> None: + with cbt_enabled(vdi), ExitStack() as stack: + snap1 = vdi.snapshot() + stack.callback(snap1.destroy) + assert_cbt_enabled(snap1) + snap2 = snap1.snapshot() + stack.callback(snap2.destroy) + assert_cbt_enabled(snap2) + snap3 = snap2.snapshot() + stack.callback(snap3.destroy) + assert_cbt_enabled(snap3) + logging.info("CBT enabled on full snapshot chain") + + def _test_cbt_parent_disable_does_not_affect_snapshot(self, host: Host, sr: SR, vdi: VDI) -> None: + with cbt_enabled(vdi): + snapshot_vdi = vdi.snapshot() + try: + assert_cbt_enabled(snapshot_vdi) + disable_cbt_with_wait(vdi) + assert_cbt_disabled(vdi) + assert_cbt_enabled(snapshot_vdi) + logging.info("Disabling CBT on parent did not affect snapshot") + enable_cbt_with_wait(vdi) + finally: + vdi.wait_for_coalesce(snapshot_vdi.destroy) + + def _test_cbt_data_destroy(self, host: Host, sr: SR, vdi: VDI) -> None: + with cbt_enabled(vdi), ExitStack() as stack: + snapshot_vdi = vdi.snapshot() + stack.callback(snapshot_vdi.destroy) + assert_cbt_enabled(snapshot_vdi) + self.assert_cbt_log_exists(host, sr, snapshot_vdi) + snapshot_vdi.data_destroy() + assert_cbt_enabled(snapshot_vdi) + self.assert_cbt_log_exists(host, sr, snapshot_vdi) + logging.info("CBT log persists after data_destroy") + + def _test_cbt_bitmap_non_zero_after_write(self, host: Host, sr: SR, vdi: VDI, vm: VM) -> None: + with cbt_enabled(vdi), ExitStack() as stack: + baseline = vdi.snapshot() + stack.callback(baseline.destroy) + vm.start() + vm.wait_for_os_booted() + vbd = vm.connect_vdi(vdi) + dev = f'/dev/{vbd.param_get("device")}' + install_randstream(vm) + vm.ssh(f'randstream generate --size 10485760 {dev}') + vm.ssh('sync') + vm.disconnect_vdi(vdi) + vm.shutdown(verify=True) + after = vdi.snapshot() + stack.callback(after.destroy) + changed = list_changed_blocks(baseline, after) + assert changed.strip('0'), \ + "CBT bitmap should contain non-zero blocks after write" + logging.info("CBT bitmap has non-zero blocks after write") From 715d4a75b09ea8c9cb21e9348cea26f531d1f77a Mon Sep 17 00:00:00 2001 From: Goulven Riou Date: Wed, 20 May 2026 09:57:29 +0200 Subject: [PATCH 2/8] implement cbt_tests on extSR Signed-off-by: Goulven Riou --- tests/storage/ext/test_ext_sr.py | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/storage/ext/test_ext_sr.py b/tests/storage/ext/test_ext_sr.py index ff335feb3..82f22bde5 100644 --- a/tests/storage/ext/test_ext_sr.py +++ b/tests/storage/ext/test_ext_sr.py @@ -13,9 +13,12 @@ from lib.vm import VM from tests.storage import ( MAX_VDI_SIZE, + CBTTest, CoalesceOperation, ImageFormat, XVACompression, + assert_cbt_log_does_not_exist_file_sr, + assert_cbt_log_exists_file_sr, coalesce_integrity, full_vdi_write, try_to_create_sr_with_missing_device, @@ -142,3 +145,59 @@ def test_reboot(self, host: Host, ext_sr: SR, vm_on_ext_sr: VM) -> None: vm.shutdown(verify=True) # *** End of tests with reboots + + +class TestEXTCBT(CBTTest): + """Test CBT functionality on EXT SR""" + + @staticmethod + def assert_cbt_log_exists(host: Host, sr: SR, vdi: VDI) -> None: + assert_cbt_log_exists_file_sr(host, sr, vdi) + + @staticmethod + def assert_cbt_log_does_not_exist(host: Host, sr: SR, vdi: VDI) -> None: + assert_cbt_log_does_not_exist_file_sr(host, sr, vdi) + + def test_enable_disable_cbt(self, host: Host, ext_sr: SR, vdi_on_ext_sr: VDI) -> None: + self._test_enable_disable_cbt(host, ext_sr, vdi_on_ext_sr) + + def test_cbt_log_creation(self, host: Host, ext_sr: SR, vdi_on_ext_sr: VDI) -> None: + self._test_cbt_log_creation(host, ext_sr, vdi_on_ext_sr) + + def test_snapshot_with_cbt(self, host: Host, ext_sr: SR, vdi_on_ext_sr: VDI) -> None: + self._test_snapshot_with_cbt(host, ext_sr, vdi_on_ext_sr) + + @pytest.mark.small_vm + def test_changed_blocks_tracking(self, host: Host, ext_sr: SR, vdi_on_ext_sr: VDI, vm_on_ext_sr: VM) -> None: + self._test_changed_blocks_tracking(host, ext_sr, vdi_on_ext_sr, vm_on_ext_sr) + + @pytest.mark.small_vm + def test_cbt_after_coalesce(self, host: Host, ext_sr: SR, vdi_on_ext_sr: VDI, vm_on_ext_sr: VM) -> None: + self._test_cbt_after_coalesce(host, ext_sr, vdi_on_ext_sr, vm_on_ext_sr) + + @pytest.mark.small_vm + def test_incremental_snap_scenario(self, host: Host, ext_sr: SR, vdi_on_ext_sr: VDI, vm_on_ext_sr: VM) -> None: + self._test_incremental_snap_scenario(host, ext_sr, vdi_on_ext_sr, vm_on_ext_sr) + + def test_disable_cbt_removes_log(self, host: Host, ext_sr: SR, vdi_on_ext_sr: VDI) -> None: + self._test_disable_cbt_removes_log(host, ext_sr, vdi_on_ext_sr) + + def test_destroy_vdi_removes_cbt_log(self, host: Host, ext_sr: SR, vdi_on_ext_sr: VDI) -> None: + self._test_destroy_vdi_removes_cbt_log(host, ext_sr, vdi_on_ext_sr) + + def test_cbt_persist_after_sr_reboot(self, host: Host, ext_sr: SR, vdi_on_ext_sr: VDI) -> None: + self._test_cbt_persist_after_sr_reboot(host, ext_sr, vdi_on_ext_sr) + + def test_cbt_on_snapshot_chain(self, host: Host, ext_sr: SR, vdi_on_ext_sr: VDI) -> None: + self._test_cbt_on_snapshot_chain(host, ext_sr, vdi_on_ext_sr) + + def test_cbt_parent_disable_does_not_affect_snapshot(self, host: Host, ext_sr: SR, vdi_on_ext_sr: VDI) -> None: + self._test_cbt_parent_disable_does_not_affect_snapshot(host, ext_sr, vdi_on_ext_sr) + + @pytest.mark.small_vm + def test_cbt_bitmap_non_zero_after_write(self, host: Host, ext_sr: SR, vdi_on_ext_sr: VDI, + vm_on_ext_sr: VM) -> None: + self._test_cbt_bitmap_non_zero_after_write(host, ext_sr, vdi_on_ext_sr, vm_on_ext_sr) + + def test_cbt_data_destroy(self, host: Host, ext_sr: SR, vdi_on_ext_sr: VDI) -> None: + self._test_cbt_data_destroy(host, ext_sr, vdi_on_ext_sr) From 8a1bf0295272aea65bd33e6f244fca1c60958967 Mon Sep 17 00:00:00 2001 From: Goulven Riou Date: Wed, 20 May 2026 10:36:36 +0200 Subject: [PATCH 3/8] implement cbt_tests on LVMoISCSI sr Signed-off-by: Goulven Riou --- tests/storage/lvmoiscsi/test_lvmoiscsi_sr.py | 65 +++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/tests/storage/lvmoiscsi/test_lvmoiscsi_sr.py b/tests/storage/lvmoiscsi/test_lvmoiscsi_sr.py index ef7e09fc2..4298115e4 100644 --- a/tests/storage/lvmoiscsi/test_lvmoiscsi_sr.py +++ b/tests/storage/lvmoiscsi/test_lvmoiscsi_sr.py @@ -4,13 +4,16 @@ from lib.common import Defer, vm_image, wait_for from lib.host import Host from lib.sr import SR -from lib.vdi import VDI, ImageFormat +from lib.vdi import VDI from lib.vm import VM from tests.storage import ( MAX_VDI_SIZE, + CBTTest, CoalesceOperation, ImageFormat, XVACompression, + assert_cbt_log_does_not_exist_lvm_sr, + assert_cbt_log_exists_lvm_sr, coalesce_integrity, full_vdi_write, vdi_export_import, @@ -95,3 +98,63 @@ def test_reboot(self, host: Host, lvmoiscsi_sr: SR, vm_on_lvmoiscsi_sr: VM) -> N vm.shutdown(verify=True) # *** End of tests with reboots + + +class TestLVMoISCSICBT(CBTTest): + """Test CBT functionality on LVMOISCSI SR""" + + @staticmethod + def assert_cbt_log_exists(host: Host, sr: SR, vdi: VDI) -> None: + assert_cbt_log_exists_lvm_sr(host, sr, vdi) + + @staticmethod + def assert_cbt_log_does_not_exist(host: Host, sr: SR, vdi: VDI) -> None: + assert_cbt_log_does_not_exist_lvm_sr(host, sr, vdi) + + def test_enable_disable_cbt(self, host: Host, lvmoiscsi_sr: SR, vdi_on_lvmoiscsi_sr: VDI) -> None: + self._test_enable_disable_cbt(host, lvmoiscsi_sr, vdi_on_lvmoiscsi_sr) + + def test_cbt_log_creation(self, host: Host, lvmoiscsi_sr: SR, vdi_on_lvmoiscsi_sr: VDI) -> None: + self._test_cbt_log_creation(host, lvmoiscsi_sr, vdi_on_lvmoiscsi_sr) + + def test_snapshot_with_cbt(self, host: Host, lvmoiscsi_sr: SR, vdi_on_lvmoiscsi_sr: VDI) -> None: + self._test_snapshot_with_cbt(host, lvmoiscsi_sr, vdi_on_lvmoiscsi_sr) + + @pytest.mark.small_vm + def test_changed_blocks_tracking(self, host: Host, lvmoiscsi_sr: SR, vdi_on_lvmoiscsi_sr: VDI, + vm_on_lvmoiscsi_sr: VM) -> None: + self._test_changed_blocks_tracking(host, lvmoiscsi_sr, vdi_on_lvmoiscsi_sr, vm_on_lvmoiscsi_sr) + + @pytest.mark.small_vm + def test_cbt_after_coalesce(self, host: Host, lvmoiscsi_sr: SR, vdi_on_lvmoiscsi_sr: VDI, + vm_on_lvmoiscsi_sr: VM) -> None: + self._test_cbt_after_coalesce(host, lvmoiscsi_sr, vdi_on_lvmoiscsi_sr, vm_on_lvmoiscsi_sr) + + @pytest.mark.small_vm + def test_incremental_snap_scenario(self, host: Host, lvmoiscsi_sr: SR, vdi_on_lvmoiscsi_sr: VDI, + vm_on_lvmoiscsi_sr: VM) -> None: + self._test_incremental_snap_scenario(host, lvmoiscsi_sr, vdi_on_lvmoiscsi_sr, vm_on_lvmoiscsi_sr) + + def test_disable_cbt_removes_log(self, host: Host, lvmoiscsi_sr: SR, vdi_on_lvmoiscsi_sr: VDI) -> None: + self._test_disable_cbt_removes_log(host, lvmoiscsi_sr, vdi_on_lvmoiscsi_sr) + + def test_destroy_vdi_removes_cbt_log(self, host: Host, lvmoiscsi_sr: SR, vdi_on_lvmoiscsi_sr: VDI) -> None: + self._test_destroy_vdi_removes_cbt_log(host, lvmoiscsi_sr, vdi_on_lvmoiscsi_sr) + + def test_cbt_persist_after_sr_reboot(self, host: Host, lvmoiscsi_sr: SR, vdi_on_lvmoiscsi_sr: VDI) -> None: + self._test_cbt_persist_after_sr_reboot(host, lvmoiscsi_sr, vdi_on_lvmoiscsi_sr) + + def test_cbt_on_snapshot_chain(self, host: Host, lvmoiscsi_sr: SR, vdi_on_lvmoiscsi_sr: VDI) -> None: + self._test_cbt_on_snapshot_chain(host, lvmoiscsi_sr, vdi_on_lvmoiscsi_sr) + + def test_cbt_parent_disable_does_not_affect_snapshot(self, host: Host, lvmoiscsi_sr: SR, + vdi_on_lvmoiscsi_sr: VDI) -> None: + self._test_cbt_parent_disable_does_not_affect_snapshot(host, lvmoiscsi_sr, vdi_on_lvmoiscsi_sr) + + @pytest.mark.small_vm + def test_cbt_bitmap_non_zero_after_write(self, host: Host, lvmoiscsi_sr: SR, vdi_on_lvmoiscsi_sr: VDI, + vm_on_lvmoiscsi_sr: VM) -> None: + self._test_cbt_bitmap_non_zero_after_write(host, lvmoiscsi_sr, vdi_on_lvmoiscsi_sr, vm_on_lvmoiscsi_sr) + + def test_cbt_data_destroy(self, host: Host, lvmoiscsi_sr: SR, vdi_on_lvmoiscsi_sr: VDI) -> None: + self._test_cbt_data_destroy(host, lvmoiscsi_sr, vdi_on_lvmoiscsi_sr) From 28b673a9737263ce611378e1a9980f341112d76f Mon Sep 17 00:00:00 2001 From: Goulven Riou Date: Wed, 20 May 2026 09:59:50 +0200 Subject: [PATCH 4/8] implement cbt_tests on lvmSR Signed-off-by: Goulven Riou --- tests/storage/lvm/test_lvm_sr.py | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/storage/lvm/test_lvm_sr.py b/tests/storage/lvm/test_lvm_sr.py index 86903c6ea..39775cca3 100644 --- a/tests/storage/lvm/test_lvm_sr.py +++ b/tests/storage/lvm/test_lvm_sr.py @@ -13,9 +13,12 @@ from lib.vm import VM from tests.storage import ( MAX_VDI_SIZE, + CBTTest, CoalesceOperation, ImageFormat, XVACompression, + assert_cbt_log_does_not_exist_lvm_sr, + assert_cbt_log_exists_lvm_sr, coalesce_integrity, full_vdi_write, try_to_create_sr_with_missing_device, @@ -161,3 +164,59 @@ def test_reboot(self, host: Host, lvm_sr: SR, vm_on_lvm_sr: VM) -> None: vm.shutdown(verify=True) # *** End of tests with reboots + + +class TestLVMCBT(CBTTest): + """Test CBT functionality on LVM SR""" + + @staticmethod + def assert_cbt_log_exists(host: Host, sr: SR, vdi: VDI) -> None: + assert_cbt_log_exists_lvm_sr(host, sr, vdi) + + @staticmethod + def assert_cbt_log_does_not_exist(host: Host, sr: SR, vdi: VDI) -> None: + assert_cbt_log_does_not_exist_lvm_sr(host, sr, vdi) + + def test_enable_disable_cbt(self, host: Host, lvm_sr: SR, vdi_on_lvm_sr: VDI) -> None: + self._test_enable_disable_cbt(host, lvm_sr, vdi_on_lvm_sr) + + def test_cbt_log_creation(self, host: Host, lvm_sr: SR, vdi_on_lvm_sr: VDI) -> None: + self._test_cbt_log_creation(host, lvm_sr, vdi_on_lvm_sr) + + def test_snapshot_with_cbt(self, host: Host, lvm_sr: SR, vdi_on_lvm_sr: VDI) -> None: + self._test_snapshot_with_cbt(host, lvm_sr, vdi_on_lvm_sr) + + @pytest.mark.small_vm + def test_changed_blocks_tracking(self, host: Host, lvm_sr: SR, vdi_on_lvm_sr: VDI, vm_on_lvm_sr: VM) -> None: + self._test_changed_blocks_tracking(host, lvm_sr, vdi_on_lvm_sr, vm_on_lvm_sr) + + @pytest.mark.small_vm + def test_cbt_after_coalesce(self, host: Host, lvm_sr: SR, vdi_on_lvm_sr: VDI, vm_on_lvm_sr: VM) -> None: + self._test_cbt_after_coalesce(host, lvm_sr, vdi_on_lvm_sr, vm_on_lvm_sr) + + @pytest.mark.small_vm + def test_incremental_snap_scenario(self, host: Host, lvm_sr: SR, vdi_on_lvm_sr: VDI, vm_on_lvm_sr: VM) -> None: + self._test_incremental_snap_scenario(host, lvm_sr, vdi_on_lvm_sr, vm_on_lvm_sr) + + def test_disable_cbt_removes_log(self, host: Host, lvm_sr: SR, vdi_on_lvm_sr: VDI) -> None: + self._test_disable_cbt_removes_log(host, lvm_sr, vdi_on_lvm_sr) + + def test_destroy_vdi_removes_cbt_log(self, host: Host, lvm_sr: SR, vdi_on_lvm_sr: VDI) -> None: + self._test_destroy_vdi_removes_cbt_log(host, lvm_sr, vdi_on_lvm_sr) + + def test_cbt_persist_after_sr_reboot(self, host: Host, lvm_sr: SR, vdi_on_lvm_sr: VDI) -> None: + self._test_cbt_persist_after_sr_reboot(host, lvm_sr, vdi_on_lvm_sr) + + def test_cbt_on_snapshot_chain(self, host: Host, lvm_sr: SR, vdi_on_lvm_sr: VDI) -> None: + self._test_cbt_on_snapshot_chain(host, lvm_sr, vdi_on_lvm_sr) + + def test_cbt_parent_disable_does_not_affect_snapshot(self, host: Host, lvm_sr: SR, vdi_on_lvm_sr: VDI) -> None: + self._test_cbt_parent_disable_does_not_affect_snapshot(host, lvm_sr, vdi_on_lvm_sr) + + @pytest.mark.small_vm + def test_cbt_bitmap_non_zero_after_write(self, host: Host, lvm_sr: SR, vdi_on_lvm_sr: VDI, + vm_on_lvm_sr: VM) -> None: + self._test_cbt_bitmap_non_zero_after_write(host, lvm_sr, vdi_on_lvm_sr, vm_on_lvm_sr) + + def test_cbt_data_destroy(self, host: Host, lvm_sr: SR, vdi_on_lvm_sr: VDI) -> None: + self._test_cbt_data_destroy(host, lvm_sr, vdi_on_lvm_sr) From 2eb3d4ea860a62172b1c43b9d3f733c04d2eb132 Mon Sep 17 00:00:00 2001 From: Goulven Riou Date: Tue, 9 Jun 2026 14:11:50 +0200 Subject: [PATCH 5/8] implement cbt_tests on lvmohba sr Signed-off-by: Goulven Riou --- tests/storage/lvmohba/test_lvmohba_sr.py | 66 +++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/tests/storage/lvmohba/test_lvmohba_sr.py b/tests/storage/lvmohba/test_lvmohba_sr.py index eb0a8da05..a708ba517 100644 --- a/tests/storage/lvmohba/test_lvmohba_sr.py +++ b/tests/storage/lvmohba/test_lvmohba_sr.py @@ -2,14 +2,18 @@ from lib.commands import SSHCommandFailed from lib.common import Defer, vm_image, wait_for +from lib.host import Host from lib.sr import SR -from lib.vdi import VDI, ImageFormat +from lib.vdi import VDI from lib.vm import VM from tests.storage import ( MAX_VDI_SIZE, + CBTTest, CoalesceOperation, ImageFormat, XVACompression, + assert_cbt_log_does_not_exist_lvm_sr, + assert_cbt_log_exists_lvm_sr, coalesce_integrity, full_vdi_write, vdi_export_import, @@ -93,3 +97,63 @@ def test_reboot(self, host, lvmohba_sr, vm_on_lvmohba_sr): vm.shutdown(verify=True) # *** End of tests with reboots + + +class TestLVMoHBACBT(CBTTest): + """Test CBT functionality on LVMOHBA SR""" + + @staticmethod + def assert_cbt_log_exists(host: Host, sr: SR, vdi: VDI) -> None: + assert_cbt_log_exists_lvm_sr(host, sr, vdi) + + @staticmethod + def assert_cbt_log_does_not_exist(host: Host, sr: SR, vdi: VDI) -> None: + assert_cbt_log_does_not_exist_lvm_sr(host, sr, vdi) + + def test_enable_disable_cbt(self, host: Host, lvmohba_sr: SR, vdi_on_lvmohba_sr: VDI) -> None: + self._test_enable_disable_cbt(host, lvmohba_sr, vdi_on_lvmohba_sr) + + def test_cbt_log_creation(self, host: Host, lvmohba_sr: SR, vdi_on_lvmohba_sr: VDI) -> None: + self._test_cbt_log_creation(host, lvmohba_sr, vdi_on_lvmohba_sr) + + def test_snapshot_with_cbt(self, host: Host, lvmohba_sr: SR, vdi_on_lvmohba_sr: VDI) -> None: + self._test_snapshot_with_cbt(host, lvmohba_sr, vdi_on_lvmohba_sr) + + @pytest.mark.small_vm + def test_changed_blocks_tracking(self, host: Host, lvmohba_sr: SR, vdi_on_lvmohba_sr: VDI, + vm_on_lvmohba_sr: VM) -> None: + self._test_changed_blocks_tracking(host, lvmohba_sr, vdi_on_lvmohba_sr, vm_on_lvmohba_sr) + + @pytest.mark.small_vm + def test_cbt_after_coalesce(self, host: Host, lvmohba_sr: SR, vdi_on_lvmohba_sr: VDI, + vm_on_lvmohba_sr: VM) -> None: + self._test_cbt_after_coalesce(host, lvmohba_sr, vdi_on_lvmohba_sr, vm_on_lvmohba_sr) + + @pytest.mark.small_vm + def test_incremental_snap_scenario(self, host: Host, lvmohba_sr: SR, vdi_on_lvmohba_sr: VDI, + vm_on_lvmohba_sr: VM) -> None: + self._test_incremental_snap_scenario(host, lvmohba_sr, vdi_on_lvmohba_sr, vm_on_lvmohba_sr) + + def test_disable_cbt_removes_log(self, host: Host, lvmohba_sr: SR, vdi_on_lvmohba_sr: VDI) -> None: + self._test_disable_cbt_removes_log(host, lvmohba_sr, vdi_on_lvmohba_sr) + + def test_destroy_vdi_removes_cbt_log(self, host: Host, lvmohba_sr: SR, vdi_on_lvmohba_sr: VDI) -> None: + self._test_destroy_vdi_removes_cbt_log(host, lvmohba_sr, vdi_on_lvmohba_sr) + + def test_cbt_persist_after_sr_reboot(self, host: Host, lvmohba_sr: SR, vdi_on_lvmohba_sr: VDI) -> None: + self._test_cbt_persist_after_sr_reboot(host, lvmohba_sr, vdi_on_lvmohba_sr) + + def test_cbt_on_snapshot_chain(self, host: Host, lvmohba_sr: SR, vdi_on_lvmohba_sr: VDI) -> None: + self._test_cbt_on_snapshot_chain(host, lvmohba_sr, vdi_on_lvmohba_sr) + + def test_cbt_parent_disable_does_not_affect_snapshot(self, host: Host, lvmohba_sr: SR, + vdi_on_lvmohba_sr: VDI) -> None: + self._test_cbt_parent_disable_does_not_affect_snapshot(host, lvmohba_sr, vdi_on_lvmohba_sr) + + @pytest.mark.small_vm + def test_cbt_bitmap_non_zero_after_write(self, host: Host, lvmohba_sr: SR, vdi_on_lvmohba_sr: VDI, + vm_on_lvmohba_sr: VM) -> None: + self._test_cbt_bitmap_non_zero_after_write(host, lvmohba_sr, vdi_on_lvmohba_sr, vm_on_lvmohba_sr) + + def test_cbt_data_destroy(self, host: Host, lvmohba_sr: SR, vdi_on_lvmohba_sr: VDI) -> None: + self._test_cbt_data_destroy(host, lvmohba_sr, vdi_on_lvmohba_sr) From 51abf89d1d8c030ae9353d72d91c1feb81912e3c Mon Sep 17 00:00:00 2001 From: Goulven Riou Date: Thu, 18 Jun 2026 11:46:12 +0200 Subject: [PATCH 6/8] implement cbt tests on XFSSR Signed-off-by: Goulven Riou --- tests/storage/xfs/test_xfs_sr.py | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/storage/xfs/test_xfs_sr.py b/tests/storage/xfs/test_xfs_sr.py index 5fb48645b..a13bbbdd3 100644 --- a/tests/storage/xfs/test_xfs_sr.py +++ b/tests/storage/xfs/test_xfs_sr.py @@ -13,9 +13,12 @@ from lib.vm import VM from tests.storage import ( MAX_VDI_SIZE, + CBTTest, CoalesceOperation, ImageFormat, XVACompression, + assert_cbt_log_does_not_exist_file_sr, + assert_cbt_log_exists_file_sr, coalesce_integrity, full_vdi_write, vdi_export_import, @@ -129,3 +132,59 @@ def test_xfsprogs_missing(self, host: Host, xfs_sr: SR) -> None: host.yum_install(['xfsprogs']) # *** End of tests with reboots + + +class TestXFSCBT(CBTTest): + """Test CBT functionality on XFS SR""" + + @staticmethod + def assert_cbt_log_exists(host: Host, sr: SR, vdi: VDI) -> None: + assert_cbt_log_exists_file_sr(host, sr, vdi) + + @staticmethod + def assert_cbt_log_does_not_exist(host: Host, sr: SR, vdi: VDI) -> None: + assert_cbt_log_does_not_exist_file_sr(host, sr, vdi) + + def test_enable_disable_cbt(self, host: Host, xfs_sr: SR, vdi_on_xfs_sr: VDI) -> None: + self._test_enable_disable_cbt(host, xfs_sr, vdi_on_xfs_sr) + + def test_cbt_log_creation(self, host: Host, xfs_sr: SR, vdi_on_xfs_sr: VDI) -> None: + self._test_cbt_log_creation(host, xfs_sr, vdi_on_xfs_sr) + + def test_snapshot_with_cbt(self, host: Host, xfs_sr: SR, vdi_on_xfs_sr: VDI) -> None: + self._test_snapshot_with_cbt(host, xfs_sr, vdi_on_xfs_sr) + + @pytest.mark.small_vm + def test_changed_blocks_tracking(self, host: Host, xfs_sr: SR, vdi_on_xfs_sr: VDI, vm_on_xfs_sr: VM) -> None: + self._test_changed_blocks_tracking(host, xfs_sr, vdi_on_xfs_sr, vm_on_xfs_sr) + + @pytest.mark.small_vm + def test_cbt_after_coalesce(self, host: Host, xfs_sr: SR, vdi_on_xfs_sr: VDI, vm_on_xfs_sr: VM) -> None: + self._test_cbt_after_coalesce(host, xfs_sr, vdi_on_xfs_sr, vm_on_xfs_sr) + + @pytest.mark.small_vm + def test_incremental_snap_scenario(self, host: Host, xfs_sr: SR, vdi_on_xfs_sr: VDI, vm_on_xfs_sr: VM) -> None: + self._test_incremental_snap_scenario(host, xfs_sr, vdi_on_xfs_sr, vm_on_xfs_sr) + + def test_disable_cbt_removes_log(self, host: Host, xfs_sr: SR, vdi_on_xfs_sr: VDI) -> None: + self._test_disable_cbt_removes_log(host, xfs_sr, vdi_on_xfs_sr) + + def test_destroy_vdi_removes_cbt_log(self, host: Host, xfs_sr: SR, vdi_on_xfs_sr: VDI) -> None: + self._test_destroy_vdi_removes_cbt_log(host, xfs_sr, vdi_on_xfs_sr) + + def test_cbt_persist_after_sr_reboot(self, host: Host, xfs_sr: SR, vdi_on_xfs_sr: VDI) -> None: + self._test_cbt_persist_after_sr_reboot(host, xfs_sr, vdi_on_xfs_sr) + + def test_cbt_on_snapshot_chain(self, host: Host, xfs_sr: SR, vdi_on_xfs_sr: VDI) -> None: + self._test_cbt_on_snapshot_chain(host, xfs_sr, vdi_on_xfs_sr) + + def test_cbt_parent_disable_does_not_affect_snapshot(self, host: Host, xfs_sr: SR, vdi_on_xfs_sr: VDI) -> None: + self._test_cbt_parent_disable_does_not_affect_snapshot(host, xfs_sr, vdi_on_xfs_sr) + + @pytest.mark.small_vm + def test_cbt_bitmap_non_zero_after_write(self, host: Host, xfs_sr: SR, vdi_on_xfs_sr: VDI, + vm_on_xfs_sr: VM) -> None: + self._test_cbt_bitmap_non_zero_after_write(host, xfs_sr, vdi_on_xfs_sr, vm_on_xfs_sr) + + def test_cbt_data_destroy(self, host: Host, xfs_sr: SR, vdi_on_xfs_sr: VDI) -> None: + self._test_cbt_data_destroy(host, xfs_sr, vdi_on_xfs_sr) From afb58db7be53ca90721aef34a390d96acc7de67a Mon Sep 17 00:00:00 2001 From: Goulven Riou Date: Thu, 18 Jun 2026 11:46:12 +0200 Subject: [PATCH 7/8] implement cbt_tests on nfs_sr Signed-off-by: Goulven Riou --- tests/storage/nfs/test_nfs_sr.py | 61 ++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/storage/nfs/test_nfs_sr.py b/tests/storage/nfs/test_nfs_sr.py index 41511ee59..0612d642e 100644 --- a/tests/storage/nfs/test_nfs_sr.py +++ b/tests/storage/nfs/test_nfs_sr.py @@ -11,9 +11,12 @@ from lib.vm import VM from tests.storage import ( MAX_VDI_SIZE, + CBTTest, CoalesceOperation, ImageFormat, XVACompression, + assert_cbt_log_does_not_exist_file_sr, + assert_cbt_log_exists_file_sr, coalesce_integrity, full_vdi_write, vdi_export_import, @@ -25,6 +28,8 @@ # - one XCP-ng host >= 8.0 with an additional unused disk for the SR # Make sure this fixture is called before the parametrized one + + @pytest.mark.usefixtures('image_format') class TestNFSSR: @pytest.mark.quicktest @@ -160,3 +165,59 @@ def test_reboot(self, host: Host, dispatch_nfs: VM) -> None: vm.shutdown(verify=True) # *** End of tests with reboots + + +class TestNFSCBT(CBTTest): + """Test CBT functionality on NFS SR""" + + @staticmethod + def assert_cbt_log_exists(host: Host, sr: SR, vdi: VDI) -> None: + assert_cbt_log_exists_file_sr(host, sr, vdi) + + @staticmethod + def assert_cbt_log_does_not_exist(host: Host, sr: SR, vdi: VDI) -> None: + assert_cbt_log_does_not_exist_file_sr(host, sr, vdi) + + def test_enable_disable_cbt(self, host: Host, nfs_sr: SR, vdi_on_nfs_sr: VDI) -> None: + self._test_enable_disable_cbt(host, nfs_sr, vdi_on_nfs_sr) + + def test_cbt_log_creation(self, host: Host, nfs_sr: SR, vdi_on_nfs_sr: VDI) -> None: + self._test_cbt_log_creation(host, nfs_sr, vdi_on_nfs_sr) + + def test_snapshot_with_cbt(self, host: Host, nfs_sr: SR, vdi_on_nfs_sr: VDI) -> None: + self._test_snapshot_with_cbt(host, nfs_sr, vdi_on_nfs_sr) + + @pytest.mark.small_vm + def test_changed_blocks_tracking(self, host: Host, nfs_sr: SR, vdi_on_nfs_sr: VDI, vm_on_nfs_sr: VM) -> None: + self._test_changed_blocks_tracking(host, nfs_sr, vdi_on_nfs_sr, vm_on_nfs_sr) + + @pytest.mark.small_vm + def test_cbt_after_coalesce(self, host: Host, nfs_sr: SR, vdi_on_nfs_sr: VDI, vm_on_nfs_sr: VM) -> None: + self._test_cbt_after_coalesce(host, nfs_sr, vdi_on_nfs_sr, vm_on_nfs_sr) + + @pytest.mark.small_vm + def test_incremental_snap_scenario(self, host: Host, nfs_sr: SR, vdi_on_nfs_sr: VDI, vm_on_nfs_sr: VM) -> None: + self._test_incremental_snap_scenario(host, nfs_sr, vdi_on_nfs_sr, vm_on_nfs_sr) + + def test_disable_cbt_removes_log(self, host: Host, nfs_sr: SR, vdi_on_nfs_sr: VDI) -> None: + self._test_disable_cbt_removes_log(host, nfs_sr, vdi_on_nfs_sr) + + def test_destroy_vdi_removes_cbt_log(self, host: Host, nfs_sr: SR, vdi_on_nfs_sr: VDI) -> None: + self._test_destroy_vdi_removes_cbt_log(host, nfs_sr, vdi_on_nfs_sr) + + def test_cbt_persist_after_sr_reboot(self, host: Host, nfs_sr: SR, vdi_on_nfs_sr: VDI) -> None: + self._test_cbt_persist_after_sr_reboot(host, nfs_sr, vdi_on_nfs_sr) + + def test_cbt_on_snapshot_chain(self, host: Host, nfs_sr: SR, vdi_on_nfs_sr: VDI) -> None: + self._test_cbt_on_snapshot_chain(host, nfs_sr, vdi_on_nfs_sr) + + def test_cbt_parent_disable_does_not_affect_snapshot(self, host: Host, nfs_sr: SR, vdi_on_nfs_sr: VDI) -> None: + self._test_cbt_parent_disable_does_not_affect_snapshot(host, nfs_sr, vdi_on_nfs_sr) + + @pytest.mark.small_vm + def test_cbt_bitmap_non_zero_after_write(self, host: Host, nfs_sr: SR, vdi_on_nfs_sr: VDI, + vm_on_nfs_sr: VM) -> None: + self._test_cbt_bitmap_non_zero_after_write(host, nfs_sr, vdi_on_nfs_sr, vm_on_nfs_sr) + + def test_cbt_data_destroy(self, host: Host, nfs_sr: SR, vdi_on_nfs_sr: VDI) -> None: + self._test_cbt_data_destroy(host, nfs_sr, vdi_on_nfs_sr) From 9bc337bc472ac399d8caed10ad591920fc53b573 Mon Sep 17 00:00:00 2001 From: Goulven Riou Date: Tue, 9 Jun 2026 13:59:29 +0200 Subject: [PATCH 8/8] implement cbt_tests on ZFSsr Signed-off-by: Goulven Riou --- tests/storage/zfs/test_zfs_sr.py | 61 +++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/tests/storage/zfs/test_zfs_sr.py b/tests/storage/zfs/test_zfs_sr.py index 0dfbe2f84..161da1072 100755 --- a/tests/storage/zfs/test_zfs_sr.py +++ b/tests/storage/zfs/test_zfs_sr.py @@ -13,9 +13,12 @@ from lib.vm import VM from tests.storage import ( MAX_VDI_SIZE, + CBTTest, CoalesceOperation, ImageFormat, XVACompression, + assert_cbt_log_does_not_exist_file_sr, + assert_cbt_log_exists_file_sr, coalesce_integrity, full_vdi_write, vdi_export_import, @@ -82,7 +85,7 @@ def test_invalid_vdi_size(self, zfs_sr: SR, image_format: ImageFormat): @pytest.mark.small_vm @pytest.mark.parametrize("compression", ["none", "gzip", "zstd"]) - def test_xva_export_import(self, vm_on_zfs_sr: VM, compression: XVACompression, temp_large_dir: str, defer: Defer) \ + def test_xva_export_import(self, vm_on_zfs_sr: VM, compression: XVACompression, temp_large_dir: str, defer: Defer)\ -> None: xva_export_import(vm_on_zfs_sr, compression, temp_large_dir, defer) @@ -160,3 +163,59 @@ def test_zfs_unmounted(self, host: Host, zfs_sr: SR) -> None: host.ssh(f'zpool import {POOL_NAME}') # *** End of tests with reboots + + +class TestZFSCBT(CBTTest): + """Test CBT functionality on ZFS SR""" + + @staticmethod + def assert_cbt_log_exists(host: Host, sr: SR, vdi: VDI) -> None: + assert_cbt_log_exists_file_sr(host, sr, vdi) + + @staticmethod + def assert_cbt_log_does_not_exist(host: Host, sr: SR, vdi: VDI) -> None: + assert_cbt_log_does_not_exist_file_sr(host, sr, vdi) + + def test_enable_disable_cbt(self, host: Host, zfs_sr: SR, vdi_on_zfs_sr: VDI) -> None: + self._test_enable_disable_cbt(host, zfs_sr, vdi_on_zfs_sr) + + def test_cbt_log_creation(self, host: Host, zfs_sr: SR, vdi_on_zfs_sr: VDI) -> None: + self._test_cbt_log_creation(host, zfs_sr, vdi_on_zfs_sr) + + def test_snapshot_with_cbt(self, host: Host, zfs_sr: SR, vdi_on_zfs_sr: VDI) -> None: + self._test_snapshot_with_cbt(host, zfs_sr, vdi_on_zfs_sr) + + @pytest.mark.small_vm + def test_changed_blocks_tracking(self, host: Host, zfs_sr: SR, vdi_on_zfs_sr: VDI, vm_on_zfs_sr: VM) -> None: + self._test_changed_blocks_tracking(host, zfs_sr, vdi_on_zfs_sr, vm_on_zfs_sr) + + @pytest.mark.small_vm + def test_cbt_after_coalesce(self, host: Host, zfs_sr: SR, vdi_on_zfs_sr: VDI, vm_on_zfs_sr: VM) -> None: + self._test_cbt_after_coalesce(host, zfs_sr, vdi_on_zfs_sr, vm_on_zfs_sr) + + @pytest.mark.small_vm + def test_incremental_snap_scenario(self, host: Host, zfs_sr: SR, vdi_on_zfs_sr: VDI, vm_on_zfs_sr: VM) -> None: + self._test_incremental_snap_scenario(host, zfs_sr, vdi_on_zfs_sr, vm_on_zfs_sr) + + def test_disable_cbt_removes_log(self, host: Host, zfs_sr: SR, vdi_on_zfs_sr: VDI) -> None: + self._test_disable_cbt_removes_log(host, zfs_sr, vdi_on_zfs_sr) + + def test_destroy_vdi_removes_cbt_log(self, host: Host, zfs_sr: SR, vdi_on_zfs_sr: VDI) -> None: + self._test_destroy_vdi_removes_cbt_log(host, zfs_sr, vdi_on_zfs_sr) + + def test_cbt_persist_after_sr_reboot(self, host: Host, zfs_sr: SR, vdi_on_zfs_sr: VDI) -> None: + self._test_cbt_persist_after_sr_reboot(host, zfs_sr, vdi_on_zfs_sr) + + def test_cbt_on_snapshot_chain(self, host: Host, zfs_sr: SR, vdi_on_zfs_sr: VDI) -> None: + self._test_cbt_on_snapshot_chain(host, zfs_sr, vdi_on_zfs_sr) + + def test_cbt_parent_disable_does_not_affect_snapshot(self, host: Host, zfs_sr: SR, vdi_on_zfs_sr: VDI) -> None: + self._test_cbt_parent_disable_does_not_affect_snapshot(host, zfs_sr, vdi_on_zfs_sr) + + @pytest.mark.small_vm + def test_cbt_bitmap_non_zero_after_write(self, host: Host, zfs_sr: SR, vdi_on_zfs_sr: VDI, + vm_on_zfs_sr: VM) -> None: + self._test_cbt_bitmap_non_zero_after_write(host, zfs_sr, vdi_on_zfs_sr, vm_on_zfs_sr) + + def test_cbt_data_destroy(self, host: Host, zfs_sr: SR, vdi_on_zfs_sr: VDI) -> None: + self._test_cbt_data_destroy(host, zfs_sr, vdi_on_zfs_sr)