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
1 change: 1 addition & 0 deletions conf/i18n/globalErrorCodeMapping/global-error-de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions conf/i18n/globalErrorCodeMapping/global-error-en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -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é",
Expand Down
1 change: 1 addition & 0 deletions conf/i18n/globalErrorCodeMapping/global-error-id-ID.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -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]がスナップショットパスに一致しません",
Expand Down
1 change: 1 addition & 0 deletions conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json
Original file line number Diff line number Diff line change
Expand Up @@ -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]에서 소유 볼륨 경로를 찾을 수 없습니다",
Expand Down
1 change: 1 addition & 0 deletions conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json
Original file line number Diff line number Diff line change
Expand Up @@ -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] не соответствует пути снимка",
Expand Down
1 change: 1 addition & 0 deletions conf/i18n/globalErrorCodeMapping/global-error-th-TH.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 ได้",
Expand Down
1 change: 1 addition & 0 deletions conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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]与快照路径不匹配",
Expand Down
1 change: 1 addition & 0 deletions conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -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]与快照路径不匹配",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -510,13 +510,29 @@ private void handle(final SelectBackupStorageMsg msg) {
.param("size", msg.getRequiredSize())
.list();

// sort by prefer type
availableBs.sort(Comparator.comparingInt(o -> preferBsTypes.indexOf(o.getType())));
availableBs = filterAndSortByPreferredTypes(availableBs, preferBsTypes);

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.toString(), self.getUuid()));
bus.reply(msg, reply);
return;
}
reply.setInventory(BackupStorageInventory.valueOf(availableBs.get(0)));

bus.reply(msg, reply);
}

// package-visible for testing
static List<BackupStorageVO> filterAndSortByPreferredTypes(
List<BackupStorageVO> candidates, List<String> preferBsTypes) {
return candidates.stream()
.filter(bs -> preferBsTypes.contains(bs.getType()))
.sorted(Comparator.comparingInt(o -> preferBsTypes.indexOf(o.getType())))
.collect(Collectors.toList());
}
Comment on lines +527 to +534
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ExternalPrimaryStorage.java package + method signature =="
sed -n '1,5p;525,535p' storage/src/main/java/org/zstack/storage/addon/primary/ExternalPrimaryStorage.java

echo
echo "== Test package + call site =="
sed -n '1,8p;60,66p' test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/ExternalPrimaryStorageSelectBackupStorageCase.groovy

echo
echo "== All call sites =="
rg -n --type=java --type=groovy 'filterAndSortByPreferredTypes\s*\('

Repository: MatheMatrix/zstack

Length of output: 3335


filterAndSortByPreferredTypes 改为 public,或将测试类移到同包。

方法 filterAndSortByPreferredTypes 目前是包级可见(无访问修饰符),但测试类在不同包 org.zstack.test.integration.storage.primary.addon 中调用它。根据 Java 可见性规则,包级可见方法不应被不同包中的类访问。

