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..15e6079bde4 100644 --- a/conf/springConfigXml/PhysicalServerManager.xml +++ b/conf/springConfigXml/PhysicalServerManager.xml @@ -133,4 +133,18 @@ + + + + + + + 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/EncryptFacade.java b/core/src/main/java/org/zstack/core/encrypt/EncryptFacade.java deleted file mode 100644 index 847c589b4db..00000000000 --- a/core/src/main/java/org/zstack/core/encrypt/EncryptFacade.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.zstack.core.encrypt; - -import org.zstack.header.core.encrypt.EncryptEntityState; -import org.zstack.header.core.encrypt.EncryptedFieldBundle; -import org.zstack.header.errorcode.ErrorableValue; - -import java.util.List; - -/** - * Created by kayo on 2018/9/7. - */ -public interface EncryptFacade { - String encrypt(String decryptString); - - String decrypt(String encryptString); - - ErrorableValue encrypt(String data, String algType); - - ErrorableValue decrypt(String data, String algType); - - void updateEncryptDataStateIfExists(String entity, String column, EncryptEntityState state); - - List getIntegrityEncryptionBundle(); - - List getConfidentialityEncryptionBundle(); -} \ No newline at end of file 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/header/src/main/java/org/zstack/header/core/convert/EncryptFacade.java b/header/src/main/java/org/zstack/header/core/convert/EncryptFacade.java new file mode 100644 index 00000000000..865e6c2d3f1 --- /dev/null +++ b/header/src/main/java/org/zstack/header/core/convert/EncryptFacade.java @@ -0,0 +1,40 @@ +package org.zstack.header.core.convert; + +import org.zstack.header.core.encrypt.EncryptEntityState; +import org.zstack.header.core.encrypt.EncryptedFieldBundle; +import org.zstack.header.errorcode.ErrorableValue; + +import java.util.List; + +/** + * Header-resident interface so header-bound entities (e.g. + * {@code PhysicalServerAO}) can reference the JPA + * {@link org.zstack.header.core.convert.PasswordConverter} without pulling in + * the {@code core} module (header is the upstream of core). + * + *

Originally lived in {@code org.zstack.core.encrypt}; moved with the + * {@code PasswordConverter} relocation in ZSTAC-85182.

+ */ +public interface EncryptFacade { + String encrypt(String decryptString); + + String decrypt(String encryptString); + + ErrorableValue encrypt(String data, String algType); + + ErrorableValue decrypt(String data, String algType); + + void updateEncryptDataStateIfExists(String entity, String column, EncryptEntityState state); + + List getIntegrityEncryptionBundle(); + + List getConfidentialityEncryptionBundle(); + + /** + * @return {@code true} when the global password-encrypt toggle is set to + * {@code None}; the {@link PasswordConverter} treats this as pass-through + * so legacy unencrypted columns stay readable. Centralised here so the + * converter does not need to depend on {@code core.EncryptGlobalConfig}. + */ + 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 69% 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..5aa31ce7731 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; @@ -16,6 +13,13 @@ /** * Created by kayo on 2018/9/7. + * + *

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 @@ - - - - - - - - - - -