diff --git a/conf/i18n/globalErrorCodeMapping/global-error-de-DE.json b/conf/i18n/globalErrorCodeMapping/global-error-de-DE.json index 41e1915fed..f23ec0e9e1 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-de-DE.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-de-DE.json @@ -629,6 +629,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "Instanztyp[%s] wird nicht unterstützt", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "Gruppenadresse [%s] ist keine Multicast-Gruppenadresse", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "Speicher ist nicht gesund:%s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "Kein verfügbarer Backup-Speicher mit bevorzugten Typen %s für primären Speicher[uuid:%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "multicastRouter[uuid:%s] wurde keinem VPC-Router zugeordnet", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "Multicast ist bereits auf dem VPC-Router UUID[:%s] aktiviert", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "Der Eigentümer-Volume-Pfad kann aus dem internen Snapshot-Pfad[%s] nicht gefunden werden, da der reguläre Ausdruck[%s] nicht mit dem Snapshot-Pfad übereinstimmt", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-en_US.json b/conf/i18n/globalErrorCodeMapping/global-error-en_US.json index 715e823d95..5e59252520 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-en_US.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-en_US.json @@ -629,6 +629,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "unsupported instance type[%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "group address [%s] is not a multicast group address", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "storage is not healthy:%s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "no available backup storage with preferred types %s for primary storage[uuid:%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "multicastRouter[uuid:%s] has not been associated with a VPC router", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "multicast already enabled on VPC router UUID[:%s]", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "cannot find the owning volume path from the internal snapshot path[%s], as the regex[%s] fails to match the snapshot path", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json b/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json index 449344a6ce..a742212f40 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json @@ -629,6 +629,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "type d'instance[%s] non supporté", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "l'adresse de groupe [%s] n'est pas une adresse de groupe multicast", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "le stockage n'est pas sain : %s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "aucun stockage de sauvegarde disponible avec les types préférés %s pour le stockage principal[uuid:%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "Le multicastRouter[uuid:%s] n'a pas été associé à un routeur VPC", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "Le multicast est déjà activé sur le routeur VPC UUID[:%s]", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "Impossible de trouver le chemin du volume propriétaire à partir du chemin d'instantané interne[%s], car l'expression régulière[%s] ne correspond pas au chemin d'instantané", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json b/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json index d8cd5bf487..58d87228b7 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json @@ -629,6 +629,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "jenis instance[%s] tidak didukung", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "alamat grup [%s] bukan alamat grup multicast", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "penyimpanan tidak sehat:%s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "tidak ada penyimpanan cadangan yang tersedia dengan tipe yang disukai %s untuk penyimpanan utama[uuid:%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "multicastRouter[uuid:%s] belum dikaitkan dengan router VPC", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "multicast sudah diaktifkan pada router VPC UUID[:%s]", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "tidak dapat menemukan jalur volume pemilik dari jalur snapshot internal[%s], karena regex[%s] tidak cocok dengan jalur snapshot", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json b/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json index 2924f65847..a935c05b20 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json @@ -629,6 +629,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "サポートされていないインスタンスタイプ[%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "グループアドレス[%s]はマルチキャストグループアドレスではありません", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "ストレージは正常ではありません:%s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "優先タイプ%sに一致する利用可能なバックアップストレージがプライマリストレージ[uuid:%s]に存在しません", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "マルチキャストルーター[uuid:%s]はVPCルーターに関連付けられていません", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "マルチキャストは既にVPCルーターUUID[:%s]で有効化されています", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "内部スナップショットパス[%s]から所有ボリュームパスが見つかりません。正規表現[%s]がスナップショットパスに一致しません", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json b/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json index c73d9f274a..ad11aec2dd 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json @@ -629,6 +629,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "지원되지 않는 인스턴스 유형[%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "그룹 주소 [%s]는 멀티캐스트 그룹 주소가 아닙니다", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "저장소가 정상 상태가 아닙니다:%s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "기본 저장소[uuid:%s]에 대해 선호하는 유형 %s의 사용 가능한 백업 저장소가 없습니다", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "multicastRouter[uuid:%s]가 VPC 라우터와 연결되지 않았습니다", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "VPC 라우터 UUID[:%s]에서 멀티캐스트가 이미 활성화되어 있습니다", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "정규식[%s]이 스냅샷 경로와 일치하지 않아 내부 스냅샷 경로[%s]에서 소유 볼륨 경로를 찾을 수 없습니다", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json b/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json index 2454d7e565..6bea92476a 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json @@ -629,6 +629,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "неподдерживаемый тип экземпляра[%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "групповой адрес [%s] не является мультикаст-групповым адресом", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "хранилище нездорово:%s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "нет доступного резервного хранилища с предпочитаемыми типами %s для первичного хранилища[uuid:%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "multicastRouter[uuid:%s] не был связан с VPC-роутером", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "multicast уже включен на VPC-роутере UUID[:%s]", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "невозможно найти путь к принадлежащему тому из внутреннего пути снимка[%s], так как регулярное выражение[%s] не соответствует пути снимка", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json b/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json index 9db32b3426..09f39b96cc 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json @@ -629,6 +629,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "instance type[%s] ไม่รองรับ", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "group address [%s] ไม่ใช่ multicast group address", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "storage ไม่พร้อมใช้งาน:%s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "ไม่มี backup storage ที่ใช้ได้ซึ่งมีประเภทที่ต้องการ %s สำหรับ primary storage[uuid:%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "multicastRouter[uuid:%s] ไม่ได้เชื่อมโยงกับ VPC router", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "multicast เปิดใช้งานอยู่แล้วบน VPC router UUID[:%s]", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "ไม่พบ path ของ volume ที่เป็นเจ้าของจาก internal snapshot path[%s] เนื่องจาก regex[%s] ไม่สามารถจับคู่กับ snapshot path ได้", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json index 01960e8eb4..94747404ca 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json @@ -629,6 +629,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "不支持的实例配置类型[%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "组地址 [%s] 不是多播地址", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "存储不健康:%s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "没有可用的镜像服务器匹配首选类型 %s,主存储[uuid:%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "多播路由器[uuid:%s]尚未绑定到VPC路由器", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "多播已在VPC路由器uuid[:%s]上启用", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "无法从内部快照路径[%s]找到所属卷路径,因为正则表达式[%s]与快照路径不匹配", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json b/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json index 35f1b8c844..57157f5d3a 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json @@ -629,6 +629,7 @@ "ORG_ZSTACK_CONFIGURATION_10006": "不支持的实例配置類型[%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10007": "组地址 [%s] 不是多播地址", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014": "儲儲不健康:%s", + "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015": "沒有可用的鏡像服務器匹配首選類型 %s,主儲存[uuid:%s]", "ORG_ZSTACK_MULTICAST_ROUTER_10000": "多播路由器[uuid:%s]尚未綁定到VPC路由器", "ORG_ZSTACK_MULTICAST_ROUTER_10001": "多播已在VPC路由器uuid[:%s]上啟用", "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10012": "無法从内部快照路径[%s]找到所属卷路径,因为正则表達式[%s]与快照路径不匹配", diff --git a/storage/src/main/java/org/zstack/storage/addon/primary/ExternalPrimaryStorage.java b/storage/src/main/java/org/zstack/storage/addon/primary/ExternalPrimaryStorage.java index 673cfed7c1..332b10331a 100644 --- a/storage/src/main/java/org/zstack/storage/addon/primary/ExternalPrimaryStorage.java +++ b/storage/src/main/java/org/zstack/storage/addon/primary/ExternalPrimaryStorage.java @@ -510,8 +510,19 @@ private void handle(final SelectBackupStorageMsg msg) { .param("size", msg.getRequiredSize()) .list(); - // sort by prefer type - availableBs.sort(Comparator.comparingInt(o -> preferBsTypes.indexOf(o.getType()))); + // filter out non-preferred types, then sort by preference order + availableBs = availableBs.stream() + .filter(bs -> preferBsTypes.contains(bs.getType())) + .sorted(Comparator.comparingInt(o -> preferBsTypes.indexOf(o.getType()))) + .collect(Collectors.toList()); + + if (availableBs.isEmpty()) { + reply.setError(operr(ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015, + "no available backup storage with preferred types %s for primary storage[uuid:%s]", + preferBsTypes, self.getUuid())); + bus.reply(msg, reply); + return; + } reply.setInventory(BackupStorageInventory.valueOf(availableBs.get(0))); bus.reply(msg, reply); diff --git a/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/ExternalPrimaryStorageSelectBackupStorageCase.groovy b/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/ExternalPrimaryStorageSelectBackupStorageCase.groovy new file mode 100644 index 0000000000..9419db1221 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/ExternalPrimaryStorageSelectBackupStorageCase.groovy @@ -0,0 +1,200 @@ +package org.zstack.test.integration.storage.primary.addon + +import org.zstack.core.Platform +import org.zstack.core.cloudbus.CloudBus +import org.zstack.core.db.DatabaseFacade +import org.zstack.core.db.SQL +import org.zstack.header.message.MessageReply +import org.zstack.header.storage.backup.BackupStorageEO +import org.zstack.header.storage.backup.BackupStorageState +import org.zstack.header.storage.backup.BackupStorageStatus +import org.zstack.header.storage.backup.BackupStorageZoneRefVO +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.PrimaryStorageInventory +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. + * + * 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. + * + * Fix: Filter out non-preferred backup storage types before sorting by preference. + * + * 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. + */ +class ExternalPrimaryStorageSelectBackupStorageCase extends SubCase { + EnvSpec env + ZoneInventory zone + PrimaryStorageInventory ps + DatabaseFacade dbf + CloudBus bus + List manualBsUuids = [] + + @Override + void clean() { + manualBsUuids.each { uuid -> + SQL.New(BackupStorageZoneRefVO.class) + .eq(BackupStorageZoneRefVO_.backupStorageUuid, uuid) + .hardDelete() + dbf.removeByPrimaryKey(uuid, BackupStorageEO.class) + } + env.delete() + } + + @Override + void setup() { + useSpring(StorageTest.springSpec) + } + + @Override + void environment() { + env = makeEnv { + sftpBackupStorage { + name = "sftp" + url = "/sftp" + username = "root" + password = "password" + hostname = "127.0.0.2" + + image { + name = "image" + url = "http://zstack.org/download/test.qcow2" + size = SizeUnit.GIGABYTE.toByte(1) + virtio = true + } + } + + zone { + name = "zone" + description = "test" + + cluster { + name = "cluster" + hypervisorType = "KVM" + + kvm { + name = "kvm" + managementIp = "127.0.0.1" + username = "root" + password = "password" + } + + attachL2Network("l2") + } + + l2NoVlanNetwork { + name = "l2" + physicalInterface = "eth0" + + l3Network { + name = "l3" + + ip { + startIp = "192.168.100.10" + endIp = "192.168.100.100" + netmask = "255.255.255.0" + gateway = "192.168.100.1" + } + } + } + + externalPrimaryStorage { + name = "zbs-ps" + identity = "zbs" + defaultOutputProtocol = "CBD" + config = '{"mdsUrls":["root:password@127.0.1.1","root:password@127.0.1.2","root:password@127.0.1.3"],"logicalPoolName":"lpool1"}' + url = "zbs" + } + + attachBackupStorage("sftp") + } + } + } + + @Override + void test() { + env.create { + zone = env.inventoryByName("zone") as ZoneInventory + ps = env.inventoryByName("zbs-ps") as PrimaryStorageInventory + dbf = bean(DatabaseFacade.class) + bus = bean(CloudBus.class) + + testErrorWhenNoPreferredTypeAvailable() + testSelectPreferredOverNonPreferred() + } + } + + /** + * 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]. + */ + 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() { + createAndAttachBackupStorage("imagestore-bs", "ImageStoreBackupStorage") + + 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}" + } + + private void createAndAttachBackupStorage(String name, String type) { + String uuid = Platform.getUuid() + + def bsEo = new BackupStorageEO() + bsEo.setUuid(uuid) + bsEo.setName(name) + bsEo.setType(type) + bsEo.setState(BackupStorageState.Enabled) + bsEo.setStatus(BackupStorageStatus.Connected) + bsEo.setTotalCapacity(SizeUnit.TERABYTE.toByte(100)) + bsEo.setAvailableCapacity(SizeUnit.TERABYTE.toByte(100)) + bsEo.setUrl("http://test-" + name) + dbf.persist(bsEo) + + def ref = new BackupStorageZoneRefVO() + ref.setBackupStorageUuid(uuid) + ref.setZoneUuid(zone.uuid) + dbf.persist(ref) + + manualBsUuids.add(uuid) + } +} diff --git a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java index 73fb1fd99e..6a4087cae0 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -6506,6 +6506,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014 = "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10014"; + public static final String ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015 = "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10015"; + public static final String ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10040 = "ORG_ZSTACK_STORAGE_ADDON_PRIMARY_10040"; public static final String ORG_ZSTACK_NETWORK_HOSTNETWORKINTERFACE_LLDP_10000 = "ORG_ZSTACK_NETWORK_HOSTNETWORKINTERFACE_LLDP_10000";