From 960b0d7c016498a007a2b3cc96434f400276aa19 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 13 Jan 2026 17:27:47 +0800 Subject: [PATCH 001/122] [kvm]: add VM edk tags VM edk tag use to save the EDK version the VM used Resolves: ZSV-11010 Change-Id: I797770686172776472746b79767876697062747a --- conf/springConfigXml/Kvm.xml | 3 +- ...sion.java => BootKvmStartVmExtension.java} | 28 ++++++++++++++++++- .../java/org/zstack/kvm/KVMAgentCommands.java | 9 ++++++ .../java/org/zstack/kvm/KVMSystemTags.java | 4 +++ .../test/resources/springConfigXml/Kvm.xml | 3 +- .../org/zstack/testlib/KVMSimulator.groovy | 1 + 6 files changed, 45 insertions(+), 3 deletions(-) rename plugin/kvm/src/main/java/org/zstack/kvm/{BootOrderKvmStartVmExtension.java => BootKvmStartVmExtension.java} (50%) diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index a96965fc01b..50b6f3ee8ba 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -171,9 +171,10 @@ - + + diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/BootOrderKvmStartVmExtension.java b/plugin/kvm/src/main/java/org/zstack/kvm/BootKvmStartVmExtension.java similarity index 50% rename from plugin/kvm/src/main/java/org/zstack/kvm/BootOrderKvmStartVmExtension.java rename to plugin/kvm/src/main/java/org/zstack/kvm/BootKvmStartVmExtension.java index d8896ae447b..9da3a907662 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/BootOrderKvmStartVmExtension.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/BootKvmStartVmExtension.java @@ -2,14 +2,21 @@ import org.zstack.compute.vm.VmSystemTags; import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.vm.VmInstanceInventory; import org.zstack.header.vm.VmInstanceSpec; import org.zstack.header.vm.VmInstanceVO; +import org.zstack.tag.SystemTagCreator; + +import static org.zstack.kvm.KVMSystemTags.EDK_RPM_TOKEN; +import static org.zstack.kvm.KVMSystemTags.VM_EDK; +import static org.zstack.utils.CollectionDSL.e; +import static org.zstack.utils.CollectionDSL.map; /** * author:kaicai.hu * Date:2019/12/25 */ -public class BootOrderKvmStartVmExtension implements KVMStartVmExtensionPoint { +public class BootKvmStartVmExtension implements KVMStartVmExtensionPoint, KVMSyncVmDeviceInfoExtensionPoint { @Override public void startVmOnKvmSuccess(KVMHostInventory host, VmInstanceSpec spec) { @@ -32,4 +39,23 @@ public void startVmOnKvmFailed(KVMHostInventory host, VmInstanceSpec spec, Error public void beforeStartVmOnKvm(KVMHostInventory host, VmInstanceSpec spec, KVMAgentCommands.StartVmCmd cmd) { } + + @Override + public void afterReceiveVmDeviceInfoResponse(VmInstanceInventory vm, KVMAgentCommands.VmDevicesInfoResponse rsp, VmInstanceSpec spec) { + saveVmEdkStatesFromCommand(spec.getVmInventory().getUuid(), rsp); + } + + @SuppressWarnings("unchecked") + private void saveVmEdkStatesFromCommand(String vmUuid, KVMAgentCommands.VmDevicesInfoResponse rsp) { + if (rsp.getEdkRpm() == null) { + VM_EDK.deleteInherentTag(vmUuid); + return; + } + + SystemTagCreator creator = VM_EDK.newSystemTagCreator(vmUuid); + creator.setTagByTokens(map(e(EDK_RPM_TOKEN, rsp.getEdkRpm()))); + creator.inherent = true; + creator.recreate = true; + creator.create(); + } } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java index ab2aeda3c09..6e0a2a7da09 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -3094,6 +3094,7 @@ public static class VmDevicesInfoResponse extends AgentResponse { private Map pciDeviceInfos; @GrayVersion(value = "5.5.6") private Map mdevDeviceInfos; + private String edkRpm; public VirtualDeviceInfo getMemBalloonInfo() { return memBalloonInfo; @@ -3127,6 +3128,14 @@ public void setVirtualizerInfo(VirtualizerInfoTO virtualizerInfo) { this.virtualizerInfo = virtualizerInfo; } + public String getEdkRpm() { + return edkRpm; + } + + public void setEdkRpm(String edkRpm) { + this.edkRpm = edkRpm; + } + public Map getPciDeviceInfos() { return pciDeviceInfos; } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMSystemTags.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMSystemTags.java index b0e4cc3fdeb..32ccece2bac 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMSystemTags.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMSystemTags.java @@ -72,4 +72,8 @@ public class KVMSystemTags { public static final String LIBVIRT_CAPABILITIES_TOKEN = "libvirtCapabilities"; public static PatternedSystemTag LIBVIRT_CAPABILITIES = new PatternedSystemTag(String.format("libvirtCapabilities::{%s}", LIBVIRT_CAPABILITIES_TOKEN), HostVO.class); + + public static final String EDK_RPM_TOKEN = "edkRpm"; + public static PatternedSystemTag VM_EDK = + new PatternedSystemTag(String.format("vm::edk::{%s}", EDK_RPM_TOKEN), VmInstanceVO.class); } diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index ec757841d12..04907868b8c 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -165,9 +165,10 @@ - + + diff --git a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy index 1aeb890345b..89ac7bc2183 100755 --- a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy +++ b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy @@ -519,6 +519,7 @@ class KVMSimulator implements Simulator { rsp.virtualizerInfo.uuid = cmd.vmInstanceUuid rsp.virtualizerInfo.virtualizer = "qemu-kvm" rsp.virtualizerInfo.version = "4.2.0-632.g6a6222b.el7" + rsp.edkRpm = "edk2-ovmf-20220126gitbb1bba3d77-3.el8.noarch" return rsp } From aedb0112fc6fd8f64a1acaf0b6a013668705143d Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 3 Feb 2026 10:27:48 +0800 Subject: [PATCH 002/122] [header]: add TPM depandency * add TPM related APIs / entities / configs * SysErrors add error of operation not supported Resolves: ZSV-11310 Resolves: ZSPHER-14 Change-Id: I746861687076626e70627a777469696e67786b66 --- .../org/zstack/compute/vm/VmGlobalConfig.java | 5 + .../compute/vm/devices/TpmApiInterceptor.java | 112 ++++++ .../vm/devices/TpmMessageAutoCompleter.java | 76 ++++ conf/errorCodes/sys.xml | 5 + conf/errorCodes/tpm.xml | 23 ++ conf/serviceConfig/tpm.xml | 22 ++ conf/springConfigXml/Kvm.xml | 7 + conf/springConfigXml/VmInstanceManager.xml | 12 + .../zstack/header/errorcode/SysErrors.java | 4 +- .../java/org/zstack/header/tpm/RBACInfo.java | 33 ++ .../org/zstack/header/tpm/TpmConstants.java | 17 + .../java/org/zstack/header/tpm/TpmErrors.java | 22 ++ .../zstack/header/tpm/api/APIAddTpmEvent.java | 31 ++ .../zstack/header/tpm/api/APIAddTpmMsg.java | 51 +++ .../tpm/api/APIGetTpmCapabilityMsg.java | 48 +++ .../tpm/api/APIGetTpmCapabilityReply.java | 24 ++ .../zstack/header/tpm/api/APIQueryTpmMsg.java | 26 ++ .../header/tpm/api/APIQueryTpmReply.java | 28 ++ .../header/tpm/api/APIRemoveTpmEvent.java | 19 + .../header/tpm/api/APIRemoveTpmMsg.java | 49 +++ .../header/tpm/api/APIUpdateTpmEvent.java | 31 ++ .../header/tpm/api/APIUpdateTpmMsg.java | 62 +++ .../org/zstack/header/tpm/api/TpmMessage.java | 8 + .../header/tpm/entity/TpmCapabilityView.java | 120 ++++++ .../tpm/entity/TpmHostRefInventory.java | 106 +++++ .../header/tpm/entity/TpmHostRefVO.java | 106 +++++ .../header/tpm/entity/TpmInventory.java | 110 ++++++ .../org/zstack/header/tpm/entity/TpmSpec.java | 30 ++ .../org/zstack/header/tpm/entity/TpmVO.java | 110 ++++++ .../zstack/header/tpm/message/AddTpmMsg.java | 45 +++ .../header/tpm/message/AddTpmReply.java | 16 + .../header/tpm/message/RemoveTpmMsg.java | 32 ++ .../header/tpm/message/RemoveTpmReply.java | 6 + ...eVmInstanceFromVolumeSnapshotGroupMsg.java | 11 + .../header/vm/APICreateVmInstanceMsg.java | 32 ++ .../org/zstack/header/vm/VmInstanceVO.java | 2 + .../header/vm/devices/VmDevicesSpec.java | 24 ++ .../java/org/zstack/kvm/KVMGlobalConfig.java | 4 + .../java/org/zstack/kvm/KVMSystemTags.java | 4 + .../org/zstack/kvm/tpm/KvmTpmManager.java | 374 ++++++++++++++++++ .../test/resources/springConfigXml/Kvm.xml | 7 + .../main/java/org/zstack/utils/StringDSL.java | 15 + 42 files changed, 1868 insertions(+), 1 deletion(-) create mode 100644 compute/src/main/java/org/zstack/compute/vm/devices/TpmApiInterceptor.java create mode 100644 compute/src/main/java/org/zstack/compute/vm/devices/TpmMessageAutoCompleter.java create mode 100644 conf/errorCodes/tpm.xml create mode 100644 conf/serviceConfig/tpm.xml create mode 100644 header/src/main/java/org/zstack/header/tpm/RBACInfo.java create mode 100644 header/src/main/java/org/zstack/header/tpm/TpmConstants.java create mode 100644 header/src/main/java/org/zstack/header/tpm/TpmErrors.java create mode 100644 header/src/main/java/org/zstack/header/tpm/api/APIAddTpmEvent.java create mode 100644 header/src/main/java/org/zstack/header/tpm/api/APIAddTpmMsg.java create mode 100644 header/src/main/java/org/zstack/header/tpm/api/APIGetTpmCapabilityMsg.java create mode 100644 header/src/main/java/org/zstack/header/tpm/api/APIGetTpmCapabilityReply.java create mode 100644 header/src/main/java/org/zstack/header/tpm/api/APIQueryTpmMsg.java create mode 100644 header/src/main/java/org/zstack/header/tpm/api/APIQueryTpmReply.java create mode 100644 header/src/main/java/org/zstack/header/tpm/api/APIRemoveTpmEvent.java create mode 100644 header/src/main/java/org/zstack/header/tpm/api/APIRemoveTpmMsg.java create mode 100644 header/src/main/java/org/zstack/header/tpm/api/APIUpdateTpmEvent.java create mode 100644 header/src/main/java/org/zstack/header/tpm/api/APIUpdateTpmMsg.java create mode 100644 header/src/main/java/org/zstack/header/tpm/api/TpmMessage.java create mode 100644 header/src/main/java/org/zstack/header/tpm/entity/TpmCapabilityView.java create mode 100644 header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefInventory.java create mode 100644 header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefVO.java create mode 100644 header/src/main/java/org/zstack/header/tpm/entity/TpmInventory.java create mode 100644 header/src/main/java/org/zstack/header/tpm/entity/TpmSpec.java create mode 100644 header/src/main/java/org/zstack/header/tpm/entity/TpmVO.java create mode 100644 header/src/main/java/org/zstack/header/tpm/message/AddTpmMsg.java create mode 100644 header/src/main/java/org/zstack/header/tpm/message/AddTpmReply.java create mode 100644 header/src/main/java/org/zstack/header/tpm/message/RemoveTpmMsg.java create mode 100644 header/src/main/java/org/zstack/header/tpm/message/RemoveTpmReply.java create mode 100644 header/src/main/java/org/zstack/header/vm/devices/VmDevicesSpec.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmManager.java diff --git a/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java b/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java index 5f18eb7fd3f..841fb9c7f4a 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java @@ -141,4 +141,9 @@ public class VmGlobalConfig { @GlobalConfigValidation(validValues = {"true", "false", "auto"}) @GlobalConfigDef(defaultValue = "false", type = String.class, description = "generate config required for vhost primary storage") public static GlobalConfig GENERATE_CONFIG_VHOST_REQUIRED = new GlobalConfig(CATEGORY, "generate.config.vhost.required"); + + @GlobalConfigDef(defaultValue = "true", type = Boolean.class, description = "whether reset TPM state after VM clone") + @GlobalConfigValidation(validValues = {"true", "false"}) + @BindResourceConfig(value = {VmInstanceVO.class, ClusterVO.class}) + public static GlobalConfig RESET_TPM_AFTER_VM_CLONE = new GlobalConfig(CATEGORY, "reset.tpm.after.vm.clone"); } diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/TpmApiInterceptor.java b/compute/src/main/java/org/zstack/compute/vm/devices/TpmApiInterceptor.java new file mode 100644 index 00000000000..78724e72a57 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/devices/TpmApiInterceptor.java @@ -0,0 +1,112 @@ +package org.zstack.compute.vm.devices; + +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.core.Platform; +import org.zstack.core.cloudbus.CloudBus; +import org.zstack.core.db.Q; +import org.zstack.header.apimediator.ApiMessageInterceptionException; +import org.zstack.header.apimediator.ApiMessageInterceptor; +import org.zstack.header.apimediator.StopRoutingException; +import org.zstack.header.errorcode.SysErrors; +import org.zstack.header.message.APIMessage; +import org.zstack.header.tpm.api.*; +import org.zstack.header.tpm.entity.TpmVO; +import org.zstack.header.tpm.entity.TpmVO_; +import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.VmInstanceVO_; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import static org.zstack.core.Platform.err; +import static org.zstack.header.tpm.TpmConstants.*; +import static org.zstack.header.tpm.TpmErrors.*; +import static org.zstack.header.vm.VmInstanceConstant.KVM_HYPERVISOR_TYPE; + +public class TpmApiInterceptor implements ApiMessageInterceptor { + private static final CLogger logger = Utils.getLogger(TpmApiInterceptor.class); + + @Autowired + private CloudBus bus; + + @Override + public APIMessage intercept(APIMessage msg) throws ApiMessageInterceptionException { + if (msg instanceof APIGetTpmCapabilityMsg) { + validate((APIGetTpmCapabilityMsg) msg); + } else if (msg instanceof APIAddTpmMsg) { + validate((APIAddTpmMsg) msg); + } else if (msg instanceof APIRemoveTpmMsg) { + validate((APIRemoveTpmMsg) msg); + } else if (msg instanceof APIUpdateTpmMsg) { + validate((APIUpdateTpmMsg) msg); + } + + return msg; + } + + private void validate(APIGetTpmCapabilityMsg msg) { + makeSureVmInstanceIsKvmType(msg.getVmInstanceUuid()); + } + + private void validate(APIAddTpmMsg msg) { + makeSureVmInstanceIsKvmType(msg.getVmInstanceUuid()); + + boolean tpmExists = Q.New(TpmVO.class) + .eq(TpmVO_.vmInstanceUuid, msg.getVmInstanceUuid()) + .isExists(); + if (tpmExists) { + throw new ApiMessageInterceptionException(err(TPM_ALREADY_EXISTS, "tpm device already exists")); + } + + boolean vmInSupportState = Q.New(VmInstanceVO.class) + .eq(VmInstanceVO_.uuid, msg.getVmInstanceUuid()) + .in(VmInstanceVO_.state, SUPPORT_VM_STATES_FOR_TPM_OPERATION) + .isExists(); + if (!vmInSupportState) { + throw new ApiMessageInterceptionException(err(VM_STATE_ERROR, + "The current VM state does not support adding TPM operations") + .withOpaque("support.vm.state", SUPPORT_VM_STATES_FOR_TPM_OPERATION)); + } + + if (msg.getResourceUuid() == null) { + msg.setResourceUuid(Platform.getUuid()); + } + } + + private void validate(APIRemoveTpmMsg msg) { + makeSureVmInstanceIsKvmType(msg.getVmInstanceUuid()); + + boolean tpmExists = Q.New(TpmVO.class) + .eq(TpmVO_.vmInstanceUuid, msg.getVmInstanceUuid()) + .isExists(); + if (!tpmExists) { + APIRemoveTpmEvent evt = new APIRemoveTpmEvent(msg.getId()); + bus.publish(evt); + throw new StopRoutingException(); + } + + boolean vmInSupportState = Q.New(VmInstanceVO.class) + .eq(VmInstanceVO_.uuid, msg.getVmInstanceUuid()) + .in(VmInstanceVO_.state, SUPPORT_VM_STATES_FOR_TPM_OPERATION) + .isExists(); + if (!vmInSupportState) { + throw new ApiMessageInterceptionException(err(VM_STATE_ERROR, + "The current VM state does not support removing TPM operations") + .withOpaque("support.vm.state", SUPPORT_VM_STATES_FOR_TPM_OPERATION)); + } + } + + private void validate(APIUpdateTpmMsg msg) { + makeSureVmInstanceIsKvmType(msg.getVmInstanceUuid()); + bus.makeTargetServiceIdByResourceUuid(msg, SERVICE_ID, msg.getTpmUuid()); + } + + private void makeSureVmInstanceIsKvmType(String vmInstanceUuid) { + String hypervisorType = Q.New(VmInstanceVO.class) + .select(VmInstanceVO_.hypervisorType) + .eq(VmInstanceVO_.uuid, vmInstanceUuid) + .findValue(); + if (!KVM_HYPERVISOR_TYPE.equals(hypervisorType)) { + throw new ApiMessageInterceptionException(err(SysErrors.NOT_SUPPORTED, "only allowed for kvm type VM instance")); + } + } +} diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/TpmMessageAutoCompleter.java b/compute/src/main/java/org/zstack/compute/vm/devices/TpmMessageAutoCompleter.java new file mode 100644 index 00000000000..a7024551cc2 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/devices/TpmMessageAutoCompleter.java @@ -0,0 +1,76 @@ +package org.zstack.compute.vm.devices; + +import org.zstack.core.db.Q; +import org.zstack.header.apimediator.ApiMessageInterceptionException; +import org.zstack.header.apimediator.GlobalApiMessageInterceptor; +import org.zstack.header.message.APIDeleteMessage; +import org.zstack.header.message.APIMessage; +import org.zstack.header.tpm.api.APIGetTpmCapabilityMsg; +import org.zstack.header.tpm.api.APIRemoveTpmMsg; +import org.zstack.header.tpm.api.APIUpdateTpmMsg; +import org.zstack.header.tpm.api.TpmMessage; +import org.zstack.header.tpm.entity.TpmVO; +import org.zstack.header.tpm.entity.TpmVO_; + +import java.util.List; + +import static org.zstack.core.Platform.argerr; +import static org.zstack.core.Platform.err; +import static org.zstack.header.tpm.TpmErrors.TPM_NOT_FOUND; +import static org.zstack.utils.CollectionDSL.list; + +public class TpmMessageAutoCompleter implements GlobalApiMessageInterceptor { + @Override + public APIMessage intercept(APIMessage msg) throws ApiMessageInterceptionException { + if (msg instanceof TpmMessage) { + validateAndComplete((TpmMessage) msg); + } + return msg; + } + + @Override + @SuppressWarnings("rawtypes") + public List getMessageClassToIntercept() { + return list(APIGetTpmCapabilityMsg.class, APIRemoveTpmMsg.class, APIUpdateTpmMsg.class); + } + + @Override + public InterceptorPosition getPosition() { + return InterceptorPosition.FRONT; + } + + private void validateAndComplete(TpmMessage msg) throws ApiMessageInterceptionException { + String tpmUuid = msg.getTpmUuid(); + String vmUuid = msg.getVmInstanceUuid(); + + if (tpmUuid == null && vmUuid == null) { + throw new ApiMessageInterceptionException(argerr("tpmUuid and vmInstanceUuid cannot be null at the same time")); + } + + if (tpmUuid != null && vmUuid != null) { + boolean exists = Q.New(TpmVO.class) + .eq(TpmVO_.uuid, tpmUuid) + .eq(TpmVO_.vmInstanceUuid, vmUuid) + .isExists(); + if (!exists) { + throw new ApiMessageInterceptionException(argerr("tpmUuid[%s] and vmInstanceUuid[%s] are not consistent", tpmUuid, vmUuid)); + } + } else if (vmUuid != null) { + tpmUuid = Q.New(TpmVO.class) + .select(TpmVO_.uuid) + .eq(TpmVO_.vmInstanceUuid, vmUuid) + .findValue(); + if (tpmUuid == null && (!(msg instanceof APIDeleteMessage))) { + throw new ApiMessageInterceptionException(err(TPM_NOT_FOUND, "tpm for vm[%s] does not exist", vmUuid)); + } else { + msg.setTpmUuid(tpmUuid); + } + } else { + vmUuid = Q.New(TpmVO.class) + .select(TpmVO_.vmInstanceUuid) + .eq(TpmVO_.uuid, tpmUuid) + .findValue(); + msg.setVmInstanceUuid(vmUuid); + } + } +} diff --git a/conf/errorCodes/sys.xml b/conf/errorCodes/sys.xml index 71cce02af39..9a0f4008b5a 100755 --- a/conf/errorCodes/sys.xml +++ b/conf/errorCodes/sys.xml @@ -90,5 +90,10 @@ 1017 A message or a operation cancelled + + + 1091 + Operation not supported + diff --git a/conf/errorCodes/tpm.xml b/conf/errorCodes/tpm.xml new file mode 100644 index 00000000000..d55f1a1308a --- /dev/null +++ b/conf/errorCodes/tpm.xml @@ -0,0 +1,23 @@ + + TPM + + + 1000 + General error + + + + 1701 + TPM already exists in this VM + + + + 1702 + TPM not found + + + + 1703 + The current VM state does not support this TPM operations + + diff --git a/conf/serviceConfig/tpm.xml b/conf/serviceConfig/tpm.xml new file mode 100644 index 00000000000..dab0467a8f6 --- /dev/null +++ b/conf/serviceConfig/tpm.xml @@ -0,0 +1,22 @@ + + + tpm + TpmApiInterceptor + + + org.zstack.header.tpm.api.APIAddTpmMsg + + + org.zstack.header.tpm.api.APIGetTpmCapabilityMsg + + + org.zstack.header.tpm.api.APIQueryTpmMsg + query + + + org.zstack.header.tpm.api.APIRemoveTpmMsg + + + org.zstack.header.tpm.api.APIUpdateTpmMsg + + diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index 50b6f3ee8ba..c066400c153 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -262,4 +262,11 @@ + + + + + + + diff --git a/conf/springConfigXml/VmInstanceManager.xml b/conf/springConfigXml/VmInstanceManager.xml index 09a9cb8d0e5..f5d224bb417 100755 --- a/conf/springConfigXml/VmInstanceManager.xml +++ b/conf/springConfigXml/VmInstanceManager.xml @@ -273,4 +273,16 @@ + + + + + + + + + + + + diff --git a/header/src/main/java/org/zstack/header/errorcode/SysErrors.java b/header/src/main/java/org/zstack/header/errorcode/SysErrors.java index 634f08e17fe..47c85dd72dd 100755 --- a/header/src/main/java/org/zstack/header/errorcode/SysErrors.java +++ b/header/src/main/java/org/zstack/header/errorcode/SysErrors.java @@ -20,7 +20,9 @@ public enum SysErrors { UNIMPLEMENTED_OPERATION_ERROR(1014), HTTP_ERROR(1015), IO_ERROR(1016), - CANCEL_ERROR(1017); + CANCEL_ERROR(1017), + NOT_SUPPORTED(1091), + ; private String code; diff --git a/header/src/main/java/org/zstack/header/tpm/RBACInfo.java b/header/src/main/java/org/zstack/header/tpm/RBACInfo.java new file mode 100644 index 00000000000..f0b9e2c82ea --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/RBACInfo.java @@ -0,0 +1,33 @@ +package org.zstack.header.tpm; + +import org.zstack.header.identity.rbac.RBACDescription; +import org.zstack.header.rest.SDKPackage; +import org.zstack.header.tpm.entity.TpmVO; + +@SDKPackage(packageName="org.zstack.sdk.tpm") +public class RBACInfo implements RBACDescription { + @Override + public void permissions() { + permissionBuilder() + .name("tpm") + .normalAPIs("org.zstack.header.tpm.**") + .targetResources(TpmVO.class) + .build(); + } + + @Override + public void contributeToRoles() { + roleContributorBuilder() + .roleName("other") + .actionsByPermissionName("tpm") + .build(); + } + + @Override + public void roles() { + } + + @Override + public void globalReadableResources() { + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/TpmConstants.java b/header/src/main/java/org/zstack/header/tpm/TpmConstants.java new file mode 100644 index 00000000000..4654c5c956d --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/TpmConstants.java @@ -0,0 +1,17 @@ +package org.zstack.header.tpm; + +import org.zstack.header.vm.VmInstanceState; + +import java.util.Collections; +import java.util.List; + +import static org.zstack.utils.CollectionDSL.list; + +public class TpmConstants { + private TpmConstants() {} + + public static final String SERVICE_ID = "tpm"; + + public static final List SUPPORT_VM_STATES_FOR_TPM_OPERATION = + Collections.unmodifiableList(list(VmInstanceState.Stopped)); +} diff --git a/header/src/main/java/org/zstack/header/tpm/TpmErrors.java b/header/src/main/java/org/zstack/header/tpm/TpmErrors.java new file mode 100644 index 00000000000..cb9eebb68c2 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/TpmErrors.java @@ -0,0 +1,22 @@ +package org.zstack.header.tpm; + +public enum TpmErrors { + GENERAL_ERROR(1000), + + // INVALID_ARGUMENT, 17xx <- SYS.1007 + TPM_ALREADY_EXISTS(1701), + TPM_NOT_FOUND(1702), + VM_STATE_ERROR(1703), + ; + + private String code; + + private TpmErrors(int id) { + code = String.format("TPM.%s", id); + } + + @Override + public String toString() { + return code; + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/api/APIAddTpmEvent.java b/header/src/main/java/org/zstack/header/tpm/api/APIAddTpmEvent.java new file mode 100644 index 00000000000..4106a67afdd --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIAddTpmEvent.java @@ -0,0 +1,31 @@ +package org.zstack.header.tpm.api; + +import org.zstack.header.message.APIEvent; +import org.zstack.header.rest.RestResponse; +import org.zstack.header.tpm.entity.TpmInventory; + +@RestResponse(allTo = "inventory") +public class APIAddTpmEvent extends APIEvent { + private TpmInventory inventory; + + public APIAddTpmEvent() { + } + + public APIAddTpmEvent(String apiId) { + super(apiId); + } + + public TpmInventory getInventory() { + return inventory; + } + + public void setInventory(TpmInventory inventory) { + this.inventory = inventory; + } + + public static APIAddTpmEvent __example__() { + APIAddTpmEvent event = new APIAddTpmEvent(); + event.setInventory(TpmInventory.__example__()); + return event; + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/api/APIAddTpmMsg.java b/header/src/main/java/org/zstack/header/tpm/api/APIAddTpmMsg.java new file mode 100644 index 00000000000..3acea51c0fc --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIAddTpmMsg.java @@ -0,0 +1,51 @@ +package org.zstack.header.tpm.api; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APICreateMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.message.DocUtils; +import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.VmInstanceMessage; +import org.zstack.header.vm.VmInstanceVO; +import org.zstack.utils.StringDSL; + +@RestRequest( + path = "/tpms", + method = HttpMethod.POST, + responseClass = APIAddTpmEvent.class, + parameterName = "params" +) +public class APIAddTpmMsg extends APICreateMessage implements VmInstanceMessage { + /** + * If null, use the default key provider from global config (if set). + */ + @APIParam(required = false, minLength = 32, maxLength = 32) + private String keyProviderUuid; + + @APIParam(resourceType = VmInstanceVO.class) + private String vmInstanceUuid; + + public String getKeyProviderUuid() { + return keyProviderUuid; + } + + public void setKeyProviderUuid(String keyProviderUuid) { + this.keyProviderUuid = keyProviderUuid; + } + + @Override + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public static APIAddTpmMsg __example__() { + APIAddTpmMsg msg = new APIAddTpmMsg(); + msg.setKeyProviderUuid(StringDSL.createFixedUuid("keyProviderUuid")); + msg.setVmInstanceUuid(DocUtils.createFixedUuid(VmInstanceVO.class)); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/api/APIGetTpmCapabilityMsg.java b/header/src/main/java/org/zstack/header/tpm/api/APIGetTpmCapabilityMsg.java new file mode 100644 index 00000000000..c687bd0e576 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIGetTpmCapabilityMsg.java @@ -0,0 +1,48 @@ +package org.zstack.header.tpm.api; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIParam; +import org.zstack.header.message.APISyncCallMessage; +import org.zstack.header.message.DocUtils; +import org.zstack.header.rest.RestRequest; +import org.zstack.header.tpm.entity.TpmVO; +import org.zstack.header.vm.VmInstanceVO; + +@RestRequest( + path = "/tpms/capability", + method = HttpMethod.GET, + responseClass = APIGetTpmCapabilityReply.class +) +public class APIGetTpmCapabilityMsg extends APISyncCallMessage implements TpmMessage { + @APIParam(required = false, resourceType = TpmVO.class) + private String tpmUuid; + + @APIParam(required = false, resourceType = VmInstanceVO.class) + private String vmInstanceUuid; + + @Override + public String getTpmUuid() { + return tpmUuid; + } + + @Override + public void setTpmUuid(String tpmUuid) { + this.tpmUuid = tpmUuid; + } + + @Override + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + @Override + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public static APIGetTpmCapabilityMsg __example__() { + APIGetTpmCapabilityMsg msg = new APIGetTpmCapabilityMsg(); + msg.setVmInstanceUuid(DocUtils.createFixedUuid(VmInstanceVO.class)); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/api/APIGetTpmCapabilityReply.java b/header/src/main/java/org/zstack/header/tpm/api/APIGetTpmCapabilityReply.java new file mode 100644 index 00000000000..4819ccfc1aa --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIGetTpmCapabilityReply.java @@ -0,0 +1,24 @@ +package org.zstack.header.tpm.api; + +import org.zstack.header.message.APIReply; +import org.zstack.header.rest.RestResponse; +import org.zstack.header.tpm.entity.TpmCapabilityView; + +@RestResponse(fieldsTo = "all") +public class APIGetTpmCapabilityReply extends APIReply { + private TpmCapabilityView inventory; + + public TpmCapabilityView getInventory() { + return inventory; + } + + public void setInventory(TpmCapabilityView inventory) { + this.inventory = inventory; + } + + public static APIGetTpmCapabilityReply __example__() { + APIGetTpmCapabilityReply reply = new APIGetTpmCapabilityReply(); + reply.setInventory(TpmCapabilityView.__example__()); + return reply; + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/api/APIQueryTpmMsg.java b/header/src/main/java/org/zstack/header/tpm/api/APIQueryTpmMsg.java new file mode 100644 index 00000000000..00730943d90 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIQueryTpmMsg.java @@ -0,0 +1,26 @@ +package org.zstack.header.tpm.api; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.DocUtils; +import org.zstack.header.query.APIQueryMessage; +import org.zstack.header.query.AutoQuery; +import org.zstack.header.rest.RestRequest; +import org.zstack.header.tpm.entity.TpmInventory; +import org.zstack.header.tpm.entity.TpmVO; + +import java.util.List; + +import static java.util.Arrays.asList; + +@AutoQuery(replyClass = APIQueryTpmReply.class, inventoryClass = TpmInventory.class) +@RestRequest( + path = "/tpms", + optionalPaths = {"/tpms/{uuid}"}, + method = HttpMethod.GET, + responseClass = APIQueryTpmReply.class +) +public class APIQueryTpmMsg extends APIQueryMessage { + public static List __example__() { + return asList("uuid=" + DocUtils.createFixedUuid(TpmVO.class)); + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/api/APIQueryTpmReply.java b/header/src/main/java/org/zstack/header/tpm/api/APIQueryTpmReply.java new file mode 100644 index 00000000000..9262fd88081 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIQueryTpmReply.java @@ -0,0 +1,28 @@ +package org.zstack.header.tpm.api; + +import org.zstack.header.query.APIQueryReply; +import org.zstack.header.rest.RestResponse; +import org.zstack.header.tpm.entity.TpmInventory; + +import java.util.List; + +import static org.zstack.utils.CollectionDSL.list; + +@RestResponse(allTo = "inventories") +public class APIQueryTpmReply extends APIQueryReply { + private List inventories; + + public List getInventories() { + return inventories; + } + + public void setInventories(List inventories) { + this.inventories = inventories; + } + + public static APIQueryTpmReply __example__() { + APIQueryTpmReply reply = new APIQueryTpmReply(); + reply.setInventories(list(TpmInventory.__example__())); + return reply; + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/api/APIRemoveTpmEvent.java b/header/src/main/java/org/zstack/header/tpm/api/APIRemoveTpmEvent.java new file mode 100644 index 00000000000..7356c2aafa4 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIRemoveTpmEvent.java @@ -0,0 +1,19 @@ +package org.zstack.header.tpm.api; + +import org.zstack.header.message.APIEvent; +import org.zstack.header.rest.RestResponse; + +@RestResponse +public class APIRemoveTpmEvent extends APIEvent { + public APIRemoveTpmEvent(String apiId) { + super(apiId); + } + + public APIRemoveTpmEvent() { + super(null); + } + + public static APIRemoveTpmEvent __example__() { + return new APIRemoveTpmEvent(); + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/api/APIRemoveTpmMsg.java b/header/src/main/java/org/zstack/header/tpm/api/APIRemoveTpmMsg.java new file mode 100644 index 00000000000..5b5bc148e09 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIRemoveTpmMsg.java @@ -0,0 +1,49 @@ +package org.zstack.header.tpm.api; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIDeleteMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.message.DocUtils; +import org.zstack.header.rest.RestRequest; +import org.zstack.header.tpm.entity.TpmVO; +import org.zstack.header.vm.VmInstanceMessage; +import org.zstack.header.vm.VmInstanceVO; + +@RestRequest( + path = "/tpms", + method = HttpMethod.DELETE, + responseClass = APIRemoveTpmEvent.class +) +public class APIRemoveTpmMsg extends APIDeleteMessage implements VmInstanceMessage, TpmMessage { + @APIParam(required = false, resourceType = TpmVO.class, successIfResourceNotExisting = true) + private String tpmUuid; + + @APIParam(required = false, resourceType = VmInstanceVO.class) + private String vmInstanceUuid; + + @Override + public String getTpmUuid() { + return tpmUuid; + } + + @Override + public void setTpmUuid(String tpmUuid) { + this.tpmUuid = tpmUuid; + } + + @Override + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + @Override + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public static APIRemoveTpmMsg __example__() { + APIRemoveTpmMsg msg = new APIRemoveTpmMsg(); + msg.setVmInstanceUuid(DocUtils.createFixedUuid(VmInstanceVO.class)); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/api/APIUpdateTpmEvent.java b/header/src/main/java/org/zstack/header/tpm/api/APIUpdateTpmEvent.java new file mode 100644 index 00000000000..3d03410e053 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIUpdateTpmEvent.java @@ -0,0 +1,31 @@ +package org.zstack.header.tpm.api; + +import org.zstack.header.message.APIEvent; +import org.zstack.header.rest.RestResponse; +import org.zstack.header.tpm.entity.TpmInventory; + +@RestResponse(allTo = "inventory") +public class APIUpdateTpmEvent extends APIEvent { + private TpmInventory inventory; + + public APIUpdateTpmEvent() { + } + + public APIUpdateTpmEvent(String apiId) { + super(apiId); + } + + public TpmInventory getInventory() { + return inventory; + } + + public void setInventory(TpmInventory inventory) { + this.inventory = inventory; + } + + public static APIUpdateTpmEvent __example__() { + APIUpdateTpmEvent event = new APIUpdateTpmEvent(); + event.setInventory(TpmInventory.__example__()); + return event; + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/api/APIUpdateTpmMsg.java b/header/src/main/java/org/zstack/header/tpm/api/APIUpdateTpmMsg.java new file mode 100644 index 00000000000..4e3f264d3c3 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIUpdateTpmMsg.java @@ -0,0 +1,62 @@ +package org.zstack.header.tpm.api; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.message.DocUtils; +import org.zstack.header.rest.RestRequest; +import org.zstack.header.tpm.entity.TpmVO; +import org.zstack.header.vm.VmInstanceVO; +import org.zstack.utils.StringDSL; + +@RestRequest( + path = "/tpms", + method = HttpMethod.PUT, + isAction = true, + responseClass = APIUpdateTpmEvent.class +) +public class APIUpdateTpmMsg extends APIMessage implements TpmMessage { + @APIParam(required = false, resourceType = VmInstanceVO.class) + private String vmInstanceUuid; + + @APIParam(required = false, resourceType = TpmVO.class) + private String tpmUuid; + + @APIParam(required = false, minLength = 32, maxLength = 32) + private String keyProviderUuid; + + @Override + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + @Override + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + @Override + public String getTpmUuid() { + return tpmUuid; + } + + @Override + public void setTpmUuid(String tpmUuid) { + this.tpmUuid = tpmUuid; + } + + public String getKeyProviderUuid() { + return keyProviderUuid; + } + + public void setKeyProviderUuid(String keyProviderUuid) { + this.keyProviderUuid = keyProviderUuid; + } + + public static APIUpdateTpmMsg __example__() { + APIUpdateTpmMsg msg = new APIUpdateTpmMsg(); + msg.setKeyProviderUuid(StringDSL.createFixedUuid("keyProviderUuid")); + msg.setVmInstanceUuid(DocUtils.createFixedUuid(VmInstanceVO.class)); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/api/TpmMessage.java b/header/src/main/java/org/zstack/header/tpm/api/TpmMessage.java new file mode 100644 index 00000000000..1ae70c46381 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/TpmMessage.java @@ -0,0 +1,8 @@ +package org.zstack.header.tpm.api; + +public interface TpmMessage { + String getVmInstanceUuid(); + void setVmInstanceUuid(String vmInstanceUuid); + String getTpmUuid(); + void setTpmUuid(String tpmUuid); +} diff --git a/header/src/main/java/org/zstack/header/tpm/entity/TpmCapabilityView.java b/header/src/main/java/org/zstack/header/tpm/entity/TpmCapabilityView.java new file mode 100644 index 00000000000..2b60316df52 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/entity/TpmCapabilityView.java @@ -0,0 +1,120 @@ +package org.zstack.header.tpm.entity; + +import org.zstack.header.configuration.PythonClass; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +@PythonClass +public class TpmCapabilityView { + // fields in TpmInventory + private String uuid; + private String name; + private String vmInstanceUuid; + private Timestamp createDate; + private Timestamp lastOpDate; + private List hostRefs; + + // related table fields + // TODO keyProviderUuid / keyProviderType / keyProviderName / keyProviderKeyVersion + + // status fields : from system tags + private String edkVersion; + private String swtpmVersion; + + // config fields : from global / resource config + private boolean resetTpmAfterVmCloneConfig; + + public void setTpmInventory(TpmInventory inventory) { + setUuid(inventory.getUuid()); + setName(inventory.getName()); + setVmInstanceUuid(inventory.getVmInstanceUuid()); + setCreateDate(inventory.getCreateDate()); + setLastOpDate(inventory.getLastOpDate()); + setHostRefs(new ArrayList<>(inventory.getHostRefs())); + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public Timestamp getCreateDate() { + return createDate; + } + + public void setCreateDate(Timestamp createDate) { + this.createDate = createDate; + } + + public Timestamp getLastOpDate() { + return lastOpDate; + } + + public void setLastOpDate(Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + + public List getHostRefs() { + return hostRefs; + } + + public void setHostRefs(List hostRefs) { + this.hostRefs = hostRefs; + } + + public String getEdkVersion() { + return edkVersion; + } + + public void setEdkVersion(String edkVersion) { + this.edkVersion = edkVersion; + } + + public String getSwtpmVersion() { + return swtpmVersion; + } + + public void setSwtpmVersion(String swtpmVersion) { + this.swtpmVersion = swtpmVersion; + } + + public boolean isResetTpmAfterVmCloneConfig() { + return resetTpmAfterVmCloneConfig; + } + + public void setResetTpmAfterVmCloneConfig(boolean resetTpmAfterVmCloneConfig) { + this.resetTpmAfterVmCloneConfig = resetTpmAfterVmCloneConfig; + } + + public static TpmCapabilityView __example__() { + TpmCapabilityView view = new TpmCapabilityView(); + view.setTpmInventory(TpmInventory.__example__()); + + view.setEdkVersion("edk2-ovmf-20220126gitbb1bba3d77-3.el8.noarch"); + view.setSwtpmVersion("0.8.2"); + + view.setResetTpmAfterVmCloneConfig(true); + return view; + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefInventory.java b/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefInventory.java new file mode 100644 index 00000000000..3253723c0d0 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefInventory.java @@ -0,0 +1,106 @@ +package org.zstack.header.tpm.entity; + +import org.zstack.header.host.HostInventory; +import org.zstack.header.host.HostVO; +import org.zstack.header.message.DocUtils; +import org.zstack.header.query.ExpandedQueries; +import org.zstack.header.query.ExpandedQuery; +import org.zstack.header.search.Inventory; + +import java.sql.Timestamp; +import java.util.Collection; +import java.util.List; + +import static org.zstack.utils.CollectionUtils.transform; + +@Inventory(mappingVOClass = TpmHostRefVO.class) +@ExpandedQueries({ + @ExpandedQuery(expandedField = "tpm", inventoryClass = TpmInventory.class, + foreignKey = "tpmUuid", expandedInventoryKey = "uuid"), + @ExpandedQuery(expandedField = "host", inventoryClass = HostInventory.class, + foreignKey = "hostUuid", expandedInventoryKey = "uuid"), +}) +public class TpmHostRefInventory { + private long id; + private String tpmUuid; + private String hostUuid; + private String path; + private Timestamp createDate; + private Timestamp lastOpDate; + + public TpmHostRefInventory() { + } + + public static TpmHostRefInventory valueOf(TpmHostRefVO vo) { + TpmHostRefInventory inv = new TpmHostRefInventory(); + inv.setId(vo.getId()); + inv.setTpmUuid(vo.getTpmUuid()); + inv.setHostUuid(vo.getHostUuid()); + inv.setPath(vo.getPath()); + inv.setCreateDate(vo.getCreateDate()); + inv.setLastOpDate(vo.getLastOpDate()); + return inv; + } + + public static List valueOf(Collection vos) { + return transform(vos, TpmHostRefInventory::valueOf); + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getTpmUuid() { + return tpmUuid; + } + + public void setTpmUuid(String tpmUuid) { + this.tpmUuid = tpmUuid; + } + + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public Timestamp getCreateDate() { + return createDate; + } + + public void setCreateDate(Timestamp createDate) { + this.createDate = createDate; + } + + public Timestamp getLastOpDate() { + return lastOpDate; + } + + public void setLastOpDate(Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + + public static TpmHostRefInventory __example__() { + TpmHostRefInventory ref = new TpmHostRefInventory(); + ref.setId(1L); + ref.setTpmUuid(DocUtils.createFixedUuid(TpmVO.class)); + ref.setHostUuid(DocUtils.createFixedUuid(HostVO.class)); + ref.setCreateDate(DocUtils.timestamp()); + ref.setLastOpDate(DocUtils.timestamp()); + return ref; + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefVO.java b/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefVO.java new file mode 100644 index 00000000000..38bdbf5049f --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefVO.java @@ -0,0 +1,106 @@ +package org.zstack.header.tpm.entity; + +import org.zstack.header.host.HostVO; +import org.zstack.header.vo.EntityGraph; +import org.zstack.header.vo.ForeignKey; +import org.zstack.header.vo.ToInventory; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import java.sql.Timestamp; + +@Entity +@Table +@EntityGraph( + friends = { + @EntityGraph.Neighbour(type = TpmVO.class, myField = "tpmUuid", targetField = "uuid"), + @EntityGraph.Neighbour(type = HostVO.class, myField = "hostUuid", targetField = "uuid"), + } +) +public class TpmHostRefVO implements ToInventory { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column + private long id; + + @Column + @ForeignKey(parentEntityClass = TpmVO.class, onDeleteAction = ForeignKey.ReferenceOption.CASCADE) + private String tpmUuid; + + @Column + @ForeignKey(parentEntityClass = HostVO.class, onDeleteAction = ForeignKey.ReferenceOption.CASCADE) + private String hostUuid; + + @Column + private String path; + + @Column + private Timestamp createDate; + + @Column + private Timestamp lastOpDate; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getTpmUuid() { + return tpmUuid; + } + + public void setTpmUuid(String tpmUuid) { + this.tpmUuid = tpmUuid; + } + + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public Timestamp getCreateDate() { + return createDate; + } + + public void setCreateDate(Timestamp createDate) { + this.createDate = createDate; + } + + public Timestamp getLastOpDate() { + return lastOpDate; + } + + public void setLastOpDate(Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + + @Override + public String toString() { + return "TpmHostRefVO{" + + "id=" + id + + ", tpmUuid='" + tpmUuid + '\'' + + ", hostUuid='" + hostUuid + '\'' + + ", path='" + path + '\'' + + ", createDate=" + createDate + + ", lastOpDate=" + lastOpDate + + '}'; + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/entity/TpmInventory.java b/header/src/main/java/org/zstack/header/tpm/entity/TpmInventory.java new file mode 100644 index 00000000000..70ea376e643 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/entity/TpmInventory.java @@ -0,0 +1,110 @@ +package org.zstack.header.tpm.entity; + +import org.zstack.header.configuration.PythonClassInventory; +import org.zstack.header.message.DocUtils; +import org.zstack.header.query.ExpandedQueries; +import org.zstack.header.query.ExpandedQuery; +import org.zstack.header.search.Inventory; +import org.zstack.header.vm.VmInstanceInventory; +import org.zstack.header.vm.VmInstanceVO; + +import java.io.Serializable; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.zstack.utils.CollectionDSL.list; +import static org.zstack.utils.CollectionUtils.transform; + +@PythonClassInventory +@Inventory(mappingVOClass = TpmVO.class) +@ExpandedQueries({ + @ExpandedQuery(expandedField = "vmInstance", inventoryClass = VmInstanceInventory.class, + foreignKey = "vmInstanceUuid", expandedInventoryKey = "uuid"), +}) +public class TpmInventory implements Serializable { + private String uuid; + private String name; + private String vmInstanceUuid; + private Timestamp createDate; + private Timestamp lastOpDate; + private List hostRefs = new ArrayList<>(); + + public TpmInventory() { + } + + public static TpmInventory valueOf(TpmVO vo) { + TpmInventory inv = new TpmInventory(); + inv.setUuid(vo.getUuid()); + inv.setName(vo.getResourceName()); + inv.setVmInstanceUuid(vo.getVmInstanceUuid()); + inv.setCreateDate(vo.getCreateDate()); + inv.setLastOpDate(vo.getLastOpDate()); + inv.setHostRefs(TpmHostRefInventory.valueOf(vo.getHostRefs())); + return inv; + } + + public static List valueOf(Collection vos) { + return transform(vos, TpmInventory::valueOf); + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public Timestamp getCreateDate() { + return createDate; + } + + public void setCreateDate(Timestamp createDate) { + this.createDate = createDate; + } + + public Timestamp getLastOpDate() { + return lastOpDate; + } + + public void setLastOpDate(Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + + public List getHostRefs() { + return hostRefs; + } + + public void setHostRefs(List hostRefs) { + this.hostRefs = hostRefs; + } + + public static TpmInventory __example__() { + TpmInventory tpm = new TpmInventory(); + tpm.setUuid(DocUtils.createFixedUuid(TpmVO.class)); + tpm.setVmInstanceUuid(DocUtils.createFixedUuid(VmInstanceVO.class)); + tpm.setName("TPM-for-VM-" + tpm.getVmInstanceUuid()); + tpm.setCreateDate(DocUtils.timestamp()); + tpm.setLastOpDate(DocUtils.timestamp()); + tpm.setHostRefs(list(TpmHostRefInventory.__example__())); + return tpm; + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/entity/TpmSpec.java b/header/src/main/java/org/zstack/header/tpm/entity/TpmSpec.java new file mode 100644 index 00000000000..60efd9a9df9 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/entity/TpmSpec.java @@ -0,0 +1,30 @@ +package org.zstack.header.tpm.entity; + +import org.zstack.utils.StringDSL; + +public class TpmSpec { + private boolean enable = true; + private String keyProviderUuid; + + public boolean isEnable() { + return enable; + } + + public void setEnable(boolean enable) { + this.enable = enable; + } + + public String getKeyProviderUuid() { + return keyProviderUuid; + } + + public void setKeyProviderUuid(String keyProviderUuid) { + this.keyProviderUuid = keyProviderUuid; + } + + public static TpmSpec __example__() { + TpmSpec tpm = new TpmSpec(); + tpm.setKeyProviderUuid(StringDSL.createFixedUuid("keyProviderUuid")); + return tpm; + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/entity/TpmVO.java b/header/src/main/java/org/zstack/header/tpm/entity/TpmVO.java new file mode 100644 index 00000000000..02bbda43431 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/entity/TpmVO.java @@ -0,0 +1,110 @@ +package org.zstack.header.tpm.entity; + +import org.zstack.header.identity.OwnedByAccount; +import org.zstack.header.tag.AutoDeleteTag; +import org.zstack.header.vm.VmInstanceEO; +import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vo.BaseResource; +import org.zstack.header.vo.EntityGraph; +import org.zstack.header.vo.ForeignKey; +import org.zstack.header.vo.NoView; +import org.zstack.header.vo.ResourceVO; +import org.zstack.header.vo.SoftDeletionCascade; +import org.zstack.header.vo.SoftDeletionCascades; +import org.zstack.header.vo.ToInventory; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.Transient; +import java.sql.Timestamp; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table +@BaseResource +@AutoDeleteTag +@SoftDeletionCascades({ + @SoftDeletionCascade(parent = VmInstanceEO.class, joinColumn = "vmInstanceUuid") +}) +@EntityGraph( + parents = { + @EntityGraph.Neighbour(type = VmInstanceVO.class, myField = "vmInstanceUuid", targetField = "uuid"), + } +) +public class TpmVO extends ResourceVO implements ToInventory, OwnedByAccount { + @Column + @ForeignKey(parentEntityClass = VmInstanceEO.class, onDeleteAction = ForeignKey.ReferenceOption.CASCADE) + private String vmInstanceUuid; + + @Column + private Timestamp createDate; + + @Column + private Timestamp lastOpDate; + + @Transient + private String accountUuid; + + @OneToMany(fetch = FetchType.EAGER) + @JoinColumn(name = "tpmUuid", insertable = false, updatable = false) + @NoView + private Set hostRefs = new HashSet<>(); + + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public Timestamp getCreateDate() { + return createDate; + } + + public void setCreateDate(Timestamp createDate) { + this.createDate = createDate; + } + + public Timestamp getLastOpDate() { + return lastOpDate; + } + + public void setLastOpDate(Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + + @Override + public String getAccountUuid() { + return accountUuid; + } + + @Override + public void setAccountUuid(String accountUuid) { + this.accountUuid = accountUuid; + } + + public Set getHostRefs() { + return hostRefs; + } + + public void setHostRefs(Set hostRefs) { + this.hostRefs = hostRefs; + } + + public TpmVO() { + } + + @Override + public String toString() { + return "TpmVO{" + + "vmInstanceUuid='" + vmInstanceUuid + '\'' + + ", uuid='" + uuid + '\'' + + '}'; + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/message/AddTpmMsg.java b/header/src/main/java/org/zstack/header/tpm/message/AddTpmMsg.java new file mode 100644 index 00000000000..2971d2b06f2 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/message/AddTpmMsg.java @@ -0,0 +1,45 @@ +package org.zstack.header.tpm.message; + +import org.zstack.header.message.NeedReplyMessage; +import org.zstack.header.tpm.api.APIAddTpmMsg; + +public class AddTpmMsg extends NeedReplyMessage { + private String keyProviderUuid; + private String vmInstanceUuid; + /** + * for creating TpmVO + */ + private String tpmUuid; + + public String getKeyProviderUuid() { + return keyProviderUuid; + } + + public void setKeyProviderUuid(String keyProviderUuid) { + this.keyProviderUuid = keyProviderUuid; + } + + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public String getTpmUuid() { + return tpmUuid; + } + + public void setTpmUuid(String tpmUuid) { + this.tpmUuid = tpmUuid; + } + + public static AddTpmMsg valueOf(APIAddTpmMsg api) { + AddTpmMsg msg = new AddTpmMsg(); + msg.setKeyProviderUuid(api.getKeyProviderUuid()); + msg.setVmInstanceUuid(api.getVmInstanceUuid()); + msg.setTpmUuid(api.getResourceUuid()); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/message/AddTpmReply.java b/header/src/main/java/org/zstack/header/tpm/message/AddTpmReply.java new file mode 100644 index 00000000000..0cbcc9bf1e3 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/message/AddTpmReply.java @@ -0,0 +1,16 @@ +package org.zstack.header.tpm.message; + +import org.zstack.header.message.MessageReply; +import org.zstack.header.tpm.entity.TpmInventory; + +public class AddTpmReply extends MessageReply { + private TpmInventory inventory; + + public TpmInventory getInventory() { + return inventory; + } + + public void setInventory(TpmInventory inventory) { + this.inventory = inventory; + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/message/RemoveTpmMsg.java b/header/src/main/java/org/zstack/header/tpm/message/RemoveTpmMsg.java new file mode 100644 index 00000000000..c68e16bc8d5 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/message/RemoveTpmMsg.java @@ -0,0 +1,32 @@ +package org.zstack.header.tpm.message; + +import org.zstack.header.message.NeedReplyMessage; +import org.zstack.header.tpm.api.APIRemoveTpmMsg; + +public class RemoveTpmMsg extends NeedReplyMessage { + private String tpmUuid; + private String vmInstanceUuid; + + public String getTpmUuid() { + return tpmUuid; + } + + public void setTpmUuid(String tpmUuid) { + this.tpmUuid = tpmUuid; + } + + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public static RemoveTpmMsg valueOf(APIRemoveTpmMsg api) { + RemoveTpmMsg msg = new RemoveTpmMsg(); + msg.setTpmUuid(api.getTpmUuid()); + msg.setVmInstanceUuid(api.getVmInstanceUuid()); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/tpm/message/RemoveTpmReply.java b/header/src/main/java/org/zstack/header/tpm/message/RemoveTpmReply.java new file mode 100644 index 00000000000..ad2e3b13292 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/message/RemoveTpmReply.java @@ -0,0 +1,6 @@ +package org.zstack.header.tpm.message; + +import org.zstack.header.message.MessageReply; + +public class RemoveTpmReply extends MessageReply { +} diff --git a/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeSnapshotGroupMsg.java b/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeSnapshotGroupMsg.java index 6c0155ca41b..1d4e3365316 100644 --- a/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeSnapshotGroupMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeSnapshotGroupMsg.java @@ -110,6 +110,9 @@ public class APICreateVmInstanceFromVolumeSnapshotGroupMsg extends APICreateMess @APIParam(required = false) private Map> dataVolumeSystemTags; + @APIParam(required = false) + private Boolean resetTpm; + public String getName() { return name; } @@ -274,4 +277,12 @@ public Map> getDataVolumeSystemTags() { public void setDataVolumeSystemTags(Map> dataVolumeSystemTags) { this.dataVolumeSystemTags = dataVolumeSystemTags; } + + public Boolean getResetTpm() { + return resetTpm; + } + + public void setResetTpm(Boolean resetTpm) { + this.resetTpm = resetTpm; + } } diff --git a/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceMsg.java index d6c0a4fd7a7..e7fc0e207aa 100755 --- a/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceMsg.java @@ -15,8 +15,10 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.primary.PrimaryStorageVO; import org.zstack.header.tag.TagResourceType; +import org.zstack.header.vm.devices.VmDevicesSpec; import org.zstack.header.volume.VolumeInventory; import org.zstack.header.zone.ZoneVO; +import org.zstack.utils.gson.JSONObjectUtil; import java.util.Collections; import java.util.List; @@ -324,6 +326,15 @@ public void setName(String name) { @APIParam(required = false) private List diskAOs; + @APIParam(required = false) + private Map devices; + + /** + * cache of {@link #devices} + */ + @APINoSee + private VmDevicesSpec devicesSpec; + public List getDiskAOs() { return diskAOs; } @@ -561,6 +572,26 @@ public void setVirtio(boolean virtio) { this.virtio = virtio; } + public Map getDevices() { + return devices; + } + + public void setDevices(Map devices) { + this.devices = devices; + } + + public VmDevicesSpec getDevicesSpec() { + if (devicesSpec == null && devices != null) { + devicesSpec = JSONObjectUtil.rehashObject(devices, VmDevicesSpec.class); + } + return devicesSpec; + } + + public void setDevicesSpec(VmDevicesSpec devicesSpec) { + this.devicesSpec = devicesSpec; + } + + @SuppressWarnings("unchecked") public static APICreateVmInstanceMsg __example__() { APICreateVmInstanceMsg msg = new APICreateVmInstanceMsg(); msg.setName("vm1"); @@ -572,6 +603,7 @@ public static APICreateVmInstanceMsg __example__() { msg.setL3NetworkUuids(Collections.singletonList(uuid())); msg.setRootVolumeSystemTags(Collections.singletonList("volumeProvisioningStrategy::ThickProvisioning")); msg.setDataVolumeSystemTags(null); + msg.setDevices(JSONObjectUtil.rehashObject(VmDevicesSpec.__example__(), Map.class)); return msg; } diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceVO.java b/header/src/main/java/org/zstack/header/vm/VmInstanceVO.java index 39dc0607e25..62e55080f14 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceVO.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceVO.java @@ -5,6 +5,7 @@ import org.zstack.header.host.HostVO; import org.zstack.header.identity.OwnedByAccount; import org.zstack.header.image.ImageVO; +import org.zstack.header.tpm.entity.TpmVO; import org.zstack.header.vm.cdrom.VmCdRomVO; import org.zstack.header.vo.*; import org.zstack.header.vo.EntityGraph; @@ -35,6 +36,7 @@ @EntityGraph.Neighbour(type = InstanceOfferingVO.class, myField = "instanceOfferingUuid", targetField = "uuid"), @EntityGraph.Neighbour(type = VolumeVO.class, myField = "rootVolumeUuid", targetField = "uuid"), @EntityGraph.Neighbour(type = VmNicVO.class, myField = "uuid", targetField = "vmInstanceUuid"), + @EntityGraph.Neighbour(type = TpmVO.class, myField = "uuid", targetField = "vmInstanceUuid"), } ) public class VmInstanceVO extends VmInstanceAO implements OwnedByAccount, ToInventory { diff --git a/header/src/main/java/org/zstack/header/vm/devices/VmDevicesSpec.java b/header/src/main/java/org/zstack/header/vm/devices/VmDevicesSpec.java new file mode 100644 index 00000000000..6b24f045a8f --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/devices/VmDevicesSpec.java @@ -0,0 +1,24 @@ +package org.zstack.header.vm.devices; + +import org.zstack.header.tpm.entity.TpmSpec; + +public class VmDevicesSpec { + /** + * Default value of tpm must be null, because tpm.enable is true by default. + */ + private TpmSpec tpm; + + public TpmSpec getTpm() { + return tpm; + } + + public void setTpm(TpmSpec tpm) { + this.tpm = tpm; + } + + public static VmDevicesSpec __example__() { + VmDevicesSpec spec = new VmDevicesSpec(); + spec.setTpm(TpmSpec.__example__()); + return spec; + } +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java index e76c271e879..e73e6a94f72 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java @@ -166,4 +166,8 @@ public class KVMGlobalConfig { @BindResourceConfig({HostVO.class, ClusterVO.class}) public static GlobalConfig MINIMUM_MEMORY_SIZE_BEFORE_START_VM = new GlobalConfig(CATEGORY, "min.free.memory.size"); + @GlobalConfigDef(defaultValue = "", + description = "Specify the EDK version to be used for the next VM startup. Default empty string indicates the use of the system's default EDK version") + @BindResourceConfig(value = {VmInstanceVO.class, ClusterVO.class}) + public static GlobalConfig VM_EDK_VERSION_CONFIG = new GlobalConfig(CATEGORY, "vm.edk.version"); } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMSystemTags.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMSystemTags.java index 32ccece2bac..45d6c174fbb 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMSystemTags.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMSystemTags.java @@ -76,4 +76,8 @@ public class KVMSystemTags { public static final String EDK_RPM_TOKEN = "edkRpm"; public static PatternedSystemTag VM_EDK = new PatternedSystemTag(String.format("vm::edk::{%s}", EDK_RPM_TOKEN), VmInstanceVO.class); + + public static final String SWTPM_VERSION_TOKEN = "version"; + public static PatternedSystemTag SWTPM_VERSION = + new PatternedSystemTag(String.format("swtpm::{%s}", SWTPM_VERSION_TOKEN), HostVO.class); } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmManager.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmManager.java new file mode 100644 index 00000000000..2d643de746b --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmManager.java @@ -0,0 +1,374 @@ +package org.zstack.kvm.tpm; + +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.core.cloudbus.CloudBus; +import org.zstack.core.cloudbus.CloudBusCallBack; +import org.zstack.core.cloudbus.MessageSafe; +import org.zstack.core.db.DatabaseFacade; +import org.zstack.core.db.Q; +import org.zstack.core.db.SQL; +import org.zstack.core.thread.ChainTask; +import org.zstack.core.thread.SyncTaskChain; +import org.zstack.core.thread.ThreadFacade; +import org.zstack.core.workflow.SimpleFlowChain; +import org.zstack.header.AbstractService; +import org.zstack.header.core.Completion; +import org.zstack.header.core.workflow.FlowDoneHandler; +import org.zstack.header.core.workflow.FlowErrorHandler; +import org.zstack.header.core.workflow.FlowTrigger; +import org.zstack.header.core.workflow.NoRollbackFlow; +import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.Message; +import org.zstack.header.message.MessageReply; +import org.zstack.header.tpm.api.APIAddTpmEvent; +import org.zstack.header.tpm.api.APIAddTpmMsg; +import org.zstack.header.tpm.api.APIGetTpmCapabilityMsg; +import org.zstack.header.tpm.api.APIGetTpmCapabilityReply; +import org.zstack.header.tpm.api.APIRemoveTpmEvent; +import org.zstack.header.tpm.api.APIRemoveTpmMsg; +import org.zstack.header.tpm.api.APIUpdateTpmMsg; +import org.zstack.header.tpm.entity.TpmCapabilityView; +import org.zstack.header.tpm.entity.TpmInventory; +import org.zstack.header.tpm.entity.TpmVO; +import org.zstack.header.tpm.entity.TpmVO_; +import org.zstack.header.tpm.message.AddTpmMsg; +import org.zstack.header.tpm.message.AddTpmReply; +import org.zstack.header.tpm.message.RemoveTpmMsg; +import org.zstack.header.tpm.message.RemoveTpmReply; +import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.VmInstanceVO_; +import org.zstack.resourceconfig.ResourceConfig; +import org.zstack.resourceconfig.ResourceConfigFacade; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.util.Map; + +import static org.zstack.compute.vm.VmGlobalConfig.RESET_TPM_AFTER_VM_CLONE; +import static org.zstack.core.Platform.err; +import static org.zstack.header.errorcode.SysErrors.NOT_SUPPORTED; +import static org.zstack.header.tpm.TpmConstants.*; +import static org.zstack.header.tpm.TpmErrors.VM_STATE_ERROR; +import static org.zstack.kvm.KVMSystemTags.EDK_RPM_TOKEN; +import static org.zstack.kvm.KVMSystemTags.SWTPM_VERSION; +import static org.zstack.kvm.KVMSystemTags.SWTPM_VERSION_TOKEN; +import static org.zstack.kvm.KVMSystemTags.VM_EDK; + +public class KvmTpmManager extends AbstractService { + private static final CLogger logger = Utils.getLogger(KvmTpmManager.class); + + @Autowired + private CloudBus bus; + @Autowired + private ThreadFacade threadFacade; + @Autowired + private DatabaseFacade databaseFacade; + @Autowired + private ResourceConfigFacade resourceConfigFacade; + + @Override + public boolean start() { + return true; + } + + @Override + public boolean stop() { + return true; + } + + @Override + public String getId() { + return bus.makeLocalServiceId(SERVICE_ID); + } + + private String tpmQueueSyncSignature(String vmUuid) { + return String.format("tpm-queue-sync-%s", vmUuid); + } + + @MessageSafe + public void handleMessage(Message msg) { + if (msg instanceof APIMessage) { + handleApiMessage((APIMessage) msg); + } else { + handleLocalMessage(msg); + } + } + + private void handleLocalMessage(Message msg) { + if (msg instanceof AddTpmMsg) { + handle((AddTpmMsg) msg); + } else if (msg instanceof RemoveTpmMsg) { + handle((RemoveTpmMsg) msg); + } else { + bus.dealWithUnknownMessage(msg); + } + } + + private void handleApiMessage(APIMessage msg) { + if (msg instanceof APIGetTpmCapabilityMsg) { + handle((APIGetTpmCapabilityMsg) msg); + } else if (msg instanceof APIAddTpmMsg) { + handle((APIAddTpmMsg) msg); + } else if (msg instanceof APIRemoveTpmMsg) { + handle((APIRemoveTpmMsg) msg); + } else if (msg instanceof APIUpdateTpmMsg) { + handle((APIUpdateTpmMsg) msg); + } else { + bus.dealWithUnknownMessage(msg); + } + } + + private void handle(AddTpmMsg msg) { + AddTpmReply reply = new AddTpmReply(); + threadFacade.chainSubmit(new ChainTask(msg) { + @Override + public void run(SyncTaskChain chain) { + AddTpmToVmContext context = AddTpmToVmContext.valueOf(msg); + addTpmToVm(context, new Completion(chain, msg) { + @Override + public void success() { + chain.next(); + TpmVO vo = Q.New(TpmVO.class) + .eq(TpmVO_.uuid, msg.getTpmUuid()) + .find(); + reply.setInventory(TpmInventory.valueOf(vo)); + bus.reply(msg, reply); + } + + @Override + public void fail(ErrorCode errorCode) { + chain.next(); + reply.setError(errorCode); + bus.reply(msg, reply); + } + }); + } + + @Override + public String getSyncSignature() { + return tpmQueueSyncSignature(msg.getVmInstanceUuid()); + } + + @Override + public String getName() { + return "queue-of-add-tpm-to-vm-" + msg.getVmInstanceUuid(); + } + }); + } + + static class AddTpmToVmContext { + String keyProviderUuid; + String vmInstanceUuid; + String tpmUuid; + + static AddTpmToVmContext valueOf(AddTpmMsg msg) { + AddTpmToVmContext context = new AddTpmToVmContext(); + context.keyProviderUuid = msg.getKeyProviderUuid(); + context.vmInstanceUuid = msg.getVmInstanceUuid(); + context.tpmUuid = msg.getTpmUuid(); + return context; + } + } + + @SuppressWarnings("rawtypes") + private void addTpmToVm(AddTpmToVmContext context, Completion completion) { + SimpleFlowChain chain = new SimpleFlowChain(); + chain.setName("add-tpm-to-vm-" + context.vmInstanceUuid); + chain.then(new NoRollbackFlow() { + String __name__ = "check-vm-status"; + + @Override + public void run(FlowTrigger trigger, Map data) { + VmInstanceVO vm = Q.New(VmInstanceVO.class) + .eq(VmInstanceVO_.uuid, context.vmInstanceUuid) + .find(); + + if (!SUPPORT_VM_STATES_FOR_TPM_OPERATION.contains(vm.getState())) { + trigger.fail(err(VM_STATE_ERROR, + "The current VM state does not support adding TPM operations") + .withOpaque("support.vm.state", SUPPORT_VM_STATES_FOR_TPM_OPERATION)); + return; + } + trigger.next(); + } + }).then(new NoRollbackFlow() { + String __name__ = "create-tpm-db-records"; + + @Override + public void run(FlowTrigger trigger, Map data) { + TpmVO tpm = new TpmVO(); + tpm.setUuid(context.tpmUuid); + tpm.setResourceName("TPM-for-VM-" + context.vmInstanceUuid); + tpm.setVmInstanceUuid(context.vmInstanceUuid); + databaseFacade.persist(tpm); + trigger.next(); + } + }).done(new FlowDoneHandler(completion) { + @Override + public void handle(Map data) { + completion.success(); + } + }).error(new FlowErrorHandler(completion) { + @Override + public void handle(ErrorCode errorCode, Map data) { + completion.fail(errorCode); + } + }).start(); + } + + private void handle(RemoveTpmMsg msg) { + RemoveTpmReply reply = new RemoveTpmReply(); + threadFacade.chainSubmit(new ChainTask(msg) { + @Override + public void run(SyncTaskChain chain) { + RemoveTpmFromVmContext context = RemoveTpmFromVmContext.valueOf(msg); + removeTpmFromVm(context, new Completion(chain, msg) { + @Override + public void success() { + chain.next(); + bus.reply(msg, reply); + } + + @Override + public void fail(ErrorCode errorCode) { + chain.next(); + reply.setError(errorCode); + bus.reply(msg, reply); + } + }); + } + + @Override + public String getSyncSignature() { + return tpmQueueSyncSignature(msg.getVmInstanceUuid()); + } + + @Override + public String getName() { + return "queue-of-remove-tpm-from-vm-" + msg.getVmInstanceUuid(); + } + }); + } + + static class RemoveTpmFromVmContext { + String vmInstanceUuid; + String tpmUuid; + + static RemoveTpmFromVmContext valueOf(RemoveTpmMsg msg) { + RemoveTpmFromVmContext context = new RemoveTpmFromVmContext(); + context.vmInstanceUuid = msg.getVmInstanceUuid(); + context.tpmUuid = msg.getTpmUuid(); + return context; + } + } + + @SuppressWarnings("rawtypes") + private void removeTpmFromVm(RemoveTpmFromVmContext context, Completion completion) { + SimpleFlowChain chain = new SimpleFlowChain(); + chain.setName("remove-tpm-from-vm-" + context.vmInstanceUuid); + chain.then(new NoRollbackFlow() { + String __name__ = "check-vm-status"; + + @Override + public void run(FlowTrigger trigger, Map data) { + VmInstanceVO vm = Q.New(VmInstanceVO.class) + .eq(VmInstanceVO_.uuid, context.vmInstanceUuid) + .find(); + + if (!SUPPORT_VM_STATES_FOR_TPM_OPERATION.contains(vm.getState())) { + trigger.fail(err(VM_STATE_ERROR, + "The current VM state does not support removing TPM operations") + .withOpaque("support.vm.state", SUPPORT_VM_STATES_FOR_TPM_OPERATION)); + return; + } + trigger.next(); + } + }).then(new NoRollbackFlow() { + String __name__ = "remove-tpm-db-records"; + + @Override + public void run(FlowTrigger trigger, Map data) { + SQL.New(TpmVO.class) + .eq(TpmVO_.uuid, context.tpmUuid) + .delete(); + trigger.next(); + } + }).done(new FlowDoneHandler(completion) { + @Override + public void handle(Map data) { + completion.success(); + } + }).error(new FlowErrorHandler(completion) { + @Override + public void handle(ErrorCode errorCode, Map data) { + completion.fail(errorCode); + } + }).start(); + } + + private void handle(APIGetTpmCapabilityMsg msg) { + TpmCapabilityView view = new TpmCapabilityView(); + + final TpmVO tpm = Q.New(TpmVO.class) + .eq(TpmVO_.uuid, msg.getTpmUuid()) + .findValue(); + final VmInstanceVO vm = Q.New(VmInstanceVO.class) + .eq(VmInstanceVO_.uuid, tpm.getVmInstanceUuid()) + .findValue(); + + view.setTpmInventory(TpmInventory.valueOf(tpm)); + view.setEdkVersion(VM_EDK.getTokenByResourceUuid(vm.getUuid(), EDK_RPM_TOKEN)); + + if (vm.getHostUuid() != null) { + view.setSwtpmVersion(SWTPM_VERSION.getTokenByResourceUuid(vm.getHostUuid(), SWTPM_VERSION_TOKEN)); + } else if (vm.getLastHostUuid() != null) { + view.setSwtpmVersion(SWTPM_VERSION.getTokenByResourceUuid(vm.getLastHostUuid(), SWTPM_VERSION_TOKEN)); + } + + ResourceConfig resourceConfig = resourceConfigFacade.getResourceConfig(RESET_TPM_AFTER_VM_CLONE.getIdentity()); + view.setResetTpmAfterVmCloneConfig(resourceConfig.getResourceConfigValue(vm.getUuid(), Boolean.class)); + + APIGetTpmCapabilityReply reply = new APIGetTpmCapabilityReply(); + reply.setInventory(view); + bus.reply(msg, reply); + } + + private void handle(APIAddTpmMsg msg) { + APIAddTpmEvent event = new APIAddTpmEvent(msg.getId()); + + AddTpmMsg inner = AddTpmMsg.valueOf(msg); + bus.makeTargetServiceIdByResourceUuid(inner, SERVICE_ID, msg.getResourceUuid()); + bus.send(inner, new CloudBusCallBack(msg) { + @Override + public void run(MessageReply reply) { + if (reply.isSuccess()) { + event.setInventory(((AddTpmReply) reply.castReply()).getInventory()); + } else { + event.setError(reply.getError()); + } + bus.publish(event); + } + }); + } + + private void handle(APIRemoveTpmMsg msg) { + APIRemoveTpmEvent event = new APIRemoveTpmEvent(msg.getId()); + + RemoveTpmMsg inner = RemoveTpmMsg.valueOf(msg); + bus.makeTargetServiceIdByResourceUuid(inner, SERVICE_ID, msg.getTpmUuid()); + bus.send(inner, new CloudBusCallBack(msg) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + event.setError(reply.getError()); + } + bus.publish(event); + } + }); + } + + private void handle(APIUpdateTpmMsg msg) { + throw err(NOT_SUPPORTED, "UpdateTpm is not supported in current version").toException(); + } +} diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index 04907868b8c..28b04789a3e 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -268,4 +268,11 @@ + + + + + + + diff --git a/utils/src/main/java/org/zstack/utils/StringDSL.java b/utils/src/main/java/org/zstack/utils/StringDSL.java index 6cd30a8122f..3dcc9354281 100755 --- a/utils/src/main/java/org/zstack/utils/StringDSL.java +++ b/utils/src/main/java/org/zstack/utils/StringDSL.java @@ -148,6 +148,21 @@ public static boolean isApiId(String apiId) { return apiId != null && apiId.matches("[0-9a-f]{8}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{12}"); } + public static String createFixedUuid(Class voClass) { + return createFixedUuid(voClass.getSimpleName()); + } + + public static String createFixedUuid(String text) { + StringBuilder builder = new StringBuilder( + UUID.nameUUIDFromBytes(text.getBytes()).toString().replace("-", "")); + builder.setCharAt(0, 'f'); + builder.setCharAt(1, 'f'); + builder.setCharAt(2, '0'); + builder.setCharAt(30, '0'); + builder.setCharAt(31, '0'); + return builder.toString(); + } + private static int hashOXR(List lst) { return lst.stream().mapToInt(Object::hashCode) .reduce(0, (l, r) -> l ^ r); From b49144f23318b7e1ea272db240e964a82685c3e8 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 4 Feb 2026 15:21:52 +0800 Subject: [PATCH 003/122] [compute]: support create VM with TPM Resolves: ZSV-11310 Change-Id: I6b746e6a666d7267626d647777677a77716a6261 (cherry picked from commit 443f4c23dfa7536f430007c2100dd28803ff273d) --- .../vm/InstantiateVmFromNewCreatedStruct.java | 12 +++++++ .../org/zstack/compute/vm/VmInstanceBase.java | 1 + .../compute/vm/VmInstanceManagerImpl.java | 17 ++++++++++ .../zstack/compute/vm/VmInstanceUtils.java | 2 +- .../compute/vm/devices/VmTpmExtensions.java | 27 ++++++++++++++++ .../compute/vm/devices/VmTpmManager.java | 29 +++++++++++++++++ conf/springConfigXml/Kvm.xml | 9 ++++-- conf/springConfigXml/VmInstanceManager.xml | 10 +++++- ...plianceVmInstanceCreateExtensionPoint.java | 5 +++ .../zstack/header/vm/CreateVmInstanceMsg.java | 9 ++++++ .../InstantiateNewCreatedVmInstanceMsg.java | 11 ++++++- .../vm/VmInstanceCreateExtensionPoint.java | 6 +++- .../org/zstack/header/vm/VmInstanceSpec.java | 10 ++++++ .../appliancevm/CreateApplianceVmJob.java | 4 +++ .../java/org/zstack/kvm/KVMAgentCommands.java | 10 ++++++ .../org/zstack/kvm/tpm/KvmTpmExtensions.java | 32 +++++++++++++++++++ .../org/zstack/kvm/tpm/KvmTpmManager.java | 16 ++++------ .../main/java/org/zstack/kvm/tpm/TpmTO.java | 15 +++++++++ .../test/resources/springConfigXml/Kvm.xml | 9 ++++-- 19 files changed, 216 insertions(+), 18 deletions(-) create mode 100644 compute/src/main/java/org/zstack/compute/vm/devices/VmTpmExtensions.java create mode 100644 compute/src/main/java/org/zstack/compute/vm/devices/VmTpmManager.java create mode 100644 header/src/main/java/org/zstack/header/vm/ApplianceVmInstanceCreateExtensionPoint.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmExtensions.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmTO.java diff --git a/compute/src/main/java/org/zstack/compute/vm/InstantiateVmFromNewCreatedStruct.java b/compute/src/main/java/org/zstack/compute/vm/InstantiateVmFromNewCreatedStruct.java index 4a0d2ba9627..eee13650bb3 100644 --- a/compute/src/main/java/org/zstack/compute/vm/InstantiateVmFromNewCreatedStruct.java +++ b/compute/src/main/java/org/zstack/compute/vm/InstantiateVmFromNewCreatedStruct.java @@ -5,6 +5,7 @@ import org.zstack.header.vm.InstantiateNewCreatedVmInstanceMsg; import org.zstack.header.vm.VmCreationStrategy; import org.zstack.header.vm.VmNicSpec; +import org.zstack.header.vm.devices.VmDevicesSpec; import java.util.ArrayList; import java.util.List; @@ -31,6 +32,7 @@ public class InstantiateVmFromNewCreatedStruct { private List sshKeyPairUuids; private final List candidatePrimaryStorageUuidsForRootVolume = new ArrayList<>(); private final List candidatePrimaryStorageUuidsForDataVolume = new ArrayList<>(); + private VmDevicesSpec devicesSpec; public List getCandidatePrimaryStorageUuidsForRootVolume() { return candidatePrimaryStorageUuidsForRootVolume; @@ -64,6 +66,14 @@ public void setDiskAOs(List diskAOs) { this.diskAOs = diskAOs; } + public VmDevicesSpec getDevicesSpec() { + return devicesSpec; + } + + public void setDevicesSpec(VmDevicesSpec devicesSpec) { + this.devicesSpec = devicesSpec; + } + public List getRootVolumeSystemTags() { return rootVolumeSystemTags; } @@ -142,6 +152,7 @@ public static InstantiateVmFromNewCreatedStruct fromMessage(InstantiateNewCreate struct.setDataVolumeSystemTagsOnIndex(msg.getDataVolumeSystemTagsOnIndex()); struct.setDisableL3Networks(msg.getDisableL3Networks()); struct.setDiskAOs(msg.getDiskAOs()); + struct.setDevicesSpec(msg.getDevicesSpec()); return struct; } @@ -161,6 +172,7 @@ public static InstantiateVmFromNewCreatedStruct fromMessage(CreateVmInstanceMsg struct.setDataVolumeSystemTagsOnIndex(msg.getDataVolumeSystemTagsOnIndex()); struct.setDisableL3Networks(msg.getDisableL3Networks()); struct.setDiskAOs(msg.getDiskAOs()); + struct.setDevicesSpec(msg.getDevicesSpec()); return struct; } diff --git a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java index e5065e0f06a..2b28c92a511 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java @@ -7767,6 +7767,7 @@ public DiskOfferingVO call(DiskOfferingVO arg) { } spec.setDiskAOs(struct.getDiskAOs()); + spec.setDevicesSpec(struct.getDevicesSpec()); List cdRomSpecs = buildVmCdRomSpecsForNewCreated(spec); spec.setCdRomSpecs(cdRomSpecs); diff --git a/compute/src/main/java/org/zstack/compute/vm/VmInstanceManagerImpl.java b/compute/src/main/java/org/zstack/compute/vm/VmInstanceManagerImpl.java index 321c7b3325d..6ebaed235ed 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceManagerImpl.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceManagerImpl.java @@ -1226,6 +1226,22 @@ public void setup() { attachOtherDisk = !otherDisks.isEmpty(); } + flow(new Flow() { + String __name__ = "call-after-persist-vm-extensions"; + @Override + public void run(FlowTrigger trigger, Map data) { + pluginRgty.getExtensionList(VmInstanceCreateExtensionPoint.class).forEach( + extensionPoint -> extensionPoint.afterPersistVmInstanceVO(finalVo, msg)); + trigger.next(); + } + + @Override + public void rollback(FlowRollback trigger, Map data) { + // do nothing + trigger.rollback(); + } + }); + flow(new Flow() { List errorCodes = new ArrayList<>(); String __name__ = String.format("instantiate-systemTag-for-vm-%s", finalVo.getUuid()); @@ -1323,6 +1339,7 @@ public void run(FlowTrigger trigger, Map data) { smsg.setDataVolumeSystemTags(msg.getDataVolumeSystemTags()); smsg.setDataVolumeSystemTagsOnIndex(msg.getDataVolumeSystemTagsOnIndex()); smsg.setDiskAOs(msg.getDiskAOs()); + smsg.setDevicesSpec(msg.getDevicesSpec()); bus.makeTargetServiceIdByResourceUuid(smsg, VmInstanceConstant.SERVICE_ID, finalVo.getUuid()); bus.send(smsg, new CloudBusCallBack(smsg) { @Override diff --git a/compute/src/main/java/org/zstack/compute/vm/VmInstanceUtils.java b/compute/src/main/java/org/zstack/compute/vm/VmInstanceUtils.java index f0bdeded2af..208e265c968 100644 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceUtils.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceUtils.java @@ -65,7 +65,7 @@ public static CreateVmInstanceMsg fromAPICreateVmInstanceMsg(APICreateVmInstance cmsg.setStrategy(msg.getStrategy()); cmsg.setDiskAOs(msg.getDiskAOs()); - + cmsg.setDevicesSpec(msg.getDevicesSpec()); if (CollectionUtils.isNotEmpty(msg.getDataDiskOfferingUuids()) || CollectionUtils.isNotEmpty(msg.getDataDiskSizes())) { cmsg.setPrimaryStorageUuidForDataVolume(getPSUuidForDataVolume(msg.getSystemTags())); } diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmExtensions.java b/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmExtensions.java new file mode 100644 index 00000000000..4b25befa82f --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmExtensions.java @@ -0,0 +1,27 @@ +package org.zstack.compute.vm.devices; + +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.header.vm.CreateVmInstanceMsg; +import org.zstack.header.vm.VmInstanceCreateExtensionPoint; +import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.devices.VmDevicesSpec; + +public class VmTpmExtensions implements VmInstanceCreateExtensionPoint { + @Autowired + private VmTpmManager vmTpmManager; + + @Override + public void preCreateVmInstance(CreateVmInstanceMsg msg) { + // do-nothing + } + + @Override + public void afterPersistVmInstanceVO(VmInstanceVO vo, CreateVmInstanceMsg msg) { + final VmDevicesSpec spec = msg.getDevicesSpec(); + if (spec == null || spec.getTpm() == null || !spec.getTpm().isEnable()) { + return; + } + + vmTpmManager.persistTpmVO(null, vo.getUuid()); + } +} diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmManager.java b/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmManager.java new file mode 100644 index 00000000000..a37e57d8426 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmManager.java @@ -0,0 +1,29 @@ +package org.zstack.compute.vm.devices; + +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.core.Platform; +import org.zstack.core.db.DatabaseFacade; +import org.zstack.header.tpm.entity.TpmVO; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +public class VmTpmManager { + private static final CLogger logger = Utils.getLogger(VmTpmManager.class); + + @Autowired + private DatabaseFacade databaseFacade; + + public TpmVO persistTpmVO(String tpmUuid, String vmUuid) { + if (tpmUuid == null) { + tpmUuid = Platform.getUuid(); + } + TpmVO tpm = new TpmVO(); + tpm.setUuid(tpmUuid); + tpm.setResourceName("TPM-for-VM-" + vmUuid); + tpm.setVmInstanceUuid(vmUuid); + databaseFacade.persistAndRefresh(tpm); + + logger.debug("Persisted TpmVO for VM " + vmUuid + " with uuid=" + tpm.getUuid()); + return tpm; + } +} diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index c066400c153..41663d2e406 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -262,11 +262,16 @@ - - + + + + + + + diff --git a/conf/springConfigXml/VmInstanceManager.xml b/conf/springConfigXml/VmInstanceManager.xml index f5d224bb417..3097c362d08 100755 --- a/conf/springConfigXml/VmInstanceManager.xml +++ b/conf/springConfigXml/VmInstanceManager.xml @@ -285,4 +285,12 @@ - + + + + + + + + + diff --git a/header/src/main/java/org/zstack/header/vm/ApplianceVmInstanceCreateExtensionPoint.java b/header/src/main/java/org/zstack/header/vm/ApplianceVmInstanceCreateExtensionPoint.java new file mode 100644 index 00000000000..ef723f29a1d --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/ApplianceVmInstanceCreateExtensionPoint.java @@ -0,0 +1,5 @@ +package org.zstack.header.vm; + +public interface ApplianceVmInstanceCreateExtensionPoint { + default void afterPersistApplianceVmInstanceVO(VmInstanceVO vo) {} +} diff --git a/header/src/main/java/org/zstack/header/vm/CreateVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/CreateVmInstanceMsg.java index dc364097a8f..3513449cf5a 100755 --- a/header/src/main/java/org/zstack/header/vm/CreateVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/CreateVmInstanceMsg.java @@ -1,6 +1,7 @@ package org.zstack.header.vm; import org.zstack.header.message.NeedReplyMessage; +import org.zstack.header.vm.devices.VmDevicesSpec; import java.util.ArrayList; import java.util.List; @@ -45,6 +46,7 @@ public class CreateVmInstanceMsg extends NeedReplyMessage implements CreateVmIns private List sshKeyPairUuids; private final List candidatePrimaryStorageUuidsForRootVolume = new ArrayList<>(); private final List candidatePrimaryStorageUuidsForDataVolume = new ArrayList<>(); + private VmDevicesSpec devicesSpec; public List getCandidatePrimaryStorageUuidsForRootVolume() { return candidatePrimaryStorageUuidsForRootVolume; @@ -384,4 +386,11 @@ public boolean getVirtio() { public void setVirtio(boolean virtio) { this.virtio = virtio; } + public VmDevicesSpec getDevicesSpec() { + return devicesSpec; + } + + public void setDevicesSpec(VmDevicesSpec devicesSpec) { + this.devicesSpec = devicesSpec; + } } diff --git a/header/src/main/java/org/zstack/header/vm/InstantiateNewCreatedVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/InstantiateNewCreatedVmInstanceMsg.java index f27bebf9802..a79a5bdab68 100755 --- a/header/src/main/java/org/zstack/header/vm/InstantiateNewCreatedVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/InstantiateNewCreatedVmInstanceMsg.java @@ -1,6 +1,7 @@ package org.zstack.header.vm; import org.zstack.header.message.NeedReplyMessage; +import org.zstack.header.vm.devices.VmDevicesSpec; import java.util.ArrayList; import java.util.List; @@ -47,6 +48,7 @@ public void setCandidatePrimaryStorageUuidsForDataVolume(List candidateP } private List diskAOs; + private VmDevicesSpec devicesSpec; public List getDiskAOs() { return diskAOs; @@ -56,6 +58,14 @@ public void setDiskAOs(List diskAOs) { this.diskAOs = diskAOs; } + public VmDevicesSpec getDevicesSpec() { + return devicesSpec; + } + + public void setDevicesSpec(VmDevicesSpec devicesSpec) { + this.devicesSpec = devicesSpec; + } + public List getSoftAvoidHostUuids() { return softAvoidHostUuids; } @@ -197,4 +207,3 @@ public void setDisableL3Networks(List disableL3Networks) { this.disableL3Networks = disableL3Networks; } } - diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceCreateExtensionPoint.java b/header/src/main/java/org/zstack/header/vm/VmInstanceCreateExtensionPoint.java index c7df865bfa1..029eaa43334 100644 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceCreateExtensionPoint.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceCreateExtensionPoint.java @@ -1,8 +1,12 @@ package org.zstack.header.vm; /** - * Created by lining on 2019/4/17. + * Only for UserVM. + * + * Appliance VM use {@link ApplianceVmInstanceCreateExtensionPoint} */ public interface VmInstanceCreateExtensionPoint { void preCreateVmInstance(CreateVmInstanceMsg msg); + + default void afterPersistVmInstanceVO(VmInstanceVO vo, CreateVmInstanceMsg msg) {} } diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java b/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java index 99ee2173b98..e3344af6e49 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java @@ -13,6 +13,7 @@ import org.zstack.header.network.l3.L3NetworkInventory; import org.zstack.header.storage.primary.PrimaryStorageInventory; import org.zstack.header.vm.VmInstanceConstant.VmOperation; +import org.zstack.header.vm.devices.VmDevicesSpec; import org.zstack.header.volume.VolumeFormat; import org.zstack.header.volume.VolumeInventory; import org.zstack.header.volume.VolumeType; @@ -405,6 +406,7 @@ public void setCandidatePrimaryStorageUuidsForDataVolume(List candidateP private List disableL3Networks; private List diskAOs; + private VmDevicesSpec devicesSpec; public List getDiskAOs() { return diskAOs; @@ -414,6 +416,14 @@ public void setDiskAOs(List diskAOs) { this.diskAOs = diskAOs; } + public VmDevicesSpec getDevicesSpec() { + return devicesSpec; + } + + public void setDevicesSpec(VmDevicesSpec devicesSpec) { + this.devicesSpec = devicesSpec; + } + public boolean isSkipIpAllocation() { return skipIpAllocation; } diff --git a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/CreateApplianceVmJob.java b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/CreateApplianceVmJob.java index f25d80f4818..62207f6cd25 100755 --- a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/CreateApplianceVmJob.java +++ b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/CreateApplianceVmJob.java @@ -182,6 +182,10 @@ protected ApplianceVmVO scripts() { } } + final ApplianceVmVO finalVO = avo; + pluginRgty.getExtensionList(ApplianceVmInstanceCreateExtensionPoint.class).forEach( + extension -> extension.afterPersistApplianceVmInstanceVO(finalVO)); + trigger.next(); } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java index 6e0a2a7da09..9d423f3ab6d 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -15,6 +15,7 @@ import org.zstack.header.vm.*; import org.zstack.header.vm.devices.DeviceAddress; import org.zstack.header.vm.devices.VirtualDeviceInfo; +import org.zstack.kvm.tpm.TpmTO; import org.zstack.network.securitygroup.RuleTO; import org.zstack.network.securitygroup.SecurityGroupMembersTO; import org.zstack.network.securitygroup.VmNicSecurityTO; @@ -2352,6 +2353,7 @@ public static class StartVmCmd extends vdiCmd implements VmAddOnsCmd { @GrayVersion(value = "5.0.0") private List cacheVolumes; private List nics; + private TpmTO tpm; @GrayVersion(value = "5.0.0") private long timeout; @GrayVersion(value = "5.0.0") @@ -2904,6 +2906,14 @@ public void setNics(List nics) { this.nics = nics; } + public TpmTO getTpm() { + return tpm; + } + + public void setTpm(TpmTO tpm) { + this.tpm = tpm; + } + public long getTimeout() { return timeout; } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmExtensions.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmExtensions.java new file mode 100644 index 00000000000..7ff72c6affa --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmExtensions.java @@ -0,0 +1,32 @@ +package org.zstack.kvm.tpm; + +import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.vm.VmInstanceSpec; +import org.zstack.header.vm.devices.VmDevicesSpec; +import org.zstack.kvm.KVMAgentCommands; +import org.zstack.kvm.KVMHostInventory; +import org.zstack.kvm.KVMStartVmExtensionPoint; + +public class KvmTpmExtensions implements KVMStartVmExtensionPoint { + @Override + public void beforeStartVmOnKvm(KVMHostInventory host, VmInstanceSpec spec, KVMAgentCommands.StartVmCmd cmd) { + final VmDevicesSpec devicesSpec = spec.getDevicesSpec(); + if (devicesSpec == null || devicesSpec.getTpm() == null || !devicesSpec.getTpm().isEnable()) { + return; + } + + TpmTO tpm = new TpmTO(); + tpm.setKeyProviderUuid(devicesSpec.getTpm().getKeyProviderUuid()); + cmd.setTpm(tpm); + } + + @Override + public void startVmOnKvmSuccess(KVMHostInventory host, VmInstanceSpec spec) { + // do-nothing + } + + @Override + public void startVmOnKvmFailed(KVMHostInventory host, VmInstanceSpec spec, ErrorCode err) { + // do-nothing + } +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmManager.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmManager.java index 2d643de746b..44b31131c87 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmManager.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmManager.java @@ -1,10 +1,10 @@ package org.zstack.kvm.tpm; import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.vm.devices.VmTpmManager; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.CloudBusCallBack; import org.zstack.core.cloudbus.MessageSafe; -import org.zstack.core.db.DatabaseFacade; import org.zstack.core.db.Q; import org.zstack.core.db.SQL; import org.zstack.core.thread.ChainTask; @@ -63,9 +63,9 @@ public class KvmTpmManager extends AbstractService { @Autowired private ThreadFacade threadFacade; @Autowired - private DatabaseFacade databaseFacade; - @Autowired private ResourceConfigFacade resourceConfigFacade; + @Autowired + private VmTpmManager vmTpmManager; @Override public boolean start() { @@ -197,11 +197,7 @@ public void run(FlowTrigger trigger, Map data) { @Override public void run(FlowTrigger trigger, Map data) { - TpmVO tpm = new TpmVO(); - tpm.setUuid(context.tpmUuid); - tpm.setResourceName("TPM-for-VM-" + context.vmInstanceUuid); - tpm.setVmInstanceUuid(context.vmInstanceUuid); - databaseFacade.persist(tpm); + vmTpmManager.persistTpmVO(context.tpmUuid, context.vmInstanceUuid); trigger.next(); } }).done(new FlowDoneHandler(completion) { @@ -312,10 +308,10 @@ private void handle(APIGetTpmCapabilityMsg msg) { final TpmVO tpm = Q.New(TpmVO.class) .eq(TpmVO_.uuid, msg.getTpmUuid()) - .findValue(); + .find(); final VmInstanceVO vm = Q.New(VmInstanceVO.class) .eq(VmInstanceVO_.uuid, tpm.getVmInstanceUuid()) - .findValue(); + .find(); view.setTpmInventory(TpmInventory.valueOf(tpm)); view.setEdkVersion(VM_EDK.getTokenByResourceUuid(vm.getUuid(), EDK_RPM_TOKEN)); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmTO.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmTO.java new file mode 100644 index 00000000000..d3210a3c2d7 --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmTO.java @@ -0,0 +1,15 @@ +package org.zstack.kvm.tpm; + +import java.io.Serializable; + +public class TpmTO implements Serializable { + private String keyProviderUuid; + + public String getKeyProviderUuid() { + return keyProviderUuid; + } + + public void setKeyProviderUuid(String keyProviderUuid) { + this.keyProviderUuid = keyProviderUuid; + } +} diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index 28b04789a3e..596812c3bd0 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -268,11 +268,16 @@ - - + + + + + + + From 9dc0572355940ebf224b8f325104318dc57da09c Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 4 Feb 2026 18:30:53 +0800 Subject: [PATCH 004/122] [compute]: add secure boot dependency Resolves: ZSV-11310 Resolves: ZSPHER-1 Change-Id: I6d787a7375636c77656176616478717872676478 (cherry picked from commit 6e6a219b00ce7af9e5db6903e49b60d23c05fd17) --- .../org/zstack/compute/vm/VmGlobalConfig.java | 1 + conf/springConfigXml/Kvm.xml | 6 +++ .../java/org/zstack/kvm/KVMAgentCommands.java | 10 ++++ .../src/main/java/org/zstack/kvm/KVMHost.java | 4 -- .../kvm/KvmResourceConfigExtension.java | 8 +++ .../kvm/efi/KvmSecureBootExtensions.java | 53 +++++++++++++++++++ .../test/resources/springConfigXml/Kvm.xml | 13 +++-- 7 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java diff --git a/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java b/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java index 841fb9c7f4a..648f4e0921c 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java @@ -117,6 +117,7 @@ public class VmGlobalConfig { @GlobalConfigDef(defaultValue = "false", type = Boolean.class, description = "sync clock after vm resume") public static GlobalConfig VM_CLOCK_SYNC_AFTER_VM_RESUME = new GlobalConfig(CATEGORY, "vm.clock.sync.after.vm.resume"); + @BindResourceConfig(value = {VmInstanceVO.class, ClusterVO.class}) @GlobalConfigValidation(validValues = {"true", "false"}) public static GlobalConfig ENABLE_UEFI_SECURE_BOOT = new GlobalConfig(CATEGORY, "enable.uefi.secure.boot"); diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index 41663d2e406..4cc9715acb2 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -274,4 +274,10 @@ + + + + + + diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java index 9d423f3ab6d..d641c592809 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -2420,6 +2420,8 @@ public static class StartVmCmd extends vdiCmd implements VmAddOnsCmd { // used when bootMode == 'UEFI' @GrayVersion(value = "5.0.0") private boolean secureBoot; + private String edkVersion; + @GrayVersion(value = "5.0.0") private boolean fromForeignHypervisor; @GrayVersion(value = "5.0.0") @@ -2586,6 +2588,14 @@ public void setSecureBoot(boolean secureBoot) { this.secureBoot = secureBoot; } + public String getEdkVersion() { + return edkVersion; + } + + public void setEdkVersion(String edkVersion) { + this.edkVersion = edkVersion; + } + public boolean isEmulateHyperV() { return emulateHyperV; } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index b7f7169918f..fc1881b7bac 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -4802,10 +4802,6 @@ protected void startVm(final VmInstanceSpec spec, final NeedReplyMessage msg, fi String bootMode = VmSystemTags.BOOT_MODE.getTokenByResourceUuid(spec.getVmInventory().getUuid(), VmSystemTags.BOOT_MODE_TOKEN); cmd.setBootMode(bootMode == null ? ImageBootMode.Legacy.toString() : bootMode); - if (cmd.getBootMode().equals(ImageBootMode.UEFI.toString()) - || cmd.getBootMode().equals(ImageBootMode.UEFI_WITH_CSM.toString())) { - cmd.setSecureBoot(VmGlobalConfig.ENABLE_UEFI_SECURE_BOOT.value(Boolean.class)); - } deviceBootOrderOperator.updateVmDeviceBootOrder(cmd, spec); cmd.setBootDev(toKvmBootDev(spec.getBootOrders())); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KvmResourceConfigExtension.java b/plugin/kvm/src/main/java/org/zstack/kvm/KvmResourceConfigExtension.java index 917648f9209..c116cfaf42c 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KvmResourceConfigExtension.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KvmResourceConfigExtension.java @@ -1,6 +1,7 @@ package org.zstack.kvm; import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.vm.VmGlobalConfig; import org.zstack.header.vm.ArchiveResourceConfigBundle; import org.zstack.header.vm.ResourceConfigMemorySnapshotExtensionPoint; import org.zstack.resourceconfig.ResourceConfigFacade; @@ -21,6 +22,13 @@ public List getNeedToArchiveRe bundle.setValue(rcf.getResourceConfigValue(KVMGlobalConfig.NESTED_VIRTUALIZATION, resourceUuid, String.class)); bundleList.add(bundle); + ArchiveResourceConfigBundle.ResourceConfigBundle secureBootBundle = + new ArchiveResourceConfigBundle.ResourceConfigBundle(); + secureBootBundle.setResourceUuid(resourceUuid); + secureBootBundle.setIdentity(VmGlobalConfig.ENABLE_UEFI_SECURE_BOOT.getIdentity()); + secureBootBundle.setValue(rcf.getResourceConfigValue(VmGlobalConfig.ENABLE_UEFI_SECURE_BOOT, resourceUuid, String.class)); + bundleList.add(secureBootBundle); + if (Boolean.TRUE.equals(KVMHostUtils.isWindowsVmByUuid(resourceUuid))) { Boolean cpuHardwareVirtualization = rcf.getResourceConfigValue(KVMGlobalConfig.VM_CPU_HARDWARE_VIRTUALIZATION, resourceUuid, Boolean.class); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java new file mode 100644 index 00000000000..1ff53a531bc --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java @@ -0,0 +1,53 @@ +package org.zstack.kvm.efi; + +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.vm.VmGlobalConfig; +import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.image.ImageBootMode; +import org.zstack.header.vm.VmInstanceSpec; +import org.zstack.kvm.KVMAgentCommands; +import org.zstack.kvm.KVMGlobalConfig; +import org.zstack.kvm.KVMHostInventory; +import org.zstack.kvm.KVMStartVmExtensionPoint; +import org.zstack.resourceconfig.ResourceConfig; +import org.zstack.resourceconfig.ResourceConfigFacade; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +public class KvmSecureBootExtensions implements KVMStartVmExtensionPoint { + private static final CLogger logger = Utils.getLogger(KvmSecureBootExtensions.class); + + @Autowired + private ResourceConfigFacade resourceConfigFacade; + + @Override + public void beforeStartVmOnKvm(KVMHostInventory host, VmInstanceSpec spec, KVMAgentCommands.StartVmCmd cmd) { + if (!isUefiBootMode(cmd.getBootMode())) { + return; + } + + ResourceConfig resourceConfig; + resourceConfig = resourceConfigFacade.getResourceConfig(VmGlobalConfig.ENABLE_UEFI_SECURE_BOOT.getIdentity()); + cmd.setSecureBoot(resourceConfig.getResourceConfigValue(spec.getVmInventory().getUuid(), Boolean.class)); + + resourceConfig = resourceConfigFacade.getResourceConfig(KVMGlobalConfig.VM_EDK_VERSION_CONFIG.getIdentity()); + final String edkVersion = resourceConfig.getResourceConfigValue(spec.getVmInventory().getUuid(), String.class); + if (edkVersion != null && !edkVersion.isEmpty()) { + cmd.setEdkVersion(edkVersion); + } + } + + @Override + public void startVmOnKvmSuccess(KVMHostInventory host, VmInstanceSpec spec) { + // do-nothing + } + + @Override + public void startVmOnKvmFailed(KVMHostInventory host, VmInstanceSpec spec, ErrorCode err) { + // do-nothing + } + + private boolean isUefiBootMode(String bootMode) { + return bootMode.equals(ImageBootMode.UEFI.toString()) || bootMode.equals(ImageBootMode.UEFI_WITH_CSM.toString()); + } +} diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index 596812c3bd0..5a446206770 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -257,10 +257,9 @@ - - - - + + + @@ -280,4 +279,10 @@ + + + + + + From da8f115c0dd211621a95e6bc6906b55090778bd7 Mon Sep 17 00:00:00 2001 From: "tao.yang" Date: Fri, 6 Feb 2026 19:55:28 +0800 Subject: [PATCH 005/122] [kms]: support kms DBImpact Resolves: ZSV-11331 Change-Id: I786f686371626e6674636772676c68747768716a (cherry picked from commit 4cfe4a49e08c2ff1d02ce6c0ad4168b4b93e995c) --- conf/persistence.xml | 6 ++ .../zstack/header/host/HostKeyIdentityVO.java | 78 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 header/src/main/java/org/zstack/header/host/HostKeyIdentityVO.java diff --git a/conf/persistence.xml b/conf/persistence.xml index e76141af3ad..15a848e5aa8 100755 --- a/conf/persistence.xml +++ b/conf/persistence.xml @@ -25,6 +25,7 @@ org.zstack.header.host.HostVO org.zstack.header.host.HostEO org.zstack.header.host.HostNetworkLabelVO + org.zstack.header.host.HostKeyIdentityVO org.zstack.header.host.CpuFeaturesHistoryVO org.zstack.header.storage.primary.PrimaryStorageVO org.zstack.header.storage.primary.PrimaryStorageEO @@ -191,6 +192,11 @@ org.zstack.header.vm.VmInstanceNumaNodeVO org.zstack.header.host.HostNumaNodeVO org.zstack.header.core.encrypt.EncryptionIntegrityVO + org.zstack.header.keyprovider.KeyProviderVO + org.zstack.header.keyprovider.KmsVO + org.zstack.header.keyprovider.KmsIdentityVO + org.zstack.header.keyprovider.NkpVO + org.zstack.header.keyprovider.EncryptedResourceKeyRefVO org.zstack.storage.primary.sharedblock.SharedBlockCapacityVO org.zstack.header.vm.devices.VmInstanceDeviceAddressVO org.zstack.header.vm.devices.VmInstanceDeviceAddressArchiveVO diff --git a/header/src/main/java/org/zstack/header/host/HostKeyIdentityVO.java b/header/src/main/java/org/zstack/header/host/HostKeyIdentityVO.java new file mode 100644 index 00000000000..b37e6a8ce84 --- /dev/null +++ b/header/src/main/java/org/zstack/header/host/HostKeyIdentityVO.java @@ -0,0 +1,78 @@ +package org.zstack.header.host; + +import org.zstack.header.vo.EntityGraph; +import org.zstack.header.vo.ForeignKey; + +import javax.persistence.*; +import java.sql.Timestamp; + +@Entity +@Table +@EntityGraph( + parents = { + @EntityGraph.Neighbour(type = HostVO.class, myField = "hostUuid", targetField = "uuid") + } +) +public class HostKeyIdentityVO { + @Id + @Column + @ForeignKey(parentEntityClass = HostEO.class, onDeleteAction = ForeignKey.ReferenceOption.CASCADE) + private String hostUuid; + + @Column + private String publicKey; + + @Column + private String fingerprint; + + @Column + private Timestamp createDate; + + @Column + private Timestamp lastOpDate; + + @PreUpdate + private void preUpdate() { + lastOpDate = null; + } + + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + public String getPublicKey() { + return publicKey; + } + + public void setPublicKey(String publicKey) { + this.publicKey = publicKey; + } + + public String getFingerprint() { + return fingerprint; + } + + public void setFingerprint(String fingerprint) { + this.fingerprint = fingerprint; + } + + public Timestamp getCreateDate() { + return createDate; + } + + public void setCreateDate(Timestamp createDate) { + this.createDate = createDate; + } + + public Timestamp getLastOpDate() { + return lastOpDate; + } + + public void setLastOpDate(Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } +} From cd084cdc204225de731a4349557942e4073c07cc Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 11 Feb 2026 18:55:48 +0800 Subject: [PATCH 006/122] [kvm]: update KVMGlobalConfig default value to "None" Resolves: ZSV-11340 Related: ZSV-11010 Change-Id: I63646f6264697877707967746f69756e776f6c71 --- plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java index e73e6a94f72..dd66ec0277f 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java @@ -166,8 +166,8 @@ public class KVMGlobalConfig { @BindResourceConfig({HostVO.class, ClusterVO.class}) public static GlobalConfig MINIMUM_MEMORY_SIZE_BEFORE_START_VM = new GlobalConfig(CATEGORY, "min.free.memory.size"); - @GlobalConfigDef(defaultValue = "", - description = "Specify the EDK version to be used for the next VM startup. Default empty string indicates the use of the system's default EDK version") + @GlobalConfigDef(defaultValue = "None", + description = "Specify the EDK version to be used for the next VM startup. None indicates the use of the system's default EDK version") @BindResourceConfig(value = {VmInstanceVO.class, ClusterVO.class}) public static GlobalConfig VM_EDK_VERSION_CONFIG = new GlobalConfig(CATEGORY, "vm.edk.version"); } From 0f1a23c91f99c8d5ea068e8ecff027447adca62e Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 12 Feb 2026 14:40:34 +0800 Subject: [PATCH 007/122] [kvm]: continue to update KVMGlobalConfig default value to "None" Resolves: ZSV-11340 Related: ZSV-11010 Change-Id: I63646f6264697877707967746f69756e776f6c73 --- plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java | 2 ++ .../kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java | 4 +++- .../java/org/zstack/kvm/efi/KvmSecureBootExtensions.java | 6 +++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java index 4ae21dac461..18c201f94fd 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -204,6 +204,8 @@ public interface KVMConstant { public static final String L2_PROVIDER_TYPE_LINUX_BRIDGE = "LinuxBridge"; + public static final String EDK_VERSION_NONE = "None"; + public static final String DHCP_BIN_FILE_PATH = "/usr/local/zstack/dnsmasq"; String KVM_HOST_NETWORK_INTERFACE_DEFAULT = "None"; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java index dd66ec0277f..ccd790b25fc 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java @@ -11,6 +11,8 @@ import org.zstack.header.host.HostVO; import org.zstack.header.zone.ZoneVO; +import static org.zstack.kvm.KVMConstant.EDK_VERSION_NONE; + /** */ @GlobalConfigDefinition @@ -166,7 +168,7 @@ public class KVMGlobalConfig { @BindResourceConfig({HostVO.class, ClusterVO.class}) public static GlobalConfig MINIMUM_MEMORY_SIZE_BEFORE_START_VM = new GlobalConfig(CATEGORY, "min.free.memory.size"); - @GlobalConfigDef(defaultValue = "None", + @GlobalConfigDef(defaultValue = EDK_VERSION_NONE, description = "Specify the EDK version to be used for the next VM startup. None indicates the use of the system's default EDK version") @BindResourceConfig(value = {VmInstanceVO.class, ClusterVO.class}) public static GlobalConfig VM_EDK_VERSION_CONFIG = new GlobalConfig(CATEGORY, "vm.edk.version"); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java index 1ff53a531bc..138fbda2d6d 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java @@ -14,6 +14,10 @@ import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import java.util.Objects; + +import static org.zstack.kvm.KVMConstant.EDK_VERSION_NONE; + public class KvmSecureBootExtensions implements KVMStartVmExtensionPoint { private static final CLogger logger = Utils.getLogger(KvmSecureBootExtensions.class); @@ -32,7 +36,7 @@ public void beforeStartVmOnKvm(KVMHostInventory host, VmInstanceSpec spec, KVMAg resourceConfig = resourceConfigFacade.getResourceConfig(KVMGlobalConfig.VM_EDK_VERSION_CONFIG.getIdentity()); final String edkVersion = resourceConfig.getResourceConfigValue(spec.getVmInventory().getUuid(), String.class); - if (edkVersion != null && !edkVersion.isEmpty()) { + if (!Objects.equals(edkVersion, EDK_VERSION_NONE)) { cmd.setEdkVersion(edkVersion); } } From 21c101437ebec6b90a8778a306536529a0bd29ab Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 24 Feb 2026 16:15:59 +0800 Subject: [PATCH 008/122] [conf]: update default value of secure.boot global config to false In previous versions, the global config "enable.uefi.secure.boot" had no practical effect, as its default value was "true". In the current version, since it affects numerous configurations, its default value has been changed to "false" to maintain consistency with VM startup settings from previous versions and minimize the risk of VM configuration changes. Related: ZSV-11310 Change-Id: I616c796e6e796a726b6f7a77656975717a65726c --- conf/globalConfig/vm.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/globalConfig/vm.xml b/conf/globalConfig/vm.xml index a7f835c82ee..9da4228ad91 100755 --- a/conf/globalConfig/vm.xml +++ b/conf/globalConfig/vm.xml @@ -289,7 +289,7 @@ enable.uefi.secure.boot enable uefi secure boot - true + false vm java.lang.Boolean From 05c3bc81381fb904f9bb9975cae239699283f966 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 13 Feb 2026 14:59:35 +0800 Subject: [PATCH 009/122] [compute]: introduce NvRam type volume If TPM or secure boot is enabled, MN will prepare NvRam volume and instantiate volume to host. Add delete extension to delete NvRam volume after related VM deleted. Resolves: ZSV-11310 Related: ZSPHER-1 Change-Id: I787672786e6873696c6273647778737364767563 --- .../zstack/compute/vm/VmDeleteVolumeFlow.java | 11 +- .../compute/vm/VmExpungeNvRamVolumeFlow.java | 83 +++++ .../org/zstack/compute/vm/VmInstanceBase.java | 2 +- .../compute/vm/devices/VmTpmExtensions.java | 21 +- .../compute/vm/devices/VmTpmManager.java | 37 +++ conf/persistence.xml | 2 + conf/springConfigXml/Kvm.xml | 1 + conf/springConfigXml/VmInstanceManager.xml | 2 + .../zstack/header/vm/VmInstanceConstant.java | 3 + .../org/zstack/header/vm/VmInstanceSpec.java | 9 + .../org/zstack/header/volume/VolumeType.java | 3 +- .../java/org/zstack/kvm/KVMAgentCommands.java | 9 + .../kvm/efi/KvmSecureBootExtensions.java | 289 +++++++++++++++++- .../primary/local/LocalStorageKvmBackend.java | 6 + .../primary/PrimaryStoragePathMaker.java | 4 + .../org/zstack/storage/volume/VolumeBase.java | 5 +- .../test/resources/springConfigXml/Kvm.xml | 1 + 17 files changed, 469 insertions(+), 19 deletions(-) create mode 100644 compute/src/main/java/org/zstack/compute/vm/VmExpungeNvRamVolumeFlow.java diff --git a/compute/src/main/java/org/zstack/compute/vm/VmDeleteVolumeFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmDeleteVolumeFlow.java index 8acb64f9bc3..d3bba11af96 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmDeleteVolumeFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmDeleteVolumeFlow.java @@ -47,7 +47,9 @@ public void run(final FlowTrigger trigger, Map data) { final boolean deleteDataDisk = VmGlobalConfig.DELETE_DATA_VOLUME_ON_VM_DESTROY.value(Boolean.class); /* data volume must be detached anyway no matter if it is going to be deleted */ - if (spec.getVmInventory().getAllVolumes().size() > 1) { + boolean anyDataVolume = spec.getVmInventory().getAllVolumes().stream() + .anyMatch(arg -> VolumeType.Data.toString().equals(arg.getType())); + if (anyDataVolume) { detachDataVolumes(spec); } @@ -58,7 +60,12 @@ public void run(final FlowTrigger trigger, Map data) { return; } - List volumeTypes = Arrays.asList(VolumeType.Root.toString(), VolumeType.Memory.toString(), VolumeType.Cache.toString()); + List volumeTypes = Arrays.asList( + VolumeType.Root.toString(), + VolumeType.Memory.toString(), + VolumeType.Cache.toString(), + VolumeType.NvRam.toString() + ); List ctx = CollectionUtils.transformToList(spec.getVmInventory().getAllVolumes(), new Function() { @Override public VolumeDeletionStruct call(VolumeInventory arg) { diff --git a/compute/src/main/java/org/zstack/compute/vm/VmExpungeNvRamVolumeFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmExpungeNvRamVolumeFlow.java new file mode 100644 index 00000000000..9b608817507 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/VmExpungeNvRamVolumeFlow.java @@ -0,0 +1,83 @@ +package org.zstack.compute.vm; + +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Configurable; +import org.zstack.core.asyncbatch.While; +import org.zstack.core.cloudbus.CloudBus; +import org.zstack.core.cloudbus.CloudBusCallBack; +import org.zstack.core.db.Q; +import org.zstack.header.core.WhileDoneCompletion; +import org.zstack.header.core.workflow.FlowTrigger; +import org.zstack.header.core.workflow.NoRollbackFlow; +import org.zstack.header.errorcode.ErrorCodeList; +import org.zstack.header.message.MessageReply; +import org.zstack.header.vm.VmInstanceConstant; +import org.zstack.header.vm.VmInstanceSpec; +import org.zstack.header.volume.ExpungeVolumeMsg; +import org.zstack.header.volume.VolumeConstant; +import org.zstack.header.volume.VolumeType; +import org.zstack.header.volume.VolumeVO; +import org.zstack.header.volume.VolumeVO_; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.util.List; +import java.util.Map; + +import static org.zstack.core.Platform.multiErr; + +@Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) +public class VmExpungeNvRamVolumeFlow extends NoRollbackFlow { + private static final CLogger logger = Utils.getLogger(VmExpungeNvRamVolumeFlow.class); + + @Autowired + protected CloudBus bus; + + @Override + @SuppressWarnings("rawtypes") + public void run(FlowTrigger trigger, Map data) { + final VmInstanceSpec spec = (VmInstanceSpec) data.get(VmInstanceConstant.Params.VmInstanceSpec.toString()); + String vmUuid = spec.getVmInventory().getUuid(); + + List volumes = Q.New(VolumeVO.class) + .eq(VolumeVO_.vmInstanceUuid, vmUuid) + .eq(VolumeVO_.type, VolumeType.NvRam) + .list(); + if (volumes.isEmpty()) { + trigger.next(); + return; + } + + new While<>(volumes).each((vol, c) -> { + ExpungeVolumeMsg msg = new ExpungeVolumeMsg(); + msg.setVolumeUuid(vol.getUuid()); + bus.makeTargetServiceIdByResourceUuid(msg, VolumeConstant.SERVICE_ID, vol.getUuid()); + bus.send(msg, new CloudBusCallBack(c) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + logger.warn(String.format("failed to expunge the NvRam volume[uuid:%s] of the vm[uuid:%s, name:%s]: %s", + vol.getUuid(), spec.getVmInventory().getUuid(), + spec.getVmInventory().getName(), reply.getError())); + + c.addError(reply.getError() + .withOpaque("volume.uuid", vol.getUuid())); + } + + c.done(); + } + }); + }).run(new WhileDoneCompletion(trigger) { + @Override + public void done(ErrorCodeList errorCodeList) { + if (!errorCodeList.getCauses().isEmpty()) { + trigger.fail(multiErr(errorCodeList.getCauses(), "failed to expunge the NvRam volumes")); + return; + } + + trigger.next(); + } + }); + } +} diff --git a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java index 2b28c92a511..b8825d20792 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java @@ -2816,7 +2816,7 @@ protected void scripts() { sql(VmNicVO.class).eq(VmNicVO_.vmInstanceUuid, self.getUuid()).hardDelete(); sql(VolumeVO.class).eq(VolumeVO_.vmInstanceUuid, self.getUuid()) - .eq(VolumeVO_.type, VolumeType.Root) + .in(VolumeVO_.type, list(VolumeType.Root, VolumeType.NvRam)) .hardDelete(); sql(VmCdRomVO.class).eq(VmCdRomVO_.vmInstanceUuid, self.getUuid()).hardDelete(); sql(VmInstanceVO.class).eq(VmInstanceVO_.uuid, self.getUuid()).hardDelete(); diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmExtensions.java b/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmExtensions.java index 4b25befa82f..6877b2680fc 100644 --- a/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmExtensions.java +++ b/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmExtensions.java @@ -1,12 +1,18 @@ package org.zstack.compute.vm.devices; import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.vm.BuildVmSpecExtensionPoint; import org.zstack.header.vm.CreateVmInstanceMsg; +import org.zstack.header.vm.DiskAO; import org.zstack.header.vm.VmInstanceCreateExtensionPoint; +import org.zstack.header.vm.VmInstanceSpec; import org.zstack.header.vm.VmInstanceVO; import org.zstack.header.vm.devices.VmDevicesSpec; -public class VmTpmExtensions implements VmInstanceCreateExtensionPoint { +import static org.zstack.header.vm.VmInstanceConstant.NV_RAM_DEFAULT_SIZE; + +public class VmTpmExtensions implements VmInstanceCreateExtensionPoint, + BuildVmSpecExtensionPoint { @Autowired private VmTpmManager vmTpmManager; @@ -24,4 +30,17 @@ public void afterPersistVmInstanceVO(VmInstanceVO vo, CreateVmInstanceMsg msg) { vmTpmManager.persistTpmVO(null, vo.getUuid()); } + + @Override + public void afterBuildVmSpec(VmInstanceSpec spec) { + String vmUuid = spec.getVmInventory().getUuid(); + if (!vmTpmManager.needRegisterNvRam(vmUuid)) { + return; + } + + DiskAO nvRamSpec = new DiskAO(); + nvRamSpec.setSize(NV_RAM_DEFAULT_SIZE); + nvRamSpec.setName("NvRam-of-VM-" + vmUuid); + spec.setNvRamSpec(nvRamSpec); + } } diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmManager.java b/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmManager.java index a37e57d8426..ced08da2918 100644 --- a/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmManager.java +++ b/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmManager.java @@ -1,17 +1,29 @@ package org.zstack.compute.vm.devices; import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.vm.VmSystemTags; import org.zstack.core.Platform; import org.zstack.core.db.DatabaseFacade; +import org.zstack.core.db.Q; +import org.zstack.header.image.ImageBootMode; import org.zstack.header.tpm.entity.TpmVO; +import org.zstack.header.tpm.entity.TpmVO_; +import org.zstack.resourceconfig.ResourceConfig; +import org.zstack.resourceconfig.ResourceConfigFacade; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import java.util.Objects; + +import static org.zstack.compute.vm.VmGlobalConfig.ENABLE_UEFI_SECURE_BOOT; + public class VmTpmManager { private static final CLogger logger = Utils.getLogger(VmTpmManager.class); @Autowired private DatabaseFacade databaseFacade; + @Autowired + private ResourceConfigFacade resourceConfigFacade; public TpmVO persistTpmVO(String tpmUuid, String vmUuid) { if (tpmUuid == null) { @@ -26,4 +38,29 @@ public TpmVO persistTpmVO(String tpmUuid, String vmUuid) { logger.debug("Persisted TpmVO for VM " + vmUuid + " with uuid=" + tpm.getUuid()); return tpm; } + + public boolean needRegisterNvRam(String vmUuid) { + boolean tpmExists = Q.New(TpmVO.class) + .eq(TpmVO_.vmInstanceUuid, vmUuid) + .isExists(); + if (tpmExists) { + return true; + } + + String bootMode = VmSystemTags.BOOT_MODE.getTokenByResourceUuid(vmUuid, VmSystemTags.BOOT_MODE_TOKEN); + if (!isUefiBootMode(bootMode)) { + return false; + } + + ResourceConfig resourceConfig = resourceConfigFacade.getResourceConfig(ENABLE_UEFI_SECURE_BOOT.getIdentity()); + return resourceConfig.getResourceConfigValue(vmUuid, Boolean.class) == Boolean.TRUE; + } + + /** + * @param bootMode boot mode, null is Legacy + */ + public static boolean isUefiBootMode(String bootMode) { + return Objects.equals(bootMode, ImageBootMode.UEFI.toString()) + || Objects.equals(bootMode, ImageBootMode.UEFI_WITH_CSM.toString()); + } } diff --git a/conf/persistence.xml b/conf/persistence.xml index 15a848e5aa8..f7e4208b782 100755 --- a/conf/persistence.xml +++ b/conf/persistence.xml @@ -18,6 +18,8 @@ org.zstack.resourceconfig.ResourceConfigVO org.zstack.header.managementnode.ManagementNodeVO org.zstack.header.managementnode.ManagementNodeContextVO + org.zstack.header.tpm.entity.TpmHostRefVO + org.zstack.header.tpm.entity.TpmVO org.zstack.header.zone.ZoneVO org.zstack.header.zone.ZoneEO org.zstack.header.cluster.ClusterVO diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index 4cc9715acb2..65f50e6749c 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -278,6 +278,7 @@ + diff --git a/conf/springConfigXml/VmInstanceManager.xml b/conf/springConfigXml/VmInstanceManager.xml index 3097c362d08..96164268b9e 100755 --- a/conf/springConfigXml/VmInstanceManager.xml +++ b/conf/springConfigXml/VmInstanceManager.xml @@ -119,6 +119,7 @@ org.zstack.compute.vm.VmExpungeSdnNicFlow org.zstack.compute.vm.VmExpungeRootVolumeFlow + org.zstack.compute.vm.VmExpungeNvRamVolumeFlow org.zstack.compute.vm.VmExpungeMemoryVolumeFlow org.zstack.compute.vm.VmExpungeCacheVolumeFlow @@ -291,6 +292,7 @@ + diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceConstant.java b/header/src/main/java/org/zstack/header/vm/VmInstanceConstant.java index ede4cab86cd..b8de900fd8b 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceConstant.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceConstant.java @@ -1,6 +1,7 @@ package org.zstack.header.vm; import org.zstack.header.configuration.PythonClass; +import org.zstack.utils.data.SizeUnit; @PythonClass public interface VmInstanceConstant { @@ -26,6 +27,8 @@ public interface VmInstanceConstant { String L2_TF_VSWITCH_TYPE = "TfL2Network"; String TF_VIRTUAL_NIC_TYPE = "TFVNIC"; + long NV_RAM_DEFAULT_SIZE = SizeUnit.MEGABYTE.toByte(1); + enum Params { VmInstanceSpec, AttachingVolumeInventory, diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java b/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java index e3344af6e49..4b98aece6db 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java @@ -407,6 +407,7 @@ public void setCandidatePrimaryStorageUuidsForDataVolume(List candidateP private List disableL3Networks; private List diskAOs; private VmDevicesSpec devicesSpec; + private DiskAO nvRamSpec; public List getDiskAOs() { return diskAOs; @@ -424,6 +425,14 @@ public void setDevicesSpec(VmDevicesSpec devicesSpec) { this.devicesSpec = devicesSpec; } + public DiskAO getNvRamSpec() { + return nvRamSpec; + } + + public void setNvRamSpec(DiskAO nvRamSpec) { + this.nvRamSpec = nvRamSpec; + } + public boolean isSkipIpAllocation() { return skipIpAllocation; } diff --git a/header/src/main/java/org/zstack/header/volume/VolumeType.java b/header/src/main/java/org/zstack/header/volume/VolumeType.java index e373e5aef98..4266a51ccec 100755 --- a/header/src/main/java/org/zstack/header/volume/VolumeType.java +++ b/header/src/main/java/org/zstack/header/volume/VolumeType.java @@ -4,5 +4,6 @@ public enum VolumeType { Root, Data, Memory, - Cache + Cache, + NvRam, } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java index d641c592809..562b42d82b3 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -2352,6 +2352,7 @@ public static class StartVmCmd extends vdiCmd implements VmAddOnsCmd { private List dataVolumes; @GrayVersion(value = "5.0.0") private List cacheVolumes; + private VolumeTO nvRam; private List nics; private TpmTO tpm; @GrayVersion(value = "5.0.0") @@ -2908,6 +2909,14 @@ public void setCacheVolumes(List cacheVolumes) { this.cacheVolumes = cacheVolumes; } + public VolumeTO getNvRam() { + return nvRam; + } + + public void setNvRam(VolumeTO nvRam) { + this.nvRam = nvRam; + } + public List getNics() { return nics; } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java index 138fbda2d6d..4387b54bfbb 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java @@ -2,43 +2,97 @@ import org.springframework.beans.factory.annotation.Autowired; import org.zstack.compute.vm.VmGlobalConfig; +import org.zstack.compute.vm.devices.VmTpmManager; +import org.zstack.core.cloudbus.CloudBus; +import org.zstack.core.cloudbus.CloudBusCallBack; +import org.zstack.core.db.Q; +import org.zstack.core.workflow.SimpleFlowChain; +import org.zstack.header.core.Completion; +import org.zstack.header.core.workflow.Flow; +import org.zstack.header.core.workflow.FlowDoneHandler; +import org.zstack.header.core.workflow.FlowErrorHandler; +import org.zstack.header.core.workflow.FlowRollback; +import org.zstack.header.core.workflow.FlowTrigger; +import org.zstack.header.core.workflow.NoRollbackFlow; import org.zstack.header.errorcode.ErrorCode; -import org.zstack.header.image.ImageBootMode; +import org.zstack.header.exception.CloudRuntimeException; +import org.zstack.header.identity.AccountResourceRefVO; +import org.zstack.header.identity.AccountResourceRefVO_; +import org.zstack.header.message.MessageReply; +import org.zstack.header.vm.APICreateVmInstanceMsg.DiskAO; +import org.zstack.header.vm.PreVmInstantiateResourceExtensionPoint; import org.zstack.header.vm.VmInstanceSpec; +import org.zstack.header.vm.VmInstantiateResourceException; +import org.zstack.header.volume.CreateVolumeMsg; +import org.zstack.header.volume.CreateVolumeReply; +import org.zstack.header.volume.DeleteVolumeMsg; +import org.zstack.header.volume.InstantiateVolumeMsg; +import org.zstack.header.volume.VolumeConstant; +import org.zstack.header.volume.VolumeDeletionPolicyManager; +import org.zstack.header.volume.VolumeInventory; +import org.zstack.header.volume.VolumeStatus; +import org.zstack.header.volume.VolumeType; +import org.zstack.header.volume.VolumeVO; +import org.zstack.header.volume.VolumeVO_; import org.zstack.kvm.KVMAgentCommands; import org.zstack.kvm.KVMGlobalConfig; import org.zstack.kvm.KVMHostInventory; import org.zstack.kvm.KVMStartVmExtensionPoint; +import org.zstack.kvm.VolumeTO; import org.zstack.resourceconfig.ResourceConfig; import org.zstack.resourceconfig.ResourceConfigFacade; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import javax.persistence.Tuple; +import java.util.Map; import java.util.Objects; +import static org.zstack.core.Platform.operr; import static org.zstack.kvm.KVMConstant.EDK_VERSION_NONE; -public class KvmSecureBootExtensions implements KVMStartVmExtensionPoint { +public class KvmSecureBootExtensions implements KVMStartVmExtensionPoint, + PreVmInstantiateResourceExtensionPoint { private static final CLogger logger = Utils.getLogger(KvmSecureBootExtensions.class); + @Autowired + private CloudBus bus; @Autowired private ResourceConfigFacade resourceConfigFacade; @Override public void beforeStartVmOnKvm(KVMHostInventory host, VmInstanceSpec spec, KVMAgentCommands.StartVmCmd cmd) { - if (!isUefiBootMode(cmd.getBootMode())) { - return; + if (isUefiBootMode(cmd.getBootMode())) { + ResourceConfig resourceConfig; + resourceConfig = resourceConfigFacade.getResourceConfig(VmGlobalConfig.ENABLE_UEFI_SECURE_BOOT.getIdentity()); + cmd.setSecureBoot(resourceConfig.getResourceConfigValue(spec.getVmInventory().getUuid(), Boolean.class)); + + resourceConfig = resourceConfigFacade.getResourceConfig(KVMGlobalConfig.VM_EDK_VERSION_CONFIG.getIdentity()); + final String edkVersion = resourceConfig.getResourceConfigValue(spec.getVmInventory().getUuid(), String.class); + if (!Objects.equals(edkVersion, EDK_VERSION_NONE)) { + cmd.setEdkVersion(edkVersion); + } } - ResourceConfig resourceConfig; - resourceConfig = resourceConfigFacade.getResourceConfig(VmGlobalConfig.ENABLE_UEFI_SECURE_BOOT.getIdentity()); - cmd.setSecureBoot(resourceConfig.getResourceConfigValue(spec.getVmInventory().getUuid(), Boolean.class)); + if (spec.getNvRamSpec() != null) { + prepareNvRamToStartVmCmd(cmd, spec.getNvRamSpec(), host); + } + } - resourceConfig = resourceConfigFacade.getResourceConfig(KVMGlobalConfig.VM_EDK_VERSION_CONFIG.getIdentity()); - final String edkVersion = resourceConfig.getResourceConfigValue(spec.getVmInventory().getUuid(), String.class); - if (!Objects.equals(edkVersion, EDK_VERSION_NONE)) { - cmd.setEdkVersion(edkVersion); + private void prepareNvRamToStartVmCmd(KVMAgentCommands.StartVmCmd cmd, DiskAO nvRamSpec, KVMHostInventory host) { + VolumeVO vo = Q.New(VolumeVO.class) + .eq(VolumeVO_.uuid, nvRamSpec.getSourceUuid()) + .find(); + if (vo == null) { + if (nvRamSpec.getSourceUuid() != null) { + throw new CloudRuntimeException(String.format("cannot find NvRam volume[uuid:%s]", nvRamSpec.getSourceUuid())); + } + return; } + + VolumeInventory nvRamVolume = VolumeInventory.valueOf(vo); + VolumeTO volume = VolumeTO.valueOfWithOutExtension(nvRamVolume, host, null); + cmd.setNvRam(volume); } @Override @@ -52,6 +106,217 @@ public void startVmOnKvmFailed(KVMHostInventory host, VmInstanceSpec spec, Error } private boolean isUefiBootMode(String bootMode) { - return bootMode.equals(ImageBootMode.UEFI.toString()) || bootMode.equals(ImageBootMode.UEFI_WITH_CSM.toString()); + return VmTpmManager.isUefiBootMode(bootMode); + } + + @Override + public void preBeforeInstantiateVmResource(VmInstanceSpec spec) throws VmInstantiateResourceException { + // do-nothing + } + + @Override + public void preInstantiateVmResource(VmInstanceSpec spec, Completion completion) { + final DiskAO nvRamSpec = spec.getNvRamSpec(); + boolean needRegisterNvRam = nvRamSpec != null; + + Tuple tuple = Q.New(VolumeVO.class) + .eq(VolumeVO_.vmInstanceUuid, spec.getVmInventory().getUuid()) + .eq(VolumeVO_.type, VolumeType.NvRam) + .select(VolumeVO_.uuid, VolumeVO_.status) + .findTuple(); + + String nvRamVolumeUuid = tuple == null ? null : tuple.get(0, String.class); + if (needRegisterNvRam && nvRamVolumeUuid != null) { + nvRamSpec.setSourceUuid(nvRamVolumeUuid); + + VolumeStatus volumeStatus = tuple.get(1, VolumeStatus.class); + if (volumeStatus != VolumeStatus.Ready) { + completion.fail(operr("NvRam volume[uuid:%s] is not ready", nvRamVolumeUuid)); + return; + } + + completion.success(); + return; + } else if (!needRegisterNvRam && nvRamVolumeUuid == null) { + completion.success(); + return; + } else if (needRegisterNvRam) { + nvRamSpec.setPrimaryStorageUuid(Q.New(VolumeVO.class) + .eq(VolumeVO_.type, VolumeType.Root) + .eq(VolumeVO_.vmInstanceUuid, spec.getVmInventory().getUuid()) + .select(VolumeVO_.primaryStorageUuid) + .findValue()); + + NvRamVolumeContext context = new NvRamVolumeContext(); + context.vmUuid = spec.getVmInventory().getUuid(); + context.nvRamSpec = nvRamSpec; + context.spec = spec; + createNvRamVolume(context, new Completion(completion) { + @Override + public void success() { + nvRamSpec.setSourceUuid(context.inventory.getUuid()); + completion.success(); + } + + @Override + public void fail(ErrorCode errorCode) { + completion.fail(errorCode); + } + }); + return; + } + + deleteNvRamVolumeIfExists(spec.getVmInventory().getUuid(), new Completion(completion) { + @Override + public void success() { + completion.success(); + } + + @Override + public void fail(ErrorCode errorCode) { + logger.warn("failed to delete NvRam but still continue: " + errorCode.getReadableDetails()); + completion.success(); + } + }); + } + + @Override + public void preReleaseVmResource(VmInstanceSpec spec, Completion completion) { + completion.success(); + } + + static class NvRamVolumeContext { + String vmUuid; + DiskAO nvRamSpec; + VmInstanceSpec spec; + + VolumeInventory inventory; + } + + @SuppressWarnings("rawtypes") + private void createNvRamVolume(NvRamVolumeContext context, Completion completion) { + SimpleFlowChain chain = new SimpleFlowChain(); + chain.setChainName("create-nv-ram-volume-for-vm-" + context.vmUuid); + chain.then(new Flow() { + String __name__ = "create-nv-ram-volume"; + + @Override + public void run(FlowTrigger trigger, Map data) { + String accountUuid = Q.New(AccountResourceRefVO.class) + .eq(AccountResourceRefVO_.resourceUuid, context.vmUuid) + .select(AccountResourceRefVO_.accountUuid) + .findValue(); + + CreateVolumeMsg msg = new CreateVolumeMsg(); + msg.setAccountUuid(accountUuid); + msg.setSize(context.nvRamSpec.getSize()); + msg.setVmInstanceUuid(context.vmUuid); + msg.setPrimaryStorageUuid(context.nvRamSpec.getPrimaryStorageUuid()); + + // NvRam file is raw type (*.fd) in libvirt 8.0.0 + // and qcow2 in libvirt 8.1.0+ (soon) + // We store it as file system (*.raw) with XFS format + msg.setFormat(VolumeConstant.VOLUME_FORMAT_RAW); + msg.setName(context.nvRamSpec.getName()); + msg.setVolumeType(VolumeType.NvRam.toString()); + + bus.makeLocalServiceId(msg, VolumeConstant.SERVICE_ID); + bus.send(msg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply reply) { + if (reply.isSuccess()) { + CreateVolumeReply castReply = reply.castReply(); + context.inventory = castReply.getInventory(); + trigger.next(); + return; + } + trigger.fail(operr("failed to create NvRam volume") + .withOpaque("vm.uuid", context.vmUuid) + .withCause(reply.getError())); + } + }); + } + + @Override + public void rollback(FlowRollback trigger, Map data) { + deleteNvRamVolumeIfExists(context.vmUuid, new Completion(trigger) { + @Override + public void success() { + trigger.rollback(); + } + + @Override + public void fail(ErrorCode errorCode) { + logger.warn("failed to delete NvRam but still continue: " + errorCode.getReadableDetails()); + trigger.rollback(); + } + }); + } + }).then(new NoRollbackFlow() { + String __name__ = "instantiate-nvram-volume"; + + @Override + public void run(FlowTrigger trigger, Map data) { + InstantiateVolumeMsg msg = new InstantiateVolumeMsg(); + msg.setHostUuid(context.spec.getDestHost().getUuid()); + msg.setPrimaryStorageUuid(context.nvRamSpec.getPrimaryStorageUuid()); + msg.setVolumeUuid(context.inventory.getUuid()); + + bus.makeTargetServiceIdByResourceUuid(msg, VolumeConstant.SERVICE_ID, msg.getVolumeUuid()); + bus.send(msg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply reply) { + if (reply.isSuccess()) { + trigger.next(); + return; + } + trigger.fail(operr("failed to instantiate NvRam volume") + .withOpaque("vm.uuid", context.vmUuid) + .withCause(reply.getError())); + } + }); + } + }).done(new FlowDoneHandler(completion) { + @Override + public void handle(Map data) { + completion.success(); + } + }).error(new FlowErrorHandler(completion) { + @Override + public void handle(ErrorCode errCode, Map data) { + completion.fail(errCode); + } + }).start(); + } + + private void deleteNvRamVolumeIfExists(String vmUuid, Completion completion) { + String volumeUuid = Q.New(VolumeVO.class) + .eq(VolumeVO_.vmInstanceUuid, vmUuid) + .eq(VolumeVO_.type, VolumeType.NvRam) + .select(VolumeVO_.uuid) + .findValue(); + if (volumeUuid == null) { + completion.success(); + return; + } + + DeleteVolumeMsg msg = new DeleteVolumeMsg(); + msg.setDetachBeforeDeleting(false); + msg.setUuid(volumeUuid); + msg.setDeletionPolicy(VolumeDeletionPolicyManager.VolumeDeletionPolicy.Direct.toString()); + bus.makeTargetServiceIdByResourceUuid(msg, VolumeConstant.SERVICE_ID, volumeUuid); + bus.send(msg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply reply) { + if (reply.isSuccess()) { + completion.success(); + return; + } + completion.fail(operr("failed to delete NvRam volume") + .withOpaque("vm.uuid", vmUuid) + .withOpaque("volume.uuid", volumeUuid) + .withCause(reply.getError())); + } + }); } } diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java index ec0b51711ba..8f0c75e9202 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java @@ -1052,6 +1052,10 @@ public String makeDataVolumeInstallUrl(String volUuid) { return PathUtil.join(self.getUrl(), PrimaryStoragePathMaker.makeDataVolumeInstallPath(volUuid)); } + public String makeNvRamVolumeInstallUrl(String volUuid) { + return PathUtil.join(self.getUrl(), PrimaryStoragePathMaker.makeNvRamVolumeInstallPath(volUuid)); + } + public boolean isCachedImageUrl(String path){ return path.startsWith(PathUtil.join(self.getUrl(), PrimaryStoragePathMaker.getCachedImageInstallDir())); } @@ -1380,6 +1384,8 @@ public void createEmptyVolumeWithBackingFile(final VolumeInventory volume, final cmd.setInstallUrl(makeMemoryVolumeInstallUrl(volume)); } else if (VolumeType.Cache.toString().equals(volume.getType())) { cmd.setInstallUrl(makeDataVolumeInstallUrl(volume.getUuid())); + } else if (VolumeType.NvRam.toString().equals(volume.getType())) { + cmd.setInstallUrl(makeNvRamVolumeInstallUrl(volume.getUuid())); } } cmd.setName(volume.getName()); diff --git a/storage/src/main/java/org/zstack/storage/primary/PrimaryStoragePathMaker.java b/storage/src/main/java/org/zstack/storage/primary/PrimaryStoragePathMaker.java index 7ef4b4dcd24..2458ee91d60 100755 --- a/storage/src/main/java/org/zstack/storage/primary/PrimaryStoragePathMaker.java +++ b/storage/src/main/java/org/zstack/storage/primary/PrimaryStoragePathMaker.java @@ -43,6 +43,10 @@ public static String makeDataVolumeInstallPath(String volUuid) { return PathUtil.join("dataVolumes", "acct-" + getAccountUuidOfResource(volUuid), "vol-" + volUuid, volUuid + ".qcow2"); } + public static String makeNvRamVolumeInstallPath(String volUuid) { + return PathUtil.join("nvRam", "acct-" + getAccountUuidOfResource(volUuid), "vol-" + volUuid, volUuid + ".raw"); + } + public static String makeImageFromSnapshotWorkspacePath(String imageUuid) { return PathUtil.join("snapshotWorkspace", String.format("image-%s", imageUuid)); } diff --git a/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java b/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java index 2e87e6b41e8..353e8233504 100755 --- a/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java +++ b/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java @@ -817,7 +817,8 @@ public void fail(ErrorCode errorCode) { } else if (msg instanceof InstantiateMemoryVolumeMsg) { instantiateMemoryVolume(msg, trigger); } else { - instantiateDataVolume(msg, trigger); + // include: data volume, NvRam, TpmState + instantiateOtherVolume(msg, trigger); } } } @@ -859,7 +860,7 @@ private void instantiateRootVolume(InstantiateRootVolumeMsg msg, FlowTrigger tri doInstantiateVolume(imsg, trigger); } - private void instantiateDataVolume(InstantiateVolumeMsg msg, FlowTrigger trigger) { + private void instantiateOtherVolume(InstantiateVolumeMsg msg, FlowTrigger trigger) { InstantiateVolumeOnPrimaryStorageMsg imsg = new InstantiateVolumeOnPrimaryStorageMsg(); prepareMsg(msg, imsg); doInstantiateVolume(imsg, trigger); diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index 5a446206770..27233b51680 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -283,6 +283,7 @@ + From 84b4024b95af138771d56941ddae2a762ec3c756 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 13 Feb 2026 18:28:55 +0800 Subject: [PATCH 010/122] [localstorage]: support create empty volume in raw types Libvirt 8.0.0 only support raw type NVRAW (ext is '.fd'). Resolves: ZSV-11310 Related: ZSPHER-1 Change-Id: I6c65797078616c706d6c716c796b63737a716375 --- .../ceph/primary/CephPrimaryStorageBase.java | 13 +++++++++++- .../primary/local/LocalStorageKvmBackend.java | 21 +++++++++++++++++-- .../nfs/NfsPrimaryStorageKVMBackend.java | 5 ++++- .../NfsPrimaryStorageKVMBackendCommands.java | 9 +++++++- .../nfs/NfsPrimaryStorageKvmHelper.java | 4 ++++ .../storage/primary/smp/KvmBackend.java | 10 ++++++++- 6 files changed, 56 insertions(+), 6 deletions(-) diff --git a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java index 53816200465..551762765c7 100755 --- a/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java +++ b/plugin/ceph/src/main/java/org/zstack/storage/ceph/primary/CephPrimaryStorageBase.java @@ -1763,7 +1763,18 @@ private void createEmptyVolume(final InstantiateVolumeOnPrimaryStorageMsg msg) { cmd.size = msg.getVolume().getSize(); cmd.setShareable(msg.getVolume().isShareable()); cmd.skipIfExisting = msg.isSkipIfExisting(); - cmd.format = msg.hasSystemTag(VolumeSystemTags.FORMAT_QCOW2.getTagFormat()) ? VolumeConstant.VOLUME_FORMAT_QCOW2 : VolumeConstant.VOLUME_FORMAT_RAW ; + + VolumeType type = Q.New(VolumeVO.class) + .eq(VolumeVO_.uuid, volumeUuid) + .select(VolumeVO_.type) + .findValue(); + if (type == VolumeType.NvRam) { + cmd.format = VolumeConstant.VOLUME_FORMAT_RAW; + } else { + cmd.format = msg.hasSystemTag(VolumeSystemTags.FORMAT_QCOW2.getTagFormat()) ? + VolumeConstant.VOLUME_FORMAT_QCOW2 : + VolumeConstant.VOLUME_FORMAT_RAW ; + } final InstantiateVolumeOnPrimaryStorageReply reply = new InstantiateVolumeOnPrimaryStorageReply(); diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java index 8f0c75e9202..afe7dce2423 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java @@ -35,6 +35,7 @@ import org.zstack.header.exception.CloudRuntimeException; import org.zstack.header.host.*; import org.zstack.header.image.ImageBackupStorageRefInventory; +import org.zstack.header.image.ImageConstant; import org.zstack.header.image.ImageConstant.ImageMediaType; import org.zstack.header.image.ImageInventory; import org.zstack.header.image.ImageStatus; @@ -216,6 +217,7 @@ public static class CreateEmptyVolumeCmd extends AgentCommand { private String volumeUuid; @GrayVersion(value = "5.0.0") private String backingFile; + private String format; public String getBackingFile() { return backingFile; @@ -225,6 +227,14 @@ public void setBackingFile(String backingFile) { this.backingFile = backingFile; } + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } + public String getInstallUrl() { return installUrl; } @@ -1350,7 +1360,7 @@ public void success(VolumeStats returnValue) { VolumeInventory vol = msg.getVolume(); vol.setInstallPath(returnValue.getInstallPath()); vol.setActualSize(returnValue.getActualSize()); - vol.setFormat(VolumeConstant.VOLUME_FORMAT_QCOW2); + vol.setFormat(returnValue.getFormat()); if (returnValue.getSize() != null) { vol.setSize(returnValue.getSize()); } @@ -1375,7 +1385,11 @@ public void createEmptyVolumeWithBackingFile(final VolumeInventory volume, final cmd.setAccountUuid(acntMgr.getOwnerAccountUuidOfResource(volume.getUuid())); if (volume.getInstallPath() != null && !volume.getInstallPath().equals("")) { cmd.setInstallUrl(volume.getInstallPath()); + cmd.setFormat(VolumeType.NvRam.toString().equals(volume.getType()) + ? ImageConstant.RAW_FORMAT_STRING + : ImageConstant.QCOW2_FORMAT_STRING); } else { + cmd.setFormat(ImageConstant.QCOW2_FORMAT_STRING); if (VolumeType.Root.toString().equals(volume.getType())) { cmd.setInstallUrl(makeRootVolumeInstallUrl(volume)); } else if (VolumeType.Data.toString().equals(volume.getType())) { @@ -1386,6 +1400,7 @@ public void createEmptyVolumeWithBackingFile(final VolumeInventory volume, final cmd.setInstallUrl(makeDataVolumeInstallUrl(volume.getUuid())); } else if (VolumeType.NvRam.toString().equals(volume.getType())) { cmd.setInstallUrl(makeNvRamVolumeInstallUrl(volume.getUuid())); + cmd.setFormat(ImageConstant.RAW_FORMAT_STRING); } } cmd.setName(volume.getName()); @@ -1396,7 +1411,9 @@ public void createEmptyVolumeWithBackingFile(final VolumeInventory volume, final httpCall(CREATE_EMPTY_VOLUME_PATH, hostUuid, cmd, CreateEmptyVolumeRsp.class, new ReturnValueCompletion(completion) { @Override public void success(CreateEmptyVolumeRsp returnValue) { - completion.success(new VolumeStats(cmd.getInstallUrl(), returnValue.actualSize, returnValue.size)); + final VolumeStats stats = new VolumeStats(cmd.getInstallUrl(), returnValue.actualSize, returnValue.size); + stats.setFormat(cmd.getFormat()); + completion.success(stats); } @Override diff --git a/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java index a1341ef4449..4689d57742c 100755 --- a/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java +++ b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java @@ -1118,6 +1118,9 @@ public void instantiateVolume(final PrimaryStorageInventory pinv, HostInventory cmd.setInstallUrl(NfsPrimaryStorageKvmHelper.makeDataVolumeInstallUrl(pinv, volume.getUuid())); } else if (volume.getType().equals(VolumeType.Cache.toString())) { cmd.setInstallUrl(NfsPrimaryStorageKvmHelper.makeDataVolumeInstallUrl(pinv, volume.getUuid())); + } else if (volume.getType().equals(VolumeType.NvRam.toString())) { + cmd.setInstallUrl(NfsPrimaryStorageKvmHelper.makeNvRamVolumeInstallUrl(pinv, volume.getUuid())); + cmd.setVolumeFormat(VolumeConstant.VOLUME_FORMAT_RAW); } else { throw new CloudRuntimeException(String.format("unknown volume type %s", volume.getType())); } @@ -1150,7 +1153,7 @@ public void run(MessageReply reply) { } volume.setInstallPath(cmd.getInstallUrl()); - volume.setFormat(VolumeConstant.VOLUME_FORMAT_QCOW2); + volume.setFormat(cmd.getVolumeFormat()); volume.setActualSize(rsp.actualSize); if (rsp.size != null) { volume.setSize(rsp.size); diff --git a/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackendCommands.java b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackendCommands.java index 1a31b5b6b7e..41e75e2b79a 100755 --- a/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackendCommands.java +++ b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackendCommands.java @@ -3,6 +3,7 @@ import org.zstack.core.upgrade.GrayVersion; import org.zstack.header.HasThreadContext; import org.zstack.header.core.validation.Validation; +import org.zstack.header.volume.VolumeConstant; import org.zstack.kvm.KVMAgentCommands; import org.zstack.kvm.KVMAgentCommands.AgentCommand; import org.zstack.kvm.KVMAgentCommands.AgentResponse; @@ -373,6 +374,7 @@ public abstract static class CreateVolumeCmd extends NfsPrimaryStorageAgentComma private String name; @GrayVersion(value = "5.0.0") private String volumeUuid; + private String volumeFormat = VolumeConstant.VOLUME_FORMAT_QCOW2; public String getInstallUrl() { return installUrl; @@ -404,7 +406,12 @@ public String getVolumeUuid() { public void setVolumeUuid(String uuid) { this.volumeUuid = uuid; } - + public String getVolumeFormat() { + return volumeFormat; + } + public void setVolumeFormat(String volumeFormat) { + this.volumeFormat = volumeFormat; + } } public static class CreateRootVolumeFromTemplateCmd extends CreateVolumeCmd { diff --git a/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKvmHelper.java b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKvmHelper.java index a58456a47db..5de2022ebc1 100755 --- a/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKvmHelper.java +++ b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKvmHelper.java @@ -44,6 +44,10 @@ public static String makeCachedImageInstallUrl(PrimaryStorageInventory pinv, Ima return ImageCacheUtil.getImageCachePath(iminv, it -> PathUtil.join(pinv.getMountPath(), PrimaryStoragePathMaker.makeCachedImageInstallPath(iminv))); } + public static String makeNvRamVolumeInstallUrl(PrimaryStorageInventory pinv, String volUuid) { + return PathUtil.join(pinv.getMountPath(), PrimaryStoragePathMaker.makeNvRamVolumeInstallPath(volUuid)); + } + public static String getCachedImageDir(PrimaryStorageInventory pinv){ return PathUtil.join(pinv.getMountPath(), PrimaryStoragePathMaker.getCachedImageInstallDir()); } diff --git a/plugin/sharedMountPointPrimaryStorage/src/main/java/org/zstack/storage/primary/smp/KvmBackend.java b/plugin/sharedMountPointPrimaryStorage/src/main/java/org/zstack/storage/primary/smp/KvmBackend.java index 97a16810f69..d5a615a6e47 100755 --- a/plugin/sharedMountPointPrimaryStorage/src/main/java/org/zstack/storage/primary/smp/KvmBackend.java +++ b/plugin/sharedMountPointPrimaryStorage/src/main/java/org/zstack/storage/primary/smp/KvmBackend.java @@ -381,6 +381,7 @@ public static class CreateEmptyVolumeCmd extends AgentCmd { @GrayVersion(value = "5.0.0") public String volumeUuid; public String backingFile; + public String volumeFormat = VolumeConstant.VOLUME_FORMAT_QCOW2; } public static class CreateEmptyVolumeRsp extends AgentRsp { @@ -692,6 +693,10 @@ public String makeCachedImageInstallUrl(ImageInventory iminv) { return ImageCacheUtil.getImageCachePath(iminv, it -> PathUtil.join(self.getMountPath(), PrimaryStoragePathMaker.makeCachedImageInstallPath(iminv))); } + public String makeNvRamVolumeInstallUrl(String volUuid) { + return PathUtil.join(self.getMountPath(), PrimaryStoragePathMaker.makeNvRamVolumeInstallPath(volUuid)); + } + public String makeCachedImageInstallUrlFromImageUuidForTemplate(String imageUuid) { return PathUtil.join(self.getMountPath(), PrimaryStoragePathMaker.makeCachedImageInstallPathFromImageUuidForTemplate(imageUuid)); } @@ -1051,6 +1056,9 @@ private void createEmptyVolume(final VolumeInventory volume, String hostUuid, fi cmd.installPath = makeRootVolumeInstallUrl(volume); } else if (VolumeType.Data.toString().equals(volume.getType())) { cmd.installPath = makeDataVolumeInstallUrl(volume.getUuid()); + } else if (VolumeType.NvRam.toString().equals(volume.getType())) { + cmd.installPath = makeNvRamVolumeInstallUrl(volume.getUuid()); + cmd.volumeFormat = VolumeConstant.VOLUME_FORMAT_RAW; } else { DebugUtils.Assert(false, "Should not be here"); } @@ -1064,7 +1072,7 @@ public void success(AgentRsp returnValue) { CreateEmptyVolumeRsp rsp = (CreateEmptyVolumeRsp) returnValue; InstantiateVolumeOnPrimaryStorageReply reply = new InstantiateVolumeOnPrimaryStorageReply(); volume.setInstallPath(cmd.installPath); - volume.setFormat(VolumeConstant.VOLUME_FORMAT_QCOW2); + volume.setFormat(cmd.volumeFormat); volume.setActualSize(rsp.actualSize); if (rsp.size != null) { volume.setSize(rsp.size); From 3821c6d42092c565ae894e6b99b1092e5e5fc6ce Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 26 Feb 2026 11:13:35 +0800 Subject: [PATCH 011/122] [localstorage]: remame CreateEmptyVolumeCmd.format to volumeFormat Related: ZSV-11310 Change-Id: I666370616e627a616378676f796971657662726f --- .../primary/local/LocalStorageKvmBackend.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java index afe7dce2423..4d350ddca2d 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java @@ -217,7 +217,7 @@ public static class CreateEmptyVolumeCmd extends AgentCommand { private String volumeUuid; @GrayVersion(value = "5.0.0") private String backingFile; - private String format; + private String volumeFormat; public String getBackingFile() { return backingFile; @@ -227,12 +227,12 @@ public void setBackingFile(String backingFile) { this.backingFile = backingFile; } - public String getFormat() { - return format; + public String getVolumeFormat() { + return volumeFormat; } - public void setFormat(String format) { - this.format = format; + public void setVolumeFormat(String volumeFormat) { + this.volumeFormat = volumeFormat; } public String getInstallUrl() { @@ -1385,11 +1385,11 @@ public void createEmptyVolumeWithBackingFile(final VolumeInventory volume, final cmd.setAccountUuid(acntMgr.getOwnerAccountUuidOfResource(volume.getUuid())); if (volume.getInstallPath() != null && !volume.getInstallPath().equals("")) { cmd.setInstallUrl(volume.getInstallPath()); - cmd.setFormat(VolumeType.NvRam.toString().equals(volume.getType()) + cmd.setVolumeFormat(VolumeType.NvRam.toString().equals(volume.getType()) ? ImageConstant.RAW_FORMAT_STRING : ImageConstant.QCOW2_FORMAT_STRING); } else { - cmd.setFormat(ImageConstant.QCOW2_FORMAT_STRING); + cmd.setVolumeFormat(ImageConstant.QCOW2_FORMAT_STRING); if (VolumeType.Root.toString().equals(volume.getType())) { cmd.setInstallUrl(makeRootVolumeInstallUrl(volume)); } else if (VolumeType.Data.toString().equals(volume.getType())) { @@ -1400,7 +1400,7 @@ public void createEmptyVolumeWithBackingFile(final VolumeInventory volume, final cmd.setInstallUrl(makeDataVolumeInstallUrl(volume.getUuid())); } else if (VolumeType.NvRam.toString().equals(volume.getType())) { cmd.setInstallUrl(makeNvRamVolumeInstallUrl(volume.getUuid())); - cmd.setFormat(ImageConstant.RAW_FORMAT_STRING); + cmd.setVolumeFormat(ImageConstant.RAW_FORMAT_STRING); } } cmd.setName(volume.getName()); @@ -1412,7 +1412,7 @@ public void createEmptyVolumeWithBackingFile(final VolumeInventory volume, final @Override public void success(CreateEmptyVolumeRsp returnValue) { final VolumeStats stats = new VolumeStats(cmd.getInstallUrl(), returnValue.actualSize, returnValue.size); - stats.setFormat(cmd.getFormat()); + stats.setFormat(cmd.getVolumeFormat()); completion.success(stats); } From 620453315b00de6bfea29a72b942bedf1ae0fc3f Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 3 Mar 2026 14:34:39 +0800 Subject: [PATCH 012/122] [header]: introduce VmHostFileVO * introduces a management infrastructure for VM-host file (ex: NvRam and TPM state), encompassing a data model KVM agent communication protocol, persistent storage and integration with Secure Boot and TPM extensions. * New global configuration properties have been adde to control the enablement behavior of NvRam volumes. Resolves: ZSV-11310 Change-Id: I716c766875616b79736c69666d7863767276706c --- .../legacy/ComputeLegacyGlobalProperty.java | 10 + .../compute/vm/devices/VmTpmExtensions.java | 41 +- .../compute/vm/devices/VmTpmManager.java | 23 - conf/persistence.xml | 2 + conf/springConfigXml/Kvm.xml | 7 + .../vm/additions/VmHostFileContentFormat.java | 6 + .../vm/additions/VmHostFileContentVO.java | 93 ++++ .../header/vm/additions/VmHostFileType.java | 8 + .../header/vm/additions/VmHostFileVO.java | 107 +++++ .../java/org/zstack/kvm/KVMAgentCommands.java | 106 +++++ .../main/java/org/zstack/kvm/KVMConstant.java | 11 + .../src/main/java/org/zstack/kvm/KVMHost.java | 10 + .../kvm/efi/KvmSecureBootExtensions.java | 412 +++++++++++++++++- .../zstack/kvm/efi/KvmSecureBootManager.java | 104 +++++ .../org/zstack/kvm/tpm/KvmTpmExtensions.java | 97 ++++- .../main/java/org/zstack/kvm/tpm/TpmTO.java | 9 + .../test/resources/springConfigXml/Kvm.xml | 7 + .../org/zstack/testlib/KVMSimulator.groovy | 20 + .../org/zstack/utils/CollectionUtils.java | 2 + 19 files changed, 1033 insertions(+), 42 deletions(-) create mode 100644 compute/src/main/java/org/zstack/compute/legacy/ComputeLegacyGlobalProperty.java create mode 100644 header/src/main/java/org/zstack/header/vm/additions/VmHostFileContentFormat.java create mode 100644 header/src/main/java/org/zstack/header/vm/additions/VmHostFileContentVO.java create mode 100644 header/src/main/java/org/zstack/header/vm/additions/VmHostFileType.java create mode 100644 header/src/main/java/org/zstack/header/vm/additions/VmHostFileVO.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootManager.java diff --git a/compute/src/main/java/org/zstack/compute/legacy/ComputeLegacyGlobalProperty.java b/compute/src/main/java/org/zstack/compute/legacy/ComputeLegacyGlobalProperty.java new file mode 100644 index 00000000000..b1e4b91caf1 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/legacy/ComputeLegacyGlobalProperty.java @@ -0,0 +1,10 @@ +package org.zstack.compute.legacy; + +import org.zstack.core.GlobalProperty; +import org.zstack.core.GlobalPropertyDefinition; + +@GlobalPropertyDefinition +public class ComputeLegacyGlobalProperty { + @GlobalProperty(name="enable.nv.ram.type.volume", defaultValue = "false") + public static boolean enableNvRamTypeVolume; +} diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmExtensions.java b/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmExtensions.java index 6877b2680fc..39ce6fcb840 100644 --- a/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmExtensions.java +++ b/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmExtensions.java @@ -2,19 +2,29 @@ import org.springframework.beans.factory.annotation.Autowired; import org.zstack.compute.vm.BuildVmSpecExtensionPoint; +import org.zstack.compute.vm.VmSystemTags; +import org.zstack.core.db.Q; +import org.zstack.header.tpm.entity.TpmSpec; +import org.zstack.header.tpm.entity.TpmVO; +import org.zstack.header.tpm.entity.TpmVO_; import org.zstack.header.vm.CreateVmInstanceMsg; import org.zstack.header.vm.DiskAO; import org.zstack.header.vm.VmInstanceCreateExtensionPoint; import org.zstack.header.vm.VmInstanceSpec; import org.zstack.header.vm.VmInstanceVO; import org.zstack.header.vm.devices.VmDevicesSpec; +import org.zstack.resourceconfig.ResourceConfig; +import org.zstack.resourceconfig.ResourceConfigFacade; +import static org.zstack.compute.vm.VmGlobalConfig.ENABLE_UEFI_SECURE_BOOT; import static org.zstack.header.vm.VmInstanceConstant.NV_RAM_DEFAULT_SIZE; public class VmTpmExtensions implements VmInstanceCreateExtensionPoint, BuildVmSpecExtensionPoint { @Autowired private VmTpmManager vmTpmManager; + @Autowired + private ResourceConfigFacade resourceConfigFacade; @Override public void preCreateVmInstance(CreateVmInstanceMsg msg) { @@ -34,13 +44,32 @@ public void afterPersistVmInstanceVO(VmInstanceVO vo, CreateVmInstanceMsg msg) { @Override public void afterBuildVmSpec(VmInstanceSpec spec) { String vmUuid = spec.getVmInventory().getUuid(); - if (!vmTpmManager.needRegisterNvRam(vmUuid)) { - return; + + boolean tpmExists = Q.New(TpmVO.class) + .eq(TpmVO_.vmInstanceUuid, vmUuid) + .isExists(); + boolean needRegisterNvRam = tpmExists; + if (!needRegisterNvRam) { + String bootMode = VmSystemTags.BOOT_MODE.getTokenByResourceUuid(vmUuid, VmSystemTags.BOOT_MODE_TOKEN); + if (vmTpmManager.isUefiBootMode(bootMode)) { + ResourceConfig resourceConfig = resourceConfigFacade.getResourceConfig(ENABLE_UEFI_SECURE_BOOT.getIdentity()); + needRegisterNvRam = resourceConfig.getResourceConfigValue(vmUuid, Boolean.class) == Boolean.TRUE; + } } - DiskAO nvRamSpec = new DiskAO(); - nvRamSpec.setSize(NV_RAM_DEFAULT_SIZE); - nvRamSpec.setName("NvRam-of-VM-" + vmUuid); - spec.setNvRamSpec(nvRamSpec); + if (needRegisterNvRam) { + DiskAO nvRamSpec = new DiskAO(); + nvRamSpec.setSize(NV_RAM_DEFAULT_SIZE); + nvRamSpec.setName("NvRam-of-VM-" + vmUuid); + spec.setNvRamSpec(nvRamSpec); + } + + if (tpmExists && (spec.getDevicesSpec() == null || spec.getDevicesSpec().getTpm() == null)) { + VmDevicesSpec devicesSpec = spec.getDevicesSpec() == null ? new VmDevicesSpec() : spec.getDevicesSpec(); + spec.setDevicesSpec(devicesSpec); + + devicesSpec.setTpm(new TpmSpec()); + devicesSpec.getTpm().setEnable(true); + } } } diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmManager.java b/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmManager.java index ced08da2918..19099f290a7 100644 --- a/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmManager.java +++ b/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmManager.java @@ -1,22 +1,16 @@ package org.zstack.compute.vm.devices; import org.springframework.beans.factory.annotation.Autowired; -import org.zstack.compute.vm.VmSystemTags; import org.zstack.core.Platform; import org.zstack.core.db.DatabaseFacade; -import org.zstack.core.db.Q; import org.zstack.header.image.ImageBootMode; import org.zstack.header.tpm.entity.TpmVO; -import org.zstack.header.tpm.entity.TpmVO_; -import org.zstack.resourceconfig.ResourceConfig; import org.zstack.resourceconfig.ResourceConfigFacade; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; import java.util.Objects; -import static org.zstack.compute.vm.VmGlobalConfig.ENABLE_UEFI_SECURE_BOOT; - public class VmTpmManager { private static final CLogger logger = Utils.getLogger(VmTpmManager.class); @@ -39,23 +33,6 @@ public TpmVO persistTpmVO(String tpmUuid, String vmUuid) { return tpm; } - public boolean needRegisterNvRam(String vmUuid) { - boolean tpmExists = Q.New(TpmVO.class) - .eq(TpmVO_.vmInstanceUuid, vmUuid) - .isExists(); - if (tpmExists) { - return true; - } - - String bootMode = VmSystemTags.BOOT_MODE.getTokenByResourceUuid(vmUuid, VmSystemTags.BOOT_MODE_TOKEN); - if (!isUefiBootMode(bootMode)) { - return false; - } - - ResourceConfig resourceConfig = resourceConfigFacade.getResourceConfig(ENABLE_UEFI_SECURE_BOOT.getIdentity()); - return resourceConfig.getResourceConfigValue(vmUuid, Boolean.class) == Boolean.TRUE; - } - /** * @param bootMode boot mode, null is Legacy */ diff --git a/conf/persistence.xml b/conf/persistence.xml index f7e4208b782..4c68460e7d3 100755 --- a/conf/persistence.xml +++ b/conf/persistence.xml @@ -20,6 +20,8 @@ org.zstack.header.managementnode.ManagementNodeContextVO org.zstack.header.tpm.entity.TpmHostRefVO org.zstack.header.tpm.entity.TpmVO + org.zstack.header.vm.additions.VmHostFileVO + org.zstack.header.vm.additions.VmHostFileContentVO org.zstack.header.zone.ZoneVO org.zstack.header.zone.ZoneEO org.zstack.header.cluster.ClusterVO diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index 65f50e6749c..cbb6448a379 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -272,6 +272,13 @@ + + + + + + + diff --git a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileContentFormat.java b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileContentFormat.java new file mode 100644 index 00000000000..1212dc054bc --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileContentFormat.java @@ -0,0 +1,6 @@ +package org.zstack.header.vm.additions; + +public enum VmHostFileContentFormat { + Raw, + TarballGzip, +} diff --git a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileContentVO.java b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileContentVO.java new file mode 100644 index 00000000000..4995d5b0f38 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileContentVO.java @@ -0,0 +1,93 @@ +package org.zstack.header.vm.additions; + +import org.zstack.header.vo.EntityGraph; +import org.zstack.header.vo.ForeignKey; +import org.zstack.header.vo.SoftDeletionCascade; +import org.zstack.header.vo.SoftDeletionCascades; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Id; +import javax.persistence.Table; +import java.sql.Timestamp; + +/** + * Virtual Machine Host-side File Content Value Object + */ +@Entity +@Table +@SoftDeletionCascades({ + @SoftDeletionCascade(parent = VmHostFileVO.class, joinColumn = "uuid"), +}) +@EntityGraph( + friends = { + @EntityGraph.Neighbour(type = VmHostFileVO.class, myField = "uuid", targetField = "uuid"), + } +) +public class VmHostFileContentVO { + @Id + @Column + @ForeignKey(parentEntityClass = VmHostFileVO.class, onDeleteAction = ForeignKey.ReferenceOption.CASCADE) + private String uuid; + @Column + private byte[] content; + @Column + @Enumerated(EnumType.STRING) + private VmHostFileContentFormat format; + @Column + private Timestamp createDate; + @Column + private Timestamp lastOpDate; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public byte[] getContent() { + return content; + } + + public void setContent(byte[] content) { + this.content = content; + } + + public VmHostFileContentFormat getFormat() { + return format; + } + + public void setFormat(VmHostFileContentFormat format) { + this.format = format; + } + + public Timestamp getCreateDate() { + return createDate; + } + + public void setCreateDate(Timestamp createDate) { + this.createDate = createDate; + } + + public Timestamp getLastOpDate() { + return lastOpDate; + } + + public void setLastOpDate(Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + + @Override + public String toString() { + return "VmHostFileContentVO{" + + "uuid='" + uuid + '\'' + + ", format=" + format + + ", createDate=" + createDate + + ", lastOpDate=" + lastOpDate + + '}'; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileType.java b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileType.java new file mode 100644 index 00000000000..16c493e6768 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileType.java @@ -0,0 +1,8 @@ +package org.zstack.header.vm.additions; + +public enum VmHostFileType { + NvRam, + TpmState, + NvRamBackup, + TpmStateBackup, +} diff --git a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileVO.java b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileVO.java new file mode 100644 index 00000000000..c0691ad5dac --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileVO.java @@ -0,0 +1,107 @@ +package org.zstack.header.vm.additions; + +import org.zstack.header.host.HostEO; +import org.zstack.header.host.HostVO; +import org.zstack.header.vm.VmInstanceEO; +import org.zstack.header.vo.EntityGraph; +import org.zstack.header.vo.ForeignKey; +import org.zstack.header.vo.ResourceVO; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.Table; +import java.sql.Timestamp; + +/** + * Virtual Machine Host-side File Value Object + * + * Include: NvRam / TpmState files + */ +@Entity +@Table +@EntityGraph( + friends = { + @EntityGraph.Neighbour(type = VmInstanceEO.class, myField = "vmInstanceUuid", targetField = "uuid"), + @EntityGraph.Neighbour(type = HostVO.class, myField = "hostUuid", targetField = "uuid"), + } +) +public class VmHostFileVO extends ResourceVO { + @Column + @ForeignKey(parentEntityClass = VmInstanceEO.class, onDeleteAction = ForeignKey.ReferenceOption.CASCADE) + private String vmInstanceUuid; + @Column + @ForeignKey(parentEntityClass = HostEO.class, onDeleteAction = ForeignKey.ReferenceOption.CASCADE) + private String hostUuid; + @Column + @Enumerated(EnumType.STRING) + private VmHostFileType type; + @Column + private String path; + @Column + private Timestamp createDate; + @Column + private Timestamp lastOpDate; + + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + public VmHostFileType getType() { + return type; + } + + public void setType(VmHostFileType type) { + this.type = type; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public Timestamp getCreateDate() { + return createDate; + } + + public void setCreateDate(Timestamp createDate) { + this.createDate = createDate; + } + + public Timestamp getLastOpDate() { + return lastOpDate; + } + + public void setLastOpDate(Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + + @Override + public String toString() { + return "VmHostFileVO{" + + "uuid='" + uuid + '\'' + + ", vmInstanceUuid='" + vmInstanceUuid + '\'' + + ", hostUuid='" + hostUuid + '\'' + + ", type=" + type + + ", path='" + path + '\'' + + ", createDate=" + createDate + + ", lastOpDate=" + lastOpDate + + '}'; + } +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java index 562b42d82b3..0ac01a3f9cb 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -13,6 +13,7 @@ import org.zstack.header.host.VmNicRedirectConfig; import org.zstack.header.log.NoLogging; import org.zstack.header.vm.*; +import org.zstack.header.vm.additions.VmHostFileContentFormat; import org.zstack.header.vm.devices.DeviceAddress; import org.zstack.header.vm.devices.VirtualDeviceInfo; import org.zstack.kvm.tpm.TpmTO; @@ -24,6 +25,8 @@ import java.util.*; import java.util.stream.Collectors; +import static org.zstack.utils.CollectionUtils.transform; + public class KVMAgentCommands { public enum BootDev { hd(VmBootDevice.HardDisk), @@ -3110,6 +3113,63 @@ public void setHostMinimumFreeMemorySize(Long hostMinimumFreeMemorySize) { public static class StartVmResponse extends VmDevicesInfoResponse { } + public static class VmHostFileTO { + private String path; + /** + * maybe "NvRam" or "TpmState" ... + * @see org.zstack.header.vm.additions.VmHostFileType + */ + private String type; + /** + * maybe "Simple" or "TarballGzip" + * @see VmHostFileContentFormat + */ + private String fileFormat; + @NoLogging + private String contentBase64; + private String error; + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getFileFormat() { + return fileFormat; + } + + public void setFileFormat(String fileFormat) { + this.fileFormat = fileFormat; + } + + public String getContentBase64() { + return contentBase64; + } + + public void setContentBase64(String contentBase64) { + this.contentBase64 = contentBase64; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + } + public static class VmDevicesInfoResponse extends AgentResponse { @GrayVersion(value = "5.0.0") private List nicInfos; @@ -3198,6 +3258,52 @@ public void setVmInstanceUuid(String vmInstanceUuid) { public static class SyncVmDeviceInfoResponse extends VmDevicesInfoResponse { } + public static class ReadVmHostFileContentCmd extends AgentCommand { + /** + * without contentBase64, fileFormat + */ + private List hostFiles = new ArrayList<>(); + + public List getPaths() { + return transform(hostFiles, VmHostFileTO::getPath); + } + + public List getHostFiles() { + return hostFiles; + } + + public void setHostFiles(List hostFiles) { + this.hostFiles = hostFiles; + } + } + + public static class ReadVmHostFileContentResponse extends AgentResponse { + private List hostFiles = new ArrayList<>(); + + public List getHostFiles() { + return hostFiles; + } + + public void setHostFiles(List hostFiles) { + this.hostFiles = hostFiles; + } + } + + public static class WriteVmHostFileContentCmd extends AgentCommand { + private List hostFiles = new ArrayList<>(); + + public List getHostFiles() { + return hostFiles; + } + + public void setHostFiles(List hostFiles) { + this.hostFiles = hostFiles; + } + } + + public static class WriteVmHostFileContentResponse extends AgentResponse { + } + public static class VmNicInfo { private String macAddress; private DeviceAddress deviceAddress; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java index 18c201f94fd..0145cd02a26 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -99,6 +99,8 @@ public interface KVMConstant { String KVM_REGISTER_PRIMARY_VM_HEARTBEAT = "/register/primary/vm/heartbeat"; String CLEAN_FIRMWARE_FLASH = "/clean/firmware/flash"; String FSTRIM_VM_PATH = "/vm/fstrim"; + String READ_VM_HOST_FILE_PATH = "/vm/hostfile/read"; + String WRITE_VM_HOST_FILE_PATH = "/vm/hostfile/write"; // ZSTAC-83157: virtiofs model mount paths String KVM_VIRTIOFS_ATTACH_PATH = "/virtiofs/attach"; @@ -205,6 +207,15 @@ public interface KVMConstant { public static final String L2_PROVIDER_TYPE_LINUX_BRIDGE = "LinuxBridge"; public static final String EDK_VERSION_NONE = "None"; + public static final String NV_RAM_FILE_PATH_FORMAT = "/var/lib/libvirt/qemu/nvram/%s-host-files/%s.fd"; + public static String buildNvramFilePath(String vmUuid) { + return String.format(NV_RAM_FILE_PATH_FORMAT, vmUuid, vmUuid); + } + + public static final String TPM_STATE_FILE_PATH_FORMAT = "/var/lib/libvirt/swtpm/%s/"; + public static String buildTpmStateFilePath(String vmUuid) { + return String.format(TPM_STATE_FILE_PATH_FORMAT, vmUuid); + } public static final String DHCP_BIN_FILE_PATH = "/usr/local/zstack/dnsmasq"; String KVM_HOST_NETWORK_INTERFACE_DEFAULT = "None"; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index fc1881b7bac..dea8f55f006 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -313,6 +313,8 @@ public static Set parseSanIps(String sanOutput) { private String blockPullPath; private String agentPackageName = KVMGlobalProperty.AGENT_PACKAGE_NAME; private String hostTakeOverFlagPath = KVMGlobalProperty.TAKEVOERFLAGPATH; + private String readVmHostFilePath; + private String writeVmHostFilePath; public KVMHost(KVMHostVO self, KVMHostContext context) { super(self); @@ -543,6 +545,14 @@ public KVMHost(KVMHostVO self, KVMHostContext context) { ub = UriComponentsBuilder.fromHttpUrl(baseUrl); ub.path(KVMConstant.KVM_BLOCK_PULL_VOLUME_PATH); blockPullPath = ub.build().toString(); + + ub = UriComponentsBuilder.fromHttpUrl(baseUrl); + ub.path(KVMConstant.READ_VM_HOST_FILE_PATH); + readVmHostFilePath = ub.build().toString(); + + ub = UriComponentsBuilder.fromHttpUrl(baseUrl); + ub.path(KVMConstant.WRITE_VM_HOST_FILE_PATH); + writeVmHostFilePath = ub.build().toString(); } static { diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java index 4387b54bfbb..18f23708a3c 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java @@ -1,13 +1,18 @@ package org.zstack.kvm.efi; import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.legacy.ComputeLegacyGlobalProperty; import org.zstack.compute.vm.VmGlobalConfig; import org.zstack.compute.vm.devices.VmTpmManager; +import org.zstack.core.Platform; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.CloudBusCallBack; +import org.zstack.core.db.DatabaseFacade; import org.zstack.core.db.Q; +import org.zstack.core.db.SQL; import org.zstack.core.workflow.SimpleFlowChain; import org.zstack.header.core.Completion; +import org.zstack.header.core.ReturnValueCompletion; import org.zstack.header.core.workflow.Flow; import org.zstack.header.core.workflow.FlowDoneHandler; import org.zstack.header.core.workflow.FlowErrorHandler; @@ -23,6 +28,12 @@ import org.zstack.header.vm.PreVmInstantiateResourceExtensionPoint; import org.zstack.header.vm.VmInstanceSpec; import org.zstack.header.vm.VmInstantiateResourceException; +import org.zstack.header.vm.additions.VmHostFileContentFormat; +import org.zstack.header.vm.additions.VmHostFileContentVO; +import org.zstack.header.vm.additions.VmHostFileContentVO_; +import org.zstack.header.vm.additions.VmHostFileType; +import org.zstack.header.vm.additions.VmHostFileVO; +import org.zstack.header.vm.additions.VmHostFileVO_; import org.zstack.header.volume.CreateVolumeMsg; import org.zstack.header.volume.CreateVolumeReply; import org.zstack.header.volume.DeleteVolumeMsg; @@ -35,9 +46,12 @@ import org.zstack.header.volume.VolumeVO; import org.zstack.header.volume.VolumeVO_; import org.zstack.kvm.KVMAgentCommands; +import org.zstack.kvm.KVMAgentCommands.*; import org.zstack.kvm.KVMGlobalConfig; import org.zstack.kvm.KVMHostInventory; import org.zstack.kvm.KVMStartVmExtensionPoint; +import org.zstack.kvm.KvmCommandSender; +import org.zstack.kvm.KvmResponseWrapper; import org.zstack.kvm.VolumeTO; import org.zstack.resourceconfig.ResourceConfig; import org.zstack.resourceconfig.ResourceConfigFacade; @@ -45,11 +59,19 @@ import org.zstack.utils.logging.CLogger; import javax.persistence.Tuple; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Objects; import static org.zstack.core.Platform.operr; -import static org.zstack.kvm.KVMConstant.EDK_VERSION_NONE; +import static org.zstack.kvm.KVMConstant.*; +import static org.zstack.utils.CollectionUtils.findOneOrNull; +import static org.zstack.utils.CollectionUtils.transform; public class KvmSecureBootExtensions implements KVMStartVmExtensionPoint, PreVmInstantiateResourceExtensionPoint { @@ -59,6 +81,10 @@ public class KvmSecureBootExtensions implements KVMStartVmExtensionPoint, private CloudBus bus; @Autowired private ResourceConfigFacade resourceConfigFacade; + @Autowired + private DatabaseFacade databaseFacade; + + private final Object hostFileLock = new Object(); @Override public void beforeStartVmOnKvm(KVMHostInventory host, VmInstanceSpec spec, KVMAgentCommands.StartVmCmd cmd) { @@ -80,19 +106,204 @@ public void beforeStartVmOnKvm(KVMHostInventory host, VmInstanceSpec spec, KVMAg } private void prepareNvRamToStartVmCmd(KVMAgentCommands.StartVmCmd cmd, DiskAO nvRamSpec, KVMHostInventory host) { - VolumeVO vo = Q.New(VolumeVO.class) - .eq(VolumeVO_.uuid, nvRamSpec.getSourceUuid()) - .find(); - if (vo == null) { - if (nvRamSpec.getSourceUuid() != null) { - throw new CloudRuntimeException(String.format("cannot find NvRam volume[uuid:%s]", nvRamSpec.getSourceUuid())); + if (ComputeLegacyGlobalProperty.enableNvRamTypeVolume) { + VolumeVO vo = Q.New(VolumeVO.class) + .eq(VolumeVO_.uuid, nvRamSpec.getSourceUuid()) + .find(); + if (vo == null) { + if (nvRamSpec.getSourceUuid() != null) { + throw new CloudRuntimeException(String.format("cannot find NvRam volume[uuid:%s]", nvRamSpec.getSourceUuid())); + } + return; } + + VolumeInventory nvRamVolume = VolumeInventory.valueOf(vo); + VolumeTO volume = VolumeTO.valueOfWithOutExtension(nvRamVolume, host, null); + cmd.setNvRam(volume); return; } - VolumeInventory nvRamVolume = VolumeInventory.valueOf(vo); - VolumeTO volume = VolumeTO.valueOfWithOutExtension(nvRamVolume, host, null); + VolumeTO volume = new VolumeTO(); + volume.setDeviceType(VolumeTO.FILE); + volume.setInstallPath(buildNvramFilePath(cmd.getVmInstanceUuid())); + volume.setVolumeUuid(null); // not a volume cmd.setNvRam(volume); + + synchronized (hostFileLock) { + VmHostFileVO nvRamFile = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, cmd.getVmInstanceUuid()) + .eq(VmHostFileVO_.type, VmHostFileType.NvRam) + .eq(VmHostFileVO_.hostUuid, host.getUuid()) + .find(); + if (nvRamFile == null) { + nvRamFile = new VmHostFileVO(); + nvRamFile.setUuid(Platform.getUuid()); + nvRamFile.setHostUuid(host.getUuid()); + nvRamFile.setVmInstanceUuid(cmd.getVmInstanceUuid()); + nvRamFile.setType(VmHostFileType.NvRam); + nvRamFile.setPath(volume.getInstallPath()); + nvRamFile.setCreateDate(Timestamp.from(Instant.now())); + nvRamFile.setResourceName("NvRam file for " + cmd.getVmInstanceUuid()); + databaseFacade.persist(nvRamFile); + } else { + SQL.New(VmHostFileVO.class) + .eq(VmHostFileVO_.uuid, nvRamFile.getUuid()) + .set(VmHostFileVO_.path, volume.getInstallPath()) + .set(VmHostFileVO_.lastOpDate, Timestamp.from(Instant.now())) + .update(); + } + } + } + + public static class SyncVmHostFilesFromHostContext { + public String hostUuid; + public String vmUuid; + + public String nvRamPath; + public String tpmStateFolder; + } + + public void syncVmHostFilesFromHost(SyncVmHostFilesFromHostContext context, Completion completion) { + KvmCommandSender sender = new KvmCommandSender(context.hostUuid); + + ReadVmHostFileContentCmd cmd = new ReadVmHostFileContentCmd(); + cmd.setHostFiles(new ArrayList<>()); + if (context.tpmStateFolder != null) { + VmHostFileTO to = new VmHostFileTO(); + to.setPath(context.tpmStateFolder); + to.setType(VmHostFileType.TpmState.toString()); + cmd.getHostFiles().add(to); + } + if (context.nvRamPath != null) { + VmHostFileTO to = new VmHostFileTO(); + to.setPath(context.nvRamPath); + to.setType(VmHostFileType.NvRam.toString()); + cmd.getHostFiles().add(to); + } + + sender.send(cmd, READ_VM_HOST_FILE_PATH, wrapper -> { + ReadVmHostFileContentResponse readRsp = wrapper.getResponse(ReadVmHostFileContentResponse.class); + return readRsp.isSuccess() ? null : + operr("failed to read file content response").causedBy(readRsp.getError()); + }, new ReturnValueCompletion(completion) { + @Override + public void success(KvmResponseWrapper wrapper) { + ReadVmHostFileContentResponse readRsp = wrapper.getResponse(ReadVmHostFileContentResponse.class); + if (!readRsp.isSuccess()) { + completion.fail(operr("failed to read file content response").causedBy(readRsp.getError())); + return; + } + + final List existsFiles = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, context.vmUuid) + .eq(VmHostFileVO_.hostUuid, context.hostUuid) + .in(VmHostFileVO_.path, cmd.getPaths()) + .list(); + final List existsContentUuid; + if (!existsFiles.isEmpty()) { + existsContentUuid = Q.New(VmHostFileContentVO.class) + .in(VmHostFileContentVO_.uuid, transform(existsFiles, VmHostFileVO::getUuid)) + .select(VmHostFileContentVO_.uuid) + .listValues(); + } else { + existsContentUuid = Collections.emptyList(); + } + + for (String path : cmd.getPaths()) { + VmHostFileTO to = findOneOrNull(readRsp.getHostFiles(), item -> item.getPath().equals(path)); + if (to == null) { + continue; + } + if (to.getError() != null) { + logger.warn(String.format("failed to read file content from host[uuid=%s] with file %s: %s", + context.hostUuid, path, to.getError())); + continue; + } + + VmHostFileType type = Objects.equals(path, context.nvRamPath) ? + VmHostFileType.NvRam : VmHostFileType.TpmState; + + VmHostFileVO file = findOneOrNull(existsFiles, item -> item.getPath().equals(path)); + boolean fileExists = file != null; + + Timestamp now = Timestamp.from(Instant.now()); + if (fileExists) { + SQL.New(VmHostFileVO.class) + .eq(VmHostFileVO_.uuid, file.getUuid()) + .set(VmHostFileVO_.lastOpDate, now) + .update(); + } else { + file = new VmHostFileVO(); + file.setUuid(Platform.getUuid()); + file.setHostUuid(context.hostUuid); + file.setVmInstanceUuid(context.vmUuid); + file.setPath(path); + file.setType(type); + file.setCreateDate(now); + file.setLastOpDate(now); + file.setResourceName(String.format("%s file for %s", type, context.vmUuid)); + databaseFacade.persist(file); + } + + byte[] bytes = Base64.getDecoder().decode(to.getContentBase64()); + if (existsContentUuid.contains(file.getUuid())) { + SQL.New(VmHostFileContentVO.class) + .eq(VmHostFileContentVO_.uuid, file.getUuid()) + .set(VmHostFileContentVO_.content, bytes) + .set(VmHostFileContentVO_.format, VmHostFileContentFormat.valueOf(to.getFileFormat())) + .set(VmHostFileContentVO_.lastOpDate, now) + .update(); + } else { + VmHostFileContentVO content = new VmHostFileContentVO(); + content.setUuid(file.getUuid()); + content.setContent(bytes); + content.setFormat(VmHostFileContentFormat.valueOf(to.getFileFormat())); + content.setCreateDate(now); + content.setLastOpDate(now); + databaseFacade.persist(content); + } + } + + completion.success(); + } + + @Override + public void fail(ErrorCode errorCode) { + completion.fail(errorCode); + } + }); + } + + public static class RewriteVmHostFilesContext { + public String hostUuid; + public List hostFiles; + } + + public void rewriteVmHostFiles(RewriteVmHostFilesContext context, Completion completion) { + KvmCommandSender sender = new KvmCommandSender(context.hostUuid); + KVMAgentCommands.WriteVmHostFileContentCmd cmd = new KVMAgentCommands.WriteVmHostFileContentCmd(); + cmd.setHostFiles(context.hostFiles); + + sender.send(cmd, WRITE_VM_HOST_FILE_PATH, wrapper -> { + KVMAgentCommands.WriteVmHostFileContentResponse writeRsp = wrapper.getResponse(KVMAgentCommands.WriteVmHostFileContentResponse.class); + return writeRsp.isSuccess() ? null : + operr("failed to write file content response").causedBy(writeRsp.getError()); + }, new ReturnValueCompletion(completion) { + @Override + public void success(KvmResponseWrapper wrapper) { + KVMAgentCommands.WriteVmHostFileContentResponse writeRsp = wrapper.getResponse(KVMAgentCommands.WriteVmHostFileContentResponse.class); + if (!writeRsp.isSuccess()) { + completion.fail(operr("failed to write file content response").causedBy(writeRsp.getError())); + return; + } + completion.success(); + } + + @Override + public void fail(ErrorCode errorCode) { + completion.fail(errorCode); + } + }); } @Override @@ -116,6 +327,14 @@ public void preBeforeInstantiateVmResource(VmInstanceSpec spec) throws VmInstant @Override public void preInstantiateVmResource(VmInstanceSpec spec, Completion completion) { + if (ComputeLegacyGlobalProperty.enableNvRamTypeVolume) { + prepareNvRamVolumeOnHost(spec, completion); + } else { + prepareNvRamHostFileOnHost(spec, completion); + } + } + + private void prepareNvRamVolumeOnHost(VmInstanceSpec spec, Completion completion) { final DiskAO nvRamSpec = spec.getNvRamSpec(); boolean needRegisterNvRam = nvRamSpec != null; @@ -180,6 +399,175 @@ public void fail(ErrorCode errorCode) { }); } + public static class PrepareHostFileContext { + public String hostUuid; + public String vmUuid; + public VmHostFileType type; + + // whether the NvRam is on the same host as before + private boolean sameHost = false; + private VmHostFileVO vmHostFile; + } + + @SuppressWarnings("rawtypes") + public void prepareHostFileOnHost(PrepareHostFileContext context, Completion completion) { + SimpleFlowChain chain = new SimpleFlowChain(); + chain.setName("prepare-vm-host-file"); + chain.then(new NoRollbackFlow() { + String __name__ = "read-vm-host-file-from-origin-host"; + + @Override + public void run(FlowTrigger trigger, Map data) { + VmHostFileVO vmHostFile = context.vmHostFile = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.type, context.type) + .eq(VmHostFileVO_.vmInstanceUuid, context.vmUuid) + .orderByDesc(VmHostFileVO_.lastOpDate) + .limit(1) + .find(); + context.sameHost = vmHostFile != null && vmHostFile.getHostUuid().equals(context.hostUuid); + if (context.sameHost) { + logger.debug(String.format("skip to read/write %s host file for VM[vmUuid=%s]: vm.host is not changed", + context.type, context.vmUuid)); + trigger.next(); + return; + } + + if (vmHostFile == null) { + logger.debug(String.format("skip to read/write %s host file for VM[vmUuid=%s]: file is not registered in MN", + context.type, context.vmUuid)); + trigger.next(); + return; + } + + SyncVmHostFilesFromHostContext syncContext = new SyncVmHostFilesFromHostContext(); + syncContext.hostUuid = vmHostFile.getHostUuid(); + syncContext.vmUuid = context.vmUuid; + + if (vmHostFile.getType() == VmHostFileType.NvRam) { + syncContext.nvRamPath = vmHostFile.getPath(); + } else if (vmHostFile.getType() == VmHostFileType.TpmState) { + syncContext.tpmStateFolder = vmHostFile.getPath(); + } else { + throw new CloudRuntimeException("unsupported vm host file type: " + vmHostFile.getType()); + } + + syncVmHostFilesFromHost(syncContext, new Completion(trigger) { + @Override + public void success() { + trigger.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + trigger.fail(errorCode); + } + }); + } + }).then(new NoRollbackFlow() { + String __name__ = "write-vm-host-file-to-dest-host"; + + @Override + public boolean skip(Map data) { + return context.sameHost || context.vmHostFile == null; + } + + @Override + public void run(FlowTrigger trigger, Map data) { + VmHostFileContentVO content = Q.New(VmHostFileContentVO.class) + .eq(VmHostFileContentVO_.uuid, context.vmHostFile.getUuid()) + .find(); + if (content == null) { + logger.debug(String.format("skip to write vm host file for VM[vmUuid=%s]: file content is not saved in MN", + context.vmUuid)); + trigger.next(); + return; + } + + VmHostFileTO to = new VmHostFileTO(); + to.setPath(context.vmHostFile.getPath()); + to.setType(context.vmHostFile.getType().toString()); + to.setFileFormat(content.getFormat().toString()); + + String contentBase64 = Base64.getEncoder().encodeToString(content.getContent()); + to.setContentBase64(contentBase64); + + RewriteVmHostFilesContext rewriteContext = new RewriteVmHostFilesContext(); + rewriteContext.hostUuid = context.hostUuid; + rewriteContext.hostFiles = Collections.singletonList(to); + + rewriteVmHostFiles(rewriteContext, new Completion(trigger) { + @Override + public void success() { + trigger.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + trigger.fail(errorCode); + } + }); + } + }).then(new NoRollbackFlow() { + String __name__ = "re-read-vm-host-file-from-dest-host"; + + @Override + public boolean skip(Map data) { + // if context.sameHost is true, we also need to re-read the host file for cache. + return context.vmHostFile == null; + } + + @Override + public void run(FlowTrigger trigger, Map data) { + KvmSecureBootExtensions.SyncVmHostFilesFromHostContext syncBackContext = + new KvmSecureBootExtensions.SyncVmHostFilesFromHostContext(); + syncBackContext.hostUuid = context.hostUuid; + syncBackContext.vmUuid = context.vmUuid; + + if (context.type == VmHostFileType.NvRam) { + syncBackContext.nvRamPath = context.vmHostFile.getPath(); + } else if (context.type == VmHostFileType.TpmState) { + syncBackContext.tpmStateFolder = context.vmHostFile.getPath(); + } + + syncVmHostFilesFromHost(syncBackContext, new Completion(trigger) { + @Override + public void success() { + trigger.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + trigger.fail(errorCode); + } + }); + } + }).done(new FlowDoneHandler(completion) { + @Override + public void handle(Map data) { + completion.success(); + } + }).error(new FlowErrorHandler(completion) { + @Override + public void handle(ErrorCode errCode, Map data) { + completion.fail(errCode); + } + }).start(); + } + + private void prepareNvRamHostFileOnHost(VmInstanceSpec spec, Completion completion) { + final DiskAO nvRamSpec = spec.getNvRamSpec(); + if (nvRamSpec == null) { + completion.success(); + return; + } + + PrepareHostFileContext context = new PrepareHostFileContext(); + context.hostUuid = spec.getDestHost().getUuid(); + context.vmUuid = spec.getVmInventory().getUuid(); + context.type = VmHostFileType.NvRam; + prepareHostFileOnHost(context, completion); + } + @Override public void preReleaseVmResource(VmInstanceSpec spec, Completion completion) { completion.success(); @@ -232,7 +620,7 @@ public void run(MessageReply reply) { } trigger.fail(operr("failed to create NvRam volume") .withOpaque("vm.uuid", context.vmUuid) - .withCause(reply.getError())); + .causedBy(reply.getError())); } }); } @@ -272,7 +660,7 @@ public void run(MessageReply reply) { } trigger.fail(operr("failed to instantiate NvRam volume") .withOpaque("vm.uuid", context.vmUuid) - .withCause(reply.getError())); + .causedBy(reply.getError())); } }); } @@ -315,7 +703,7 @@ public void run(MessageReply reply) { completion.fail(operr("failed to delete NvRam volume") .withOpaque("vm.uuid", vmUuid) .withOpaque("volume.uuid", volumeUuid) - .withCause(reply.getError())); + .causedBy(reply.getError())); } }); } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootManager.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootManager.java new file mode 100644 index 00000000000..e6fc30333cc --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootManager.java @@ -0,0 +1,104 @@ +package org.zstack.kvm.efi; + +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.legacy.ComputeLegacyGlobalProperty; +import org.zstack.core.cloudbus.EventCallback; +import org.zstack.core.cloudbus.EventFacadeImpl; +import org.zstack.core.db.Q; +import org.zstack.header.Component; +import org.zstack.header.core.Completion; +import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.vm.VmCanonicalEvents; +import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.VmInstanceVO_; +import org.zstack.header.vm.additions.VmHostFileType; +import org.zstack.header.vm.additions.VmHostFileVO; +import org.zstack.header.vm.additions.VmHostFileVO_; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import javax.persistence.Tuple; +import java.util.List; +import java.util.Map; + +import static org.zstack.utils.CollectionDSL.list; +import static org.zstack.utils.CollectionUtils.findOneOrNull; + +public class KvmSecureBootManager implements Component { + private static final CLogger logger = Utils.getLogger(KvmSecureBootManager.class); + + @Autowired + private EventFacadeImpl eventFacade; + @Autowired + private KvmSecureBootExtensions secureBootExtensions; + + @Override + public boolean start() { + setupCanonicalEvents(); + return true; + } + + @Override + public boolean stop() { + return true; + } + + @SuppressWarnings("rawtypes") + private void setupCanonicalEvents() { + eventFacade.on(VmCanonicalEvents.VM_LIBVIRT_REPORT_SHUTDOWN, new EventCallback() { + @Override + protected void run(Map tokens, Object data) { + if (ComputeLegacyGlobalProperty.enableNvRamTypeVolume) { + return; + } + + String vmUuid = (String) data; + Tuple tuple = Q.New(VmInstanceVO.class) + .select(VmInstanceVO_.hostUuid, VmInstanceVO_.lastHostUuid) + .eq(VmInstanceVO_.uuid, vmUuid) + .findTuple(); + if (tuple == null) { + return; + } + + String hostUuid = (String) tuple.get(0); + if (hostUuid == null) { + hostUuid = (String) tuple.get(1); + } + + List hostFiles = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) + .eq(VmHostFileVO_.hostUuid, hostUuid) + .in(VmHostFileVO_.type, list(VmHostFileType.NvRam, VmHostFileType.TpmState)) + .list(); + if (hostFiles.isEmpty()) { + return; + } + + VmHostFileVO nvRamFile = findOneOrNull(hostFiles, it -> it.getType() == VmHostFileType.NvRam); + VmHostFileVO tpmStateFile = findOneOrNull(hostFiles, it -> it.getType() == VmHostFileType.TpmState); + if (nvRamFile == null && tpmStateFile == null) { + return; + } + + KvmSecureBootExtensions.SyncVmHostFilesFromHostContext context = new KvmSecureBootExtensions.SyncVmHostFilesFromHostContext(); + context.hostUuid = hostUuid; + context.vmUuid = vmUuid; + context.nvRamPath = nvRamFile == null ? null : nvRamFile.getPath(); + context.tpmStateFolder = tpmStateFile == null ? null : tpmStateFile.getPath(); + secureBootExtensions.syncVmHostFilesFromHost(context, new Completion(null) { + @Override + public void success() { + logger.info(String.format("success to read file content from host[uuid=%s]", context.hostUuid)); + } + + @Override + public void fail(ErrorCode errorCode) { + logger.warn(String.format("failed to read file content from host[uuid=%s]: %s", + context.hostUuid, errorCode.getReadableDetails())); + } + }); + } + }); + } +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmExtensions.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmExtensions.java index 7ff72c6affa..0d98a310d7a 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmExtensions.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmExtensions.java @@ -1,13 +1,43 @@ package org.zstack.kvm.tpm; +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.core.Platform; +import org.zstack.core.db.DatabaseFacade; +import org.zstack.core.db.Q; +import org.zstack.core.db.SQL; +import org.zstack.header.core.Completion; import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.vm.PreVmInstantiateResourceExtensionPoint; import org.zstack.header.vm.VmInstanceSpec; +import org.zstack.header.vm.VmInstantiateResourceException; +import org.zstack.header.vm.additions.VmHostFileType; +import org.zstack.header.vm.additions.VmHostFileVO; +import org.zstack.header.vm.additions.VmHostFileVO_; import org.zstack.header.vm.devices.VmDevicesSpec; import org.zstack.kvm.KVMAgentCommands; import org.zstack.kvm.KVMHostInventory; import org.zstack.kvm.KVMStartVmExtensionPoint; +import org.zstack.kvm.efi.KvmSecureBootExtensions; +import org.zstack.kvm.efi.KvmSecureBootExtensions.*; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.sql.Timestamp; +import java.time.Instant; + +import static org.zstack.kvm.KVMConstant.*; + +public class KvmTpmExtensions implements KVMStartVmExtensionPoint, + PreVmInstantiateResourceExtensionPoint { + private static final CLogger logger = Utils.getLogger(KvmTpmExtensions.class); + + @Autowired + private KvmSecureBootExtensions secureBootExtensions; + @Autowired + private DatabaseFacade databaseFacade; + + private final Object hostFileLock = new Object(); -public class KvmTpmExtensions implements KVMStartVmExtensionPoint { @Override public void beforeStartVmOnKvm(KVMHostInventory host, VmInstanceSpec spec, KVMAgentCommands.StartVmCmd cmd) { final VmDevicesSpec devicesSpec = spec.getDevicesSpec(); @@ -17,7 +47,33 @@ public void beforeStartVmOnKvm(KVMHostInventory host, VmInstanceSpec spec, KVMAg TpmTO tpm = new TpmTO(); tpm.setKeyProviderUuid(devicesSpec.getTpm().getKeyProviderUuid()); + tpm.setInstallPath(buildTpmStateFilePath(cmd.getVmInstanceUuid())); cmd.setTpm(tpm); + + synchronized (hostFileLock) { + VmHostFileVO tpmStateFile = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, cmd.getVmInstanceUuid()) + .eq(VmHostFileVO_.type, VmHostFileType.TpmState) + .eq(VmHostFileVO_.hostUuid, host.getUuid()) + .find(); + if (tpmStateFile == null) { + tpmStateFile = new VmHostFileVO(); + tpmStateFile.setUuid(Platform.getUuid()); + tpmStateFile.setHostUuid(host.getUuid()); + tpmStateFile.setVmInstanceUuid(cmd.getVmInstanceUuid()); + tpmStateFile.setType(VmHostFileType.TpmState); + tpmStateFile.setPath(tpm.getInstallPath()); + tpmStateFile.setCreateDate(Timestamp.from(Instant.now())); + tpmStateFile.setResourceName("TpmState file for " + cmd.getVmInstanceUuid()); + databaseFacade.persist(tpmStateFile); + } else { + SQL.New(VmHostFileVO.class) + .eq(VmHostFileVO_.uuid, tpmStateFile.getUuid()) + .set(VmHostFileVO_.path, tpm.getInstallPath()) + .set(VmHostFileVO_.lastOpDate, Timestamp.from(Instant.now())) + .update(); + } + } } @Override @@ -29,4 +85,43 @@ public void startVmOnKvmSuccess(KVMHostInventory host, VmInstanceSpec spec) { public void startVmOnKvmFailed(KVMHostInventory host, VmInstanceSpec spec, ErrorCode err) { // do-nothing } + + @Override + public void preBeforeInstantiateVmResource(VmInstanceSpec spec) throws VmInstantiateResourceException { + // do-nothing + } + + @Override + public void preInstantiateVmResource(VmInstanceSpec spec, Completion completion) { + prepareTpmStateHostFileOnHost(spec, completion); + } + + static class PrepareTpmStateHostFileContext { + String hostUuid; + String vmUuid; + + // whether the NvRam is on the same host as before + boolean sameHost = false; + VmHostFileVO tpmStateFile; + } + + private void prepareTpmStateHostFileOnHost(VmInstanceSpec spec, Completion completion) { + final VmDevicesSpec devicesSpec = spec.getDevicesSpec(); + if (devicesSpec == null || devicesSpec.getTpm() == null || !devicesSpec.getTpm().isEnable()) { + completion.success(); + return; + } + + PrepareHostFileContext context = new PrepareHostFileContext(); + context.hostUuid = spec.getDestHost().getUuid(); + context.vmUuid = spec.getVmInventory().getUuid(); + context.type = VmHostFileType.TpmState; + secureBootExtensions.prepareHostFileOnHost(context, completion); + } + + + @Override + public void preReleaseVmResource(VmInstanceSpec spec, Completion completion) { + completion.success(); + } } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmTO.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmTO.java index d3210a3c2d7..c1de0d42c1b 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmTO.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmTO.java @@ -4,6 +4,7 @@ public class TpmTO implements Serializable { private String keyProviderUuid; + private String installPath; public String getKeyProviderUuid() { return keyProviderUuid; @@ -12,4 +13,12 @@ public String getKeyProviderUuid() { public void setKeyProviderUuid(String keyProviderUuid) { this.keyProviderUuid = keyProviderUuid; } + + public String getInstallPath() { + return installPath; + } + + public void setInstallPath(String installPath) { + this.installPath = installPath; + } } diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index 27233b51680..2d6b9d93879 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -277,6 +277,13 @@ + + + + + + + diff --git a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy index 89ac7bc2183..bdce9f0cde3 100755 --- a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy +++ b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy @@ -12,6 +12,7 @@ import org.zstack.header.storage.snapshot.TakeSnapshotsOnKvmResultStruct import org.zstack.header.vm.VmInstanceState import org.zstack.header.vm.VmInstanceVO import org.zstack.header.vm.VmInstanceVO_ +import org.zstack.header.vm.additions.VmHostFileContentFormat import org.zstack.header.vm.devices.DeviceAddress import org.zstack.header.vm.devices.VirtualDeviceInfo import org.zstack.header.volume.VolumeInventory @@ -704,5 +705,24 @@ class KVMSimulator implements Simulator { spec.simulator(KVMConstant.HA_NETWORK_GROUP_SYNC_PATH) { return new KVMAgentCommands.AgentResponse() } + + spec.simulator(KVMConstant.READ_VM_HOST_FILE_PATH) { HttpEntity e -> + def cmd = JSONObjectUtil.toObject(e.body, ReadVmHostFileContentCmd) + + def rsp = new ReadVmHostFileContentResponse() + for (final def param in cmd.hostFiles) { + def to = new VmHostFileTO() + to.path = param.path + to.type = param.type + to.fileFormat = VmHostFileContentFormat.Raw.toString() + to.contentBase64 = "dGVzdA==" + rsp.hostFiles.add(to) + } + return rsp + } + + spec.simulator(KVMConstant.WRITE_VM_HOST_FILE_PATH) { HttpEntity e -> + return new WriteVmHostFileContentResponse() + } } } diff --git a/utils/src/main/java/org/zstack/utils/CollectionUtils.java b/utils/src/main/java/org/zstack/utils/CollectionUtils.java index b04a0635668..65e4f0e439d 100755 --- a/utils/src/main/java/org/zstack/utils/CollectionUtils.java +++ b/utils/src/main/java/org/zstack/utils/CollectionUtils.java @@ -5,6 +5,7 @@ import org.zstack.utils.function.ListFunction; import org.zstack.utils.logging.CLogger; +import javax.annotation.Nullable; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; @@ -64,6 +65,7 @@ public static List filter(Collection from, Predicate tester) { return from.stream().filter(tester).collect(Collectors.toList()); } + @Nullable public static T findOneOrNull(Collection from, Predicate tester) { return from.stream().filter(tester).findFirst().orElse(null); } From 51b34cb46c976a34fa0498e58fe9f8d9a9f2377d Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 6 Mar 2026 10:15:37 +0800 Subject: [PATCH 013/122] [header]: merge TpmHostRefVO to VmHostFileVO * Removes database tables and corresponding entity classes related to TPM host references * Introducing a more generic VM-host inventory class as a replacement This is patch for feature "vTPM and Secure Boot" Resolves: ZSV-11310 Change-Id: I7a686a77686f63637974786a74656a776472686e --- conf/persistence.xml | 1 - .../header/tpm/entity/TpmCapabilityView.java | 19 +-- .../tpm/entity/TpmHostRefInventory.java | 106 ----------------- .../header/tpm/entity/TpmHostRefVO.java | 106 ----------------- .../header/tpm/entity/TpmInventory.java | 13 --- .../org/zstack/header/tpm/entity/TpmVO.java | 19 --- .../header/vm/additions/PackageInfo.java | 7 ++ .../vm/additions/VmHostFileInventory.java | 108 ++++++++++++++++++ .../org/zstack/kvm/tpm/KvmTpmManager.java | 14 ++- 9 files changed, 140 insertions(+), 253 deletions(-) delete mode 100644 header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefInventory.java delete mode 100644 header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefVO.java create mode 100644 header/src/main/java/org/zstack/header/vm/additions/PackageInfo.java create mode 100644 header/src/main/java/org/zstack/header/vm/additions/VmHostFileInventory.java diff --git a/conf/persistence.xml b/conf/persistence.xml index 4c68460e7d3..80528cfc337 100755 --- a/conf/persistence.xml +++ b/conf/persistence.xml @@ -18,7 +18,6 @@ org.zstack.resourceconfig.ResourceConfigVO org.zstack.header.managementnode.ManagementNodeVO org.zstack.header.managementnode.ManagementNodeContextVO - org.zstack.header.tpm.entity.TpmHostRefVO org.zstack.header.tpm.entity.TpmVO org.zstack.header.vm.additions.VmHostFileVO org.zstack.header.vm.additions.VmHostFileContentVO diff --git a/header/src/main/java/org/zstack/header/tpm/entity/TpmCapabilityView.java b/header/src/main/java/org/zstack/header/tpm/entity/TpmCapabilityView.java index 2b60316df52..3af24b8662c 100644 --- a/header/src/main/java/org/zstack/header/tpm/entity/TpmCapabilityView.java +++ b/header/src/main/java/org/zstack/header/tpm/entity/TpmCapabilityView.java @@ -1,11 +1,13 @@ package org.zstack.header.tpm.entity; import org.zstack.header.configuration.PythonClass; +import org.zstack.header.vm.additions.VmHostFileInventory; import java.sql.Timestamp; -import java.util.ArrayList; import java.util.List; +import static org.zstack.utils.CollectionDSL.list; + @PythonClass public class TpmCapabilityView { // fields in TpmInventory @@ -14,7 +16,10 @@ public class TpmCapabilityView { private String vmInstanceUuid; private Timestamp createDate; private Timestamp lastOpDate; - private List hostRefs; + /** + * collect VmHostFileInventory(VmHostFileVO) type=NvRam or type=TpmState + */ + private List fileRefs; // related table fields // TODO keyProviderUuid / keyProviderType / keyProviderName / keyProviderKeyVersion @@ -32,7 +37,6 @@ public void setTpmInventory(TpmInventory inventory) { setVmInstanceUuid(inventory.getVmInstanceUuid()); setCreateDate(inventory.getCreateDate()); setLastOpDate(inventory.getLastOpDate()); - setHostRefs(new ArrayList<>(inventory.getHostRefs())); } public String getUuid() { @@ -75,12 +79,12 @@ public void setLastOpDate(Timestamp lastOpDate) { this.lastOpDate = lastOpDate; } - public List getHostRefs() { - return hostRefs; + public List getFileRefs() { + return fileRefs; } - public void setHostRefs(List hostRefs) { - this.hostRefs = hostRefs; + public void setFileRefs(List fileRefs) { + this.fileRefs = fileRefs; } public String getEdkVersion() { @@ -110,6 +114,7 @@ public void setResetTpmAfterVmCloneConfig(boolean resetTpmAfterVmCloneConfig) { public static TpmCapabilityView __example__() { TpmCapabilityView view = new TpmCapabilityView(); view.setTpmInventory(TpmInventory.__example__()); + view.setFileRefs(list(VmHostFileInventory.__example__())); view.setEdkVersion("edk2-ovmf-20220126gitbb1bba3d77-3.el8.noarch"); view.setSwtpmVersion("0.8.2"); diff --git a/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefInventory.java b/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefInventory.java deleted file mode 100644 index 3253723c0d0..00000000000 --- a/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefInventory.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.zstack.header.tpm.entity; - -import org.zstack.header.host.HostInventory; -import org.zstack.header.host.HostVO; -import org.zstack.header.message.DocUtils; -import org.zstack.header.query.ExpandedQueries; -import org.zstack.header.query.ExpandedQuery; -import org.zstack.header.search.Inventory; - -import java.sql.Timestamp; -import java.util.Collection; -import java.util.List; - -import static org.zstack.utils.CollectionUtils.transform; - -@Inventory(mappingVOClass = TpmHostRefVO.class) -@ExpandedQueries({ - @ExpandedQuery(expandedField = "tpm", inventoryClass = TpmInventory.class, - foreignKey = "tpmUuid", expandedInventoryKey = "uuid"), - @ExpandedQuery(expandedField = "host", inventoryClass = HostInventory.class, - foreignKey = "hostUuid", expandedInventoryKey = "uuid"), -}) -public class TpmHostRefInventory { - private long id; - private String tpmUuid; - private String hostUuid; - private String path; - private Timestamp createDate; - private Timestamp lastOpDate; - - public TpmHostRefInventory() { - } - - public static TpmHostRefInventory valueOf(TpmHostRefVO vo) { - TpmHostRefInventory inv = new TpmHostRefInventory(); - inv.setId(vo.getId()); - inv.setTpmUuid(vo.getTpmUuid()); - inv.setHostUuid(vo.getHostUuid()); - inv.setPath(vo.getPath()); - inv.setCreateDate(vo.getCreateDate()); - inv.setLastOpDate(vo.getLastOpDate()); - return inv; - } - - public static List valueOf(Collection vos) { - return transform(vos, TpmHostRefInventory::valueOf); - } - - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getTpmUuid() { - return tpmUuid; - } - - public void setTpmUuid(String tpmUuid) { - this.tpmUuid = tpmUuid; - } - - public String getHostUuid() { - return hostUuid; - } - - public void setHostUuid(String hostUuid) { - this.hostUuid = hostUuid; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - public Timestamp getCreateDate() { - return createDate; - } - - public void setCreateDate(Timestamp createDate) { - this.createDate = createDate; - } - - public Timestamp getLastOpDate() { - return lastOpDate; - } - - public void setLastOpDate(Timestamp lastOpDate) { - this.lastOpDate = lastOpDate; - } - - public static TpmHostRefInventory __example__() { - TpmHostRefInventory ref = new TpmHostRefInventory(); - ref.setId(1L); - ref.setTpmUuid(DocUtils.createFixedUuid(TpmVO.class)); - ref.setHostUuid(DocUtils.createFixedUuid(HostVO.class)); - ref.setCreateDate(DocUtils.timestamp()); - ref.setLastOpDate(DocUtils.timestamp()); - return ref; - } -} diff --git a/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefVO.java b/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefVO.java deleted file mode 100644 index 38bdbf5049f..00000000000 --- a/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefVO.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.zstack.header.tpm.entity; - -import org.zstack.header.host.HostVO; -import org.zstack.header.vo.EntityGraph; -import org.zstack.header.vo.ForeignKey; -import org.zstack.header.vo.ToInventory; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Table; -import java.sql.Timestamp; - -@Entity -@Table -@EntityGraph( - friends = { - @EntityGraph.Neighbour(type = TpmVO.class, myField = "tpmUuid", targetField = "uuid"), - @EntityGraph.Neighbour(type = HostVO.class, myField = "hostUuid", targetField = "uuid"), - } -) -public class TpmHostRefVO implements ToInventory { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column - private long id; - - @Column - @ForeignKey(parentEntityClass = TpmVO.class, onDeleteAction = ForeignKey.ReferenceOption.CASCADE) - private String tpmUuid; - - @Column - @ForeignKey(parentEntityClass = HostVO.class, onDeleteAction = ForeignKey.ReferenceOption.CASCADE) - private String hostUuid; - - @Column - private String path; - - @Column - private Timestamp createDate; - - @Column - private Timestamp lastOpDate; - - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getTpmUuid() { - return tpmUuid; - } - - public void setTpmUuid(String tpmUuid) { - this.tpmUuid = tpmUuid; - } - - public String getHostUuid() { - return hostUuid; - } - - public void setHostUuid(String hostUuid) { - this.hostUuid = hostUuid; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - public Timestamp getCreateDate() { - return createDate; - } - - public void setCreateDate(Timestamp createDate) { - this.createDate = createDate; - } - - public Timestamp getLastOpDate() { - return lastOpDate; - } - - public void setLastOpDate(Timestamp lastOpDate) { - this.lastOpDate = lastOpDate; - } - - @Override - public String toString() { - return "TpmHostRefVO{" + - "id=" + id + - ", tpmUuid='" + tpmUuid + '\'' + - ", hostUuid='" + hostUuid + '\'' + - ", path='" + path + '\'' + - ", createDate=" + createDate + - ", lastOpDate=" + lastOpDate + - '}'; - } -} diff --git a/header/src/main/java/org/zstack/header/tpm/entity/TpmInventory.java b/header/src/main/java/org/zstack/header/tpm/entity/TpmInventory.java index 70ea376e643..eafd210a61d 100644 --- a/header/src/main/java/org/zstack/header/tpm/entity/TpmInventory.java +++ b/header/src/main/java/org/zstack/header/tpm/entity/TpmInventory.java @@ -10,11 +10,9 @@ import java.io.Serializable; import java.sql.Timestamp; -import java.util.ArrayList; import java.util.Collection; import java.util.List; -import static org.zstack.utils.CollectionDSL.list; import static org.zstack.utils.CollectionUtils.transform; @PythonClassInventory @@ -29,7 +27,6 @@ public class TpmInventory implements Serializable { private String vmInstanceUuid; private Timestamp createDate; private Timestamp lastOpDate; - private List hostRefs = new ArrayList<>(); public TpmInventory() { } @@ -41,7 +38,6 @@ public static TpmInventory valueOf(TpmVO vo) { inv.setVmInstanceUuid(vo.getVmInstanceUuid()); inv.setCreateDate(vo.getCreateDate()); inv.setLastOpDate(vo.getLastOpDate()); - inv.setHostRefs(TpmHostRefInventory.valueOf(vo.getHostRefs())); return inv; } @@ -89,14 +85,6 @@ public void setLastOpDate(Timestamp lastOpDate) { this.lastOpDate = lastOpDate; } - public List getHostRefs() { - return hostRefs; - } - - public void setHostRefs(List hostRefs) { - this.hostRefs = hostRefs; - } - public static TpmInventory __example__() { TpmInventory tpm = new TpmInventory(); tpm.setUuid(DocUtils.createFixedUuid(TpmVO.class)); @@ -104,7 +92,6 @@ public static TpmInventory __example__() { tpm.setName("TPM-for-VM-" + tpm.getVmInstanceUuid()); tpm.setCreateDate(DocUtils.timestamp()); tpm.setLastOpDate(DocUtils.timestamp()); - tpm.setHostRefs(list(TpmHostRefInventory.__example__())); return tpm; } } diff --git a/header/src/main/java/org/zstack/header/tpm/entity/TpmVO.java b/header/src/main/java/org/zstack/header/tpm/entity/TpmVO.java index 02bbda43431..e5fafea5689 100644 --- a/header/src/main/java/org/zstack/header/tpm/entity/TpmVO.java +++ b/header/src/main/java/org/zstack/header/tpm/entity/TpmVO.java @@ -7,7 +7,6 @@ import org.zstack.header.vo.BaseResource; import org.zstack.header.vo.EntityGraph; import org.zstack.header.vo.ForeignKey; -import org.zstack.header.vo.NoView; import org.zstack.header.vo.ResourceVO; import org.zstack.header.vo.SoftDeletionCascade; import org.zstack.header.vo.SoftDeletionCascades; @@ -15,14 +14,9 @@ import javax.persistence.Column; import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.JoinColumn; -import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.Transient; import java.sql.Timestamp; -import java.util.HashSet; -import java.util.Set; @Entity @Table @@ -50,11 +44,6 @@ public class TpmVO extends ResourceVO implements ToInventory, OwnedByAccount { @Transient private String accountUuid; - @OneToMany(fetch = FetchType.EAGER) - @JoinColumn(name = "tpmUuid", insertable = false, updatable = false) - @NoView - private Set hostRefs = new HashSet<>(); - public String getVmInstanceUuid() { return vmInstanceUuid; } @@ -89,14 +78,6 @@ public void setAccountUuid(String accountUuid) { this.accountUuid = accountUuid; } - public Set getHostRefs() { - return hostRefs; - } - - public void setHostRefs(Set hostRefs) { - this.hostRefs = hostRefs; - } - public TpmVO() { } diff --git a/header/src/main/java/org/zstack/header/vm/additions/PackageInfo.java b/header/src/main/java/org/zstack/header/vm/additions/PackageInfo.java new file mode 100644 index 00000000000..dc9921af1f4 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/additions/PackageInfo.java @@ -0,0 +1,7 @@ +package org.zstack.header.vm.additions; + +import org.zstack.header.rest.SDKPackage; + +@SDKPackage(packageName="org.zstack.sdk.vm.entity") +public class PackageInfo { +} diff --git a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileInventory.java b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileInventory.java new file mode 100644 index 00000000000..fc5ecb9bb9f --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileInventory.java @@ -0,0 +1,108 @@ +package org.zstack.header.vm.additions; + +import org.zstack.header.host.HostVO; +import org.zstack.header.message.DocUtils; +import org.zstack.header.vm.VmInstanceVO; + +import java.sql.Timestamp; +import java.util.Collection; +import java.util.List; + +import static org.zstack.utils.CollectionUtils.transform; + +public class VmHostFileInventory { + private String uuid; + private String vmInstanceUuid; + private String hostUuid; + private String type; + private String path; + private Timestamp createDate; + private Timestamp lastOpDate; + + public VmHostFileInventory() { + } + + public static VmHostFileInventory valueOf(VmHostFileVO vo) { + VmHostFileInventory inv = new VmHostFileInventory(); + inv.setUuid(vo.getUuid()); + inv.setVmInstanceUuid(vo.getVmInstanceUuid()); + inv.setHostUuid(vo.getHostUuid()); + inv.setType(vo.getType().toString()); + inv.setPath(vo.getPath()); + inv.setCreateDate(vo.getCreateDate()); + inv.setLastOpDate(vo.getLastOpDate()); + return inv; + } + + public static List valueOf(Collection vos) { + return transform(vos, VmHostFileInventory::valueOf); + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public Timestamp getCreateDate() { + return createDate; + } + + public void setCreateDate(Timestamp createDate) { + this.createDate = createDate; + } + + public Timestamp getLastOpDate() { + return lastOpDate; + } + + public void setLastOpDate(Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + + public static VmHostFileInventory __example__() { + VmHostFileInventory ref = new VmHostFileInventory(); + ref.setUuid(DocUtils.createFixedUuid(VmHostFileVO.class)); + ref.setVmInstanceUuid(DocUtils.createFixedUuid(VmInstanceVO.class)); + ref.setHostUuid(DocUtils.createFixedUuid(HostVO.class)); + ref.setType(VmHostFileType.TpmState.toString()); + ref.setPath("/var/lib/libvirt/swtpm/" + ref.getHostUuid() + "/"); + ref.setCreateDate(DocUtils.timestamp()); + ref.setLastOpDate(DocUtils.timestamp()); + return ref; + } +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmManager.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmManager.java index 44b31131c87..4eb436a4b72 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmManager.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmManager.java @@ -38,11 +38,16 @@ import org.zstack.header.tpm.message.RemoveTpmReply; import org.zstack.header.vm.VmInstanceVO; import org.zstack.header.vm.VmInstanceVO_; +import org.zstack.header.vm.additions.VmHostFileInventory; +import org.zstack.header.vm.additions.VmHostFileType; +import org.zstack.header.vm.additions.VmHostFileVO; +import org.zstack.header.vm.additions.VmHostFileVO_; import org.zstack.resourceconfig.ResourceConfig; import org.zstack.resourceconfig.ResourceConfigFacade; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import java.util.List; import java.util.Map; import static org.zstack.compute.vm.VmGlobalConfig.RESET_TPM_AFTER_VM_CLONE; @@ -54,6 +59,7 @@ import static org.zstack.kvm.KVMSystemTags.SWTPM_VERSION; import static org.zstack.kvm.KVMSystemTags.SWTPM_VERSION_TOKEN; import static org.zstack.kvm.KVMSystemTags.VM_EDK; +import static org.zstack.utils.CollectionDSL.list; public class KvmTpmManager extends AbstractService { private static final CLogger logger = Utils.getLogger(KvmTpmManager.class); @@ -312,8 +318,14 @@ private void handle(APIGetTpmCapabilityMsg msg) { final VmInstanceVO vm = Q.New(VmInstanceVO.class) .eq(VmInstanceVO_.uuid, tpm.getVmInstanceUuid()) .find(); - view.setTpmInventory(TpmInventory.valueOf(tpm)); + + List files = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, vm.getUuid()) + .in(VmHostFileVO_.type, list(VmHostFileType.TpmState, VmHostFileType.NvRam)) + .list(); + view.setFileRefs(VmHostFileInventory.valueOf(files)); + view.setEdkVersion(VM_EDK.getTokenByResourceUuid(vm.getUuid(), EDK_RPM_TOKEN)); if (vm.getHostUuid() != null) { From 6ffd613f27b4d3fe778ba507f2877beba9036c60 Mon Sep 17 00:00:00 2001 From: "tao.yang" Date: Fri, 6 Mar 2026 17:43:07 +0800 Subject: [PATCH 014/122] [sdk]: rekey API and root key synchronization Resolves: ZSV-11331 Change-Id: I66626a707a6465616d70616171656469796d6476 --- .../vm/devices/VmTpmRekeyAssociation.java | 35 +++++++++++++++++++ conf/springConfigXml/VmInstanceManager.xml | 6 ++++ ...roviderRekeyAssociationExtensionPoint.java | 11 ++++++ 3 files changed, 52 insertions(+) create mode 100644 compute/src/main/java/org/zstack/compute/vm/devices/VmTpmRekeyAssociation.java create mode 100644 header/src/main/java/org/zstack/header/keyprovider/KeyProviderRekeyAssociationExtensionPoint.java diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmRekeyAssociation.java b/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmRekeyAssociation.java new file mode 100644 index 00000000000..ae469a0a9b4 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmRekeyAssociation.java @@ -0,0 +1,35 @@ +package org.zstack.compute.vm.devices; + +import org.zstack.core.db.Q; +import org.zstack.header.keyprovider.KeyProviderRekeyAssociationExtensionPoint; +import org.zstack.header.tpm.entity.TpmVO; +import org.zstack.header.tpm.entity.TpmVO_; +import org.zstack.header.vm.VmInstanceVO; +import org.zstack.utils.CollectionUtils; + +import java.util.Collections; +import java.util.List; + +public class VmTpmRekeyAssociation implements KeyProviderRekeyAssociationExtensionPoint { + @Override + public String getType() { + return VmInstanceVO.class.getSimpleName(); + } + + @Override + public String getAssociatedResourceType() { + return TpmVO.class.getSimpleName(); + } + + @Override + public List getAssociatedResourceUuids(List resourceUuids) { + if (CollectionUtils.isEmpty(resourceUuids)) { + return Collections.emptyList(); + } + + return Q.New(TpmVO.class) + .in(TpmVO_.vmInstanceUuid, resourceUuids) + .select(TpmVO_.uuid) + .listValues(); + } +} diff --git a/conf/springConfigXml/VmInstanceManager.xml b/conf/springConfigXml/VmInstanceManager.xml index 96164268b9e..cfc95692ac0 100755 --- a/conf/springConfigXml/VmInstanceManager.xml +++ b/conf/springConfigXml/VmInstanceManager.xml @@ -295,4 +295,10 @@ + + + + + + diff --git a/header/src/main/java/org/zstack/header/keyprovider/KeyProviderRekeyAssociationExtensionPoint.java b/header/src/main/java/org/zstack/header/keyprovider/KeyProviderRekeyAssociationExtensionPoint.java new file mode 100644 index 00000000000..e23f704b259 --- /dev/null +++ b/header/src/main/java/org/zstack/header/keyprovider/KeyProviderRekeyAssociationExtensionPoint.java @@ -0,0 +1,11 @@ +package org.zstack.header.keyprovider; + +import java.util.List; + +public interface KeyProviderRekeyAssociationExtensionPoint { + String getType(); + + String getAssociatedResourceType(); + + List getAssociatedResourceUuids(List resourceUuids); +} From 32803e76a030abcb6902490fcd6f3b3185d7a602 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 14:10:15 +0800 Subject: [PATCH 015/122] [header]: Envelope wrapper dek to compute * Add verified to HostKeyIdentityVO Related: ZSV-0 Change-Id: I76646e716678687571656965696b7a726d69716f --- .../zstack/header/host/HostKeyIdentityVO.java | 11 + ...retCreateExt \342\200\216ensionPoint.java" | 19 ++ .../secret/SecretGetExtensionPoint.java | 19 ++ .../header/secret/SecretHostDefineMsg.java | 30 ++ .../header/secret/SecretHostDefineReply.java | 28 ++ plugin/kvm/pom.xml | 13 +- .../zstack/kvm/HostSecretEnvelopeCrypto.java | 132 +++++++++ .../java/org/zstack/kvm/KVMAgentCommands.java | 84 ++++++ .../main/java/org/zstack/kvm/KVMConstant.java | 10 + .../src/main/java/org/zstack/kvm/KVMHost.java | 270 ++++++++++++++++++ .../kvm/host/HostSecretCase.groovy | 149 ++++++++++ 11 files changed, 761 insertions(+), 4 deletions(-) create mode 100644 "header/src/main/java/org/zstack/header/secret/SecretCreateExt \342\200\216ensionPoint.java" create mode 100644 header/src/main/java/org/zstack/header/secret/SecretGetExtensionPoint.java create mode 100644 header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java create mode 100644 header/src/main/java/org/zstack/header/secret/SecretHostDefineReply.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCrypto.java create mode 100644 test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretCase.groovy diff --git a/header/src/main/java/org/zstack/header/host/HostKeyIdentityVO.java b/header/src/main/java/org/zstack/header/host/HostKeyIdentityVO.java index b37e6a8ce84..e77873d70fe 100644 --- a/header/src/main/java/org/zstack/header/host/HostKeyIdentityVO.java +++ b/header/src/main/java/org/zstack/header/host/HostKeyIdentityVO.java @@ -25,6 +25,9 @@ public class HostKeyIdentityVO { @Column private String fingerprint; + @Column(nullable = false) + private Boolean verified = false; + @Column private Timestamp createDate; @@ -60,6 +63,14 @@ public void setFingerprint(String fingerprint) { this.fingerprint = fingerprint; } + public Boolean getVerified() { + return verified; + } + + public void setVerified(Boolean verified) { + this.verified = verified; + } + public Timestamp getCreateDate() { return createDate; } diff --git "a/header/src/main/java/org/zstack/header/secret/SecretCreateExt \342\200\216ensionPoint.java" "b/header/src/main/java/org/zstack/header/secret/SecretCreateExt \342\200\216ensionPoint.java" new file mode 100644 index 00000000000..7c8ab192ecd --- /dev/null +++ "b/header/src/main/java/org/zstack/header/secret/SecretCreateExt \342\200\216ensionPoint.java" @@ -0,0 +1,19 @@ + +package org.zstack.header.secret; + +import org.zstack.header.core.ReturnValueCompletion; + +/** + * Extension point for creating a secret in key-manager (e.g. premium with NKP/KMS). + * Used for VM (e.g. vTPM at VM create). Premium implements with key-manager create; + * success returns secretId/name for later get. + */ +public interface SecretCreateExtensionPoint { + /** + * Create a secret. Implementation (e.g. premium) calls key-manager create. + * + * @param secretName name or identifier for the secret + * @param completion success(secretIdOrName) for later get, or fail(error) + */ + void createSecret(String secretName, ReturnValueCompletion completion); +} diff --git a/header/src/main/java/org/zstack/header/secret/SecretGetExtensionPoint.java b/header/src/main/java/org/zstack/header/secret/SecretGetExtensionPoint.java new file mode 100644 index 00000000000..78b28283ab9 --- /dev/null +++ b/header/src/main/java/org/zstack/header/secret/SecretGetExtensionPoint.java @@ -0,0 +1,19 @@ + +package org.zstack.header.secret; + +import org.zstack.header.core.ReturnValueCompletion; + +/** + * Extension point for getting plaintext DEK from key-manager (e.g. premium with NKP/KMS). + * Used for VM (e.g. to send DEK to host via SecretHostDefineMsg). Premium implements with + * key-manager get; success returns dekBase64 (plaintext DEK, base64). + */ +public interface SecretGetExtensionPoint { + /** + * Get plaintext DEK. Implementation (e.g. premium) calls key-manager get. + * + * @param secretNameOrId secret name or id (from create or stored) + * @param completion success(dekBase64) with plaintext DEK in base64, or fail(error) + */ + void getSecret(String secretNameOrId, ReturnValueCompletion completion); +} diff --git a/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java b/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java new file mode 100644 index 00000000000..1d868bd085e --- /dev/null +++ b/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java @@ -0,0 +1,30 @@ +package org.zstack.header.secret; + +import org.zstack.header.host.HostMessage; +import org.zstack.header.message.NeedReplyMessage; + +/** + * Request to define secret on KVM host (for VM e.g. vTPM). Caller provides plaintext DEK (dekBase64). + * Host seals it with host public key (HPKE) and sends envelope to agent. + */ +public class SecretHostDefineMsg extends NeedReplyMessage implements HostMessage { + private String hostUuid; + private String dekBase64; + + @Override + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + public String getDekBase64() { + return dekBase64; + } + + public void setDekBase64(String dekBase64) { + this.dekBase64 = dekBase64; + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/secret/SecretHostDefineReply.java b/header/src/main/java/org/zstack/header/secret/SecretHostDefineReply.java new file mode 100644 index 00000000000..2149bbdb3ba --- /dev/null +++ b/header/src/main/java/org/zstack/header/secret/SecretHostDefineReply.java @@ -0,0 +1,28 @@ +package org.zstack.header.secret; + +import org.zstack.header.message.MessageReply; + +/** Reply for SecretHostDefineMsg (define secret on host for VM e.g. vTPM). */ +public class SecretHostDefineReply extends MessageReply { + public static final String ERROR_CODE_KEYS_NOT_ON_DISK = "KEY_AGENT_KEYS_NOT_ON_DISK"; + public static final String ERROR_CODE_KEY_FILES_INTEGRITY_MISMATCH = "KEY_AGENT_KEY_FILES_INTEGRITY_MISMATCH"; + + private String errorCode; + private String secretUuid; + + public String getErrorCode() { + return errorCode; + } + + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } + + public String getSecretUuid() { + return secretUuid; + } + + public void setSecretUuid(String secretUuid) { + this.secretUuid = secretUuid; + } +} diff --git a/plugin/kvm/pom.xml b/plugin/kvm/pom.xml index 73e4c81afe3..d81071f90dc 100755 --- a/plugin/kvm/pom.xml +++ b/plugin/kvm/pom.xml @@ -2,13 +2,18 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - plugin - org.zstack + plugin + org.zstack 5.5.0 - .. - + .. + kvm + + org.bouncycastle + bcprov-jdk15on + 1.67 + org.zstack compute diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCrypto.java b/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCrypto.java new file mode 100644 index 00000000000..21dc57a2fc7 --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCrypto.java @@ -0,0 +1,132 @@ +package org.zstack.kvm; + +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.agreement.X25519Agreement; +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.generators.HKDFBytesGenerator; +import org.bouncycastle.crypto.generators.X25519KeyPairGenerator; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.X25519KeyGenerationParameters; +import org.bouncycastle.crypto.params.X25519PublicKeyParameters; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; + +import java.security.SecureRandom; +import java.util.Arrays; + +/** + * HPKE seal (RFC 9180) compatible with Go: KEM_X25519_HKDF_SHA256, KDF_HKDF_SHA256, AEAD_AES256GCM. + * Seal: encrypt wrapper DEK with host public key; output = enc (32) || ciphertext (for agent to open with private key). + */ +public final class HostSecretEnvelopeCrypto { + private static final String HPKE_V1 = "HPKE-v1"; + private static final byte[] KEM_ID = new byte[]{0x00, 0x20}; // X25519 HKDF-SHA256 + private static final byte[] KDF_ID = new byte[]{0x00, 0x01}; // HKDF-SHA256 + private static final byte[] AEAD_ID = new byte[]{0x00, 0x02}; // AES-256-GCM + private static final byte[] KEM_SUITE_ID = concat("KEM".getBytes(), KEM_ID); // for DHKEM ExtractAndExpand + private static final byte[] SUITE_ID = concat(concat("HPKE".getBytes(), KEM_ID), concat(KDF_ID, AEAD_ID)); + private static final int NH = 32; + private static final int NK = 32; + private static final int NN = 12; + + private static byte[] concat(byte[] a, byte[] b) { + byte[] r = new byte[a.length + b.length]; + System.arraycopy(a, 0, r, 0, a.length); + System.arraycopy(b, 0, r, a.length, b.length); + return r; + } + + private static byte[] i2osp(int n, int w) { + byte[] r = new byte[w]; + for (int i = w - 1; i >= 0; i--) { + r[i] = (byte) (n & 0xff); + n >>= 8; + } + return r; + } + + /** RFC 9180 LabeledExtract(salt, label, ikm); use kemSuiteId for KEM layer, null for HPKE layer (uses SUITE_ID). */ + private static byte[] labeledExtract(byte[] salt, String label, byte[] ikm, Digest digest, byte[] suiteId) { + byte[] sid = suiteId != null ? suiteId : SUITE_ID; + byte[] labeledIkm = concat(concat(concat(HPKE_V1.getBytes(), sid), label.getBytes()), ikm != null ? ikm : new byte[0]); + return hkdfExtract(salt, labeledIkm, digest); + } + + /** RFC 9180 LabeledExpand(prk, label, info, L); use kemSuiteId for KEM layer, null for HPKE layer. */ + private static byte[] labeledExpand(byte[] prk, String label, byte[] info, int L, Digest digest, byte[] suiteId) { + byte[] sid = suiteId != null ? suiteId : SUITE_ID; + byte[] labeledInfo = concat(concat(concat(concat(i2osp(L, 2), HPKE_V1.getBytes()), sid), label.getBytes()), info != null ? info : new byte[0]); + return hkdfExpand(prk, labeledInfo, L, digest); + } + + private static byte[] hkdfExtract(byte[] salt, byte[] ikm, Digest digest) { + HKDFBytesGenerator gen = new HKDFBytesGenerator(digest); + gen.init(new org.bouncycastle.crypto.params.HKDFParameters(ikm, salt != null ? salt : new byte[NH], null)); + byte[] out = new byte[NH]; + gen.generateBytes(out, 0, out.length); + return out; + } + + private static byte[] hkdfExpand(byte[] prk, byte[] info, int L, Digest digest) { + HKDFBytesGenerator gen = new HKDFBytesGenerator(digest); + gen.init(new org.bouncycastle.crypto.params.HKDFParameters(prk, null, info)); + byte[] out = new byte[L]; + gen.generateBytes(out, 0, out.length); + return out; + } + + /** + * Seal plaintext with recipient's X25519 public key (raw 32 bytes). + * Returns envelope = enc (32 bytes) || ciphertext (AEAD output). + */ + public static byte[] seal(byte[] recipientPublicKey, byte[] plaintext) throws InvalidCipherTextException { + if (recipientPublicKey == null || recipientPublicKey.length != 32 || plaintext == null) { + throw new IllegalArgumentException("recipientPublicKey must be 32 bytes, plaintext non-null"); + } + SecureRandom random = new SecureRandom(); + Digest digest = new SHA256Digest(); + + // 1. Generate ephemeral X25519 key pair (BC crypto) + X25519KeyPairGenerator kpg = new X25519KeyPairGenerator(); + kpg.init(new X25519KeyGenerationParameters(random)); + AsymmetricCipherKeyPair ephemeralKp = kpg.generateKeyPair(); + X25519PublicKeyParameters ephemeralPub = (X25519PublicKeyParameters) ephemeralKp.getPublic(); + byte[] enc = ephemeralPub.getEncoded(); + + // 2. DH shared secret (ephemeral priv, recipient pub) + X25519PublicKeyParameters recipientPub = new X25519PublicKeyParameters(recipientPublicKey, 0); + X25519Agreement agreement = new X25519Agreement(); + agreement.init(ephemeralKp.getPrivate()); + byte[] sharedSecret = new byte[32]; + agreement.calculateAgreement(recipientPub, sharedSecret, 0); + + // 3. KEM shared_secret (DHKEM ExtractAndExpand) with KEM suite_id + byte[] kemContext = concat(enc, recipientPublicKey); + byte[] eaePrk = labeledExtract(new byte[0], "eae_prk", sharedSecret, digest, KEM_SUITE_ID); + byte[] kemSharedSecret = labeledExpand(eaePrk, "shared_secret", kemContext, NH, digest, KEM_SUITE_ID); + + // 4. Key schedule (base mode, empty psk, empty info) with HPKE suite_id + byte[] pskIdHash = labeledExtract(new byte[0], "psk_id_hash", new byte[0], digest, null); + byte[] infoHash = labeledExtract(new byte[0], "info_hash", new byte[0], digest, null); + byte[] keyScheduleContext = concat(new byte[]{0x00}, concat(pskIdHash, infoHash)); + byte[] secret = labeledExtract(kemSharedSecret, "secret", new byte[0], digest, null); + byte[] key = labeledExpand(secret, "key", keyScheduleContext, NK, digest, null); + byte[] baseNonce = labeledExpand(secret, "base_nonce", keyScheduleContext, NN, digest, null); + + // 5. AEAD Seal (AES-256-GCM, empty aad) + GCMBlockCipher cipher = new GCMBlockCipher(new AESEngine()); + cipher.init(true, new AEADParameters(new KeyParameter(key), 128, baseNonce)); + byte[] out = new byte[cipher.getOutputSize(plaintext.length)]; + int len = cipher.processBytes(plaintext, 0, plaintext.length, out, 0); + len += cipher.doFinal(out, len); + byte[] ct = Arrays.copyOf(out, len); + + return concat(enc, ct); + } + + private HostSecretEnvelopeCrypto() { + } +} \ No newline at end of file diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java index 0ac01a3f9cb..2cd8b9fd903 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -402,6 +402,90 @@ public void setAgentStartTimeMillis(long agentStartTimeMillis) { } } + public static class CreatePublicKeyCmd extends AgentCommand { + } + + public static class CreatePublicKeyResponse extends AgentResponse { + } + + public static class GetPublicKeyCmd extends AgentCommand { + } + + public static class GetPublicKeyResponse extends AgentResponse { + private String publicKey; + private String errorCode; + + public String getPublicKey() { + return publicKey; + } + + public void setPublicKey(String publicKey) { + this.publicKey = publicKey; + } + + public String getErrorCode() { + return errorCode; + } + + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } + } + + public static class RotatePublicKeyCmd extends AgentCommand { + } + + public static class RotatePublicKeyResponse extends AgentResponse { + } + + public static class VerifyPublicKeyCmd extends AgentCommand { + } + + public static class VerifyPublicKeyResponse extends AgentResponse { + private String errorCode; + + public String getErrorCode() { + return errorCode; + } + + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } + } + + public static class SecretHostDefineCmd extends AgentCommand { + private String envelopeDekBase64; + + public String getEnvelopeDekBase64() { + return envelopeDekBase64; + } + + public void setEnvelopeDekBase64(String envelopeDekBase64) { + this.envelopeDekBase64 = envelopeDekBase64; + } + } + + public static class SecretHostDefineResponse extends AgentResponse { + private String errorCode; + private String secretUuid; + + public String getErrorCode() { + return errorCode; + } + + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } + + public String getSecretUuid() { + return secretUuid; + } + + public void setSecretUuid(String secretUuid) { + this.secretUuid = secretUuid; + } + } + public static class PingCmd extends AgentCommand { public String hostUuid; public long kvmagentPhysicalMemoryUsageAlarmThreshold; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java index 0145cd02a26..40caa9e0642 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -146,6 +146,16 @@ public interface KVMConstant { String RECONCILE_VM_HA_ENABLED_METADATA_LIVE_PATH = "/host/vm/reconcileHaEnabledMetadata/live"; String HA_NETWORK_GROUP_SYNC_PATH = "/ha/networkgroup/sync"; + String KVM_CREATE_ENVELOPE_KEY_PATH = "/host/key/envelope/createEnvelopeKey"; + String KVM_GET_ENVELOPE_KEY_PATH = "/host/key/envelope/getEnvelopePublicKey"; + String KVM_ROTATE_ENVELOPE_KEY_PATH = "/host/key/envelope/rotateEnvelopeKey"; + String KVM_VERIFY_ENVELOPE_KEY_PATH = "/host/key/envelope/checkEnvelopeKey"; + String KVM_ENSURE_SECRET_PATH = "/host/key/envelope/ensureSecret"; + + String KVM_HOST_FILE_DOWNLOAD_PATH = "/host/file/download"; + String KVM_HOST_FILE_UPLOAD_PATH = "/host/file/upload"; + String KVM_HOST_FILE_DOWNLOAD_PROGRESS_PATH = "/host/file/progress"; + String KVM_HOST_IPSET_ATTACH_NIC_PATH = "/network/ipset/attach"; String KVM_HOST_IPSET_DETACH_NIC_PATH = "/network/ipset/detach"; String KVM_HOST_IPSET_SYNC_PATH = "/network/ipset/sync"; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index dea8f55f006..ac4db9a8791 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -65,6 +65,8 @@ import org.zstack.header.host.*; import org.zstack.header.host.MigrateVmOnHypervisorMsg.StorageMigrationPolicy; import org.zstack.header.image.*; +import org.zstack.header.secret.SecretHostDefineMsg; +import org.zstack.header.secret.SecretHostDefineReply; import org.zstack.header.message.APIMessage; import org.zstack.header.message.Message; import org.zstack.header.message.MessageReply; @@ -815,6 +817,8 @@ protected void handleLocalMessage(Message msg) { handle((UpdateVmConsolePasswordOnHypervisorMsg) msg); } else if (msg instanceof GetBlockDevicesOnHostMsg) { handle((GetBlockDevicesOnHostMsg) msg); + } else if (msg instanceof SecretHostDefineMsg) { + handle((SecretHostDefineMsg) msg); } else { super.handleLocalMessage(msg); } @@ -4102,6 +4106,23 @@ protected void stopVm(final StopVmOnHypervisorMsg msg, final NoErrorCompletion c checkStatus(); final VmInstanceInventory vminv = msg.getVmInventory(); + { + String dekBase64 = "dGVzdERFSw=="; + SecretHostDefineMsg defineMsg = new SecretHostDefineMsg(); + defineMsg.setHostUuid(getSelf().getUuid()); + defineMsg.setDekBase64(dekBase64); + bus.makeTargetServiceIdByResourceUuid(defineMsg, HostConstant.SERVICE_ID, getSelf().getUuid()); + MessageReply defineReply = bus.call(defineMsg); + if (!defineReply.isSuccess()) { + logger.warn(String.format("debug SecretDefine before stop vm[uuid:%s] failed: %s", vminv.getUuid(), defineReply.getError())); + } else { + SecretHostDefineReply srep = defineReply.castReply(); + if (srep != null && srep.getSecretUuid() != null) { + logger.info(String.format("debug SecretDefine before stop vm[uuid:%s] success, secretUuid=%s", vminv.getUuid(), srep.getSecretUuid())); + } + } + } + StopVmCmd cmd = new StopVmCmd(); cmd.setUuid(vminv.getUuid()); cmd.setType(msg.getType()); @@ -5409,6 +5430,21 @@ public void fail(ErrorCode errorCode) { } }); + flow(new NoRollbackFlow() { + String __name__ = "sync-envelope-public-key"; + + @Override + public boolean skip(Map data) { + return data.get(KVMConstant.KVM_HOST_SKIP_PING_NO_FAILURE_EXTENSIONS) != null; + } + + @Override + public void run(FlowTrigger trigger, Map data) { + syncEnvelopeKeyAfterPing(); + trigger.next(); + } + }); + done(new FlowDoneHandler(completion) { @Override public void handle(Map data) { @@ -5426,6 +5462,240 @@ public void handle(ErrorCode errCode, Map data) { }).start(); } + private void syncEnvelopeKeyAfterPing() { + KVMHostVO kvo = dbf.reload(getSelf()); + String hostUuid = kvo.getUuid(); + try { + HostKeyIdentityVO identity = getHostKeyIdentity(hostUuid); + if (identity != null && StringUtils.isNotBlank(identity.getPublicKey())) { + String verifyUrl = buildUrl(KVMConstant.KVM_VERIFY_ENVELOPE_KEY_PATH); + KVMAgentCommands.VerifyPublicKeyResponse vrsp = restf.syncJsonPost(verifyUrl, + new KVMAgentCommands.VerifyPublicKeyCmd(), KVMAgentCommands.VerifyPublicKeyResponse.class); + if (vrsp != null && vrsp.isSuccess()) { + setHostKeyIdentityVerified(hostUuid, true); + return; + } + if (vrsp != null && !vrsp.isSuccess() && isRotateNeededGetError(vrsp.getErrorCode())) { + String rotateUrl = buildUrl(KVMConstant.KVM_ROTATE_ENVELOPE_KEY_PATH); + KVMAgentCommands.RotatePublicKeyResponse rotateRsp = restf.syncJsonPost(rotateUrl, + new KVMAgentCommands.RotatePublicKeyCmd(), KVMAgentCommands.RotatePublicKeyResponse.class); + if (rotateRsp.isSuccess()) { + String getUrl = buildUrl(KVMConstant.KVM_GET_ENVELOPE_KEY_PATH); + KVMAgentCommands.GetPublicKeyResponse getRsp = restf.syncJsonPost(getUrl, + new KVMAgentCommands.GetPublicKeyCmd(), KVMAgentCommands.GetPublicKeyResponse.class); + if (getRsp.isSuccess() && StringUtils.isNotBlank(getRsp.getPublicKey())) { + saveOrUpdateHostKeyIdentity(hostUuid, getRsp.getPublicKey().trim(), true); + return; + } + } else { + logger.warn("verify failed then rotate key on agent failed for host " + hostUuid + ": " + rotateRsp.getError()); + } + } + setHostKeyIdentityVerified(hostUuid, false); + return; + } + String createUrl = buildUrl(KVMConstant.KVM_CREATE_ENVELOPE_KEY_PATH); + KVMAgentCommands.CreatePublicKeyResponse createRsp = restf.syncJsonPost(createUrl, + new KVMAgentCommands.CreatePublicKeyCmd(), KVMAgentCommands.CreatePublicKeyResponse.class); + if (!createRsp.isSuccess()) { + logger.warn("create key on agent failed for host " + hostUuid + ": " + createRsp.getError()); + setHostKeyIdentityVerified(hostUuid, false); + return; + } + String getUrl = buildUrl(KVMConstant.KVM_GET_ENVELOPE_KEY_PATH); + KVMAgentCommands.GetPublicKeyResponse getRsp = restf.syncJsonPost(getUrl, + new KVMAgentCommands.GetPublicKeyCmd(), KVMAgentCommands.GetPublicKeyResponse.class); + if (getRsp.isSuccess() && StringUtils.isNotBlank(getRsp.getPublicKey())) { + saveOrUpdateHostKeyIdentity(hostUuid, getRsp.getPublicKey().trim(), true); + return; + } + if (!getRsp.isSuccess() && isRotateNeededGetError(getRsp.getErrorCode())) { + String rotateUrl = buildUrl(KVMConstant.KVM_ROTATE_ENVELOPE_KEY_PATH); + KVMAgentCommands.RotatePublicKeyResponse rotateRsp = restf.syncJsonPost(rotateUrl, + new KVMAgentCommands.RotatePublicKeyCmd(), KVMAgentCommands.RotatePublicKeyResponse.class); + if (!rotateRsp.isSuccess()) { + logger.warn("rotate key on agent failed for host " + hostUuid + ": " + rotateRsp.getError()); + setHostKeyIdentityVerified(hostUuid, false); + return; + } + getRsp = restf.syncJsonPost(getUrl, new KVMAgentCommands.GetPublicKeyCmd(), KVMAgentCommands.GetPublicKeyResponse.class); + if (getRsp.isSuccess() && StringUtils.isNotBlank(getRsp.getPublicKey())) { + saveOrUpdateHostKeyIdentity(hostUuid, getRsp.getPublicKey().trim(), true); + return; + } + } + logger.warn("get public key from agent failed for host " + hostUuid + ": " + (getRsp != null ? getRsp.getError() : "null")); + setHostKeyIdentityVerified(hostUuid, false); + } catch (Exception e) { + logger.warn("sync secret key after connect failed for host " + hostUuid + ": " + e.getMessage()); + try { + setHostKeyIdentityVerified(hostUuid, false); + } catch (Exception ignored) { + } + } + } + + private void setHostKeyIdentityVerified(String hostUuid, boolean verified) { + HostKeyIdentityVO vo = getHostKeyIdentity(hostUuid); + if (vo != null) { + vo.setVerified(verified); + dbf.update(vo); + } + } + + private static boolean isRotateNeededGetError(String errorCode) { + if (errorCode == null) return false; + return SecretHostDefineReply.ERROR_CODE_KEYS_NOT_ON_DISK.equals(errorCode) + || SecretHostDefineReply.ERROR_CODE_KEY_FILES_INTEGRITY_MISMATCH.equals(errorCode); + } + + private HostKeyIdentityVO getHostKeyIdentity(String hostUuid) { + SimpleQuery q = dbf.createQuery(HostKeyIdentityVO.class); + q.add(HostKeyIdentityVO_.hostUuid, Op.EQ, hostUuid); + return q.find(); + } + + private void saveOrUpdateHostKeyIdentity(String hostUuid, String publicKey, boolean verified) { + if (StringUtils.isBlank(publicKey)) { + return; + } + String keyToSave = publicKey.trim(); + HostKeyIdentityVO vo = getHostKeyIdentity(hostUuid); + if (vo == null) { + vo = new HostKeyIdentityVO(); + vo.setHostUuid(hostUuid); + vo.setPublicKey(keyToSave); + vo.setFingerprint(""); + vo.setVerified(verified); + vo.setCreateDate(new java.sql.Timestamp(System.currentTimeMillis())); + dbf.persist(vo); + return; + } + vo.setPublicKey(keyToSave); + vo.setVerified(verified); + dbf.update(vo); + } + + private void handle(SecretHostDefineMsg msg) { + SecretHostDefineReply reply = new SecretHostDefineReply(); + if (org.apache.commons.lang.StringUtils.isBlank(msg.getDekBase64())) { + reply.setError(operr("dekBase64 is required")); + bus.reply(msg, reply); + return; + } + String hostUuid = getSelf().getUuid(); + HostKeyIdentityVO identity = getHostKeyIdentity(hostUuid); + String pubKey = identity != null ? org.apache.commons.lang.StringUtils.trimToNull(identity.getPublicKey()) : null; + Boolean verifyOk = identity != null ? identity.getVerified() : null; + if (pubKey == null) { + reply.setError(operr("no public key for host, connect/reconnect did not sync key")); + bus.reply(msg, reply); + return; + } + if (!Boolean.TRUE.equals(verifyOk)) { + reply.setError(operr("host secret key verify not ok, not synced")); + bus.reply(msg, reply); + return; + } + byte[] dekRaw; + try { + dekRaw = java.util.Base64.getDecoder().decode(msg.getDekBase64().trim()); + } catch (IllegalArgumentException e) { + reply.setError(operr("invalid dekBase64: %s", e.getMessage())); + bus.reply(msg, reply); + return; + } + if (dekRaw == null || dekRaw.length == 0) { + reply.setError(operr("dekBase64 decoded to empty")); + bus.reply(msg, reply); + return; + } + + byte[] pubKeyBytes; + try { + pubKeyBytes = java.util.Base64.getDecoder().decode(pubKey); + } catch (IllegalArgumentException e) { + reply.setError(operr("invalid host public key in DB: %s", e.getMessage())); + bus.reply(msg, reply); + return; + } + if (pubKeyBytes == null || pubKeyBytes.length != 32) { + reply.setError(operr("host public key must be 32 bytes (X25519)")); + bus.reply(msg, reply); + return; + } + byte[] envelope; + try { + envelope = HostSecretEnvelopeCrypto.seal(pubKeyBytes, dekRaw); + } catch (org.bouncycastle.crypto.InvalidCipherTextException e) { + reply.setError(operr("HPKE seal failed: %s", e.getMessage())); + bus.reply(msg, reply); + return; + } + String envelopeDekBase64 = java.util.Base64.getEncoder().encodeToString(envelope); + try { + String url = buildUrl(KVMConstant.KVM_ENSURE_SECRET_PATH); + KVMAgentCommands.SecretHostDefineCmd cmd = new KVMAgentCommands.SecretHostDefineCmd(); + cmd.setEnvelopeDekBase64(envelopeDekBase64); + KVMAgentCommands.SecretHostDefineResponse rsp = restf.syncJsonPost(url, cmd, KVMAgentCommands.SecretHostDefineResponse.class); + if (rsp.isSuccess()) { + if (rsp.getSecretUuid() != null) { + reply.setSecretUuid(rsp.getSecretUuid()); + } + bus.reply(msg, reply); + return; + } + if (isRotateNeededGetError(rsp.getErrorCode())) { + String rotateUrl = buildUrl(KVMConstant.KVM_ROTATE_ENVELOPE_KEY_PATH); + KVMAgentCommands.RotatePublicKeyResponse rotateRsp = restf.syncJsonPost(rotateUrl, + new KVMAgentCommands.RotatePublicKeyCmd(), KVMAgentCommands.RotatePublicKeyResponse.class); + if (!rotateRsp.isSuccess()) { + reply.setError(operr("ensure secret failed, rotate key then retry failed: %s", rotateRsp.getError())); + bus.reply(msg, reply); + return; + } + String getUrl = buildUrl(KVMConstant.KVM_GET_ENVELOPE_KEY_PATH); + KVMAgentCommands.GetPublicKeyResponse getRsp = restf.syncJsonPost(getUrl, + new KVMAgentCommands.GetPublicKeyCmd(), KVMAgentCommands.GetPublicKeyResponse.class); + if (!getRsp.isSuccess() || StringUtils.isBlank(getRsp.getPublicKey())) { + reply.setError(operr("ensure secret failed, rotate then get public key failed: %s", + getRsp != null ? getRsp.getError() : "null")); + bus.reply(msg, reply); + return; + } + saveOrUpdateHostKeyIdentity(hostUuid, getRsp.getPublicKey().trim(), true); + String newPubKey = getRsp.getPublicKey().trim(); + byte[] newPubKeyBytes = java.util.Base64.getDecoder().decode(newPubKey); + byte[] newEnvelope; + try { + newEnvelope = HostSecretEnvelopeCrypto.seal(newPubKeyBytes, dekRaw); + } catch (org.bouncycastle.crypto.InvalidCipherTextException e) { + reply.setError(operr("ensure secret failed after rotate, HPKE seal failed: %s", e.getMessage())); + bus.reply(msg, reply); + return; + } + String newEnvelopeDekBase64 = java.util.Base64.getEncoder().encodeToString(newEnvelope); + cmd.setEnvelopeDekBase64(newEnvelopeDekBase64); + rsp = restf.syncJsonPost(url, cmd, KVMAgentCommands.SecretHostDefineResponse.class); + if (rsp.isSuccess()) { + if (rsp.getSecretUuid() != null) { + reply.setSecretUuid(rsp.getSecretUuid()); + } + bus.reply(msg, reply); + return; + } + } + reply.setError(operr(rsp.getError())); + if (rsp.getErrorCode() != null) { + reply.setErrorCode(rsp.getErrorCode()); + } + bus.reply(msg, reply); + } catch (RestClientException e) { + reply.setError(operr("ensure secret on agent failed: %s", e.getMessage())); + bus.reply(msg, reply); + } + } + @Override protected void deleteTakeOverFlag(Completion completion) { if (CoreGlobalProperty.UNIT_TEST_ON) { diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretCase.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretCase.groovy new file mode 100644 index 00000000000..86708eba7d4 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretCase.groovy @@ -0,0 +1,149 @@ +package org.zstack.test.integration.kvm.host + +import org.zstack.core.Platform +import org.zstack.core.cloudbus.CloudBus +import org.zstack.header.host.AddHostReply +import org.zstack.header.host.HostConstant +import org.zstack.header.host.HostInventory +import org.zstack.header.host.HostStatus +import org.zstack.header.message.MessageReply +import org.zstack.kvm.AddKVMHostMsg +import org.zstack.kvm.KVMConstant +import org.zstack.kvm.KVMAgentCommands +import org.zstack.storage.primary.local.LocalStorageKvmBackend +import org.zstack.test.integration.kvm.KvmTest +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.SubCase +import org.zstack.header.secret.SecretHostDefineMsg +import org.zstack.header.secret.SecretHostDefineReply + +/** + * Integration test for host secret: create/get/rotate/verify public key on connect, + * and SecretHostDefine (ensure secret on agent). + * Uses simulated agent for all secret paths. + */ +class HostSecretCase extends SubCase { + EnvSpec env + def cluster + CloudBus bus + HostInventory addedHost + + /** 32-byte X25519 public key (base64) for simulator; must be valid for HPKE seal. */ + static final String MOCK_PUBLIC_KEY_BASE64 = "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA=" + + @Override + void setup() { + useSpring(KvmTest.springSpec) + } + + @Override + void environment() { + env = HostEnv.noHostBasicEnv() + } + + @Override + void test() { + env.create { + prepare() + testAddHostWithSecretSync() + testSecretHostDefineSuccess() + testSecretHostDefineFailWhenNoDek() + } + } + + @Override + void clean() { + env.delete() + } + + void prepare() { + cluster = env.inventoryByName("cluster") + bus = bean(CloudBus.class) + } + + void registerSecretSimulators() { + env.simulator(KVMConstant.KVM_CONNECT_PATH) { + def rsp = new KVMAgentCommands.ConnectResponse() + rsp.success = true + rsp.libvirtVersion = "1.0.0" + rsp.qemuVersion = "1.3.0" + return rsp + } + env.simulator(KVMConstant.KVM_HOST_FACT_PATH) { + def rsp = new KVMAgentCommands.HostFactResponse() + rsp.osDistribution = "CentOS" + rsp.osVersion = "7.0" + return rsp + } + env.simulator(LocalStorageKvmBackend.INIT_PATH) { rsp, _ -> return rsp } + + env.simulator(KVMConstant.KVM_CREATE_ENVELOPE_KEY_PATH) { + return new KVMAgentCommands.CreatePublicKeyResponse() + } + env.simulator(KVMConstant.KVM_GET_ENVELOPE_KEY_PATH) { + def rsp = new KVMAgentCommands.GetPublicKeyResponse() + rsp.publicKey = MOCK_PUBLIC_KEY_BASE64 + return rsp + } + env.simulator(KVMConstant.KVM_VERIFY_ENVELOPE_KEY_PATH) { + return new KVMAgentCommands.VerifyPublicKeyResponse() + } + env.simulator(KVMConstant.KVM_ROTATE_ENVELOPE_KEY_PATH) { + return new KVMAgentCommands.RotatePublicKeyResponse() + } + env.simulator(KVMConstant.KVM_ENSURE_SECRET_PATH) { + def rsp = new KVMAgentCommands.SecretHostDefineResponse() + rsp.secretUuid = Platform.uuid + return rsp + } + } + + void testAddHostWithSecretSync() { + registerSecretSimulators() + + AddKVMHostMsg amsg = new AddKVMHostMsg() + amsg.accountUuid = loginAsAdmin().accountUuid + amsg.name = "kvm" + amsg.managementIp = "127.0.0.2" + amsg.resourceUuid = Platform.uuid + amsg.clusterUuid = cluster.uuid + amsg.setPassword("password") + amsg.setUsername("root") + + bus.makeLocalServiceId(amsg, HostConstant.SERVICE_ID) + AddHostReply reply = (AddHostReply) bus.call(amsg) + assert reply != null + assert reply.isSuccess() + assert reply.inventory.status == HostStatus.Connected.toString() + addedHost = reply.inventory + } + + void testSecretHostDefineSuccess() { + assert addedHost != null + + SecretHostDefineMsg msg = new SecretHostDefineMsg() + msg.hostUuid = addedHost.uuid + msg.dekBase64 = "dGVzdERFSw==" // base64 of "testDEK" + + bus.makeTargetServiceIdByResourceUuid(msg, HostConstant.SERVICE_ID, addedHost.uuid) + MessageReply reply = bus.call(msg) + assert reply != null + assert reply.isSuccess() + SecretHostDefineReply defineReply = reply.castReply() + assert defineReply.secretUuid != null + } + + void testSecretHostDefineFailWhenNoDek() { + assert addedHost != null + + SecretHostDefineMsg msg = new SecretHostDefineMsg() + msg.hostUuid = addedHost.uuid + msg.dekBase64 = null + + bus.makeTargetServiceIdByResourceUuid(msg, HostConstant.SERVICE_ID, addedHost.uuid) + MessageReply reply = bus.call(msg) + assert reply != null + assert !reply.isSuccess() + assert reply.error != null + } +} \ No newline at end of file From 74e85a6715ed6c36bea6e244af417690570dd5c6 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 15:30:24 +0800 Subject: [PATCH 016/122] [kvm]: Remove rotate public key in initial pingHook * Add variable to SecretHostDefineCmd * Remove rotate public key when define secret * Envelope public key async call kvm agent * Limit dek length when define libvirt secret Related: ZSV-0 Change-Id: I63706e636d6a646a6b7169617161616876677677 --- .../secret/SecretCreateExtensionPoint.java | 0 .../header/secret/SecretHostDefineMsg.java | 37 +++ .../zstack/kvm/HostSecretEnvelopeCrypto.java | 37 ++- .../java/org/zstack/kvm/KVMAgentCommands.java | 46 ++- .../src/main/java/org/zstack/kvm/KVMHost.java | 267 ++++++++++-------- 5 files changed, 263 insertions(+), 124 deletions(-) rename "header/src/main/java/org/zstack/header/secret/SecretCreateExt \342\200\216ensionPoint.java" => header/src/main/java/org/zstack/header/secret/SecretCreateExtensionPoint.java (100%) diff --git "a/header/src/main/java/org/zstack/header/secret/SecretCreateExt \342\200\216ensionPoint.java" b/header/src/main/java/org/zstack/header/secret/SecretCreateExtensionPoint.java similarity index 100% rename from "header/src/main/java/org/zstack/header/secret/SecretCreateExt \342\200\216ensionPoint.java" rename to header/src/main/java/org/zstack/header/secret/SecretCreateExtensionPoint.java diff --git a/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java b/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java index 1d868bd085e..4305af89491 100644 --- a/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java +++ b/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java @@ -6,10 +6,15 @@ /** * Request to define secret on KVM host (for VM e.g. vTPM). Caller provides plaintext DEK (dekBase64). * Host seals it with host public key (HPKE) and sends envelope to agent. + * vmUuid, purpose, providerName are required by key-agent for DEK cache key. */ public class SecretHostDefineMsg extends NeedReplyMessage implements HostMessage { private String hostUuid; private String dekBase64; + private String vmUuid; + private String purpose; + private String providerName; + private String description; @Override public String getHostUuid() { @@ -27,4 +32,36 @@ public String getDekBase64() { public void setDekBase64(String dekBase64) { this.dekBase64 = dekBase64; } + + public String getVmUuid() { + return vmUuid; + } + + public void setVmUuid(String vmUuid) { + this.vmUuid = vmUuid; + } + + public String getPurpose() { + return purpose; + } + + public void setPurpose(String purpose) { + this.purpose = purpose; + } + + public String getProviderName() { + return providerName; + } + + public void setProviderName(String providerName) { + this.providerName = providerName; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } } \ No newline at end of file diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCrypto.java b/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCrypto.java index 21dc57a2fc7..acaca72d4f1 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCrypto.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCrypto.java @@ -5,8 +5,10 @@ import org.bouncycastle.crypto.engines.AESEngine; import org.bouncycastle.crypto.generators.HKDFBytesGenerator; import org.bouncycastle.crypto.generators.X25519KeyPairGenerator; +import org.bouncycastle.crypto.macs.HMac; import org.bouncycastle.crypto.modes.GCMBlockCipher; import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.HKDFParameters; import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.X25519KeyGenerationParameters; import org.bouncycastle.crypto.params.X25519PublicKeyParameters; @@ -14,15 +16,19 @@ import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.Arrays; /** - * HPKE seal (RFC 9180) compatible with Go: KEM_X25519_HKDF_SHA256, KDF_HKDF_SHA256, AEAD_AES256GCM. + * HPKE seal (RFC 9180) compatible with Go key-agent: KEM_X25519_HKDF_SHA256, KDF_HKDF_SHA256, AEAD_AES256GCM. * Seal: encrypt wrapper DEK with host public key; output = enc (32) || ciphertext (for agent to open with private key). + * Must use the same HPKE "info" as key-agent (cmd/key-agent: info := []byte("key-agent hpke info")) for key schedule. */ public final class HostSecretEnvelopeCrypto { private static final String HPKE_V1 = "HPKE-v1"; + /** HPKE application info; must match key-agent main.go: info := []byte("key-agent hpke info") */ + private static final byte[] HPKE_INFO = "key-agent hpke info".getBytes(StandardCharsets.UTF_8); private static final byte[] KEM_ID = new byte[]{0x00, 0x20}; // X25519 HKDF-SHA256 private static final byte[] KDF_ID = new byte[]{0x00, 0x01}; // HKDF-SHA256 private static final byte[] AEAD_ID = new byte[]{0x00, 0x02}; // AES-256-GCM @@ -62,17 +68,30 @@ private static byte[] labeledExpand(byte[] prk, String label, byte[] info, int L return hkdfExpand(prk, labeledInfo, L, digest); } + /** + * RFC 5869 / RFC 9180 HKDF-Extract: returns PRK = HMAC-Hash(salt, IKM). + * Must not use HKDFBytesGenerator with full init (that does Extract+Expand); Bouncy Castle + * would then return Expand(PRK, "", L) instead of PRK. We implement Extract only via HMAC. + */ private static byte[] hkdfExtract(byte[] salt, byte[] ikm, Digest digest) { - HKDFBytesGenerator gen = new HKDFBytesGenerator(digest); - gen.init(new org.bouncycastle.crypto.params.HKDFParameters(ikm, salt != null ? salt : new byte[NH], null)); - byte[] out = new byte[NH]; - gen.generateBytes(out, 0, out.length); - return out; + byte[] saltBytes = (salt != null && salt.length > 0) ? salt : new byte[NH]; + HMac hmac = new HMac(digest); + hmac.init(new KeyParameter(saltBytes)); + hmac.update(ikm, 0, ikm.length); + byte[] prk = new byte[NH]; + hmac.doFinal(prk, 0); + return prk; } + /** + * RFC 5869 / RFC 9180 HKDF-Expand: OKM = HKDF-Expand(PRK, info, L). + * Must use skipExtractParameters(prk, info) so that Bouncy Castle uses prk as PRK and + * only performs Expand. Using HKDFParameters(prk, null, info) would make BC do + * Extract(null, prk) then Expand, which is wrong. + */ private static byte[] hkdfExpand(byte[] prk, byte[] info, int L, Digest digest) { HKDFBytesGenerator gen = new HKDFBytesGenerator(digest); - gen.init(new org.bouncycastle.crypto.params.HKDFParameters(prk, null, info)); + gen.init(HKDFParameters.skipExtractParameters(prk, info != null ? info : new byte[0])); byte[] out = new byte[L]; gen.generateBytes(out, 0, out.length); return out; @@ -108,9 +127,9 @@ public static byte[] seal(byte[] recipientPublicKey, byte[] plaintext) throws In byte[] eaePrk = labeledExtract(new byte[0], "eae_prk", sharedSecret, digest, KEM_SUITE_ID); byte[] kemSharedSecret = labeledExpand(eaePrk, "shared_secret", kemContext, NH, digest, KEM_SUITE_ID); - // 4. Key schedule (base mode, empty psk, empty info) with HPKE suite_id + // 4. Key schedule (base mode, empty psk; info must match key-agent NewReceiver(sk, info)) byte[] pskIdHash = labeledExtract(new byte[0], "psk_id_hash", new byte[0], digest, null); - byte[] infoHash = labeledExtract(new byte[0], "info_hash", new byte[0], digest, null); + byte[] infoHash = labeledExtract(new byte[0], "info_hash", HPKE_INFO, digest, null); byte[] keyScheduleContext = concat(new byte[]{0x00}, concat(pskIdHash, infoHash)); byte[] secret = labeledExtract(kemSharedSecret, "secret", new byte[0], digest, null); byte[] key = labeledExpand(secret, "key", keyScheduleContext, NK, digest, null); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java index 2cd8b9fd903..4881b342a38 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -455,13 +455,51 @@ public void setErrorCode(String errorCode) { public static class SecretHostDefineCmd extends AgentCommand { private String envelopeDekBase64; + /** Base64 wrapped DEK; agent expects this field name (encryptedDek). */ + private String encryptedDek; + private String vmUuid; + private String purpose; + private String providerName; + private String description; + + public String getEncryptedDek() { + return encryptedDek; + } + + public void setEncryptedDek(String encryptedDek) { + this.encryptedDek = encryptedDek; + } + + public String getVmUuid() { + return vmUuid; + } + + public void setVmUuid(String vmUuid) { + this.vmUuid = vmUuid; + } + + public String getPurpose() { + return purpose; + } + + public void setPurpose(String purpose) { + this.purpose = purpose; + } + + public String getProviderName() { + return providerName; + } + + public void setProviderName(String providerName) { + this.providerName = providerName; + } - public String getEnvelopeDekBase64() { - return envelopeDekBase64; + public String getDescription() { + return description; } - public void setEnvelopeDekBase64(String envelopeDekBase64) { - this.envelopeDekBase64 = envelopeDekBase64; + public void setDescription(String description) { + this.description = description; } } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index ac4db9a8791..62cc3e1046d 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -4111,6 +4111,9 @@ protected void stopVm(final StopVmOnHypervisorMsg msg, final NoErrorCompletion c SecretHostDefineMsg defineMsg = new SecretHostDefineMsg(); defineMsg.setHostUuid(getSelf().getUuid()); defineMsg.setDekBase64(dekBase64); + defineMsg.setVmUuid(vminv.getUuid()); + defineMsg.setPurpose("vm"); + defineMsg.setProviderName("zstack"); bus.makeTargetServiceIdByResourceUuid(defineMsg, HostConstant.SERVICE_ID, getSelf().getUuid()); MessageReply defineReply = bus.call(defineMsg); if (!defineReply.isSuccess()) { @@ -5440,8 +5443,17 @@ public boolean skip(Map data) { @Override public void run(FlowTrigger trigger, Map data) { - syncEnvelopeKeyAfterPing(); - trigger.next(); + syncEnvelopeKeyAfterPing(new Completion(trigger) { + @Override + public void success() { + trigger.next(); + } + + @Override + public void fail(ErrorCode errCode) { + trigger.next(); + } + }); } }); @@ -5462,76 +5474,123 @@ public void handle(ErrorCode errCode, Map data) { }).start(); } - private void syncEnvelopeKeyAfterPing() { + private static final long ENVELOPE_KEY_HTTP_TIMEOUT_SEC = 5L; + + private void syncEnvelopeKeyAfterPing(Completion completion) { KVMHostVO kvo = dbf.reload(getSelf()); - String hostUuid = kvo.getUuid(); + final String hostUuid = kvo.getUuid(); try { HostKeyIdentityVO identity = getHostKeyIdentity(hostUuid); if (identity != null && StringUtils.isNotBlank(identity.getPublicKey())) { String verifyUrl = buildUrl(KVMConstant.KVM_VERIFY_ENVELOPE_KEY_PATH); - KVMAgentCommands.VerifyPublicKeyResponse vrsp = restf.syncJsonPost(verifyUrl, - new KVMAgentCommands.VerifyPublicKeyCmd(), KVMAgentCommands.VerifyPublicKeyResponse.class); - if (vrsp != null && vrsp.isSuccess()) { - setHostKeyIdentityVerified(hostUuid, true); - return; - } - if (vrsp != null && !vrsp.isSuccess() && isRotateNeededGetError(vrsp.getErrorCode())) { - String rotateUrl = buildUrl(KVMConstant.KVM_ROTATE_ENVELOPE_KEY_PATH); - KVMAgentCommands.RotatePublicKeyResponse rotateRsp = restf.syncJsonPost(rotateUrl, - new KVMAgentCommands.RotatePublicKeyCmd(), KVMAgentCommands.RotatePublicKeyResponse.class); - if (rotateRsp.isSuccess()) { - String getUrl = buildUrl(KVMConstant.KVM_GET_ENVELOPE_KEY_PATH); - KVMAgentCommands.GetPublicKeyResponse getRsp = restf.syncJsonPost(getUrl, - new KVMAgentCommands.GetPublicKeyCmd(), KVMAgentCommands.GetPublicKeyResponse.class); - if (getRsp.isSuccess() && StringUtils.isNotBlank(getRsp.getPublicKey())) { - saveOrUpdateHostKeyIdentity(hostUuid, getRsp.getPublicKey().trim(), true); - return; - } - } else { - logger.warn("verify failed then rotate key on agent failed for host " + hostUuid + ": " + rotateRsp.getError()); - } - } - setHostKeyIdentityVerified(hostUuid, false); + restf.asyncJsonPost(verifyUrl, new KVMAgentCommands.VerifyPublicKeyCmd(), + new JsonAsyncRESTCallback(completion) { + @Override + public void fail(ErrorCode err) { + setHostKeyIdentityVerified(hostUuid, false); + completion.success(); + } + + @Override + public void success(KVMAgentCommands.VerifyPublicKeyResponse vrsp) { + if (vrsp != null && vrsp.isSuccess()) { + setHostKeyIdentityVerified(hostUuid, true); + completion.success(); + return; + } + if (vrsp != null && !vrsp.isSuccess() && isRotateNeededGetError(vrsp.getErrorCode())) { + String rotateUrl = buildUrl(KVMConstant.KVM_ROTATE_ENVELOPE_KEY_PATH); + restf.asyncJsonPost(rotateUrl, new KVMAgentCommands.RotatePublicKeyCmd(), + new JsonAsyncRESTCallback(completion) { + @Override + public void fail(ErrorCode err) { + setHostKeyIdentityVerified(hostUuid, false); + completion.success(); + } + + @Override + public void success(KVMAgentCommands.RotatePublicKeyResponse rotateRsp) { + if (rotateRsp == null || !rotateRsp.isSuccess()) { + logger.warn("verify failed then rotate key on agent failed for host " + hostUuid + ": " + (rotateRsp != null ? rotateRsp.getError() : "null")); + setHostKeyIdentityVerified(hostUuid, false); + completion.success(); + return; + } + String getUrl = buildUrl(KVMConstant.KVM_GET_ENVELOPE_KEY_PATH); + restf.asyncJsonPost(getUrl, new KVMAgentCommands.GetPublicKeyCmd(), + new JsonAsyncRESTCallback(completion) { + @Override + public void fail(ErrorCode err) { + setHostKeyIdentityVerified(hostUuid, false); + completion.success(); + } + + @Override + public void success(KVMAgentCommands.GetPublicKeyResponse getRsp) { + if (getRsp != null && getRsp.isSuccess() && StringUtils.isNotBlank(getRsp.getPublicKey())) { + saveOrUpdateHostKeyIdentity(hostUuid, getRsp.getPublicKey().trim(), true); + } else { + setHostKeyIdentityVerified(hostUuid, false); + } + completion.success(); + } + + @Override + public Class getReturnClass() { + return KVMAgentCommands.GetPublicKeyResponse.class; + } + }, TimeUnit.SECONDS, ENVELOPE_KEY_HTTP_TIMEOUT_SEC); + } + + @Override + public Class getReturnClass() { + return KVMAgentCommands.RotatePublicKeyResponse.class; + } + }, TimeUnit.SECONDS, ENVELOPE_KEY_HTTP_TIMEOUT_SEC); + return; + } + setHostKeyIdentityVerified(hostUuid, false); + completion.success(); + } + + @Override + public Class getReturnClass() { + return KVMAgentCommands.VerifyPublicKeyResponse.class; + } + }, TimeUnit.SECONDS, ENVELOPE_KEY_HTTP_TIMEOUT_SEC); return; } String createUrl = buildUrl(KVMConstant.KVM_CREATE_ENVELOPE_KEY_PATH); - KVMAgentCommands.CreatePublicKeyResponse createRsp = restf.syncJsonPost(createUrl, - new KVMAgentCommands.CreatePublicKeyCmd(), KVMAgentCommands.CreatePublicKeyResponse.class); - if (!createRsp.isSuccess()) { - logger.warn("create key on agent failed for host " + hostUuid + ": " + createRsp.getError()); - setHostKeyIdentityVerified(hostUuid, false); - return; - } - String getUrl = buildUrl(KVMConstant.KVM_GET_ENVELOPE_KEY_PATH); - KVMAgentCommands.GetPublicKeyResponse getRsp = restf.syncJsonPost(getUrl, - new KVMAgentCommands.GetPublicKeyCmd(), KVMAgentCommands.GetPublicKeyResponse.class); - if (getRsp.isSuccess() && StringUtils.isNotBlank(getRsp.getPublicKey())) { - saveOrUpdateHostKeyIdentity(hostUuid, getRsp.getPublicKey().trim(), true); - return; - } - if (!getRsp.isSuccess() && isRotateNeededGetError(getRsp.getErrorCode())) { - String rotateUrl = buildUrl(KVMConstant.KVM_ROTATE_ENVELOPE_KEY_PATH); - KVMAgentCommands.RotatePublicKeyResponse rotateRsp = restf.syncJsonPost(rotateUrl, - new KVMAgentCommands.RotatePublicKeyCmd(), KVMAgentCommands.RotatePublicKeyResponse.class); - if (!rotateRsp.isSuccess()) { - logger.warn("rotate key on agent failed for host " + hostUuid + ": " + rotateRsp.getError()); - setHostKeyIdentityVerified(hostUuid, false); - return; - } - getRsp = restf.syncJsonPost(getUrl, new KVMAgentCommands.GetPublicKeyCmd(), KVMAgentCommands.GetPublicKeyResponse.class); - if (getRsp.isSuccess() && StringUtils.isNotBlank(getRsp.getPublicKey())) { - saveOrUpdateHostKeyIdentity(hostUuid, getRsp.getPublicKey().trim(), true); - return; - } - } - logger.warn("get public key from agent failed for host " + hostUuid + ": " + (getRsp != null ? getRsp.getError() : "null")); - setHostKeyIdentityVerified(hostUuid, false); + restf.asyncJsonPost(createUrl, new KVMAgentCommands.CreatePublicKeyCmd(), + new JsonAsyncRESTCallback(completion) { + @Override + public void fail(ErrorCode err) { + logger.warn("create key on agent failed for host " + hostUuid + ": " + (err != null ? err.getDetails() : "")); + setHostKeyIdentityVerified(hostUuid, false); + completion.success(); + } + + @Override + public void success(KVMAgentCommands.CreatePublicKeyResponse createRsp) { + if (createRsp == null || !createRsp.isSuccess()) { + logger.warn("create key on agent failed for host " + hostUuid + ": " + (createRsp != null ? createRsp.getError() : "null")); + setHostKeyIdentityVerified(hostUuid, false); + } + completion.success(); + } + + @Override + public Class getReturnClass() { + return KVMAgentCommands.CreatePublicKeyResponse.class; + } + }, TimeUnit.SECONDS, ENVELOPE_KEY_HTTP_TIMEOUT_SEC); } catch (Exception e) { logger.warn("sync secret key after connect failed for host " + hostUuid + ": " + e.getMessage()); try { setHostKeyIdentityVerified(hostUuid, false); } catch (Exception ignored) { } + completion.success(); } } @@ -5576,6 +5635,8 @@ private void saveOrUpdateHostKeyIdentity(String hostUuid, String publicKey, bool dbf.update(vo); } + private static final int MAX_DEK_BYTES = 1024; + private void handle(SecretHostDefineMsg msg) { SecretHostDefineReply reply = new SecretHostDefineReply(); if (org.apache.commons.lang.StringUtils.isBlank(msg.getDekBase64())) { @@ -5583,6 +5644,11 @@ private void handle(SecretHostDefineMsg msg) { bus.reply(msg, reply); return; } + if (StringUtils.isBlank(msg.getVmUuid()) || StringUtils.isBlank(msg.getPurpose()) || StringUtils.isBlank(msg.getProviderName())) { + reply.setError(operr("vmUuid, purpose and providerName are required for ensure secret")); + bus.reply(msg, reply); + return; + } String hostUuid = getSelf().getUuid(); HostKeyIdentityVO identity = getHostKeyIdentity(hostUuid); String pubKey = identity != null ? org.apache.commons.lang.StringUtils.trimToNull(identity.getPublicKey()) : null; @@ -5611,6 +5677,12 @@ private void handle(SecretHostDefineMsg msg) { return; } + if (dekRaw.length > MAX_DEK_BYTES) { + reply.setError(operr("dekBase64 decoded payload is too large")); + bus.reply(msg, reply); + return; + } + byte[] pubKeyBytes; try { pubKeyBytes = java.util.Base64.getDecoder().decode(pubKey); @@ -5633,67 +5705,40 @@ private void handle(SecretHostDefineMsg msg) { return; } String envelopeDekBase64 = java.util.Base64.getEncoder().encodeToString(envelope); - try { - String url = buildUrl(KVMConstant.KVM_ENSURE_SECRET_PATH); - KVMAgentCommands.SecretHostDefineCmd cmd = new KVMAgentCommands.SecretHostDefineCmd(); - cmd.setEnvelopeDekBase64(envelopeDekBase64); - KVMAgentCommands.SecretHostDefineResponse rsp = restf.syncJsonPost(url, cmd, KVMAgentCommands.SecretHostDefineResponse.class); - if (rsp.isSuccess()) { - if (rsp.getSecretUuid() != null) { - reply.setSecretUuid(rsp.getSecretUuid()); - } + String url = buildUrl(KVMConstant.KVM_ENSURE_SECRET_PATH); + KVMAgentCommands.SecretHostDefineCmd cmd = new KVMAgentCommands.SecretHostDefineCmd(); + cmd.setEncryptedDek(envelopeDekBase64); + cmd.setVmUuid(msg.getVmUuid()); + cmd.setPurpose(msg.getPurpose()); + cmd.setProviderName(msg.getProviderName()); + cmd.setDescription(msg.getDescription() != null ? msg.getDescription() : ""); + restf.asyncJsonPost(url, cmd, new JsonAsyncRESTCallback(msg, reply) { + @Override + public void fail(ErrorCode err) { + reply.setError(err != null ? err : operr("ensure secret on agent failed")); bus.reply(msg, reply); - return; } - if (isRotateNeededGetError(rsp.getErrorCode())) { - String rotateUrl = buildUrl(KVMConstant.KVM_ROTATE_ENVELOPE_KEY_PATH); - KVMAgentCommands.RotatePublicKeyResponse rotateRsp = restf.syncJsonPost(rotateUrl, - new KVMAgentCommands.RotatePublicKeyCmd(), KVMAgentCommands.RotatePublicKeyResponse.class); - if (!rotateRsp.isSuccess()) { - reply.setError(operr("ensure secret failed, rotate key then retry failed: %s", rotateRsp.getError())); - bus.reply(msg, reply); - return; - } - String getUrl = buildUrl(KVMConstant.KVM_GET_ENVELOPE_KEY_PATH); - KVMAgentCommands.GetPublicKeyResponse getRsp = restf.syncJsonPost(getUrl, - new KVMAgentCommands.GetPublicKeyCmd(), KVMAgentCommands.GetPublicKeyResponse.class); - if (!getRsp.isSuccess() || StringUtils.isBlank(getRsp.getPublicKey())) { - reply.setError(operr("ensure secret failed, rotate then get public key failed: %s", - getRsp != null ? getRsp.getError() : "null")); - bus.reply(msg, reply); - return; - } - saveOrUpdateHostKeyIdentity(hostUuid, getRsp.getPublicKey().trim(), true); - String newPubKey = getRsp.getPublicKey().trim(); - byte[] newPubKeyBytes = java.util.Base64.getDecoder().decode(newPubKey); - byte[] newEnvelope; - try { - newEnvelope = HostSecretEnvelopeCrypto.seal(newPubKeyBytes, dekRaw); - } catch (org.bouncycastle.crypto.InvalidCipherTextException e) { - reply.setError(operr("ensure secret failed after rotate, HPKE seal failed: %s", e.getMessage())); - bus.reply(msg, reply); - return; - } - String newEnvelopeDekBase64 = java.util.Base64.getEncoder().encodeToString(newEnvelope); - cmd.setEnvelopeDekBase64(newEnvelopeDekBase64); - rsp = restf.syncJsonPost(url, cmd, KVMAgentCommands.SecretHostDefineResponse.class); - if (rsp.isSuccess()) { + + @Override + public void success(KVMAgentCommands.SecretHostDefineResponse rsp) { + if (rsp != null && rsp.isSuccess()) { if (rsp.getSecretUuid() != null) { reply.setSecretUuid(rsp.getSecretUuid()); } - bus.reply(msg, reply); - return; + } else { + reply.setError(operr(rsp != null ? rsp.getError() : "ensure secret failed")); + if (rsp != null && rsp.getErrorCode() != null) { + reply.setErrorCode(rsp.getErrorCode()); + } } + bus.reply(msg, reply); } - reply.setError(operr(rsp.getError())); - if (rsp.getErrorCode() != null) { - reply.setErrorCode(rsp.getErrorCode()); + + @Override + public Class getReturnClass() { + return KVMAgentCommands.SecretHostDefineResponse.class; } - bus.reply(msg, reply); - } catch (RestClientException e) { - reply.setError(operr("ensure secret on agent failed: %s", e.getMessage())); - bus.reply(msg, reply); - } + }, TimeUnit.SECONDS, ENVELOPE_KEY_HTTP_TIMEOUT_SEC); } @Override From c64e7941df7c232ddec943e716306669a9f11c41 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Sat, 7 Mar 2026 19:57:13 +0800 Subject: [PATCH 017/122] [header]: Hide dek in log * Remove test point in stopVm * Improve HostSecretCase test * Compute fingerprint of public key * Verify fingerprint in secret define * Remove secret host get/create extension point * Fix primary key confict error * Use UTF-8 instead HPKE default charset Related: ZSV-0 Change-Id: I7578756666697767616e66636e65667465766363 --- .../secret/SecretCreateExtensionPoint.java | 19 -- .../secret/SecretGetExtensionPoint.java | 19 -- .../header/secret/SecretHostDefineMsg.java | 2 + .../zstack/kvm/HostSecretEnvelopeCrypto.java | 14 +- .../src/main/java/org/zstack/kvm/KVMHost.java | 196 +++++++++++------- .../kvm/host/HostSecretCase.groovy | 108 +++++++++- 6 files changed, 238 insertions(+), 120 deletions(-) delete mode 100644 header/src/main/java/org/zstack/header/secret/SecretCreateExtensionPoint.java delete mode 100644 header/src/main/java/org/zstack/header/secret/SecretGetExtensionPoint.java diff --git a/header/src/main/java/org/zstack/header/secret/SecretCreateExtensionPoint.java b/header/src/main/java/org/zstack/header/secret/SecretCreateExtensionPoint.java deleted file mode 100644 index 7c8ab192ecd..00000000000 --- a/header/src/main/java/org/zstack/header/secret/SecretCreateExtensionPoint.java +++ /dev/null @@ -1,19 +0,0 @@ - -package org.zstack.header.secret; - -import org.zstack.header.core.ReturnValueCompletion; - -/** - * Extension point for creating a secret in key-manager (e.g. premium with NKP/KMS). - * Used for VM (e.g. vTPM at VM create). Premium implements with key-manager create; - * success returns secretId/name for later get. - */ -public interface SecretCreateExtensionPoint { - /** - * Create a secret. Implementation (e.g. premium) calls key-manager create. - * - * @param secretName name or identifier for the secret - * @param completion success(secretIdOrName) for later get, or fail(error) - */ - void createSecret(String secretName, ReturnValueCompletion completion); -} diff --git a/header/src/main/java/org/zstack/header/secret/SecretGetExtensionPoint.java b/header/src/main/java/org/zstack/header/secret/SecretGetExtensionPoint.java deleted file mode 100644 index 78b28283ab9..00000000000 --- a/header/src/main/java/org/zstack/header/secret/SecretGetExtensionPoint.java +++ /dev/null @@ -1,19 +0,0 @@ - -package org.zstack.header.secret; - -import org.zstack.header.core.ReturnValueCompletion; - -/** - * Extension point for getting plaintext DEK from key-manager (e.g. premium with NKP/KMS). - * Used for VM (e.g. to send DEK to host via SecretHostDefineMsg). Premium implements with - * key-manager get; success returns dekBase64 (plaintext DEK, base64). - */ -public interface SecretGetExtensionPoint { - /** - * Get plaintext DEK. Implementation (e.g. premium) calls key-manager get. - * - * @param secretNameOrId secret name or id (from create or stored) - * @param completion success(dekBase64) with plaintext DEK in base64, or fail(error) - */ - void getSecret(String secretNameOrId, ReturnValueCompletion completion); -} diff --git a/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java b/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java index 4305af89491..c7280743dc0 100644 --- a/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java +++ b/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java @@ -1,6 +1,7 @@ package org.zstack.header.secret; import org.zstack.header.host.HostMessage; +import org.zstack.header.log.NoLogging; import org.zstack.header.message.NeedReplyMessage; /** @@ -10,6 +11,7 @@ */ public class SecretHostDefineMsg extends NeedReplyMessage implements HostMessage { private String hostUuid; + @NoLogging(type = NoLogging.Type.Simple) private String dekBase64; private String vmUuid; private String purpose; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCrypto.java b/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCrypto.java index acaca72d4f1..394bdc6933e 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCrypto.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCrypto.java @@ -29,13 +29,17 @@ public final class HostSecretEnvelopeCrypto { private static final String HPKE_V1 = "HPKE-v1"; /** HPKE application info; must match key-agent main.go: info := []byte("key-agent hpke info") */ private static final byte[] HPKE_INFO = "key-agent hpke info".getBytes(StandardCharsets.UTF_8); - private static final byte[] KEM_ID = new byte[]{0x00, 0x20}; // X25519 HKDF-SHA256 + /** RFC 9180 / IANA HPKE: KEM_ID 0x0020 = DHKEM(X25519, HKDF-SHA256); Nenc = Npk = 32. */ + private static final byte[] KEM_ID = new byte[]{0x00, 0x20}; private static final byte[] KDF_ID = new byte[]{0x00, 0x01}; // HKDF-SHA256 private static final byte[] AEAD_ID = new byte[]{0x00, 0x02}; // AES-256-GCM - private static final byte[] KEM_SUITE_ID = concat("KEM".getBytes(), KEM_ID); // for DHKEM ExtractAndExpand - private static final byte[] SUITE_ID = concat(concat("HPKE".getBytes(), KEM_ID), concat(KDF_ID, AEAD_ID)); + private static final byte[] KEM_SUITE_ID = concat("KEM".getBytes(StandardCharsets.UTF_8), KEM_ID); + private static final byte[] SUITE_ID = concat(concat("HPKE".getBytes(StandardCharsets.UTF_8), KEM_ID), concat(KDF_ID, AEAD_ID)); + /** Nh: KDF output size (SHA-256 = 32). RFC 9180 Section 4. */ private static final int NH = 32; + /** Nk: AEAD key size (AES-256 = 32). RFC 9180 Section 7.3. */ private static final int NK = 32; + /** Nn: AEAD nonce size (AES-GCM = 12). RFC 9180 Section 7.3. */ private static final int NN = 12; private static byte[] concat(byte[] a, byte[] b) { @@ -57,14 +61,14 @@ private static byte[] i2osp(int n, int w) { /** RFC 9180 LabeledExtract(salt, label, ikm); use kemSuiteId for KEM layer, null for HPKE layer (uses SUITE_ID). */ private static byte[] labeledExtract(byte[] salt, String label, byte[] ikm, Digest digest, byte[] suiteId) { byte[] sid = suiteId != null ? suiteId : SUITE_ID; - byte[] labeledIkm = concat(concat(concat(HPKE_V1.getBytes(), sid), label.getBytes()), ikm != null ? ikm : new byte[0]); + byte[] labeledIkm = concat(concat(concat(HPKE_V1.getBytes(StandardCharsets.UTF_8), sid), label.getBytes(StandardCharsets.UTF_8)), ikm != null ? ikm : new byte[0]); return hkdfExtract(salt, labeledIkm, digest); } /** RFC 9180 LabeledExpand(prk, label, info, L); use kemSuiteId for KEM layer, null for HPKE layer. */ private static byte[] labeledExpand(byte[] prk, String label, byte[] info, int L, Digest digest, byte[] suiteId) { byte[] sid = suiteId != null ? suiteId : SUITE_ID; - byte[] labeledInfo = concat(concat(concat(concat(i2osp(L, 2), HPKE_V1.getBytes()), sid), label.getBytes()), info != null ? info : new byte[0]); + byte[] labeledInfo = concat(concat(concat(concat(i2osp(L, 2), HPKE_V1.getBytes(StandardCharsets.UTF_8)), sid), label.getBytes(StandardCharsets.UTF_8)), info != null ? info : new byte[0]); return hkdfExpand(prk, labeledInfo, L, digest); } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index 62cc3e1046d..9d07988b18f 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -116,6 +116,9 @@ import javax.persistence.TypedQuery; import java.io.IOException; import java.net.URI; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; import java.net.URISyntaxException; import java.util.*; import java.util.concurrent.TimeUnit; @@ -4106,26 +4109,6 @@ protected void stopVm(final StopVmOnHypervisorMsg msg, final NoErrorCompletion c checkStatus(); final VmInstanceInventory vminv = msg.getVmInventory(); - { - String dekBase64 = "dGVzdERFSw=="; - SecretHostDefineMsg defineMsg = new SecretHostDefineMsg(); - defineMsg.setHostUuid(getSelf().getUuid()); - defineMsg.setDekBase64(dekBase64); - defineMsg.setVmUuid(vminv.getUuid()); - defineMsg.setPurpose("vm"); - defineMsg.setProviderName("zstack"); - bus.makeTargetServiceIdByResourceUuid(defineMsg, HostConstant.SERVICE_ID, getSelf().getUuid()); - MessageReply defineReply = bus.call(defineMsg); - if (!defineReply.isSuccess()) { - logger.warn(String.format("debug SecretDefine before stop vm[uuid:%s] failed: %s", vminv.getUuid(), defineReply.getError())); - } else { - SecretHostDefineReply srep = defineReply.castReply(); - if (srep != null && srep.getSecretUuid() != null) { - logger.info(String.format("debug SecretDefine before stop vm[uuid:%s] success, secretUuid=%s", vminv.getUuid(), srep.getSecretUuid())); - } - } - } - StopVmCmd cmd = new StopVmCmd(); cmd.setUuid(vminv.getUuid()); cmd.setType(msg.getType()); @@ -5476,6 +5459,57 @@ public void handle(ErrorCode errCode, Map data) { private static final long ENVELOPE_KEY_HTTP_TIMEOUT_SEC = 5L; + private void doRotateAndGetThenSave(String hostUuid, Completion completion) { + String rotateUrl = buildUrl(KVMConstant.KVM_ROTATE_ENVELOPE_KEY_PATH); + restf.asyncJsonPost(rotateUrl, new KVMAgentCommands.RotatePublicKeyCmd(), + new JsonAsyncRESTCallback(completion) { + @Override + public void fail(ErrorCode err) { + setHostKeyIdentityVerified(hostUuid, false); + completion.success(); + } + + @Override + public void success(KVMAgentCommands.RotatePublicKeyResponse rotateRsp) { + if (rotateRsp == null || !rotateRsp.isSuccess()) { + logger.warn("rotate key on agent failed for host " + hostUuid + ": " + (rotateRsp != null ? rotateRsp.getError() : "null")); + setHostKeyIdentityVerified(hostUuid, false); + completion.success(); + return; + } + String getUrl = buildUrl(KVMConstant.KVM_GET_ENVELOPE_KEY_PATH); + restf.asyncJsonPost(getUrl, new KVMAgentCommands.GetPublicKeyCmd(), + new JsonAsyncRESTCallback(completion) { + @Override + public void fail(ErrorCode err) { + setHostKeyIdentityVerified(hostUuid, false); + completion.success(); + } + + @Override + public void success(KVMAgentCommands.GetPublicKeyResponse getRsp) { + if (getRsp != null && getRsp.isSuccess() && StringUtils.isNotBlank(getRsp.getPublicKey())) { + saveOrUpdateHostKeyIdentity(hostUuid, getRsp.getPublicKey().trim(), true); + } else { + setHostKeyIdentityVerified(hostUuid, false); + } + completion.success(); + } + + @Override + public Class getReturnClass() { + return KVMAgentCommands.GetPublicKeyResponse.class; + } + }, TimeUnit.SECONDS, ENVELOPE_KEY_HTTP_TIMEOUT_SEC); + } + + @Override + public Class getReturnClass() { + return KVMAgentCommands.RotatePublicKeyResponse.class; + } + }, TimeUnit.SECONDS, ENVELOPE_KEY_HTTP_TIMEOUT_SEC); + } + private void syncEnvelopeKeyAfterPing(Completion completion) { KVMHostVO kvo = dbf.reload(getSelf()); final String hostUuid = kvo.getUuid(); @@ -5494,59 +5528,21 @@ public void fail(ErrorCode err) { @Override public void success(KVMAgentCommands.VerifyPublicKeyResponse vrsp) { if (vrsp != null && vrsp.isSuccess()) { + String storedFp = identity.getFingerprint(); + if (StringUtils.isNotBlank(storedFp)) { + String computed = fingerprintFromPublicKey(identity.getPublicKey()); + if (!storedFp.equals(computed)) { + logger.warn("host " + hostUuid + " verify ok but fingerprint mismatch, rotating and re-getting key"); + doRotateAndGetThenSave(hostUuid, completion); + return; + } + } setHostKeyIdentityVerified(hostUuid, true); completion.success(); return; } if (vrsp != null && !vrsp.isSuccess() && isRotateNeededGetError(vrsp.getErrorCode())) { - String rotateUrl = buildUrl(KVMConstant.KVM_ROTATE_ENVELOPE_KEY_PATH); - restf.asyncJsonPost(rotateUrl, new KVMAgentCommands.RotatePublicKeyCmd(), - new JsonAsyncRESTCallback(completion) { - @Override - public void fail(ErrorCode err) { - setHostKeyIdentityVerified(hostUuid, false); - completion.success(); - } - - @Override - public void success(KVMAgentCommands.RotatePublicKeyResponse rotateRsp) { - if (rotateRsp == null || !rotateRsp.isSuccess()) { - logger.warn("verify failed then rotate key on agent failed for host " + hostUuid + ": " + (rotateRsp != null ? rotateRsp.getError() : "null")); - setHostKeyIdentityVerified(hostUuid, false); - completion.success(); - return; - } - String getUrl = buildUrl(KVMConstant.KVM_GET_ENVELOPE_KEY_PATH); - restf.asyncJsonPost(getUrl, new KVMAgentCommands.GetPublicKeyCmd(), - new JsonAsyncRESTCallback(completion) { - @Override - public void fail(ErrorCode err) { - setHostKeyIdentityVerified(hostUuid, false); - completion.success(); - } - - @Override - public void success(KVMAgentCommands.GetPublicKeyResponse getRsp) { - if (getRsp != null && getRsp.isSuccess() && StringUtils.isNotBlank(getRsp.getPublicKey())) { - saveOrUpdateHostKeyIdentity(hostUuid, getRsp.getPublicKey().trim(), true); - } else { - setHostKeyIdentityVerified(hostUuid, false); - } - completion.success(); - } - - @Override - public Class getReturnClass() { - return KVMAgentCommands.GetPublicKeyResponse.class; - } - }, TimeUnit.SECONDS, ENVELOPE_KEY_HTTP_TIMEOUT_SEC); - } - - @Override - public Class getReturnClass() { - return KVMAgentCommands.RotatePublicKeyResponse.class; - } - }, TimeUnit.SECONDS, ENVELOPE_KEY_HTTP_TIMEOUT_SEC); + doRotateAndGetThenSave(hostUuid, completion); return; } setHostKeyIdentityVerified(hostUuid, false); @@ -5575,8 +5571,34 @@ public void success(KVMAgentCommands.CreatePublicKeyResponse createRsp) { if (createRsp == null || !createRsp.isSuccess()) { logger.warn("create key on agent failed for host " + hostUuid + ": " + (createRsp != null ? createRsp.getError() : "null")); setHostKeyIdentityVerified(hostUuid, false); + completion.success(); + return; } - completion.success(); + String getUrl = buildUrl(KVMConstant.KVM_GET_ENVELOPE_KEY_PATH); + restf.asyncJsonPost(getUrl, new KVMAgentCommands.GetPublicKeyCmd(), + new JsonAsyncRESTCallback(completion) { + @Override + public void fail(ErrorCode err) { + logger.warn("get public key after create failed for host " + hostUuid + ": " + (err != null ? err.getDetails() : "")); + setHostKeyIdentityVerified(hostUuid, false); + completion.success(); + } + + @Override + public void success(KVMAgentCommands.GetPublicKeyResponse getRsp) { + if (getRsp != null && getRsp.isSuccess() && StringUtils.isNotBlank(getRsp.getPublicKey())) { + saveOrUpdateHostKeyIdentity(hostUuid, getRsp.getPublicKey().trim(), true); + } else { + setHostKeyIdentityVerified(hostUuid, false); + } + completion.success(); + } + + @Override + public Class getReturnClass() { + return KVMAgentCommands.GetPublicKeyResponse.class; + } + }, TimeUnit.SECONDS, ENVELOPE_KEY_HTTP_TIMEOUT_SEC); } @Override @@ -5614,23 +5636,50 @@ private HostKeyIdentityVO getHostKeyIdentity(String hostUuid) { return q.find(); } + /** + * Compute fingerprint from public key (base64): SHA-256 of decoded key bytes, hex-encoded. + * Returns empty string if key is invalid or hashing fails. + */ + private static String fingerprintFromPublicKey(String publicKeyBase64) { + if (publicKeyBase64 == null || publicKeyBase64.isEmpty()) { + return ""; + } + try { + byte[] keyBytes = Base64.getDecoder().decode(publicKeyBase64.trim()); + if (keyBytes == null || keyBytes.length == 0) { + return ""; + } + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] hash = md.digest(keyBytes); + StringBuilder sb = new StringBuilder(hash.length * 2); + for (byte b : hash) { + sb.append(String.format("%02x", b & 0xff)); + } + return sb.toString(); + } catch (IllegalArgumentException | NoSuchAlgorithmException e) { + return ""; + } + } + private void saveOrUpdateHostKeyIdentity(String hostUuid, String publicKey, boolean verified) { if (StringUtils.isBlank(publicKey)) { return; } String keyToSave = publicKey.trim(); + String fingerprint = fingerprintFromPublicKey(keyToSave); HostKeyIdentityVO vo = getHostKeyIdentity(hostUuid); if (vo == null) { vo = new HostKeyIdentityVO(); vo.setHostUuid(hostUuid); vo.setPublicKey(keyToSave); - vo.setFingerprint(""); + vo.setFingerprint(fingerprint); vo.setVerified(verified); vo.setCreateDate(new java.sql.Timestamp(System.currentTimeMillis())); dbf.persist(vo); return; } vo.setPublicKey(keyToSave); + vo.setFingerprint(fingerprint); vo.setVerified(verified); dbf.update(vo); } @@ -5658,6 +5707,13 @@ private void handle(SecretHostDefineMsg msg) { bus.reply(msg, reply); return; } + String storedFingerprint = identity.getFingerprint(); + String computed = fingerprintFromPublicKey(pubKey); + if (!storedFingerprint.equals(computed)) { + reply.setError(operr("host public key fingerprint mismatch, key may be corrupted or tampered")); + bus.reply(msg, reply); + return; + } if (!Boolean.TRUE.equals(verifyOk)) { reply.setError(operr("host secret key verify not ok, not synced")); bus.reply(msg, reply); diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretCase.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretCase.groovy index 86708eba7d4..afcdd213a77 100644 --- a/test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretCase.groovy @@ -2,21 +2,32 @@ package org.zstack.test.integration.kvm.host import org.zstack.core.Platform import org.zstack.core.cloudbus.CloudBus +import org.zstack.core.db.DatabaseFacade +import org.zstack.core.db.SimpleQuery +import org.zstack.core.db.SimpleQuery.Op import org.zstack.header.host.AddHostReply import org.zstack.header.host.HostConstant import org.zstack.header.host.HostInventory +import org.zstack.header.host.HostKeyIdentityVO +import org.zstack.header.host.HostKeyIdentityVO_ import org.zstack.header.host.HostStatus +import org.zstack.header.host.PingHostMsg +import org.zstack.header.host.PingHostReply import org.zstack.header.message.MessageReply import org.zstack.kvm.AddKVMHostMsg import org.zstack.kvm.KVMConstant import org.zstack.kvm.KVMAgentCommands import org.zstack.storage.primary.local.LocalStorageKvmBackend import org.zstack.test.integration.kvm.KvmTest +import org.springframework.http.HttpEntity import org.zstack.testlib.EnvSpec import org.zstack.testlib.SubCase import org.zstack.header.secret.SecretHostDefineMsg import org.zstack.header.secret.SecretHostDefineReply +import java.security.MessageDigest +import java.util.concurrent.atomic.AtomicInteger + /** * Integration test for host secret: create/get/rotate/verify public key on connect, * and SecretHostDefine (ensure secret on agent). @@ -28,9 +39,29 @@ class HostSecretCase extends SubCase { CloudBus bus HostInventory addedHost + /** Counters for simulator call assertions (async secret sync / ensureSecret). */ + AtomicInteger createEnvelopeKeyCallCount + AtomicInteger ensureSecretCallCount + /** 32-byte X25519 public key (base64) for simulator; must be valid for HPKE seal. */ static final String MOCK_PUBLIC_KEY_BASE64 = "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA=" + /** Same algorithm as KVMHost.fingerprintFromPublicKey: SHA-256(decoded base64) in hex. */ + static String fingerprintFromPublicKey(String publicKeyBase64) { + if (publicKeyBase64 == null || publicKeyBase64.isEmpty()) return "" + try { + byte[] keyBytes = java.util.Base64.getDecoder().decode(publicKeyBase64.trim()) + if (keyBytes == null || keyBytes.length == 0) return "" + MessageDigest md = MessageDigest.getInstance("SHA-256") + byte[] hash = md.digest(keyBytes) + StringBuilder sb = new StringBuilder(hash.length * 2) + for (byte b : hash) sb.append(String.format("%02x", b & 0xff)) + return sb.toString() + } catch (Exception e) { + return "" + } + } + @Override void setup() { useSpring(KvmTest.springSpec) @@ -62,6 +93,9 @@ class HostSecretCase extends SubCase { } void registerSecretSimulators() { + createEnvelopeKeyCallCount = new AtomicInteger(0) + ensureSecretCallCount = new AtomicInteger(0) + env.simulator(KVMConstant.KVM_CONNECT_PATH) { def rsp = new KVMAgentCommands.ConnectResponse() rsp.success = true @@ -69,15 +103,36 @@ class HostSecretCase extends SubCase { rsp.qemuVersion = "1.3.0" return rsp } - env.simulator(KVMConstant.KVM_HOST_FACT_PATH) { - def rsp = new KVMAgentCommands.HostFactResponse() - rsp.osDistribution = "CentOS" - rsp.osVersion = "7.0" + // Use afterSimulator like AddHostCase: rely on testlib default HostFactResponse, only set what this test needs. + env.afterSimulator(KVMConstant.KVM_HOST_FACT_PATH) { KVMAgentCommands.HostFactResponse rsp -> + rsp.hvmCpuFlag = "vmx" // default is ""; connect needs vmx/svm to pass checkVirtualizationEnabled + return rsp + } + env.simulator(LocalStorageKvmBackend.INIT_PATH) { HttpEntity e -> + def rsp = new LocalStorageKvmBackend.InitRsp() + rsp.success = true + rsp.localStorageUsedCapacity = 0L + rsp.totalCapacity = 0L + rsp.availableCapacity = 0L + return rsp + } + + // Ping simulator so we can trigger pingHook (which runs sync-envelope-public-key -> KVM_CREATE_ENVELOPE_KEY_PATH). + // needReconnectHost() is true when rsp.version != dbf.getDbVersion(), which sets KVM_HOST_SKIP_PING_NO_FAILURE_EXTENSIONS + // and skips sync-envelope-public-key; so we must return the actual DB version. + def dbVersion = bean(DatabaseFacade.class).getDbVersion() + env.simulator(KVMConstant.KVM_PING_PATH) { HttpEntity e -> + def cmd = org.zstack.utils.gson.JSONObjectUtil.toObject(e.body, KVMAgentCommands.PingCmd.class) + def rsp = new KVMAgentCommands.PingResponse() + rsp.success = true + rsp.hostUuid = cmd.hostUuid + rsp.version = dbVersion + rsp.sendCommandUrl = "http://127.0.0.2:7272" return rsp } - env.simulator(LocalStorageKvmBackend.INIT_PATH) { rsp, _ -> return rsp } env.simulator(KVMConstant.KVM_CREATE_ENVELOPE_KEY_PATH) { + createEnvelopeKeyCallCount?.incrementAndGet() return new KVMAgentCommands.CreatePublicKeyResponse() } env.simulator(KVMConstant.KVM_GET_ENVELOPE_KEY_PATH) { @@ -92,6 +147,7 @@ class HostSecretCase extends SubCase { return new KVMAgentCommands.RotatePublicKeyResponse() } env.simulator(KVMConstant.KVM_ENSURE_SECRET_PATH) { + ensureSecretCallCount?.incrementAndGet() def rsp = new KVMAgentCommands.SecretHostDefineResponse() rsp.secretUuid = Platform.uuid return rsp @@ -113,17 +169,52 @@ class HostSecretCase extends SubCase { bus.makeLocalServiceId(amsg, HostConstant.SERVICE_ID) AddHostReply reply = (AddHostReply) bus.call(amsg) assert reply != null - assert reply.isSuccess() + assert reply.isSuccess() : "AddHost failed: ${reply.error?.toString() ?: 'no error'}" assert reply.inventory.status == HostStatus.Connected.toString() addedHost = reply.inventory + + // Envelope key sync runs inside pingHook, not during connect. Trigger a ping so that + // sync-envelope-public-key runs and KVM_CREATE_ENVELOPE_KEY_PATH is invoked. + PingHostMsg pingMsg = new PingHostMsg() + pingMsg.hostUuid = addedHost.uuid + bus.makeTargetServiceIdByResourceUuid(pingMsg, HostConstant.SERVICE_ID, addedHost.uuid) + MessageReply pingReply = bus.call(pingMsg) + assert pingReply.isSuccess() : "PingHost failed: ${pingReply.error}" + + assert createEnvelopeKeyCallCount.get() >= 1 : "envelope key sync (KVM_CREATE_ENVELOPE_KEY_PATH) should be triggered at least once after add host" + + // Create/ping may already persist HostKeyIdentityVO (sync path calls GET then saveOrUpdateHostKeyIdentity). + // Ensure HostKeyIdentity exists with expected key so SecretHostDefineMsg finds it and fingerprint check passes. + DatabaseFacade dbf = bean(DatabaseFacade.class) + SimpleQuery q = dbf.createQuery(HostKeyIdentityVO.class) + q.add(HostKeyIdentityVO_.hostUuid, Op.EQ, addedHost.uuid) + HostKeyIdentityVO keyVo = q.find() + if (keyVo == null) { + keyVo = new HostKeyIdentityVO() + keyVo.hostUuid = addedHost.uuid + keyVo.publicKey = MOCK_PUBLIC_KEY_BASE64 + keyVo.fingerprint = fingerprintFromPublicKey(MOCK_PUBLIC_KEY_BASE64) + keyVo.verified = true + dbf.persist(keyVo) + } else { + keyVo.publicKey = MOCK_PUBLIC_KEY_BASE64 + keyVo.fingerprint = fingerprintFromPublicKey(MOCK_PUBLIC_KEY_BASE64) + keyVo.verified = true + dbf.update(keyVo) + } } void testSecretHostDefineSuccess() { assert addedHost != null + int countBefore = ensureSecretCallCount.get() + SecretHostDefineMsg msg = new SecretHostDefineMsg() msg.hostUuid = addedHost.uuid - msg.dekBase64 = "dGVzdERFSw==" // base64 of "testDEK" + msg.dekBase64 = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + msg.vmUuid = Platform.uuid + msg.purpose = "test-vtpm" + msg.providerName = "vtpm" bus.makeTargetServiceIdByResourceUuid(msg, HostConstant.SERVICE_ID, addedHost.uuid) MessageReply reply = bus.call(msg) @@ -131,6 +222,9 @@ class HostSecretCase extends SubCase { assert reply.isSuccess() SecretHostDefineReply defineReply = reply.castReply() assert defineReply.secretUuid != null + + // Ensure KVM_ENSURE_SECRET_PATH was actually called (asyncJsonPost to agent). + assert ensureSecretCallCount.get() == countBefore + 1 : "KVM_ENSURE_SECRET_PATH simulator should be called exactly once for SecretHostDefineMsg" } void testSecretHostDefineFailWhenNoDek() { From 637ff5c7249e5412d4ad48c3074fefd6a8c5ac93 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 10 Mar 2026 15:59:11 +0800 Subject: [PATCH 018/122] [kvm]: fix swtpm path error Modified the ``buildTpmStateFilePath`` method in ``KVMConstant.java`` to add normalization processing for the VM UUID. This converts it to a hyphenated format before using it in path construction. Resolves: ZSV-11463 Related: ZSV-11310 Change-Id: I786176646a6a756e727571696374716e6f6f6265 --- plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java index 40caa9e0642..a5d836e06ab 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -224,7 +224,8 @@ public static String buildNvramFilePath(String vmUuid) { public static final String TPM_STATE_FILE_PATH_FORMAT = "/var/lib/libvirt/swtpm/%s/"; public static String buildTpmStateFilePath(String vmUuid) { - return String.format(TPM_STATE_FILE_PATH_FORMAT, vmUuid); + String vmUuidWithHyphen = vmUuid.replaceFirst("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5"); + return String.format(TPM_STATE_FILE_PATH_FORMAT, vmUuidWithHyphen); } public static final String DHCP_BIN_FILE_PATH = "/usr/local/zstack/dnsmasq"; From 1cf6a8d6c0589d3edb891bb9e47674ee523736f5 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Tue, 10 Mar 2026 14:06:15 +0800 Subject: [PATCH 019/122] [kvm]: Move some logic to premium * Move HPKE host secret envelope crypto to premium * Move logic of syncing public key to premium * Remove redunrant extension config * Add envelope test * Introduce host key helper Resolves: ZSPHER-113 Change-Id: I61786a65706a67716766716d796a61616464726b --- .../zstack/header/host/HostKeyIdentityVO.java | 2 +- .../header/secret/SecretHostDefineMsg.java | 2 +- .../header/secret/SecretHostDefineReply.java | 9 - plugin/kvm/pom.xml | 15 +- .../org/zstack/kvm/HostKeyIdentityHelper.java | 123 ++++++++ .../zstack/kvm/HostSecretEnvelopeCrypto.java | 155 ---------- ...ostSecretEnvelopeCryptoExtensionPoint.java | 13 + .../java/org/zstack/kvm/KVMAgentCommands.java | 2 +- .../main/java/org/zstack/kvm/KVMConstant.java | 6 + .../src/main/java/org/zstack/kvm/KVMHost.java | 279 ++---------------- test/pom.xml | 12 +- .../kvm/host/HostSecretCase.groovy | 243 --------------- 12 files changed, 174 insertions(+), 687 deletions(-) create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/HostKeyIdentityHelper.java delete mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCrypto.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCryptoExtensionPoint.java delete mode 100644 test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretCase.groovy diff --git a/header/src/main/java/org/zstack/header/host/HostKeyIdentityVO.java b/header/src/main/java/org/zstack/header/host/HostKeyIdentityVO.java index e77873d70fe..1433e075954 100644 --- a/header/src/main/java/org/zstack/header/host/HostKeyIdentityVO.java +++ b/header/src/main/java/org/zstack/header/host/HostKeyIdentityVO.java @@ -25,7 +25,7 @@ public class HostKeyIdentityVO { @Column private String fingerprint; - @Column(nullable = false) + @Column private Boolean verified = false; @Column diff --git a/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java b/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java index c7280743dc0..e8aeb6d9370 100644 --- a/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java +++ b/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java @@ -11,7 +11,7 @@ */ public class SecretHostDefineMsg extends NeedReplyMessage implements HostMessage { private String hostUuid; - @NoLogging(type = NoLogging.Type.Simple) + @NoLogging private String dekBase64; private String vmUuid; private String purpose; diff --git a/header/src/main/java/org/zstack/header/secret/SecretHostDefineReply.java b/header/src/main/java/org/zstack/header/secret/SecretHostDefineReply.java index 2149bbdb3ba..13ef13c07df 100644 --- a/header/src/main/java/org/zstack/header/secret/SecretHostDefineReply.java +++ b/header/src/main/java/org/zstack/header/secret/SecretHostDefineReply.java @@ -7,17 +7,8 @@ public class SecretHostDefineReply extends MessageReply { public static final String ERROR_CODE_KEYS_NOT_ON_DISK = "KEY_AGENT_KEYS_NOT_ON_DISK"; public static final String ERROR_CODE_KEY_FILES_INTEGRITY_MISMATCH = "KEY_AGENT_KEY_FILES_INTEGRITY_MISMATCH"; - private String errorCode; private String secretUuid; - public String getErrorCode() { - return errorCode; - } - - public void setErrorCode(String errorCode) { - this.errorCode = errorCode; - } - public String getSecretUuid() { return secretUuid; } diff --git a/plugin/kvm/pom.xml b/plugin/kvm/pom.xml index d81071f90dc..49594a296e3 100755 --- a/plugin/kvm/pom.xml +++ b/plugin/kvm/pom.xml @@ -7,17 +7,12 @@ 5.5.0 .. - kvm - + kvm + - org.bouncycastle - bcprov-jdk15on - 1.67 - - - org.zstack - compute - ${project.version} + org.zstack + compute + ${project.version} org.zstack diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/HostKeyIdentityHelper.java b/plugin/kvm/src/main/java/org/zstack/kvm/HostKeyIdentityHelper.java new file mode 100644 index 00000000000..ffe65edca47 --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/HostKeyIdentityHelper.java @@ -0,0 +1,123 @@ +package org.zstack.kvm; + +import org.apache.commons.lang.StringUtils; +import org.zstack.core.db.DatabaseFacade; +import org.zstack.core.db.SimpleQuery; +import org.zstack.core.db.SimpleQuery.Op; +import org.zstack.header.host.HostKeyIdentityVO; +import org.zstack.header.host.HostKeyIdentityVO_; +import org.zstack.header.secret.SecretHostDefineReply; +import org.zstack.utils.logging.CLogger; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +/** + * Shared helper for host envelope key identity: fingerprint and save/update. + * Used by KVMHost (zstack) and KVMEnvelopeKeySyncExtension (premium). + */ +public final class HostKeyIdentityHelper { + private static final CLogger logger = org.zstack.utils.logging.CLoggerImpl.getLogger(HostKeyIdentityHelper.class); + + private HostKeyIdentityHelper() { + } + + /** + * Compute fingerprint from public key (base64): SHA-256 of decoded key bytes, hex-encoded. + * Returns empty string if key is invalid or hashing fails. + */ + public static String fingerprintFromPublicKey(String publicKeyBase64) { + if (publicKeyBase64 == null || publicKeyBase64.isEmpty()) { + return ""; + } + try { + byte[] keyBytes = Base64.getDecoder().decode(publicKeyBase64.trim()); + if (keyBytes == null || keyBytes.length == 0) { + return ""; + } + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] hash = md.digest(keyBytes); + StringBuilder sb = new StringBuilder(hash.length * 2); + for (byte b : hash) { + sb.append(String.format("%02x", b & 0xff)); + } + return sb.toString(); + } catch (IllegalArgumentException | NoSuchAlgorithmException e) { + return ""; + } + } + + public static HostKeyIdentityVO getHostKeyIdentity(DatabaseFacade dbf, String hostUuid) { + SimpleQuery q = dbf.createQuery(HostKeyIdentityVO.class); + q.add(HostKeyIdentityVO_.hostUuid, Op.EQ, hostUuid); + return q.find(); + } + + /** + * Save or update host key identity. Validates publicKey (base64, 32 bytes, non-empty fingerprint). + * On invalid key, marks existing record as verified=false and returns without persisting bad data. + */ + public static void saveOrUpdateHostKeyIdentity(DatabaseFacade dbf, String hostUuid, String publicKey, boolean verified) { + if (StringUtils.isBlank(publicKey)) { + return; + } + + String keyToSave = publicKey.trim(); + byte[] decodedKey; + try { + decodedKey = Base64.getDecoder().decode(keyToSave); + } catch (IllegalArgumentException e) { + logger.warn("host " + hostUuid + " returned an invalid envelope public key"); + setVerified(dbf, hostUuid, false); + return; + } + if (decodedKey.length != 32) { + logger.warn("host " + hostUuid + " returned an envelope public key with unexpected length: " + decodedKey.length); + setVerified(dbf, hostUuid, false); + return; + } + String fingerprint = fingerprintFromPublicKey(keyToSave); + if (StringUtils.isBlank(fingerprint)) { + logger.warn("host " + hostUuid + " returned an envelope public key with empty fingerprint"); + setVerified(dbf, hostUuid, false); + return; + } + + HostKeyIdentityVO vo = getHostKeyIdentity(dbf, hostUuid); + if (vo == null) { + vo = new HostKeyIdentityVO(); + vo.setHostUuid(hostUuid); + vo.setPublicKey(keyToSave); + vo.setFingerprint(fingerprint); + vo.setVerified(verified); + vo.setCreateDate(new java.sql.Timestamp(System.currentTimeMillis())); + dbf.persist(vo); + return; + } + vo.setPublicKey(keyToSave); + vo.setFingerprint(fingerprint); + vo.setVerified(verified); + dbf.update(vo); + } + + /** + * Set verified flag for existing host key identity record, if present. + */ + public static void setVerified(DatabaseFacade dbf, String hostUuid, boolean verified) { + HostKeyIdentityVO vo = getHostKeyIdentity(dbf, hostUuid); + if (vo != null) { + vo.setVerified(verified); + dbf.update(vo); + } + } + + /** + * Whether the verify response error code indicates that key rotate/re-fetch is needed. + */ + public static boolean isRotateNeededGetError(String errorCode) { + if (errorCode == null) return false; + return SecretHostDefineReply.ERROR_CODE_KEYS_NOT_ON_DISK.equals(errorCode) + || SecretHostDefineReply.ERROR_CODE_KEY_FILES_INTEGRITY_MISMATCH.equals(errorCode); + } +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCrypto.java b/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCrypto.java deleted file mode 100644 index 394bdc6933e..00000000000 --- a/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCrypto.java +++ /dev/null @@ -1,155 +0,0 @@ -package org.zstack.kvm; - -import org.bouncycastle.crypto.InvalidCipherTextException; -import org.bouncycastle.crypto.agreement.X25519Agreement; -import org.bouncycastle.crypto.engines.AESEngine; -import org.bouncycastle.crypto.generators.HKDFBytesGenerator; -import org.bouncycastle.crypto.generators.X25519KeyPairGenerator; -import org.bouncycastle.crypto.macs.HMac; -import org.bouncycastle.crypto.modes.GCMBlockCipher; -import org.bouncycastle.crypto.params.AEADParameters; -import org.bouncycastle.crypto.params.HKDFParameters; -import org.bouncycastle.crypto.params.KeyParameter; -import org.bouncycastle.crypto.params.X25519KeyGenerationParameters; -import org.bouncycastle.crypto.params.X25519PublicKeyParameters; -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.digests.SHA256Digest; -import org.bouncycastle.crypto.AsymmetricCipherKeyPair; - -import java.nio.charset.StandardCharsets; -import java.security.SecureRandom; -import java.util.Arrays; - -/** - * HPKE seal (RFC 9180) compatible with Go key-agent: KEM_X25519_HKDF_SHA256, KDF_HKDF_SHA256, AEAD_AES256GCM. - * Seal: encrypt wrapper DEK with host public key; output = enc (32) || ciphertext (for agent to open with private key). - * Must use the same HPKE "info" as key-agent (cmd/key-agent: info := []byte("key-agent hpke info")) for key schedule. - */ -public final class HostSecretEnvelopeCrypto { - private static final String HPKE_V1 = "HPKE-v1"; - /** HPKE application info; must match key-agent main.go: info := []byte("key-agent hpke info") */ - private static final byte[] HPKE_INFO = "key-agent hpke info".getBytes(StandardCharsets.UTF_8); - /** RFC 9180 / IANA HPKE: KEM_ID 0x0020 = DHKEM(X25519, HKDF-SHA256); Nenc = Npk = 32. */ - private static final byte[] KEM_ID = new byte[]{0x00, 0x20}; - private static final byte[] KDF_ID = new byte[]{0x00, 0x01}; // HKDF-SHA256 - private static final byte[] AEAD_ID = new byte[]{0x00, 0x02}; // AES-256-GCM - private static final byte[] KEM_SUITE_ID = concat("KEM".getBytes(StandardCharsets.UTF_8), KEM_ID); - private static final byte[] SUITE_ID = concat(concat("HPKE".getBytes(StandardCharsets.UTF_8), KEM_ID), concat(KDF_ID, AEAD_ID)); - /** Nh: KDF output size (SHA-256 = 32). RFC 9180 Section 4. */ - private static final int NH = 32; - /** Nk: AEAD key size (AES-256 = 32). RFC 9180 Section 7.3. */ - private static final int NK = 32; - /** Nn: AEAD nonce size (AES-GCM = 12). RFC 9180 Section 7.3. */ - private static final int NN = 12; - - private static byte[] concat(byte[] a, byte[] b) { - byte[] r = new byte[a.length + b.length]; - System.arraycopy(a, 0, r, 0, a.length); - System.arraycopy(b, 0, r, a.length, b.length); - return r; - } - - private static byte[] i2osp(int n, int w) { - byte[] r = new byte[w]; - for (int i = w - 1; i >= 0; i--) { - r[i] = (byte) (n & 0xff); - n >>= 8; - } - return r; - } - - /** RFC 9180 LabeledExtract(salt, label, ikm); use kemSuiteId for KEM layer, null for HPKE layer (uses SUITE_ID). */ - private static byte[] labeledExtract(byte[] salt, String label, byte[] ikm, Digest digest, byte[] suiteId) { - byte[] sid = suiteId != null ? suiteId : SUITE_ID; - byte[] labeledIkm = concat(concat(concat(HPKE_V1.getBytes(StandardCharsets.UTF_8), sid), label.getBytes(StandardCharsets.UTF_8)), ikm != null ? ikm : new byte[0]); - return hkdfExtract(salt, labeledIkm, digest); - } - - /** RFC 9180 LabeledExpand(prk, label, info, L); use kemSuiteId for KEM layer, null for HPKE layer. */ - private static byte[] labeledExpand(byte[] prk, String label, byte[] info, int L, Digest digest, byte[] suiteId) { - byte[] sid = suiteId != null ? suiteId : SUITE_ID; - byte[] labeledInfo = concat(concat(concat(concat(i2osp(L, 2), HPKE_V1.getBytes(StandardCharsets.UTF_8)), sid), label.getBytes(StandardCharsets.UTF_8)), info != null ? info : new byte[0]); - return hkdfExpand(prk, labeledInfo, L, digest); - } - - /** - * RFC 5869 / RFC 9180 HKDF-Extract: returns PRK = HMAC-Hash(salt, IKM). - * Must not use HKDFBytesGenerator with full init (that does Extract+Expand); Bouncy Castle - * would then return Expand(PRK, "", L) instead of PRK. We implement Extract only via HMAC. - */ - private static byte[] hkdfExtract(byte[] salt, byte[] ikm, Digest digest) { - byte[] saltBytes = (salt != null && salt.length > 0) ? salt : new byte[NH]; - HMac hmac = new HMac(digest); - hmac.init(new KeyParameter(saltBytes)); - hmac.update(ikm, 0, ikm.length); - byte[] prk = new byte[NH]; - hmac.doFinal(prk, 0); - return prk; - } - - /** - * RFC 5869 / RFC 9180 HKDF-Expand: OKM = HKDF-Expand(PRK, info, L). - * Must use skipExtractParameters(prk, info) so that Bouncy Castle uses prk as PRK and - * only performs Expand. Using HKDFParameters(prk, null, info) would make BC do - * Extract(null, prk) then Expand, which is wrong. - */ - private static byte[] hkdfExpand(byte[] prk, byte[] info, int L, Digest digest) { - HKDFBytesGenerator gen = new HKDFBytesGenerator(digest); - gen.init(HKDFParameters.skipExtractParameters(prk, info != null ? info : new byte[0])); - byte[] out = new byte[L]; - gen.generateBytes(out, 0, out.length); - return out; - } - - /** - * Seal plaintext with recipient's X25519 public key (raw 32 bytes). - * Returns envelope = enc (32 bytes) || ciphertext (AEAD output). - */ - public static byte[] seal(byte[] recipientPublicKey, byte[] plaintext) throws InvalidCipherTextException { - if (recipientPublicKey == null || recipientPublicKey.length != 32 || plaintext == null) { - throw new IllegalArgumentException("recipientPublicKey must be 32 bytes, plaintext non-null"); - } - SecureRandom random = new SecureRandom(); - Digest digest = new SHA256Digest(); - - // 1. Generate ephemeral X25519 key pair (BC crypto) - X25519KeyPairGenerator kpg = new X25519KeyPairGenerator(); - kpg.init(new X25519KeyGenerationParameters(random)); - AsymmetricCipherKeyPair ephemeralKp = kpg.generateKeyPair(); - X25519PublicKeyParameters ephemeralPub = (X25519PublicKeyParameters) ephemeralKp.getPublic(); - byte[] enc = ephemeralPub.getEncoded(); - - // 2. DH shared secret (ephemeral priv, recipient pub) - X25519PublicKeyParameters recipientPub = new X25519PublicKeyParameters(recipientPublicKey, 0); - X25519Agreement agreement = new X25519Agreement(); - agreement.init(ephemeralKp.getPrivate()); - byte[] sharedSecret = new byte[32]; - agreement.calculateAgreement(recipientPub, sharedSecret, 0); - - // 3. KEM shared_secret (DHKEM ExtractAndExpand) with KEM suite_id - byte[] kemContext = concat(enc, recipientPublicKey); - byte[] eaePrk = labeledExtract(new byte[0], "eae_prk", sharedSecret, digest, KEM_SUITE_ID); - byte[] kemSharedSecret = labeledExpand(eaePrk, "shared_secret", kemContext, NH, digest, KEM_SUITE_ID); - - // 4. Key schedule (base mode, empty psk; info must match key-agent NewReceiver(sk, info)) - byte[] pskIdHash = labeledExtract(new byte[0], "psk_id_hash", new byte[0], digest, null); - byte[] infoHash = labeledExtract(new byte[0], "info_hash", HPKE_INFO, digest, null); - byte[] keyScheduleContext = concat(new byte[]{0x00}, concat(pskIdHash, infoHash)); - byte[] secret = labeledExtract(kemSharedSecret, "secret", new byte[0], digest, null); - byte[] key = labeledExpand(secret, "key", keyScheduleContext, NK, digest, null); - byte[] baseNonce = labeledExpand(secret, "base_nonce", keyScheduleContext, NN, digest, null); - - // 5. AEAD Seal (AES-256-GCM, empty aad) - GCMBlockCipher cipher = new GCMBlockCipher(new AESEngine()); - cipher.init(true, new AEADParameters(new KeyParameter(key), 128, baseNonce)); - byte[] out = new byte[cipher.getOutputSize(plaintext.length)]; - int len = cipher.processBytes(plaintext, 0, plaintext.length, out, 0); - len += cipher.doFinal(out, len); - byte[] ct = Arrays.copyOf(out, len); - - return concat(enc, ct); - } - - private HostSecretEnvelopeCrypto() { - } -} \ No newline at end of file diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCryptoExtensionPoint.java b/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCryptoExtensionPoint.java new file mode 100644 index 00000000000..252bffbf055 --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCryptoExtensionPoint.java @@ -0,0 +1,13 @@ +package org.zstack.kvm; + +/** + * Extension point for sealing plaintext (e.g. DEK) with recipient's X25519 public key for host secret (e.g. vTPM). + * Implementation is provided by premium/crypto when available; KVM uses PluginRegistry.getExtensionList() to get it. + */ +public interface HostSecretEnvelopeCryptoExtensionPoint { + /** + * Seal plaintext with recipient's X25519 public key (raw 32 bytes). + * @return envelope = enc (32 bytes) || ciphertext (AEAD output), compatible with key-agent open. + */ + byte[] seal(byte[] recipientPublicKey, byte[] plaintext) throws Exception; +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java index 4881b342a38..5fbdbe7d671 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -455,7 +455,7 @@ public void setErrorCode(String errorCode) { public static class SecretHostDefineCmd extends AgentCommand { private String envelopeDekBase64; - /** Base64 wrapped DEK; agent expects this field name (encryptedDek). */ + /** Base64 envelope of DEK; agent expects this field name (encryptedDek). */ private String encryptedDek; private String vmUuid; private String purpose; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java index a5d836e06ab..a3f3a0c46ea 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -152,6 +152,12 @@ public interface KVMConstant { String KVM_VERIFY_ENVELOPE_KEY_PATH = "/host/key/envelope/checkEnvelopeKey"; String KVM_ENSURE_SECRET_PATH = "/host/key/envelope/ensureSecret"; + /** HTTP timeout in seconds for envelope key sync (verify/create/rotate/get) to agent. */ + long ENVELOPE_KEY_HTTP_TIMEOUT_SEC = 5L; + + /** Max size in bytes for DEK payload in SecretHostDefine (decoded from dekBase64). */ + int MAX_DEK_BYTES = 1024; + String KVM_HOST_FILE_DOWNLOAD_PATH = "/host/file/download"; String KVM_HOST_FILE_UPLOAD_PATH = "/host/file/upload"; String KVM_HOST_FILE_DOWNLOAD_PROGRESS_PATH = "/host/file/progress"; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index 9d07988b18f..adc23a62869 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -5416,30 +5416,6 @@ public void fail(ErrorCode errorCode) { } }); - flow(new NoRollbackFlow() { - String __name__ = "sync-envelope-public-key"; - - @Override - public boolean skip(Map data) { - return data.get(KVMConstant.KVM_HOST_SKIP_PING_NO_FAILURE_EXTENSIONS) != null; - } - - @Override - public void run(FlowTrigger trigger, Map data) { - syncEnvelopeKeyAfterPing(new Completion(trigger) { - @Override - public void success() { - trigger.next(); - } - - @Override - public void fail(ErrorCode errCode) { - trigger.next(); - } - }); - } - }); - done(new FlowDoneHandler(completion) { @Override public void handle(Map data) { @@ -5457,235 +5433,6 @@ public void handle(ErrorCode errCode, Map data) { }).start(); } - private static final long ENVELOPE_KEY_HTTP_TIMEOUT_SEC = 5L; - - private void doRotateAndGetThenSave(String hostUuid, Completion completion) { - String rotateUrl = buildUrl(KVMConstant.KVM_ROTATE_ENVELOPE_KEY_PATH); - restf.asyncJsonPost(rotateUrl, new KVMAgentCommands.RotatePublicKeyCmd(), - new JsonAsyncRESTCallback(completion) { - @Override - public void fail(ErrorCode err) { - setHostKeyIdentityVerified(hostUuid, false); - completion.success(); - } - - @Override - public void success(KVMAgentCommands.RotatePublicKeyResponse rotateRsp) { - if (rotateRsp == null || !rotateRsp.isSuccess()) { - logger.warn("rotate key on agent failed for host " + hostUuid + ": " + (rotateRsp != null ? rotateRsp.getError() : "null")); - setHostKeyIdentityVerified(hostUuid, false); - completion.success(); - return; - } - String getUrl = buildUrl(KVMConstant.KVM_GET_ENVELOPE_KEY_PATH); - restf.asyncJsonPost(getUrl, new KVMAgentCommands.GetPublicKeyCmd(), - new JsonAsyncRESTCallback(completion) { - @Override - public void fail(ErrorCode err) { - setHostKeyIdentityVerified(hostUuid, false); - completion.success(); - } - - @Override - public void success(KVMAgentCommands.GetPublicKeyResponse getRsp) { - if (getRsp != null && getRsp.isSuccess() && StringUtils.isNotBlank(getRsp.getPublicKey())) { - saveOrUpdateHostKeyIdentity(hostUuid, getRsp.getPublicKey().trim(), true); - } else { - setHostKeyIdentityVerified(hostUuid, false); - } - completion.success(); - } - - @Override - public Class getReturnClass() { - return KVMAgentCommands.GetPublicKeyResponse.class; - } - }, TimeUnit.SECONDS, ENVELOPE_KEY_HTTP_TIMEOUT_SEC); - } - - @Override - public Class getReturnClass() { - return KVMAgentCommands.RotatePublicKeyResponse.class; - } - }, TimeUnit.SECONDS, ENVELOPE_KEY_HTTP_TIMEOUT_SEC); - } - - private void syncEnvelopeKeyAfterPing(Completion completion) { - KVMHostVO kvo = dbf.reload(getSelf()); - final String hostUuid = kvo.getUuid(); - try { - HostKeyIdentityVO identity = getHostKeyIdentity(hostUuid); - if (identity != null && StringUtils.isNotBlank(identity.getPublicKey())) { - String verifyUrl = buildUrl(KVMConstant.KVM_VERIFY_ENVELOPE_KEY_PATH); - restf.asyncJsonPost(verifyUrl, new KVMAgentCommands.VerifyPublicKeyCmd(), - new JsonAsyncRESTCallback(completion) { - @Override - public void fail(ErrorCode err) { - setHostKeyIdentityVerified(hostUuid, false); - completion.success(); - } - - @Override - public void success(KVMAgentCommands.VerifyPublicKeyResponse vrsp) { - if (vrsp != null && vrsp.isSuccess()) { - String storedFp = identity.getFingerprint(); - if (StringUtils.isNotBlank(storedFp)) { - String computed = fingerprintFromPublicKey(identity.getPublicKey()); - if (!storedFp.equals(computed)) { - logger.warn("host " + hostUuid + " verify ok but fingerprint mismatch, rotating and re-getting key"); - doRotateAndGetThenSave(hostUuid, completion); - return; - } - } - setHostKeyIdentityVerified(hostUuid, true); - completion.success(); - return; - } - if (vrsp != null && !vrsp.isSuccess() && isRotateNeededGetError(vrsp.getErrorCode())) { - doRotateAndGetThenSave(hostUuid, completion); - return; - } - setHostKeyIdentityVerified(hostUuid, false); - completion.success(); - } - - @Override - public Class getReturnClass() { - return KVMAgentCommands.VerifyPublicKeyResponse.class; - } - }, TimeUnit.SECONDS, ENVELOPE_KEY_HTTP_TIMEOUT_SEC); - return; - } - String createUrl = buildUrl(KVMConstant.KVM_CREATE_ENVELOPE_KEY_PATH); - restf.asyncJsonPost(createUrl, new KVMAgentCommands.CreatePublicKeyCmd(), - new JsonAsyncRESTCallback(completion) { - @Override - public void fail(ErrorCode err) { - logger.warn("create key on agent failed for host " + hostUuid + ": " + (err != null ? err.getDetails() : "")); - setHostKeyIdentityVerified(hostUuid, false); - completion.success(); - } - - @Override - public void success(KVMAgentCommands.CreatePublicKeyResponse createRsp) { - if (createRsp == null || !createRsp.isSuccess()) { - logger.warn("create key on agent failed for host " + hostUuid + ": " + (createRsp != null ? createRsp.getError() : "null")); - setHostKeyIdentityVerified(hostUuid, false); - completion.success(); - return; - } - String getUrl = buildUrl(KVMConstant.KVM_GET_ENVELOPE_KEY_PATH); - restf.asyncJsonPost(getUrl, new KVMAgentCommands.GetPublicKeyCmd(), - new JsonAsyncRESTCallback(completion) { - @Override - public void fail(ErrorCode err) { - logger.warn("get public key after create failed for host " + hostUuid + ": " + (err != null ? err.getDetails() : "")); - setHostKeyIdentityVerified(hostUuid, false); - completion.success(); - } - - @Override - public void success(KVMAgentCommands.GetPublicKeyResponse getRsp) { - if (getRsp != null && getRsp.isSuccess() && StringUtils.isNotBlank(getRsp.getPublicKey())) { - saveOrUpdateHostKeyIdentity(hostUuid, getRsp.getPublicKey().trim(), true); - } else { - setHostKeyIdentityVerified(hostUuid, false); - } - completion.success(); - } - - @Override - public Class getReturnClass() { - return KVMAgentCommands.GetPublicKeyResponse.class; - } - }, TimeUnit.SECONDS, ENVELOPE_KEY_HTTP_TIMEOUT_SEC); - } - - @Override - public Class getReturnClass() { - return KVMAgentCommands.CreatePublicKeyResponse.class; - } - }, TimeUnit.SECONDS, ENVELOPE_KEY_HTTP_TIMEOUT_SEC); - } catch (Exception e) { - logger.warn("sync secret key after connect failed for host " + hostUuid + ": " + e.getMessage()); - try { - setHostKeyIdentityVerified(hostUuid, false); - } catch (Exception ignored) { - } - completion.success(); - } - } - - private void setHostKeyIdentityVerified(String hostUuid, boolean verified) { - HostKeyIdentityVO vo = getHostKeyIdentity(hostUuid); - if (vo != null) { - vo.setVerified(verified); - dbf.update(vo); - } - } - - private static boolean isRotateNeededGetError(String errorCode) { - if (errorCode == null) return false; - return SecretHostDefineReply.ERROR_CODE_KEYS_NOT_ON_DISK.equals(errorCode) - || SecretHostDefineReply.ERROR_CODE_KEY_FILES_INTEGRITY_MISMATCH.equals(errorCode); - } - - private HostKeyIdentityVO getHostKeyIdentity(String hostUuid) { - SimpleQuery q = dbf.createQuery(HostKeyIdentityVO.class); - q.add(HostKeyIdentityVO_.hostUuid, Op.EQ, hostUuid); - return q.find(); - } - - /** - * Compute fingerprint from public key (base64): SHA-256 of decoded key bytes, hex-encoded. - * Returns empty string if key is invalid or hashing fails. - */ - private static String fingerprintFromPublicKey(String publicKeyBase64) { - if (publicKeyBase64 == null || publicKeyBase64.isEmpty()) { - return ""; - } - try { - byte[] keyBytes = Base64.getDecoder().decode(publicKeyBase64.trim()); - if (keyBytes == null || keyBytes.length == 0) { - return ""; - } - MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] hash = md.digest(keyBytes); - StringBuilder sb = new StringBuilder(hash.length * 2); - for (byte b : hash) { - sb.append(String.format("%02x", b & 0xff)); - } - return sb.toString(); - } catch (IllegalArgumentException | NoSuchAlgorithmException e) { - return ""; - } - } - - private void saveOrUpdateHostKeyIdentity(String hostUuid, String publicKey, boolean verified) { - if (StringUtils.isBlank(publicKey)) { - return; - } - String keyToSave = publicKey.trim(); - String fingerprint = fingerprintFromPublicKey(keyToSave); - HostKeyIdentityVO vo = getHostKeyIdentity(hostUuid); - if (vo == null) { - vo = new HostKeyIdentityVO(); - vo.setHostUuid(hostUuid); - vo.setPublicKey(keyToSave); - vo.setFingerprint(fingerprint); - vo.setVerified(verified); - vo.setCreateDate(new java.sql.Timestamp(System.currentTimeMillis())); - dbf.persist(vo); - return; - } - vo.setPublicKey(keyToSave); - vo.setFingerprint(fingerprint); - vo.setVerified(verified); - dbf.update(vo); - } - - private static final int MAX_DEK_BYTES = 1024; - private void handle(SecretHostDefineMsg msg) { SecretHostDefineReply reply = new SecretHostDefineReply(); if (org.apache.commons.lang.StringUtils.isBlank(msg.getDekBase64())) { @@ -5699,7 +5446,7 @@ private void handle(SecretHostDefineMsg msg) { return; } String hostUuid = getSelf().getUuid(); - HostKeyIdentityVO identity = getHostKeyIdentity(hostUuid); + HostKeyIdentityVO identity = HostKeyIdentityHelper.getHostKeyIdentity(dbf, hostUuid); String pubKey = identity != null ? org.apache.commons.lang.StringUtils.trimToNull(identity.getPublicKey()) : null; Boolean verifyOk = identity != null ? identity.getVerified() : null; if (pubKey == null) { @@ -5708,7 +5455,7 @@ private void handle(SecretHostDefineMsg msg) { return; } String storedFingerprint = identity.getFingerprint(); - String computed = fingerprintFromPublicKey(pubKey); + String computed = HostKeyIdentityHelper.fingerprintFromPublicKey(pubKey); if (!storedFingerprint.equals(computed)) { reply.setError(operr("host public key fingerprint mismatch, key may be corrupted or tampered")); bus.reply(msg, reply); @@ -5733,7 +5480,7 @@ private void handle(SecretHostDefineMsg msg) { return; } - if (dekRaw.length > MAX_DEK_BYTES) { + if (dekRaw.length > KVMConstant.MAX_DEK_BYTES) { reply.setError(operr("dekBase64 decoded payload is too large")); bus.reply(msg, reply); return; @@ -5752,10 +5499,16 @@ private void handle(SecretHostDefineMsg msg) { bus.reply(msg, reply); return; } + java.util.List sealers = pluginRegistry.getExtensionList(HostSecretEnvelopeCryptoExtensionPoint.class); + if (sealers == null || sealers.isEmpty()) { + reply.setError(operr("host secret envelope sealer not available (premium crypto module required)")); + bus.reply(msg, reply); + return; + } byte[] envelope; try { - envelope = HostSecretEnvelopeCrypto.seal(pubKeyBytes, dekRaw); - } catch (org.bouncycastle.crypto.InvalidCipherTextException e) { + envelope = sealers.get(0).seal(pubKeyBytes, dekRaw); + } catch (Exception e) { reply.setError(operr("HPKE seal failed: %s", e.getMessage())); bus.reply(msg, reply); return; @@ -5782,9 +5535,13 @@ public void success(KVMAgentCommands.SecretHostDefineResponse rsp) { reply.setSecretUuid(rsp.getSecretUuid()); } } else { - reply.setError(operr(rsp != null ? rsp.getError() : "ensure secret failed")); if (rsp != null && rsp.getErrorCode() != null) { - reply.setErrorCode(rsp.getErrorCode()); + ErrorCode err = new ErrorCode(); + err.setCode(rsp.getErrorCode()); + err.setDetails(rsp.getError() != null ? rsp.getError() : "ensure secret failed"); + reply.setError(err); + } else { + reply.setError(operr(rsp != null ? rsp.getError() : "ensure secret failed")); } } bus.reply(msg, reply); @@ -5794,7 +5551,7 @@ public void success(KVMAgentCommands.SecretHostDefineResponse rsp) { public Class getReturnClass() { return KVMAgentCommands.SecretHostDefineResponse.class; } - }, TimeUnit.SECONDS, ENVELOPE_KEY_HTTP_TIMEOUT_SEC); + }, TimeUnit.SECONDS, KVMConstant.ENVELOPE_KEY_HTTP_TIMEOUT_SEC); } @Override diff --git a/test/pom.xml b/test/pom.xml index 6505ec9324c..f90034ca817 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -6,12 +6,12 @@ ${skipJacoco} 4.0.0 - - zstack - org.zstack - 5.5.0 - .. - + + zstack + org.zstack + 5.5.0 + .. + test \n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def queryKeyProvider(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.keyprovider.api.QueryKeyProviderAction.class) Closure c) { + def a = new org.zstack.sdk.keyprovider.api.QueryKeyProviderAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + a.conditions = a.conditions.collect { it.toString() } + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def rekeyKeyProviderRefs(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.keyprovider.api.RekeyKeyProviderRefsAction.class) Closure c) { + def a = new org.zstack.sdk.keyprovider.api.RekeyKeyProviderRefsAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def createKms(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.keyprovider.kms.api.CreateKmsAction.class) Closure c) { + def a = new org.zstack.sdk.keyprovider.kms.api.CreateKmsAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def deleteKms(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.keyprovider.kms.api.DeleteKmsAction.class) Closure c) { + def a = new org.zstack.sdk.keyprovider.kms.api.DeleteKmsAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def getKmsServerCertFromKms(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.keyprovider.kms.api.GetKmsServerCertFromKmsAction.class) Closure c) { + def a = new org.zstack.sdk.keyprovider.kms.api.GetKmsServerCertFromKmsAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def queryKms(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.keyprovider.kms.api.QueryKmsAction.class) Closure c) { + def a = new org.zstack.sdk.keyprovider.kms.api.QueryKmsAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + a.conditions = a.conditions.collect { it.toString() } + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def updateKms(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.keyprovider.kms.api.UpdateKmsAction.class) Closure c) { + def a = new org.zstack.sdk.keyprovider.kms.api.UpdateKmsAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def uploadKmsClientCsr(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.keyprovider.kms.api.UploadKmsClientCsrAction.class) Closure c) { + def a = new org.zstack.sdk.keyprovider.kms.api.UploadKmsClientCsrAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def uploadKmsClientIdentity(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.keyprovider.kms.api.UploadKmsClientIdentityAction.class) Closure c) { + def a = new org.zstack.sdk.keyprovider.kms.api.UploadKmsClientIdentityAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def uploadKmsClientSignedCert(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.keyprovider.kms.api.UploadKmsClientSignedCertAction.class) Closure c) { + def a = new org.zstack.sdk.keyprovider.kms.api.UploadKmsClientSignedCertAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def uploadKmsServerCert(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.keyprovider.kms.api.UploadKmsServerCertAction.class) Closure c) { + def a = new org.zstack.sdk.keyprovider.kms.api.UploadKmsServerCertAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def backupNkp(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.keyprovider.nkp.api.BackupNkpAction.class) Closure c) { + def a = new org.zstack.sdk.keyprovider.nkp.api.BackupNkpAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def createNkp(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.keyprovider.nkp.api.CreateNkpAction.class) Closure c) { + def a = new org.zstack.sdk.keyprovider.nkp.api.CreateNkpAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def deleteNkp(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.keyprovider.nkp.api.DeleteNkpAction.class) Closure c) { + def a = new org.zstack.sdk.keyprovider.nkp.api.DeleteNkpAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def parseNkpRestore(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.keyprovider.nkp.api.ParseNkpRestoreAction.class) Closure c) { + def a = new org.zstack.sdk.keyprovider.nkp.api.ParseNkpRestoreAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def queryNkp(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.keyprovider.nkp.api.QueryNkpAction.class) Closure c) { + def a = new org.zstack.sdk.keyprovider.nkp.api.QueryNkpAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + a.conditions = a.conditions.collect { it.toString() } + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def restoreNkp(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.keyprovider.nkp.api.RestoreNkpAction.class) Closure c) { + def a = new org.zstack.sdk.keyprovider.nkp.api.RestoreNkpAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() if (System.getProperty("apipath") != null) { @@ -52820,6 +53285,31 @@ abstract class ApiHelper { } + def updateNkp(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.keyprovider.nkp.api.UpdateNkpAction.class) Closure c) { + def a = new org.zstack.sdk.keyprovider.nkp.api.UpdateNkpAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } def getLicenseAuthorizedCapacity(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.license.api.server.GetLicenseAuthorizedCapacityAction.class) Closure c) { def a = new org.zstack.sdk.license.api.server.GetLicenseAuthorizedCapacityAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -58517,4 +59007,141 @@ abstract class ApiHelper { } + def addTpm(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.tpm.api.AddTpmAction.class) Closure c) { + def a = new org.zstack.sdk.tpm.api.AddTpmAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def getTpmCapability(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.tpm.api.GetTpmCapabilityAction.class) Closure c) { + def a = new org.zstack.sdk.tpm.api.GetTpmCapabilityAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def queryTpm(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.tpm.api.QueryTpmAction.class) Closure c) { + def a = new org.zstack.sdk.tpm.api.QueryTpmAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + a.conditions = a.conditions.collect { it.toString() } + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def removeTpm(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.tpm.api.RemoveTpmAction.class) Closure c) { + def a = new org.zstack.sdk.tpm.api.RemoveTpmAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + + def updateTpm(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.tpm.api.UpdateTpmAction.class) Closure c) { + def a = new org.zstack.sdk.tpm.api.UpdateTpmAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + + + if (System.getProperty("apipath") != null) { + if (a.apiId == null) { + a.apiId = Platform.uuid + } + + def tracker = new ApiPathTracker(a.apiId) + def out = errorOut(a.call()) + def path = tracker.getApiPath() + if (!path.isEmpty()) { + Test.apiPaths[a.class.name] = path.join(" --->\n") + } + + return out + } else { + return errorOut(a.call()) + } + } + + } From 4ead394097c4a16d2ac7ee066e8a59f9d7d9734b Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Mon, 17 Aug 2026 22:57:55 +0800 Subject: [PATCH 122/122] [zstack]: adapt vTPM migration compile issues Add cloud ErrorCode API adaptations for vTPM/KMS migration code and restore small helper methods required by generated API documentation. Resolves: ZSV-12866 Change-Id: Ie05fdde8db2bd54ed314b58965070fa266f1a8e1 --- .../zstack/compute/vm/VmCascadeExtension.java | 4 +- .../compute/vm/VmInstanceApiInterceptor.java | 4 +- .../DummyEncryptedResourceKeyManager.java | 7 ++- .../compute/vm/devices/TpmApiInterceptor.java | 11 +++-- .../vm/devices/TpmCascadeExtension.java | 8 ++-- .../vm/devices/TpmMessageAutoCompleter.java | 10 ++-- .../VmHostBackupFileCascadeExtension.java | 8 ++-- .../devices/VmHostFileCascadeExtension.java | 8 ++-- .../compute/vm/devices/VmTpmManager.java | 4 ++ .../global-error-de-DE.json | 4 ++ .../global-error-en_US.json | 4 ++ .../global-error-fr-FR.json | 4 ++ .../global-error-id-ID.json | 4 ++ .../global-error-ja-JP.json | 4 ++ .../global-error-ko-KR.json | 4 ++ .../global-error-ru-RU.json | 4 ++ .../global-error-th-TH.json | 4 ++ .../global-error-zh_CN.json | 4 ++ .../global-error-zh_TW.json | 4 ++ .../org/zstack/header/message/DocUtils.java | 11 +++++ .../org/zstack/header/message/Message.java | 4 ++ .../src/main/java/org/zstack/kvm/KVMHost.java | 48 +++++++++++-------- .../kvm/efi/KvmSecureBootExtensions.java | 9 ++-- .../zstack/kvm/efi/KvmSecureBootManager.java | 48 ++++++++++--------- .../zstack/kvm/efi/KvmVmHostFileFactory.java | 8 +++- .../org/zstack/kvm/tpm/KvmTpmExtensions.java | 15 +++--- .../org/zstack/kvm/tpm/KvmTpmManager.java | 30 +++++++----- .../VmHostFileAllocatorExtensionPoint.java | 31 ++++++------ .../kvm/vmfiles/VmHostFileAutoCompleter.java | 22 +-------- .../zstack/kvm/vmfiles/VmHostFileTracker.java | 3 +- .../snapshot/VolumeSnapshotTreeBase.java | 2 +- .../group/VolumeSnapshotGroupBase.java | 2 +- .../org/zstack/storage/volume/VolumeBase.java | 5 +- .../main/java/org/zstack/testlib/Test.groovy | 5 ++ .../main/java/org/zstack/utils/StringDSL.java | 1 + .../CloudOperationsErrorCode.java | 8 ++++ 36 files changed, 226 insertions(+), 130 deletions(-) diff --git a/compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java b/compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java index 37e994fd226..63e25019a7f 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java @@ -464,7 +464,9 @@ public void done(ErrorCodeList errorCodeList) { .collect(Collectors.toList()); dbf.removeByPrimaryKeys(cdRomUuids, VmCdRomVO.class); - List vmUuidList = transform(vminvs, p -> p.getInventory().getUuid()); + List vmUuidList = vminvs.stream() + .map(vm -> vm.getInventory().getUuid()) + .collect(Collectors.toList()); dbf.removeByPrimaryKeys(vmUuidList, VmInstanceVO.class); } diff --git a/compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java b/compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java index b4bd30ad435..e61d9489c79 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java @@ -1094,8 +1094,8 @@ private void validateRootDiskOffering(ImageMediaType imgFormat, APICreateVmInsta } if (msg.getRootDiskSize() <= 0) { - throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10182, "Unexpected root disk settings") - .withException("DiskAO[0].size is mandatory when image format is ISO")); + throw new ApiMessageInterceptionException(operr(ORG_ZSTACK_COMPUTE_VM_10182, + "Unexpected root disk settings: DiskAO[0].size is mandatory when image format is ISO")); } } } diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/DummyEncryptedResourceKeyManager.java b/compute/src/main/java/org/zstack/compute/vm/devices/DummyEncryptedResourceKeyManager.java index 0bd6a8eaaae..6563fa16d7f 100644 --- a/compute/src/main/java/org/zstack/compute/vm/devices/DummyEncryptedResourceKeyManager.java +++ b/compute/src/main/java/org/zstack/compute/vm/devices/DummyEncryptedResourceKeyManager.java @@ -8,6 +8,7 @@ import org.zstack.utils.logging.CLogger; import static org.zstack.core.Platform.operr; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; public class DummyEncryptedResourceKeyManager implements EncryptedResourceKeyManager { private static final CLogger logger = Utils.getLogger(DummyEncryptedResourceKeyManager.class); @@ -17,14 +18,16 @@ public void getOrCreateKey(GetOrCreateResourceKeyContext ctx, ReturnValueCompletion completion) { logger.warn(String.format("crypto module not installed, cannot create resource key for %s[uuid:%s]", ctx.getResourceType(), ctx.getResourceUuid())); - completion.fail(operr("crypto module is not installed, cannot manage resource encryption keys")); + completion.fail(operr(ORG_ZSTACK_COMPUTE_VM_DEVICES_10009, + "crypto module is not installed, cannot manage resource encryption keys")); } @Override public ResourceKeyResult getKey(GetOrCreateResourceKeyContext ctx) { logger.warn(String.format("crypto module not installed, cannot get resource key for %s[uuid:%s]", ctx.getResourceType(), ctx.getResourceUuid())); - throw new OperationFailureException(operr("crypto module is not installed, cannot manage resource encryption keys")); + throw new OperationFailureException(operr(ORG_ZSTACK_COMPUTE_VM_DEVICES_10009, + "crypto module is not installed, cannot manage resource encryption keys")); } @Override diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/TpmApiInterceptor.java b/compute/src/main/java/org/zstack/compute/vm/devices/TpmApiInterceptor.java index 78724e72a57..1acdcc5070f 100644 --- a/compute/src/main/java/org/zstack/compute/vm/devices/TpmApiInterceptor.java +++ b/compute/src/main/java/org/zstack/compute/vm/devices/TpmApiInterceptor.java @@ -21,6 +21,7 @@ import static org.zstack.header.tpm.TpmConstants.*; import static org.zstack.header.tpm.TpmErrors.*; import static org.zstack.header.vm.VmInstanceConstant.KVM_HYPERVISOR_TYPE; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; public class TpmApiInterceptor implements ApiMessageInterceptor { private static final CLogger logger = Utils.getLogger(TpmApiInterceptor.class); @@ -54,7 +55,8 @@ private void validate(APIAddTpmMsg msg) { .eq(TpmVO_.vmInstanceUuid, msg.getVmInstanceUuid()) .isExists(); if (tpmExists) { - throw new ApiMessageInterceptionException(err(TPM_ALREADY_EXISTS, "tpm device already exists")); + throw new ApiMessageInterceptionException(err(ORG_ZSTACK_COMPUTE_VM_DEVICES_10009, + TPM_ALREADY_EXISTS, "tpm device already exists")); } boolean vmInSupportState = Q.New(VmInstanceVO.class) @@ -62,7 +64,7 @@ private void validate(APIAddTpmMsg msg) { .in(VmInstanceVO_.state, SUPPORT_VM_STATES_FOR_TPM_OPERATION) .isExists(); if (!vmInSupportState) { - throw new ApiMessageInterceptionException(err(VM_STATE_ERROR, + throw new ApiMessageInterceptionException(err(ORG_ZSTACK_COMPUTE_VM_DEVICES_10009, VM_STATE_ERROR, "The current VM state does not support adding TPM operations") .withOpaque("support.vm.state", SUPPORT_VM_STATES_FOR_TPM_OPERATION)); } @@ -89,7 +91,7 @@ private void validate(APIRemoveTpmMsg msg) { .in(VmInstanceVO_.state, SUPPORT_VM_STATES_FOR_TPM_OPERATION) .isExists(); if (!vmInSupportState) { - throw new ApiMessageInterceptionException(err(VM_STATE_ERROR, + throw new ApiMessageInterceptionException(err(ORG_ZSTACK_COMPUTE_VM_DEVICES_10009, VM_STATE_ERROR, "The current VM state does not support removing TPM operations") .withOpaque("support.vm.state", SUPPORT_VM_STATES_FOR_TPM_OPERATION)); } @@ -106,7 +108,8 @@ private void makeSureVmInstanceIsKvmType(String vmInstanceUuid) { .eq(VmInstanceVO_.uuid, vmInstanceUuid) .findValue(); if (!KVM_HYPERVISOR_TYPE.equals(hypervisorType)) { - throw new ApiMessageInterceptionException(err(SysErrors.NOT_SUPPORTED, "only allowed for kvm type VM instance")); + throw new ApiMessageInterceptionException(err(ORG_ZSTACK_COMPUTE_VM_DEVICES_10009, + SysErrors.NOT_SUPPORTED, "only allowed for kvm type VM instance")); } } } diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/TpmCascadeExtension.java b/compute/src/main/java/org/zstack/compute/vm/devices/TpmCascadeExtension.java index d834ac43d3b..a0b8e48a524 100644 --- a/compute/src/main/java/org/zstack/compute/vm/devices/TpmCascadeExtension.java +++ b/compute/src/main/java/org/zstack/compute/vm/devices/TpmCascadeExtension.java @@ -29,6 +29,7 @@ import static org.zstack.header.tpm.TpmConstants.SERVICE_ID; import static org.zstack.utils.CollectionDSL.list; import static org.zstack.utils.CollectionUtils.transformAndRemoveNull; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; public class TpmCascadeExtension extends AbstractAsyncCascadeExtension { private static final CLogger logger = Utils.getLogger(TpmCascadeExtension.class); @@ -137,9 +138,10 @@ public void run(MessageReply reply) { }).run(new WhileDoneCompletion(completion) { @Override public void done(ErrorCodeList errorCodeList) { - if (!errorCodeList.isEmpty()) { - completion.fail(operr("failed to delete Tpm from VM[uuid:%s]", tpmList.get(0).getVmInstanceUuid()) - .withCause(errorCodeList)); + if (!errorCodeList.getCauses().isEmpty()) { + completion.fail(operr(ORG_ZSTACK_COMPUTE_VM_DEVICES_10009, + "failed to delete Tpm from VM[uuid:%s]", tpmList.get(0).getVmInstanceUuid()) + .causedBy(errorCodeList.getCauses())); return; } completion.success(); diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/TpmMessageAutoCompleter.java b/compute/src/main/java/org/zstack/compute/vm/devices/TpmMessageAutoCompleter.java index 3e55208820e..af13333c7d8 100644 --- a/compute/src/main/java/org/zstack/compute/vm/devices/TpmMessageAutoCompleter.java +++ b/compute/src/main/java/org/zstack/compute/vm/devices/TpmMessageAutoCompleter.java @@ -18,6 +18,7 @@ import static org.zstack.core.Platform.err; import static org.zstack.header.tpm.TpmErrors.TPM_NOT_FOUND; import static org.zstack.utils.CollectionDSL.list; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; public class TpmMessageAutoCompleter implements GlobalApiMessageInterceptor { @Override @@ -44,7 +45,8 @@ private void validate(TpmMessage msg) throws ApiMessageInterceptionException { String vmUuid = msg.getVmInstanceUuid(); if (tpmUuid == null && vmUuid == null) { - throw new ApiMessageInterceptionException(argerr("tpmUuid and vmInstanceUuid cannot be null at the same time")); + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_DEVICES_10009, + "tpmUuid and vmInstanceUuid cannot be null at the same time")); } if (tpmUuid != null && vmUuid != null) { @@ -53,7 +55,8 @@ private void validate(TpmMessage msg) throws ApiMessageInterceptionException { .eq(TpmVO_.vmInstanceUuid, vmUuid) .isExists(); if (!exists) { - throw new ApiMessageInterceptionException(argerr("tpmUuid[%s] and vmInstanceUuid[%s] are not consistent", tpmUuid, vmUuid)); + throw new ApiMessageInterceptionException(argerr(ORG_ZSTACK_COMPUTE_VM_DEVICES_10009, + "tpmUuid[%s] and vmInstanceUuid[%s] are not consistent", tpmUuid, vmUuid)); } } else if (vmUuid != null) { tpmUuid = Q.New(TpmVO.class) @@ -61,7 +64,8 @@ private void validate(TpmMessage msg) throws ApiMessageInterceptionException { .eq(TpmVO_.vmInstanceUuid, vmUuid) .findValue(); if (tpmUuid == null && (!(msg instanceof APIDeleteMessage))) { - throw new ApiMessageInterceptionException(err(TPM_NOT_FOUND, "tpm for vm[%s] does not exist", vmUuid)); + throw new ApiMessageInterceptionException(err(ORG_ZSTACK_COMPUTE_VM_DEVICES_10009, + TPM_NOT_FOUND, "tpm for vm[%s] does not exist", vmUuid)); } else { msg.setTpmUuid(tpmUuid); } diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/VmHostBackupFileCascadeExtension.java b/compute/src/main/java/org/zstack/compute/vm/devices/VmHostBackupFileCascadeExtension.java index d22b9725ffc..e316a429e19 100644 --- a/compute/src/main/java/org/zstack/compute/vm/devices/VmHostBackupFileCascadeExtension.java +++ b/compute/src/main/java/org/zstack/compute/vm/devices/VmHostBackupFileCascadeExtension.java @@ -35,6 +35,7 @@ import static org.zstack.core.Platform.operr; import static org.zstack.utils.CollectionDSL.list; import static org.zstack.utils.CollectionUtils.transformAndRemoveNull; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; public class VmHostBackupFileCascadeExtension extends AbstractAsyncCascadeExtension { private static final CLogger logger = Utils.getLogger(VmHostBackupFileCascadeExtension.class); @@ -192,9 +193,10 @@ public void run(MessageReply reply) { }).run(new WhileDoneCompletion(completion) { @Override public void done(ErrorCodeList errorCodeList) { - if (!errorCodeList.isEmpty()) { - completion.fail(operr("failed to delete VmHostBackupFile from resource[uuid:%s]", voList.get(0).getResourceUuid()) - .withCause(errorCodeList)); + if (!errorCodeList.getCauses().isEmpty()) { + completion.fail(operr(ORG_ZSTACK_COMPUTE_VM_DEVICES_10009, + "failed to delete VmHostBackupFile from resource[uuid:%s]", voList.get(0).getResourceUuid()) + .causedBy(errorCodeList.getCauses())); return; } completion.success(); diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/VmHostFileCascadeExtension.java b/compute/src/main/java/org/zstack/compute/vm/devices/VmHostFileCascadeExtension.java index 26983d7d6a2..d65f4062b8a 100644 --- a/compute/src/main/java/org/zstack/compute/vm/devices/VmHostFileCascadeExtension.java +++ b/compute/src/main/java/org/zstack/compute/vm/devices/VmHostFileCascadeExtension.java @@ -35,6 +35,7 @@ import static org.zstack.core.Platform.operr; import static org.zstack.utils.CollectionDSL.list; import static org.zstack.utils.CollectionUtils.transformAndRemoveNull; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; public class VmHostFileCascadeExtension extends AbstractAsyncCascadeExtension { private static final CLogger logger = Utils.getLogger(VmHostFileCascadeExtension.class); @@ -189,9 +190,10 @@ public void run(MessageReply reply) { }).run(new WhileDoneCompletion(completion) { @Override public void done(ErrorCodeList errorCodeList) { - if (!errorCodeList.isEmpty()) { - completion.fail(operr("failed to delete VmHostFile from VM[uuid:%s]", voList.get(0).getVmInstanceUuid()) - .withCause(errorCodeList)); + if (!errorCodeList.getCauses().isEmpty()) { + completion.fail(operr(ORG_ZSTACK_COMPUTE_VM_DEVICES_10009, + "failed to delete VmHostFile from VM[uuid:%s]", voList.get(0).getVmInstanceUuid()) + .causedBy(errorCodeList.getCauses())); return; } completion.success(); diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmManager.java b/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmManager.java index b486fe09379..38e9cc2f990 100644 --- a/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmManager.java +++ b/compute/src/main/java/org/zstack/compute/vm/devices/VmTpmManager.java @@ -9,6 +9,7 @@ import org.zstack.header.tpm.entity.TpmVO; import org.zstack.header.tpm.entity.TpmVO_; import org.zstack.header.vm.additions.VmHostFileType; +import org.zstack.identity.AccountManager; import org.zstack.resourceconfig.ResourceConfig; import org.zstack.resourceconfig.ResourceConfigFacade; import org.zstack.utils.Utils; @@ -31,6 +32,8 @@ public class VmTpmManager { private DatabaseFacade databaseFacade; @Autowired private ResourceConfigFacade resourceConfigFacade; + @Autowired + private AccountManager accountManager; public TpmVO persistTpmVO(String tpmUuid, String vmUuid) { if (tpmUuid == null) { @@ -40,6 +43,7 @@ public TpmVO persistTpmVO(String tpmUuid, String vmUuid) { tpm.setUuid(tpmUuid); tpm.setResourceName("TPM-for-VM-" + vmUuid); tpm.setVmInstanceUuid(vmUuid); + tpm.setAccountUuid(accountManager.getOwnerAccountUuidOfResource(vmUuid)); databaseFacade.persistAndRefresh(tpm); logger.debug("Persisted TpmVO for VM " + vmUuid + " with uuid=" + tpm.getUuid()); diff --git a/conf/i18n/globalErrorCodeMapping/global-error-de-DE.json b/conf/i18n/globalErrorCodeMapping/global-error-de-DE.json index b3602b32e16..b29ab243858 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-de-DE.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-de-DE.json @@ -484,6 +484,10 @@ "ORG_ZSTACK_COMPUTE_VM_DEVICES_10007": "Virtuelle Maschine mit UUID: %s nicht gefunden", "ORG_ZSTACK_VPC_10019": "Konnte L3-Netzwerk nicht vom VPC-Router [uuid:%s] trennen, da die Zustände von Master und Slave inkonsistent sind", "ORG_ZSTACK_COMPUTE_VM_DEVICES_10008": "Virtuelles Maschinengerät mit UUID: %s nicht gefunden", + "ORG_ZSTACK_COMPUTE_VM_DEVICES_10009": "TPM-/host-file-Operation des VM-Geräts fehlgeschlagen: %s", + "ORG_ZSTACK_KVM_10163": "KVM-vTPM-/host-file-Operation fehlgeschlagen: %s", + "ORG_ZSTACK_STORAGE_VOLUME_10101": "Volume-Protokolloperation fehlgeschlagen: %s", + "ORG_ZSTACK_STORAGE_SNAPSHOT_10030": "Volume-Snapshot-Operation fehlgeschlagen: %s", "ORG_ZSTACK_STORAGE_CEPH_PRIMARY_10030": "Kann keinen Backup-Speicher finden, um das Image [%s] auf den Primärspeicher [%s] herunterzuladen, da kein bereitgestelltes und zugängliches Image vorhanden ist", "ORG_ZSTACK_VPC_10018": "Konnte L3-Netzwerk nicht vom VPC-Router [uuid:%s] trennen, da sein Zustand nicht aktiv oder gestoppt ist", "ORG_ZSTACK_ALIYUN_CORE_10037": "Das Image existiert bereits remote; bitte verwenden Sie zunächst sync, um die Konsistenz sicherzustellen.", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-en_US.json b/conf/i18n/globalErrorCodeMapping/global-error-en_US.json index 8fda69f4152..161b0bf5a3b 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-en_US.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-en_US.json @@ -488,6 +488,10 @@ "ORG_ZSTACK_COMPUTE_VM_DEVICES_10007": "cannot find virtual machine with UUID: %s", "ORG_ZSTACK_VPC_10019": "could not detach L3 network from VPC router[uuid:%s] because the states of the master and slave are inconsistent", "ORG_ZSTACK_COMPUTE_VM_DEVICES_10008": "cannot find virtual machine device with uuid: %s", + "ORG_ZSTACK_COMPUTE_VM_DEVICES_10009": "VM device TPM/host-file operation failed: %s", + "ORG_ZSTACK_KVM_10163": "KVM vTPM/host-file operation failed: %s", + "ORG_ZSTACK_STORAGE_VOLUME_10101": "volume protocol operation failed: %s", + "ORG_ZSTACK_STORAGE_SNAPSHOT_10030": "volume snapshot operation failed: %s", "ORG_ZSTACK_STORAGE_CEPH_PRIMARY_10030": "cannot find backup storage to download image [%s] to primary storage [%s] due to lack of ready and accessible image", "ORG_ZSTACK_VPC_10018": "could not detach L3 network from VPC router[uuid:%s] as its state is not running or stopped", "ORG_ZSTACK_ALIYUN_CORE_10037": "The image already existed remotely; please use sync first to ensure consistency.", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json b/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json index a0e299aff9f..eb0bb3452b5 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-fr-FR.json @@ -484,6 +484,10 @@ "ORG_ZSTACK_COMPUTE_VM_DEVICES_10007": "impossible de trouver la machine virtuelle avec l'UUID : %s", "ORG_ZSTACK_VPC_10019": "impossible de détacher le réseau L3 du routeur VPC [uuid:%s] car les états du maître et de l'esclave sont incohérents", "ORG_ZSTACK_COMPUTE_VM_DEVICES_10008": "impossible de trouver le dispositif de machine virtuelle avec l'uuid : %s", + "ORG_ZSTACK_COMPUTE_VM_DEVICES_10009": "échec de l'opération TPM/host-file du périphérique de VM : %s", + "ORG_ZSTACK_KVM_10163": "échec de l'opération KVM vTPM/host-file : %s", + "ORG_ZSTACK_STORAGE_VOLUME_10101": "échec de l'opération du protocole de volume : %s", + "ORG_ZSTACK_STORAGE_SNAPSHOT_10030": "échec de l'opération de snapshot de volume : %s", "ORG_ZSTACK_STORAGE_CEPH_PRIMARY_10030": "impossible de trouver un stockage de sauvegarde pour télécharger l'image [%s] vers le stockage principal [%s] en raison de l'absence d'image prête et accessible", "ORG_ZSTACK_VPC_10018": "impossible de détacher le réseau L3 du routeur VPC [uuid:%s] car son état n'est pas en cours d'exécution ou arrêté", "ORG_ZSTACK_ALIYUN_CORE_10037": "L'image existe déjà à distance ; veuillez d'abord utiliser sync pour assurer la cohérence.", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json b/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json index d27b78d3499..12c632427e9 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-id-ID.json @@ -484,6 +484,10 @@ "ORG_ZSTACK_COMPUTE_VM_DEVICES_10007": "tidak dapat menemukan mesin virtual dengan UUID: %s", "ORG_ZSTACK_VPC_10019": "tidak dapat melepaskan jaringan L3 dari router VPC [uuid:%s] karena status master dan slave tidak konsisten", "ORG_ZSTACK_COMPUTE_VM_DEVICES_10008": "tidak dapat menemukan perangkat mesin virtual dengan uuid: %s", + "ORG_ZSTACK_COMPUTE_VM_DEVICES_10009": "operasi TPM/host-file perangkat VM gagal: %s", + "ORG_ZSTACK_KVM_10163": "operasi KVM vTPM/host-file gagal: %s", + "ORG_ZSTACK_STORAGE_VOLUME_10101": "operasi protokol volume gagal: %s", + "ORG_ZSTACK_STORAGE_SNAPSHOT_10030": "operasi snapshot volume gagal: %s", "ORG_ZSTACK_STORAGE_CEPH_PRIMARY_10030": "tidak dapat menemukan penyimpanan cadangan untuk mengunduh gambar [%s] ke penyimpanan primer [%s] karena tidak ada gambar yang siap dan dapat diakses", "ORG_ZSTACK_VPC_10018": "tidak dapat melepaskan jaringan L3 dari router VPC [uuid:%s] karena statusnya tidak berjalan atau berhenti", "ORG_ZSTACK_ALIYUN_CORE_10037": "gambar sudah ada secara remote; harap gunakan sync terlebih dahulu untuk memastikan konsistensi", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json b/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json index 4acbf1c06e4..8c456addbf3 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-ja-JP.json @@ -484,6 +484,10 @@ "ORG_ZSTACK_COMPUTE_VM_DEVICES_10007": "UUID:%sの仮想マシンが見つかりません", "ORG_ZSTACK_VPC_10019": "マスターとスレーブの状態が一致しないため、VPCルーター[uuid:%s]からL3ネットワークをデタッチできませんでした", "ORG_ZSTACK_COMPUTE_VM_DEVICES_10008": "uuid:%sの仮想マシンデバイスが見つかりません", + "ORG_ZSTACK_COMPUTE_VM_DEVICES_10009": "VMデバイスのTPM/host-file操作に失敗しました: %s", + "ORG_ZSTACK_KVM_10163": "KVM vTPM/host-file操作に失敗しました: %s", + "ORG_ZSTACK_STORAGE_VOLUME_10101": "ボリュームプロトコル操作に失敗しました: %s", + "ORG_ZSTACK_STORAGE_SNAPSHOT_10030": "ボリュームスナップショット操作に失敗しました: %s", "ORG_ZSTACK_STORAGE_CEPH_PRIMARY_10030": "準備ができてアクセス可能な画像がないため、画像を[%s]からプライマリストレージ[%s]にダウンロードするためのバックアップストレージが見つかりません", "ORG_ZSTACK_VPC_10018": "状態が実行中または停止していないため、VPCルーター[uuid:%s]からL3ネットワークをデタッチできませんでした", "ORG_ZSTACK_ALIYUN_CORE_10037": "画像はリモートに既に存在します。整合性を確保するために、まず同期を使用してください。", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json b/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json index d9d5de55de9..a7da8dcf59e 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-ko-KR.json @@ -484,6 +484,10 @@ "ORG_ZSTACK_COMPUTE_VM_DEVICES_10007": "UUID가 %s인 가상 머신을 찾을 수 없습니다", "ORG_ZSTACK_VPC_10019": "마스터와 슬레이브의 상태가 일치하지 않아 VPC 라우터[uuid:%s]에서 L3 네트워크를 분리할 수 없습니다", "ORG_ZSTACK_COMPUTE_VM_DEVICES_10008": "uuid가 %s인 가상 머신 디바이스를 찾을 수 없습니다", + "ORG_ZSTACK_COMPUTE_VM_DEVICES_10009": "VM 디바이스 TPM/host-file 작업에 실패했습니다: %s", + "ORG_ZSTACK_KVM_10163": "KVM vTPM/host-file 작업에 실패했습니다: %s", + "ORG_ZSTACK_STORAGE_VOLUME_10101": "볼륨 프로토콜 작업에 실패했습니다: %s", + "ORG_ZSTACK_STORAGE_SNAPSHOT_10030": "볼륨 스냅샷 작업에 실패했습니다: %s", "ORG_ZSTACK_STORAGE_CEPH_PRIMARY_10030": "준비되고 접근 가능한 이미지가 부족하여 이미지 [%s]를 주 스토리지 [%s]로 다운로드할 백업 스토리지를 찾을 수 없습니다", "ORG_ZSTACK_VPC_10018": "VPC 라우터[uuid:%s]의 상태가 실행 중이거나 중지되어 있지 않아 L3 네트워크를 분리할 수 없습니다", "ORG_ZSTACK_ALIYUN_CORE_10037": "이미지가 원격지에 존재합니다. 동기화(sync)를 먼저 사용하여 일관성을 확보하세요.", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json b/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json index 2bcbcfe039e..63d3868899b 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-ru-RU.json @@ -484,6 +484,10 @@ "ORG_ZSTACK_COMPUTE_VM_DEVICES_10007": "не удается найти виртуальную машину с UUID: %s", "ORG_ZSTACK_VPC_10019": "не удалось отсоединить сеть L3 от VPC-роутера [uuid:%s], так как состояния основного и резервного узлов несовместимы", "ORG_ZSTACK_COMPUTE_VM_DEVICES_10008": "не удается найти устройство виртуальной машины с uuid: %s", + "ORG_ZSTACK_COMPUTE_VM_DEVICES_10009": "операция TPM/host-file устройства ВМ не удалась: %s", + "ORG_ZSTACK_KVM_10163": "операция KVM vTPM/host-file не удалась: %s", + "ORG_ZSTACK_STORAGE_VOLUME_10101": "операция протокола тома не удалась: %s", + "ORG_ZSTACK_STORAGE_SNAPSHOT_10030": "операция снимка тома не удалась: %s", "ORG_ZSTACK_STORAGE_CEPH_PRIMARY_10030": "не удается найти резервное хранилище для загрузки образа [%s] в первичное хранилище [%s] из-за отсутствия готового и доступного образа", "ORG_ZSTACK_VPC_10018": "не удалось отсоединить сеть L3 от VPC-роутера [uuid:%s], так как его состояние не является запущенным или остановленным", "ORG_ZSTACK_ALIYUN_CORE_10037": "образ уже существует удаленно; сначала используйте sync для обеспечения согласованности", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json b/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json index 3c8c04b2cf7..8f88ad642a5 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-th-TH.json @@ -484,6 +484,10 @@ "ORG_ZSTACK_COMPUTE_VM_DEVICES_10007": "", "ORG_ZSTACK_VPC_10019": "ไม่พบเครื่องเสมือนที่มี UUID: %s", "ORG_ZSTACK_COMPUTE_VM_DEVICES_10008": "", + "ORG_ZSTACK_COMPUTE_VM_DEVICES_10009": "การดำเนินการ TPM/host-file ของอุปกรณ์ VM ล้มเหลว: %s", + "ORG_ZSTACK_KVM_10163": "การดำเนินการ KVM vTPM/host-file ล้มเหลว: %s", + "ORG_ZSTACK_STORAGE_VOLUME_10101": "การดำเนินการโปรโตคอลของโวลุ่มล้มเหลว: %s", + "ORG_ZSTACK_STORAGE_SNAPSHOT_10030": "การดำเนินการสแนปช็อตของโวลุ่มล้มเหลว: %s", "ORG_ZSTACK_STORAGE_CEPH_PRIMARY_10030": "ไม่สามารถถอดเครือขาย L3 จากเราเตอร์ VPC [uuid:%s] ได้เนื่องจากสถานะของ master และ slave ไม่สอดคล้องกัน", "ORG_ZSTACK_VPC_10018": "", "ORG_ZSTACK_ALIYUN_CORE_10037": "ไม่พบอุปกรณ์เครื่องเสมือนที่มี uuid: %s", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json index 0c36ae4980b..26a7beb03b4 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-zh_CN.json @@ -488,6 +488,10 @@ "ORG_ZSTACK_COMPUTE_VM_DEVICES_10007": "无法找到具有 UUID:%s 的 VM", "ORG_ZSTACK_VPC_10019": "无法将三层网络从uuid:%s的VPC路由器解绑,因为主从状态不一致", "ORG_ZSTACK_COMPUTE_VM_DEVICES_10008": "无法找到具有 UUID:%s 的虚拟机设备", + "ORG_ZSTACK_COMPUTE_VM_DEVICES_10009": "虚拟机设备 TPM/host-file 操作失败:%s", + "ORG_ZSTACK_KVM_10163": "KVM vTPM/host-file 操作失败:%s", + "ORG_ZSTACK_STORAGE_VOLUME_10101": "云盘协议操作失败:%s", + "ORG_ZSTACK_STORAGE_SNAPSHOT_10030": "云盘快照操作失败:%s", "ORG_ZSTACK_STORAGE_CEPH_PRIMARY_10030": "无法找到备份存储以将图像 [%s] 下载到主存储 [%s],原因是没有可用的 Ready 和可访问的图像", "ORG_ZSTACK_VPC_10018": "无法将三层网络从VPC路由器[uuid:%s]解绑,因为其状态不是运行中或已停止", "ORG_ZSTACK_ALIYUN_CORE_10037": "镜像已在远程存在,请先使用同步。", diff --git a/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json b/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json index 16f440db0ea..c304c781941 100644 --- a/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json +++ b/conf/i18n/globalErrorCodeMapping/global-error-zh_TW.json @@ -484,6 +484,10 @@ "ORG_ZSTACK_COMPUTE_VM_DEVICES_10007": "無法找到具有 UUID:%s 的 VM", "ORG_ZSTACK_VPC_10019": "無法将三层网络从uuid:%s的VPC路由器解綁,因为主从状态不一致", "ORG_ZSTACK_COMPUTE_VM_DEVICES_10008": "無法找到具有 UUID:%s 的虚拟機設备", + "ORG_ZSTACK_COMPUTE_VM_DEVICES_10009": "虛擬機設備 TPM/host-file 操作失敗:%s", + "ORG_ZSTACK_KVM_10163": "KVM vTPM/host-file 操作失敗:%s", + "ORG_ZSTACK_STORAGE_VOLUME_10101": "雲盤協議操作失敗:%s", + "ORG_ZSTACK_STORAGE_SNAPSHOT_10030": "雲盤快照操作失敗:%s", "ORG_ZSTACK_STORAGE_CEPH_PRIMARY_10030": "無法找到备份儲儲以将圖像 [%s] 下载到主儲儲 [%s],原因是没有可用的 Ready 和可访问的圖像", "ORG_ZSTACK_VPC_10018": "無法将三层网络从VPC路由器[uuid:%s]解綁,因为其状态不是運行中或已停止", "ORG_ZSTACK_ALIYUN_CORE_10037": "镜像已在远程儲在,請先使用同步。", diff --git a/header/src/main/java/org/zstack/header/message/DocUtils.java b/header/src/main/java/org/zstack/header/message/DocUtils.java index 5e9957cf4d8..39c25faf252 100644 --- a/header/src/main/java/org/zstack/header/message/DocUtils.java +++ b/header/src/main/java/org/zstack/header/message/DocUtils.java @@ -1,5 +1,8 @@ package org.zstack.header.message; +import org.zstack.utils.StringDSL; + +import java.sql.Timestamp; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -30,4 +33,12 @@ public static String uuidForAPIDoc(){ public static void removeApiUuidMap(String apiName){ apiUuidMap.remove(apiName); } + + public static String createFixedUuid(Class voClass) { + return StringDSL.createFixedUuid(voClass); + } + + public static Timestamp timestamp() { + return new Timestamp(date); + } } diff --git a/header/src/main/java/org/zstack/header/message/Message.java b/header/src/main/java/org/zstack/header/message/Message.java index 42f9fcc6f08..1b2db87baaf 100755 --- a/header/src/main/java/org/zstack/header/message/Message.java +++ b/header/src/main/java/org/zstack/header/message/Message.java @@ -54,6 +54,10 @@ protected static String uuid() { } } + protected static String uuid(Class voClass) { + return DocUtils.createFixedUuid(voClass); + } + public Message() { createdTime = System.currentTimeMillis(); id = UUID.randomUUID().toString().replace("-", ""); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index 4ad5d946694..ebb7e03866c 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -5452,7 +5452,8 @@ private void handle(SecretHostGetMsg msg) { SecretHostGetReply reply = new SecretHostGetReply(); if (StringUtils.isBlank(msg.getVmUuid()) || StringUtils.isBlank(msg.getPurpose()) || msg.getKeyVersion() == null || StringUtils.isBlank(msg.getUsageInstance())) { - reply.setError(operr("vmUuid, purpose, keyVersion and usageInstance are required for get secret")); + reply.setError(operr(ORG_ZSTACK_KVM_10163, + "vmUuid, purpose, keyVersion and usageInstance are required for get secret")); bus.reply(msg, reply); return; } @@ -5470,7 +5471,7 @@ private void handle(SecretHostGetMsg msg) { restf.asyncJsonPost(url, http.commandStr, headers, new JsonAsyncRESTCallback(msg, reply) { @Override public void fail(ErrorCode err) { - reply.setError(err != null ? err : operr("get secret on agent failed")); + reply.setError(err != null ? err : operr(ORG_ZSTACK_KVM_10163, "get secret on agent failed")); bus.reply(msg, reply); } @@ -5499,7 +5500,8 @@ public Class getReturnClass() { private void handle(ResolveVtpmLibvirtSecretOnHypervisorMsg msg) { ResolveVtpmLibvirtSecretOnHypervisorReply reply = new ResolveVtpmLibvirtSecretOnHypervisorReply(); if (StringUtils.isBlank(msg.getVmUuid()) || StringUtils.isBlank(msg.getHostUuid())) { - reply.setError(operr("vmUuid and hostUuid are required for vTPM resolve libvirt secret uuid")); + reply.setError(operr(ORG_ZSTACK_KVM_10163, + "vmUuid and hostUuid are required for vTPM resolve libvirt secret uuid")); bus.reply(msg, reply); return; } @@ -5525,7 +5527,7 @@ public void run(MessageReply r) { if (rsp != null && rsp.isSuccess() && StringUtils.isNotBlank(rsp.getSecretUuid())) { reply.setSecretUuid(rsp.getSecretUuid()); } else if (rsp != null && rsp.isSuccess()) { - reply.setError(operr("vTPM resolve succeeded but secretUuid is empty")); + reply.setError(operr(ORG_ZSTACK_KVM_10163, "vTPM resolve succeeded but secretUuid is empty")); } else { reply.setError(buildSecretAgentError(rsp, "vTPM resolve libvirt secret uuid failed")); } @@ -5537,13 +5539,14 @@ public void run(MessageReply r) { private void handle(SecretHostDefineMsg msg) { SecretHostDefineReply reply = new SecretHostDefineReply(); if (org.apache.commons.lang.StringUtils.isBlank(msg.getDekBase64())) { - reply.setError(operr("dekBase64 is required")); + reply.setError(operr(ORG_ZSTACK_KVM_10163, "dekBase64 is required")); bus.reply(msg, reply); return; } if (StringUtils.isBlank(msg.getVmUuid()) || StringUtils.isBlank(msg.getPurpose()) || msg.getKeyVersion() == null || StringUtils.isBlank(msg.getUsageInstance())) { - reply.setError(operr("vmUuid, purpose, keyVersion and usageInstance are required for ensure secret")); + reply.setError(operr(ORG_ZSTACK_KVM_10163, + "vmUuid, purpose, keyVersion and usageInstance are required for ensure secret")); bus.reply(msg, reply); return; } @@ -5552,19 +5555,20 @@ private void handle(SecretHostDefineMsg msg) { String pubKey = identity != null ? org.apache.commons.lang.StringUtils.trimToNull(identity.getPublicKey()) : null; Boolean verifyOk = identity != null ? identity.getVerified() : null; if (pubKey == null) { - reply.setError(operr("no public key for host, connect/reconnect did not sync key")); + reply.setError(operr(ORG_ZSTACK_KVM_10163, "no public key for host, connect/reconnect did not sync key")); bus.reply(msg, reply); return; } String storedFingerprint = StringUtils.trimToNull(identity.getFingerprint()); String computed = HostKeyIdentityHelper.fingerprintFromPublicKey(pubKey); if (storedFingerprint == null || !StringUtils.equals(storedFingerprint, computed)) { - reply.setError(operr("host public key fingerprint mismatch, key may be corrupted or tampered")); + reply.setError(operr(ORG_ZSTACK_KVM_10163, + "host public key fingerprint mismatch, key may be corrupted or tampered")); bus.reply(msg, reply); return; } if (!Boolean.TRUE.equals(verifyOk)) { - reply.setError(operr("host secret key verify not ok, not synced")); + reply.setError(operr(ORG_ZSTACK_KVM_10163, "host secret key verify not ok, not synced")); bus.reply(msg, reply); return; } @@ -5572,18 +5576,18 @@ private void handle(SecretHostDefineMsg msg) { try { dekRaw = java.util.Base64.getDecoder().decode(msg.getDekBase64().trim()); } catch (IllegalArgumentException e) { - reply.setError(operr("invalid dekBase64: %s", e.getMessage())); + reply.setError(operr(ORG_ZSTACK_KVM_10163, "invalid dekBase64: %s", e.getMessage())); bus.reply(msg, reply); return; } if (dekRaw == null || dekRaw.length == 0) { - reply.setError(operr("dekBase64 decoded to empty")); + reply.setError(operr(ORG_ZSTACK_KVM_10163, "dekBase64 decoded to empty")); bus.reply(msg, reply); return; } if (dekRaw.length > KVMConstant.MAX_DEK_BYTES) { - reply.setError(operr("dekBase64 decoded payload is too large")); + reply.setError(operr(ORG_ZSTACK_KVM_10163, "dekBase64 decoded payload is too large")); bus.reply(msg, reply); return; } @@ -5592,18 +5596,19 @@ private void handle(SecretHostDefineMsg msg) { try { pubKeyBytes = java.util.Base64.getDecoder().decode(pubKey); } catch (IllegalArgumentException e) { - reply.setError(operr("invalid host public key in DB: %s", e.getMessage())); + reply.setError(operr(ORG_ZSTACK_KVM_10163, "invalid host public key in DB: %s", e.getMessage())); bus.reply(msg, reply); return; } if (pubKeyBytes == null || pubKeyBytes.length != 32) { - reply.setError(operr("host public key must be 32 bytes (X25519)")); + reply.setError(operr(ORG_ZSTACK_KVM_10163, "host public key must be 32 bytes (X25519)")); bus.reply(msg, reply); return; } java.util.List sealers = pluginRegistry.getExtensionList(HostSecretEnvelopeCryptoExtensionPoint.class); if (sealers == null || sealers.isEmpty()) { - reply.setError(operr("host secret envelope sealer not available (premium crypto module required)")); + reply.setError(operr(ORG_ZSTACK_KVM_10163, + "host secret envelope sealer not available (premium crypto module required)")); bus.reply(msg, reply); return; } @@ -5611,7 +5616,7 @@ private void handle(SecretHostDefineMsg msg) { try { envelope = sealers.get(0).seal(pubKeyBytes, dekRaw); } catch (Exception e) { - reply.setError(operr("HPKE seal failed: %s", e.getMessage())); + reply.setError(operr(ORG_ZSTACK_KVM_10163, "HPKE seal failed: %s", e.getMessage())); bus.reply(msg, reply); return; } @@ -5632,7 +5637,7 @@ private void handle(SecretHostDefineMsg msg) { restf.asyncJsonPost(url, http.commandStr, headers, new JsonAsyncRESTCallback(msg, reply) { @Override public void fail(ErrorCode err) { - reply.setError(err != null ? err : operr("ensure secret on agent failed")); + reply.setError(err != null ? err : operr(ORG_ZSTACK_KVM_10163, "ensure secret on agent failed")); bus.reply(msg, reply); } @@ -5659,7 +5664,8 @@ private void handle(SecretHostDeleteMsg msg) { SecretHostDeleteReply reply = new SecretHostDeleteReply(); if (StringUtils.isBlank(msg.getVmUuid()) || StringUtils.isBlank(msg.getPurpose()) || StringUtils.isBlank(msg.getUsageInstance()) || msg.getKeyVersion() == null) { - reply.setError(operr("vmUuid, purpose, keyVersion and usageInstance are required for delete secret")); + reply.setError(operr(ORG_ZSTACK_KVM_10163, + "vmUuid, purpose, keyVersion and usageInstance are required for delete secret")); bus.reply(msg, reply); return; } @@ -5677,7 +5683,7 @@ private void handle(SecretHostDeleteMsg msg) { restf.asyncJsonPost(url, http.commandStr, headers, new JsonAsyncRESTCallback(msg, reply) { @Override public void fail(ErrorCode err) { - reply.setError(err != null ? err : operr("delete secret on agent failed")); + reply.setError(err != null ? err : operr(ORG_ZSTACK_KVM_10163, "delete secret on agent failed")); bus.reply(msg, reply); } @@ -5702,7 +5708,7 @@ public Class getReturnClass() { private ErrorCode buildSecretAgentError(KVMAgentCommands.AgentResponse rsp, String defaultMessage) { String raw = rsp != null ? StringUtils.trimToNull(rsp.getError()) : null; if (raw == null) { - return operr(defaultMessage); + return operr(ORG_ZSTACK_KVM_10163, defaultMessage); } String stable = stableKeyAgentErrorCodeFromRawMessage(raw); if (stable != null) { @@ -5711,7 +5717,7 @@ private ErrorCode buildSecretAgentError(KVMAgentCommands.AgentResponse rsp, Stri err.setDetails(raw); return err; } - return operr("%s: %s", defaultMessage, raw); + return operr(ORG_ZSTACK_KVM_10163, "%s: %s", defaultMessage, raw); } private static String stableKeyAgentErrorCodeFromRawMessage(String raw) { diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java index 17b619b0dc8..51713c6c7c2 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java @@ -77,6 +77,7 @@ import java.util.Objects; import static org.zstack.core.Platform.operr; +import static org.zstack.header.vm.additions.VmHostFileSyncReason.PreMigration; import static org.zstack.header.vm.additions.VmHostFileSyncReason.PostMigration; import static org.zstack.header.vm.additions.VmHostFileSyncReason.BeforeHaStart; import static org.zstack.header.vm.additions.VmHostFileSyncReason.PrepareReRead; @@ -86,6 +87,7 @@ import static org.zstack.header.vm.additions.VmHostFileType.NvRam; import static org.zstack.kvm.KVMConstant.*; import static org.zstack.utils.CollectionDSL.list; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; public class KvmSecureBootExtensions implements KVMStartVmExtensionPoint, PreVmInstantiateResourceExtensionPoint, @@ -283,13 +285,14 @@ public void rewriteVmHostFiles(RewriteVmHostFilesContext context, Completion com sender.send(cmd, WRITE_VM_HOST_FILE_PATH, wrapper -> { KVMAgentCommands.WriteVmHostFileContentResponse writeRsp = wrapper.getResponse(KVMAgentCommands.WriteVmHostFileContentResponse.class); return writeRsp.isSuccess() ? null : - operr("failed to write file content response").causedBy(writeRsp.getError()); + operr(ORG_ZSTACK_KVM_10163, "failed to write file content response: %s", writeRsp.getError()); }, new ReturnValueCompletion(completion) { @Override public void success(KvmResponseWrapper wrapper) { KVMAgentCommands.WriteVmHostFileContentResponse writeRsp = wrapper.getResponse(KVMAgentCommands.WriteVmHostFileContentResponse.class); if (!writeRsp.isSuccess()) { - completion.fail(operr("failed to write file content response").causedBy(writeRsp.getError())); + completion.fail(operr(ORG_ZSTACK_KVM_10163, + "failed to write file content response: %s", writeRsp.getError())); return; } completion.success(); @@ -472,7 +475,7 @@ public void run(FlowTrigger trigger, Map data) { .eq(VmHostBackupFileVO_.uuid, context.backupUuid) .find(); if (context.vmBackupFileVO == null) { - trigger.fail(operr("cannot find matched vm-host backup file[backupUuid:%s]", + trigger.fail(operr(ORG_ZSTACK_KVM_10163, "cannot find matched vm-host backup file[backupUuid:%s]", context.backupUuid)); return; } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootManager.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootManager.java index 3546e9c4174..cd65bf8de3e 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootManager.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootManager.java @@ -90,6 +90,7 @@ import static org.zstack.kvm.KVMConstant.*; import static org.zstack.utils.CollectionDSL.list; import static org.zstack.utils.CollectionUtils.*; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; public class KvmSecureBootManager extends AbstractService implements VmHostFileManager { private static final CLogger logger = Utils.getLogger(KvmSecureBootManager.class); @@ -304,13 +305,14 @@ private void handle(SyncVmHostFilesFromHostMsg msg) { sender.send(cmd, READ_VM_HOST_FILE_PATH, wrapper -> { KVMAgentCommands.ReadVmHostFileContentResponse readRsp = wrapper.getResponse(KVMAgentCommands.ReadVmHostFileContentResponse.class); return readRsp.isSuccess() ? null : - operr("failed to read file content response").causedBy(readRsp.getError()); + operr(ORG_ZSTACK_KVM_10163, "failed to read file content response: %s", readRsp.getError()); }, new ReturnValueCompletion(msg) { @Override public void success(KvmResponseWrapper wrapper) { KVMAgentCommands.ReadVmHostFileContentResponse readRsp = wrapper.getResponse(KVMAgentCommands.ReadVmHostFileContentResponse.class); if (!readRsp.isSuccess()) { - reply.setError(operr("failed to read file content response").causedBy(readRsp.getError())); + reply.setError(operr(ORG_ZSTACK_KVM_10163, + "failed to read file content response: %s", readRsp.getError())); bus.reply(msg, reply); return; } @@ -363,9 +365,8 @@ private ErrorCode syncToHostFiles(SyncVmHostFilesFromHostMsg msg, continue; } if (to.getError() != null) { - errors.add(operr("failed to read file %s", path) - .withOpaque("path", path) - .causedBy(to.getError())); + errors.add(operr(ORG_ZSTACK_KVM_10163, "failed to read file %s: %s", path, to.getError()) + .withOpaque("path", path)); continue; } @@ -437,14 +438,14 @@ protected void scripts() { return null; } - return operr("failed to read file content from host[uuid=%s]", msg.getHostUuid()) + return operr(ORG_ZSTACK_KVM_10163, "failed to read file content from host[uuid=%s]", msg.getHostUuid()) .causedBy(errors); } private ErrorCode syncToBackupFiles(SyncVmHostFilesFromHostMsg msg, KVMAgentCommands.ReadVmHostFileContentResponse readRsp) { if (msg.getBackupResourceUuid() == null || msg.getBackupResourceUuid().isEmpty()) { - return operr("backupResourceUuid is required when syncToBackup is true"); + return operr(ORG_ZSTACK_KVM_10163, "backupResourceUuid is required when syncToBackup is true"); } // Query the source VmHostFileVO records for afterBackup callback @@ -465,13 +466,13 @@ private ErrorCode syncToBackupFiles(SyncVmHostFilesFromHostMsg msg, continue; } if (to.getError() != null) { - errors.add(operr("failed to read backup file %s", to.getPath()) - .withOpaque("path", to.getPath()) - .causedBy(to.getError())); + errors.add(operr(ORG_ZSTACK_KVM_10163, + "failed to read backup file %s: %s", to.getPath(), to.getError()) + .withOpaque("path", to.getPath())); continue; } if (to.getContentBase64() == null) { - errors.add(operr("backup file %s returns empty content", to.getPath()) + errors.add(operr(ORG_ZSTACK_KVM_10163, "backup file %s returns empty content", to.getPath()) .withOpaque("path", to.getPath())); continue; } @@ -545,7 +546,7 @@ protected void scripts() { return null; } - return operr("failed to read backup file content from host[uuid=%s]", msg.getHostUuid()) + return operr(ORG_ZSTACK_KVM_10163, "failed to read backup file content from host[uuid=%s]", msg.getHostUuid()) .causedBy(errors); } @@ -667,7 +668,7 @@ public void run(MessageReply reply) { }).run(new WhileDoneCompletion(trigger) { @Override public void done(ErrorCodeList errorCodeList) { - if (errorCodeList == null || errorCodeList.isEmpty()) { + if (errorCodeList == null || errorCodeList.getCauses().isEmpty()) { trigger.next(); return; } @@ -678,8 +679,8 @@ public void done(ErrorCodeList errorCodeList) { msg.getSrcVmUuid(), details)); trigger.next(); } else { - trigger.fail(operr("failed to sync host file for VM[uuid=%s]", msg.getSrcVmUuid()) - .withCause(errorCodeList)); + trigger.fail(operr(ORG_ZSTACK_KVM_10163, "failed to sync host file for VM[uuid=%s]", msg.getSrcVmUuid()) + .causedBy(errorCodeList)); } } }); @@ -863,7 +864,7 @@ private void handle(RestoreVmHostFileMsg msg) { .eq(VmInstanceVO_.uuid, msg.getVmInstanceUuid()) .findTuple(); if (tuple == null) { - reply.setError(operr("VM instance [uuid:%s] not found", msg.getVmInstanceUuid())); + reply.setError(operr(ORG_ZSTACK_KVM_10163, "VM instance [uuid:%s] not found", msg.getVmInstanceUuid())); bus.reply(msg, reply); return; } @@ -873,7 +874,7 @@ private void handle(RestoreVmHostFileMsg msg) { hostUuid = tuple.get(1, String.class); } if (hostUuid == null) { - reply.setError(operr("VM instance [uuid:%s] has no host", msg.getVmInstanceUuid())); + reply.setError(operr(ORG_ZSTACK_KVM_10163, "VM instance [uuid:%s] has no host", msg.getVmInstanceUuid())); bus.reply(msg, reply); return; } @@ -963,7 +964,8 @@ private void handle(RestoreVmHostFileMsg msg) { sender.send(cmd, WRITE_VM_HOST_FILE_PATH, wrapper -> { KVMAgentCommands.WriteVmHostFileContentResponse writeRsp = wrapper.getResponse(KVMAgentCommands.WriteVmHostFileContentResponse.class); return writeRsp.isSuccess() ? null : - operr("failed to write/delete host file response").causedBy(writeRsp.getError()); + operr(ORG_ZSTACK_KVM_10163, + "failed to write/delete host file response: %s", writeRsp.getError()); }, new ReturnValueCompletion(trigger) { @Override public void success(KvmResponseWrapper wrapper) { @@ -974,7 +976,7 @@ public void success(KvmResponseWrapper wrapper) { @Override public void fail(ErrorCode errorCode) { - trigger.fail(operr("failed to restore host files for VM[uuid:%s]", msg.getVmInstanceUuid()) + trigger.fail(operr(ORG_ZSTACK_KVM_10163, "failed to restore host files for VM[uuid:%s]", msg.getVmInstanceUuid()) .causedBy(errorCode)); } }); @@ -1091,9 +1093,9 @@ private void handle(BackupVmHostFileOnHypervisorMsg msg) { sender.send(cmd, BACKUP_VM_HOST_FILE_PATH, wrapper -> { KVMAgentCommands.BackupVmHostFileResponse rsp = wrapper.getResponse(KVMAgentCommands.BackupVmHostFileResponse.class); return rsp.isSuccess() ? null : - operr("failed to backup vm host file on hypervisor[hostUuid=%s]", msg.getHostUuid()) + operr(ORG_ZSTACK_KVM_10163, "failed to backup vm host file on hypervisor[hostUuid=%s]", msg.getHostUuid()) .withOpaque("host.uuid", msg.getHostUuid()) - .causedBy(rsp.getError()); + .withOpaque("agent.error", rsp.getError()); }, new ReturnValueCompletion(msg) { @Override public void success(KvmResponseWrapper wrapper) { @@ -1125,8 +1127,8 @@ private void handle(VmHostBackupFileDeletionMsg msg) { logger.warn(String.format("force delete VmHostBackupFile[uuid:%s] ignored cleanup error: %s", backup.getUuid(), e.getMessage()), e); } else { - reply.setError(operr("failed to delete VmHostBackupFile[uuid:%s]", backup.getUuid()) - .withException(e)); + reply.setError(operr(ORG_ZSTACK_KVM_10163, "failed to delete VmHostBackupFile[uuid:%s]: %s", + backup.getUuid(), e.getMessage())); } } } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmVmHostFileFactory.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmVmHostFileFactory.java index c4e13957cdf..98cedff3612 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmVmHostFileFactory.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmVmHostFileFactory.java @@ -2,6 +2,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.zstack.compute.vm.devices.VmTpmManager; +import org.zstack.header.errorcode.OperationFailureException; import org.zstack.header.vm.additions.VmHostBackupFileVO; import org.zstack.header.vm.additions.VmHostFileType; import org.zstack.header.vm.additions.VmHostFileVO; @@ -14,6 +15,7 @@ import java.util.Set; import static org.zstack.core.Platform.operr; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; public class KvmVmHostFileFactory { @Autowired @@ -25,7 +27,8 @@ public AbstractVmHostFileBase createBase(VmHostFileVO file) { switch (file.getType()) { case NvRam: return new NvRamVmHostFileBase(file); case TpmState: return new TpmStateVmHostFileBase(file); - default: throw operr("invalid VM host file type: " + file.getType()).toException(); + default: throw new OperationFailureException(operr(ORG_ZSTACK_KVM_10163, + "invalid VM host file type: %s", file.getType())); } } @@ -33,7 +36,8 @@ public AbstractVmHostBackupFileBase createBackupBase(VmHostBackupFileVO backupFi switch (backupFile.getType()) { case NvRam: return new NvRamVmHostBackupFileBase(backupFile); case TpmState: return new TpmStateVmHostBackupFileBase(backupFile); - default: throw operr("invalid VM host file type: " + backupFile.getType()).toException(); + default: throw new OperationFailureException(operr(ORG_ZSTACK_KVM_10163, + "invalid VM host file type: %s", backupFile.getType())); } } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmExtensions.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmExtensions.java index 6c0c35a70a5..534a08ab0be 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmExtensions.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmExtensions.java @@ -80,6 +80,7 @@ import static org.zstack.header.tpm.TpmConstants.SERVICE_ID; import static org.zstack.kvm.KVMConstant.*; import static org.zstack.core.Platform.operr; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; public class KvmTpmExtensions implements KVMStartVmExtensionPoint, PreVmInstantiateResourceExtensionPoint, @@ -331,7 +332,7 @@ public void run(MessageReply reply) { return; } - trigger.fail(errorCode != null ? errorCode : operr("get secret on host failed")); + trigger.fail(errorCode != null ? errorCode : operr(ORG_ZSTACK_KVM_10163, "get secret on host failed")); } }); } @@ -359,7 +360,7 @@ public void success(ResourceKeyResult result) { context.dekBase64 = result.getDekBase64(); context.keyVersion = result.getKeyVersion(); if (context.keyVersion == null) { - trigger.fail(operr("missing keyVersion for tpm[uuid:%s] after getOrCreateKey", context.tpmUuid)); + trigger.fail(operr(ORG_ZSTACK_KVM_10163, "missing keyVersion for tpm[uuid:%s] after getOrCreateKey", context.tpmUuid)); return; } trigger.next(); @@ -382,7 +383,7 @@ public boolean skip(Map data) { @Override public void run(FlowTrigger trigger, Map data) { if (context.dekBase64 == null) { - trigger.fail(operr("missing dekBase64 for tpm[uuid:%s] before define-secret-on-host", context.tpmUuid)); + trigger.fail(operr(ORG_ZSTACK_KVM_10163, "missing dekBase64 for tpm[uuid:%s] before define-secret-on-host", context.tpmUuid)); return; } @@ -619,11 +620,11 @@ private void ensureVtpmKeyBindingAndDekLoaded(VtpmMigratePreAgentContext ctx) { ctx.setProviderUuid(resourceKeyBackend.findKeyProviderUuidByTpm(tpmUuid)); ctx.setProviderName(resourceKeyBackend.findKeyProviderNameByTpm(tpmUuid)); if (StringUtils.isBlank(ctx.getProviderUuid()) && StringUtils.isBlank(ctx.getProviderName())) { - throw new OperationFailureException(operr("missing TPM resource key binding for tpm[uuid:%s] before migrate", tpmUuid)); + throw new OperationFailureException(operr(ORG_ZSTACK_KVM_10163, "missing TPM resource key binding for tpm[uuid:%s] before migrate", tpmUuid)); } ctx.setKeyVersion(resourceKeyBackend.findKeyVersionByTpm(tpmUuid)); if (ctx.getKeyVersion() == null) { - throw new OperationFailureException(operr("cannot find keyVersion for tpm[uuid:%s] before migrate", tpmUuid)); + throw new OperationFailureException(operr(ORG_ZSTACK_KVM_10163, "cannot find keyVersion for tpm[uuid:%s] before migrate", tpmUuid)); } GetOrCreateResourceKeyContext keyCtx = new GetOrCreateResourceKeyContext(); @@ -634,7 +635,7 @@ private void ensureVtpmKeyBindingAndDekLoaded(VtpmMigratePreAgentContext ctx) { keyCtx.setPurpose("vtpm"); ResourceKeyResult result = resourceKeyManager.getKey(keyCtx); if (StringUtils.isBlank(result.getDekBase64())) { - throw new OperationFailureException(operr("missing DEK for tpm[uuid:%s] after getKey before migrate", tpmUuid)); + throw new OperationFailureException(operr(ORG_ZSTACK_KVM_10163, "missing DEK for tpm[uuid:%s] after getKey before migrate", tpmUuid)); } ctx.setResourceKeyResult(result); } @@ -674,7 +675,7 @@ public void run(MessageReply r) { private void ensureDestinationHostVtpmSecretDefined(VtpmMigratePreAgentContext ctx, Completion completion) { ResourceKeyResult keyResult = ctx.getResourceKeyResult(); if (keyResult == null || StringUtils.isBlank(keyResult.getDekBase64())) { - completion.fail(operr("missing DEK for tpm[uuid:%s] before ensure secret on destination", ctx.getTpmUuid())); + completion.fail(operr(ORG_ZSTACK_KVM_10163, "missing DEK for tpm[uuid:%s] before ensure secret on destination", ctx.getTpmUuid())); return; } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmManager.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmManager.java index 7da06ad11cd..0ab602cef63 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmManager.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/KvmTpmManager.java @@ -24,6 +24,7 @@ import org.zstack.header.core.workflow.Flow; import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.errorcode.ErrorCodeList; +import org.zstack.header.errorcode.OperationFailureException; import org.zstack.header.host.HostConstant; import org.zstack.header.message.APIMessage; import org.zstack.header.message.Message; @@ -102,6 +103,7 @@ import static org.zstack.utils.CollectionDSL.list; import static org.zstack.utils.CollectionUtils.isEmpty; import static org.zstack.utils.CollectionUtils.transform; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; public class KvmTpmManager extends AbstractService { private static final CLogger logger = Utils.getLogger(KvmTpmManager.class); @@ -189,7 +191,8 @@ private void handleApiMessage(APIMessage msg) { private void handle(AddTpmMsg msg) { if (msg.getKeyProviderUuid() != null && msg.getResourceUuidKeyFrom() != null) { - throw operr("keyProviderUuid and resourceUuidKeyFrom cannot be set at the same time").toException(); + throw new OperationFailureException(operr(ORG_ZSTACK_KVM_10163, + "keyProviderUuid and resourceUuidKeyFrom cannot be set at the same time")); } AddTpmReply reply = new AddTpmReply(); @@ -258,7 +261,7 @@ private void addTpmToVm(AddTpmToVmContext context, Completion completion) { .find(); if (!SUPPORT_VM_STATES_FOR_TPM_OPERATION.contains(vm.getState())) { - trigger.fail(err(VM_STATE_ERROR, + trigger.fail(err(ORG_ZSTACK_KVM_10163, VM_STATE_ERROR, "The current VM state does not support adding TPM operations") .withOpaque("support.vm.state", SUPPORT_VM_STATES_FOR_TPM_OPERATION)); return; @@ -396,7 +399,7 @@ private void removeTpmFromVm(RemoveTpmFromVmContext context, Completion completi .find(); if (!SUPPORT_VM_STATES_FOR_TPM_OPERATION.contains(vm.getState())) { - trigger.fail(err(VM_STATE_ERROR, + trigger.fail(err(ORG_ZSTACK_KVM_10163, VM_STATE_ERROR, "The current VM state does not support removing TPM operations") .withOpaque("support.vm.state", SUPPORT_VM_STATES_FOR_TPM_OPERATION)); return; @@ -441,7 +444,7 @@ private void removeTpmFromVm(RemoveTpmFromVmContext context, Completion completi new KvmCommandSender(hostUuid).send(cmd, KVMConstant.WRITE_VM_HOST_FILE_PATH, wrapper -> { KVMAgentCommands.WriteVmHostFileContentResponse rsp = wrapper.getResponse(KVMAgentCommands.WriteVmHostFileContentResponse.class); - return rsp.isSuccess() ? null : operr("failed to delete host files on host[uuid=%s]", hostUuid); + return rsp.isSuccess() ? null : operr(ORG_ZSTACK_KVM_10163, "failed to delete host files on host[uuid=%s]", hostUuid); }, new ReturnValueCompletion(whileCompletion) { @Override public void success(KvmResponseWrapper wrapper) { @@ -632,11 +635,11 @@ public void fail(ErrorCode errorCode) { }).run(new WhileDoneCompletion(trigger) { @Override public void done(ErrorCodeList errorCodeList) { - if (errorCodeList.isEmpty()) { + if (errorCodeList.getCauses().isEmpty()) { trigger.next(); return; } - trigger.fail(operr("Failed to clone encrypted resource key") + trigger.fail(operr(ORG_ZSTACK_KVM_10163, "Failed to clone encrypted resource key") .withOpaque("src.tpm.uuid", originTpmUuid) .causedBy(errorCodeList)); } @@ -813,7 +816,7 @@ public void success() { @Override public void fail(ErrorCode errorCode) { - context.errorsOnSendCmd.add(errorCode.withOpaque("host.uuid", entry.getKey())); + context.errorsOnSendCmd.getCauses().add(errorCode.withOpaque("host.uuid", entry.getKey())); whileCompletion.done(); } }); @@ -838,13 +841,13 @@ public void done(ErrorCodeList errorCodeList) { .handle(trigger -> { // If any host failed to delete, abort the chain to preserve // the last-modified TPM record as a recovery point. - if (context.errorsOnSendCmd.hasError()) { - if (context.errorsOnSendCmd.size() == 1) { + if (!context.errorsOnSendCmd.getCauses().isEmpty()) { + if (context.errorsOnSendCmd.getCauses().size() == 1) { trigger.fail(context.errorsOnSendCmd.getCauses().get(0)); } else { - trigger.fail(operr("failed to delete TPM files on multiple hosts") + trigger.fail(operr(ORG_ZSTACK_KVM_10163, "failed to delete TPM files on multiple hosts") .withOpaque("vm.uuid", vmUuid) - .withCause(context.errorsOnSendCmd.getCauses())); + .causedBy(context.errorsOnSendCmd.getCauses())); } return; } @@ -926,7 +929,7 @@ public void run(MessageReply reply) { }).run(new WhileDoneCompletion(trigger) { @Override public void done(ErrorCodeList errorCodeList) { - if (errorCodeList.hasError()) { + if (!errorCodeList.getCauses().isEmpty()) { String details = String.join("\n", transform(errorCodeList.getCauses(), ErrorCode::getReadableDetails)); logger.warn("failed to clean backup files but still continue:\n" + details); } @@ -1076,6 +1079,7 @@ public void run(MessageReply reply) { } private void handle(APIUpdateTpmMsg msg) { - throw err(NOT_SUPPORTED, "UpdateTpm is not supported in current version").toException(); + throw new OperationFailureException(err(ORG_ZSTACK_KVM_10163, NOT_SUPPORTED, + "UpdateTpm is not supported in current version")); } } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileAllocatorExtensionPoint.java b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileAllocatorExtensionPoint.java index 981e0a9d678..15cf08f1f70 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileAllocatorExtensionPoint.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileAllocatorExtensionPoint.java @@ -2,7 +2,7 @@ import org.zstack.header.allocator.HostAllocatorFilterExtensionPoint; import org.zstack.header.allocator.HostAllocatorSpec; -import org.zstack.header.allocator.HostCandidate; +import org.zstack.header.host.HostVO; import org.zstack.header.vm.VmInstanceConstant; import org.zstack.header.vm.additions.VmHostFileType; import org.zstack.header.vm.additions.VmHostFileVO; @@ -11,9 +11,8 @@ import org.zstack.utils.logging.CLogger; import org.zstack.core.db.Q; -import com.google.common.base.Objects; - import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import static org.zstack.core.Platform.i18n; @@ -22,18 +21,18 @@ public class VmHostFileAllocatorExtensionPoint implements HostAllocatorFilterExt private static final CLogger logger = Utils.getLogger(VmHostFileAllocatorExtensionPoint.class); @Override - public void filter(List candidates, HostAllocatorSpec spec) { + public List filterHostCandidates(List candidates, HostAllocatorSpec spec) { if (spec == null) { - return; + return candidates; } String vmOperation = spec.getVmOperation(); if (vmOperation == null || !VmInstanceConstant.VmOperation.Start.toString().equals(vmOperation)) { - return; + return candidates; } if (spec.getVmInstance() == null) { - return; + return candidates; } String vmUuid = spec.getVmInstance().getUuid(); @@ -46,18 +45,22 @@ public void filter(List candidates, HostAllocatorSpec spec) { .notNull(VmHostFileVO_.changeDate) .listValues(); if (files.isEmpty()) { - return; + return candidates; } String types = files.stream() .map(Enum::name) .collect(Collectors.joining(",")); - for (HostCandidate c : candidates) { - if (!Objects.equal(lastHostUuid, c.getUuid())) { - String reason = i18n("only allowed start on last host: unsynchronized %s VM host files exist", types); - c.markAsRejected(getClass(), reason); - } - } + logger.debug(String.format("only allow VM[uuid:%s] start on last host[uuid:%s], unsynchronized %s VM host files exist", + vmUuid, lastHostUuid, types)); + return candidates.stream() + .filter(c -> Objects.equals(lastHostUuid, c.getUuid())) + .collect(Collectors.toList()); + } + + @Override + public String filterErrorReason() { + return i18n("only allowed start on last host: unsynchronized VM host files exist"); } } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileAutoCompleter.java b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileAutoCompleter.java index 640794a4329..6dccf665bb1 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileAutoCompleter.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileAutoCompleter.java @@ -6,7 +6,6 @@ import org.zstack.header.apimediator.GlobalApiMessageInterceptor; import org.zstack.header.message.APIMessage; import org.zstack.core.db.Q; -import org.zstack.header.vm.APIConvertTemplatedVmInstanceToVmInstanceMsg; import org.zstack.header.vm.APICreateVmInstanceFromVolumeSnapshotGroupMsg; import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO; import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO_; @@ -28,16 +27,13 @@ public class VmHostFileAutoCompleter implements GlobalApiMessageInterceptor { @SuppressWarnings("rawtypes") public List getMessageClassToIntercept() { return list( - APIConvertTemplatedVmInstanceToVmInstanceMsg.class, APICreateVmInstanceFromVolumeSnapshotGroupMsg.class ); } @Override public APIMessage intercept(APIMessage msg) throws ApiMessageInterceptionException { - if (msg instanceof APIConvertTemplatedVmInstanceToVmInstanceMsg) { - validate((APIConvertTemplatedVmInstanceToVmInstanceMsg) msg); - } else if (msg instanceof APICreateVmInstanceFromVolumeSnapshotGroupMsg) { + if (msg instanceof APICreateVmInstanceFromVolumeSnapshotGroupMsg) { validate((APICreateVmInstanceFromVolumeSnapshotGroupMsg) msg); } return msg; @@ -48,22 +44,6 @@ public InterceptorPosition getPosition() { return InterceptorPosition.FRONT; } - private void validate(APIConvertTemplatedVmInstanceToVmInstanceMsg msg) { - if (msg.getResetTpm() != null) { - return; - } - - // template VM is also a VM -> templateUuid is vmUuid - String templateUuid = msg.getTemplatedVmInstanceUuid(); - if (templateUuid == null) { - return; - } - - Boolean resolved = resourceConfigFacade.getResourceConfigValue( - VmGlobalConfig.RESET_TPM_AFTER_VM_CLONE, templateUuid, Boolean.class); - msg.setResetTpm(resolved); - } - private void validate(APICreateVmInstanceFromVolumeSnapshotGroupMsg msg) { if (msg.getResetTpm() != null) { return; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java index ee8557f3c09..e5caf259e54 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java @@ -59,6 +59,7 @@ import static org.zstack.core.Platform.operr; import static org.zstack.utils.CollectionUtils.transformToSet; +import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; public class VmHostFileTracker implements Component { private static final CLogger logger = Utils.getLogger(VmHostFileTracker.class); @@ -522,7 +523,7 @@ private boolean sendDeleteCommandToHost(VmHostFileVO file) { new KvmCommandSender(hostUuid).send(cmd, KVMConstant.WRITE_VM_HOST_FILE_PATH, wrapper -> { KVMAgentCommands.WriteVmHostFileContentResponse rsp = wrapper.getResponse(KVMAgentCommands.WriteVmHostFileContentResponse.class); - return rsp.isSuccess() ? null : operr("failed to delete host file[path=%s] on host[uuid=%s]", + return rsp.isSuccess() ? null : operr(ORG_ZSTACK_KVM_10163, "failed to delete host file[path=%s] on host[uuid=%s]", file.getPath(), hostUuid); }, future); diff --git a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotTreeBase.java b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotTreeBase.java index 16952ff8d96..3db27d21682 100755 --- a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotTreeBase.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotTreeBase.java @@ -2152,7 +2152,7 @@ public void run(MessageReply reply) { }).run(new WhileDoneCompletion(null) { @Override public void done(ErrorCodeList errorCodeList) { - if (errorCodeList.hasError()) { + if (!errorCodeList.getCauses().isEmpty()) { logger.warn("failed to delete some VmHostBackupFiles:\n" + String.join("\n", transform(errorCodeList.getCauses(), ErrorCode::getReadableDetails))); } diff --git a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java index c0f3b7d4e20..687039c87be 100644 --- a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java @@ -592,7 +592,7 @@ public void run(MessageReply r) { }, concurrencyNum).run(new WhileDoneCompletion(msg) { @Override public void done(ErrorCodeList errorCodeList) { - DebugUtils.Assert(!errorCodeList.hasError(), "no errorCode expected"); + DebugUtils.Assert(errorCodeList.getCauses().isEmpty(), "no errorCode expected"); trigger.next(); } }); diff --git a/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java b/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java index 39fd4713d74..8f5b88d8e8d 100755 --- a/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java +++ b/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java @@ -3480,7 +3480,7 @@ public void run(MessageReply reply) { }).run(new WhileDoneCompletion(trigger) { @Override public void done(ErrorCodeList errorCodeList) { - if (errorCodeList.hasError()) { + if (!errorCodeList.getCauses().isEmpty()) { logger.warn("failed to delete some VmHostBackupFiles:\n" + String.join("\n", transform(errorCodeList.getCauses(), ErrorCode::getReadableDetails))); } @@ -3753,7 +3753,8 @@ private void handle(APIChangeVolumeProtocolMsg msg) { .param("uuid", msg.getVolumeUuid()) .execute(); if (updated == 0) { - evt.setError(operr("volume[uuid:%s] no longer exists, cannot change its protocol", msg.getVolumeUuid())); + evt.setError(operr(ORG_ZSTACK_STORAGE_VOLUME_10101, + "volume[uuid:%s] no longer exists, cannot change its protocol", msg.getVolumeUuid())); bus.publish(evt); return; } diff --git a/testlib/src/main/java/org/zstack/testlib/Test.groovy b/testlib/src/main/java/org/zstack/testlib/Test.groovy index ba84a10c09a..696694fc6bb 100755 --- a/testlib/src/main/java/org/zstack/testlib/Test.groovy +++ b/testlib/src/main/java/org/zstack/testlib/Test.groovy @@ -28,6 +28,7 @@ import org.zstack.testlib.util.Retry import org.zstack.testlib.util.TestConfigUtils import org.zstack.utils.ShellUtils import org.zstack.utils.Utils +import org.zstack.utils.data.SizeUnit import org.zstack.utils.gson.JSONObjectUtil import org.zstack.utils.logging.CLogger import org.zstack.utils.path.PathUtil @@ -1093,4 +1094,8 @@ mysqldump -u root zstack > ${failureLogDir.absolutePath}/dbdump.sql StartMode getCaseMode() { return StartMode.DEFAULT } + + static long gb(double gb) { + return SizeUnit.GIGABYTE.toByte(gb) + } } diff --git a/utils/src/main/java/org/zstack/utils/StringDSL.java b/utils/src/main/java/org/zstack/utils/StringDSL.java index 3dcc9354281..705b88a8b4d 100755 --- a/utils/src/main/java/org/zstack/utils/StringDSL.java +++ b/utils/src/main/java/org/zstack/utils/StringDSL.java @@ -11,6 +11,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java index 9fc175de2c1..403f1013b0a 100644 --- a/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java +++ b/utils/src/main/java/org/zstack/utils/clouderrorcode/CloudOperationsErrorCode.java @@ -1400,6 +1400,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_STORAGE_SNAPSHOT_10029 = "ORG_ZSTACK_STORAGE_SNAPSHOT_10029"; + public static final String ORG_ZSTACK_STORAGE_SNAPSHOT_10030 = "ORG_ZSTACK_STORAGE_SNAPSHOT_10030"; + public static final String ORG_ZSTACK_ZWATCH_NAMESPACE_10000 = "ORG_ZSTACK_ZWATCH_NAMESPACE_10000"; public static final String ORG_ZSTACK_ZWATCH_NAMESPACE_10001 = "ORG_ZSTACK_ZWATCH_NAMESPACE_10001"; @@ -2748,6 +2750,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_KVM_10162 = "ORG_ZSTACK_KVM_10162"; + public static final String ORG_ZSTACK_KVM_10163 = "ORG_ZSTACK_KVM_10163"; + public static final String ORG_ZSTACK_ZSV_10000 = "ORG_ZSTACK_ZSV_10000"; public static final String ORG_ZSTACK_ZSV_10001 = "ORG_ZSTACK_ZSV_10001"; @@ -11537,6 +11541,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_STORAGE_VOLUME_10100 = "ORG_ZSTACK_STORAGE_VOLUME_10100"; + public static final String ORG_ZSTACK_STORAGE_VOLUME_10101 = "ORG_ZSTACK_STORAGE_VOLUME_10101"; + public static final String ORG_ZSTACK_TEST_INTEGRATION_PREMIUM_STORAGE_PRIMARY_BLOCK_VENDOR_XSTOR_10000 = "ORG_ZSTACK_TEST_INTEGRATION_PREMIUM_STORAGE_PRIMARY_BLOCK_VENDOR_XSTOR_10000"; public static final String ORG_ZSTACK_TEST_INTEGRATION_PREMIUM_STORAGE_PRIMARY_BLOCK_VENDOR_XSTOR_10001 = "ORG_ZSTACK_TEST_INTEGRATION_PREMIUM_STORAGE_PRIMARY_BLOCK_VENDOR_XSTOR_10001"; @@ -12875,6 +12881,8 @@ public class CloudOperationsErrorCode { public static final String ORG_ZSTACK_COMPUTE_VM_DEVICES_10008 = "ORG_ZSTACK_COMPUTE_VM_DEVICES_10008"; + public static final String ORG_ZSTACK_COMPUTE_VM_DEVICES_10009 = "ORG_ZSTACK_COMPUTE_VM_DEVICES_10009"; + public static final String ORG_ZSTACK_TEST_INTEGRATION_PREMIUM_FAULTTOLERANCE_10000 = "ORG_ZSTACK_TEST_INTEGRATION_PREMIUM_FAULTTOLERANCE_10000"; public static final String ORG_ZSTACK_TEST_INTEGRATION_PREMIUM_FAULTTOLERANCE_10001 = "ORG_ZSTACK_TEST_INTEGRATION_PREMIUM_FAULTTOLERANCE_10001";