From fd36fcd1d66bc16d4339c25b03aa90b473ff88ce Mon Sep 17 00:00:00 2001 From: J M Date: Mon, 1 Jun 2026 17:35:12 +0800 Subject: [PATCH] [zbs]: fix linked-clone parent volume residual stats()/flatten() returned parentUri, and batchStats() returned the volume install path, as the agent cbd path while the rest of the code expects zbs:// paths. The snapshot-reference GC fed the cbd parentUri back into stats(); prepareCmd() converted it again and the backing chain query failed with ZBS_10026, rolling back the cleanup and leaving the parent volume and snapshot on storage. batchStats() set the VolumeStats install path to the cbd key, so BatchSyncVolumeSize could not map actual size back to the volume uuid. Normalize agent-returned cbd paths to zbs:// so they stay in one path namespace. Resolves: ZSTAC-85707 Change-Id: I1c3dbe96086b681b83a4a627bdce37e76575564f --- .../org/zstack/storage/zbs/ZbsHelper.java | 4 + .../storage/zbs/ZbsStorageController.java | 6 +- .../addon/zbs/ZbsPrimaryStorageCase.groovy | 75 +++++++++++++++++++ 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsHelper.java b/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsHelper.java index 3484be39c20..6808a02bb1e 100644 --- a/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsHelper.java +++ b/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsHelper.java @@ -19,6 +19,10 @@ public static String convertCbdPathToZbsPath(String cbdPath) { return cbdPath.replaceFirst(".+?/", "zbs://"); } + public static String normalizeToZbsPath(String path) { + return path != null && path.startsWith("cbd:") ? convertCbdPathToZbsPath(path) : path; + } + public static String convertZbsPathToCbdPath(String zbsPath, Function physicalPoolGetter) { String logicalPool = getPoolFromVolumePath(zbsPath); String physicalPool = physicalPoolGetter.apply(logicalPool); diff --git a/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageController.java b/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageController.java index 1a7342fc4a4..27810c6bb45 100644 --- a/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageController.java +++ b/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageController.java @@ -1022,7 +1022,7 @@ public void success(FlattenVolumeRsp returnValue) { stats.setSize(returnValue.getSize()); stats.setActualSize(returnValue.getActualSize()); stats.setFormat(VolumeConstant.VOLUME_FORMAT_RAW); - stats.setParentUri(returnValue.getParentUri()); + stats.setParentUri(ZbsHelper.normalizeToZbsPath(returnValue.getParentUri())); comp.success(stats); } @@ -1046,7 +1046,7 @@ public void success(QueryVolumeRsp returnValue) { stats.setSize(returnValue.getSize()); stats.setActualSize(returnValue.getActualSize()); stats.setFormat(VolumeConstant.VOLUME_FORMAT_RAW); - stats.setParentUri(returnValue.getParentUri()); + stats.setParentUri(ZbsHelper.normalizeToZbsPath(returnValue.getParentUri())); comp.success(stats); } @@ -1069,7 +1069,7 @@ public void batchStats(Collection installPaths, ReturnValueCompletion
  • stats = returnValue.getVolumes().entrySet().stream().map(v -> { VolumeStats s = new VolumeStats(); - s.setInstallPath(v.getKey()); + s.setInstallPath(ZbsHelper.normalizeToZbsPath(v.getKey())); s.setSize(v.getValue().get("length")); s.setActualSize(v.getValue().get("usedSize")); s.setFormat(VolumeConstant.VOLUME_FORMAT_RAW); diff --git a/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/zbs/ZbsPrimaryStorageCase.groovy b/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/zbs/ZbsPrimaryStorageCase.groovy index d938f1da8fc..089a8ac2c1c 100644 --- a/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/zbs/ZbsPrimaryStorageCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/zbs/ZbsPrimaryStorageCase.groovy @@ -1,10 +1,17 @@ package org.zstack.test.integration.storage.primary.addon.zbs import org.springframework.http.HttpEntity +import org.zstack.core.cloudbus.CloudBus import org.zstack.core.cloudbus.EventCallback import org.zstack.core.cloudbus.EventFacade import org.zstack.core.db.DatabaseFacade import org.zstack.core.db.Q +import org.zstack.header.message.MessageReply +import org.zstack.header.storage.primary.GetVolumeBackingChainFromPrimaryStorageMsg +import org.zstack.header.storage.primary.GetVolumeBackingChainFromPrimaryStorageReply +import org.zstack.header.storage.primary.PrimaryStorageConstant +import org.zstack.header.volume.BatchSyncVolumeSizeOnPrimaryStorageMsg +import org.zstack.header.volume.BatchSyncVolumeSizeOnPrimaryStorageReply import org.zstack.header.storage.addon.primary.ExternalPrimaryStorageVO import org.zstack.header.storage.addon.primary.ExternalPrimaryStorageSpaceVO import org.zstack.header.storage.addon.primary.ExternalPrimaryStorageVO_ @@ -191,6 +198,8 @@ class ZbsPrimaryStorageCase extends SubCase { testDataVolumeNegativeScenario() testDecodeMdsUriWithSpecialPassword() testMdsReconnectAfterMaximumPingFailures() + testGetBackingChainNormalizesCbdParentUri() + testBatchStatsNormalizesCbdInstallPath() } } @@ -1095,6 +1104,72 @@ class ZbsPrimaryStorageCase extends SubCase { env.cleanAfterSimulatorHandlers() } + void testGetBackingChainNormalizesCbdParentUri() { + String childSnapPath = "zbs://lpool1/volume_child@snapshot_s1" + String parentZbsPath = "zbs://lpool1/volume_parent" + String parentCbdPath = "cbd:pool1/lpool1/volume_parent" + + env.simulator(ZbsStorageController.QUERY_VOLUME_PATH) { HttpEntity e, EnvSpec spec -> + def cmd = JSONObjectUtil.toObject(e.body, ZbsStorageController.QueryVolumeCmd.class) + def rsp = new ZbsStorageController.QueryVolumeRsp() + rsp.size = SizeUnit.GIGABYTE.toByte(8) + rsp.actualSize = SizeUnit.MEGABYTE.toByte(1) + if (cmd.path.contains("volume_child")) { + rsp.parentUri = parentCbdPath + } else { + rsp.parentUri = null + } + return rsp + } + + CloudBus bus = bean(CloudBus.class) + GetVolumeBackingChainFromPrimaryStorageMsg msg = new GetVolumeBackingChainFromPrimaryStorageMsg() + msg.setPrimaryStorageUuid(ps.uuid) + msg.setVolumeUuid(childSnapPath) + msg.setRootInstallPaths([childSnapPath]) + bus.makeTargetServiceIdByResourceUuid(msg, PrimaryStorageConstant.SERVICE_ID, ps.uuid) + MessageReply reply = bus.call(msg) + + assert reply.isSuccess() : "backing chain walk must not fail on cbd-form parentUri: ${reply.error}" + GetVolumeBackingChainFromPrimaryStorageReply r = reply as GetVolumeBackingChainFromPrimaryStorageReply + List chain = r.getBackingChainInstallPath(childSnapPath) + assert chain != null && chain.contains(parentZbsPath) : + "parent must be resolved as a zbs:// path, got ${chain}" + + env.cleanSimulatorHandlers() + } + + void testBatchStatsNormalizesCbdInstallPath() { + String volUuid = "vol85707batch" + String zbsPath = "zbs://lpool1/volume_batch" + long usedSize = SizeUnit.MEGABYTE.toByte(7) + + env.simulator(ZbsStorageController.BATCH_QUERY_VOLUME_PATH) { HttpEntity e, EnvSpec spec -> + def cmd = JSONObjectUtil.toObject(e.body, ZbsStorageController.BatchQueryVolumeCmd.class) + def rsp = new ZbsStorageController.BatchQueryVolumeRsp() + Map> volumes = new HashMap<>() + cmd.installPaths.each { cbd -> + volumes.put(cbd, ["length": SizeUnit.GIGABYTE.toByte(8), "usedSize": usedSize]) + } + rsp.setVolumes(volumes) + return rsp + } + + CloudBus bus = bean(CloudBus.class) + BatchSyncVolumeSizeOnPrimaryStorageMsg msg = new BatchSyncVolumeSizeOnPrimaryStorageMsg() + msg.setPrimaryStorageUuid(ps.uuid) + msg.setVolumeUuidInstallPaths([(volUuid): zbsPath]) + bus.makeTargetServiceIdByResourceUuid(msg, PrimaryStorageConstant.SERVICE_ID, ps.uuid) + MessageReply reply = bus.call(msg) + + assert reply.isSuccess() : "batch size sync must not fail on cbd-form install path: ${reply.error}" + BatchSyncVolumeSizeOnPrimaryStorageReply r = reply as BatchSyncVolumeSizeOnPrimaryStorageReply + assert r.actualSizes.get(volUuid) == usedSize : + "actualSize must map back to uuid via zbs:// install path, got ${r.actualSizes}" + + env.cleanSimulatorHandlers() + } + void deleteVolume(String volUuid) { deleteDataVolume { uuid = volUuid