From 8cc2648ce7deeae30fca80afe39f0ea9122dfe28 Mon Sep 17 00:00:00 2001 From: J M Date: Mon, 29 Jun 2026 19:50:30 +0800 Subject: [PATCH] [storage]: local PS capacity excludes non-local data disk LocalStorageAllocatorFactory summed root+all data disks against local host capacity, wrongly raising NO_AVAILABLE_HOST in mixed storage where data disks land on NFS. Carry localStorageDiskSize (root + data only when data PS is local) so the local check counts only volumes on that local PS. Test CreateVmDataDiskOnNfsLocalCapacityCase covers it. Resolves: ZSTAC-84163 Change-Id: I3908f006e8aaf04ee9ee57d81fc09fabeda1c197 --- .../zstack/compute/vm/VmAllocateHostFlow.java | 25 ++- .../header/allocator/AllocateHostMsg.java | 9 + .../header/allocator/HostAllocatorSpec.java | 10 ++ .../local/LocalStorageAllocatorFactory.java | 7 +- ...ateVmDataDiskOnNfsLocalCapacityCase.groovy | 157 ++++++++++++++++++ 5 files changed, 201 insertions(+), 7 deletions(-) create mode 100644 test/src/test/groovy/org/zstack/test/integration/storage/primary/local_nfs/allocator/host/CreateVmDataDiskOnNfsLocalCapacityCase.groovy diff --git a/compute/src/main/java/org/zstack/compute/vm/VmAllocateHostFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmAllocateHostFlow.java index 8993b278e9b..aa84360b62f 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmAllocateHostFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmAllocateHostFlow.java @@ -11,6 +11,9 @@ import org.zstack.header.allocator.*; import org.zstack.header.configuration.DiskOfferingInventory; import org.zstack.header.configuration.DiskOfferingVO; +import org.zstack.header.storage.primary.PrimaryStorageConstants; +import org.zstack.header.storage.primary.PrimaryStorageVO; +import org.zstack.header.storage.primary.PrimaryStorageVO_; import org.zstack.header.core.workflow.Flow; import org.zstack.header.core.workflow.FlowRollback; import org.zstack.header.core.workflow.FlowTrigger; @@ -60,25 +63,39 @@ private long getTotalDataDiskSize(VmInstanceSpec spec) { return size; } + private long getDataDiskSizeOnLocalStorage(VmInstanceSpec spec) { + String dataPs = spec.getRequiredPrimaryStorageUuidForDataVolume(); + if (dataPs == null) { + return getTotalDataDiskSize(spec); + } + boolean dataOnLocal = PrimaryStorageConstants.LOCAL_STORAGE_TYPE.equals( + org.zstack.core.db.Q.New(PrimaryStorageVO.class) + .eq(PrimaryStorageVO_.uuid, dataPs) + .select(PrimaryStorageVO_.type) + .findValue()); + return dataOnLocal ? getTotalDataDiskSize(spec) : 0; + } + protected AllocateHostMsg prepareMsg(VmInstanceSpec spec) { DesignatedAllocateHostMsg msg = new DesignatedAllocateHostMsg(); List diskOfferings = new ArrayList<>(); ImageInventory image = spec.getImageSpec().getInventory(); - long diskSize; + long rootDiskSize; if (image == null || (image.getMediaType() != null && image.getMediaType().equals(ImageMediaType.ISO.toString()))) { DiskOfferingVO dvo = dbf.findByUuid(spec.getRootDiskOffering().getUuid(), DiskOfferingVO.class); - diskSize = dvo.getDiskSize(); + rootDiskSize = dvo.getDiskSize(); diskOfferings.add(DiskOfferingInventory.valueOf(dvo)); } else { - diskSize = image.getSize(); + rootDiskSize = image.getSize(); } - diskSize += getTotalDataDiskSize(spec); + long diskSize = rootDiskSize + getTotalDataDiskSize(spec); diskOfferings.addAll(spec.getDataDiskOfferings()); msg.setSoftAvoidHostUuids(spec.getSoftAvoidHostUuids()); msg.setAvoidHostUuids(spec.getAvoidHostUuids()); msg.setDiskOfferings(diskOfferings); msg.setDiskSize(diskSize); + msg.setLocalStorageDiskSize(rootDiskSize + getDataDiskSizeOnLocalStorage(spec)); msg.setCpuCapacity(spec.getVmInventory().getCpuNum()); msg.setMemoryCapacity(spec.getVmInventory().getMemorySize()); msg.setClusterUuids(spec.getRequiredClusterUuids()); diff --git a/header/src/main/java/org/zstack/header/allocator/AllocateHostMsg.java b/header/src/main/java/org/zstack/header/allocator/AllocateHostMsg.java index 132637b8635..ef4fc2edb5c 100755 --- a/header/src/main/java/org/zstack/header/allocator/AllocateHostMsg.java +++ b/header/src/main/java/org/zstack/header/allocator/AllocateHostMsg.java @@ -11,6 +11,7 @@ public class AllocateHostMsg extends NeedReplyMessage { private long cpuCapacity; private long memoryCapacity; private long diskSize; + private long localStorageDiskSize; private String allocatorStrategy; private List avoidHostUuids; private List softAvoidHostUuids; @@ -150,6 +151,14 @@ public void setDiskSize(long diskSize) { this.diskSize = diskSize; } + public long getLocalStorageDiskSize() { + return localStorageDiskSize; + } + + public void setLocalStorageDiskSize(long localStorageDiskSize) { + this.localStorageDiskSize = localStorageDiskSize; + } + public String getAllocatorStrategy() { return allocatorStrategy; } diff --git a/header/src/main/java/org/zstack/header/allocator/HostAllocatorSpec.java b/header/src/main/java/org/zstack/header/allocator/HostAllocatorSpec.java index 33d39896c82..9932a17753a 100755 --- a/header/src/main/java/org/zstack/header/allocator/HostAllocatorSpec.java +++ b/header/src/main/java/org/zstack/header/allocator/HostAllocatorSpec.java @@ -15,6 +15,7 @@ public class HostAllocatorSpec { private long memoryCapacity; private List l3NetworkUuids; private long diskSize; + private long localStorageDiskSize; private String hypervisorType; private String allocatorStrategy; private VmInstanceInventory vmInstance; @@ -199,6 +200,14 @@ public void setDiskSize(long diskSize) { this.diskSize = diskSize; } + public long getLocalStorageDiskSize() { + return localStorageDiskSize > 0 ? localStorageDiskSize : diskSize; + } + + public void setLocalStorageDiskSize(long localStorageDiskSize) { + this.localStorageDiskSize = localStorageDiskSize; + } + public String getHypervisorType() { return hypervisorType; } @@ -262,6 +271,7 @@ public static HostAllocatorSpec fromAllocationMsg(AllocateHostMsg msg) { spec.setSoftAvoidHostUuids(msg.getSoftAvoidHostUuids()); spec.setCpuCapacity(msg.getCpuCapacity()); spec.setDiskSize(msg.getDiskSize()); + spec.setLocalStorageDiskSize(msg.getLocalStorageDiskSize()); spec.setListAllHosts(msg.isListAllHosts()); spec.setDryRun(msg.isDryRun()); spec.setFullAllocate(msg.isFullAllocate()); diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageAllocatorFactory.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageAllocatorFactory.java index 785b4092144..81af413402e 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageAllocatorFactory.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageAllocatorFactory.java @@ -122,6 +122,7 @@ public List filterHostCandidates(List candidates, HostAllocatorS } List refs = q.list(); + long requiredSize = spec.getLocalStorageDiskSize(); final Set toRemoveHuuids = new HashSet<>(); final Set toAddHuuids = new HashSet<>(); for (LocalStorageHostRefVO ref : refs) { @@ -130,7 +131,7 @@ public List filterHostCandidates(List candidates, HostAllocatorS // check primary storage capacity and host physical capacity boolean capacityChecked = PrimaryStorageCapacityChecker.New(psUuid, ref.getAvailableCapacity(), ref.getTotalPhysicalCapacity(), ref.getAvailablePhysicalCapacity()) - .checkRequiredSize(spec.getDiskSize()); + .checkRequiredSize(requiredSize); if (!capacityChecked) { addHostPrimaryStorageBlacklist(huuid, psUuid, spec); @@ -143,7 +144,7 @@ public List filterHostCandidates(List candidates, HostAllocatorS toRemoveHuuids.removeAll(toAddHuuids); if (!toRemoveHuuids.isEmpty()) { logger.debug(String.format("local storage filters out hosts%s, because they don't have required disk capacity[%s bytes]", - toRemoveHuuids, spec.getDiskSize())); + toRemoveHuuids, requiredSize)); candidates = CollectionUtils.transformToList(candidates, new Function() { @Override @@ -155,7 +156,7 @@ public HostVO call(HostVO arg) { if (candidates.isEmpty()) { throw new OperationFailureException(err(ORG_ZSTACK_STORAGE_PRIMARY_LOCAL_10020, HostAllocatorError.NO_AVAILABLE_HOST, "the local primary storage has no hosts with enough disk capacity[%s bytes] required by the vm[uuid:%s]", - spec.getDiskSize(), spec.getVmInstance().getUuid() + requiredSize, spec.getVmInstance().getUuid() )); } } diff --git a/test/src/test/groovy/org/zstack/test/integration/storage/primary/local_nfs/allocator/host/CreateVmDataDiskOnNfsLocalCapacityCase.groovy b/test/src/test/groovy/org/zstack/test/integration/storage/primary/local_nfs/allocator/host/CreateVmDataDiskOnNfsLocalCapacityCase.groovy new file mode 100644 index 00000000000..f726819b244 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/storage/primary/local_nfs/allocator/host/CreateVmDataDiskOnNfsLocalCapacityCase.groovy @@ -0,0 +1,157 @@ +package org.zstack.test.integration.storage.primary.local_nfs.allocator.host + +import org.zstack.compute.vm.VmSystemTags +import org.zstack.sdk.* +import org.zstack.test.integration.storage.StorageTest +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.SubCase +import org.zstack.utils.data.SizeUnit + +/** + * Regression: mixed storage. Root volume on local PS (fits), data volumes on NFS. + * Local PS host capacity only fits root; LocalStorageAllocatorFactory must NOT + * sum root+data (200G) against local host ref, otherwise NO_AVAILABLE_HOST is + * wrongly raised. See "no hosts with enough disk capacity[214748364800 bytes]". + */ +class CreateVmDataDiskOnNfsLocalCapacityCase extends SubCase { + EnvSpec env + + @Override + void setup() { + useSpring(StorageTest.springSpec) + } + + @Override + void environment() { + env = env { + instanceOffering { + name = "instanceOffering" + memory = SizeUnit.GIGABYTE.toByte(1) + cpu = 1 + } + + diskOffering { + name = "rootDisk" + diskSize = SizeUnit.GIGABYTE.toByte(40) + } + + diskOffering { + name = "dataDisk" + diskSize = SizeUnit.GIGABYTE.toByte(80) + } + + sftpBackupStorage { + name = "sftp" + url = "/sftp" + username = "root" + password = "password" + hostname = "localhost" + + image { + name = "image" + url = "http://zstack.org/download/test.qcow2" + format = "raw" + mediaType = "ISO" + size = 0 + } + } + + zone { + name = "zone" + description = "test" + + cluster { + name = "cluster" + hypervisorType = "KVM" + + kvm { + name = "kvm" + managementIp = "localhost" + username = "root" + password = "password" + totalCpu = 88 + totalMem = SizeUnit.GIGABYTE.toByte(100) + } + + attachPrimaryStorage("local") + attachPrimaryStorage("nfs") + attachL2Network("l2") + } + + nfsPrimaryStorage { + name = "nfs" + url = "172.20.0.1:/nfs_root" + totalCapacity = SizeUnit.GIGABYTE.toByte(1000) + availableCapacity = SizeUnit.GIGABYTE.toByte(1000) + } + + localPrimaryStorage { + name = "local" + url = "/local_ps" + totalCapacity = SizeUnit.GIGABYTE.toByte(50) + availableCapacity = SizeUnit.GIGABYTE.toByte(50) + } + + l2NoVlanNetwork { + name = "l2" + physicalInterface = "eth0" + + l3Network { + name = "l3" + + ip { + startIp = "12.16.10.10" + endIp = "12.16.10.100" + netmask = "255.255.255.0" + gateway = "12.16.10.1" + } + } + } + + attachBackupStorage("sftp") + } + } + } + + @Override + void test() { + env.create { + testCreateVmRootLocalDataNfs() + } + } + + @Override + void clean() { + env.delete() + } + + void testCreateVmRootLocalDataNfs() { + InstanceOfferingInventory instanceOffering = env.inventoryByName("instanceOffering") as InstanceOfferingInventory + DiskOfferingInventory rootDisk = env.inventoryByName("rootDisk") as DiskOfferingInventory + DiskOfferingInventory dataDisk = env.inventoryByName("dataDisk") as DiskOfferingInventory + ImageInventory image = env.inventoryByName("image") as ImageInventory + L3NetworkInventory l3 = env.inventoryByName("l3") as L3NetworkInventory + HostInventory host = env.inventoryByName("kvm") + PrimaryStorageInventory nfs = env.inventoryByName("nfs") + PrimaryStorageInventory local = env.inventoryByName("local") + + CreateVmInstanceAction a = new CreateVmInstanceAction( + name: "mixed", + instanceOfferingUuid: instanceOffering.uuid, + imageUuid: image.uuid, + l3NetworkUuids: [l3.uuid], + hostUuid: host.uuid, + rootDiskOfferingUuid: rootDisk.uuid, + dataDiskOfferingUuids: [dataDisk.uuid, dataDisk.uuid], + primaryStorageUuidForRootVolume: local.uuid, + systemTags: [VmSystemTags.PRIMARY_STORAGE_UUID_FOR_DATA_VOLUME.instantiateTag( + [(VmSystemTags.PRIMARY_STORAGE_UUID_FOR_DATA_VOLUME_TOKEN): nfs.uuid])], + sessionId: currentEnvSpec.session.uuid + ) + + CreateVmInstanceAction.Result r = a.call() + assert r.error == null: + "root(40G) on local(50G), data(160G) on nfs: local capacity check wrongly summed root+data " + + "and raised NO_AVAILABLE_HOST. error=${r.error}" + } +}