虽然 Groovy 的运行时反射机制可能允许此类访问,但这违反了 Java 封装原则,且易导致维护问题。建议:

  • 改为 public static(添加注释说明仅供测试和工具方法使用),或
  • 将 ExternalPrimaryStorageSelectBackupStorageCase 测试类移至 org.zstack.storage.addon.primary.test 或同包位置。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@storage/src/main/java/org/zstack/storage/addon/primary/ExternalPrimaryStorage.java`
around lines 527 - 534, The method filterAndSortByPreferredTypes is
package-private but is used from a test in a different package; make it public
(change its signature to public static List<BackupStorageVO>
filterAndSortByPreferredTypes(...)) and add a short javadoc noting it’s exposed
for tests/tools, or alternatively move the
ExternalPrimaryStorageSelectBackupStorageCase test into the same package as
ExternalPrimaryStorage (org.zstack.storage.addon.primary) so package-private
visibility is valid.


private void handle(SetVolumeQosOnPrimaryStorageMsg msg) {
SetVolumeQosOnPrimaryStorageReply reply = new SetVolumeQosOnPrimaryStorageReply();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package org.zstack.test.integration.storage.primary.addon

import org.zstack.header.storage.backup.BackupStorageVO
import org.zstack.storage.addon.primary.ExternalPrimaryStorage
import org.zstack.test.integration.storage.StorageTest
import org.zstack.testlib.EnvSpec
import org.zstack.testlib.SubCase

/**
* Test for ZSTAC-71706: ExternalPrimaryStorage backup storage selection logic.
*
* Bug: List.indexOf() returns -1 for types not in preferBsTypes,
* causing ascending sort to place non-preferred types (e.g. VCenterBackupStorage)
* before preferred types, leading to wrong backup storage selection.
*
* Fix: Filter out non-preferred backup storage types before sorting by preference.
* The filter+sort logic is extracted into ExternalPrimaryStorage.filterAndSortByPreferredTypes()
* which this test validates directly.
*/
class ExternalPrimaryStorageSelectBackupStorageCase extends SubCase {
EnvSpec env

@Override
void clean() {
env.delete()
}

@Override
void setup() {
useSpring(StorageTest.springSpec)
}

@Override
void environment() {
env = makeEnv {}
}

@Override
void test() {
env.create {
testBugReproduction()
testFilterThenSortSelectsPreferredType()
testPreferenceOrderingAmongMultipleTypes()
testEmptyResultWhenNoMatchingTypes()
testAllTypesArePreferred()
testSinglePreferredTypeAmongMany()
}
}

/**
* Reproduces the original bug: indexOf returns -1 for non-preferred types,
* causing them to sort before preferred ones in ascending order.
* With the fix, non-preferred types are filtered out entirely.
*/
void testBugReproduction() {
def vcenterBs = makeBackupStorageVO("vcenter-uuid", "VCenterBackupStorage")
def imagestoreBs = makeBackupStorageVO("imagestore-uuid", "ImageStoreBackupStorage")

def candidates = [imagestoreBs, vcenterBs]
def preferBsTypes = ["ImageStoreBackupStorage"]

def result = ExternalPrimaryStorage.filterAndSortByPreferredTypes(candidates, preferBsTypes)

assert result.size() == 1 : "VCenterBackupStorage should be filtered out"
assert result[0].type == "ImageStoreBackupStorage"
}

/**
* Verifies the fix: filter first, then sort.
* Non-preferred types should be excluded, preferred type selected.
*/
void testFilterThenSortSelectsPreferredType() {
def vcenterBs = makeBackupStorageVO("vcenter-uuid", "VCenterBackupStorage")
def imagestoreBs = makeBackupStorageVO("imagestore-uuid", "ImageStoreBackupStorage")

def candidates = [vcenterBs, imagestoreBs]
def preferBsTypes = ["ImageStoreBackupStorage"]

def result = ExternalPrimaryStorage.filterAndSortByPreferredTypes(candidates, preferBsTypes)

assert !result.isEmpty() : "Should have matching backup storages after filtering"
assert result.size() == 1
assert result[0].type == "ImageStoreBackupStorage"
}

/**
* Multiple preferred types: verifies ordering matches preferBsTypes list order.
*/
void testPreferenceOrderingAmongMultipleTypes() {
def vcenterBs = makeBackupStorageVO("vcenter-uuid", "VCenterBackupStorage")
def cephBs = makeBackupStorageVO("ceph-uuid", "CephBackupStorage")
def imagestoreBs = makeBackupStorageVO("imagestore-uuid", "ImageStoreBackupStorage")

def candidates = [vcenterBs, cephBs, imagestoreBs]
def preferBsTypes = ["ImageStoreBackupStorage", "CephBackupStorage"]

def result = ExternalPrimaryStorage.filterAndSortByPreferredTypes(candidates, preferBsTypes)

assert result.size() == 2 : "VCenterBackupStorage should be filtered out"
assert result[0].type == "ImageStoreBackupStorage" : "First preference should be first"
assert result[1].type == "CephBackupStorage" : "Second preference should be second"
}

/**
* No matching types: result should be empty (caller returns error).
*/
void testEmptyResultWhenNoMatchingTypes() {
def vcenterBs = makeBackupStorageVO("vcenter-uuid", "VCenterBackupStorage")

def candidates = [vcenterBs]
def preferBsTypes = ["ImageStoreBackupStorage", "CephBackupStorage"]

def result = ExternalPrimaryStorage.filterAndSortByPreferredTypes(candidates, preferBsTypes)

assert result.isEmpty() : "Should be empty when no backup storage matches preferred types"
}

/**
* All candidates are preferred: all should be retained and sorted by preference.
*/
void testAllTypesArePreferred() {
def cephBs = makeBackupStorageVO("ceph-uuid", "CephBackupStorage")
def imagestoreBs = makeBackupStorageVO("imagestore-uuid", "ImageStoreBackupStorage")

def candidates = [cephBs, imagestoreBs]
def preferBsTypes = ["ImageStoreBackupStorage", "CephBackupStorage"]

def result = ExternalPrimaryStorage.filterAndSortByPreferredTypes(candidates, preferBsTypes)

assert result.size() == 2
assert result[0].type == "ImageStoreBackupStorage"
assert result[1].type == "CephBackupStorage"
}

/**
* Single preferred type among many candidates: only matching ones retained.
*/
void testSinglePreferredTypeAmongMany() {
def vcenter1 = makeBackupStorageVO("vcenter-uuid-1", "VCenterBackupStorage")
def vcenter2 = makeBackupStorageVO("vcenter-uuid-2", "VCenterBackupStorage")
def imagestoreBs = makeBackupStorageVO("imagestore-uuid", "ImageStoreBackupStorage")
def cephBs = makeBackupStorageVO("ceph-uuid", "CephBackupStorage")

def candidates = [vcenter1, cephBs, vcenter2, imagestoreBs]
def preferBsTypes = ["ImageStoreBackupStorage"]

def result = ExternalPrimaryStorage.filterAndSortByPreferredTypes(candidates, preferBsTypes)

assert result.size() == 1
assert result[0].uuid == "imagestore-uuid"
assert result[0].type == "ImageStoreBackupStorage"
}

private static BackupStorageVO makeBackupStorageVO(String uuid, String type) {
def vo = new BackupStorageVO()
vo.setUuid(uuid)
vo.setType(type)
return vo
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down