From 715b1b3ad530f6a8e849ad7a3f00c99fd64077b2 Mon Sep 17 00:00:00 2001
From: Mark Syms <mark.syms@cloud.com>
Date: Wed, 19 Mar 2025 09:21:49 +0000
Subject: [PATCH] CA-408452: remove vhd parent if it does not have one

A previous change stopped the cleanup code unilaterally removing the
`vhd-parent` configuration of the lef coalesced VHD which resulted in
failures where VHDs that should have declared a parent no longer did
until a subsequent SR scan restored the configuration value. This is
the alternate mirror case where the leaf coalesce collapses the entire
tree out and so the `vhd-parent` becomes redundant, again until a
subsequent SR scan removes it. To satisfy both use cases remove the
`vhd-parent` property if and only if the coalesced VHD has no parent.

Signed-off-by: Mark Syms <mark.syms@cloud.com>
diff --git a/drivers/cleanup.py b/drivers/cleanup.py
index 13da555..a62d5f1 100755
--- a/drivers/cleanup.py
+++ b/drivers/cleanup.py
@@ -2335,6 +2335,9 @@ class SR:
         vdi.parent.children = []
         vdi.parent = None
 
+        if parent.parent is None:
+            parent.delConfig(VDI.DB_VHD_PARENT)
+
         extraSpace = self._calcExtraSpaceNeeded(vdi, parent)
         freeSpace = self.getFreeSpace()
         if freeSpace < extraSpace:
diff --git a/tests/test_cleanup.py b/tests/test_cleanup.py
index 679b93b..eef170f 100644
--- a/tests/test_cleanup.py
+++ b/tests/test_cleanup.py
@@ -7,6 +7,7 @@ import uuid
 from uuid import uuid4
 
 import cleanup
+import fjournaler
 import lock
 
 import util
@@ -18,6 +19,8 @@ import XenAPI
 from XenAPI import Failure
 from util import SMException
 
+MEGA = 1024 * 1024
+
 
 class FakeFile(object):
     pass
@@ -1883,6 +1886,46 @@ class TestSR(unittest.TestCase):
         # Assert
         self.assertIsNotNone(sr)
 
+    def test_doCoalesceLeaf_no_parent_after(self):
+        sr = create_cleanup_sr(self.xapi_mock)
+        sr.journaler = mock.create_autospec(fjournaler.Journaler)
+        parent_uuid = str(uuid.uuid4())
+        mock_parent = mock.MagicMock()
+        mock_parent.uuid = parent_uuid
+        mock_parent.parent = None
+        mock_parent.raw = False
+        mock_parent.getSizeVHD.return_value = 10 * MEGA
+
+        vdi_uuid = str(uuid.uuid4())
+        mock_vdi = mock.MagicMock()
+        mock_vdi.uuid = vdi_uuid
+        mock_vdi.getSizeVHD.return_value = 10 * MEGA
+        mock_vdi.parent = mock_parent
+
+        sr._doCoalesceLeaf(mock_vdi)
+
+        mock_parent.delConfig.assert_called_with("vhd-parent")
+
+    def test_doCoalesceLeaf_parent_remains_after(self):
+        sr = create_cleanup_sr(self.xapi_mock)
+        sr.journaler = mock.create_autospec(fjournaler.Journaler)
+        parent_uuid = str(uuid.uuid4())
+        mock_parent = mock.MagicMock()
+        mock_parent.uuid = parent_uuid
+        mock_parent.parent = mock.MagicMock()
+        mock_parent.raw = False
+        mock_parent.getSizeVHD.return_value = 10 * MEGA
+
+        vdi_uuid = str(uuid.uuid4())
+        mock_vdi = mock.MagicMock()
+        mock_vdi.uuid = vdi_uuid
+        mock_vdi.getSizeVHD.return_value = 10 * MEGA
+        mock_vdi.parent = mock_parent
+
+        sr._doCoalesceLeaf(mock_vdi)
+
+        self.assertNotIn('vhd-parent', mock_parent.delConfig.call_args)
+
 
 class TestLockGCActive(unittest.TestCase):
 
