Relocated from {@code org.zstack.core.convert} to header in ZSTAC-85182 so
+ * header-resident entities (e.g. {@code PhysicalServerAO.oobPassword}) can
+ * apply {@code @Convert(converter = PasswordConverter.class)} directly. The
+ * gating against the global {@code enable.password.encrypt} toggle moved to
+ * {@link EncryptFacade#isEncryptionDisabled()} to keep this class free of any
+ * {@code core} import.
*/
@Component
@Converter
@@ -32,7 +36,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 +47,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..088141d09ee 100644
--- a/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerApiInterceptor.java
+++ b/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerApiInterceptor.java
@@ -33,10 +33,31 @@ 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;
}
+ /**
+ * ZSTAC-85190: pre-check (serverUuid, roleType) uniqueness so the caller gets a
+ * named, actionable error instead of the SYS.1006 "An operation failed, details:
+ * <serverUuid>" the deeper handler currently surfaces. The handler still holds
+ * the PESSIMISTIC_WRITE serialization lock as a race guard.
+ */
+ 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,22 @@ private void validate(APICreatePhysicalServerMsg msg) {
));
}
}
+
+ // ZSTAC-85184: pre-check serialNumber uniqueness within zone so callers see a
+ // readable error instead of the SYS.1000 ConstraintViolationException bubbling
+ // from the DB. The DB unique key remains as a belt-and-braces guard for races.
+ 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 +114,32 @@ private void validate(APICreateProvisionNetworkMsg msg) {
if (msg.getDhcpRangeGateway() != null && !NetworkUtils.isIpv4Address(msg.getDhcpRangeGateway())) {
throw new ApiMessageInterceptionException(argerr("invalid dhcpRangeGateway[%s]", msg.getDhcpRangeGateway()));
}
+
+ // ZSTAC-85350: GATEWAY_PXE requires DHCP wiring to be usable as a PXE network.
+ // Pre-fix the API would persist a half-built ProvisionNetwork with Enabled state
+ // that would later fail at provisioning time. Surface the missing fields up-front
+ // with one consolidated error so the caller knows exactly what to supply.
+ if ("GATEWAY_PXE".equals(msg.getType())) {
+ java.util.List missing = new java.util.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..f1f7f8693e8
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/server/PhysicalServerKvmIdentityBackfillExtension.java
@@ -0,0 +1,89 @@
+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;
+
+/**
+ * QA gap (Confluence pageId=208903964 #2) — PRD §2.5.1 requires
+ * {@code PhysicalServerVO.serialNumber / manufacturer / model} to be backfilled from
+ * Connect-stage signals so {@code QueryPhysicalServer} reflects basic identity
+ * without waiting for the async HardwareDiscoveryQueue.
+ *
+ * The natural candidate ({@code InitPhysicalServerCapacityFlow}) runs at the head
+ * of the AddHost chain (positions 2-5/10) because RoleVO + PSC must exist BEFORE
+ * the connect flow runs (ADR-012 fail-loud ordering, NB-24). That is too early —
+ * {@code saveGeneralHostHardwareFacts} writes {@code HostSystemTags.SYSTEM_*} only
+ * after {@code send-connect-host-message} (position 7/10). Reading SystemTag from
+ * InitFlow returns null and the backfill silently no-ops.
+ *
+ *
This extension hooks {@code call-after-add-host-extension} (position 10/10),
+ * which fires after the connect flow and the SystemTag writes. By then the host
+ * has been added, RoleVO + PSC are persisted, and the SystemTag tokens are
+ * available. Null-only update preserves any value the user supplied on path-1
+ * ({@code APICreatePhysicalServer}) or set out of band.
+ *
+ *
KVM only — BM2 / Container chassis have no top-level identity columns;
+ * their backfill ships once the FRU / nodeInfo discovery adapters land.
+ */
+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..1af6c6caf47 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,23 @@ 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) {
+ // Test seam: PhysicalServerPowerTracker.shellResultOverride is null in production;
+ // IT cases set it to simulate the `chassis power status` shell-out leaf. Prod-side
+ // PowerStatusParser always runs on the returned ShellResult.
+ 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 +190,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..ed36799baba 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,11 @@ 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;
+ // Test seam (UNIT_TEST_ON only): (oobAddress, oobUsername) -> raw ShellResult from
+ // a simulated `ipmitool ... chassis power status`. Consumed by
+ // PhysicalServerManagerImpl.handle(PingPhysicalServerMsg). Prod-side parser always
+ // runs on the result so the parsing path is covered.
+ 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..e0321f0c446 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,10 @@ 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;
+ // Test seam (UNIT_TEST_ON only): (ip, username) -> raw ShellResult from a simulated
+ // `ipmitool ... chassis power status`. Prod-side PowerStatusParser + isAuthFailure
+ // classifier always run on the result, so cases exercise the real parsing/classification.
+ public static volatile BiFunction shellResultOverride;
@Autowired
private DatabaseFacade dbf;
@@ -203,16 +200,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 +230,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..fa1af99295d 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
@@ -86,15 +86,16 @@ public void mergingNullSourceIsSafe() {
@Test
public void numericZeroIsTreatedAsValue() {
- // gpuCount=0 is meaningful (host has no GPU); must not be skipped as "missing".
+ // totalMemoryBytes=0L would be nonsense in prod, but mergeNonNull must not treat
+ // numeric-zero as "missing" — null is the only "missing" sentinel.
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 +115,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 +123,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 +143,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 +157,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 +172,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..bb176d71a66
--- /dev/null
+++ b/test/src/test/groovy/org/zstack/test/integration/server/PhysicalServerInterceptorErrorsCase.groovy
@@ -0,0 +1,194 @@
+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
+
+/**
+ * Argument-validation regressions surfaced by QA review of v5.5.18 hardware-unified APIs.
+ * Each test pre-cooks the conflicting / under-specified state, then verifies the API
+ * fail-message names the offending field instead of bubbling a SYS.1000 / SYS.1006
+ * generic "internal error" / "operation failed".
+ *
+ *
+ * - ZSTAC-85184: CreatePhysicalServer with duplicate serialNumber in same zone
+ * - ZSTAC-85190: AttachPhysicalServerRole binding the same roleType twice
+ * - ZSTAC-85350: CreateProvisionNetwork type=GATEWAY_PXE without DHCP wiring
+ *
+ */
+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)
+ }
+
+ // ----------------------------------------------------------------
+ // ZSTAC-85184
+ // ----------------------------------------------------------------
+
+ 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}"
+ }
+ }
+
+ // ----------------------------------------------------------------
+ // ZSTAC-85190
+ // ----------------------------------------------------------------
+
+ 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
+
+ // Fixture-style seed of an existing KVM_HOST role row to bypass the real
+ // connect flow (no live host on the test runner). The interceptor pre-check
+ // we are exercising runs entirely off PhysicalServerRoleVO, so a persisted
+ // row is all that is required to drive the error-path under test.
+ 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}"
+ }
+ }
+
+ // ----------------------------------------------------------------
+ // ZSTAC-85350
+ // ----------------------------------------------------------------
+
+ void testGatewayPxeRequiresDhcpFields() {
+ def zone = env.inventoryByName("zone") as ZoneInventory
+
+ expectApiFailure {
+ createProvisionNetwork {
+ name = "net-85350-bad"
+ zoneUuid = zone.uuid
+ type = "GATEWAY_PXE"
+ // DHCP wiring intentionally omitted
+ }
+ } {
+ 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}"
+ }
+ }
+
+ // Sanity: with all DHCP fields supplied, the create succeeds.
+ 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..d1fab6f0356
--- /dev/null
+++ b/test/src/test/groovy/org/zstack/test/integration/server/PhysicalServerKvmBackfillCase.groovy
@@ -0,0 +1,206 @@
+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
+
+/**
+ * QA gap (Confluence pageId=208903964 #2) — 真实物理机 path-2 添加后 PhysicalServerVO
+ * 的 serialNumber / manufacturer / model 一直为 null,违反 PRD §2.5.1 明文要求:
+ * "这些字段由 InitPhysicalServerCapacityFlow(或 AutoAssociateFlow)从 Connect 阶段
+ * 已获取的基础信息回填 —— 不依赖 HardwareDiscoveryQueue"。
+ *
+ * 数据通路:KVM agent (simulator override) -> HostFactResponse.systemSerialNumber/
+ * SystemManufacturer/SystemProductName -> saveGeneralHostHardwareFacts() 写 HostSystemTag
+ * -> InitPhysicalServerCapacityFlow 读 tag null-only 回填 PS.
+ *
+ * 覆盖:
+ * testTier3AutoCreatedPsBackfilled — AutoAssociate 新建 PS(三字段全 null)→ 全部回填
+ * testPreResolvedPsBackfilled — caller 提供 serverUuid,预建 PS(三字段全 null)→ 全部回填
+ * testUserSuppliedFieldNotOverwritten — 预建 PS 手填 manufacturer → backfill 不动它
+ */
+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 :
+ "QA-2 失败: PS.serialNumber expected=${FAKE_SN}, got=${ps.serialNumber}"
+ assert ps.manufacturer == FAKE_MFR :
+ "QA-2 失败: PS.manufacturer expected=${FAKE_MFR}, got=${ps.manufacturer}"
+ assert ps.model == FAKE_MODEL :
+ "QA-2 失败: 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 :
+ "QA-2 失败 (pre-resolved): PS.serialNumber expected=${FAKE_SN}, got=${after.serialNumber}"
+ assert after.manufacturer == FAKE_MFR :
+ "QA-2 失败 (pre-resolved): PS.manufacturer expected=${FAKE_MFR}, got=${after.manufacturer}"
+ assert after.model == FAKE_MODEL :
+ "QA-2 失败 (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 :
+ "QA-2 失败 (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..be97c643d26
--- /dev/null
+++ b/test/src/test/groovy/org/zstack/test/integration/server/PhysicalServerOobPasswordEncryptCase.groovy
@@ -0,0 +1,181 @@
+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
+
+/**
+ * ZSTAC-85182: PhysicalServerVO.oobPassword must be encrypt-at-rest.
+ *
+ * The fix relocates {@code PasswordConverter} and {@code EncryptFacade} to
+ * {@code header.core.encrypt} so {@link org.zstack.header.server.PhysicalServerAO}
+ * can wire {@code @Convert(PasswordConverter.class)} directly — same mechanism
+ * KVMHostVO uses. This case exercises the same surface that
+ * {@code HostPasswordEncryptCase} uses for KVMHostVO.
+ */
+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"
+ }
+ }
+
+ /**
+ * The startup scanner in {@code EncryptFacadeImpl.collectAllEncryptPassword} registers
+ * every {@code @Convert(PasswordConverter.class)} field into {@code EncryptEntityMetadataVO}.
+ * If the @Convert annotation on PhysicalServerAO.oobPassword is missing, this row will
+ * not exist and the assert is the RED→GREEN signal for ZSTAC-85182.
+ */
+ 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)?"
+ }
+
+ /**
+ * With encryption enabled, the read-through-the-getter must return the original plaintext.
+ * After toggling encryption, the metadata row must be flipped to Encrypted so encryption
+ * actually runs on subsequent writes (mirrors HostPasswordEncryptCase).
+ */
+ 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
+
+ // Q.findValue applies the converter; getter returns plaintext. The actual ciphertext
+ // is hidden behind the converter, the metadata row above is what proves the column
+ // is going through the encrypt path on write.
+ 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 }
+ }
+
+ /**
+ * When encryption is globally disabled (legacy / opt-out), the converter is a pass-through
+ * — matches the rest of the platform's behaviour for PasswordConverter-annotated fields.
+ */
+ 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..258407c2505 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 (data-plane mocks for ipmitool leaf only) ---
- 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 ---
@@ -373,84 +364,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 +393,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,7 +418,7 @@ class PhysicalServerOpsCase extends SubCase {
assert ps.oobUsername == "good-user"
}
} finally {
- PhysicalServerScanner.probeOverride = null
+ PhysicalServerScanner.shellResultOverride = null
deleteServersInPool(pool.uuid)
deleteServerPool { uuid = pool.uuid }
}
@@ -526,15 +438,15 @@ class PhysicalServerOpsCase extends SubCase {
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
+ // Map each oobAddress IP to its intended shell-out result
+ def shellByIp = [
+ "192.168.100.1": ipmiPowerOn(), // discovered (new) — parser → POWER_ON
+ "192.168.100.2": ipmiPowerOn(), // existing (matched by oobAddress)
+ "192.168.100.3": ipmiAuthFailed(), // classifier → AUTH_FAILED
+ "192.168.100.4": ipmiUnreachable(), // classifier → UNREACHABLE
]
- PhysicalServerScanner.probeOverride = { String ip, String username ->
- statusByIp.getOrDefault(ip, PhysicalServerScanner.ProbeStatus.SUCCESS)
+ PhysicalServerScanner.shellResultOverride = { String ip, String username ->
+ shellByIp.getOrDefault(ip, ipmiPowerOn())
}
try {
@@ -552,7 +464,7 @@ 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 }
}
@@ -569,8 +481,8 @@ class PhysicalServerOpsCase extends SubCase {
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,7 +515,7 @@ 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 }
@@ -624,8 +536,8 @@ class PhysicalServerOpsCase extends SubCase {
// Production API path (createPhysicalServer without oob fields) — no direct dbf write.
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 +558,20 @@ 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.
+ // shellResultOverride simulates per-IP `chassis power status` stdout; prod parser maps it to PowerStatus.
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,15 +590,14 @@ 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
+ // Mock PowerTracker.shellResultOverride and dispatch a PingPhysicalServerMsg directly via the bus
// (avoids waiting on the periodic scheduler); assert DB row reflects probed value.
void testPowerTrackerSyncsPowerStatus() {
def pool = createPool("pool-power-tracker")
@@ -702,8 +610,8 @@ class PhysicalServerOpsCase extends SubCase {
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 +625,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 @@
-
-
-
-
-
-
-
-
-
-
-