diff --git a/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorManagerImpl.java b/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorManagerImpl.java
index 74176e30bb4..e8943e454b7 100755
--- a/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorManagerImpl.java
+++ b/compute/src/main/java/org/zstack/compute/allocator/HostAllocatorManagerImpl.java
@@ -196,7 +196,20 @@ private void handle(ReportHostCapacityMessage msg) {
// U-A (NB-30): physical fields written here under PESSIMISTIC_WRITE lock; available*
// fields are the sole responsibility of psCapacityUpdater.recalculate() below.
- String serverUuid = HostCapacityUpdater.resolveServerUuidOrThrow(msg.getHostUuid());
+ String serverUuid = HostCapacityUpdater.resolveServerUuid(msg.getHostUuid());
+ if (serverUuid == null) {
+ // Non-unified-hardware host (ESX/vCenter etc.): no KVM_HOST PhysicalServerRoleVO and no
+ // PhysicalServerVO. The HostCapacityVO MERGE VIEW keys these via COALESCE(serverUuid,
+ // hostUuid), so report into a PhysicalServerCapacityVO row keyed by hostUuid; available*
+ // fields are set inline since recalculate() (which needs a PhysicalServerVO) is skipped.
+ // A KVM host missing its role is a FlowChain timing bug — keep failing loud (NB-24).
+ if (HostCapacityUpdater.isKvmHost(msg.getHostUuid())) {
+ HostCapacityUpdater.resolveServerUuidOrThrow(msg.getHostUuid());
+ }
+ handleReportForNonUnifiedHost(msg, totalCpu, availPhysMem);
+ bus.reply(msg, new MessageReply());
+ return;
+ }
PhysicalServerCapacityVO vo = dbf.getEntityManager()
.find(PhysicalServerCapacityVO.class, serverUuid, javax.persistence.LockModeType.PESSIMISTIC_WRITE);
if (vo == null) {
@@ -234,6 +247,47 @@ private boolean needUpdateCapacity(PhysicalServerCapacityVO vo, ReportHostCapaci
|| vo.getCpuSockets() != msg.getCpuSockets() || vo.getCpuCoreNum() != msg.getCpuCoreNum();
}
+ private void handleReportForNonUnifiedHost(ReportHostCapacityMessage msg, long totalCpu, long availPhysMem) {
+ long availCpu = totalCpu - msg.getUsedCpu();
+ availCpu = availCpu > 0 ? availCpu : 0;
+
+ PhysicalServerCapacityVO vo = dbf.getEntityManager()
+ .find(PhysicalServerCapacityVO.class, msg.getHostUuid(), javax.persistence.LockModeType.PESSIMISTIC_WRITE);
+ if (vo == null) {
+ vo = new PhysicalServerCapacityVO();
+ vo.setUuid(msg.getHostUuid());
+ vo.setTotalCpu(totalCpu);
+ vo.setAvailableCpu(availCpu);
+ vo.setTotalMemory(msg.getTotalMemory());
+ vo.setAvailableMemory(availPhysMem);
+ vo.setTotalPhysicalMemory(msg.getTotalMemory());
+ vo.setAvailablePhysicalMemory(availPhysMem);
+ vo.setCpuNum(msg.getCpuNum());
+ vo.setCpuSockets(msg.getCpuSockets());
+ vo.setCpuCoreNum(msg.getCpuCoreNum());
+ dbf.getEntityManager().persist(vo);
+ } else if (needUpdateNonUnifiedCapacity(vo, msg, totalCpu, availCpu, availPhysMem)) {
+ vo.setCpuNum(msg.getCpuNum());
+ vo.setTotalCpu(totalCpu);
+ vo.setAvailableCpu(availCpu);
+ vo.setTotalPhysicalMemory(msg.getTotalMemory());
+ vo.setAvailablePhysicalMemory(availPhysMem);
+ vo.setTotalMemory(msg.getTotalMemory());
+ vo.setAvailableMemory(availPhysMem);
+ vo.setCpuSockets(msg.getCpuSockets());
+ vo.setCpuCoreNum(msg.getCpuCoreNum());
+ dbf.getEntityManager().merge(vo);
+ }
+ }
+
+ private boolean needUpdateNonUnifiedCapacity(PhysicalServerCapacityVO vo, ReportHostCapacityMessage msg, long totalCpu, long availCpu, long availPhysMem) {
+ return vo.getCpuNum() != msg.getCpuNum() || vo.getTotalCpu() != totalCpu
+ || vo.getAvailableCpu() != availCpu || vo.getTotalPhysicalMemory() != msg.getTotalMemory()
+ || vo.getAvailablePhysicalMemory() != availPhysMem || vo.getTotalMemory() != msg.getTotalMemory()
+ || vo.getAvailableMemory() != availPhysMem
+ || vo.getCpuSockets() != msg.getCpuSockets() || vo.getCpuCoreNum() != msg.getCpuCoreNum();
+ }
+
private void handle(final AllocateHostMsg msg) {
if (HostAllocatorGlobalConfig.HOST_ALLOCATOR_ALLOW_CONCURRENT.value(Boolean.class)) {
thdf.chainSubmit(new ChainTask(msg) {
diff --git a/compute/src/main/java/org/zstack/compute/allocator/HostCapacityUpdater.java b/compute/src/main/java/org/zstack/compute/allocator/HostCapacityUpdater.java
index 2dec86ba0de..50ded633b58 100755
--- a/compute/src/main/java/org/zstack/compute/allocator/HostCapacityUpdater.java
+++ b/compute/src/main/java/org/zstack/compute/allocator/HostCapacityUpdater.java
@@ -9,10 +9,13 @@
import org.zstack.core.db.Q;
import org.zstack.header.allocator.HostCapacityVO;
import org.zstack.header.exception.CloudRuntimeException;
+import org.zstack.header.host.HostVO;
+import org.zstack.header.host.HostVO_;
import org.zstack.header.server.PhysicalServerCapacityVO;
import org.zstack.header.server.PhysicalServerRoleVO;
import org.zstack.header.server.PhysicalServerRoleVO_;
import org.zstack.header.server.ServerRoleType;
+import org.zstack.header.vm.VmInstanceConstant;
import org.zstack.utils.Utils;
import org.zstack.utils.logging.CLogger;
@@ -70,10 +73,12 @@ public HostCapacityUpdater(String hostUuid) {
/**
* Resolve PhysicalServer UUID from a KVM host UUID via PhysicalServerRoleVO mapping.
*
- *
Throws {@link CloudRuntimeException} when no KVM_HOST role mapping is found (NB-24,
- * 2026-04-22). Previous NB-22 "log null + boolean" silent-drop was reverted — fail-loud
- * surfaces FlowChain timing bugs / orphan windows instead of masking them as silent capacity
- * update losses. The existing "host deleted naturally" semantic is still carried by
+ *
Throws {@link CloudRuntimeException} when a KVM host has no KVM_HOST role mapping (NB-24,
+ * 2026-04-22) — a FlowChain timing bug. Non-KVM hosts (ESX/vCenter etc.) legitimately have no
+ * role mapping and return {@code hostUuid} (their PhysicalServerCapacityVO is keyed by hostUuid
+ * via the COALESCE(serverUuid, hostUuid) view). Previous NB-22 "log null + boolean" silent-drop
+ * was reverted — fail-loud surfaces FlowChain timing bugs / orphan windows instead of masking
+ * them as silent capacity update losses. The existing "host deleted naturally" semantic is still carried by
* {@link #lockCapacity()} returning {@code false} when the capacity row itself is absent.
*
*
NB-30: Phase 2 lock key invariant. All PESSIMISTIC_WRITE paths on PhysicalServerCapacityVO
@@ -81,18 +86,41 @@ public HostCapacityUpdater(String hostUuid) {
* {@code serverUuid}.
*/
public static String resolveServerUuidOrThrow(String hostUuid) {
- String serverUuid = Q.New(PhysicalServerRoleVO.class)
+ String serverUuid = resolveServerUuid(hostUuid);
+ if (serverUuid == null) {
+ // Non-unified-hardware hosts (ESX/vCenter etc.) legitimately have no KVM_HOST role;
+ // their PhysicalServerCapacityVO is keyed by hostUuid via COALESCE(serverUuid, hostUuid).
+ // A KVM host missing its role is a FlowChain timing bug — keep failing loud (NB-24).
+ if (isKvmHost(hostUuid)) {
+ throw new CloudRuntimeException(String.format(
+ "cannot resolve PhysicalServer UUID for host[uuid:%s]: no KVM_HOST "
+ + "PhysicalServerRoleVO found. FlowChain timing bug or orphan "
+ + "PhysicalServerVO — capacity PRD NB-24.", hostUuid));
+ }
+ return hostUuid;
+ }
+ return serverUuid;
+ }
+
+ /**
+ * Non-throwing variant of {@link #resolveServerUuidOrThrow(String)}: returns the KVM_HOST
+ * {@code serverUuid} mapping or {@code null} when none exists. Callers decide fail-loud vs
+ * fallback; non-unified-hardware hosts (ESX/vCenter etc.) legitimately have no role mapping.
+ */
+ public static String resolveServerUuid(String hostUuid) {
+ return Q.New(PhysicalServerRoleVO.class)
.eq(PhysicalServerRoleVO_.roleUuid, hostUuid)
.eq(PhysicalServerRoleVO_.roleType, ServerRoleType.KVM_HOST.toString())
.select(PhysicalServerRoleVO_.serverUuid)
.findValue();
- if (serverUuid == null) {
- throw new CloudRuntimeException(String.format(
- "cannot resolve PhysicalServer UUID for host[uuid:%s]: no KVM_HOST "
- + "PhysicalServerRoleVO found. FlowChain timing bug or orphan "
- + "PhysicalServerVO — capacity PRD NB-24.", hostUuid));
- }
- return serverUuid;
+ }
+
+ public static boolean isKvmHost(String hostUuid) {
+ String hypervisorType = Q.New(HostVO.class)
+ .eq(HostVO_.uuid, hostUuid)
+ .select(HostVO_.hypervisorType)
+ .findValue();
+ return VmInstanceConstant.KVM_HYPERVISOR_TYPE.equals(hypervisorType);
}
private void logDeletedHost() {
diff --git a/conf/db/upgrade/V5.5.18__schema.sql b/conf/db/upgrade/V5.5.18__schema.sql
index c89fd33133c..69c832faf01 100644
--- a/conf/db/upgrade/V5.5.18__schema.sql
+++ b/conf/db/upgrade/V5.5.18__schema.sql
@@ -173,13 +173,6 @@ CREATE TABLE IF NOT EXISTS `PhysicalServerHardwareInfoVO` (
`cpuCores` INT DEFAULT NULL,
`cpuArchitecture` VARCHAR(255) DEFAULT NULL,
`totalMemoryBytes` BIGINT DEFAULT NULL,
- `memoryModuleCount` INT DEFAULT NULL,
- `totalDiskBytes` BIGINT DEFAULT NULL,
- `diskCount` INT DEFAULT NULL,
- `nicCount` INT DEFAULT NULL,
- `gpuCount` INT DEFAULT NULL,
- `healthStatus` VARCHAR(255) DEFAULT NULL,
- `discoverSource` VARCHAR(255) DEFAULT NULL,
`lastDiscoverDate` TIMESTAMP NULL DEFAULT NULL,
`lastOpDate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`createDate` TIMESTAMP NOT NULL DEFAULT '2000-01-01 00:00:00',
diff --git a/conf/springConfigXml/PhysicalServerManager.xml b/conf/springConfigXml/PhysicalServerManager.xml
index 8bcc926f8b1..460fd13729d 100644
--- a/conf/springConfigXml/PhysicalServerManager.xml
+++ b/conf/springConfigXml/PhysicalServerManager.xml
@@ -133,4 +133,11 @@
+
+
+
+
+
+
diff --git a/conf/springConfigXml/encrypt.xml b/conf/springConfigXml/encrypt.xml
index 4cac18b6b70..638afe998b1 100644
--- a/conf/springConfigXml/encrypt.xml
+++ b/conf/springConfigXml/encrypt.xml
@@ -42,6 +42,6 @@
-
+
\ No newline at end of file
diff --git a/core/src/main/java/org/zstack/core/convert/SpecialDataConverter.java b/core/src/main/java/org/zstack/core/convert/SpecialDataConverter.java
index ad63a9ef87e..b5110fc38c6 100644
--- a/core/src/main/java/org/zstack/core/convert/SpecialDataConverter.java
+++ b/core/src/main/java/org/zstack/core/convert/SpecialDataConverter.java
@@ -5,7 +5,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.stereotype.Component;
-import org.zstack.core.encrypt.EncryptFacade;
+import org.zstack.header.core.convert.EncryptFacade;
import org.zstack.core.encrypt.EncryptGlobalConfig;
import org.zstack.header.core.encrypt.PasswordEncryptType;
import org.zstack.utils.Utils;
diff --git a/core/src/main/java/org/zstack/core/encrypt/EncryptFacadeImpl.java b/core/src/main/java/org/zstack/core/encrypt/EncryptFacadeImpl.java
index 92b0e2c7178..1bfa8dd85bc 100644
--- a/core/src/main/java/org/zstack/core/encrypt/EncryptFacadeImpl.java
+++ b/core/src/main/java/org/zstack/core/encrypt/EncryptFacadeImpl.java
@@ -5,7 +5,7 @@
import org.zstack.core.Platform;
import org.zstack.core.componentloader.PluginRegistry;
import org.zstack.core.config.*;
-import org.zstack.core.convert.PasswordConverter;
+import org.zstack.header.core.convert.PasswordConverter;
import org.zstack.core.convert.SpecialDataConverter;
import org.zstack.core.db.DatabaseFacade;
import org.zstack.core.db.Q;
@@ -13,6 +13,7 @@
import org.zstack.core.db.SQLBatch;
import org.zstack.header.Component;
import org.zstack.header.core.encrypt.*;
+import org.zstack.header.core.convert.EncryptFacade;
import org.zstack.header.errorcode.ErrorableValue;
import org.zstack.header.errorcode.OperationFailureException;
import org.zstack.header.exception.CloudRuntimeException;
@@ -76,6 +77,12 @@ public ErrorableValue decrypt(String data, String algType) {
return encryptDriver.decrypt(data, algType);
}
+ @Override
+ public boolean isEncryptionDisabled() {
+ return PasswordEncryptType.None.toString()
+ .equals(EncryptGlobalConfig.ENABLE_PASSWORD_ENCRYPT.value(String.class));
+ }
+
private String getQuerySql(List covertSubClasses, String className, String fieldName, String uuid) {
List whereSqlList = covertSubClasses.stream()
.filter(subClass -> subClass.classSimpleName().equals(className) && !subClass.columnName().isEmpty())
diff --git a/core/src/main/java/org/zstack/core/encrypt/EncryptFacade.java b/header/src/main/java/org/zstack/header/core/convert/EncryptFacade.java
similarity index 89%
rename from core/src/main/java/org/zstack/core/encrypt/EncryptFacade.java
rename to header/src/main/java/org/zstack/header/core/convert/EncryptFacade.java
index 847c589b4db..ccd97f8ce2e 100644
--- a/core/src/main/java/org/zstack/core/encrypt/EncryptFacade.java
+++ b/header/src/main/java/org/zstack/header/core/convert/EncryptFacade.java
@@ -1,4 +1,4 @@
-package org.zstack.core.encrypt;
+package org.zstack.header.core.convert;
import org.zstack.header.core.encrypt.EncryptEntityState;
import org.zstack.header.core.encrypt.EncryptedFieldBundle;
@@ -6,9 +6,6 @@
import java.util.List;
-/**
- * Created by kayo on 2018/9/7.
- */
public interface EncryptFacade {
String encrypt(String decryptString);
@@ -23,4 +20,6 @@ public interface EncryptFacade {
List getIntegrityEncryptionBundle();
List getConfidentialityEncryptionBundle();
-}
\ No newline at end of file
+
+ boolean isEncryptionDisabled();
+}
diff --git a/core/src/main/java/org/zstack/core/convert/PasswordConverter.java b/header/src/main/java/org/zstack/header/core/convert/PasswordConverter.java
similarity index 76%
rename from core/src/main/java/org/zstack/core/convert/PasswordConverter.java
rename to header/src/main/java/org/zstack/header/core/convert/PasswordConverter.java
index 04a122c278c..fb7a7b1438f 100644
--- a/core/src/main/java/org/zstack/core/convert/PasswordConverter.java
+++ b/header/src/main/java/org/zstack/header/core/convert/PasswordConverter.java
@@ -1,13 +1,10 @@
-package org.zstack.core.convert;
+package org.zstack.header.core.convert;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.stereotype.Component;
-import org.zstack.core.encrypt.EncryptFacade;
-import org.zstack.core.encrypt.EncryptGlobalConfig;
-import org.zstack.header.core.encrypt.PasswordEncryptType;
import org.zstack.utils.Utils;
import org.zstack.utils.logging.CLogger;
@@ -32,7 +29,7 @@ public void initEncryptFacade(EncryptFacade encryptFacade){
@Override
public String convertToDatabaseColumn(String attribute) {
- if (PasswordEncryptType.None.toString().equals(EncryptGlobalConfig.ENABLE_PASSWORD_ENCRYPT.value(String.class))) {
+ if (encryptFacade == null || encryptFacade.isEncryptionDisabled()) {
return attribute;
}
if (StringUtils.isEmpty(attribute)) {
@@ -43,7 +40,7 @@ public String convertToDatabaseColumn(String attribute) {
@Override
public String convertToEntityAttribute(String dbData) {
- if (PasswordEncryptType.None.toString().equals(EncryptGlobalConfig.ENABLE_PASSWORD_ENCRYPT.value(String.class))) {
+ if (encryptFacade == null || encryptFacade.isEncryptionDisabled()) {
return dbData;
}
diff --git a/header/src/main/java/org/zstack/header/server/PhysicalServerAO.java b/header/src/main/java/org/zstack/header/server/PhysicalServerAO.java
index afc1c13ce94..b645e3b0d95 100644
--- a/header/src/main/java/org/zstack/header/server/PhysicalServerAO.java
+++ b/header/src/main/java/org/zstack/header/server/PhysicalServerAO.java
@@ -1,5 +1,6 @@
package org.zstack.header.server;
+import org.zstack.header.core.convert.PasswordConverter;
import org.zstack.header.vo.ForeignKey;
import org.zstack.header.vo.ForeignKey.ReferenceOption;
import org.zstack.header.vo.ResourceVO;
@@ -70,6 +71,7 @@ public class PhysicalServerAO extends ResourceVO {
@EncryptColumn
@NoLogging
+ @Convert(converter = PasswordConverter.class)
@Column
private String oobPassword;
diff --git a/header/src/main/java/org/zstack/header/server/PhysicalServerHardwareDiscoveryExtensionPoint.java b/header/src/main/java/org/zstack/header/server/PhysicalServerHardwareDiscoveryExtensionPoint.java
index 44bcf6623db..8bfe7bd8b5f 100644
--- a/header/src/main/java/org/zstack/header/server/PhysicalServerHardwareDiscoveryExtensionPoint.java
+++ b/header/src/main/java/org/zstack/header/server/PhysicalServerHardwareDiscoveryExtensionPoint.java
@@ -80,11 +80,5 @@ interface HardwareInfoCarrier {
void setCpuCores(Integer v);
void setCpuArchitecture(String v);
void setTotalMemoryBytes(Long v);
- void setMemoryModuleCount(Integer v);
- void setTotalDiskBytes(Long v);
- void setDiskCount(Integer v);
- void setNicCount(Integer v);
- void setGpuCount(Integer v);
- void setHealthStatus(String v);
}
}
diff --git a/header/src/main/java/org/zstack/header/server/PhysicalServerHardwareInfoVO.java b/header/src/main/java/org/zstack/header/server/PhysicalServerHardwareInfoVO.java
index da9ac8633a8..8346a71083e 100644
--- a/header/src/main/java/org/zstack/header/server/PhysicalServerHardwareInfoVO.java
+++ b/header/src/main/java/org/zstack/header/server/PhysicalServerHardwareInfoVO.java
@@ -53,36 +53,6 @@ public class PhysicalServerHardwareInfoVO {
@Column
private Long totalMemoryBytes;
- @Column
- private Integer memoryModuleCount;
-
- @Column
- private Long totalDiskBytes;
-
- @Column
- private Integer diskCount;
-
- @Column
- private Integer nicCount;
-
- @Column
- private Integer gpuCount;
-
- @Column
- private String healthStatus;
-
- /**
- * P1-3: first-writer-wins. The first {@code discoverHardware} pass that produced any
- * non-null carrier field writes its winning source here (per the in-pass ordering
- * IPMI_FRU > KVM_AGENT > K8S_NODEINFO). Subsequent passes refresh data columns
- * and {@link #lastDiscoverDate} but do NOT overwrite this value — it is a stable
- * "who first identified this host" tag, not a churning "currently primary contributor"
- * signal. Operators wanting per-field provenance should look at lastDiscoverDate +
- * field-level audit (out of scope for v5.5.18).
- */
- @Column
- private String discoverSource;
-
@Column
private Timestamp lastDiscoverDate;
@@ -172,62 +142,6 @@ public void setTotalMemoryBytes(Long totalMemoryBytes) {
this.totalMemoryBytes = totalMemoryBytes;
}
- public Integer getMemoryModuleCount() {
- return memoryModuleCount;
- }
-
- public void setMemoryModuleCount(Integer memoryModuleCount) {
- this.memoryModuleCount = memoryModuleCount;
- }
-
- public Long getTotalDiskBytes() {
- return totalDiskBytes;
- }
-
- public void setTotalDiskBytes(Long totalDiskBytes) {
- this.totalDiskBytes = totalDiskBytes;
- }
-
- public Integer getDiskCount() {
- return diskCount;
- }
-
- public void setDiskCount(Integer diskCount) {
- this.diskCount = diskCount;
- }
-
- public Integer getNicCount() {
- return nicCount;
- }
-
- public void setNicCount(Integer nicCount) {
- this.nicCount = nicCount;
- }
-
- public Integer getGpuCount() {
- return gpuCount;
- }
-
- public void setGpuCount(Integer gpuCount) {
- this.gpuCount = gpuCount;
- }
-
- public String getHealthStatus() {
- return healthStatus;
- }
-
- public void setHealthStatus(String healthStatus) {
- this.healthStatus = healthStatus;
- }
-
- public String getDiscoverSource() {
- return discoverSource;
- }
-
- public void setDiscoverSource(String discoverSource) {
- this.discoverSource = discoverSource;
- }
-
public Timestamp getLastDiscoverDate() {
return lastDiscoverDate;
}
diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/CephMonAO.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/CephMonAO.java
index fe1f41b8303..17452d74d4f 100755
--- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/CephMonAO.java
+++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/CephMonAO.java
@@ -1,6 +1,6 @@
package org.zstack.storage.ceph;
-import org.zstack.core.convert.PasswordConverter;
+import org.zstack.header.core.convert.PasswordConverter;
import org.zstack.header.core.encrypt.EncryptColumn;
import org.zstack.header.vo.ResourceVO;
diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostVO.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostVO.java
index 9d3b7166766..76dbcc1c3f6 100755
--- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostVO.java
+++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostVO.java
@@ -1,6 +1,6 @@
package org.zstack.kvm;
-import org.zstack.core.convert.PasswordConverter;
+import org.zstack.header.core.convert.PasswordConverter;
import org.zstack.header.core.encrypt.EncryptColumn;
import org.zstack.header.host.HostEO;
import org.zstack.header.host.HostVO;
diff --git a/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerApiInterceptor.java b/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerApiInterceptor.java
index 8d7b5e2f267..9f5389a6744 100644
--- a/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerApiInterceptor.java
+++ b/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerApiInterceptor.java
@@ -9,7 +9,13 @@
import org.zstack.header.server.*;
import org.zstack.utils.network.NetworkUtils;
+import java.util.ArrayList;
+import java.util.List;
+
import static org.zstack.core.Platform.argerr;
+import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_PHYSICAL_SERVER_ROLE_DUPLICATE;
+import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_PHYSICAL_SERVER_SERIAL_NUMBER_DUPLICATE;
+import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_PROVISION_NETWORK_DHCP_MISSING;
public class PhysicalServerApiInterceptor implements ApiMessageInterceptor {
@Autowired
@@ -33,10 +39,25 @@ public APIMessage intercept(APIMessage msg) throws ApiMessageInterceptionExcepti
validate((APIDeleteProvisionNetworkMsg) msg);
} else if (msg instanceof APICreateProvisionNetworkMsg) {
validate((APICreateProvisionNetworkMsg) msg);
+ } else if (msg instanceof APIAttachPhysicalServerRoleMsg) {
+ validate((APIAttachPhysicalServerRoleMsg) msg);
}
return msg;
}
+ private void validate(APIAttachPhysicalServerRoleMsg msg) {
+ boolean exists = Q.New(PhysicalServerRoleVO.class)
+ .eq(PhysicalServerRoleVO_.serverUuid, msg.getServerUuid())
+ .eq(PhysicalServerRoleVO_.roleType, msg.getRoleType())
+ .isExists();
+ if (exists) {
+ throw new ApiMessageInterceptionException(argerr(
+ ORG_ZSTACK_PHYSICAL_SERVER_ROLE_DUPLICATE,
+ "PhysicalServer[uuid:%s] already has role[type:%s] attached; detach first or pick a different roleType",
+ msg.getServerUuid(), msg.getRoleType()));
+ }
+ }
+
private void validate(APICreateServerPoolMsg msg) {
// Zone existence validated by @APIParam(resourceType = ZoneVO.class)
}
@@ -56,6 +77,19 @@ private void validate(APICreatePhysicalServerMsg msg) {
));
}
}
+
+ if (msg.getSerialNumber() != null && !msg.getSerialNumber().isEmpty() && msg.getZoneUuid() != null) {
+ boolean dup = Q.New(PhysicalServerVO.class)
+ .eq(PhysicalServerAO_.zoneUuid, msg.getZoneUuid())
+ .eq(PhysicalServerAO_.serialNumber, msg.getSerialNumber())
+ .isExists();
+ if (dup) {
+ throw new ApiMessageInterceptionException(argerr(
+ ORG_ZSTACK_PHYSICAL_SERVER_SERIAL_NUMBER_DUPLICATE,
+ "PhysicalServer with serialNumber[%s] already exists in Zone[uuid:%s]; serialNumber must be unique per zone",
+ msg.getSerialNumber(), msg.getZoneUuid()));
+ }
+ }
}
private void validate(APIUpdatePhysicalServerMsg msg) {
@@ -77,6 +111,28 @@ private void validate(APICreateProvisionNetworkMsg msg) {
if (msg.getDhcpRangeGateway() != null && !NetworkUtils.isIpv4Address(msg.getDhcpRangeGateway())) {
throw new ApiMessageInterceptionException(argerr("invalid dhcpRangeGateway[%s]", msg.getDhcpRangeGateway()));
}
+
+ if ("GATEWAY_PXE".equals(msg.getType())) {
+ List missing = new ArrayList<>();
+ if (msg.getDhcpInterface() == null || msg.getDhcpInterface().isEmpty()) {
+ missing.add("dhcpInterface");
+ }
+ if (msg.getDhcpRangeStartIp() == null || msg.getDhcpRangeStartIp().isEmpty()) {
+ missing.add("dhcpRangeStartIp");
+ }
+ if (msg.getDhcpRangeEndIp() == null || msg.getDhcpRangeEndIp().isEmpty()) {
+ missing.add("dhcpRangeEndIp");
+ }
+ if (msg.getDhcpRangeNetmask() == null || msg.getDhcpRangeNetmask().isEmpty()) {
+ missing.add("dhcpRangeNetmask");
+ }
+ if (!missing.isEmpty()) {
+ throw new ApiMessageInterceptionException(argerr(
+ ORG_ZSTACK_PROVISION_NETWORK_DHCP_MISSING,
+ "ProvisionNetwork[type:GATEWAY_PXE] requires DHCP wiring; missing field(s): %s",
+ String.join(", ", missing)));
+ }
+ }
}
private void validate(APIDeleteServerPoolMsg msg) {
diff --git a/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerKvmIdentityBackfillExtension.java b/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerKvmIdentityBackfillExtension.java
new file mode 100644
index 00000000000..979ac1dae50
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerKvmIdentityBackfillExtension.java
@@ -0,0 +1,67 @@
+package org.zstack.server;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.zstack.compute.host.HostSystemTags;
+import org.zstack.core.db.DatabaseFacade;
+import org.zstack.core.db.Q;
+import org.zstack.header.core.Completion;
+import org.zstack.header.host.AbstractHostAddExtensionPoint;
+import org.zstack.header.host.HostInventory;
+import org.zstack.header.server.PhysicalServerRoleVO;
+import org.zstack.header.server.PhysicalServerRoleVO_;
+import org.zstack.header.server.PhysicalServerVO;
+import org.zstack.header.server.ServerRoleType;
+
+public class PhysicalServerKvmIdentityBackfillExtension extends AbstractHostAddExtensionPoint {
+ @Autowired
+ private DatabaseFacade dbf;
+
+ @Override
+ public void afterAddHost(HostInventory host, Completion completion) {
+ String hostUuid = host.getUuid();
+ PhysicalServerRoleVO role = Q.New(PhysicalServerRoleVO.class)
+ .eq(PhysicalServerRoleVO_.roleUuid, hostUuid)
+ .eq(PhysicalServerRoleVO_.roleType, ServerRoleType.KVM_HOST.toString())
+ .find();
+ if (role == null) {
+ completion.success();
+ return;
+ }
+
+ PhysicalServerVO ps = dbf.findByUuid(role.getServerUuid(), PhysicalServerVO.class);
+ if (ps == null) {
+ completion.success();
+ return;
+ }
+
+ boolean changed = false;
+ if (ps.getSerialNumber() == null) {
+ String sn = HostSystemTags.SYSTEM_SERIAL_NUMBER.getTokenByResourceUuid(
+ hostUuid, HostSystemTags.SYSTEM_SERIAL_NUMBER_TOKEN);
+ if (sn != null && !sn.isEmpty()) {
+ ps.setSerialNumber(sn);
+ changed = true;
+ }
+ }
+ if (ps.getManufacturer() == null) {
+ String mfr = HostSystemTags.SYSTEM_MANUFACTURER.getTokenByResourceUuid(
+ hostUuid, HostSystemTags.SYSTEM_MANUFACTURER_TOKEN);
+ if (mfr != null && !mfr.isEmpty()) {
+ ps.setManufacturer(mfr);
+ changed = true;
+ }
+ }
+ if (ps.getModel() == null) {
+ String model = HostSystemTags.SYSTEM_PRODUCT_NAME.getTokenByResourceUuid(
+ hostUuid, HostSystemTags.SYSTEM_PRODUCT_NAME_TOKEN);
+ if (model != null && !model.isEmpty()) {
+ ps.setModel(model);
+ changed = true;
+ }
+ }
+ if (changed) {
+ dbf.update(ps);
+ }
+ completion.success();
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerManagerImpl.java b/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerManagerImpl.java
index ec356b7744b..95adeefeb3e 100644
--- a/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerManagerImpl.java
+++ b/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerManagerImpl.java
@@ -164,16 +164,20 @@ private void handle(PingPhysicalServerMsg msg) {
}
private PhysicalServerPowerStatus probePowerStatus(PhysicalServerVO vo) {
- // Test seam: PhysicalServerPowerTracker.powerOverride is null in production; IT cases
- // set it to drive the handler without a real BMC. Mirrors the static-override pattern
- // used by PhysicalServerScanner.{probe,power}Override.
- if (PhysicalServerPowerTracker.powerOverride != null) {
- return PhysicalServerPowerTracker.powerOverride.apply(vo.getOobAddress(), vo.getOobUsername());
- }
if (vo.getOobAddress() == null || vo.getOobUsername() == null || vo.getOobPassword() == null) {
return PhysicalServerPowerStatus.POWER_UNKNOWN;
}
+ ShellResult ret = runIpmiPowerStatus(vo);
+ if (ret.getRetCode() != 0) {
+ return PhysicalServerPowerStatus.POWER_UNKNOWN;
+ }
+ return PhysicalServerPowerStatusParser.parse(ret.getStdout());
+ }
+ private ShellResult runIpmiPowerStatus(PhysicalServerVO vo) {
+ if (PhysicalServerPowerTracker.shellResultOverride != null) {
+ return PhysicalServerPowerTracker.shellResultOverride.apply(vo.getOobAddress(), vo.getOobUsername());
+ }
String passFile = PathUtil.createTempFileWithContent(vo.getOobPassword());
try {
int port = vo.getOobPort() == null ? 623 : vo.getOobPort();
@@ -183,11 +187,7 @@ private PhysicalServerPowerStatus probePowerStatus(PhysicalServerVO vo) {
port,
SshCmdHelper.shellQuote(vo.getOobUsername()),
SshCmdHelper.shellQuote(passFile));
- ShellResult ret = ShellUtils.runAndReturn(cmd);
- if (ret.getRetCode() != 0) {
- return PhysicalServerPowerStatus.POWER_UNKNOWN;
- }
- return PhysicalServerPowerStatusParser.parse(ret.getStdout());
+ return ShellUtils.runAndReturn(cmd);
} finally {
PathUtil.forceRemoveFile(passFile);
}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerPowerTracker.java b/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerPowerTracker.java
index 7b1ef02c1f8..26c25a0547e 100644
--- a/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerPowerTracker.java
+++ b/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerPowerTracker.java
@@ -15,6 +15,7 @@
import org.zstack.header.server.PhysicalServerPowerStatus;
import org.zstack.header.server.PhysicalServerVO;
import org.zstack.header.server.PingPhysicalServerMsg;
+import org.zstack.utils.ShellResult;
import org.zstack.utils.Utils;
import org.zstack.utils.logging.CLogger;
@@ -27,10 +28,7 @@ public class PhysicalServerPowerTracker extends PingTracker implements
ManagementNodeReadyExtensionPoint {
private static final CLogger logger = Utils.getLogger(PhysicalServerPowerTracker.class);
- // Test seam (UNIT_TEST_ON only): (oobAddress, oobUsername) -> simulated power status.
- // Consumed by PhysicalServerManagerImpl.handle(PingPhysicalServerMsg) so IT cases
- // can drive the tracker without a real BMC.
- public static volatile BiFunction powerOverride;
+ public static volatile BiFunction shellResultOverride;
@Autowired
private ResourceDestinationMaker destinationMaker;
diff --git a/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerScanner.java b/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerScanner.java
index b0818083c67..308dff240a5 100644
--- a/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerScanner.java
+++ b/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerScanner.java
@@ -36,13 +36,7 @@ public class PhysicalServerScanner {
private static final int DEFAULT_OOB_PORT = 623;
private static final int DEFAULT_TIMEOUT_PER_HOST = 3;
- // Test seam (UNIT_TEST_ON only): (ip, username) -> ProbeStatus override
- public static volatile BiFunction probeOverride;
-
- // Test seam (UNIT_TEST_ON only): (ip, username) -> simulated PhysicalServerPowerStatus.
- // Consulted only when probeOverride returns SUCCESS; defaults to POWER_UNKNOWN if unset
- // (preserves prior behavior of legacy IT cases that only set probeOverride).
- public static volatile BiFunction powerOverride;
+ public static volatile BiFunction shellResultOverride;
@Autowired
private DatabaseFacade dbf;
@@ -203,16 +197,25 @@ private ProbeResult probe(String ip, Integer oobPort, List credentia
}
private ProbeOutcome runProbe(String ip, Integer oobPort, Credential credential, Integer timeoutPerHost) {
- if (CoreGlobalProperty.UNIT_TEST_ON) {
- ProbeStatus status = probeOverride != null
- ? probeOverride.apply(ip, credential.username)
- : ProbeStatus.SUCCESS;
- PhysicalServerPowerStatus power = (status == ProbeStatus.SUCCESS && powerOverride != null)
- ? powerOverride.apply(ip, credential.username)
- : PhysicalServerPowerStatus.POWER_UNKNOWN;
- return new ProbeOutcome(status, power);
+ ShellResult ret = runIpmiPowerStatus(ip, oobPort, credential, timeoutPerHost);
+ if (ret.getRetCode() == 0) {
+ return new ProbeOutcome(ProbeStatus.SUCCESS, PhysicalServerPowerStatusParser.parse(ret.getStdout()));
}
+ ProbeStatus failStatus = isAuthFailure(ret) ? ProbeStatus.AUTH_FAILED : ProbeStatus.UNREACHABLE;
+ return new ProbeOutcome(failStatus, PhysicalServerPowerStatus.POWER_UNKNOWN);
+ }
+ private ShellResult runIpmiPowerStatus(String ip, Integer oobPort, Credential credential, Integer timeoutPerHost) {
+ if (CoreGlobalProperty.UNIT_TEST_ON) {
+ if (shellResultOverride != null) {
+ return shellResultOverride.apply(ip, credential.username);
+ }
+ ShellResult ret = new ShellResult();
+ ret.setRetCode(0);
+ ret.setStdout("Chassis Power is on");
+ ret.setStderr("");
+ return ret;
+ }
String passFile = PathUtil.createTempFileWithContent(credential.password);
try {
int timeout = timeoutPerHost == null ? DEFAULT_TIMEOUT_PER_HOST : Math.max(1, timeoutPerHost);
@@ -224,12 +227,7 @@ private ProbeOutcome runProbe(String ip, Integer oobPort, Credential credential,
port,
SshCmdHelper.shellQuote(credential.username),
SshCmdHelper.shellQuote(passFile));
- ShellResult ret = ShellUtils.runAndReturn(cmd);
- if (ret.getRetCode() == 0) {
- return new ProbeOutcome(ProbeStatus.SUCCESS, PhysicalServerPowerStatusParser.parse(ret.getStdout()));
- }
- ProbeStatus failStatus = isAuthFailure(ret) ? ProbeStatus.AUTH_FAILED : ProbeStatus.UNREACHABLE;
- return new ProbeOutcome(failStatus, PhysicalServerPowerStatus.POWER_UNKNOWN);
+ return ShellUtils.runAndReturn(cmd);
} finally {
PathUtil.forceRemoveFile(passFile);
}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/server/hardware/PhysicalServerHardwareService.java b/plugin/physicalServer/src/main/java/org/zstack/server/hardware/PhysicalServerHardwareService.java
index 3a68f072ba1..e39288863f8 100644
--- a/plugin/physicalServer/src/main/java/org/zstack/server/hardware/PhysicalServerHardwareService.java
+++ b/plugin/physicalServer/src/main/java/org/zstack/server/hardware/PhysicalServerHardwareService.java
@@ -65,7 +65,6 @@ public UnifiedHardwareInfo discoverHardware(String serverUuid) {
}
UnifiedHardwareInfo merged = new UnifiedHardwareInfo();
- String winningSource = null;
// P1-2: drop the per-source hasActiveRole() pre-check. The SPI's discover()
// contract now requires each impl to resolve its own role uuid exactly once
@@ -75,23 +74,12 @@ public UnifiedHardwareInfo discoverHardware(String serverUuid) {
// out-of-band link is configured at all (a server-level field, not a PSR
// query) — but BM2's adapter still validates its own role row inside discover.
if (server.getOobAddress() != null) {
- UnifiedHardwareInfo fru = runExt(SOURCE_IPMI_FRU, server);
- if (mergeNonNull(merged, fru)) {
- winningSource = SOURCE_IPMI_FRU;
- }
- }
-
- UnifiedHardwareInfo kvm = runExt(SOURCE_KVM_AGENT, server);
- if (mergeNonNull(merged, kvm) && winningSource == null) {
- winningSource = SOURCE_KVM_AGENT;
+ mergeNonNull(merged, runExt(SOURCE_IPMI_FRU, server));
}
+ mergeNonNull(merged, runExt(SOURCE_KVM_AGENT, server));
+ mergeNonNull(merged, runExt(SOURCE_K8S_NODEINFO, server));
- UnifiedHardwareInfo k8s = runExt(SOURCE_K8S_NODEINFO, server);
- if (mergeNonNull(merged, k8s) && winningSource == null) {
- winningSource = SOURCE_K8S_NODEINFO;
- }
-
- persistHardwareInfo(serverUuid, merged, winningSource);
+ persistHardwareInfo(serverUuid, merged);
return merged;
}
@@ -116,12 +104,6 @@ public UnifiedHardwareInfo getHardware(String serverUuid) {
info.setCpuCores(row.getCpuCores());
info.setCpuArchitecture(row.getCpuArchitecture());
info.setTotalMemoryBytes(row.getTotalMemoryBytes());
- info.setMemoryModuleCount(row.getMemoryModuleCount());
- info.setTotalDiskBytes(row.getTotalDiskBytes());
- info.setDiskCount(row.getDiskCount());
- info.setNicCount(row.getNicCount());
- info.setGpuCount(row.getGpuCount());
- info.setHealthStatus(row.getHealthStatus());
return info;
}
@@ -202,30 +184,6 @@ boolean mergeNonNull(UnifiedHardwareInfo target, UnifiedHardwareInfo source) {
target.setTotalMemoryBytes(source.getTotalMemoryBytes());
changed = true;
}
- if (target.getMemoryModuleCount() == null && source.getMemoryModuleCount() != null) {
- target.setMemoryModuleCount(source.getMemoryModuleCount());
- changed = true;
- }
- if (target.getTotalDiskBytes() == null && source.getTotalDiskBytes() != null) {
- target.setTotalDiskBytes(source.getTotalDiskBytes());
- changed = true;
- }
- if (target.getDiskCount() == null && source.getDiskCount() != null) {
- target.setDiskCount(source.getDiskCount());
- changed = true;
- }
- if (target.getNicCount() == null && source.getNicCount() != null) {
- target.setNicCount(source.getNicCount());
- changed = true;
- }
- if (target.getGpuCount() == null && source.getGpuCount() != null) {
- target.setGpuCount(source.getGpuCount());
- changed = true;
- }
- if (target.getHealthStatus() == null && source.getHealthStatus() != null) {
- target.setHealthStatus(source.getHealthStatus());
- changed = true;
- }
return changed;
}
@@ -233,7 +191,7 @@ boolean mergeNonNull(UnifiedHardwareInfo target, UnifiedHardwareInfo source) {
* Upsert merged hardware info. Existing row's non-null columns are preserved when the
* incoming value for the same column is null (mergeNonNull at the row level).
*/
- void persistHardwareInfo(String serverUuid, UnifiedHardwareInfo info, String discoverSource) {
+ void persistHardwareInfo(String serverUuid, UnifiedHardwareInfo info) {
PhysicalServerHardwareInfoVO existing = Q.New(PhysicalServerHardwareInfoVO.class)
.eq(PhysicalServerHardwareInfoVO_.serverUuid, serverUuid)
.find();
@@ -243,27 +201,17 @@ void persistHardwareInfo(String serverUuid, UnifiedHardwareInfo info, String dis
PhysicalServerHardwareInfoVO row = new PhysicalServerHardwareInfoVO();
row.setServerUuid(serverUuid);
applyNonNull(row, info);
- row.setDiscoverSource(discoverSource);
row.setLastDiscoverDate(now);
row.setCreateDate(now);
row.setLastOpDate(now);
dbf.persist(row);
- logger.debug(String.format("persisted hardware info for server[uuid:%s] source=%s", serverUuid, discoverSource));
+ logger.debug(String.format("persisted hardware info for server[uuid:%s]", serverUuid));
return;
}
applyNonNull(existing, info);
- // P1-3: first-writer-wins for discoverSource. The INSERT branch above writes the
- // initial source tag; subsequent passes refresh the data fields and lastDiscoverDate
- // but MUST NOT overwrite the source. Rationale: a fleet's discoverSource column
- // should be a stable signal of "who first identified this host" — not a churning
- // value that flips when an IPMI tier appears mid-life or a K8s-only adapter
- // contributes one extra field. Operators wanting "currently strongest contributor"
- // should derive it from the per-source field provenance once that's wired (out of
- // scope for v5.5.18); lastDiscoverDate alone tells when the row was last touched.
existing.setLastDiscoverDate(now);
dbf.update(existing);
- logger.debug(String.format("updated hardware info for server[uuid:%s] originalSource=%s",
- serverUuid, existing.getDiscoverSource()));
+ logger.debug(String.format("updated hardware info for server[uuid:%s]", serverUuid));
}
/**
@@ -299,23 +247,5 @@ private void applyNonNull(PhysicalServerHardwareInfoVO row, UnifiedHardwareInfo
if (info.getTotalMemoryBytes() != null) {
row.setTotalMemoryBytes(info.getTotalMemoryBytes());
}
- if (info.getMemoryModuleCount() != null) {
- row.setMemoryModuleCount(info.getMemoryModuleCount());
- }
- if (info.getTotalDiskBytes() != null) {
- row.setTotalDiskBytes(info.getTotalDiskBytes());
- }
- if (info.getDiskCount() != null) {
- row.setDiskCount(info.getDiskCount());
- }
- if (info.getNicCount() != null) {
- row.setNicCount(info.getNicCount());
- }
- if (info.getGpuCount() != null) {
- row.setGpuCount(info.getGpuCount());
- }
- if (info.getHealthStatus() != null) {
- row.setHealthStatus(info.getHealthStatus());
- }
}
}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/server/hardware/UnifiedHardwareInfo.java b/plugin/physicalServer/src/main/java/org/zstack/server/hardware/UnifiedHardwareInfo.java
index c07e56949d9..f7e04d7411d 100644
--- a/plugin/physicalServer/src/main/java/org/zstack/server/hardware/UnifiedHardwareInfo.java
+++ b/plugin/physicalServer/src/main/java/org/zstack/server/hardware/UnifiedHardwareInfo.java
@@ -24,23 +24,11 @@ public class UnifiedHardwareInfo implements HardwareInfoCarrier {
// CPU summary
private String cpuModel;
private Integer cpuSockets;
- private Integer cpuCores; // all sockets summed
+ private Integer cpuCores;
private String cpuArchitecture; // x86_64 / aarch64
// Memory summary
private Long totalMemoryBytes;
- private Integer memoryModuleCount;
-
- // Storage summary
- private Long totalDiskBytes;
- private Integer diskCount;
-
- // NIC / GPU summary
- private Integer nicCount;
- private Integer gpuCount;
-
- // Health
- private String healthStatus; // OK / Warning / Critical / Unknown
public String getManufacturer() {
return manufacturer;
@@ -86,6 +74,7 @@ public Integer getCpuSockets() {
return cpuSockets;
}
+ @Override
public void setCpuSockets(Integer cpuSockets) {
this.cpuSockets = cpuSockets;
}
@@ -94,6 +83,7 @@ public Integer getCpuCores() {
return cpuCores;
}
+ @Override
public void setCpuCores(Integer cpuCores) {
this.cpuCores = cpuCores;
}
@@ -113,52 +103,4 @@ public Long getTotalMemoryBytes() {
public void setTotalMemoryBytes(Long totalMemoryBytes) {
this.totalMemoryBytes = totalMemoryBytes;
}
-
- public Integer getMemoryModuleCount() {
- return memoryModuleCount;
- }
-
- public void setMemoryModuleCount(Integer memoryModuleCount) {
- this.memoryModuleCount = memoryModuleCount;
- }
-
- public Long getTotalDiskBytes() {
- return totalDiskBytes;
- }
-
- public void setTotalDiskBytes(Long totalDiskBytes) {
- this.totalDiskBytes = totalDiskBytes;
- }
-
- public Integer getDiskCount() {
- return diskCount;
- }
-
- public void setDiskCount(Integer diskCount) {
- this.diskCount = diskCount;
- }
-
- public Integer getNicCount() {
- return nicCount;
- }
-
- public void setNicCount(Integer nicCount) {
- this.nicCount = nicCount;
- }
-
- public Integer getGpuCount() {
- return gpuCount;
- }
-
- public void setGpuCount(Integer gpuCount) {
- this.gpuCount = gpuCount;
- }
-
- public String getHealthStatus() {
- return healthStatus;
- }
-
- public void setHealthStatus(String healthStatus) {
- this.healthStatus = healthStatus;
- }
}
diff --git a/plugin/physicalServer/src/test/java/org/zstack/server/hardware/UnifiedHardwareInfoMergeTest.java b/plugin/physicalServer/src/test/java/org/zstack/server/hardware/UnifiedHardwareInfoMergeTest.java
index 09981f4d2d9..c109133130e 100644
--- a/plugin/physicalServer/src/test/java/org/zstack/server/hardware/UnifiedHardwareInfoMergeTest.java
+++ b/plugin/physicalServer/src/test/java/org/zstack/server/hardware/UnifiedHardwareInfoMergeTest.java
@@ -10,13 +10,6 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-/**
- * U16: covers the most subtle invariant of {@link PhysicalServerHardwareService#mergeNonNull}
- * — null incoming fields must not clobber non-null target fields, and existing non-null
- * target values must not be overwritten by later contributors. The "first non-empty
- * source wins" semantics underpin {@code discoverSource} attribution and the post-restart
- * sticky behaviour for {@code serialNumber}.
- */
public class UnifiedHardwareInfoMergeTest {
private final PhysicalServerHardwareService svc = new PhysicalServerHardwareService();
@@ -86,15 +79,14 @@ public void mergingNullSourceIsSafe() {
@Test
public void numericZeroIsTreatedAsValue() {
- // gpuCount=0 is meaningful (host has no GPU); must not be skipped as "missing".
UnifiedHardwareInfo target = new UnifiedHardwareInfo();
UnifiedHardwareInfo source = new UnifiedHardwareInfo();
- source.setGpuCount(0);
+ source.setTotalMemoryBytes(0L);
boolean changed = svc.mergeNonNull(target, source);
assertTrue(changed);
- assertEquals(Integer.valueOf(0), target.getGpuCount());
+ assertEquals(Long.valueOf(0L), target.getTotalMemoryBytes());
}
@Test
@@ -114,12 +106,6 @@ public void allFieldsFlowThroughOnEmptyTarget() {
assertEquals(Integer.valueOf(64), target.getCpuCores());
assertEquals("x86_64", target.getCpuArchitecture());
assertEquals(Long.valueOf(549755813888L), target.getTotalMemoryBytes());
- assertEquals(Integer.valueOf(16), target.getMemoryModuleCount());
- assertEquals(Long.valueOf(8796093022208L), target.getTotalDiskBytes());
- assertEquals(Integer.valueOf(8), target.getDiskCount());
- assertEquals(Integer.valueOf(4), target.getNicCount());
- assertEquals(Integer.valueOf(2), target.getGpuCount());
- assertEquals("OK", target.getHealthStatus());
}
@Test
@@ -128,7 +114,7 @@ public void emptyTargetWithEmptySourceLeavesEverythingNull() {
boolean changed = svc.mergeNonNull(target, new UnifiedHardwareInfo());
assertFalse(changed);
assertNull(target.getManufacturer());
- assertNull(target.getCpuCores());
+ assertNull(target.getCpuModel());
assertNull(target.getTotalMemoryBytes());
}
@@ -148,19 +134,13 @@ public void applyNonNullKeepsVoFieldCoverageAlignedWithMergeNonNull() throws Exc
assertEquals(source.getCpuCores(), row.getCpuCores());
assertEquals(source.getCpuArchitecture(), row.getCpuArchitecture());
assertEquals(source.getTotalMemoryBytes(), row.getTotalMemoryBytes());
- assertEquals(source.getMemoryModuleCount(), row.getMemoryModuleCount());
- assertEquals(source.getTotalDiskBytes(), row.getTotalDiskBytes());
- assertEquals(source.getDiskCount(), row.getDiskCount());
- assertEquals(source.getNicCount(), row.getNicCount());
- assertEquals(source.getGpuCount(), row.getGpuCount());
- assertEquals(source.getHealthStatus(), row.getHealthStatus());
}
@Test
public void applyNonNullDoesNotClobberVoFieldsWithNullSourceValues() throws Exception {
PhysicalServerHardwareInfoVO row = new PhysicalServerHardwareInfoVO();
row.setSerialNumber("SN-FROM-DB");
- row.setGpuCount(0);
+ row.setCpuSockets(2);
UnifiedHardwareInfo source = new UnifiedHardwareInfo();
source.setCpuArchitecture("x86_64");
@@ -168,7 +148,7 @@ public void applyNonNullDoesNotClobberVoFieldsWithNullSourceValues() throws Exce
applyNonNull(row, source);
assertEquals("SN-FROM-DB", row.getSerialNumber());
- assertEquals(Integer.valueOf(0), row.getGpuCount());
+ assertEquals(Integer.valueOf(2), row.getCpuSockets());
assertEquals("x86_64", row.getCpuArchitecture());
}
@@ -183,12 +163,6 @@ private UnifiedHardwareInfo fullyPopulated() {
s.setCpuCores(64);
s.setCpuArchitecture("x86_64");
s.setTotalMemoryBytes(549755813888L);
- s.setMemoryModuleCount(16);
- s.setTotalDiskBytes(8796093022208L);
- s.setDiskCount(8);
- s.setNicCount(4);
- s.setGpuCount(2);
- s.setHealthStatus("OK");
return s;
}
diff --git a/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/SftpBackupStorageVO.java b/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/SftpBackupStorageVO.java
index ce164f34a39..68c8171800f 100755
--- a/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/SftpBackupStorageVO.java
+++ b/plugin/sftpBackupStorage/src/main/java/org/zstack/storage/backup/sftp/SftpBackupStorageVO.java
@@ -1,6 +1,6 @@
package org.zstack.storage.backup.sftp;
-import org.zstack.core.convert.PasswordConverter;
+import org.zstack.header.core.convert.PasswordConverter;
import org.zstack.header.storage.backup.BackupStorageEO;
import org.zstack.header.storage.backup.BackupStorageVO;
import org.zstack.header.tag.AutoDeleteTag;
diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/KvmTest.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/KvmTest.groovy
index b67b683bfe1..06756491be0 100755
--- a/test/src/test/groovy/org/zstack/test/integration/kvm/KvmTest.groovy
+++ b/test/src/test/groovy/org/zstack/test/integration/kvm/KvmTest.groovy
@@ -25,7 +25,6 @@ class KvmTest extends Test {
include("LongJobManager.xml")
include("HostAllocateExtension.xml")
include("PhysicalServerManager.xml")
- include("PhysicalServerTestProviders.xml")
}
@Override
diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/host/HostPasswordEncryptCase.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/host/HostPasswordEncryptCase.groovy
index ab5b58b656b..86ed2c65f3c 100644
--- a/test/src/test/groovy/org/zstack/test/integration/kvm/host/HostPasswordEncryptCase.groovy
+++ b/test/src/test/groovy/org/zstack/test/integration/kvm/host/HostPasswordEncryptCase.groovy
@@ -1,9 +1,9 @@
package org.zstack.test.integration.kvm.host
-import org.zstack.core.convert.PasswordConverter
+import org.zstack.header.core.convert.PasswordConverter
import org.zstack.core.db.Q
import org.zstack.core.db.SQL
-import org.zstack.core.encrypt.EncryptFacade
+import org.zstack.header.core.convert.EncryptFacade
import org.zstack.core.encrypt.EncryptFacadeImpl
import org.zstack.core.encrypt.EncryptGlobalConfig
import org.zstack.header.core.encrypt.EncryptEntityMetadataVO
diff --git a/test/src/test/groovy/org/zstack/test/integration/server/PhysicalServerInterceptorErrorsCase.groovy b/test/src/test/groovy/org/zstack/test/integration/server/PhysicalServerInterceptorErrorsCase.groovy
new file mode 100644
index 00000000000..1ef422c9e6c
--- /dev/null
+++ b/test/src/test/groovy/org/zstack/test/integration/server/PhysicalServerInterceptorErrorsCase.groovy
@@ -0,0 +1,164 @@
+package org.zstack.test.integration.server
+
+import org.zstack.header.errorcode.OperationFailureException
+import org.zstack.core.Platform
+import org.zstack.core.db.DatabaseFacade
+import org.zstack.header.server.PhysicalServerRoleVO
+import org.zstack.header.server.SchedulingMode
+import org.zstack.sdk.ClusterInventory
+import org.zstack.sdk.PhysicalServerInventory
+import org.zstack.sdk.PhysicalServerRoleInventory
+import org.zstack.sdk.ServerPoolInventory
+import org.zstack.sdk.ZoneInventory
+import org.zstack.test.integration.kvm.KvmTest
+import org.zstack.testlib.EnvSpec
+import org.zstack.testlib.SubCase
+
+class PhysicalServerInterceptorErrorsCase extends SubCase {
+ EnvSpec env
+
+ @Override
+ void clean() {
+ env.delete()
+ }
+
+ @Override
+ void setup() {
+ useSpring(KvmTest.springSpec)
+ }
+
+ @Override
+ void environment() {
+ env = makeEnv {
+ zone {
+ name = "zone"
+
+ cluster {
+ name = "cluster-85190"
+ hypervisorType = "KVM"
+ }
+ }
+ }
+ }
+
+ @Override
+ void test() {
+ env.create {
+ testDuplicateSerialNumberInSameZoneFailsLoudly()
+ testAttachSameRoleTwiceFailsLoudly()
+ testGatewayPxeRequiresDhcpFields()
+ }
+ }
+
+ private ServerPoolInventory createPool(String poolName) {
+ def zone = env.inventoryByName("zone") as ZoneInventory
+ return createServerPool {
+ name = poolName
+ zoneUuid = zone.uuid
+ } as ServerPoolInventory
+ }
+
+ private void persistKvmRole(String serverUuid, String clusterUuid) {
+ DatabaseFacade dbf = bean(DatabaseFacade.class)
+ PhysicalServerRoleVO role = new PhysicalServerRoleVO()
+ role.uuid = Platform.getUuid()
+ role.serverUuid = serverUuid
+ role.roleType = "KVM_HOST"
+ role.roleUuid = Platform.getUuid()
+ role.schedulingMode = SchedulingMode.INTERNAL_SHARED
+ dbf.persist(role)
+ }
+
+ void testDuplicateSerialNumberInSameZoneFailsLoudly() {
+ def zone = env.inventoryByName("zone") as ZoneInventory
+ def pool = createPool("pool-85184")
+
+ createPhysicalServer {
+ name = "ps-85184-first"
+ zoneUuid = zone.uuid
+ poolUuid = pool.uuid
+ managementIp = "10.0.85.1"
+ serialNumber = "TC-DUP-SN-01"
+ } as PhysicalServerInventory
+
+ expectApiFailure {
+ createPhysicalServer {
+ name = "ps-85184-dup"
+ zoneUuid = zone.uuid
+ poolUuid = pool.uuid
+ managementIp = "10.0.85.2"
+ serialNumber = "TC-DUP-SN-01"
+ }
+ } {
+ assert details.contains("serialNumber") || details.contains("TC-DUP-SN-01") :
+ "error details should name the offending serialNumber, got: ${details}"
+ assert !details.contains("could not execute statement") :
+ "user must not see Hibernate ConstraintViolationException leak; got: ${details}"
+ }
+ }
+
+ void testAttachSameRoleTwiceFailsLoudly() {
+ def zone = env.inventoryByName("zone") as ZoneInventory
+ def pool = createPool("pool-85190")
+
+ def server = createPhysicalServer {
+ name = "ps-85190"
+ zoneUuid = zone.uuid
+ poolUuid = pool.uuid
+ managementIp = "10.0.85.10"
+ serialNumber = "TC-85190"
+ } as PhysicalServerInventory
+
+ def cluster = env.inventoryByName("cluster-85190") as ClusterInventory
+
+ persistKvmRole(server.uuid, cluster.uuid)
+
+ expectApiFailure {
+ attachPhysicalServerRole {
+ delegate.serverUuid = server.uuid
+ roleType = "KVM_HOST"
+ clusterUuid = cluster.uuid
+ roleConfig = [
+ username: "root",
+ password: "password"
+ ]
+ }
+ } {
+ assert details.contains("KVM_HOST") :
+ "error details should name the offending roleType, got: ${details}"
+ assert details.contains("already has role") || details.contains("detach first") :
+ "error details should explain remedy (detach first), got: ${details}"
+ }
+ }
+
+ void testGatewayPxeRequiresDhcpFields() {
+ def zone = env.inventoryByName("zone") as ZoneInventory
+
+ expectApiFailure {
+ createProvisionNetwork {
+ name = "net-85350-bad"
+ zoneUuid = zone.uuid
+ type = "GATEWAY_PXE"
+ }
+ } {
+ assert details.contains("GATEWAY_PXE") :
+ "error details should name the offending type, got: ${details}"
+ ["dhcpInterface", "dhcpRangeStartIp", "dhcpRangeEndIp", "dhcpRangeNetmask"].each { f ->
+ assert details.contains(f) : "error details should list missing field ${f}, got: ${details}"
+ }
+ }
+
+ def good = createProvisionNetwork {
+ name = "net-85350-good"
+ zoneUuid = zone.uuid
+ type = "GATEWAY_PXE"
+ dhcpInterface = "eth0"
+ dhcpRangeStartIp = "192.168.50.10"
+ dhcpRangeEndIp = "192.168.50.100"
+ dhcpRangeNetmask = "255.255.255.0"
+ dhcpRangeGateway = "192.168.50.1"
+ }
+ assert good != null
+ deleteProvisionNetwork { uuid = good.uuid }
+ }
+}
diff --git a/test/src/test/groovy/org/zstack/test/integration/server/PhysicalServerKvmBackfillCase.groovy b/test/src/test/groovy/org/zstack/test/integration/server/PhysicalServerKvmBackfillCase.groovy
new file mode 100644
index 00000000000..f38b6ec94f4
--- /dev/null
+++ b/test/src/test/groovy/org/zstack/test/integration/server/PhysicalServerKvmBackfillCase.groovy
@@ -0,0 +1,191 @@
+package org.zstack.test.integration.server
+
+import org.zstack.core.db.Q
+import org.zstack.header.server.PhysicalServerAO_
+import org.zstack.header.server.PhysicalServerVO
+import org.zstack.kvm.KVMAgentCommands
+import org.zstack.sdk.ClusterInventory
+import org.zstack.sdk.HostInventory
+import org.zstack.sdk.PhysicalServerInventory
+import org.zstack.sdk.ServerPoolInventory
+import org.zstack.sdk.ZoneInventory
+import org.zstack.test.integration.kvm.KvmTest
+import org.zstack.test.integration.kvm.host.HostEnv
+import org.zstack.testlib.EnvSpec
+import org.zstack.testlib.SubCase
+
+import static org.zstack.kvm.KVMConstant.KVM_HOST_FACT_PATH
+
+class PhysicalServerKvmBackfillCase extends SubCase {
+ EnvSpec env
+
+ static final String FAKE_SN = "SN-BACKFILL-1234"
+ static final String FAKE_MFR = "Dell Inc."
+ static final String FAKE_MODEL = "PowerEdge R750"
+
+ @Override
+ void setup() {
+ useSpring(KvmTest.springSpec)
+ }
+
+ @Override
+ void environment() {
+ env = HostEnv.noHostBasicEnv()
+ }
+
+ @Override
+ void test() {
+ env.create {
+ installHostFactSimulator()
+ testTier3AutoCreatedPsBackfilled()
+ testPreResolvedPsBackfilled()
+ testUserSuppliedFieldNotOverwritten()
+ }
+ }
+
+ @Override
+ void clean() {
+ env.delete()
+ }
+
+ private void installHostFactSimulator() {
+ env.afterSimulator(KVM_HOST_FACT_PATH) { KVMAgentCommands.HostFactResponse rsp ->
+ rsp.setSystemSerialNumber(FAKE_SN)
+ rsp.setSystemManufacturer(FAKE_MFR)
+ rsp.setSystemProductName(FAKE_MODEL)
+ return rsp
+ }
+ }
+
+ void testTier3AutoCreatedPsBackfilled() {
+ def zone = env.inventoryByName("zone") as ZoneInventory
+ def cluster = env.inventoryByName("cluster") as ClusterInventory
+
+ def pool = createServerPool {
+ name = "pool-backfill-auto"
+ delegate.zoneUuid = zone.uuid
+ } as ServerPoolInventory
+
+ changeClusterServerPool {
+ delegate.clusterUuid = cluster.uuid
+ delegate.serverPoolUuid = pool.uuid
+ }
+
+ def host = addKVMHost {
+ name = "host-backfill-auto"
+ managementIp = "127.0.0.40"
+ clusterUuid = cluster.uuid
+ username = "root"
+ password = "password"
+ } as HostInventory
+
+ PhysicalServerVO ps = Q.New(PhysicalServerVO.class)
+ .eq(PhysicalServerAO_.managementIp, "127.0.0.40")
+ .eq(PhysicalServerAO_.zoneUuid, zone.uuid)
+ .find()
+ assert ps != null : "auto-created PS not found"
+ assert ps.serialNumber == FAKE_SN :
+ "PS.serialNumber expected=${FAKE_SN}, got=${ps.serialNumber}"
+ assert ps.manufacturer == FAKE_MFR :
+ "PS.manufacturer expected=${FAKE_MFR}, got=${ps.manufacturer}"
+ assert ps.model == FAKE_MODEL :
+ "PS.model expected=${FAKE_MODEL}, got=${ps.model}"
+
+ detachPhysicalServerRole { delegate.serverUuid = ps.uuid; roleType = "KVM_HOST" }
+ deleteHost { uuid = host.uuid }
+ deletePhysicalServer { uuid = ps.uuid }
+ deleteServerPool { uuid = pool.uuid }
+ }
+
+ void testPreResolvedPsBackfilled() {
+ def zone = env.inventoryByName("zone") as ZoneInventory
+ def cluster = env.inventoryByName("cluster") as ClusterInventory
+
+ def pool = createServerPool {
+ name = "pool-backfill-pre"
+ delegate.zoneUuid = zone.uuid
+ } as ServerPoolInventory
+
+ def ps = createPhysicalServer {
+ name = "ps-backfill-pre"
+ delegate.zoneUuid = zone.uuid
+ delegate.poolUuid = pool.uuid
+ managementIp = "127.0.0.41"
+ } as PhysicalServerInventory
+
+ // sanity: bare CreatePhysicalServer leaves identity fields null
+ PhysicalServerVO before = Q.New(PhysicalServerVO.class)
+ .eq(PhysicalServerAO_.uuid, ps.uuid)
+ .find()
+ assert before.serialNumber == null
+ assert before.manufacturer == null
+ assert before.model == null
+
+ def host = addKVMHost {
+ name = "host-backfill-pre"
+ managementIp = "127.0.0.41"
+ clusterUuid = cluster.uuid
+ username = "root"
+ password = "password"
+ delegate.serverUuid = ps.uuid
+ } as HostInventory
+
+ PhysicalServerVO after = Q.New(PhysicalServerVO.class)
+ .eq(PhysicalServerAO_.uuid, ps.uuid)
+ .find()
+ assert after.serialNumber == FAKE_SN :
+ "(pre-resolved): PS.serialNumber expected=${FAKE_SN}, got=${after.serialNumber}"
+ assert after.manufacturer == FAKE_MFR :
+ "(pre-resolved): PS.manufacturer expected=${FAKE_MFR}, got=${after.manufacturer}"
+ assert after.model == FAKE_MODEL :
+ "(pre-resolved): PS.model expected=${FAKE_MODEL}, got=${after.model}"
+
+ detachPhysicalServerRole { delegate.serverUuid = ps.uuid; roleType = "KVM_HOST" }
+ deleteHost { uuid = host.uuid }
+ deletePhysicalServer { uuid = ps.uuid }
+ deleteServerPool { uuid = pool.uuid }
+ }
+
+ void testUserSuppliedFieldNotOverwritten() {
+ def zone = env.inventoryByName("zone") as ZoneInventory
+ def cluster = env.inventoryByName("cluster") as ClusterInventory
+
+ def pool = createServerPool {
+ name = "pool-backfill-keep"
+ delegate.zoneUuid = zone.uuid
+ } as ServerPoolInventory
+
+ final String userMfr = "UserSetMfr"
+ def ps = createPhysicalServer {
+ name = "ps-backfill-keep"
+ delegate.zoneUuid = zone.uuid
+ delegate.poolUuid = pool.uuid
+ managementIp = "127.0.0.42"
+ delegate.manufacturer = userMfr
+ } as PhysicalServerInventory
+
+ def host = addKVMHost {
+ name = "host-backfill-keep"
+ managementIp = "127.0.0.42"
+ clusterUuid = cluster.uuid
+ username = "root"
+ password = "password"
+ delegate.serverUuid = ps.uuid
+ } as HostInventory
+
+ PhysicalServerVO after = Q.New(PhysicalServerVO.class)
+ .eq(PhysicalServerAO_.uuid, ps.uuid)
+ .find()
+ // user-supplied manufacturer preserved (null-only update)
+ assert after.manufacturer == userMfr :
+ "(idempotent): user-supplied manufacturer overwritten, got=${after.manufacturer}"
+ // null fields still backfilled
+ assert after.serialNumber == FAKE_SN
+ assert after.model == FAKE_MODEL
+
+ detachPhysicalServerRole { delegate.serverUuid = ps.uuid; roleType = "KVM_HOST" }
+ deleteHost { uuid = host.uuid }
+ deletePhysicalServer { uuid = ps.uuid }
+ deleteServerPool { uuid = pool.uuid }
+ }
+}
diff --git a/test/src/test/groovy/org/zstack/test/integration/server/PhysicalServerOobPasswordEncryptCase.groovy b/test/src/test/groovy/org/zstack/test/integration/server/PhysicalServerOobPasswordEncryptCase.groovy
new file mode 100644
index 00000000000..4b6f976902a
--- /dev/null
+++ b/test/src/test/groovy/org/zstack/test/integration/server/PhysicalServerOobPasswordEncryptCase.groovy
@@ -0,0 +1,154 @@
+package org.zstack.test.integration.server
+
+import org.zstack.core.db.Q
+import org.zstack.core.encrypt.EncryptGlobalConfig
+import org.zstack.header.core.encrypt.EncryptEntityMetadataVO
+import org.zstack.header.core.encrypt.EncryptEntityMetadataVO_
+import org.zstack.header.core.encrypt.EncryptEntityState
+import org.zstack.header.server.PhysicalServerAO_
+import org.zstack.header.server.PhysicalServerVO
+import org.zstack.sdk.PhysicalServerInventory
+import org.zstack.sdk.ServerPoolInventory
+import org.zstack.sdk.ZoneInventory
+import org.zstack.test.integration.kvm.KvmTest
+import org.zstack.testlib.EnvSpec
+import org.zstack.testlib.SubCase
+
+class PhysicalServerOobPasswordEncryptCase extends SubCase {
+ EnvSpec env
+
+ @Override
+ void clean() {
+ env.delete()
+ }
+
+ @Override
+ void setup() {
+ useSpring(KvmTest.springSpec)
+ spring {
+ include("encrypt.xml")
+ }
+ }
+
+ @Override
+ void environment() {
+ env = makeEnv {
+ zone {
+ name = "zone"
+ }
+ }
+ }
+
+ @Override
+ void test() {
+ env.create {
+ testOobPasswordFieldRegisteredForEncryption()
+ testOobPasswordRoundTripsWhenEncryptionEnabled()
+ testEncryptionDisabledStaysPlaintext()
+ }
+ }
+
+ private ServerPoolInventory createPool(String poolName) {
+ def zone = env.inventoryByName("zone") as ZoneInventory
+ return createServerPool {
+ name = poolName
+ zoneUuid = zone.uuid
+ } as ServerPoolInventory
+ }
+
+ private void enableEncryption() {
+ updateGlobalConfig {
+ category = EncryptGlobalConfig.CATEGORY
+ name = EncryptGlobalConfig.ENABLE_PASSWORD_ENCRYPT.name
+ value = "LocalEncryption"
+ }
+ }
+
+ private void disableEncryption() {
+ updateGlobalConfig {
+ category = EncryptGlobalConfig.CATEGORY
+ name = EncryptGlobalConfig.ENABLE_PASSWORD_ENCRYPT.name
+ value = "None"
+ }
+ }
+
+ void testOobPasswordFieldRegisteredForEncryption() {
+ EncryptEntityState state = Q.New(EncryptEntityMetadataVO.class)
+ .select(EncryptEntityMetadataVO_.state)
+ .eq(EncryptEntityMetadataVO_.entityName, PhysicalServerVO.class.getSimpleName())
+ .eq(EncryptEntityMetadataVO_.columnName, "oobPassword")
+ .findValue() as EncryptEntityState
+ assert state != null : "PhysicalServerVO.oobPassword should be registered in EncryptEntityMetadataVO; missing @Convert(PasswordConverter.class)?"
+ }
+
+ void testOobPasswordRoundTripsWhenEncryptionEnabled() {
+ enableEncryption()
+ retryInSecs {
+ assert Q.New(EncryptEntityMetadataVO.class)
+ .select(EncryptEntityMetadataVO_.state)
+ .eq(EncryptEntityMetadataVO_.entityName, PhysicalServerVO.class.getSimpleName())
+ .eq(EncryptEntityMetadataVO_.columnName, "oobPassword")
+ .findValue() == EncryptEntityState.Encrypted
+ }
+
+ def zone = env.inventoryByName("zone") as ZoneInventory
+ def pool = createPool("pool-encrypt")
+ def plaintext = "topSecret-OOB!"
+
+ def server = createPhysicalServer {
+ name = "ps-encrypt"
+ zoneUuid = zone.uuid
+ poolUuid = pool.uuid
+ managementIp = "192.168.71.1"
+ oobManagementType = "IPMI"
+ oobAddress = "192.168.100.1"
+ oobPort = 623
+ oobUsername = "admin"
+ oobPassword = plaintext
+ } as PhysicalServerInventory
+
+ String roundTrip = Q.New(PhysicalServerVO.class)
+ .select(PhysicalServerAO_.oobPassword)
+ .eq(PhysicalServerAO_.uuid, server.uuid)
+ .findValue() as String
+ assert roundTrip == plaintext : "oobPassword should round-trip back to plaintext via the converter"
+
+ deletePhysicalServer { uuid = server.uuid }
+ deleteServerPool { uuid = pool.uuid }
+ }
+
+ void testEncryptionDisabledStaysPlaintext() {
+ disableEncryption()
+ retryInSecs {
+ assert Q.New(EncryptEntityMetadataVO.class)
+ .select(EncryptEntityMetadataVO_.state)
+ .eq(EncryptEntityMetadataVO_.entityName, PhysicalServerVO.class.getSimpleName())
+ .eq(EncryptEntityMetadataVO_.columnName, "oobPassword")
+ .findValue() == EncryptEntityState.NewAdded
+ }
+
+ def zone = env.inventoryByName("zone") as ZoneInventory
+ def pool = createPool("pool-disabled")
+ def plaintext = "no-encrypt-mode"
+
+ def server = createPhysicalServer {
+ name = "ps-plain"
+ zoneUuid = zone.uuid
+ poolUuid = pool.uuid
+ managementIp = "192.168.71.3"
+ oobManagementType = "IPMI"
+ oobAddress = "192.168.100.3"
+ oobUsername = "admin"
+ oobPassword = plaintext
+ } as PhysicalServerInventory
+
+ String roundTrip = Q.New(PhysicalServerVO.class)
+ .select(PhysicalServerAO_.oobPassword)
+ .eq(PhysicalServerAO_.uuid, server.uuid)
+ .findValue() as String
+ assert roundTrip == plaintext : "with encryption disabled, the value must be unchanged"
+
+ deletePhysicalServer { uuid = server.uuid }
+ deleteServerPool { uuid = pool.uuid }
+ }
+}
diff --git a/test/src/test/groovy/org/zstack/test/integration/server/PhysicalServerOpsCase.groovy b/test/src/test/groovy/org/zstack/test/integration/server/PhysicalServerOpsCase.groovy
index 3cf65cd0cce..c7968b90b26 100644
--- a/test/src/test/groovy/org/zstack/test/integration/server/PhysicalServerOpsCase.groovy
+++ b/test/src/test/groovy/org/zstack/test/integration/server/PhysicalServerOpsCase.groovy
@@ -2,15 +2,11 @@ package org.zstack.test.integration.server
import org.zstack.core.cloudbus.CloudBus
import org.zstack.core.db.DatabaseFacade
-import org.zstack.header.longjob.LongJobState
-import org.zstack.header.longjob.LongJobVO
-import org.zstack.header.server.APIProvisionPhysicalServerMsg
import org.zstack.header.server.PhysicalServerConstant
import org.zstack.header.server.PhysicalServerPowerStatus
import org.zstack.header.server.PhysicalServerVO
import org.zstack.header.server.PingPhysicalServerMsg
import org.zstack.header.server.PingPhysicalServerReply
-import org.zstack.sdk.ImageInventory
import org.zstack.sdk.PhysicalServerInventory
import org.zstack.sdk.PhysicalServerProvisionNetworkInventory
import org.zstack.sdk.ServerPoolInventory
@@ -20,7 +16,7 @@ import org.zstack.server.PhysicalServerScanner
import org.zstack.test.integration.kvm.KvmTest
import org.zstack.testlib.EnvSpec
import org.zstack.testlib.SubCase
-import org.zstack.utils.gson.JSONObjectUtil
+import org.zstack.utils.ShellResult
// FR-032: Power Management, FR-033: Hardware Discovery, FR-034: Server Scan
class PhysicalServerOpsCase extends SubCase {
@@ -30,9 +26,6 @@ class PhysicalServerOpsCase extends SubCase {
@Override
void setup() {
useSpring(KvmTest.springSpec)
- spring {
- include("PhysicalServerTestProviders.xml")
- }
}
@Override
@@ -90,9 +83,6 @@ class PhysicalServerOpsCase extends SubCase {
testScanDedupLegacyManagementIpFallback()
testScanRecordsRealPowerStatus()
testPowerTrackerSyncsPowerStatus()
- // FR-012: ProvisionProvider orchestration
- testProvisionPhysicalServerStandaloneLongJob()
- testProvisionPhysicalServerNoProviderFailsLongJob()
// Supplementary
testQueryProvisionNetwork()
testDeleteProvisionNetworkBlockedByCluster()
@@ -149,37 +139,38 @@ class PhysicalServerOpsCase extends SubCase {
}
}
- private static final String PROVISION_NIC_MAC = "52:54:00:12:34:56"
+ // ShellResult helpers
- private void ensureProvisionNic(String serverUuid) {
- org.zstack.header.server.PhysicalServerHardwareDetailVO nic = new org.zstack.header.server.PhysicalServerHardwareDetailVO()
- nic.serverUuid = serverUuid
- nic.type = "NIC"
- nic.extraInfo = """{"mac":"${PROVISION_NIC_MAC}","primary":true}"""
- dbf.persistAndRefresh(nic)
+ private static ShellResult ipmiPowerOn() {
+ ShellResult r = new ShellResult()
+ r.retCode = 0
+ r.stdout = "Chassis Power is on"
+ r.stderr = ""
+ return r
}
- private LongJobVO submitProvisionJob(PhysicalServerInventory server,
- PhysicalServerProvisionNetworkInventory network,
- ImageInventory image) {
- ensureProvisionNic(server.uuid)
-
- APIProvisionPhysicalServerMsg msg = new APIProvisionPhysicalServerMsg()
- msg.serverUuid = server.uuid
- msg.networkUuid = network.uuid
- msg.osImageUuid = image.uuid
- msg.osDistribution = "rocky9"
- msg.kickstartTemplate = "install-script"
- msg.provisionNicMac = PROVISION_NIC_MAC
- msg.customParams = [role: "kvm", username: "root"]
-
- def job = submitLongJob {
- jobName = msg.class.simpleName
- jobData = JSONObjectUtil.toJsonString(msg)
- targetResourceUuid = server.uuid
- }
+ private static ShellResult ipmiPowerOff() {
+ ShellResult r = new ShellResult()
+ r.retCode = 0
+ r.stdout = "Chassis Power is off"
+ r.stderr = ""
+ return r
+ }
- return dbFindByUuid(job.uuid, LongJobVO.class)
+ private static ShellResult ipmiAuthFailed() {
+ ShellResult r = new ShellResult()
+ r.retCode = 1
+ r.stdout = ""
+ r.stderr = "Error: Unable to establish IPMI v2 / RMCP+ session\nauthentication failed"
+ return r
+ }
+
+ private static ShellResult ipmiUnreachable() {
+ ShellResult r = new ShellResult()
+ r.retCode = 1
+ r.stdout = ""
+ r.stderr = "Error: Unable to establish IPMI session: connection timed out"
+ return r
}
// --- FR-032: Power Management ---
@@ -269,12 +260,6 @@ class PhysicalServerOpsCase extends SubCase {
conditions = ["uuid=${server.uuid}".toString()]
}
assert queried.size() == 1
- // Hardware info should be populated after discover (TDD - will fail until implemented)
-
- // FR-003-AC1/AC2: After discovery, hardware info should be populated
- // TDD: When HardwareDiscoverable is implemented:
- // assert queried[0].hardwareInfo != null
- // assert queried[0].hardwareInfo.cpuModel != null
deletePhysicalServer { uuid = server.uuid }
deleteServerPool { uuid = pool.uuid }
@@ -373,84 +358,6 @@ class PhysicalServerOpsCase extends SubCase {
deleteServerPool { uuid = pool.uuid }
}
- // --- FR-012: ProvisionProvider orchestration ---
-
- // AC-PR-01: GATEWAY_PXE with registered provider — long job succeeds and jobResult contains serverUuid/networkUuid
- void testProvisionPhysicalServerStandaloneLongJob() {
- def zone = env.inventoryByName("zone") as ZoneInventory
- def pool = createPool("pool-provision-standalone")
- def server = createServerWithOob("server-provision-standalone", "192.168.62.1", pool.uuid)
- def image = env.inventoryByName("provision-rocky9") as ImageInventory
-
- def net = createProvisionNetwork {
- name = "pxe-provision-standalone"
- zoneUuid = zone.uuid
- type = "GATEWAY_PXE"
- } as PhysicalServerProvisionNetworkInventory
-
- attachProvisionNetworkToPool {
- networkUuid = net.uuid
- poolUuid = pool.uuid
- }
-
- LongJobVO job = submitProvisionJob(server, net, image)
-
- retryInSecs {
- job = dbFindByUuid(job.uuid, LongJobVO.class)
- assert job.state == LongJobState.Succeeded
- assert job.targetResourceUuid == server.uuid
- assert job.jobResult.contains(server.uuid)
- assert job.jobResult.contains(net.uuid)
- }
-
- detachProvisionNetworkFromPool {
- networkUuid = net.uuid
- poolUuid = pool.uuid
- }
- deleteProvisionNetwork { uuid = net.uuid }
- deletePhysicalServer { uuid = server.uuid }
- deleteServerPool { uuid = pool.uuid }
- }
-
- // AC-PR-02: STANDALONE_PXE provider is a reserved stub for phase-2 implementation —
- // long job must fail with a clear "reserved" error from the registered stub provider.
- void testProvisionPhysicalServerNoProviderFailsLongJob() {
- def zone = env.inventoryByName("zone") as ZoneInventory
- def pool = createPool("pool-provision-no-provider")
- def server = createServerWithOob("server-provision-no-provider", "192.168.62.2", pool.uuid)
- def image = env.inventoryByName("provision-no-provider") as ImageInventory
-
- def net = createProvisionNetwork {
- name = "pxe-provision-no-provider"
- zoneUuid = zone.uuid
- type = "STANDALONE_PXE"
- } as PhysicalServerProvisionNetworkInventory
-
- attachProvisionNetworkToPool {
- networkUuid = net.uuid
- poolUuid = pool.uuid
- }
-
- LongJobVO job = submitProvisionJob(server, net, image)
-
- retryInSecs {
- job = dbFindByUuid(job.uuid, LongJobVO.class)
- assert job.state == LongJobState.Failed
- // ZSTAC-84191: STANDALONE_PXE stub bean is registered (PhysicalServerManager.xml),
- // so lookup succeeds and startProvisioning returns the explicit "reserved" error
- // instead of the bare "no ProvisionProvider registered" lookup miss.
- assert job.jobResult.contains("STANDALONE_PXE ProvisionProvider is reserved and not implemented yet")
- }
-
- detachProvisionNetworkFromPool {
- networkUuid = net.uuid
- poolUuid = pool.uuid
- }
- deleteProvisionNetwork { uuid = net.uuid }
- deletePhysicalServer { uuid = server.uuid }
- deleteServerPool { uuid = pool.uuid }
- }
-
// --- Supplementary ---
// Query provision network by name
@@ -480,9 +387,8 @@ class PhysicalServerOpsCase extends SubCase {
def pool = createPool("pool-scan-cred-rotate")
// bad-user always AUTH_FAILED; good-user always SUCCESS
- PhysicalServerScanner.probeOverride = { String ip, String username ->
- username == "good-user" ? PhysicalServerScanner.ProbeStatus.SUCCESS
- : PhysicalServerScanner.ProbeStatus.AUTH_FAILED
+ PhysicalServerScanner.shellResultOverride = { String ip, String username ->
+ username == "good-user" ? ipmiPowerOn() : ipmiAuthFailed()
}
try {
@@ -506,35 +412,28 @@ class PhysicalServerOpsCase extends SubCase {
assert ps.oobUsername == "good-user"
}
} finally {
- PhysicalServerScanner.probeOverride = null
+ PhysicalServerScanner.shellResultOverride = null
deleteServersInPool(pool.uuid)
deleteServerPool { uuid = pool.uuid }
}
}
- // AC-PS-19: scan returns all 4 status counts correctly.
- // Note: scan input IP is the BMC/IPMI address — dedup is keyed on oobAddress
- // (next-session.md §A.2.5). createServerWithOob's helper sets oobAddress to
- // the 192.168.100.X namespace, so the scan range here uses that namespace,
- // and the pre-created PS is matched via oobAddress, not managementIp.
+ // AC-PS-19: scan returns all 4 status counts correctly
void testScanReturnsAllFourStatusCounts() {
def zone = env.inventoryByName("zone") as ZoneInventory
def pool = createPool("pool-scan-four-counts")
- // Pre-create an existing server so it shows up as existingCount.
- // helper produces oobAddress = "192.168.100.${last octet of managementIp}".
+ // pre-create an existing server so it shows up as existingCount
createServerWithOob("server-existing-63-2", "192.168.63.2", pool.uuid)
- // -> oobAddress = "192.168.100.2"
-
- // Map each oobAddress IP to its intended probe status
- def statusByIp = [
- "192.168.100.1": PhysicalServerScanner.ProbeStatus.SUCCESS, // discovered (new)
- "192.168.100.2": PhysicalServerScanner.ProbeStatus.SUCCESS, // existing (matched by oobAddress)
- "192.168.100.3": PhysicalServerScanner.ProbeStatus.AUTH_FAILED, // auth-failed
- "192.168.100.4": PhysicalServerScanner.ProbeStatus.UNREACHABLE, // unreachable
+
+ def shellByIp = [
+ "192.168.100.1": ipmiPowerOn(),
+ "192.168.100.2": ipmiPowerOn(),
+ "192.168.100.3": ipmiAuthFailed(),
+ "192.168.100.4": ipmiUnreachable(),
]
- PhysicalServerScanner.probeOverride = { String ip, String username ->
- statusByIp.getOrDefault(ip, PhysicalServerScanner.ProbeStatus.SUCCESS)
+ PhysicalServerScanner.shellResultOverride = { String ip, String username ->
+ shellByIp.getOrDefault(ip, ipmiPowerOn())
}
try {
@@ -552,25 +451,21 @@ class PhysicalServerOpsCase extends SubCase {
assert result.unreachableCount == 1
assert result.authFailedIps.contains("192.168.100.3")
} finally {
- PhysicalServerScanner.probeOverride = null
+ PhysicalServerScanner.shellResultOverride = null
deleteServersInPool(pool.uuid)
deleteServerPool { uuid = pool.uuid }
}
}
- // AC-PS-20: BMC IP is zone-globally unique; scanning the same IP into a second pool
- // must NOT create a duplicate row, it must return existingCount=1 against the pool-A row.
- // Regression for next-session.md §A.2.5: dedup was wrongly scoped to (zone, pool, managementIp)
- // so the same BMC IP on a different pool slipped past dedup and produced a duplicate
- // PhysicalServerVO. After fix, dedup is (zone, oobAddress) with managementIp legacy fallback.
+ // AC-PS-20: scanning the same BMC IP into a second pool must not create a duplicate row
void testScanDedupAcrossPools() {
def zone = env.inventoryByName("zone") as ZoneInventory
def poolA = createPool("pool-dedup-A")
def poolB = createPool("pool-dedup-B")
def sharedIp = "192.168.64.10"
- PhysicalServerScanner.probeOverride = { String ip, String username ->
- PhysicalServerScanner.ProbeStatus.SUCCESS
+ PhysicalServerScanner.shellResultOverride = { String ip, String username ->
+ ipmiPowerOn()
}
try {
@@ -603,29 +498,25 @@ class PhysicalServerOpsCase extends SubCase {
assert all.size() == 1
assert all[0].poolUuid == poolA.uuid
} finally {
- PhysicalServerScanner.probeOverride = null
+ PhysicalServerScanner.shellResultOverride = null
deleteServersInPool(poolA.uuid)
deleteServerPool { uuid = poolA.uuid }
deleteServerPool { uuid = poolB.uuid }
}
}
- // AC-PS-21: legacy data with oobAddress=NULL but managementIp set to the BMC IP
- // (created before scan started populating oobAddress) must be matched by the
- // managementIp fallback branch of findExisting, so a re-scan returns existing
- // instead of creating a parallel new row.
+ // AC-PS-21: legacy row with oobAddress=NULL matched by managementIp fallback on re-scan
void testScanDedupLegacyManagementIpFallback() {
def zone = env.inventoryByName("zone") as ZoneInventory
def pool = createPool("pool-dedup-legacy")
def legacyIp = "192.168.65.10"
- // Pre-create a server WITHOUT oobAddress (legacy/migrated row); managementIp is the BMC IP.
- // Production API path (createPhysicalServer without oob fields) — no direct dbf write.
+ // pre-create a server without oobAddress (legacy row); managementIp is the BMC IP
createServerWithoutOob("server-legacy-65-10", legacyIp, pool.uuid)
- PhysicalServerScanner.probeOverride = { String ip, String username ->
- PhysicalServerScanner.ProbeStatus.SUCCESS
+ PhysicalServerScanner.shellResultOverride = { String ip, String username ->
+ ipmiPowerOn()
}
try {
@@ -646,23 +537,19 @@ class PhysicalServerOpsCase extends SubCase {
}
assert all.size() == 1
} finally {
- PhysicalServerScanner.probeOverride = null
+ PhysicalServerScanner.shellResultOverride = null
deleteServersInPool(pool.uuid)
deleteServerPool { uuid = pool.uuid }
}
}
- // next-session.md §B.3.1: scan probe must record real OOB power status, not hardcode POWER_UNKNOWN.
- // probeOverride forces SUCCESS; powerOverride injects per-IP power; assert PS.powerStatus matches.
+ // scan probe records real OOB power status, not hardcoded POWER_UNKNOWN
void testScanRecordsRealPowerStatus() {
def zone = env.inventoryByName("zone") as ZoneInventory
def pool = createPool("pool-scan-power")
- PhysicalServerScanner.probeOverride = { String ip, String username ->
- PhysicalServerScanner.ProbeStatus.SUCCESS
- }
- PhysicalServerScanner.powerOverride = { String ip, String username ->
- ip.endsWith(".1") ? PhysicalServerPowerStatus.POWER_ON : PhysicalServerPowerStatus.POWER_OFF
+ PhysicalServerScanner.shellResultOverride = { String ip, String username ->
+ ip.endsWith(".1") ? ipmiPowerOn() : ipmiPowerOff()
}
try {
@@ -681,29 +568,23 @@ class PhysicalServerOpsCase extends SubCase {
assert s1.powerStatus == "POWER_ON"
assert s2.powerStatus == "POWER_OFF"
} finally {
- PhysicalServerScanner.probeOverride = null
- PhysicalServerScanner.powerOverride = null
+ PhysicalServerScanner.shellResultOverride = null
deleteServersInPool(pool.uuid)
deleteServerPool { uuid = pool.uuid }
}
}
- // next-session.md §B.3.2: PowerTracker periodic OOB probe must reconcile PS.powerStatus.
- // Mock PowerTracker.powerOverride and dispatch a PingPhysicalServerMsg directly via the bus
- // (avoids waiting on the periodic scheduler); assert DB row reflects probed value.
+ // PowerTracker OOB probe reconciles PS.powerStatus
void testPowerTrackerSyncsPowerStatus() {
def pool = createPool("pool-power-tracker")
def server = createServerWithOob("server-power-tracker", "192.168.67.1", pool.uuid)
def bus = bean(CloudBus.class) as CloudBus
- // server is created via API → ManagerImpl path, which still hardcodes POWER_UNKNOWN
- // (B.3.1 fix only covers scan-time path). PowerTracker is the mechanism that brings
- // it to a real value.
def beforeVo = dbFindByUuid(server.uuid, PhysicalServerVO.class)
assert beforeVo.powerStatus == PhysicalServerPowerStatus.POWER_UNKNOWN
- PhysicalServerPowerTracker.powerOverride = { String ip, String username ->
- PhysicalServerPowerStatus.POWER_OFF
+ PhysicalServerPowerTracker.shellResultOverride = { String ip, String username ->
+ ipmiPowerOff()
}
try {
@@ -717,7 +598,7 @@ class PhysicalServerOpsCase extends SubCase {
def afterVo = dbFindByUuid(server.uuid, PhysicalServerVO.class)
assert afterVo.powerStatus == PhysicalServerPowerStatus.POWER_OFF
} finally {
- PhysicalServerPowerTracker.powerOverride = null
+ PhysicalServerPowerTracker.shellResultOverride = null
deletePhysicalServer { uuid = server.uuid }
deleteServerPool { uuid = pool.uuid }
}
diff --git a/test/src/test/java/org/zstack/test/TestGatewayPxeProvisionProvider.java b/test/src/test/java/org/zstack/test/TestGatewayPxeProvisionProvider.java
deleted file mode 100644
index d5ad012e13d..00000000000
--- a/test/src/test/java/org/zstack/test/TestGatewayPxeProvisionProvider.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package org.zstack.test;
-
-import org.zstack.header.core.Completion;
-import org.zstack.header.core.ReturnValueCompletion;
-import org.zstack.header.server.*;
-
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * Deterministic OSS test-only provider for GATEWAY_PXE.
- * Captures the ProvisionRequest so tests can assert PhysicalServer-first fields.
- * Not imported by any premium BM2 code.
- */
-public class TestGatewayPxeProvisionProvider implements ProvisionProvider {
-
- private final AtomicReference lastRequest = new AtomicReference<>();
-
- @Override
- public ProvisionNetworkType getType() {
- return ProvisionNetworkType.GATEWAY_PXE;
- }
-
- @Override
- public void prepareNetwork(PhysicalServerProvisionNetworkInventory network,
- String poolUuid,
- Completion completion) {
- completion.success();
- }
-
- @Override
- public void destroyNetwork(PhysicalServerProvisionNetworkInventory network,
- String poolUuid,
- Completion completion) {
- completion.success();
- }
-
- @Override
- public void startProvisioning(ProvisionRequest request,
- ReturnValueCompletion completion) {
- lastRequest.set(request);
- ProvisionResult result = new ProvisionResult()
- .setServerUuid(request.getServerUuid())
- .setNetworkUuid(request.getNetworkUuid())
- .setProviderType(getType().toString())
- .setProviderResourceUuid(request.getServerUuid());
- completion.success(result);
- }
-
- public ProvisionRequest getLastRequest() {
- return lastRequest.get();
- }
-
- public void reset() {
- lastRequest.set(null);
- }
-}
diff --git a/test/src/test/resources/springConfigXml/PhysicalServerTestProviders.xml b/test/src/test/resources/springConfigXml/PhysicalServerTestProviders.xml
deleted file mode 100644
index 4b75af9d4d8..00000000000
--- a/test/src/test/resources/springConfigXml/PhysicalServerTestProviders.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy
index 855f88d17c3..7cbd3907681 100644
--- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy
+++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy
@@ -28151,6 +28151,33 @@ abstract class ApiHelper {
}
+ def mountModelToVmInstance(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.MountModelToVmInstanceAction.class) Closure c) {
+ def a = new org.zstack.sdk.MountModelToVmInstanceAction()
+ a.sessionId = Test.currentEnvSpec?.session?.uuid
+ c.resolveStrategy = Closure.OWNER_FIRST
+ c.delegate = a
+ c()
+
+
+ if (System.getProperty("apipath") != null) {
+ if (a.apiId == null) {
+ a.apiId = Platform.uuid
+ }
+
+ def tracker = new ApiPathTracker(a.apiId)
+ def out = errorOut(a.call())
+ def path = tracker.getApiPath()
+ if (!path.isEmpty()) {
+ Test.apiPaths[a.class.name] = path.join(" --->\n")
+ }
+
+ return out
+ } else {
+ return errorOut(a.call())
+ }
+ }
+
+
def mountVmInstanceRecoveryPoint(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.MountVmInstanceRecoveryPointAction.class) Closure c) {
def a = new org.zstack.sdk.MountVmInstanceRecoveryPointAction()
a.sessionId = Test.currentEnvSpec?.session?.uuid
@@ -36814,6 +36841,35 @@ abstract class ApiHelper {
}
+ def queryVmModelMount(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryVmModelMountAction.class) Closure c) {
+ def a = new org.zstack.sdk.QueryVmModelMountAction()
+ a.sessionId = Test.currentEnvSpec?.session?.uuid
+ c.resolveStrategy = Closure.OWNER_FIRST
+ c.delegate = a
+ c()
+
+ a.conditions = a.conditions.collect { it.toString() }
+
+
+ if (System.getProperty("apipath") != null) {
+ if (a.apiId == null) {
+ a.apiId = Platform.uuid
+ }
+
+ def tracker = new ApiPathTracker(a.apiId)
+ def out = errorOut(a.call())
+ def path = tracker.getApiPath()
+ if (!path.isEmpty()) {
+ Test.apiPaths[a.class.name] = path.join(" --->\n")
+ }
+
+ return out
+ } else {
+ return errorOut(a.call())
+ }
+ }
+
+
def queryVmNic(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.QueryVmNicAction.class) Closure c) {
def a = new org.zstack.sdk.QueryVmNicAction()
a.sessionId = Test.currentEnvSpec?.session?.uuid
@@ -43304,6 +43360,33 @@ abstract class ApiHelper {
}
+ def unmountModelFromVmInstance(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UnmountModelFromVmInstanceAction.class) Closure c) {
+ def a = new org.zstack.sdk.UnmountModelFromVmInstanceAction()
+ a.sessionId = Test.currentEnvSpec?.session?.uuid
+ c.resolveStrategy = Closure.OWNER_FIRST
+ c.delegate = a
+ c()
+
+
+ if (System.getProperty("apipath") != null) {
+ if (a.apiId == null) {
+ a.apiId = Platform.uuid
+ }
+
+ def tracker = new ApiPathTracker(a.apiId)
+ def out = errorOut(a.call())
+ def path = tracker.getApiPath()
+ if (!path.isEmpty()) {
+ Test.apiPaths[a.class.name] = path.join(" --->\n")
+ }
+
+ return out
+ } else {
+ return errorOut(a.call())
+ }
+ }
+
+
def unmountVmInstanceRecoveryPoint(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UnmountVmInstanceRecoveryPointAction.class) Closure c) {
def a = new org.zstack.sdk.UnmountVmInstanceRecoveryPointAction()
a.sessionId = Test.currentEnvSpec?.session?.uuid
diff --git a/testlib/src/main/java/org/zstack/testlib/EnvSpec.groovy b/testlib/src/main/java/org/zstack/testlib/EnvSpec.groovy
index 5491d321f1b..3bd7c9b7550 100755
--- a/testlib/src/main/java/org/zstack/testlib/EnvSpec.groovy
+++ b/testlib/src/main/java/org/zstack/testlib/EnvSpec.groovy
@@ -24,6 +24,9 @@ import org.zstack.header.identity.SessionVO
import org.zstack.header.image.GuestOsCategoryVO
import org.zstack.header.image.ImageDeletionPolicyManager
import org.zstack.header.message.Message
+import org.zstack.header.server.PhysicalServerProvisionNetworkClusterRefVO
+import org.zstack.header.server.PhysicalServerProvisionNetworkPoolRefVO
+import org.zstack.header.server.PhysicalServerProvisionNetworkVO
import org.zstack.header.rest.RESTConstant
import org.zstack.header.vm.VmInstanceDeletionPolicyManager
import org.zstack.header.vm.VmSchedHistoryVO
@@ -860,6 +863,12 @@ class EnvSpec extends ApiHelper implements Node {
GLOBAL_DELETE_HOOK()
}
+ // Detach provision networks before EO cleanup deletes them: a cluster/pool-attached
+ // PhysicalServerProvisionNetwork blocks deletion and leaks across stability iterations.
+ SQL.New(PhysicalServerProvisionNetworkClusterRefVO.class).hardDelete()
+ SQL.New(PhysicalServerProvisionNetworkPoolRefVO.class).hardDelete()
+ SQL.New(PhysicalServerProvisionNetworkVO.class).hardDelete()
+
cleanupEO()
makeSureAllEntitiesDeleted()
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 7105b0904ba..62b84ba3625 100644
--- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java
+++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java
@@ -16083,4 +16083,10 @@ public class CloudOperationsErrorCode {
public static final String ORG_ZSTACK_DGPU_10011 = "ORG_ZSTACK_DGPU_10011";
public static final String ORG_ZSTACK_DGPU_10012 = "ORG_ZSTACK_DGPU_10012";
+
+ public static final String ORG_ZSTACK_PHYSICAL_SERVER_ROLE_DUPLICATE = "ORG_ZSTACK_PHYSICAL_SERVER_ROLE_DUPLICATE";
+
+ public static final String ORG_ZSTACK_PHYSICAL_SERVER_SERIAL_NUMBER_DUPLICATE = "ORG_ZSTACK_PHYSICAL_SERVER_SERIAL_NUMBER_DUPLICATE";
+
+ public static final String ORG_ZSTACK_PROVISION_NETWORK_DHCP_MISSING = "ORG_ZSTACK_PROVISION_NETWORK_DHCP_MISSING";
}