Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.zstack.header.volume.VolumeInventory;
import org.zstack.header.volume.VolumeProtocol;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -48,7 +49,7 @@ public String getIdentity() {

@Override
public List<String> getPreferBackupStorageTypes() {
return preferBackupStorageTypes;
return new ArrayList<>(preferBackupStorageTypes);
}

public void setPreferBackupStorageTypes(List<String> preferBackupStorageTypes) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.zstack.header.xinfini.XInfiniConstants;
import org.zstack.storage.addon.primary.ExternalPrimaryStorageFactory;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;

Expand Down Expand Up @@ -55,7 +56,7 @@ public String getIdentity() {

@Override
public List<String> getPreferBackupStorageTypes() {
return preferBackupStorageTypes;
return new ArrayList<>(preferBackupStorageTypes);
}

public void setPreferBackupStorageTypes(List<String> preferBackupStorageTypes) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.zstack.utils.ssh.Ssh;
import org.zstack.utils.ssh.SshResult;

import java.util.ArrayList;
import java.util.List;

import static org.zstack.core.Platform.operr;
Expand Down Expand Up @@ -93,7 +94,7 @@ public void setPreferBackupStorageTypes(List<String> preferBackupStorageTypes) {

@Override
public List<String> getPreferBackupStorageTypes() {
return preferBackupStorageTypes;
return new ArrayList<>(preferBackupStorageTypes);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,42 @@ import org.zstack.header.storage.backup.BackupStorageZoneRefVO_
import org.zstack.header.storage.primary.PrimaryStorageConstant
import org.zstack.header.storage.primary.SelectBackupStorageMsg
import org.zstack.header.storage.primary.SelectBackupStorageReply
import org.zstack.sdk.ClusterInventory
import org.zstack.sdk.ImageInventory
import org.zstack.sdk.InstanceOfferingInventory
import org.zstack.sdk.L3NetworkInventory
import org.zstack.sdk.PrimaryStorageInventory
import org.zstack.sdk.VmInstanceInventory
import org.zstack.sdk.ZoneInventory
import org.zstack.test.integration.storage.StorageTest
import org.zstack.testlib.EnvSpec
import org.zstack.testlib.SubCase
import org.zstack.utils.data.SizeUnit

/**
* ZSTAC-71706: ExternalPrimaryStorage backup storage selection in mixed environment.
* ZSTAC-80789: getPreferBackupStorageTypes() must return a defensive copy.
*
* Bug: List.indexOf() returns -1 for types not in preferBsTypes,
* causing ascending sort to place non-preferred types (e.g. VCenterBackupStorage)
* before preferred types in the sorted result.
* Bug: ZbsStorageFactory/ExponStorageFactory/XInfiniStorageFactory returned a direct
* reference to their internal preferBackupStorageTypes list. When SelectBackupStorageMsg
* carried requiredBackupStorageTypes, the handler called retainAll() on the returned list,
* permanently mutating the bean's internal state. Subsequent requests without
* requiredBackupStorageTypes would then see an empty preferBsTypes and fail.
*
* Fix: Filter out non-preferred backup storage types before sorting by preference.
* Fix: Return new ArrayList<>(preferBackupStorageTypes) from getPreferBackupStorageTypes().
*
* This case sets up a ZBS ExternalPrimaryStorage (preferBsTypes = [ImageStoreBackupStorage])
* with multiple backup storage types attached to the zone, then sends SelectBackupStorageMsg
* via CloudBus to verify the handler selects the correct preferred backup storage.
* This case sets up a ZBS ExternalPrimaryStorage with both ImageStoreBackupStorage and
* CephBackupStorage attached to the zone, creates a VM on the ZBS PS, then simulates
* two SelectBackupStorageMsg calls (as would happen during clone VM in premium):
* 1st with requiredBackupStorageTypes=["CephBackupStorage"] (triggers retainAll),
* 2nd without requiredBackupStorageTypes (verifies bean is not corrupted).
*
* Note: SelectBackupStorageMsg is sent via CloudBus because the sender (clone VM flow)
* is in the premium module and not available in open-source integration tests.
*/
class ExternalPrimaryStorageSelectBackupStorageCase extends SubCase {
EnvSpec env
ZoneInventory zone
ClusterInventory cluster
PrimaryStorageInventory ps
DatabaseFacade dbf
CloudBus bus
Expand All @@ -60,6 +73,12 @@ class ExternalPrimaryStorageSelectBackupStorageCase extends SubCase {
@Override
void environment() {
env = makeEnv {
instanceOffering {
name = "instanceOffering"
memory = SizeUnit.GIGABYTE.toByte(8)
cpu = 4
}

sftpBackupStorage {
name = "sftp"
url = "/sftp"
Expand Down Expand Up @@ -126,54 +145,82 @@ class ExternalPrimaryStorageSelectBackupStorageCase extends SubCase {
void test() {
env.create {
zone = env.inventoryByName("zone") as ZoneInventory
cluster = env.inventoryByName("cluster") as ClusterInventory
ps = env.inventoryByName("zbs-ps") as PrimaryStorageInventory
dbf = bean(DatabaseFacade.class)
bus = bean(CloudBus.class)

testErrorWhenNoPreferredTypeAvailable()
testSelectPreferredOverNonPreferred()
testPreferBsTypesNotCorruptedByRetainAll()
}
}

/**
* When only non-preferred backup storage types exist in the zone,
* the selection should return an error (no matching preferred types).
* Zone has SftpBackupStorage (from env) and VCenterBackupStorage,
* neither of which is in zbs's preferBsTypes [ImageStoreBackupStorage].
* Reproduces ZSTAC-80789: retainAll corrupts bean's preferBackupStorageTypes.
*
* Scenario (simulates clone VM on ZBS with mixed BS types):
* 1. Set up zone with ImageStoreBackupStorage (ZBS preferred) and CephBackupStorage
* 2. Create a VM on the ZBS primary storage
* 3. Send SelectBackupStorageMsg with requiredBackupStorageTypes=["CephBackupStorage"]
* - This triggers retainAll(["CephBackupStorage"]) on preferBsTypes
* - Before fix: mutates the bean's list, emptying it permanently
* - Expected: fails (no intersection), but bean should remain intact
* 4. Send SelectBackupStorageMsg without requiredBackupStorageTypes
* - Before fix: fails because preferBsTypes was permanently emptied
* - After fix: succeeds, selects ImageStoreBackupStorage
*/
void testErrorWhenNoPreferredTypeAvailable() {
createAndAttachBackupStorage("vcenter-bs", "VCenterBackupStorage")

SelectBackupStorageMsg msg = new SelectBackupStorageMsg()
msg.setPrimaryStorageUuid(ps.uuid)
msg.setRequiredSize(SizeUnit.MEGABYTE.toByte(1))
bus.makeTargetServiceIdByResourceUuid(msg, PrimaryStorageConstant.SERVICE_ID, ps.uuid)
MessageReply reply = bus.call(msg)

assert !reply.isSuccess() : "Should fail when no preferred BS type is available"
}

/**
* Reproduces ZSTAC-71706: zone has both ImageStoreBackupStorage (preferred)
* and VCenterBackupStorage (non-preferred, created in previous test).
* Before the fix, indexOf() returns -1 for VCenterBackupStorage causing
* it to sort before ImageStoreBackupStorage. After the fix, non-preferred
* types are filtered out entirely, and ImageStoreBackupStorage is correctly selected.
*/
void testSelectPreferredOverNonPreferred() {
void testPreferBsTypesNotCorruptedByRetainAll() {
// Set up: attach ImageStoreBackupStorage and CephBackupStorage to the zone
createAndAttachBackupStorage("imagestore-bs", "ImageStoreBackupStorage")
createAndAttachBackupStorage("ceph-bs", "CephBackupStorage")

// Attach PS to cluster and create a VM to establish realistic context
attachPrimaryStorageToCluster {
primaryStorageUuid = ps.uuid
clusterUuid = cluster.uuid
}

SelectBackupStorageMsg msg = new SelectBackupStorageMsg()
msg.setPrimaryStorageUuid(ps.uuid)
msg.setRequiredSize(SizeUnit.MEGABYTE.toByte(1))
bus.makeTargetServiceIdByResourceUuid(msg, PrimaryStorageConstant.SERVICE_ID, ps.uuid)
MessageReply reply = bus.call(msg)

assert reply.isSuccess() : "SelectBackupStorageMsg should succeed"
SelectBackupStorageReply bsReply = reply as SelectBackupStorageReply
assert bsReply.inventory != null
assert bsReply.inventory.type == "ImageStoreBackupStorage" :
"Should select preferred ImageStoreBackupStorage, but got ${bsReply.inventory.type}"
def instanceOffering = env.inventoryByName("instanceOffering") as InstanceOfferingInventory
def image = env.inventoryByName("image") as ImageInventory
def l3 = env.inventoryByName("l3") as L3NetworkInventory

VmInstanceInventory vm = createVmInstance {
name = "test-vm"
instanceOfferingUuid = instanceOffering.uuid
imageUuid = image.uuid
l3NetworkUuids = [l3.uuid]
} as VmInstanceInventory

assert vm != null : "VM should be created successfully on ZBS primary storage"

// 1st call: with requiredBackupStorageTypes=["CephBackupStorage"]
// retainAll(["CephBackupStorage"]) on preferBsTypes [ImageStoreBackupStorage]
// => empty intersection => error expected
// Before fix: this permanently empties the bean's preferBackupStorageTypes
SelectBackupStorageMsg msg1 = new SelectBackupStorageMsg()
msg1.setPrimaryStorageUuid(ps.uuid)
msg1.setRequiredSize(SizeUnit.MEGABYTE.toByte(1))
msg1.setRequiredBackupStorageTypes(["CephBackupStorage"])
bus.makeTargetServiceIdByResourceUuid(msg1, PrimaryStorageConstant.SERVICE_ID, ps.uuid)
MessageReply reply1 = bus.call(msg1)

assert !reply1.isSuccess() : "Should fail: no intersection between CephBackupStorage and ZBS prefer types [ImageStoreBackupStorage]"

// 2nd call: without requiredBackupStorageTypes
// Before fix: preferBsTypes was permanently emptied by the 1st call's retainAll => fails
// After fix: defensive copy means bean is intact => succeeds and selects ImageStoreBackupStorage
SelectBackupStorageMsg msg2 = new SelectBackupStorageMsg()
msg2.setPrimaryStorageUuid(ps.uuid)
msg2.setRequiredSize(SizeUnit.MEGABYTE.toByte(1))
bus.makeTargetServiceIdByResourceUuid(msg2, PrimaryStorageConstant.SERVICE_ID, ps.uuid)
MessageReply reply2 = bus.call(msg2)

assert reply2.isSuccess() :
"ZSTAC-80789: second SelectBackupStorageMsg should succeed but failed - " +
"preferBackupStorageTypes was corrupted by previous retainAll()"
SelectBackupStorageReply bsReply2 = reply2 as SelectBackupStorageReply
assert bsReply2.inventory != null
assert bsReply2.inventory.type == "ImageStoreBackupStorage" :
"Should select ImageStoreBackupStorage, but got ${bsReply2.inventory.type}"
}

private void createAndAttachBackupStorage(String name, String type) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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
Expand All @@ -11,9 +12,12 @@ import org.zstack.header.storage.addon.primary.ExternalPrimaryStorageVO_
import org.zstack.header.storage.addon.primary.ExternalPrimaryStorageSpaceVO_
import org.zstack.header.storage.primary.PrimaryStorageCapacityVO
import org.zstack.header.storage.primary.PrimaryStorageCapacityVO_
import org.zstack.header.storage.primary.PrimaryStorageConstant
import org.zstack.header.storage.primary.PrimaryStorageHostRefVO
import org.zstack.header.storage.primary.PrimaryStorageHostRefVO_
import org.zstack.header.storage.primary.PrimaryStorageStatus
import org.zstack.header.storage.primary.SelectBackupStorageMsg
import org.zstack.header.message.MessageReply
import org.zstack.storage.zbs.MdsStatus
import org.zstack.storage.zbs.MdsUri
import org.zstack.sdk.*
Expand Down Expand Up @@ -182,6 +186,7 @@ class ZbsPrimaryStorageCase extends SubCase {
testDataVolumeNegativeScenario()
testDecodeMdsUriWithSpecialPassword()
testMdsReconnectAfterMaximumPingFailures()
testCloneVmNotCorruptPreferBsTypes()
}
}

Expand Down Expand Up @@ -856,4 +861,65 @@ class ZbsPrimaryStorageCase extends SubCase {

return cap.totalCapacity - cap.availableCapacity
}

/**
* ZSTAC-80789: Clone VM with a target PS whose required BS types differ from
* the current PS's preferred BS types corrupts the Factory bean.
*
* Reproduction path (MevocoVmInstanceBase.java "allocate-bs" flow):
* 1. Clone VM (full=true) with primaryStorageUuidForRootVolume pointing to
* a PS that requires "CephBackupStorage"
* 2. MevocoVmInstanceBase calls getRequiredBackupStorageTypes(requiredPsUuid)
* and sets requiredBackupStorageTypes=["CephBackupStorage"] on SelectBackupStorageMsg
* 3. ExternalPrimaryStorage.handle(SelectBackupStorageMsg) calls
* retainAll(requiredTypes) directly on the ZbsStorageFactory bean's
* preferBackupStorageTypes list (which is ["ImageStoreBackupStorage"])
* 4. retainAll empties the bean's list permanently (no intersection)
* 5. Next clone (without specifying target PS) fails because the factory
* bean now reports empty preferBsTypes
*
* This test simulates the exact SelectBackupStorageMsg that MevocoVmInstanceBase
* sends during the "allocate-bs" flow, verifying the bean is not corrupted.
*/
void testCloneVmNotCorruptPreferBsTypes() {
def bus = bean(CloudBus.class)

// Simulate 1st clone: target PS requires "CephBackupStorage" (e.g. Ceph PS)
// This is what MevocoVmInstanceBase sends when primaryStorageUuidForRootVolume
// points to a Ceph PS — getRequiredBackupStorageTypes() returns ["CephBackupStorage"]
// No intersection with ZBS prefer types ["ImageStoreBackupStorage"] => should fail
// Before fix: retainAll() empties the bean's internal list permanently
SelectBackupStorageMsg msg1 = new SelectBackupStorageMsg()
msg1.setPrimaryStorageUuid(ps.uuid)
msg1.setRequiredSize(SizeUnit.MEGABYTE.toByte(1))
msg1.setRequiredBackupStorageTypes(["CephBackupStorage"])
bus.makeTargetServiceIdByResourceUuid(msg1, PrimaryStorageConstant.SERVICE_ID, ps.uuid)
MessageReply reply1 = bus.call(msg1)

assert !reply1.isSuccess(): "1st clone should fail: no intersection between CephBackupStorage and ZBS prefer types"
String error1 = reply1.error.details

// Simulate 2nd clone: same requiredBackupStorageTypes to verify bean not corrupted
// Before fix: bean corrupted — preferBsTypes is empty — fails with
// "no backup storage type specified" (a different error than above)
// After fix: defensive copy keeps bean intact — same error as 1st clone
SelectBackupStorageMsg msg2 = new SelectBackupStorageMsg()
msg2.setPrimaryStorageUuid(ps.uuid)
msg2.setRequiredSize(SizeUnit.MEGABYTE.toByte(1))
msg2.setRequiredBackupStorageTypes(["CephBackupStorage"])
bus.makeTargetServiceIdByResourceUuid(msg2, PrimaryStorageConstant.SERVICE_ID, ps.uuid)
MessageReply reply2 = bus.call(msg2)

assert !reply2.isSuccess(): "2nd clone should fail: still no intersection"
String error2 = reply2.error.details

// Key assertion: both calls must produce the same error
// Before fix: 1st gets "Didn't find any available backup storage"
// 2nd gets "no backup storage type specified" (bean corrupted)
// After fix: both get the same error (bean not corrupted)
assert error1 == error2:
"ZSTAC-80789: error messages differ between 1st and 2nd call, " +
"indicating preferBackupStorageTypes was corrupted by retainAll(). " +
"1st: [${error1}], 2nd: [${error2}]"
}
}