From 27e83e41904b8aa652fc223c7dd343a5f1ab4178 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 13 Jan 2026 14:57:37 +0800 Subject: [PATCH 01/64] [compute]: add opaque on check root disk settings failed Related: ZSV-10444 Change-Id: I7a6864656668797a6c6a62786a7467626b73797a --- .../java/org/zstack/compute/vm/VmInstanceApiInterceptor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 c17cf5d5179..9d110a31e46 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java @@ -1012,7 +1012,8 @@ private void validateRootDiskOffering(ImageMediaType imgFormat, APICreateVmInsta } if (msg.getRootDiskSize() <= 0) { - throw new ApiMessageInterceptionException(operr("Unexpected root disk settings")); + throw new ApiMessageInterceptionException(operr("Unexpected root disk settings") + .withException("DiskAO[0].size is mandatory when image format is ISO")); } } } From 6369276bc9d8635900555279f5910833f8488187 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 13 Jan 2026 17:27:47 +0800 Subject: [PATCH 02/64] [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 7536337cff7..3fcef810769 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -165,9 +165,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 92e76ede2c5..f69eff87317 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -2703,6 +2703,7 @@ public static class VmDevicesInfoResponse extends AgentResponse { private List virtualDeviceInfoList; private VirtualDeviceInfo memBalloonInfo; private VirtualizerInfoTO virtualizerInfo; + private String edkRpm; @NoLogging private String vmXml; @@ -2738,6 +2739,14 @@ public void setVirtualizerInfo(VirtualizerInfoTO virtualizerInfo) { this.virtualizerInfo = virtualizerInfo; } + public String getEdkRpm() { + return edkRpm; + } + + public void setEdkRpm(String edkRpm) { + this.edkRpm = edkRpm; + } + public String getVmXml() { return vmXml; } 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 f630fa79b39..63735015f57 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMSystemTags.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMSystemTags.java @@ -67,4 +67,8 @@ public class KVMSystemTags { public static SystemTag FORCE_DEPLOYMENT_ONCE = new SystemTag("force::deployment::once", 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 70e706ddef2..6bf72dbe39a 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -159,9 +159,10 @@ - + + diff --git a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy index 94fc178245d..f8794f26acd 100755 --- a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy +++ b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy @@ -489,6 +489,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 c828e7e67a06d9d9312f5d2117babe42a1860f47 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 5 Feb 2026 10:40:16 +0800 Subject: [PATCH 03/64] [conf]: add V5.0.0__schema.sql V5.0.0__schema will move to conf/db/zsv, and ref files will move to conf/db/zsv_ref Related: ZSV-11310 Change-Id: I6d756c7a65746f646575726a61686c6d6f6a6a62 --- build/deploydb.sh | 1 + conf/db/zsv/V5.0.0__schema.sql | 22 +++++++++++++++++++ conf/db/{zsv => zsv_ref}/4.1.0 | 0 conf/db/{zsv => zsv_ref}/4.1.6 | 0 conf/db/{zsv => zsv_ref}/4.2.0 | 0 conf/db/{zsv => zsv_ref}/4.2.6 | 0 conf/db/{zsv => zsv_ref}/4.2.8 | 0 conf/db/{zsv => zsv_ref}/4.3.0 | 0 conf/db/{zsv => zsv_ref}/4.3.1 | 0 conf/deploydb.sh | 1 + .../db/schema/CheckNotNullFieldCase.groovy | 8 ++++--- 11 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 conf/db/zsv/V5.0.0__schema.sql rename conf/db/{zsv => zsv_ref}/4.1.0 (100%) rename conf/db/{zsv => zsv_ref}/4.1.6 (100%) rename conf/db/{zsv => zsv_ref}/4.2.0 (100%) rename conf/db/{zsv => zsv_ref}/4.2.6 (100%) rename conf/db/{zsv => zsv_ref}/4.2.8 (100%) rename conf/db/{zsv => zsv_ref}/4.3.0 (100%) rename conf/db/{zsv => zsv_ref}/4.3.1 (100%) diff --git a/build/deploydb.sh b/build/deploydb.sh index 49964c58754..f8446f31450 100755 --- a/build/deploydb.sh +++ b/build/deploydb.sh @@ -42,6 +42,7 @@ mkdir -p ${flyway_sql} eval "rm -f ${flyway_sql}/*" cp ${base}/../conf/db/V0.6__schema.sql ${flyway_sql} cp ${base}/../conf/db/upgrade/* ${flyway_sql} +cp ${base}/../conf/db/zsv/* ${flyway_sql} if [[ ! -n $host ]] || [[ ! -n $port ]];then url="jdbc:mysql://localhost:3306/zstack" diff --git a/conf/db/zsv/V5.0.0__schema.sql b/conf/db/zsv/V5.0.0__schema.sql new file mode 100644 index 00000000000..3ee7e729906 --- /dev/null +++ b/conf/db/zsv/V5.0.0__schema.sql @@ -0,0 +1,22 @@ +-- Feature: vTPM & Secure Boot | ZSPHER-1, ZSPHER-14 + +CREATE TABLE IF NOT EXISTS `zstack`.`TpmVO` ( + `uuid` char(32) NOT NULL UNIQUE, + `vmInstanceUuid` char(32) NOT NULL, + `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59', + PRIMARY KEY (`uuid`), + CONSTRAINT `fkTpmVOVmInstanceVO` FOREIGN KEY (`vmInstanceUuid`) REFERENCES `VmInstanceEO` (`uuid`) ON UPDATE RESTRICT ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`TpmHostRefVO` ( + `id` bigint unsigned NOT NULL UNIQUE AUTO_INCREMENT, + `tpmUuid` char(32) NOT NULL, + `hostUuid` char(32) NOT NULL, + `path` varchar(255) NOT NULL, + `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59', + PRIMARY KEY (`id`), + CONSTRAINT `fkTpmHostRefVOTpmVO` FOREIGN KEY (`tpmUuid`) REFERENCES `TpmVO` (`uuid`) ON UPDATE RESTRICT ON DELETE CASCADE, + CONSTRAINT `fkTpmHostRefVOHostVO` FOREIGN KEY (`hostUuid`) REFERENCES `HostEO` (`uuid`) ON UPDATE RESTRICT ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/conf/db/zsv/4.1.0 b/conf/db/zsv_ref/4.1.0 similarity index 100% rename from conf/db/zsv/4.1.0 rename to conf/db/zsv_ref/4.1.0 diff --git a/conf/db/zsv/4.1.6 b/conf/db/zsv_ref/4.1.6 similarity index 100% rename from conf/db/zsv/4.1.6 rename to conf/db/zsv_ref/4.1.6 diff --git a/conf/db/zsv/4.2.0 b/conf/db/zsv_ref/4.2.0 similarity index 100% rename from conf/db/zsv/4.2.0 rename to conf/db/zsv_ref/4.2.0 diff --git a/conf/db/zsv/4.2.6 b/conf/db/zsv_ref/4.2.6 similarity index 100% rename from conf/db/zsv/4.2.6 rename to conf/db/zsv_ref/4.2.6 diff --git a/conf/db/zsv/4.2.8 b/conf/db/zsv_ref/4.2.8 similarity index 100% rename from conf/db/zsv/4.2.8 rename to conf/db/zsv_ref/4.2.8 diff --git a/conf/db/zsv/4.3.0 b/conf/db/zsv_ref/4.3.0 similarity index 100% rename from conf/db/zsv/4.3.0 rename to conf/db/zsv_ref/4.3.0 diff --git a/conf/db/zsv/4.3.1 b/conf/db/zsv_ref/4.3.1 similarity index 100% rename from conf/db/zsv/4.3.1 rename to conf/db/zsv_ref/4.3.1 diff --git a/conf/deploydb.sh b/conf/deploydb.sh index a9f8dd90673..0a9bdc2a659 100755 --- a/conf/deploydb.sh +++ b/conf/deploydb.sh @@ -42,6 +42,7 @@ mkdir -p $flyway_sql cp $base/db/V0.6__schema.sql $flyway_sql cp $base/db/upgrade/* $flyway_sql +cp $base/db/zsv/* $flyway_sql url="jdbc:mysql://$host:$port/zstack" diff --git a/test/src/test/groovy/org/zstack/test/integration/db/schema/CheckNotNullFieldCase.groovy b/test/src/test/groovy/org/zstack/test/integration/db/schema/CheckNotNullFieldCase.groovy index ce902e842ab..77d411b2e7f 100644 --- a/test/src/test/groovy/org/zstack/test/integration/db/schema/CheckNotNullFieldCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/db/schema/CheckNotNullFieldCase.groovy @@ -3,8 +3,6 @@ package org.zstack.test.integration.db.schema import org.zstack.core.db.Q import org.zstack.header.vo.ResourceVO import org.zstack.header.vo.ResourceVO_ -import org.zstack.header.identity.AccountResourceRefVO -import org.zstack.header.identity.AccountResourceRefVO_ import org.zstack.testlib.EnvSpec import org.zstack.testlib.SubCase import org.zstack.utils.VersionComparator @@ -31,7 +29,11 @@ class CheckNotNullFieldCase extends SubCase{ @Override void test() { - String upgradeSchemaDir = Paths.get("../conf/db/upgrade").toAbsolutePath().normalize().toString() + checkNotNullField(Paths.get("../conf/db/upgrade").toAbsolutePath().normalize().toString()) + checkNotNullField(Paths.get("../conf/db/zsv").toAbsolutePath().normalize().toString()) + } + + static void checkNotNullField(String upgradeSchemaDir) { File dir = new File(upgradeSchemaDir) dir.eachFileRecurse { schema -> if (!schema.name.contains("__")){ From 16751a647be95f91a51c82b618158656fb9bf2f2 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 3 Feb 2026 10:27:48 +0800 Subject: [PATCH 04/64] [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 | 24 ++ conf/serviceConfig/tpm.xml | 22 ++ conf/springConfigXml/Kvm.xml | 7 + conf/springConfigXml/VmInstanceManager.xml | 12 + .../zstack/header/errorcode/SysErrors.java | 1 + .../header/identity/rbac/RBACDescription.java | 23 ++ .../java/org/zstack/header/tpm/RBACInfo.java | 36 ++ .../org/zstack/header/tpm/TpmConstants.java | 17 + .../java/org/zstack/header/tpm/TpmErrors.java | 22 ++ .../zstack/header/tpm/api/APIAddTpmEvent.java | 31 ++ .../tpm/api/APIAddTpmEventDoc_zh_cn.groovy | 32 ++ .../zstack/header/tpm/api/APIAddTpmMsg.java | 51 +++ .../tpm/api/APIAddTpmMsgDoc_zh_cn.groovy | 85 ++++ .../tpm/api/APIGetTpmCapabilityMsg.java | 48 +++ .../APIGetTpmCapabilityMsgDoc_zh_cn.groovy | 67 ++++ .../tpm/api/APIGetTpmCapabilityReply.java | 24 ++ .../APIGetTpmCapabilityReplyDoc_zh_cn.groovy | 32 ++ .../zstack/header/tpm/api/APIQueryTpmMsg.java | 26 ++ .../tpm/api/APIQueryTpmMsgDoc_zh_cn.groovy | 31 ++ .../header/tpm/api/APIQueryTpmReply.java | 28 ++ .../tpm/api/APIQueryTpmReplyDoc_zh_cn.groovy | 32 ++ .../header/tpm/api/APIRemoveTpmEvent.java | 19 + .../tpm/api/APIRemoveTpmEventDoc_zh_cn.groovy | 23 ++ .../header/tpm/api/APIRemoveTpmMsg.java | 49 +++ .../tpm/api/APIRemoveTpmMsgDoc_zh_cn.groovy | 76 ++++ .../header/tpm/api/APIUpdateTpmEvent.java | 31 ++ .../tpm/api/APIUpdateTpmEventDoc_zh_cn.groovy | 32 ++ .../header/tpm/api/APIUpdateTpmMsg.java | 62 +++ .../tpm/api/APIUpdateTpmMsgDoc_zh_cn.groovy | 76 ++++ .../org/zstack/header/tpm/api/TpmMessage.java | 8 + .../header/tpm/entity/TpmCapabilityView.java | 120 ++++++ .../entity/TpmCapabilityViewDoc_zh_cn.groovy | 66 ++++ .../tpm/entity/TpmHostRefInventory.java | 106 +++++ .../TpmHostRefInventoryDoc_zh_cn.groovy | 45 +++ .../header/tpm/entity/TpmHostRefVO.java | 106 +++++ .../header/tpm/entity/TpmHostRefVO_.java | 15 + .../header/tpm/entity/TpmInventory.java | 110 ++++++ .../tpm/entity/TpmInventoryDoc_zh_cn.groovy | 48 +++ .../org/zstack/header/tpm/entity/TpmSpec.java | 30 ++ .../org/zstack/header/tpm/entity/TpmVO.java | 110 ++++++ .../org/zstack/header/tpm/entity/TpmVO_.java | 14 + .../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 + ...ertTemplatedVmInstanceToVmInstanceMsg.java | 11 + ...dVmInstanceToVmInstanceMsgDoc_zh_cn.groovy | 9 + ...eVmInstanceFromVolumeSnapshotGroupMsg.java | 11 + ...FromVolumeSnapshotGroupMsgDoc_zh_cn.groovy | 9 + .../header/vm/APICreateVmInstanceMsg.java | 33 ++ .../org/zstack/header/vm/VmInstanceVO.java | 2 + .../header/vm/devices/VmDevicesSpec.java | 24 ++ .../java/org/zstack/kvm/KVMGlobalConfig.java | 5 + .../java/org/zstack/kvm/KVMSystemTags.java | 4 + .../org/zstack/kvm/tpm/KvmTpmManager.java | 374 ++++++++++++++++++ sdk/src/main/java/SourceClassMap.java | 6 + .../CreateVmFromVolumeBackupAction.java | 3 + .../org/zstack/sdk/CloneVmInstanceAction.java | 3 + ...TemplatedVmInstanceToVmInstanceAction.java | 3 + .../zstack/sdk/CreateVmInstanceAction.java | 3 + ...InstanceFromTemplatedVmInstanceAction.java | 3 + ...InstanceFromVolumeSnapshotGroupAction.java | 3 + .../org/zstack/sdk/tpm/api/AddTpmAction.java | 110 ++++++ .../org/zstack/sdk/tpm/api/AddTpmResult.java | 14 + .../sdk/tpm/api/GetTpmCapabilityAction.java | 98 +++++ .../sdk/tpm/api/GetTpmCapabilityResult.java | 14 + .../zstack/sdk/tpm/api/QueryTpmAction.java | 75 ++++ .../zstack/sdk/tpm/api/QueryTpmResult.java | 22 ++ .../zstack/sdk/tpm/api/RemoveTpmAction.java | 107 +++++ .../zstack/sdk/tpm/api/RemoveTpmResult.java | 7 + .../zstack/sdk/tpm/api/UpdateTpmAction.java | 107 +++++ .../zstack/sdk/tpm/api/UpdateTpmResult.java | 14 + .../sdk/tpm/entity/TpmCapabilityView.java | 79 ++++ .../sdk/tpm/entity/TpmHostRefInventory.java | 55 +++ .../zstack/sdk/tpm/entity/TpmInventory.java | 55 +++ .../test/resources/springConfigXml/Kvm.xml | 7 + .../java/org/zstack/testlib/ApiHelper.groovy | 275 ++++++++++--- .../main/java/org/zstack/utils/StringDSL.java | 6 +- 82 files changed, 3579 insertions(+), 66 deletions(-) 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/APIAddTpmEventDoc_zh_cn.groovy 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/APIAddTpmMsgDoc_zh_cn.groovy 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/APIGetTpmCapabilityMsgDoc_zh_cn.groovy 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/APIGetTpmCapabilityReplyDoc_zh_cn.groovy 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/APIQueryTpmMsgDoc_zh_cn.groovy 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/APIQueryTpmReplyDoc_zh_cn.groovy 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/APIRemoveTpmEventDoc_zh_cn.groovy 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/APIRemoveTpmMsgDoc_zh_cn.groovy 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/APIUpdateTpmEventDoc_zh_cn.groovy 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/APIUpdateTpmMsgDoc_zh_cn.groovy 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/TpmCapabilityViewDoc_zh_cn.groovy 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/TpmHostRefInventoryDoc_zh_cn.groovy 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/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/TpmInventoryDoc_zh_cn.groovy 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/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 create mode 100644 sdk/src/main/java/org/zstack/sdk/tpm/api/AddTpmAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/tpm/api/AddTpmResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/tpm/api/GetTpmCapabilityAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/tpm/api/GetTpmCapabilityResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/tpm/api/QueryTpmAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/tpm/api/QueryTpmResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/tpm/api/RemoveTpmAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/tpm/api/RemoveTpmResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/tpm/api/UpdateTpmAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/tpm/api/UpdateTpmResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmCapabilityView.java create mode 100644 sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmHostRefInventory.java create mode 100644 sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmInventory.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 bd79900c13c..37621cca6a7 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java @@ -133,4 +133,9 @@ public class VmGlobalConfig { @GlobalConfigValidation(validValues = {"None", "AuthenticAMD"}) @BindResourceConfig(value = {VmInstanceVO.class}) public static GlobalConfig VM_CPUID_VENDOR = new GlobalConfig(CATEGORY, "vm.cpuid.vendor"); + + @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 c5b763f86ba..b484cbe5c1d 100755 --- a/conf/errorCodes/sys.xml +++ b/conf/errorCodes/sys.xml @@ -105,5 +105,10 @@ 1090 Multiple reasons + + + 1091 + Operation not supported + diff --git a/conf/errorCodes/tpm.xml b/conf/errorCodes/tpm.xml new file mode 100644 index 00000000000..e93b159fa71 --- /dev/null +++ b/conf/errorCodes/tpm.xml @@ -0,0 +1,24 @@ + + 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 3fcef810769..7a7160db29a 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -244,4 +244,11 @@ + + + + + + + diff --git a/conf/springConfigXml/VmInstanceManager.xml b/conf/springConfigXml/VmInstanceManager.xml index 20e094378aa..8d3f7cc41e6 100755 --- a/conf/springConfigXml/VmInstanceManager.xml +++ b/conf/springConfigXml/VmInstanceManager.xml @@ -267,4 +267,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 c59fba9c5b5..79e27fa69ac 100755 --- a/header/src/main/java/org/zstack/header/errorcode/SysErrors.java +++ b/header/src/main/java/org/zstack/header/errorcode/SysErrors.java @@ -26,6 +26,7 @@ public enum SysErrors { // ZSphere only MULTIPLE_REASONS(1090), + NOT_SUPPORTED(1091), ; private String code; diff --git a/header/src/main/java/org/zstack/header/identity/rbac/RBACDescription.java b/header/src/main/java/org/zstack/header/identity/rbac/RBACDescription.java index 02d7b038c92..5b379e19fd7 100755 --- a/header/src/main/java/org/zstack/header/identity/rbac/RBACDescription.java +++ b/header/src/main/java/org/zstack/header/identity/rbac/RBACDescription.java @@ -37,6 +37,29 @@ default RBAC.AttributeSupportResourceBuilder attributeSupportResourceBuilder() { return new RBAC.AttributeSupportResourceBuilder(); } + /** + * If you want to contribute a resource to a resource ensemble, you can use this method: + * + * Ex: (Make TpmVO as a child resource of VmInstanceVO) + *
+     * resourceEnsembleContributorBuilder()
+     *     .resource(TpmVO.class)
+     *     .contributeTo(VmInstanceVO.class)
+     *     .build();
+     * 
+ * + * You must set @EntityGraph.Neighbour on VmInstanceVO.class + *
+     * \@EntityGraph(
+     *         friends = {
+     *                 \@EntityGraph.Neighbour(type = TpmVO.class, myField = "uuid", targetField = "vmInstanceUuid"),
+     *         }
+     * )
+     * 
+ * + * or use {@link org.zstack.header.identity.rbac.RBAC.ResourceEnsembleContributorBuilder#resourceWithCustomizeFindingMethods(java.lang.Class, java.util.function.Consumer, java.util.function.Consumer)} + * to specify how to find the resource by SQL. + */ default RBAC.ResourceEnsembleContributorBuilder resourceEnsembleContributorBuilder() { return new RBAC.ResourceEnsembleContributorBuilder(); } 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..b8b56608d86 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/RBACInfo.java @@ -0,0 +1,36 @@ +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; +import org.zstack.header.vm.VmInstanceVO; + +@SDKPackage(packageName="org.zstack.sdk.tpm") +public class RBACInfo implements RBACDescription { + @Override + public String permissionName() { + return "tpm"; + } + + @Override + public void permissions() { + permissionBuilder() + .communityAvailable() + .zsvBasicAvailable() + .zsvProAvailable() + .build(); + + resourceEnsembleContributorBuilder() + .resource(TpmVO.class) + .contributeTo(VmInstanceVO.class) + .build(); + } + + @Override + public void roles() { + roleContributorBuilder() + .actionsInThisPermission() + .toOtherRole() + .build(); + } +} 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/APIAddTpmEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/tpm/api/APIAddTpmEventDoc_zh_cn.groovy new file mode 100644 index 00000000000..31d912bd097 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIAddTpmEventDoc_zh_cn.groovy @@ -0,0 +1,32 @@ +package org.zstack.header.tpm.api + +import org.zstack.header.tpm.entity.TpmInventory +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "虚拟机添加 TPM 的结果" + + ref { + name "inventory" + path "org.zstack.header.tpm.api.APIAddTpmEvent.inventory" + desc "TPM 信息" + type "TpmInventory" + since "5.0.0" + clz TpmInventory.class + } + field { + name "success" + desc "添加是否成功" + type "boolean" + since "5.0.0" + } + ref { + name "error" + path "org.zstack.header.tpm.api.APIAddTpmEvent.error" + desc "错误码,若不为 null,则表示操作失败, 操作成功时该字段为 null" + type "ErrorCode" + since "5.0.0" + clz ErrorCode.class + } +} 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/APIAddTpmMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/tpm/api/APIAddTpmMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..465d0ddd7c5 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIAddTpmMsgDoc_zh_cn.groovy @@ -0,0 +1,85 @@ +package org.zstack.header.tpm.api + +import org.zstack.header.tpm.api.APIAddTpmEvent + +doc { + title "AddTpm" + + category "tpm" + + desc """虚拟机添加 TPM""" + + rest { + request { + url "POST /v1/tpms" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIAddTpmMsg.class + + desc """""" + + params { + + column { + name "keyProviderUuid" + enclosedIn "params" + desc "密钥提供程序 UUID" + location "body" + type "String" + optional true + since "5.0.0" + } + column { + name "vmInstanceUuid" + enclosedIn "params" + desc "虚拟机 UUID" + location "body" + type "String" + optional false + since "5.0.0" + } + column { + name "resourceUuid" + enclosedIn "params" + desc "资源 UUID" + location "body" + type "String" + optional true + since "5.0.0" + } + column { + name "tagUuids" + enclosedIn "params" + desc "标签 UUID 列表" + location "body" + type "List" + optional true + since "5.0.0" + } + column { + name "systemTags" + enclosedIn "" + desc "系统标签" + location "body" + type "List" + optional true + since "5.0.0" + } + column { + name "userTags" + enclosedIn "" + desc "用户标签" + location "body" + type "List" + optional true + since "5.0.0" + } + } + } + + response { + clz APIAddTpmEvent.class + } + } +} \ No newline at end of file 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/APIGetTpmCapabilityMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/tpm/api/APIGetTpmCapabilityMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..d5d3bb770aa --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIGetTpmCapabilityMsgDoc_zh_cn.groovy @@ -0,0 +1,67 @@ +package org.zstack.header.tpm.api + +import org.zstack.header.tpm.api.APIGetTpmCapabilityReply + +doc { + title "GetTpmCapability" + + category "tpm" + + desc """获取 TPM 详情数据""" + + rest { + request { + url "GET /v1/tpms/capability" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIGetTpmCapabilityMsg.class + + desc """""" + + params { + + column { + name "tpmUuid" + enclosedIn "" + desc "TPM UUID" + location "query" + type "String" + optional true + since "5.0.0" + } + column { + name "vmInstanceUuid" + enclosedIn "" + desc "虚拟机 UUID" + location "query" + type "String" + optional true + since "5.0.0" + } + column { + name "systemTags" + enclosedIn "" + desc "系统标签" + location "query" + type "List" + optional true + since "5.0.0" + } + column { + name "userTags" + enclosedIn "" + desc "用户标签" + location "query" + type "List" + optional true + since "5.0.0" + } + } + } + + response { + clz APIGetTpmCapabilityReply.class + } + } +} \ No newline at end of file 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/APIGetTpmCapabilityReplyDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/tpm/api/APIGetTpmCapabilityReplyDoc_zh_cn.groovy new file mode 100644 index 00000000000..216b1ba86b8 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIGetTpmCapabilityReplyDoc_zh_cn.groovy @@ -0,0 +1,32 @@ +package org.zstack.header.tpm.api + +import org.zstack.header.tpm.entity.TpmCapabilityView +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "获取 TPM 详情数据的结果" + + ref { + name "inventory" + path "org.zstack.header.tpm.api.APIGetTpmCapabilityReply.inventory" + desc "TPM 性能和信息数据" + type "TpmCapabilityView" + since "5.0.0" + clz TpmCapabilityView.class + } + field { + name "success" + desc "获取是否成功" + type "boolean" + since "5.0.0" + } + ref { + name "error" + path "org.zstack.header.tpm.api.APIGetTpmCapabilityReply.error" + desc "错误码,若不为 null,则表示操作失败, 操作成功时该字段为 null" + type "ErrorCode" + since "5.0.0" + clz ErrorCode.class + } +} 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/APIQueryTpmMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/tpm/api/APIQueryTpmMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..2254f22e12f --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIQueryTpmMsgDoc_zh_cn.groovy @@ -0,0 +1,31 @@ +package org.zstack.header.tpm.api + +import org.zstack.header.tpm.api.APIQueryTpmReply +import org.zstack.header.query.APIQueryMessage + +doc { + title "QueryTpm" + + category "tpm" + + desc """查询 TPM""" + + rest { + request { + url "GET /v1/tpms" + url "GET /v1/tpms/{uuid}" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIQueryTpmMsg.class + + desc """""" + + params APIQueryMessage.class + } + + response { + clz APIQueryTpmReply.class + } + } +} \ No newline at end of file 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/APIQueryTpmReplyDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/tpm/api/APIQueryTpmReplyDoc_zh_cn.groovy new file mode 100644 index 00000000000..bff2c89f924 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIQueryTpmReplyDoc_zh_cn.groovy @@ -0,0 +1,32 @@ +package org.zstack.header.tpm.api + +import org.zstack.header.tpm.entity.TpmInventory +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "查询 TPM 的结果" + + ref { + name "inventories" + path "org.zstack.header.tpm.api.APIQueryTpmReply.inventories" + desc "TPM 列表" + type "List" + since "5.0.0" + clz TpmInventory.class + } + field { + name "success" + desc "查询是否成功" + type "boolean" + since "5.0.0" + } + ref { + name "error" + path "org.zstack.header.tpm.api.APIQueryTpmReply.error" + desc "错误码,若不为 null,则表示操作失败, 操作成功时该字段为 null" + type "ErrorCode" + since "5.0.0" + clz ErrorCode.class + } +} 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/APIRemoveTpmEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/tpm/api/APIRemoveTpmEventDoc_zh_cn.groovy new file mode 100644 index 00000000000..bd77dfe1d5f --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIRemoveTpmEventDoc_zh_cn.groovy @@ -0,0 +1,23 @@ +package org.zstack.header.tpm.api + +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "虚拟机删除 TPM 的结果" + + field { + name "success" + desc "删除是否成功" + type "boolean" + since "5.0.0" + } + ref { + name "error" + path "org.zstack.header.tpm.api.APIRemoveTpmEvent.error" + desc "错误码,若不为 null,则表示操作失败, 操作成功时该字段为 null" + type "ErrorCode" + since "5.0.0" + clz ErrorCode.class + } +} 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/APIRemoveTpmMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/tpm/api/APIRemoveTpmMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..c10c5e8526d --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIRemoveTpmMsgDoc_zh_cn.groovy @@ -0,0 +1,76 @@ +package org.zstack.header.tpm.api + +import org.zstack.header.tpm.api.APIRemoveTpmEvent + +doc { + title "RemoveTpm" + + category "tpm" + + desc """虚拟机删除 TPM""" + + rest { + request { + url "DELETE /v1/tpms" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIRemoveTpmMsg.class + + desc """""" + + params { + + column { + name "tpmUuid" + enclosedIn "" + desc "TPM UUID" + location "query" + type "String" + optional true + since "5.0.0" + } + column { + name "vmInstanceUuid" + enclosedIn "" + desc "虚拟机 UUID" + location "query" + type "String" + optional true + since "5.0.0" + } + column { + name "deleteMode" + enclosedIn "" + desc "删除模式(Permissive / Enforcing,Permissive)" + location "query" + type "String" + optional true + since "5.0.0" + } + column { + name "systemTags" + enclosedIn "" + desc "系统标签" + location "query" + type "List" + optional true + since "5.0.0" + } + column { + name "userTags" + enclosedIn "" + desc "用户标签" + location "query" + type "List" + optional true + since "5.0.0" + } + } + } + + response { + clz APIRemoveTpmEvent.class + } + } +} \ No newline at end of file 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/APIUpdateTpmEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/tpm/api/APIUpdateTpmEventDoc_zh_cn.groovy new file mode 100644 index 00000000000..33f2e7dab69 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIUpdateTpmEventDoc_zh_cn.groovy @@ -0,0 +1,32 @@ +package org.zstack.header.tpm.api + +import org.zstack.header.tpm.entity.TpmInventory +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "更新 TPM 的结果" + + ref { + name "inventory" + path "org.zstack.header.tpm.api.APIUpdateTpmEvent.inventory" + desc "更新后的 TPM 信息" + type "TpmInventory" + since "5.0.0" + clz TpmInventory.class + } + field { + name "success" + desc "更新是否成功" + type "boolean" + since "5.0.0" + } + ref { + name "error" + path "org.zstack.header.tpm.api.APIUpdateTpmEvent.error" + desc "错误码,若不为 null,则表示操作失败, 操作成功时该字段为 null" + type "ErrorCode" + since "5.0.0" + clz ErrorCode.class + } +} 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/APIUpdateTpmMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/tpm/api/APIUpdateTpmMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..4147d9b6554 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/api/APIUpdateTpmMsgDoc_zh_cn.groovy @@ -0,0 +1,76 @@ +package org.zstack.header.tpm.api + +import org.zstack.header.tpm.api.APIUpdateTpmEvent + +doc { + title "UpdateTpm" + + category "tpm" + + desc """更新 TPM""" + + rest { + request { + url "PUT /v1/tpms" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIUpdateTpmMsg.class + + desc """""" + + params { + + column { + name "vmInstanceUuid" + enclosedIn "updateTpm" + desc "虚拟机 UUID" + location "body" + type "String" + optional true + since "5.0.0" + } + column { + name "tpmUuid" + enclosedIn "updateTpm" + desc "TPM UUID" + location "body" + type "String" + optional true + since "5.0.0" + } + column { + name "keyProviderUuid" + enclosedIn "updateTpm" + desc "密钥提供程序 UUID" + location "body" + type "String" + optional true + since "5.0.0" + } + column { + name "systemTags" + enclosedIn "" + desc "系统标签" + location "body" + type "List" + optional true + since "5.0.0" + } + column { + name "userTags" + enclosedIn "" + desc "用户标签" + location "body" + type "List" + optional true + since "5.0.0" + } + } + } + + response { + clz APIUpdateTpmEvent.class + } + } +} \ No newline at end of file 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/TpmCapabilityViewDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/tpm/entity/TpmCapabilityViewDoc_zh_cn.groovy new file mode 100644 index 00000000000..a286e9d74c7 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/entity/TpmCapabilityViewDoc_zh_cn.groovy @@ -0,0 +1,66 @@ +package org.zstack.header.tpm.entity + +import java.sql.Timestamp +import org.zstack.header.tpm.entity.TpmHostRefInventory + +doc { + + title "TPM 详情" + + field { + name "uuid" + desc "TPM UUID" + type "String" + since "5.0.0" + } + field { + name "name" + desc "TPM 资源名称" + type "String" + since "5.0.0" + } + field { + name "vmInstanceUuid" + desc "虚拟机 UUID" + type "String" + since "5.0.0" + } + field { + name "createDate" + desc "创建时间" + type "Timestamp" + since "5.0.0" + } + field { + name "lastOpDate" + desc "最后一次修改时间" + type "Timestamp" + since "5.0.0" + } + ref { + name "hostRefs" + path "org.zstack.header.tpm.entity.TpmCapabilityView.hostRefs" + desc "TPM 与主机的相关数据列表" + type "List" + since "5.0.0" + clz TpmHostRefInventory.class + } + field { + name "edkVersion" + desc "EDK 套件版本" + type "String" + since "5.0.0" + } + field { + name "swtpmVersion" + desc "SWTPM 版本" + type "String" + since "5.0.0" + } + field { + name "resetTpmAfterVmCloneConfig" + desc "是否在虚拟机克隆后重置 TPM 状态的配置" + type "boolean" + since "5.0.0" + } +} 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/TpmHostRefInventoryDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefInventoryDoc_zh_cn.groovy new file mode 100644 index 00000000000..d8330cb7ff6 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefInventoryDoc_zh_cn.groovy @@ -0,0 +1,45 @@ +package org.zstack.header.tpm.entity + +import java.sql.Timestamp + +doc { + + title "TPM 与主机的相关数据" + + field { + name "id" + desc "自增主键" + type "long" + since "5.0.0" + } + field { + name "tpmUuid" + desc "TPM UUID" + type "String" + since "5.0.0" + } + field { + name "hostUuid" + desc "主机 UUID" + type "String" + since "5.0.0" + } + field { + name "path" + desc "遗留 TPM 状态文件的位置" + type "String" + since "5.0.0" + } + field { + name "createDate" + desc "创建时间" + type "Timestamp" + since "5.0.0" + } + field { + name "lastOpDate" + desc "最后一次修改时间" + type "Timestamp" + since "5.0.0" + } +} 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/TpmHostRefVO_.java b/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefVO_.java new file mode 100644 index 00000000000..ee4d9654711 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefVO_.java @@ -0,0 +1,15 @@ +package org.zstack.header.tpm.entity; + +import javax.persistence.metamodel.SingularAttribute; +import javax.persistence.metamodel.StaticMetamodel; +import java.sql.Timestamp; + +@StaticMetamodel(TpmHostRefVO.class) +public class TpmHostRefVO_ { + public static volatile SingularAttribute id; + public static volatile SingularAttribute tpmUuid; + public static volatile SingularAttribute hostUuid; + public static volatile SingularAttribute path; + public static volatile SingularAttribute createDate; + public static volatile SingularAttribute 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/TpmInventoryDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/tpm/entity/TpmInventoryDoc_zh_cn.groovy new file mode 100644 index 00000000000..9908023d6c4 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/entity/TpmInventoryDoc_zh_cn.groovy @@ -0,0 +1,48 @@ +package org.zstack.header.tpm.entity + +import java.sql.Timestamp +import org.zstack.header.tpm.entity.TpmHostRefInventory + +doc { + + title "TPM 信息" + + field { + name "uuid" + desc "TPM UUID" + type "String" + since "5.0.0" + } + field { + name "name" + desc "TPM 资源名称" + type "String" + since "5.0.0" + } + field { + name "vmInstanceUuid" + desc "虚拟机 UUID" + type "String" + since "5.0.0" + } + field { + name "createDate" + desc "创建时间" + type "Timestamp" + since "5.0.0" + } + field { + name "lastOpDate" + desc "最后一次修改时间" + type "Timestamp" + since "5.0.0" + } + ref { + name "hostRefs" + path "org.zstack.header.tpm.entity.TpmInventory.hostRefs" + desc "TPM 与主机的相关数据列表" + type "List" + since "5.0.0" + clz TpmHostRefInventory.class + } +} 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/entity/TpmVO_.java b/header/src/main/java/org/zstack/header/tpm/entity/TpmVO_.java new file mode 100644 index 00000000000..99fb5aa3ba5 --- /dev/null +++ b/header/src/main/java/org/zstack/header/tpm/entity/TpmVO_.java @@ -0,0 +1,14 @@ +package org.zstack.header.tpm.entity; + +import org.zstack.header.vo.ResourceVO_; + +import javax.persistence.metamodel.SingularAttribute; +import javax.persistence.metamodel.StaticMetamodel; +import java.sql.Timestamp; + +@StaticMetamodel(TpmVO.class) +public class TpmVO_ extends ResourceVO_ { + public static volatile SingularAttribute vmInstanceUuid; + public static volatile SingularAttribute createDate; + public static volatile SingularAttribute lastOpDate; +} 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/APIConvertTemplatedVmInstanceToVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsg.java index 1a128bfaf84..46e443c35cf 100644 --- a/header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsg.java @@ -21,6 +21,9 @@ public class APIConvertTemplatedVmInstanceToVmInstanceMsg extends APIMessage imp @APIParam(maxLength = 255, required = true) private String name; + @APIParam(required = false) + private Boolean resetTpm; + @APINoSee private String vmInstanceUuid; @@ -40,6 +43,14 @@ public void setName(String name) { this.name = name; } + public Boolean getResetTpm() { + return resetTpm; + } + + public void setResetTpm(Boolean resetTpm) { + this.resetTpm = resetTpm; + } + @Override public String getVmInstanceUuid() { return vmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsgDoc_zh_cn.groovy index 554439a51d0..1f20e124b8c 100644 --- a/header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsgDoc_zh_cn.groovy +++ b/header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsgDoc_zh_cn.groovy @@ -57,6 +57,15 @@ doc { optional true since "zsv 4.2.6" } + column { + name "resetTpm" + enclosedIn "params" + desc "转换成虚拟机后是否重置 TPM 状态" + location "body" + type "Boolean" + optional true + since "5.0.0" + } } } 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 2d27e9b264f..94d63ca232a 100644 --- a/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeSnapshotGroupMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeSnapshotGroupMsg.java @@ -115,6 +115,9 @@ public class APICreateVmInstanceFromVolumeSnapshotGroupMsg extends APICreateMess @APINoSee private String platform; + @APIParam(required = false) + private Boolean resetTpm; + public String getName() { return name; } @@ -292,4 +295,12 @@ public String getPlatform() { public void setPlatform(String platform) { this.platform = platform; } + + public Boolean getResetTpm() { + return resetTpm; + } + + public void setResetTpm(Boolean resetTpm) { + this.resetTpm = resetTpm; + } } diff --git a/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeSnapshotGroupMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeSnapshotGroupMsgDoc_zh_cn.groovy index b2ba50497ec..e8262a13931 100644 --- a/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeSnapshotGroupMsgDoc_zh_cn.groovy +++ b/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceFromVolumeSnapshotGroupMsgDoc_zh_cn.groovy @@ -221,6 +221,15 @@ doc { optional true since "4.10.10" } + column { + name "resetTpm" + enclosedIn "params" + desc "是否重置 TPM 状态" + location "body" + type "Boolean" + optional true + since "5.0.0" + } } } 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 f6a924c477d..e085a0a0239 100755 --- a/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APICreateVmInstanceMsg.java @@ -10,11 +10,14 @@ import org.zstack.header.message.*; import org.zstack.header.network.l3.L3NetworkVO; import org.zstack.header.other.APIAuditor; +import org.zstack.header.rest.APINoSee; 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.VolumeVO; import org.zstack.header.zone.ZoneVO; +import org.zstack.utils.gson.JSONObjectUtil; import java.util.Collections; import java.util.List; @@ -225,6 +228,15 @@ public class APICreateVmInstanceMsg extends APICreateMessage implements APIAudit @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; } @@ -466,6 +478,26 @@ public void setAllocatorStrategy(String allocatorStrategy) { this.allocatorStrategy = allocatorStrategy; } + 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"); @@ -489,6 +521,7 @@ public static APICreateVmInstanceMsg __example__() { disk2.setPrimaryStorageUuid(uuid(PrimaryStorageVO.class)); msg.setDiskAOs(list(disk1, disk2)); + 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 0923c8e356a..5306142ee46 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; @@ -36,6 +37,7 @@ @EntityGraph.Neighbour(type = VolumeVO.class, myField = "rootVolumeUuid", targetField = "uuid"), @EntityGraph.Neighbour(type = VmNicVO.class, myField = "uuid", targetField = "vmInstanceUuid"), @EntityGraph.Neighbour(type = VmCdRomVO.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 6d1a7eaeb5c..4a724abac2a 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java @@ -159,4 +159,9 @@ public class KVMGlobalConfig { type = Long.class ) public static GlobalConfig KVMAGENT_PHYSICAL_MEMORY_USAGE_HARD_LIMIT = new GlobalConfig(CATEGORY, "kvmagent.physicalmemory.usage.hardlimit"); + + @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 63735015f57..8f0442f9246 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMSystemTags.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMSystemTags.java @@ -71,4 +71,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/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 146b132c459..821f401de47 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -270,6 +270,9 @@ public class SourceClassMap { put("org.zstack.header.tag.TagPatternInventory", "org.zstack.sdk.TagPatternInventory"); put("org.zstack.header.tag.TagPatternType", "org.zstack.sdk.TagPatternType"); put("org.zstack.header.tag.UserTagInventory", "org.zstack.sdk.UserTagInventory"); + put("org.zstack.header.tpm.entity.TpmCapabilityView", "org.zstack.sdk.tpm.entity.TpmCapabilityView"); + put("org.zstack.header.tpm.entity.TpmHostRefInventory", "org.zstack.sdk.tpm.entity.TpmHostRefInventory"); + put("org.zstack.header.tpm.entity.TpmInventory", "org.zstack.sdk.tpm.entity.TpmInventory"); put("org.zstack.header.vdpa.VmVdpaNicInventory", "org.zstack.sdk.VmVdpaNicInventory"); put("org.zstack.header.vipQos.VipQosInventory", "org.zstack.sdk.VipQosInventory"); put("org.zstack.header.vm.CloneVmInstanceInventory", "org.zstack.sdk.CloneVmInstanceInventory"); @@ -1270,6 +1273,9 @@ public class SourceClassMap { put("org.zstack.sdk.sns.platform.wecom.SNSWeComEndpointInventory", "org.zstack.sns.platform.wecom.SNSWeComEndpointInventory"); put("org.zstack.sdk.softwarePackage.header.JobDetails", "org.zstack.softwarePackage.header.JobDetails"); put("org.zstack.sdk.softwarePackage.header.SoftwarePackageInventory", "org.zstack.softwarePackage.header.SoftwarePackageInventory"); + put("org.zstack.sdk.tpm.entity.TpmCapabilityView", "org.zstack.header.tpm.entity.TpmCapabilityView"); + put("org.zstack.sdk.tpm.entity.TpmHostRefInventory", "org.zstack.header.tpm.entity.TpmHostRefInventory"); + put("org.zstack.sdk.tpm.entity.TpmInventory", "org.zstack.header.tpm.entity.TpmInventory"); put("org.zstack.sdk.zbox.ZBoxBackupInventory", "org.zstack.externalbackup.zbox.ZBoxBackupInventory"); put("org.zstack.sdk.zbox.ZBoxBackupStorageBackupInfo", "org.zstack.externalbackup.zbox.ZBoxBackupStorageBackupInfo"); put("org.zstack.sdk.zbox.ZBoxInventory", "org.zstack.zbox.ZBoxInventory"); diff --git a/sdk/src/main/java/org/zstack/heder/storage/volume/backup/CreateVmFromVolumeBackupAction.java b/sdk/src/main/java/org/zstack/heder/storage/volume/backup/CreateVmFromVolumeBackupAction.java index 1a56f081ab4..a1a9af07824 100644 --- a/sdk/src/main/java/org/zstack/heder/storage/volume/backup/CreateVmFromVolumeBackupAction.java +++ b/sdk/src/main/java/org/zstack/heder/storage/volume/backup/CreateVmFromVolumeBackupAction.java @@ -73,6 +73,9 @@ public Result throwExceptionIfError() { @Param(required = false, validValues = {"InstantStart","JustCreate","CreateStopped"}, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String strategy = "InstantStart"; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Boolean resetTpm; + @Param(required = false) public java.lang.String resourceUuid; diff --git a/sdk/src/main/java/org/zstack/sdk/CloneVmInstanceAction.java b/sdk/src/main/java/org/zstack/sdk/CloneVmInstanceAction.java index 23ebc2033ca..661106961c5 100644 --- a/sdk/src/main/java/org/zstack/sdk/CloneVmInstanceAction.java +++ b/sdk/src/main/java/org/zstack/sdk/CloneVmInstanceAction.java @@ -67,6 +67,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public org.zstack.sdk.VmCustomSpecificationStruct vmCustomSpecification; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Boolean resetTpm; + @Param(required = false) public java.util.List systemTags; diff --git a/sdk/src/main/java/org/zstack/sdk/ConvertTemplatedVmInstanceToVmInstanceAction.java b/sdk/src/main/java/org/zstack/sdk/ConvertTemplatedVmInstanceToVmInstanceAction.java index 1bc3dbc2656..5989a4c96d0 100644 --- a/sdk/src/main/java/org/zstack/sdk/ConvertTemplatedVmInstanceToVmInstanceAction.java +++ b/sdk/src/main/java/org/zstack/sdk/ConvertTemplatedVmInstanceToVmInstanceAction.java @@ -31,6 +31,9 @@ public Result throwExceptionIfError() { @Param(required = true, maxLength = 255, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String name; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Boolean resetTpm; + @Param(required = false) public java.util.List systemTags; diff --git a/sdk/src/main/java/org/zstack/sdk/CreateVmInstanceAction.java b/sdk/src/main/java/org/zstack/sdk/CreateVmInstanceAction.java index 8c05a04e241..f475d3c4761 100644 --- a/sdk/src/main/java/org/zstack/sdk/CreateVmInstanceAction.java +++ b/sdk/src/main/java/org/zstack/sdk/CreateVmInstanceAction.java @@ -123,6 +123,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.util.List diskAOs; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.Map devices; + @Param(required = false) public java.lang.String resourceUuid; diff --git a/sdk/src/main/java/org/zstack/sdk/CreateVmInstanceFromTemplatedVmInstanceAction.java b/sdk/src/main/java/org/zstack/sdk/CreateVmInstanceFromTemplatedVmInstanceAction.java index 8994903a456..4e699ce5481 100644 --- a/sdk/src/main/java/org/zstack/sdk/CreateVmInstanceFromTemplatedVmInstanceAction.java +++ b/sdk/src/main/java/org/zstack/sdk/CreateVmInstanceFromTemplatedVmInstanceAction.java @@ -76,6 +76,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public org.zstack.sdk.VmCustomSpecificationStruct vmCustomSpecification; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Boolean resetTpm; + @Param(required = false) public java.util.List systemTags; diff --git a/sdk/src/main/java/org/zstack/sdk/CreateVmInstanceFromVolumeSnapshotGroupAction.java b/sdk/src/main/java/org/zstack/sdk/CreateVmInstanceFromVolumeSnapshotGroupAction.java index 2eb0f1fa80c..7f477d9b0fb 100644 --- a/sdk/src/main/java/org/zstack/sdk/CreateVmInstanceFromVolumeSnapshotGroupAction.java +++ b/sdk/src/main/java/org/zstack/sdk/CreateVmInstanceFromVolumeSnapshotGroupAction.java @@ -79,6 +79,9 @@ public Result throwExceptionIfError() { @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.util.Map dataVolumeSystemTags; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.Boolean resetTpm; + @Param(required = false) public java.lang.String resourceUuid; diff --git a/sdk/src/main/java/org/zstack/sdk/tpm/api/AddTpmAction.java b/sdk/src/main/java/org/zstack/sdk/tpm/api/AddTpmAction.java new file mode 100644 index 00000000000..63fdcb78673 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/tpm/api/AddTpmAction.java @@ -0,0 +1,110 @@ +package org.zstack.sdk.tpm.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class AddTpmAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.tpm.api.AddTpmResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = false, maxLength = 32, minLength = 32, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String keyProviderUuid; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vmInstanceUuid; + + @Param(required = false) + public java.lang.String resourceUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List tagUuids; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.tpm.api.AddTpmResult value = res.getResult(org.zstack.sdk.tpm.api.AddTpmResult.class); + ret.value = value == null ? new org.zstack.sdk.tpm.api.AddTpmResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "POST"; + info.path = "/tpms"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/tpm/api/AddTpmResult.java b/sdk/src/main/java/org/zstack/sdk/tpm/api/AddTpmResult.java new file mode 100644 index 00000000000..05effc20779 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/tpm/api/AddTpmResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.tpm.api; + +import org.zstack.sdk.tpm.entity.TpmInventory; + +public class AddTpmResult { + public TpmInventory inventory; + public void setInventory(TpmInventory inventory) { + this.inventory = inventory; + } + public TpmInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/tpm/api/GetTpmCapabilityAction.java b/sdk/src/main/java/org/zstack/sdk/tpm/api/GetTpmCapabilityAction.java new file mode 100644 index 00000000000..d4a8c793040 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/tpm/api/GetTpmCapabilityAction.java @@ -0,0 +1,98 @@ +package org.zstack.sdk.tpm.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class GetTpmCapabilityAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.tpm.api.GetTpmCapabilityResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String tpmUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vmInstanceUuid; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.tpm.api.GetTpmCapabilityResult value = res.getResult(org.zstack.sdk.tpm.api.GetTpmCapabilityResult.class); + ret.value = value == null ? new org.zstack.sdk.tpm.api.GetTpmCapabilityResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "GET"; + info.path = "/tpms/capability"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/tpm/api/GetTpmCapabilityResult.java b/sdk/src/main/java/org/zstack/sdk/tpm/api/GetTpmCapabilityResult.java new file mode 100644 index 00000000000..66d43f8a70d --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/tpm/api/GetTpmCapabilityResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.tpm.api; + +import org.zstack.sdk.tpm.entity.TpmCapabilityView; + +public class GetTpmCapabilityResult { + public TpmCapabilityView inventory; + public void setInventory(TpmCapabilityView inventory) { + this.inventory = inventory; + } + public TpmCapabilityView getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/tpm/api/QueryTpmAction.java b/sdk/src/main/java/org/zstack/sdk/tpm/api/QueryTpmAction.java new file mode 100644 index 00000000000..e6b41993ebb --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/tpm/api/QueryTpmAction.java @@ -0,0 +1,75 @@ +package org.zstack.sdk.tpm.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class QueryTpmAction extends QueryAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.tpm.api.QueryTpmResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.tpm.api.QueryTpmResult value = res.getResult(org.zstack.sdk.tpm.api.QueryTpmResult.class); + ret.value = value == null ? new org.zstack.sdk.tpm.api.QueryTpmResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "GET"; + info.path = "/tpms"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/tpm/api/QueryTpmResult.java b/sdk/src/main/java/org/zstack/sdk/tpm/api/QueryTpmResult.java new file mode 100644 index 00000000000..1414409e00a --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/tpm/api/QueryTpmResult.java @@ -0,0 +1,22 @@ +package org.zstack.sdk.tpm.api; + + + +public class QueryTpmResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + + public java.lang.Long total; + public void setTotal(java.lang.Long total) { + this.total = total; + } + public java.lang.Long getTotal() { + return this.total; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/tpm/api/RemoveTpmAction.java b/sdk/src/main/java/org/zstack/sdk/tpm/api/RemoveTpmAction.java new file mode 100644 index 00000000000..a404cfe8df3 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/tpm/api/RemoveTpmAction.java @@ -0,0 +1,107 @@ +package org.zstack.sdk.tpm.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class RemoveTpmAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.tpm.api.RemoveTpmResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String tpmUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vmInstanceUuid; + + @Param(required = false) + public java.lang.String deleteMode = "Permissive"; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.tpm.api.RemoveTpmResult value = res.getResult(org.zstack.sdk.tpm.api.RemoveTpmResult.class); + ret.value = value == null ? new org.zstack.sdk.tpm.api.RemoveTpmResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "DELETE"; + info.path = "/tpms"; + info.needSession = true; + info.needPoll = true; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/tpm/api/RemoveTpmResult.java b/sdk/src/main/java/org/zstack/sdk/tpm/api/RemoveTpmResult.java new file mode 100644 index 00000000000..93f74261a83 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/tpm/api/RemoveTpmResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk.tpm.api; + + + +public class RemoveTpmResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/tpm/api/UpdateTpmAction.java b/sdk/src/main/java/org/zstack/sdk/tpm/api/UpdateTpmAction.java new file mode 100644 index 00000000000..28fd0257b14 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/tpm/api/UpdateTpmAction.java @@ -0,0 +1,107 @@ +package org.zstack.sdk.tpm.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class UpdateTpmAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.tpm.api.UpdateTpmResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String vmInstanceUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String tpmUuid; + + @Param(required = false, maxLength = 32, minLength = 32, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String keyProviderUuid; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.tpm.api.UpdateTpmResult value = res.getResult(org.zstack.sdk.tpm.api.UpdateTpmResult.class); + ret.value = value == null ? new org.zstack.sdk.tpm.api.UpdateTpmResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "PUT"; + info.path = "/tpms"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "updateTpm"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/tpm/api/UpdateTpmResult.java b/sdk/src/main/java/org/zstack/sdk/tpm/api/UpdateTpmResult.java new file mode 100644 index 00000000000..1157fd732cb --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/tpm/api/UpdateTpmResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.tpm.api; + +import org.zstack.sdk.tpm.entity.TpmInventory; + +public class UpdateTpmResult { + public TpmInventory inventory; + public void setInventory(TpmInventory inventory) { + this.inventory = inventory; + } + public TpmInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmCapabilityView.java b/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmCapabilityView.java new file mode 100644 index 00000000000..f77138cdfc7 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmCapabilityView.java @@ -0,0 +1,79 @@ +package org.zstack.sdk.tpm.entity; + + + +public class TpmCapabilityView { + + public java.lang.String uuid; + public void setUuid(java.lang.String uuid) { + this.uuid = uuid; + } + public java.lang.String getUuid() { + return this.uuid; + } + + public java.lang.String name; + public void setName(java.lang.String name) { + this.name = name; + } + public java.lang.String getName() { + return this.name; + } + + public java.lang.String vmInstanceUuid; + public void setVmInstanceUuid(java.lang.String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + public java.lang.String getVmInstanceUuid() { + return this.vmInstanceUuid; + } + + public java.sql.Timestamp createDate; + public void setCreateDate(java.sql.Timestamp createDate) { + this.createDate = createDate; + } + public java.sql.Timestamp getCreateDate() { + return this.createDate; + } + + public java.sql.Timestamp lastOpDate; + public void setLastOpDate(java.sql.Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + public java.sql.Timestamp getLastOpDate() { + return this.lastOpDate; + } + + public java.util.List hostRefs; + public void setHostRefs(java.util.List hostRefs) { + this.hostRefs = hostRefs; + } + public java.util.List getHostRefs() { + return this.hostRefs; + } + + public java.lang.String edkVersion; + public void setEdkVersion(java.lang.String edkVersion) { + this.edkVersion = edkVersion; + } + public java.lang.String getEdkVersion() { + return this.edkVersion; + } + + public java.lang.String swtpmVersion; + public void setSwtpmVersion(java.lang.String swtpmVersion) { + this.swtpmVersion = swtpmVersion; + } + public java.lang.String getSwtpmVersion() { + return this.swtpmVersion; + } + + public boolean resetTpmAfterVmCloneConfig; + public void setResetTpmAfterVmCloneConfig(boolean resetTpmAfterVmCloneConfig) { + this.resetTpmAfterVmCloneConfig = resetTpmAfterVmCloneConfig; + } + public boolean getResetTpmAfterVmCloneConfig() { + return this.resetTpmAfterVmCloneConfig; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmHostRefInventory.java b/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmHostRefInventory.java new file mode 100644 index 00000000000..85db7ad9eb8 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmHostRefInventory.java @@ -0,0 +1,55 @@ +package org.zstack.sdk.tpm.entity; + + + +public class TpmHostRefInventory { + + public long id; + public void setId(long id) { + this.id = id; + } + public long getId() { + return this.id; + } + + public java.lang.String tpmUuid; + public void setTpmUuid(java.lang.String tpmUuid) { + this.tpmUuid = tpmUuid; + } + public java.lang.String getTpmUuid() { + return this.tpmUuid; + } + + public java.lang.String hostUuid; + public void setHostUuid(java.lang.String hostUuid) { + this.hostUuid = hostUuid; + } + public java.lang.String getHostUuid() { + return this.hostUuid; + } + + public java.lang.String path; + public void setPath(java.lang.String path) { + this.path = path; + } + public java.lang.String getPath() { + return this.path; + } + + public java.sql.Timestamp createDate; + public void setCreateDate(java.sql.Timestamp createDate) { + this.createDate = createDate; + } + public java.sql.Timestamp getCreateDate() { + return this.createDate; + } + + public java.sql.Timestamp lastOpDate; + public void setLastOpDate(java.sql.Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + public java.sql.Timestamp getLastOpDate() { + return this.lastOpDate; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmInventory.java b/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmInventory.java new file mode 100644 index 00000000000..e4fa21a0746 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmInventory.java @@ -0,0 +1,55 @@ +package org.zstack.sdk.tpm.entity; + + + +public class TpmInventory { + + public java.lang.String uuid; + public void setUuid(java.lang.String uuid) { + this.uuid = uuid; + } + public java.lang.String getUuid() { + return this.uuid; + } + + public java.lang.String name; + public void setName(java.lang.String name) { + this.name = name; + } + public java.lang.String getName() { + return this.name; + } + + public java.lang.String vmInstanceUuid; + public void setVmInstanceUuid(java.lang.String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + public java.lang.String getVmInstanceUuid() { + return this.vmInstanceUuid; + } + + public java.sql.Timestamp createDate; + public void setCreateDate(java.sql.Timestamp createDate) { + this.createDate = createDate; + } + public java.sql.Timestamp getCreateDate() { + return this.createDate; + } + + public java.sql.Timestamp lastOpDate; + public void setLastOpDate(java.sql.Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + public java.sql.Timestamp getLastOpDate() { + return this.lastOpDate; + } + + public java.util.List hostRefs; + public void setHostRefs(java.util.List hostRefs) { + this.hostRefs = hostRefs; + } + public java.util.List getHostRefs() { + return this.hostRefs; + } + +} diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index 6bf72dbe39a..7a23a1a7b42 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -243,4 +243,11 @@ + + + + + + + diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index 07c05b73b9e..51b3f4811da 100644 --- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy @@ -6929,33 +6929,6 @@ abstract class ApiHelper { } - def updateHostname(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UpdateHostnameAction.class) Closure c) { - def a = new org.zstack.sdk.UpdateHostnameAction() - 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 createIPsecConnection(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.CreateIPsecConnectionAction.class) Closure c) { def a = new org.zstack.sdk.CreateIPsecConnectionAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -32212,6 +32185,33 @@ abstract class ApiHelper { } + def updateHostname(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UpdateHostnameAction.class) Closure c) { + def a = new org.zstack.sdk.UpdateHostnameAction() + 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 updateIPsecConnection(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UpdateIPsecConnectionAction.class) Closure c) { def a = new org.zstack.sdk.UpdateIPsecConnectionAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -38395,8 +38395,8 @@ abstract class ApiHelper { } - def addZBox(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zbox.AddZBoxAction.class) Closure c) { - def a = new org.zstack.sdk.zbox.AddZBoxAction() + def cleanSoftwarePackage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.CleanSoftwarePackageAction.class) Closure c) { + def a = new org.zstack.sdk.softwarePackage.header.CleanSoftwarePackageAction() a.sessionId = Test.currentEnvSpec?.session?.uuid c.resolveStrategy = Closure.OWNER_FIRST c.delegate = a @@ -38422,8 +38422,8 @@ abstract class ApiHelper { } - def createZBoxBackup(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zbox.CreateZBoxBackupAction.class) Closure c) { - def a = new org.zstack.sdk.zbox.CreateZBoxBackupAction() + def getDirectoryUsage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.GetDirectoryUsageAction.class) Closure c) { + def a = new org.zstack.sdk.softwarePackage.header.GetDirectoryUsageAction() a.sessionId = Test.currentEnvSpec?.session?.uuid c.resolveStrategy = Closure.OWNER_FIRST c.delegate = a @@ -38449,8 +38449,8 @@ abstract class ApiHelper { } - def ejectZBox(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zbox.EjectZBoxAction.class) Closure c) { - def a = new org.zstack.sdk.zbox.EjectZBoxAction() + def getUploadSoftwarePackageJobDetails(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.GetUploadSoftwarePackageJobDetailsAction.class) Closure c) { + def a = new org.zstack.sdk.softwarePackage.header.GetUploadSoftwarePackageJobDetailsAction() a.sessionId = Test.currentEnvSpec?.session?.uuid c.resolveStrategy = Closure.OWNER_FIRST c.delegate = a @@ -38476,8 +38476,8 @@ abstract class ApiHelper { } - def getZBoxBackupDetails(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zbox.GetZBoxBackupDetailsAction.class) Closure c) { - def a = new org.zstack.sdk.zbox.GetZBoxBackupDetailsAction() + def installSoftwarePackage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.InstallSoftwarePackageAction.class) Closure c) { + def a = new org.zstack.sdk.softwarePackage.header.InstallSoftwarePackageAction() a.sessionId = Test.currentEnvSpec?.session?.uuid c.resolveStrategy = Closure.OWNER_FIRST c.delegate = a @@ -38503,8 +38503,8 @@ abstract class ApiHelper { } - def queryZBox(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zbox.QueryZBoxAction.class) Closure c) { - def a = new org.zstack.sdk.zbox.QueryZBoxAction() + def querySoftwarePackage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.QuerySoftwarePackageAction.class) Closure c) { + def a = new org.zstack.sdk.softwarePackage.header.QuerySoftwarePackageAction() a.sessionId = Test.currentEnvSpec?.session?.uuid c.resolveStrategy = Closure.OWNER_FIRST c.delegate = a @@ -38532,15 +38532,13 @@ abstract class ApiHelper { } - def queryZBoxBackup(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zbox.QueryZBoxBackupAction.class) Closure c) { - def a = new org.zstack.sdk.zbox.QueryZBoxBackupAction() + def uninstallSoftwarePackage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.UninstallSoftwarePackageAction.class) Closure c) { + def a = new org.zstack.sdk.softwarePackage.header.UninstallSoftwarePackageAction() 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) { @@ -38561,8 +38559,8 @@ abstract class ApiHelper { } - def syncZBoxCapacity(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zbox.SyncZBoxCapacityAction.class) Closure c) { - def a = new org.zstack.sdk.zbox.SyncZBoxCapacityAction() + def uploadSoftwarePackage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.UploadSoftwarePackageAction.class) Closure c) { + def a = new org.zstack.sdk.softwarePackage.header.UploadSoftwarePackageAction() a.sessionId = Test.currentEnvSpec?.session?.uuid c.resolveStrategy = Closure.OWNER_FIRST c.delegate = a @@ -38588,25 +38586,26 @@ abstract class ApiHelper { } - def cleanSoftwarePackage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.CleanSoftwarePackageAction.class) Closure c) { - def a = new org.zstack.sdk.softwarePackage.header.CleanSoftwarePackageAction() + 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()) @@ -38614,25 +38613,82 @@ abstract class ApiHelper { } - def getDirectoryUsage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.GetDirectoryUsageAction.class) Closure c) { - def a = new org.zstack.sdk.softwarePackage.header.GetDirectoryUsageAction() + 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()) @@ -38640,25 +38696,80 @@ abstract class ApiHelper { } - def getUploadSoftwarePackageJobDetails(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.GetUploadSoftwarePackageJobDetailsAction.class) Closure c) { - def a = new org.zstack.sdk.softwarePackage.header.GetUploadSoftwarePackageJobDetailsAction() + 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()) + } + } + + + def addZBox(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zbox.AddZBoxAction.class) Closure c) { + def a = new org.zstack.sdk.zbox.AddZBoxAction() + 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 createZBoxBackup(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zbox.CreateZBoxBackupAction.class) Closure c) { + def a = new org.zstack.sdk.zbox.CreateZBoxBackupAction() + 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()) @@ -38666,22 +38777,26 @@ abstract class ApiHelper { } - def installSoftwarePackage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.InstallSoftwarePackageAction.class) Closure c) { - def a = new org.zstack.sdk.softwarePackage.header.InstallSoftwarePackageAction() + def ejectZBox(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zbox.EjectZBoxAction.class) Closure c) { + def a = new org.zstack.sdk.zbox.EjectZBoxAction() 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()) @@ -38689,13 +38804,40 @@ abstract class ApiHelper { } - def querySoftwarePackage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.QuerySoftwarePackageAction.class) Closure c) { - def a = new org.zstack.sdk.softwarePackage.header.QuerySoftwarePackageAction() + def getZBoxBackupDetails(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zbox.GetZBoxBackupDetailsAction.class) Closure c) { + def a = new org.zstack.sdk.zbox.GetZBoxBackupDetailsAction() 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 queryZBox(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zbox.QueryZBoxAction.class) Closure c) { + def a = new org.zstack.sdk.zbox.QueryZBoxAction() + a.sessionId = Test.currentEnvSpec?.session?.uuid + c.resolveStrategy = Closure.OWNER_FIRST + c.delegate = a + c() + a.conditions = a.conditions.collect { it.toString() } @@ -38703,14 +38845,14 @@ abstract class ApiHelper { 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()) @@ -38718,26 +38860,28 @@ abstract class ApiHelper { } - def uninstallSoftwarePackage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.UninstallSoftwarePackageAction.class) Closure c) { - def a = new org.zstack.sdk.softwarePackage.header.UninstallSoftwarePackageAction() + def queryZBoxBackup(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zbox.QueryZBoxBackupAction.class) Closure c) { + def a = new org.zstack.sdk.zbox.QueryZBoxBackupAction() 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()) @@ -38745,25 +38889,26 @@ abstract class ApiHelper { } - def uploadSoftwarePackage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.softwarePackage.header.UploadSoftwarePackageAction.class) Closure c) { - def a = new org.zstack.sdk.softwarePackage.header.UploadSoftwarePackageAction() + def syncZBoxCapacity(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.zbox.SyncZBoxCapacityAction.class) Closure c) { + def a = new org.zstack.sdk.zbox.SyncZBoxCapacityAction() 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()) diff --git a/utils/src/main/java/org/zstack/utils/StringDSL.java b/utils/src/main/java/org/zstack/utils/StringDSL.java index 54cbf4135b8..705b88a8b4d 100755 --- a/utils/src/main/java/org/zstack/utils/StringDSL.java +++ b/utils/src/main/java/org/zstack/utils/StringDSL.java @@ -150,8 +150,12 @@ public static boolean isApiId(String apiId) { } public static String createFixedUuid(Class voClass) { + return createFixedUuid(voClass.getSimpleName()); + } + + public static String createFixedUuid(String text) { StringBuilder builder = new StringBuilder( - UUID.nameUUIDFromBytes(voClass.getSimpleName().getBytes()).toString().replace("-", "")); + UUID.nameUUIDFromBytes(text.getBytes()).toString().replace("-", "")); builder.setCharAt(0, 'f'); builder.setCharAt(1, 'f'); builder.setCharAt(2, '0'); From 6c80457c663ce81238a2a730018987327b94959b Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 4 Feb 2026 15:21:52 +0800 Subject: [PATCH 05/64] [compute]: support create VM with TPM Resolves: ZSV-11310 Change-Id: I6b746e6a666d7267626d647777677a77716a6261 --- .../vm/InstantiateVmFromNewCreatedStruct.java | 12 +++++++ .../org/zstack/compute/vm/VmInstanceBase.java | 1 + .../compute/vm/VmInstanceManagerImpl.java | 3 +- .../zstack/compute/vm/VmInstanceUtils.java | 1 + .../compute/vm/devices/VmTpmExtensions.java | 27 ++++++++++++++++ .../compute/vm/devices/VmTpmManager.java | 29 +++++++++++++++++ conf/springConfigXml/Kvm.xml | 6 ++++ conf/springConfigXml/VmInstanceManager.xml | 8 +++++ ...plianceVmInstanceCreateExtensionPoint.java | 5 +++ .../zstack/header/vm/CreateVmInstanceMsg.java | 10 ++++++ .../InstantiateNewCreatedVmInstanceMsg.java | 10 ++++++ .../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 | 6 ++++ 19 files changed, 196 insertions(+), 15 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 485d78a1b79..55d5eb7a1c1 100644 --- a/compute/src/main/java/org/zstack/compute/vm/InstantiateVmFromNewCreatedStruct.java +++ b/compute/src/main/java/org/zstack/compute/vm/InstantiateVmFromNewCreatedStruct.java @@ -3,6 +3,7 @@ import org.zstack.header.configuration.VmCustomSpecificationStruct; import org.zstack.header.host.CpuArchitecture; import org.zstack.header.vm.*; +import org.zstack.header.vm.devices.VmDevicesSpec; import java.util.ArrayList; import java.util.List; @@ -32,6 +33,7 @@ public class InstantiateVmFromNewCreatedStruct { private List dataDisks; private List deprecatedDataVolumeSpecs = new ArrayList<>(); private VmCustomSpecificationStruct vmCustomSpecification; + private VmDevicesSpec devicesSpec; public List getCandidatePrimaryStorageUuidsForRootVolume() { return candidatePrimaryStorageUuidsForRootVolume; @@ -87,6 +89,14 @@ public void setVmCustomSpecification(VmCustomSpecificationStruct vmCustomSpecifi this.vmCustomSpecification = vmCustomSpecification; } + public VmDevicesSpec getDevicesSpec() { + return devicesSpec; + } + + public void setDevicesSpec(VmDevicesSpec devicesSpec) { + this.devicesSpec = devicesSpec; + } + public List getRootVolumeSystemTags() { return rootVolumeSystemTags; } @@ -167,6 +177,7 @@ public static InstantiateVmFromNewCreatedStruct fromMessage(InstantiateNewCreate struct.setDataDisks(msg.getDataDisks()); struct.setDeprecatedDataVolumeSpecs(msg.getDeprecatedDataVolumeSpecs()); struct.setVmCustomSpecification(msg.getVmCustomSpecification()); + struct.setDevicesSpec(msg.getDevicesSpec()); return struct; } @@ -187,6 +198,7 @@ public static InstantiateVmFromNewCreatedStruct fromMessage(CreateVmInstanceMsg struct.setDataDisks(msg.getDataDisks()); struct.setDeprecatedDataVolumeSpecs(msg.getDeprecatedDataVolumeSpecs()); struct.setVmCustomSpecification(msg.getVmCustomSpecification()); + 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 e31bc001218..9ad123a6ef1 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java @@ -7705,6 +7705,7 @@ private VmInstanceSpec buildVmInstanceSpecFromStruct(InstantiateVmFromNewCreated spec.setDataDisks(struct.getDataDisks()); spec.setDeprecatedDisksSpecs(struct.getDeprecatedDataVolumeSpecs()); spec.setVmCustomSpecification(struct.getVmCustomSpecification()); + 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 0838c24d9c3..d3a6d6396b2 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceManagerImpl.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceManagerImpl.java @@ -1217,7 +1217,7 @@ public void setup() { @Override public void run(FlowTrigger trigger, Map data) { pluginRgty.getExtensionList(VmInstanceCreateExtensionPoint.class).forEach( - extensionPoint -> extensionPoint.afterPersistVmInstanceVO(finalVo)); + extensionPoint -> extensionPoint.afterPersistVmInstanceVO(finalVo, msg)); trigger.next(); } @@ -1352,6 +1352,7 @@ public void run(FlowTrigger trigger, Map data) { smsg.setDataDisks(msg.getDataDisks()); smsg.setDeprecatedDataVolumeSpecs(msg.getDeprecatedDataVolumeSpecs()); smsg.setVmCustomSpecification(msg.getVmCustomSpecification()); + 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 33afa043278..bebcae3520b 100644 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceUtils.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceUtils.java @@ -44,6 +44,7 @@ public static CreateVmInstanceMsg fromAPICreateVmInstanceMsg(APICreateVmInstance cmsg.setGuestOsType(msg.getGuestOsType()); cmsg.setArchitecture(msg.getArchitecture()); cmsg.setStrategy(msg.getStrategy()); + 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 7a7160db29a..4fb474f97a9 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -251,4 +251,10 @@ + + + + + + diff --git a/conf/springConfigXml/VmInstanceManager.xml b/conf/springConfigXml/VmInstanceManager.xml index 8d3f7cc41e6..6786f86c780 100755 --- a/conf/springConfigXml/VmInstanceManager.xml +++ b/conf/springConfigXml/VmInstanceManager.xml @@ -279,4 +279,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 037d9f8b323..5b6f04dac3a 100755 --- a/header/src/main/java/org/zstack/header/vm/CreateVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/CreateVmInstanceMsg.java @@ -2,6 +2,7 @@ import org.zstack.header.configuration.VmCustomSpecificationStruct; 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 dataDisks; private List deprecatedDataVolumeSpecs = new ArrayList<>(); private VmCustomSpecificationStruct vmCustomSpecification; + private VmDevicesSpec devicesSpec; public List getCandidatePrimaryStorageUuidsForRootVolume() { return candidatePrimaryStorageUuidsForRootVolume; @@ -363,4 +365,12 @@ public VmCustomSpecificationStruct getVmCustomSpecification() { public void setVmCustomSpecification(VmCustomSpecificationStruct vmCustomSpecification) { this.vmCustomSpecification = vmCustomSpecification; } + + 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 55a57474009..8c01f099ea2 100755 --- a/header/src/main/java/org/zstack/header/vm/InstantiateNewCreatedVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/InstantiateNewCreatedVmInstanceMsg.java @@ -3,6 +3,7 @@ import org.zstack.header.configuration.VmCustomSpecificationStruct; import org.zstack.header.host.CpuArchitecture; import org.zstack.header.message.NeedReplyMessage; +import org.zstack.header.vm.devices.VmDevicesSpec; import java.util.ArrayList; import java.util.List; @@ -59,6 +60,7 @@ public void setCandidatePrimaryStorageUuidsForDataVolume(List candidateP private DiskAO rootDisk; private List dataDisks; private List deprecatedDataVolumeSpecs; + private VmDevicesSpec devicesSpec; public DiskAO getRootDisk() { return rootDisk; @@ -84,6 +86,14 @@ public void setDeprecatedDataVolumeSpecs(List deprecatedDataVolumeSpecs) this.deprecatedDataVolumeSpecs = deprecatedDataVolumeSpecs; } + public VmDevicesSpec getDevicesSpec() { + return devicesSpec; + } + + public void setDevicesSpec(VmDevicesSpec devicesSpec) { + this.devicesSpec = devicesSpec; + } + public List getSoftAvoidHostUuids() { return softAvoidHostUuids; } 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 e3bbe33bf11..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,10 +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) {} + 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 4732fb36d40..18620a65fad 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java @@ -17,6 +17,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; @@ -403,6 +404,7 @@ public void setCandidatePrimaryStorageUuidsForDataVolume(List candidateP private List dataDisks; private List deprecatedDisksSpecs = new ArrayList<>(); private VmCustomSpecificationStruct vmCustomSpecification; + private VmDevicesSpec devicesSpec; public DiskAO getRootDisk() { return rootDisk; @@ -436,6 +438,14 @@ public void setVmCustomSpecification(VmCustomSpecificationStruct vmCustomSpecifi this.vmCustomSpecification = vmCustomSpecification; } + 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 98419fcc8a2..48e3f429ab4 100755 --- a/plugin/applianceVm/src/main/java/org/zstack/appliancevm/CreateApplianceVmJob.java +++ b/plugin/applianceVm/src/main/java/org/zstack/appliancevm/CreateApplianceVmJob.java @@ -177,8 +177,8 @@ protected ApplianceVmVO scripts() { } final ApplianceVmVO finalVO = avo; - pluginRgty.getExtensionList(VmInstanceCreateExtensionPoint.class).forEach( - extension -> extension.afterPersistVmInstanceVO(finalVO)); + 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 f69eff87317..2dd6e223fc3 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.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; @@ -2068,6 +2069,7 @@ public static class StartVmCmd extends vdiCmd implements VmAddOnsCmd { private List dataVolumes; private List cacheVolumes; private List nics; + private TpmTO tpm; private long timeout; private Map addons; private boolean instanceOfferingOnlineChange; @@ -2545,6 +2547,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 7a23a1a7b42..9551587dd5d 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -250,4 +250,10 @@ + + + + + + From 2407b21b90b80c09b7e75c5c3dd37734dddde2ba Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 4 Feb 2026 18:30:53 +0800 Subject: [PATCH 06/64] [compute]: add secure boot dependency Resolves: ZSV-11310 Resolves: ZSPHER-1 Change-Id: I6d787a7375636c77656176616478717872676478 --- .../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/efi/KvmSecureBootExtensions.java | 53 +++++++++++++++++++ .../test/resources/springConfigXml/Kvm.xml | 6 +++ 6 files changed, 76 insertions(+), 4 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 37621cca6a7..d588936ee53 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java @@ -113,6 +113,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 4fb474f97a9..9536f559eb3 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -257,4 +257,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 2dd6e223fc3..6ec7e3429b7 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -2104,6 +2104,8 @@ public static class StartVmCmd extends vdiCmd implements VmAddOnsCmd { private String bootMode; // used when bootMode == 'UEFI' private boolean secureBoot; + private String edkVersion; + private boolean fromForeignHypervisor; private String machineType; private Integer pciePortNums; @@ -2260,6 +2262,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 23d7b1cfe47..9d22cf50976 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -4519,10 +4519,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/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 9551587dd5d..462aa352dc8 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -256,4 +256,10 @@ + + + + + + From 131a9ca75781a60a759e73e518958cd6172d041d Mon Sep 17 00:00:00 2001 From: "tao.yang" Date: Fri, 6 Feb 2026 19:55:28 +0800 Subject: [PATCH 07/64] [kms]: support kms DBImpact Resolves: ZSV-11331 Change-Id: I786f686371626e6674636772676c68747768716a --- conf/db/zsv/V5.0.0__schema.sql | 83 ++++ conf/persistence.xml | 6 + .../zstack/header/host/HostKeyIdentityVO.java | 78 ++++ .../header/host/HostKeyIdentityVO_.java | 14 + sdk/src/main/java/SourceClassMap.java | 10 + .../org/zstack/sdk/KeyProviderInventory.java | 63 ++++ .../org/zstack/sdk/KmsIdentityInventory.java | 71 ++++ .../java/org/zstack/sdk/KmsInventory.java | 71 ++++ .../java/org/zstack/sdk/NkpInventory.java | 39 ++ .../java/org/zstack/sdk/NkpRestoreInfo.java | 63 ++++ .../api/QueryKeyProviderAction.java | 75 ++++ .../api/QueryKeyProviderResult.java | 22 ++ .../api/RekeyKeyProviderRefsAction.java | 104 +++++ .../api/RekeyKeyProviderRefsResult.java | 7 + .../keyprovider/kms/api/CreateKmsAction.java | 128 +++++++ .../keyprovider/kms/api/CreateKmsResult.java | 14 + .../keyprovider/kms/api/DeleteKmsAction.java | 104 +++++ .../keyprovider/kms/api/DeleteKmsResult.java | 7 + .../keyprovider/kms/api/QueryKmsAction.java | 75 ++++ .../keyprovider/kms/api/QueryKmsResult.java | 22 ++ .../keyprovider/kms/api/UpdateKmsAction.java | 119 ++++++ .../keyprovider/kms/api/UpdateKmsResult.java | 14 + .../keyprovider/nkp/api/BackupNkpAction.java | 104 +++++ .../keyprovider/nkp/api/BackupNkpResult.java | 14 + .../keyprovider/nkp/api/CreateNkpAction.java | 119 ++++++ .../keyprovider/nkp/api/CreateNkpResult.java | 14 + .../keyprovider/nkp/api/DeleteNkpAction.java | 104 +++++ .../keyprovider/nkp/api/DeleteNkpResult.java | 7 + .../nkp/api/ParseNkpRestoreAction.java | 104 +++++ .../nkp/api/ParseNkpRestoreResult.java | 14 + .../keyprovider/nkp/api/QueryNkpAction.java | 75 ++++ .../keyprovider/nkp/api/QueryNkpResult.java | 22 ++ .../keyprovider/nkp/api/RestoreNkpAction.java | 104 +++++ .../keyprovider/nkp/api/RestoreNkpResult.java | 14 + .../keyprovider/nkp/api/UpdateNkpAction.java | 104 +++++ .../keyprovider/nkp/api/UpdateNkpResult.java | 14 + .../java/org/zstack/testlib/ApiHelper.groovy | 357 ++++++++++++++++++ 37 files changed, 2359 insertions(+) create mode 100644 header/src/main/java/org/zstack/header/host/HostKeyIdentityVO.java create mode 100644 header/src/main/java/org/zstack/header/host/HostKeyIdentityVO_.java create mode 100644 sdk/src/main/java/org/zstack/sdk/KeyProviderInventory.java create mode 100644 sdk/src/main/java/org/zstack/sdk/KmsIdentityInventory.java create mode 100644 sdk/src/main/java/org/zstack/sdk/KmsInventory.java create mode 100644 sdk/src/main/java/org/zstack/sdk/NkpInventory.java create mode 100644 sdk/src/main/java/org/zstack/sdk/NkpRestoreInfo.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/api/QueryKeyProviderAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/api/QueryKeyProviderResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/CreateKmsAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/CreateKmsResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/DeleteKmsAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/DeleteKmsResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/QueryKmsAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/QueryKmsResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UpdateKmsAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UpdateKmsResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/BackupNkpAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/BackupNkpResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/CreateNkpAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/CreateNkpResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/DeleteNkpAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/DeleteNkpResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/ParseNkpRestoreAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/ParseNkpRestoreResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/QueryNkpAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/QueryNkpResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/RestoreNkpAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/RestoreNkpResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/UpdateNkpAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/UpdateNkpResult.java diff --git a/conf/db/zsv/V5.0.0__schema.sql b/conf/db/zsv/V5.0.0__schema.sql index 3ee7e729906..590c58ee528 100644 --- a/conf/db/zsv/V5.0.0__schema.sql +++ b/conf/db/zsv/V5.0.0__schema.sql @@ -20,3 +20,86 @@ CREATE TABLE IF NOT EXISTS `zstack`.`TpmHostRefVO` ( CONSTRAINT `fkTpmHostRefVOTpmVO` FOREIGN KEY (`tpmUuid`) REFERENCES `TpmVO` (`uuid`) ON UPDATE RESTRICT ON DELETE CASCADE, CONSTRAINT `fkTpmHostRefVOHostVO` FOREIGN KEY (`hostUuid`) REFERENCES `HostEO` (`uuid`) ON UPDATE RESTRICT ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Feature: KMS | ZSPHER-46, ZSPHER-60, ZSPHER-61, ZSPHER-62 + +CREATE TABLE IF NOT EXISTS `zstack`.`KeyProviderVO` ( + `uuid` varchar(32) NOT NULL UNIQUE, + `name` varchar(255) NOT NULL, + `description` varchar(2048) DEFAULT NULL, + `type` varchar(32) NOT NULL, + `connected` boolean NOT NULL DEFAULT FALSE, + `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59', + PRIMARY KEY (`uuid`), + UNIQUE KEY `ukKeyProviderVOName` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`KmsVO` ( + `uuid` varchar(32) NOT NULL UNIQUE, + `endpoint` varchar(255) NOT NULL, + `port` int unsigned NOT NULL, + `kmipVersion` varchar(32) DEFAULT NULL, + `username` varchar(255) DEFAULT NULL, + `password` varchar(255) DEFAULT NULL, + `trusted` boolean NOT NULL DEFAULT FALSE, + `activeIdentityUuid` varchar(32) DEFAULT NULL, + `serverCertExpiredDate` timestamp NULL DEFAULT NULL, + `serverCertPem` text DEFAULT NULL, + PRIMARY KEY (`uuid`), + INDEX `idxKmsVOActiveIdentityUuid` (`activeIdentityUuid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`KmsIdentityVO` ( + `uuid` varchar(32) NOT NULL UNIQUE, + `kmsUuid` varchar(32) NOT NULL, + `identityType` varchar(32) NOT NULL, + `clientCertPem` text DEFAULT NULL, + `clientKeyPem` text DEFAULT NULL, + `csrPem` text DEFAULT NULL, + `certExpiredDate` timestamp NULL DEFAULT NULL, + `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59', + PRIMARY KEY (`uuid`), + UNIQUE KEY `ukKmsIdentityVOKmsUuidType` (`kmsUuid`, `identityType`), + INDEX `idxKmsIdentityVOKmsUuid` (`kmsUuid`), + CONSTRAINT `fkKmsIdentityVOKmsVO` FOREIGN KEY (`kmsUuid`) REFERENCES `KmsVO` (`uuid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`NkpVO` ( + `uuid` varchar(32) NOT NULL UNIQUE, + `kdf` varchar(64) NOT NULL, + `saltPolicy` varchar(64) NOT NULL, + `backedUp` boolean NOT NULL DEFAULT FALSE, + `currentVersion` int unsigned DEFAULT NULL, + PRIMARY KEY (`uuid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`HostKeyIdentityVO` ( + `hostUuid` varchar(32) NOT NULL UNIQUE, + `publicKey` text NOT NULL, + `fingerprint` varchar(128) NOT NULL, + `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59', + PRIMARY KEY (`hostUuid`), + CONSTRAINT `fkHostKeyIdentityVOHostEO` FOREIGN KEY (`hostUuid`) REFERENCES `HostEO` (`uuid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`EncryptedResourceKeyRefVO` ( + `id` bigint unsigned NOT NULL UNIQUE AUTO_INCREMENT, + `resourceType` varchar(255) NOT NULL, + `resourceUuid` varchar(32) NOT NULL, + `providerUuid` varchar(32) DEFAULT NULL, + `providerName` varchar(255) NOT NULL, + `keyVersion` int unsigned DEFAULT NULL, + `kekRef` varchar(255) DEFAULT NULL, + `wrappedDek` text NOT NULL, + `algorithm` varchar(64) DEFAULT NULL, + `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59', + PRIMARY KEY (`id`), + INDEX `idxEncryptedResourceKeyRefVOResource` (`resourceUuid`, `resourceType`), + INDEX `idxEncryptedResourceKeyRefVOProviderUuid` (`providerUuid`), + INDEX `idxEncryptedResourceKeyRefVOProviderName` (`providerName`), + CONSTRAINT `fkEncryptedResourceKeyRefVOProviderUuid` FOREIGN KEY (`providerUuid`) REFERENCES `KeyProviderVO` (`uuid`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/conf/persistence.xml b/conf/persistence.xml index aa295bcb365..ff82022f554 100755 --- a/conf/persistence.xml +++ b/conf/persistence.xml @@ -24,6 +24,7 @@ org.zstack.header.cluster.ClusterEO org.zstack.header.host.HostVO org.zstack.header.host.HostEO + org.zstack.header.host.HostKeyIdentityVO org.zstack.header.host.CpuFeaturesHistoryVO org.zstack.header.storage.primary.PrimaryStorageVO org.zstack.header.storage.primary.PrimaryStorageEO @@ -182,6 +183,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.VmInstanceResourceMetadataVO org.zstack.header.vm.devices.VmInstanceResourceMetadataArchiveVO 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; + } +} 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..a43d01375a0 --- /dev/null +++ b/header/src/main/java/org/zstack/header/host/HostKeyIdentityVO_.java @@ -0,0 +1,14 @@ +package org.zstack.header.host; + +import javax.persistence.metamodel.SingularAttribute; +import javax.persistence.metamodel.StaticMetamodel; +import java.sql.Timestamp; + +@StaticMetamodel(HostKeyIdentityVO.class) +public class HostKeyIdentityVO_ { + public static volatile SingularAttribute hostUuid; + public static volatile SingularAttribute publicKey; + public static volatile SingularAttribute fingerprint; + public static volatile SingularAttribute createDate; + public static volatile SingularAttribute lastOpDate; +} diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 821f401de47..3829d9077ac 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -178,6 +178,11 @@ public class SourceClassMap { put("org.zstack.header.image.APIGetUploadImageJobDetailsReply$JobDetails", "org.zstack.sdk.JobDetails"); put("org.zstack.header.image.ImageBackupStorageRefInventory", "org.zstack.sdk.ImageBackupStorageRefInventory"); put("org.zstack.header.image.ImageInventory", "org.zstack.sdk.ImageInventory"); + put("org.zstack.header.keyprovider.KeyProviderInventory", "org.zstack.sdk.KeyProviderInventory"); + put("org.zstack.header.keyprovider.KmsIdentityInventory", "org.zstack.sdk.KmsIdentityInventory"); + put("org.zstack.header.keyprovider.KmsInventory", "org.zstack.sdk.KmsInventory"); + put("org.zstack.header.keyprovider.NkpInventory", "org.zstack.sdk.NkpInventory"); + put("org.zstack.header.keyprovider.NkpRestoreInfo", "org.zstack.sdk.NkpRestoreInfo"); put("org.zstack.header.longjob.LongJobInventory", "org.zstack.sdk.LongJobInventory"); put("org.zstack.header.longjob.LongJobState", "org.zstack.sdk.LongJobState"); put("org.zstack.header.managementnode.ManagementNodeInventory", "org.zstack.sdk.ManagementNodeInventory"); @@ -882,6 +887,9 @@ public class SourceClassMap { put("org.zstack.sdk.KVMCephVolumeTO", "org.zstack.storage.ceph.primary.KVMCephVolumeTO"); put("org.zstack.sdk.KVMHostInventory", "org.zstack.kvm.KVMHostInventory"); put("org.zstack.sdk.KVMIsoTO", "org.zstack.kvm.KVMIsoTO"); + put("org.zstack.sdk.KeyProviderInventory", "org.zstack.header.keyprovider.KeyProviderInventory"); + put("org.zstack.sdk.KmsIdentityInventory", "org.zstack.header.keyprovider.KmsIdentityInventory"); + put("org.zstack.sdk.KmsInventory", "org.zstack.header.keyprovider.KmsInventory"); put("org.zstack.sdk.KvmCephCdRomTO", "org.zstack.storage.ceph.primary.KvmCephCdRomTO"); put("org.zstack.sdk.KvmCephIsoTO", "org.zstack.storage.ceph.primary.KvmCephIsoTO"); put("org.zstack.sdk.KvmHostHypervisorMetadataInventory", "org.zstack.kvm.hypervisor.datatype.KvmHostHypervisorMetadataInventory"); @@ -951,6 +959,8 @@ public class SourceClassMap { put("org.zstack.sdk.NetworkServiceL3NetworkRefInventory", "org.zstack.header.network.service.NetworkServiceL3NetworkRefInventory"); put("org.zstack.sdk.NetworkServiceProviderInventory", "org.zstack.header.network.service.NetworkServiceProviderInventory"); put("org.zstack.sdk.NicTO", "org.zstack.kvm.KVMAgentCommands$NicTO"); + put("org.zstack.sdk.NkpInventory", "org.zstack.header.keyprovider.NkpInventory"); + put("org.zstack.sdk.NkpRestoreInfo", "org.zstack.header.keyprovider.NkpRestoreInfo"); put("org.zstack.sdk.NormalIpRangeInventory", "org.zstack.header.network.l3.NormalIpRangeInventory"); put("org.zstack.sdk.NvmeLunHostRefInventory", "org.zstack.storage.device.nvme.NvmeLunHostRefInventory"); put("org.zstack.sdk.NvmeLunInventory", "org.zstack.storage.device.nvme.NvmeLunInventory"); diff --git a/sdk/src/main/java/org/zstack/sdk/KeyProviderInventory.java b/sdk/src/main/java/org/zstack/sdk/KeyProviderInventory.java new file mode 100644 index 00000000000..a2eef45777e --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/KeyProviderInventory.java @@ -0,0 +1,63 @@ +package org.zstack.sdk; + + + +public class KeyProviderInventory { + + public java.lang.String uuid; + public void setUuid(java.lang.String uuid) { + this.uuid = uuid; + } + public java.lang.String getUuid() { + return this.uuid; + } + + public java.lang.String name; + public void setName(java.lang.String name) { + this.name = name; + } + public java.lang.String getName() { + return this.name; + } + + public java.lang.String description; + public void setDescription(java.lang.String description) { + this.description = description; + } + public java.lang.String getDescription() { + return this.description; + } + + public java.lang.String type; + public void setType(java.lang.String type) { + this.type = type; + } + public java.lang.String getType() { + return this.type; + } + + public boolean connected; + public void setConnected(boolean connected) { + this.connected = connected; + } + public boolean getConnected() { + return this.connected; + } + + public java.sql.Timestamp createDate; + public void setCreateDate(java.sql.Timestamp createDate) { + this.createDate = createDate; + } + public java.sql.Timestamp getCreateDate() { + return this.createDate; + } + + public java.sql.Timestamp lastOpDate; + public void setLastOpDate(java.sql.Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + public java.sql.Timestamp getLastOpDate() { + return this.lastOpDate; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/KmsIdentityInventory.java b/sdk/src/main/java/org/zstack/sdk/KmsIdentityInventory.java new file mode 100644 index 00000000000..d621aa1d749 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/KmsIdentityInventory.java @@ -0,0 +1,71 @@ +package org.zstack.sdk; + + + +public class KmsIdentityInventory { + + public java.lang.String uuid; + public void setUuid(java.lang.String uuid) { + this.uuid = uuid; + } + public java.lang.String getUuid() { + return this.uuid; + } + + public java.lang.String kmsUuid; + public void setKmsUuid(java.lang.String kmsUuid) { + this.kmsUuid = kmsUuid; + } + public java.lang.String getKmsUuid() { + return this.kmsUuid; + } + + public java.lang.String identityType; + public void setIdentityType(java.lang.String identityType) { + this.identityType = identityType; + } + public java.lang.String getIdentityType() { + return this.identityType; + } + + public java.lang.String clientCertPem; + public void setClientCertPem(java.lang.String clientCertPem) { + this.clientCertPem = clientCertPem; + } + public java.lang.String getClientCertPem() { + return this.clientCertPem; + } + + public java.lang.String csrPem; + public void setCsrPem(java.lang.String csrPem) { + this.csrPem = csrPem; + } + public java.lang.String getCsrPem() { + return this.csrPem; + } + + public java.sql.Timestamp certExpiredDate; + public void setCertExpiredDate(java.sql.Timestamp certExpiredDate) { + this.certExpiredDate = certExpiredDate; + } + public java.sql.Timestamp getCertExpiredDate() { + return this.certExpiredDate; + } + + public java.sql.Timestamp createDate; + public void setCreateDate(java.sql.Timestamp createDate) { + this.createDate = createDate; + } + public java.sql.Timestamp getCreateDate() { + return this.createDate; + } + + public java.sql.Timestamp lastOpDate; + public void setLastOpDate(java.sql.Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + public java.sql.Timestamp getLastOpDate() { + return this.lastOpDate; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/KmsInventory.java b/sdk/src/main/java/org/zstack/sdk/KmsInventory.java new file mode 100644 index 00000000000..fb544369699 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/KmsInventory.java @@ -0,0 +1,71 @@ +package org.zstack.sdk; + +import org.zstack.sdk.KmsIdentityInventory; + +public class KmsInventory extends org.zstack.sdk.KeyProviderInventory { + + public java.lang.String endpoint; + public void setEndpoint(java.lang.String endpoint) { + this.endpoint = endpoint; + } + public java.lang.String getEndpoint() { + return this.endpoint; + } + + public java.lang.Integer port; + public void setPort(java.lang.Integer port) { + this.port = port; + } + public java.lang.Integer getPort() { + return this.port; + } + + public java.lang.String kmipVersion; + public void setKmipVersion(java.lang.String kmipVersion) { + this.kmipVersion = kmipVersion; + } + public java.lang.String getKmipVersion() { + return this.kmipVersion; + } + + public java.lang.String username; + public void setUsername(java.lang.String username) { + this.username = username; + } + public java.lang.String getUsername() { + return this.username; + } + + public boolean trusted; + public void setTrusted(boolean trusted) { + this.trusted = trusted; + } + public boolean getTrusted() { + return this.trusted; + } + + public java.lang.String activeIdentityUuid; + public void setActiveIdentityUuid(java.lang.String activeIdentityUuid) { + this.activeIdentityUuid = activeIdentityUuid; + } + public java.lang.String getActiveIdentityUuid() { + return this.activeIdentityUuid; + } + + public java.sql.Timestamp serverCertExpiredDate; + public void setServerCertExpiredDate(java.sql.Timestamp serverCertExpiredDate) { + this.serverCertExpiredDate = serverCertExpiredDate; + } + public java.sql.Timestamp getServerCertExpiredDate() { + return this.serverCertExpiredDate; + } + + public KmsIdentityInventory activeIdentity; + public void setActiveIdentity(KmsIdentityInventory activeIdentity) { + this.activeIdentity = activeIdentity; + } + public KmsIdentityInventory getActiveIdentity() { + return this.activeIdentity; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/NkpInventory.java b/sdk/src/main/java/org/zstack/sdk/NkpInventory.java new file mode 100644 index 00000000000..b76e38f3313 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/NkpInventory.java @@ -0,0 +1,39 @@ +package org.zstack.sdk; + + + +public class NkpInventory extends org.zstack.sdk.KeyProviderInventory { + + public java.lang.String kdf; + public void setKdf(java.lang.String kdf) { + this.kdf = kdf; + } + public java.lang.String getKdf() { + return this.kdf; + } + + public java.lang.String saltPolicy; + public void setSaltPolicy(java.lang.String saltPolicy) { + this.saltPolicy = saltPolicy; + } + public java.lang.String getSaltPolicy() { + return this.saltPolicy; + } + + public boolean backedUp; + public void setBackedUp(boolean backedUp) { + this.backedUp = backedUp; + } + public boolean getBackedUp() { + return this.backedUp; + } + + public java.lang.Integer currentVersion; + public void setCurrentVersion(java.lang.Integer currentVersion) { + this.currentVersion = currentVersion; + } + public java.lang.Integer getCurrentVersion() { + return this.currentVersion; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/NkpRestoreInfo.java b/sdk/src/main/java/org/zstack/sdk/NkpRestoreInfo.java new file mode 100644 index 00000000000..ae041ab435b --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/NkpRestoreInfo.java @@ -0,0 +1,63 @@ +package org.zstack.sdk; + + + +public class NkpRestoreInfo { + + public java.lang.String uuid; + public void setUuid(java.lang.String uuid) { + this.uuid = uuid; + } + public java.lang.String getUuid() { + return this.uuid; + } + + public java.lang.String name; + public void setName(java.lang.String name) { + this.name = name; + } + public java.lang.String getName() { + return this.name; + } + + public java.lang.String description; + public void setDescription(java.lang.String description) { + this.description = description; + } + public java.lang.String getDescription() { + return this.description; + } + + public java.lang.String kdf; + public void setKdf(java.lang.String kdf) { + this.kdf = kdf; + } + public java.lang.String getKdf() { + return this.kdf; + } + + public java.lang.String saltPolicy; + public void setSaltPolicy(java.lang.String saltPolicy) { + this.saltPolicy = saltPolicy; + } + public java.lang.String getSaltPolicy() { + return this.saltPolicy; + } + + public java.lang.String encryptedMasterSeed; + public void setEncryptedMasterSeed(java.lang.String encryptedMasterSeed) { + this.encryptedMasterSeed = encryptedMasterSeed; + } + public java.lang.String getEncryptedMasterSeed() { + return this.encryptedMasterSeed; + } + + public java.lang.Integer currentVersion; + public void setCurrentVersion(java.lang.Integer currentVersion) { + this.currentVersion = currentVersion; + } + public java.lang.Integer getCurrentVersion() { + return this.currentVersion; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/api/QueryKeyProviderAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/QueryKeyProviderAction.java new file mode 100644 index 00000000000..e3d50dc92c0 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/QueryKeyProviderAction.java @@ -0,0 +1,75 @@ +package org.zstack.sdk.keyprovider.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class QueryKeyProviderAction extends QueryAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.keyprovider.api.QueryKeyProviderResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.keyprovider.api.QueryKeyProviderResult value = res.getResult(org.zstack.sdk.keyprovider.api.QueryKeyProviderResult.class); + ret.value = value == null ? new org.zstack.sdk.keyprovider.api.QueryKeyProviderResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "GET"; + info.path = "/key-providers"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/api/QueryKeyProviderResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/QueryKeyProviderResult.java new file mode 100644 index 00000000000..b331fb5348a --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/QueryKeyProviderResult.java @@ -0,0 +1,22 @@ +package org.zstack.sdk.keyprovider.api; + + + +public class QueryKeyProviderResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + + public java.lang.Long total; + public void setTotal(java.lang.Long total) { + this.total = total; + } + public java.lang.Long getTotal() { + return this.total; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsAction.java new file mode 100644 index 00000000000..2d005f5c3dc --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk.keyprovider.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class RekeyKeyProviderRefsAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.keyprovider.api.RekeyKeyProviderRefsResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.util.List refIds; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String providerUuid; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.keyprovider.api.RekeyKeyProviderRefsResult value = res.getResult(org.zstack.sdk.keyprovider.api.RekeyKeyProviderRefsResult.class); + ret.value = value == null ? new org.zstack.sdk.keyprovider.api.RekeyKeyProviderRefsResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "PUT"; + info.path = "/key-providers/{providerUuid}/rekey"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "rekeyKeyProviderRefs"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsResult.java new file mode 100644 index 00000000000..ffb0dd51c80 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk.keyprovider.api; + + + +public class RekeyKeyProviderRefsResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/CreateKmsAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/CreateKmsAction.java new file mode 100644 index 00000000000..827c6d5fa38 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/CreateKmsAction.java @@ -0,0 +1,128 @@ +package org.zstack.sdk.keyprovider.kms.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class CreateKmsAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.keyprovider.kms.api.CreateKmsResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, maxLength = 255, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String endpoint; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, numberRange = {1L,65535L}, noTrim = false) + public java.lang.Integer port; + + @Param(required = false, validValues = {"1.0","1.1","1.2","1.3","1.4","2.0","2.1"}, maxLength = 32, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String kmipVersion = "1.2"; + + @Param(required = false, maxLength = 255, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String username; + + @Param(required = false, maxLength = 255, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String password; + + @Param(required = true, maxLength = 255, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String name; + + @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String description; + + @Param(required = false) + public java.lang.String type; + + @Param(required = false) + public java.lang.String resourceUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List tagUuids; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.keyprovider.kms.api.CreateKmsResult value = res.getResult(org.zstack.sdk.keyprovider.kms.api.CreateKmsResult.class); + ret.value = value == null ? new org.zstack.sdk.keyprovider.kms.api.CreateKmsResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "POST"; + info.path = "/key-providers/kms"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/CreateKmsResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/CreateKmsResult.java new file mode 100644 index 00000000000..ae1e24307a1 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/CreateKmsResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.keyprovider.kms.api; + +import org.zstack.sdk.KeyProviderInventory; + +public class CreateKmsResult { + public KeyProviderInventory inventory; + public void setInventory(KeyProviderInventory inventory) { + this.inventory = inventory; + } + public KeyProviderInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/DeleteKmsAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/DeleteKmsAction.java new file mode 100644 index 00000000000..e724f5bb544 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/DeleteKmsAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk.keyprovider.kms.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class DeleteKmsAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.keyprovider.kms.api.DeleteKmsResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @Param(required = false) + public java.lang.String deleteMode = "Permissive"; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.keyprovider.kms.api.DeleteKmsResult value = res.getResult(org.zstack.sdk.keyprovider.kms.api.DeleteKmsResult.class); + ret.value = value == null ? new org.zstack.sdk.keyprovider.kms.api.DeleteKmsResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "DELETE"; + info.path = "/key-providers/kms/{uuid}"; + info.needSession = true; + info.needPoll = true; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/DeleteKmsResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/DeleteKmsResult.java new file mode 100644 index 00000000000..a88e9a3559e --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/DeleteKmsResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk.keyprovider.kms.api; + + + +public class DeleteKmsResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/QueryKmsAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/QueryKmsAction.java new file mode 100644 index 00000000000..184ed6729d0 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/QueryKmsAction.java @@ -0,0 +1,75 @@ +package org.zstack.sdk.keyprovider.kms.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class QueryKmsAction extends QueryAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.keyprovider.kms.api.QueryKmsResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.keyprovider.kms.api.QueryKmsResult value = res.getResult(org.zstack.sdk.keyprovider.kms.api.QueryKmsResult.class); + ret.value = value == null ? new org.zstack.sdk.keyprovider.kms.api.QueryKmsResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "GET"; + info.path = "/key-providers/kms"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/QueryKmsResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/QueryKmsResult.java new file mode 100644 index 00000000000..d0d76d2f982 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/QueryKmsResult.java @@ -0,0 +1,22 @@ +package org.zstack.sdk.keyprovider.kms.api; + + + +public class QueryKmsResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + + public java.lang.Long total; + public void setTotal(java.lang.Long total) { + this.total = total; + } + public java.lang.Long getTotal() { + return this.total; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UpdateKmsAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UpdateKmsAction.java new file mode 100644 index 00000000000..566dd7087f0 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UpdateKmsAction.java @@ -0,0 +1,119 @@ +package org.zstack.sdk.keyprovider.kms.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class UpdateKmsAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.keyprovider.kms.api.UpdateKmsResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = false, maxLength = 255, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String endpoint; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = false, numberRange = {1L,65535L}, noTrim = false) + public java.lang.Integer port; + + @Param(required = false, validValues = {"1.0","1.1","1.2","1.3","1.4","2.0","2.1"}, maxLength = 32, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String kmipVersion; + + @Param(required = false, maxLength = 255, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String username; + + @Param(required = false, maxLength = 255, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String password; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String uuid; + + @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String description; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.keyprovider.kms.api.UpdateKmsResult value = res.getResult(org.zstack.sdk.keyprovider.kms.api.UpdateKmsResult.class); + ret.value = value == null ? new org.zstack.sdk.keyprovider.kms.api.UpdateKmsResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "PUT"; + info.path = "/key-providers/kms/{uuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "updateKms"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UpdateKmsResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UpdateKmsResult.java new file mode 100644 index 00000000000..8f48d409109 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UpdateKmsResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.keyprovider.kms.api; + +import org.zstack.sdk.KeyProviderInventory; + +public class UpdateKmsResult { + public KeyProviderInventory inventory; + public void setInventory(KeyProviderInventory inventory) { + this.inventory = inventory; + } + public KeyProviderInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/BackupNkpAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/BackupNkpAction.java new file mode 100644 index 00000000000..e4e996818c1 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/BackupNkpAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk.keyprovider.nkp.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class BackupNkpAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.keyprovider.nkp.api.BackupNkpResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String uuid; + + @Param(required = false, maxLength = 255, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String password; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.keyprovider.nkp.api.BackupNkpResult value = res.getResult(org.zstack.sdk.keyprovider.nkp.api.BackupNkpResult.class); + ret.value = value == null ? new org.zstack.sdk.keyprovider.nkp.api.BackupNkpResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "PUT"; + info.path = "/key-providers/nkp/{uuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "backupNkp"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/BackupNkpResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/BackupNkpResult.java new file mode 100644 index 00000000000..a8880061e50 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/BackupNkpResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.keyprovider.nkp.api; + + + +public class BackupNkpResult { + public java.lang.String content; + public void setContent(java.lang.String content) { + this.content = content; + } + public java.lang.String getContent() { + return this.content; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/CreateNkpAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/CreateNkpAction.java new file mode 100644 index 00000000000..45ca9d824c9 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/CreateNkpAction.java @@ -0,0 +1,119 @@ +package org.zstack.sdk.keyprovider.nkp.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class CreateNkpAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.keyprovider.nkp.api.CreateNkpResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = false, validValues = {"HKDF-SHA256"}, maxLength = 64, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String kdf = "HKDF-SHA256"; + + @Param(required = false, validValues = {"providerName"}, maxLength = 64, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String saltPolicy = "providerName"; + + @Param(required = true, maxLength = 255, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String name; + + @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String description; + + @Param(required = false) + public java.lang.String type; + + @Param(required = false) + public java.lang.String resourceUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.util.List tagUuids; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.keyprovider.nkp.api.CreateNkpResult value = res.getResult(org.zstack.sdk.keyprovider.nkp.api.CreateNkpResult.class); + ret.value = value == null ? new org.zstack.sdk.keyprovider.nkp.api.CreateNkpResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "POST"; + info.path = "/key-providers/nkp"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/CreateNkpResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/CreateNkpResult.java new file mode 100644 index 00000000000..e00c3112235 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/CreateNkpResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.keyprovider.nkp.api; + +import org.zstack.sdk.KeyProviderInventory; + +public class CreateNkpResult { + public KeyProviderInventory inventory; + public void setInventory(KeyProviderInventory inventory) { + this.inventory = inventory; + } + public KeyProviderInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/DeleteNkpAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/DeleteNkpAction.java new file mode 100644 index 00000000000..7e7b433eaf6 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/DeleteNkpAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk.keyprovider.nkp.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class DeleteNkpAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.keyprovider.nkp.api.DeleteNkpResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @Param(required = false) + public java.lang.String deleteMode = "Permissive"; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.keyprovider.nkp.api.DeleteNkpResult value = res.getResult(org.zstack.sdk.keyprovider.nkp.api.DeleteNkpResult.class); + ret.value = value == null ? new org.zstack.sdk.keyprovider.nkp.api.DeleteNkpResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "DELETE"; + info.path = "/key-providers/nkp/{uuid}"; + info.needSession = true; + info.needPoll = true; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/DeleteNkpResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/DeleteNkpResult.java new file mode 100644 index 00000000000..3dbd48e495b --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/DeleteNkpResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk.keyprovider.nkp.api; + + + +public class DeleteNkpResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/ParseNkpRestoreAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/ParseNkpRestoreAction.java new file mode 100644 index 00000000000..db7265e783f --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/ParseNkpRestoreAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk.keyprovider.nkp.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class ParseNkpRestoreAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.keyprovider.nkp.api.ParseNkpRestoreResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String contentBase64; + + @Param(required = false, maxLength = 255, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String password; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.keyprovider.nkp.api.ParseNkpRestoreResult value = res.getResult(org.zstack.sdk.keyprovider.nkp.api.ParseNkpRestoreResult.class); + ret.value = value == null ? new org.zstack.sdk.keyprovider.nkp.api.ParseNkpRestoreResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "PUT"; + info.path = "/key-providers/nkp/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "parseNkpRestore"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/ParseNkpRestoreResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/ParseNkpRestoreResult.java new file mode 100644 index 00000000000..2fee861ac11 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/ParseNkpRestoreResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.keyprovider.nkp.api; + +import org.zstack.sdk.NkpRestoreInfo; + +public class ParseNkpRestoreResult { + public NkpRestoreInfo restoreInfo; + public void setRestoreInfo(NkpRestoreInfo restoreInfo) { + this.restoreInfo = restoreInfo; + } + public NkpRestoreInfo getRestoreInfo() { + return this.restoreInfo; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/QueryNkpAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/QueryNkpAction.java new file mode 100644 index 00000000000..6eb869c7821 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/QueryNkpAction.java @@ -0,0 +1,75 @@ +package org.zstack.sdk.keyprovider.nkp.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class QueryNkpAction extends QueryAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.keyprovider.nkp.api.QueryNkpResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.keyprovider.nkp.api.QueryNkpResult value = res.getResult(org.zstack.sdk.keyprovider.nkp.api.QueryNkpResult.class); + ret.value = value == null ? new org.zstack.sdk.keyprovider.nkp.api.QueryNkpResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "GET"; + info.path = "/key-providers/nkp"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/QueryNkpResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/QueryNkpResult.java new file mode 100644 index 00000000000..3a1b8f44abd --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/QueryNkpResult.java @@ -0,0 +1,22 @@ +package org.zstack.sdk.keyprovider.nkp.api; + + + +public class QueryNkpResult { + public java.util.List inventories; + public void setInventories(java.util.List inventories) { + this.inventories = inventories; + } + public java.util.List getInventories() { + return this.inventories; + } + + public java.lang.Long total; + public void setTotal(java.lang.Long total) { + this.total = total; + } + public java.lang.Long getTotal() { + return this.total; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/RestoreNkpAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/RestoreNkpAction.java new file mode 100644 index 00000000000..f7c44403fe8 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/RestoreNkpAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk.keyprovider.nkp.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class RestoreNkpAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.keyprovider.nkp.api.RestoreNkpResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String contentBase64; + + @Param(required = false, maxLength = 255, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String password; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.keyprovider.nkp.api.RestoreNkpResult value = res.getResult(org.zstack.sdk.keyprovider.nkp.api.RestoreNkpResult.class); + ret.value = value == null ? new org.zstack.sdk.keyprovider.nkp.api.RestoreNkpResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "PUT"; + info.path = "/key-providers/nkp/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "restoreNkp"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/RestoreNkpResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/RestoreNkpResult.java new file mode 100644 index 00000000000..812f84e8fa9 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/RestoreNkpResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.keyprovider.nkp.api; + +import org.zstack.sdk.NkpInventory; + +public class RestoreNkpResult { + public NkpInventory inventory; + public void setInventory(NkpInventory inventory) { + this.inventory = inventory; + } + public NkpInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/UpdateNkpAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/UpdateNkpAction.java new file mode 100644 index 00000000000..3020624f823 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/UpdateNkpAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk.keyprovider.nkp.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class UpdateNkpAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.keyprovider.nkp.api.UpdateNkpResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String uuid; + + @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String description; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.keyprovider.nkp.api.UpdateNkpResult value = res.getResult(org.zstack.sdk.keyprovider.nkp.api.UpdateNkpResult.class); + ret.value = value == null ? new org.zstack.sdk.keyprovider.nkp.api.UpdateNkpResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "PUT"; + info.path = "/key-providers/nkp/{uuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "updateNkp"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/UpdateNkpResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/UpdateNkpResult.java new file mode 100644 index 00000000000..b7302e2c291 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/UpdateNkpResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.keyprovider.nkp.api; + +import org.zstack.sdk.KeyProviderInventory; + +public class UpdateNkpResult { + public KeyProviderInventory inventory; + public void setInventory(KeyProviderInventory inventory) { + this.inventory = inventory; + } + public KeyProviderInventory getInventory() { + return this.inventory; + } + +} diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index 51b3f4811da..cfd879afb45 100644 --- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy @@ -36498,6 +36498,363 @@ abstract class ApiHelper { } + 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 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 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) { + 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 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 getManagementNodesStatus(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.managements.common.GetManagementNodesStatusAction.class) Closure c) { def a = new org.zstack.sdk.managements.common.GetManagementNodesStatusAction() a.sessionId = Test.currentEnvSpec?.session?.uuid From 902413bf825f36453ba672b74dda5f8eca46a97f Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 11 Feb 2026 15:35:53 +0800 Subject: [PATCH 08/64] [storage]: improve error message in VolumeSnapshotApiInterceptor This patch is for zsv_5.0.0 Related: ZSV-11338 Change-Id: I666c7763796f7070796762767669786e6e75736b --- .../snapshot/VolumeSnapshotConstant.java | 12 ++++++++++ .../VolumeSnapshotApiInterceptor.java | 22 ++++++++++--------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/VolumeSnapshotConstant.java b/header/src/main/java/org/zstack/header/storage/snapshot/VolumeSnapshotConstant.java index 8f784a72051..adb5c5ea436 100755 --- a/header/src/main/java/org/zstack/header/storage/snapshot/VolumeSnapshotConstant.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/VolumeSnapshotConstant.java @@ -1,5 +1,10 @@ package org.zstack.header.storage.snapshot; +import org.zstack.header.vm.VmInstanceState; + +import java.util.Arrays; +import java.util.List; + /** */ public interface VolumeSnapshotConstant { @@ -16,4 +21,11 @@ public interface VolumeSnapshotConstant { String VOLUME_SNAPSHOT_STRUCT = "VolumeSnapshotStruct"; String NEED_TAKE_SNAPSHOTS_ON_HYPERVISOR = "needTakeSnapshotOnHypervisor"; String NEED_BLOCK_STREAM_ON_HYPERVISOR = "needBlockStreamOnHypervisor"; + + public static final List ALLOW_TAKE_SNAPSHOTS_VM_STATES = Arrays.asList( + VmInstanceState.Running, VmInstanceState.Stopped, VmInstanceState.Paused + ); + public static final List ALLOW_TAKE_MEMORY_SNAPSHOTS_VM_STATES = Arrays.asList( + VmInstanceState.Stopped + ); } diff --git a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotApiInterceptor.java b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotApiInterceptor.java index 1da009fc2d9..417fdb4c390 100755 --- a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotApiInterceptor.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotApiInterceptor.java @@ -5,7 +5,6 @@ import org.zstack.core.componentloader.PluginRegistry; import org.zstack.core.db.DatabaseFacade; import org.zstack.core.db.Q; -import org.zstack.core.db.SQL; import org.zstack.core.db.SimpleQuery; import org.zstack.core.db.SimpleQuery.Op; import org.zstack.core.errorcode.ErrorFacade; @@ -16,7 +15,6 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.storage.snapshot.*; import org.zstack.header.storage.snapshot.group.*; -import org.zstack.header.vm.VmInstanceInventory; import org.zstack.header.vm.VmInstanceState; import org.zstack.header.vm.VmInstanceVO; import org.zstack.header.vm.VmInstanceVO_; @@ -29,7 +27,6 @@ import static org.zstack.storage.snapshot.VolumeSnapshotMessageRouter.getResourceIdToRouteMsg; import javax.persistence.Tuple; -import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -113,13 +110,18 @@ private void validate(APIRevertVmFromSnapshotGroupMsg msg) { } } - if (isWithMemoryForSnapshotGroup(group) - && Q.New(VmInstanceVO.class) - .eq(VmInstanceVO_.uuid, group.getVmInstanceUuid()) - .in(VmInstanceVO_.state, Arrays.asList(VmInstanceState.Running, VmInstanceState.Paused)) - .isExists()) { - throw new ApiMessageInterceptionException(argerr("Can not take memory snapshot, expected vm states are [%s, %s]", - VmInstanceState.Running.toString(), VmInstanceState.Paused.toString())); + if (isWithMemoryForSnapshotGroup(group)) { + VmInstanceState vmState = Q.New(VmInstanceVO.class) + .select(VmInstanceVO_.state) + .eq(VmInstanceVO_.uuid, group.getVmInstanceUuid()) + .findValue(); + if (!VolumeSnapshotConstant.ALLOW_TAKE_MEMORY_SNAPSHOTS_VM_STATES.contains(vmState)) { + throw new ApiMessageInterceptionException(argerr( + "Can not revert VM with memory snapshot: unexpected VM state") + .withOpaque("vm.uuid", group.getVmInstanceUuid()) + .withOpaque("vm.state", vmState.toString()) + .withOpaque("expect.states", VolumeSnapshotConstant.ALLOW_TAKE_MEMORY_SNAPSHOTS_VM_STATES)); + } } msg.setVmInstanceUuid(group.getVmInstanceUuid()); From c56181bbf02438a0b49d86359ed6a25f8a79a78d Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 11 Feb 2026 18:55:48 +0800 Subject: [PATCH 09/64] [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 4a724abac2a..1b6e5bce8aa 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java @@ -160,8 +160,8 @@ public class KVMGlobalConfig { ) public static GlobalConfig KVMAGENT_PHYSICAL_MEMORY_USAGE_HARD_LIMIT = new GlobalConfig(CATEGORY, "kvmagent.physicalmemory.usage.hardlimit"); - @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 5e65a39208054aca378e9720630bc1cd9be8bc0c Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 12 Feb 2026 14:40:34 +0800 Subject: [PATCH 10/64] [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 7cd78c36c93..cb79da59838 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -183,6 +183,8 @@ public interface KVMConstant { public static final String L2_PROVIDER_TYPE_OVS_DPDK = "OvsDpdk"; public static final String L2_PROVIDER_TYPE_MACVLAN = "MacVlan"; + public static final String EDK_VERSION_NONE = "None"; + public static final String DHCP_BIN_FILE_PATH = "/usr/local/zstack/dnsmasq"; enum KvmVmState { 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 1b6e5bce8aa..96caaa0e5b6 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 @@ -160,7 +162,7 @@ public class KVMGlobalConfig { ) public static GlobalConfig KVMAGENT_PHYSICAL_MEMORY_USAGE_HARD_LIMIT = new GlobalConfig(CATEGORY, "kvmagent.physicalmemory.usage.hardlimit"); - @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 694b4a75241900b28186f27eed068088abbf34d8 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 24 Feb 2026 16:15:59 +0800 Subject: [PATCH 11/64] [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 8563169b335..2542bc2cb03 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 0af3ed1eeb1e43b5da1f09e8d42012658f140ec4 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 13 Feb 2026 14:59:35 +0800 Subject: [PATCH 12/64] [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 b791ecb1658..a9dac33e70f 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmDeleteVolumeFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmDeleteVolumeFlow.java @@ -49,7 +49,9 @@ public void run(final FlowTrigger trigger, Map data) { final boolean templated = isTemplated(spec.getVmInventory().getUuid()); /* 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); } @@ -60,7 +62,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 = transformAndRemoveNull(spec.getVmInventory().getAllVolumes(), arg -> { if (VolumeType.Data.toString().equals(arg.getType()) && !deleteDataDisk && !templated) { return null; 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 9ad123a6ef1..49b076a2fc6 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java @@ -2593,7 +2593,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 ff82022f554..5a1b855d9e8 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 9536f559eb3..16cd80fadf9 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -261,6 +261,7 @@ + diff --git a/conf/springConfigXml/VmInstanceManager.xml b/conf/springConfigXml/VmInstanceManager.xml index 6786f86c780..0e82353d042 100755 --- a/conf/springConfigXml/VmInstanceManager.xml +++ b/conf/springConfigXml/VmInstanceManager.xml @@ -116,6 +116,7 @@ org.zstack.compute.vm.VmExpungeRootVolumeFlow + org.zstack.compute.vm.VmExpungeNvRamVolumeFlow org.zstack.compute.vm.VmExpungeMemoryVolumeFlow org.zstack.compute.vm.VmExpungeCacheVolumeFlow @@ -285,6 +286,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 9d0efdd77f1..e767df877f1 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 { @@ -25,6 +26,8 @@ public interface VmInstanceConstant { String SHUTDOWN_DETAIL_BY_GUEST = "by guest"; String SHUTDOWN_DETAIL_FINISHED = "finished"; + 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 18620a65fad..95ccdc22bf2 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java @@ -405,6 +405,7 @@ public void setCandidatePrimaryStorageUuidsForDataVolume(List candidateP private List deprecatedDisksSpecs = new ArrayList<>(); private VmCustomSpecificationStruct vmCustomSpecification; private VmDevicesSpec devicesSpec; + private DiskAO nvRamSpec; public DiskAO getRootDisk() { return rootDisk; @@ -446,6 +447,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 6ec7e3429b7..16865309ec0 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -2068,6 +2068,7 @@ public static class StartVmCmd extends vdiCmd implements VmAddOnsCmd { private List cdRoms = new ArrayList<>(); private List dataVolumes; private List cacheVolumes; + private VolumeTO nvRam; private List nics; private TpmTO tpm; private long timeout; @@ -2549,6 +2550,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..8f2e33af8ff 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.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 e8d268e518a..9bba32e1d57 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 @@ -963,6 +963,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())); } @@ -1288,6 +1292,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 272d75d7a09..e0ef4a6141a 100755 --- a/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java +++ b/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java @@ -551,7 +551,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); } } } @@ -593,7 +594,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 462aa352dc8..4e57880a7ce 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -260,6 +260,7 @@ + From 5f7c7138357237d2b95769ae0f0cc87c3d3dc41f Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 13 Feb 2026 18:28:55 +0800 Subject: [PATCH 13/64] [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 | 8 +++++++ .../nfs/NfsPrimaryStorageKvmHelper.java | 4 ++++ .../storage/primary/smp/KvmBackend.java | 10 ++++++++- 6 files changed, 56 insertions(+), 5 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 9429bf63645..68e311dbcb1 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 @@ -1729,7 +1729,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 9bba32e1d57..9f3c1431589 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 @@ -33,6 +33,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; @@ -202,6 +203,7 @@ public static class CreateEmptyVolumeCmd extends AgentCommand { private String name; private String volumeUuid; private String backingFile; + private String format; public String getBackingFile() { return backingFile; @@ -211,6 +213,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; } @@ -1258,7 +1268,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()); } @@ -1283,7 +1293,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())) { @@ -1294,6 +1308,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()); @@ -1304,7 +1319,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 93d3d7aab99..b52fb127eda 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 @@ -1107,6 +1107,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())); } @@ -1139,7 +1142,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 d7cebfaa4c6..a105552fdd9 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 @@ -2,6 +2,7 @@ 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; @@ -319,6 +320,7 @@ public abstract static class CreateVolumeCmd extends NfsPrimaryStorageAgentComma private String hypervisorType; private String name; private String volumeUuid; + private String volumeFormat = VolumeConstant.VOLUME_FORMAT_QCOW2; protected long virtualSize; public String getInstallUrl() { @@ -351,6 +353,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 long getVirtualSize() { return virtualSize; } 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 ee7faa6c40c..af5e39a76bb 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 @@ -321,6 +321,7 @@ public static class CreateEmptyVolumeCmd extends AgentCmd { public String name; public String volumeUuid; public String backingFile; + public String volumeFormat = VolumeConstant.VOLUME_FORMAT_QCOW2; } public static class CreateEmptyVolumeRsp extends AgentRsp { @@ -617,6 +618,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)); } @@ -969,6 +974,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"); } @@ -982,7 +990,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 97ac90821a4a266dce460ccabfce9b40c6cc3fed Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 26 Feb 2026 11:13:35 +0800 Subject: [PATCH 14/64] [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 9f3c1431589..f45e9127817 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 @@ -203,7 +203,7 @@ public static class CreateEmptyVolumeCmd extends AgentCommand { private String name; private String volumeUuid; private String backingFile; - private String format; + private String volumeFormat; public String getBackingFile() { return backingFile; @@ -213,12 +213,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() { @@ -1293,11 +1293,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())) { @@ -1308,7 +1308,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()); @@ -1320,7 +1320,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 369c182f9f320886f2d435a7c1d7deb90bb70824 Mon Sep 17 00:00:00 2001 From: "tao.yang" Date: Thu, 26 Feb 2026 21:40:29 +0800 Subject: [PATCH 15/64] [kms]: support kms trust API Resolves: ZSV-11331 Change-Id: I63646d7974756278777565696276797066796f68 --- sdk/src/main/java/SourceClassMap.java | 2 + .../java/org/zstack/sdk/CertificateInfo.java | 55 +++++++ .../java/org/zstack/sdk/KmsInventory.java | 19 ++- .../java/org/zstack/sdk/NkpRestoreInfo.java | 16 +-- .../keyprovider/kms/api/CreateKmsAction.java | 3 - .../api/GetKmsServerCertFromKmsAction.java | 101 +++++++++++++ .../api/GetKmsServerCertFromKmsResult.java | 30 ++++ .../kms/api/UploadKmsClientCsrAction.java | 107 ++++++++++++++ .../kms/api/UploadKmsClientCsrResult.java | 14 ++ .../api/UploadKmsClientIdentityAction.java | 110 ++++++++++++++ .../api/UploadKmsClientIdentityResult.java | 14 ++ .../api/UploadKmsClientSignedCertAction.java | 104 ++++++++++++++ .../api/UploadKmsClientSignedCertResult.java | 14 ++ .../kms/api/UploadKmsServerCertAction.java | 104 ++++++++++++++ .../kms/api/UploadKmsServerCertResult.java | 14 ++ .../keyprovider/nkp/api/CreateNkpAction.java | 3 - .../nkp/api/ParseNkpRestoreResult.java | 16 +++ .../java/org/zstack/testlib/ApiHelper.groovy | 135 ++++++++++++++++++ 18 files changed, 842 insertions(+), 19 deletions(-) create mode 100644 sdk/src/main/java/org/zstack/sdk/CertificateInfo.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/GetKmsServerCertFromKmsAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/GetKmsServerCertFromKmsResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientCsrAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientCsrResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientIdentityAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientIdentityResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientSignedCertAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientSignedCertResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsServerCertAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsServerCertResult.java diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 3829d9077ac..160dc8aad5e 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -178,6 +178,7 @@ public class SourceClassMap { put("org.zstack.header.image.APIGetUploadImageJobDetailsReply$JobDetails", "org.zstack.sdk.JobDetails"); put("org.zstack.header.image.ImageBackupStorageRefInventory", "org.zstack.sdk.ImageBackupStorageRefInventory"); put("org.zstack.header.image.ImageInventory", "org.zstack.sdk.ImageInventory"); + put("org.zstack.header.keyprovider.CertificateInfo", "org.zstack.sdk.CertificateInfo"); put("org.zstack.header.keyprovider.KeyProviderInventory", "org.zstack.sdk.KeyProviderInventory"); put("org.zstack.header.keyprovider.KmsIdentityInventory", "org.zstack.sdk.KmsIdentityInventory"); put("org.zstack.header.keyprovider.KmsInventory", "org.zstack.sdk.KmsInventory"); @@ -763,6 +764,7 @@ public class SourceClassMap { put("org.zstack.sdk.CephPrimaryStorageInventory", "org.zstack.storage.ceph.primary.CephPrimaryStorageInventory"); put("org.zstack.sdk.CephPrimaryStorageMonInventory", "org.zstack.storage.ceph.primary.CephPrimaryStorageMonInventory"); put("org.zstack.sdk.CephPrimaryStoragePoolInventory", "org.zstack.storage.ceph.primary.CephPrimaryStoragePoolInventory"); + put("org.zstack.sdk.CertificateInfo", "org.zstack.header.keyprovider.CertificateInfo"); put("org.zstack.sdk.CertificateInventory", "org.zstack.network.service.lb.CertificateInventory"); put("org.zstack.sdk.ChainInfo", "org.zstack.header.core.progress.ChainInfo"); put("org.zstack.sdk.ChronyServerInfo", "org.zstack.zops.ChronyServerInfo"); diff --git a/sdk/src/main/java/org/zstack/sdk/CertificateInfo.java b/sdk/src/main/java/org/zstack/sdk/CertificateInfo.java new file mode 100644 index 00000000000..5dc36854ed1 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/CertificateInfo.java @@ -0,0 +1,55 @@ +package org.zstack.sdk; + + + +public class CertificateInfo { + + public java.lang.String subject; + public void setSubject(java.lang.String subject) { + this.subject = subject; + } + public java.lang.String getSubject() { + return this.subject; + } + + public java.lang.String issuer; + public void setIssuer(java.lang.String issuer) { + this.issuer = issuer; + } + public java.lang.String getIssuer() { + return this.issuer; + } + + public java.lang.String commonName; + public void setCommonName(java.lang.String commonName) { + this.commonName = commonName; + } + public java.lang.String getCommonName() { + return this.commonName; + } + + public java.util.List subjectAltNamesDns; + public void setSubjectAltNamesDns(java.util.List subjectAltNamesDns) { + this.subjectAltNamesDns = subjectAltNamesDns; + } + public java.util.List getSubjectAltNamesDns() { + return this.subjectAltNamesDns; + } + + public java.util.List subjectAltNamesIp; + public void setSubjectAltNamesIp(java.util.List subjectAltNamesIp) { + this.subjectAltNamesIp = subjectAltNamesIp; + } + public java.util.List getSubjectAltNamesIp() { + return this.subjectAltNamesIp; + } + + public java.sql.Timestamp expiredDate; + public void setExpiredDate(java.sql.Timestamp expiredDate) { + this.expiredDate = expiredDate; + } + public java.sql.Timestamp getExpiredDate() { + return this.expiredDate; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/KmsInventory.java b/sdk/src/main/java/org/zstack/sdk/KmsInventory.java index fb544369699..65ee119c300 100644 --- a/sdk/src/main/java/org/zstack/sdk/KmsInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/KmsInventory.java @@ -1,5 +1,6 @@ package org.zstack.sdk; +import org.zstack.sdk.CertificateInfo; import org.zstack.sdk.KmsIdentityInventory; public class KmsInventory extends org.zstack.sdk.KeyProviderInventory { @@ -52,12 +53,20 @@ public java.lang.String getActiveIdentityUuid() { return this.activeIdentityUuid; } - public java.sql.Timestamp serverCertExpiredDate; - public void setServerCertExpiredDate(java.sql.Timestamp serverCertExpiredDate) { - this.serverCertExpiredDate = serverCertExpiredDate; + public java.lang.String serverCertPem; + public void setServerCertPem(java.lang.String serverCertPem) { + this.serverCertPem = serverCertPem; } - public java.sql.Timestamp getServerCertExpiredDate() { - return this.serverCertExpiredDate; + public java.lang.String getServerCertPem() { + return this.serverCertPem; + } + + public CertificateInfo serverCertInfo; + public void setServerCertInfo(CertificateInfo serverCertInfo) { + this.serverCertInfo = serverCertInfo; + } + public CertificateInfo getServerCertInfo() { + return this.serverCertInfo; } public KmsIdentityInventory activeIdentity; diff --git a/sdk/src/main/java/org/zstack/sdk/NkpRestoreInfo.java b/sdk/src/main/java/org/zstack/sdk/NkpRestoreInfo.java index ae041ab435b..a9f108dab40 100644 --- a/sdk/src/main/java/org/zstack/sdk/NkpRestoreInfo.java +++ b/sdk/src/main/java/org/zstack/sdk/NkpRestoreInfo.java @@ -44,14 +44,6 @@ public java.lang.String getSaltPolicy() { return this.saltPolicy; } - public java.lang.String encryptedMasterSeed; - public void setEncryptedMasterSeed(java.lang.String encryptedMasterSeed) { - this.encryptedMasterSeed = encryptedMasterSeed; - } - public java.lang.String getEncryptedMasterSeed() { - return this.encryptedMasterSeed; - } - public java.lang.Integer currentVersion; public void setCurrentVersion(java.lang.Integer currentVersion) { this.currentVersion = currentVersion; @@ -60,4 +52,12 @@ public java.lang.Integer getCurrentVersion() { return this.currentVersion; } + public java.lang.Long backupTime; + public void setBackupTime(java.lang.Long backupTime) { + this.backupTime = backupTime; + } + public java.lang.Long getBackupTime() { + return this.backupTime; + } + } diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/CreateKmsAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/CreateKmsAction.java index 827c6d5fa38..ba43ed60a34 100644 --- a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/CreateKmsAction.java +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/CreateKmsAction.java @@ -46,9 +46,6 @@ public Result throwExceptionIfError() { @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String description; - @Param(required = false) - public java.lang.String type; - @Param(required = false) public java.lang.String resourceUuid; diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/GetKmsServerCertFromKmsAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/GetKmsServerCertFromKmsAction.java new file mode 100644 index 00000000000..405d905ffc5 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/GetKmsServerCertFromKmsAction.java @@ -0,0 +1,101 @@ +package org.zstack.sdk.keyprovider.kms.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class GetKmsServerCertFromKmsAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.keyprovider.kms.api.GetKmsServerCertFromKmsResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String uuid; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.keyprovider.kms.api.GetKmsServerCertFromKmsResult value = res.getResult(org.zstack.sdk.keyprovider.kms.api.GetKmsServerCertFromKmsResult.class); + ret.value = value == null ? new org.zstack.sdk.keyprovider.kms.api.GetKmsServerCertFromKmsResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "PUT"; + info.path = "/key-providers/kms/{uuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "getKmsServerCertFromKms"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/GetKmsServerCertFromKmsResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/GetKmsServerCertFromKmsResult.java new file mode 100644 index 00000000000..08a835fb0a5 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/GetKmsServerCertFromKmsResult.java @@ -0,0 +1,30 @@ +package org.zstack.sdk.keyprovider.kms.api; + +import org.zstack.sdk.CertificateInfo; + +public class GetKmsServerCertFromKmsResult { + public java.lang.String serverCertPem; + public void setServerCertPem(java.lang.String serverCertPem) { + this.serverCertPem = serverCertPem; + } + public java.lang.String getServerCertPem() { + return this.serverCertPem; + } + + public boolean selfSigned; + public void setSelfSigned(boolean selfSigned) { + this.selfSigned = selfSigned; + } + public boolean getSelfSigned() { + return this.selfSigned; + } + + public CertificateInfo serverCertInfo; + public void setServerCertInfo(CertificateInfo serverCertInfo) { + this.serverCertInfo = serverCertInfo; + } + public CertificateInfo getServerCertInfo() { + return this.serverCertInfo; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientCsrAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientCsrAction.java new file mode 100644 index 00000000000..84f2ad9b579 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientCsrAction.java @@ -0,0 +1,107 @@ +package org.zstack.sdk.keyprovider.kms.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class UploadKmsClientCsrAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.keyprovider.kms.api.UploadKmsClientCsrResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String uuid; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String csrPem; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String csrKeyPem; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.keyprovider.kms.api.UploadKmsClientCsrResult value = res.getResult(org.zstack.sdk.keyprovider.kms.api.UploadKmsClientCsrResult.class); + ret.value = value == null ? new org.zstack.sdk.keyprovider.kms.api.UploadKmsClientCsrResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "PUT"; + info.path = "/key-providers/kms/{uuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "uploadKmsClientCsr"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientCsrResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientCsrResult.java new file mode 100644 index 00000000000..562abefd6f9 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientCsrResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.keyprovider.kms.api; + +import org.zstack.sdk.KmsIdentityInventory; + +public class UploadKmsClientCsrResult { + public KmsIdentityInventory inventory; + public void setInventory(KmsIdentityInventory inventory) { + this.inventory = inventory; + } + public KmsIdentityInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientIdentityAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientIdentityAction.java new file mode 100644 index 00000000000..adc46da986f --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientIdentityAction.java @@ -0,0 +1,110 @@ +package org.zstack.sdk.keyprovider.kms.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class UploadKmsClientIdentityAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.keyprovider.kms.api.UploadKmsClientIdentityResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String uuid; + + @Param(required = true, validValues = {"PLATFORM","UPLOADED","CSR"}, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String identityType; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String kmsClientCertPem; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String kmsClientKeyPem; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.keyprovider.kms.api.UploadKmsClientIdentityResult value = res.getResult(org.zstack.sdk.keyprovider.kms.api.UploadKmsClientIdentityResult.class); + ret.value = value == null ? new org.zstack.sdk.keyprovider.kms.api.UploadKmsClientIdentityResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "PUT"; + info.path = "/key-providers/kms/{uuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "uploadKmsClientIdentity"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientIdentityResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientIdentityResult.java new file mode 100644 index 00000000000..e71e7a51c1e --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientIdentityResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.keyprovider.kms.api; + +import org.zstack.sdk.KmsIdentityInventory; + +public class UploadKmsClientIdentityResult { + public KmsIdentityInventory inventory; + public void setInventory(KmsIdentityInventory inventory) { + this.inventory = inventory; + } + public KmsIdentityInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientSignedCertAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientSignedCertAction.java new file mode 100644 index 00000000000..eb2a7e94085 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientSignedCertAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk.keyprovider.kms.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class UploadKmsClientSignedCertAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.keyprovider.kms.api.UploadKmsClientSignedCertResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String uuid; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String signedClientCertPem; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.keyprovider.kms.api.UploadKmsClientSignedCertResult value = res.getResult(org.zstack.sdk.keyprovider.kms.api.UploadKmsClientSignedCertResult.class); + ret.value = value == null ? new org.zstack.sdk.keyprovider.kms.api.UploadKmsClientSignedCertResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "PUT"; + info.path = "/key-providers/kms/{uuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "uploadKmsClientSignedCert"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientSignedCertResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientSignedCertResult.java new file mode 100644 index 00000000000..92ada658c38 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsClientSignedCertResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.keyprovider.kms.api; + +import org.zstack.sdk.KmsIdentityInventory; + +public class UploadKmsClientSignedCertResult { + public KmsIdentityInventory inventory; + public void setInventory(KmsIdentityInventory inventory) { + this.inventory = inventory; + } + public KmsIdentityInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsServerCertAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsServerCertAction.java new file mode 100644 index 00000000000..023bd8d469e --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsServerCertAction.java @@ -0,0 +1,104 @@ +package org.zstack.sdk.keyprovider.kms.api; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class UploadKmsServerCertAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.keyprovider.kms.api.UploadKmsServerCertResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String uuid; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String serverCertPem; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.keyprovider.kms.api.UploadKmsServerCertResult value = res.getResult(org.zstack.sdk.keyprovider.kms.api.UploadKmsServerCertResult.class); + ret.value = value == null ? new org.zstack.sdk.keyprovider.kms.api.UploadKmsServerCertResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "PUT"; + info.path = "/key-providers/kms/{uuid}/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "uploadKmsServerCert"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsServerCertResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsServerCertResult.java new file mode 100644 index 00000000000..7a0101c99c4 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/kms/api/UploadKmsServerCertResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk.keyprovider.kms.api; + +import org.zstack.sdk.KmsInventory; + +public class UploadKmsServerCertResult { + public KmsInventory inventory; + public void setInventory(KmsInventory inventory) { + this.inventory = inventory; + } + public KmsInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/CreateNkpAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/CreateNkpAction.java index 45ca9d824c9..15a3642d5c0 100644 --- a/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/CreateNkpAction.java +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/CreateNkpAction.java @@ -37,9 +37,6 @@ public Result throwExceptionIfError() { @Param(required = false, maxLength = 2048, nonempty = false, nullElements = false, emptyString = true, noTrim = false) public java.lang.String description; - @Param(required = false) - public java.lang.String type; - @Param(required = false) public java.lang.String resourceUuid; diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/ParseNkpRestoreResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/ParseNkpRestoreResult.java index 2fee861ac11..5b5ef838c90 100644 --- a/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/ParseNkpRestoreResult.java +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/nkp/api/ParseNkpRestoreResult.java @@ -11,4 +11,20 @@ public NkpRestoreInfo getRestoreInfo() { return this.restoreInfo; } + public java.lang.String code; + public void setCode(java.lang.String code) { + this.code = code; + } + public java.lang.String getCode() { + return this.code; + } + + public java.lang.String reason; + public void setReason(java.lang.String reason) { + this.reason = reason; + } + public java.lang.String getReason() { + return this.reason; + } + } diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index cfd879afb45..24fd79034bd 100644 --- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy @@ -36608,6 +36608,33 @@ abstract class ApiHelper { } + 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 @@ -36664,6 +36691,114 @@ abstract class ApiHelper { } + 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 From 633773dc8ad6d2db707bcedda5108f8f86345cab Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 3 Mar 2026 14:34:39 +0800 Subject: [PATCH 16/64] [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 | 7 + .../compute/vm/devices/VmTpmExtensions.java | 41 +- .../compute/vm/devices/VmTpmManager.java | 23 - conf/db/zsv/V5.0.0__schema.sql | 26 ++ conf/persistence.xml | 2 + conf/springConfigXml/Kvm.xml | 7 + .../vm/additions/VmHostFileContentFormat.java | 6 + .../vm/additions/VmHostFileContentVO.java | 93 ++++ .../vm/additions/VmHostFileContentVO_.java | 14 + .../header/vm/additions/VmHostFileType.java | 8 + .../header/vm/additions/VmHostFileVO.java | 107 +++++ .../header/vm/additions/VmHostFileVO_.java | 17 + .../java/org/zstack/kvm/KVMAgentCommands.java | 105 +++++ .../main/java/org/zstack/kvm/KVMConstant.java | 11 + .../src/main/java/org/zstack/kvm/KVMHost.java | 10 + .../kvm/efi/KvmSecureBootExtensions.java | 406 +++++++++++++++++- .../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 + 22 files changed, 1083 insertions(+), 39 deletions(-) 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/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 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 index c0f1c80a38d..be845c85c2b 100644 --- a/compute/src/main/java/org/zstack/compute/legacy/ComputeLegacyGlobalProperty.java +++ b/compute/src/main/java/org/zstack/compute/legacy/ComputeLegacyGlobalProperty.java @@ -10,4 +10,11 @@ public class ComputeLegacyGlobalProperty { */ @GlobalProperty(name="legacyCpuTopologyFix", defaultValue = "false") public static boolean cpuTopologyFix; + /** + * if enableNvRamTypeVolume = true, NvRam type volume will be created if UEFI is enabled; + * if enableNvRamTypeVolume = false, NvRam type volume will not be created, NvRam & TpmState will save in host + * (not in Primary storage); + */ + @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/db/zsv/V5.0.0__schema.sql b/conf/db/zsv/V5.0.0__schema.sql index 590c58ee528..73125957d48 100644 --- a/conf/db/zsv/V5.0.0__schema.sql +++ b/conf/db/zsv/V5.0.0__schema.sql @@ -21,6 +21,32 @@ CREATE TABLE IF NOT EXISTS `zstack`.`TpmHostRefVO` ( CONSTRAINT `fkTpmHostRefVOHostVO` FOREIGN KEY (`hostUuid`) REFERENCES `HostEO` (`uuid`) ON UPDATE RESTRICT ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE IF NOT EXISTS `zstack`.`VmHostFileVO` ( + `uuid` char(32) NOT NULL UNIQUE, + `vmInstanceUuid` char(32) NOT NULL, + `hostUuid` char(32) NOT NULL, + `type` varchar(64) NOT NULL COMMENT 'NvRam, TpmState, NvRamBackup, TpmStateBackup', + `path` varchar(1024) NOT NULL COMMENT 'Absolute path of the file on the host', + `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59', + PRIMARY KEY (`uuid`), + INDEX `idxVmHostFileVOVmInstanceUuid` (`vmInstanceUuid`), + INDEX `idxVmHostFileVOHostUuid` (`hostUuid`), + CONSTRAINT `fkVmHostFileVOVmInstanceVO` FOREIGN KEY (`vmInstanceUuid`) REFERENCES `VmInstanceEO` (`uuid`) ON DELETE CASCADE, + CONSTRAINT `fkVmHostFileVOHostVO` FOREIGN KEY (`hostUuid`) REFERENCES `HostEO` (`uuid`) ON DELETE CASCADE, + UNIQUE KEY `ukVmHostFileVO` (`vmInstanceUuid`, `hostUuid`, `type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`VmHostFileContentVO` ( + `uuid` char(32) NOT NULL UNIQUE, + `content` MEDIUMBLOB DEFAULT '', + `format` varchar(64) NOT NULL COMMENT 'Raw, TarballGzip', + `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59', + PRIMARY KEY (`uuid`), + CONSTRAINT `fkVmHostFileContentVOVmHostFileVO` FOREIGN KEY (`uuid`) REFERENCES `VmHostFileVO` (`uuid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + -- Feature: KMS | ZSPHER-46, ZSPHER-60, ZSPHER-61, ZSPHER-62 CREATE TABLE IF NOT EXISTS `zstack`.`KeyProviderVO` ( diff --git a/conf/persistence.xml b/conf/persistence.xml index 5a1b855d9e8..eb4d624e6af 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 16cd80fadf9..88886397a1f 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -255,6 +255,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/VmHostFileContentVO_.java b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileContentVO_.java new file mode 100644 index 00000000000..c375650337b --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileContentVO_.java @@ -0,0 +1,14 @@ +package org.zstack.header.vm.additions; + +import javax.persistence.metamodel.SingularAttribute; +import javax.persistence.metamodel.StaticMetamodel; +import java.sql.Timestamp; + +@StaticMetamodel(VmHostFileContentVO.class) +public class VmHostFileContentVO_ { + public static volatile SingularAttribute uuid; + public static volatile SingularAttribute content; + public static volatile SingularAttribute format; + public static volatile SingularAttribute createDate; + public static volatile SingularAttribute 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/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..39fdb742797 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileVO_.java @@ -0,0 +1,17 @@ +package org.zstack.header.vm.additions; + +import org.zstack.header.vo.ResourceVO_; + +import javax.persistence.metamodel.SingularAttribute; +import javax.persistence.metamodel.StaticMetamodel; +import java.sql.Timestamp; + +@StaticMetamodel(VmHostFileVO.class) +public class VmHostFileVO_ extends ResourceVO_ { + public static volatile SingularAttribute vmInstanceUuid; + public static volatile SingularAttribute hostUuid; + public static volatile SingularAttribute type; + public static volatile SingularAttribute path; + public static volatile SingularAttribute createDate; + public static volatile SingularAttribute 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 16865309ec0..63a2f9759e7 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -11,6 +11,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; @@ -25,6 +26,7 @@ import static org.zstack.utils.CollectionDSL.e; import static org.zstack.utils.CollectionDSL.map; +import static org.zstack.utils.CollectionUtils.transform; import static org.zstack.utils.opaque.OpaqueConstants.OPAQUE_KEY_RESPONSE_ERROR; public class KVMAgentCommands { @@ -2727,6 +2729,63 @@ public void setVmCpuVendorId(String vmCpuVendorId) { 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 { private List nicInfos; private List virtualDeviceInfoList; @@ -2800,6 +2859,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 cb79da59838..cae9533b7f2 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -85,6 +85,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"; String ISO_TO = "kvm.isoto"; String ANSIBLE_PLAYBOOK_NAME = "kvm.py"; @@ -184,6 +186,15 @@ public interface KVMConstant { public static final String L2_PROVIDER_TYPE_MACVLAN = "MacVlan"; 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"; 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 9d22cf50976..adcfc545808 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -230,6 +230,8 @@ public class KVMHost extends HostBase implements Host { private String fileDownloadPath; private String fileUploadPath; private String fileDownloadProgressPath; + private String readVmHostFilePath; + private String writeVmHostFilePath; public KVMHost(KVMHostVO self, KVMHostContext context) { super(self); @@ -480,6 +482,14 @@ public KVMHost(KVMHostVO self, KVMHostContext context) { ub = UriComponentsBuilder.fromHttpUrl(baseUrl); ub.path(KVMConstant.KVM_HOST_FILE_DOWNLOAD_PROGRESS_PATH); fileDownloadProgressPath = 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 8f2e33af8ff..f9d8f923a76 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").withException(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").withException(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").withException(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").withException(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(); 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 4e57880a7ce..53cf63d951b 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -254,6 +254,13 @@ + + + + + + + diff --git a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy index f8794f26acd..f7e20a79f80 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 @@ -681,5 +682,24 @@ class KVMSimulator implements Simulator { spec.simulator(KVMConstant.KVM_UPDATE_HOSTNAME_PATH) { return new UpdateHostnameRsp() } + + 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 8ddb29b647e..12a7ed72c61 100755 --- a/utils/src/main/java/org/zstack/utils/CollectionUtils.java +++ b/utils/src/main/java/org/zstack/utils/CollectionUtils.java @@ -4,6 +4,7 @@ import org.zstack.utils.function.Function; import org.zstack.utils.logging.CLogger; +import javax.annotation.Nullable; import java.lang.reflect.Method; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -56,6 +57,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 10041b05370adb7f5c3a7da6c8940c48c5b9c6e8 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 5 Mar 2026 17:49:21 +0800 Subject: [PATCH 17/64] [conf]: update VmHostFileContentVO.content default value Related: ZSV-11310 Change-Id: I646d6f65756f67686c7a766b7361796768677163 --- conf/db/zsv/V5.0.0__schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/db/zsv/V5.0.0__schema.sql b/conf/db/zsv/V5.0.0__schema.sql index 73125957d48..a01bae3a95f 100644 --- a/conf/db/zsv/V5.0.0__schema.sql +++ b/conf/db/zsv/V5.0.0__schema.sql @@ -39,7 +39,7 @@ CREATE TABLE IF NOT EXISTS `zstack`.`VmHostFileVO` ( CREATE TABLE IF NOT EXISTS `zstack`.`VmHostFileContentVO` ( `uuid` char(32) NOT NULL UNIQUE, - `content` MEDIUMBLOB DEFAULT '', + `content` MEDIUMBLOB DEFAULT NULL, `format` varchar(64) NOT NULL COMMENT 'Raw, TarballGzip', `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59', From 7863c74db0e7593d31658818f998ac6deaa84866 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 6 Mar 2026 10:15:37 +0800 Subject: [PATCH 18/64] [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/db/zsv/V5.0.0__schema.sql | 12 -- conf/persistence.xml | 1 - .../header/tpm/entity/TpmCapabilityView.java | 19 +-- .../entity/TpmCapabilityViewDoc_zh_cn.groovy | 10 +- .../tpm/entity/TpmHostRefInventory.java | 106 ----------------- .../header/tpm/entity/TpmHostRefVO.java | 106 ----------------- .../header/tpm/entity/TpmHostRefVO_.java | 15 --- .../header/tpm/entity/TpmInventory.java | 13 --- .../tpm/entity/TpmInventoryDoc_zh_cn.groovy | 5 +- .../org/zstack/header/tpm/entity/TpmVO.java | 19 --- .../header/vm/additions/PackageInfo.java | 7 ++ .../vm/additions/VmHostFileInventory.java | 108 ++++++++++++++++++ .../VmHostFileInventoryDoc_zh_cn.groovy} | 24 ++-- .../org/zstack/kvm/tpm/KvmTpmManager.java | 14 ++- sdk/src/main/java/SourceClassMap.java | 4 +- .../sdk/tpm/entity/TpmCapabilityView.java | 10 +- .../zstack/sdk/tpm/entity/TpmInventory.java | 8 -- .../entity/VmHostFileInventory.java} | 32 ++++-- 18 files changed, 188 insertions(+), 325 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 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 rename header/src/main/java/org/zstack/header/{tpm/entity/TpmHostRefInventoryDoc_zh_cn.groovy => vm/additions/VmHostFileInventoryDoc_zh_cn.groovy} (51%) rename sdk/src/main/java/org/zstack/sdk/{tpm/entity/TpmHostRefInventory.java => vm/entity/VmHostFileInventory.java} (57%) diff --git a/conf/db/zsv/V5.0.0__schema.sql b/conf/db/zsv/V5.0.0__schema.sql index a01bae3a95f..f03734152a5 100644 --- a/conf/db/zsv/V5.0.0__schema.sql +++ b/conf/db/zsv/V5.0.0__schema.sql @@ -9,18 +9,6 @@ CREATE TABLE IF NOT EXISTS `zstack`.`TpmVO` ( CONSTRAINT `fkTpmVOVmInstanceVO` FOREIGN KEY (`vmInstanceUuid`) REFERENCES `VmInstanceEO` (`uuid`) ON UPDATE RESTRICT ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -CREATE TABLE IF NOT EXISTS `zstack`.`TpmHostRefVO` ( - `id` bigint unsigned NOT NULL UNIQUE AUTO_INCREMENT, - `tpmUuid` char(32) NOT NULL, - `hostUuid` char(32) NOT NULL, - `path` varchar(255) NOT NULL, - `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59', - PRIMARY KEY (`id`), - CONSTRAINT `fkTpmHostRefVOTpmVO` FOREIGN KEY (`tpmUuid`) REFERENCES `TpmVO` (`uuid`) ON UPDATE RESTRICT ON DELETE CASCADE, - CONSTRAINT `fkTpmHostRefVOHostVO` FOREIGN KEY (`hostUuid`) REFERENCES `HostEO` (`uuid`) ON UPDATE RESTRICT ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - CREATE TABLE IF NOT EXISTS `zstack`.`VmHostFileVO` ( `uuid` char(32) NOT NULL UNIQUE, `vmInstanceUuid` char(32) NOT NULL, diff --git a/conf/persistence.xml b/conf/persistence.xml index eb4d624e6af..0fa065a6a71 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/TpmCapabilityViewDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/tpm/entity/TpmCapabilityViewDoc_zh_cn.groovy index a286e9d74c7..4ee7ba90b8a 100644 --- a/header/src/main/java/org/zstack/header/tpm/entity/TpmCapabilityViewDoc_zh_cn.groovy +++ b/header/src/main/java/org/zstack/header/tpm/entity/TpmCapabilityViewDoc_zh_cn.groovy @@ -1,7 +1,7 @@ package org.zstack.header.tpm.entity import java.sql.Timestamp -import org.zstack.header.tpm.entity.TpmHostRefInventory +import org.zstack.header.vm.additions.VmHostFileInventory doc { @@ -38,12 +38,12 @@ doc { since "5.0.0" } ref { - name "hostRefs" - path "org.zstack.header.tpm.entity.TpmCapabilityView.hostRefs" - desc "TPM 与主机的相关数据列表" + name "fileRefs" + path "org.zstack.header.tpm.entity.TpmCapabilityView.fileRefs" + desc "TPM 相关的主机侧文件或目录数据列表" type "List" since "5.0.0" - clz TpmHostRefInventory.class + clz VmHostFileInventory.class } field { name "edkVersion" 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/TpmHostRefVO_.java b/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefVO_.java deleted file mode 100644 index ee4d9654711..00000000000 --- a/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefVO_.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.zstack.header.tpm.entity; - -import javax.persistence.metamodel.SingularAttribute; -import javax.persistence.metamodel.StaticMetamodel; -import java.sql.Timestamp; - -@StaticMetamodel(TpmHostRefVO.class) -public class TpmHostRefVO_ { - public static volatile SingularAttribute id; - public static volatile SingularAttribute tpmUuid; - public static volatile SingularAttribute hostUuid; - public static volatile SingularAttribute path; - public static volatile SingularAttribute createDate; - public static volatile SingularAttribute 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/TpmInventoryDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/tpm/entity/TpmInventoryDoc_zh_cn.groovy index 9908023d6c4..f67ac502f79 100644 --- a/header/src/main/java/org/zstack/header/tpm/entity/TpmInventoryDoc_zh_cn.groovy +++ b/header/src/main/java/org/zstack/header/tpm/entity/TpmInventoryDoc_zh_cn.groovy @@ -1,7 +1,6 @@ package org.zstack.header.tpm.entity -import java.sql.Timestamp -import org.zstack.header.tpm.entity.TpmHostRefInventory +import org.zstack.header.vm.additions.VmHostFileInventory doc { @@ -43,6 +42,6 @@ doc { desc "TPM 与主机的相关数据列表" type "List" since "5.0.0" - clz TpmHostRefInventory.class + clz VmHostFileInventory.class } } 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/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefInventoryDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileInventoryDoc_zh_cn.groovy similarity index 51% rename from header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefInventoryDoc_zh_cn.groovy rename to header/src/main/java/org/zstack/header/vm/additions/VmHostFileInventoryDoc_zh_cn.groovy index d8330cb7ff6..0ec8eb79265 100644 --- a/header/src/main/java/org/zstack/header/tpm/entity/TpmHostRefInventoryDoc_zh_cn.groovy +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileInventoryDoc_zh_cn.groovy @@ -1,20 +1,18 @@ -package org.zstack.header.tpm.entity - -import java.sql.Timestamp +package org.zstack.header.vm.additions doc { - title "TPM 与主机的相关数据" + title "虚拟机在主机侧的相关文件或目录数据" field { - name "id" - desc "自增主键" - type "long" + name "uuid" + desc "相关文件 UUID" + type "String" since "5.0.0" } field { - name "tpmUuid" - desc "TPM UUID" + name "vmInstanceUuid" + desc "虚拟机 UUID" type "String" since "5.0.0" } @@ -24,9 +22,15 @@ doc { type "String" since "5.0.0" } + field { + name "type" + desc "文件类型, 按用途分类, 可能是 NvRam 或者 TpmState" + type "String" + since "5.0.0" + } field { name "path" - desc "遗留 TPM 状态文件的位置" + desc "主机侧相关文件或目录的路径" type "String" since "5.0.0" } 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) { diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 160dc8aad5e..0970b63b62f 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -277,7 +277,6 @@ public class SourceClassMap { put("org.zstack.header.tag.TagPatternType", "org.zstack.sdk.TagPatternType"); put("org.zstack.header.tag.UserTagInventory", "org.zstack.sdk.UserTagInventory"); put("org.zstack.header.tpm.entity.TpmCapabilityView", "org.zstack.sdk.tpm.entity.TpmCapabilityView"); - put("org.zstack.header.tpm.entity.TpmHostRefInventory", "org.zstack.sdk.tpm.entity.TpmHostRefInventory"); put("org.zstack.header.tpm.entity.TpmInventory", "org.zstack.sdk.tpm.entity.TpmInventory"); put("org.zstack.header.vdpa.VmVdpaNicInventory", "org.zstack.sdk.VmVdpaNicInventory"); put("org.zstack.header.vipQos.VipQosInventory", "org.zstack.sdk.VipQosInventory"); @@ -295,6 +294,7 @@ public class SourceClassMap { put("org.zstack.header.vm.VmPriorityConfigInventory", "org.zstack.sdk.VmPriorityConfigInventory"); put("org.zstack.header.vm.VmPriorityLevel", "org.zstack.sdk.VmPriorityLevel"); put("org.zstack.header.vm.VmSchedHistoryInventory", "org.zstack.sdk.VmSchedHistoryInventory"); + put("org.zstack.header.vm.additions.VmHostFileInventory", "org.zstack.sdk.vm.entity.VmHostFileInventory"); put("org.zstack.header.vm.cdrom.VmCdRomInventory", "org.zstack.sdk.VmCdRomInventory"); put("org.zstack.header.vm.devices.DeviceAddress", "org.zstack.sdk.DeviceAddress"); put("org.zstack.header.vm.devices.VmInstanceResourceMetadataArchiveInventory", "org.zstack.sdk.VmInstanceResourceMetadataArchiveInventory"); @@ -1286,8 +1286,8 @@ public class SourceClassMap { put("org.zstack.sdk.softwarePackage.header.JobDetails", "org.zstack.softwarePackage.header.JobDetails"); put("org.zstack.sdk.softwarePackage.header.SoftwarePackageInventory", "org.zstack.softwarePackage.header.SoftwarePackageInventory"); put("org.zstack.sdk.tpm.entity.TpmCapabilityView", "org.zstack.header.tpm.entity.TpmCapabilityView"); - put("org.zstack.sdk.tpm.entity.TpmHostRefInventory", "org.zstack.header.tpm.entity.TpmHostRefInventory"); put("org.zstack.sdk.tpm.entity.TpmInventory", "org.zstack.header.tpm.entity.TpmInventory"); + put("org.zstack.sdk.vm.entity.VmHostFileInventory", "org.zstack.header.vm.additions.VmHostFileInventory"); put("org.zstack.sdk.zbox.ZBoxBackupInventory", "org.zstack.externalbackup.zbox.ZBoxBackupInventory"); put("org.zstack.sdk.zbox.ZBoxBackupStorageBackupInfo", "org.zstack.externalbackup.zbox.ZBoxBackupStorageBackupInfo"); put("org.zstack.sdk.zbox.ZBoxInventory", "org.zstack.zbox.ZBoxInventory"); diff --git a/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmCapabilityView.java b/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmCapabilityView.java index f77138cdfc7..38fa336e3d5 100644 --- a/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmCapabilityView.java +++ b/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmCapabilityView.java @@ -44,12 +44,12 @@ public java.sql.Timestamp getLastOpDate() { return this.lastOpDate; } - public java.util.List hostRefs; - public void setHostRefs(java.util.List hostRefs) { - this.hostRefs = hostRefs; + public java.util.List fileRefs; + public void setFileRefs(java.util.List fileRefs) { + this.fileRefs = fileRefs; } - public java.util.List getHostRefs() { - return this.hostRefs; + public java.util.List getFileRefs() { + return this.fileRefs; } public java.lang.String edkVersion; diff --git a/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmInventory.java b/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmInventory.java index e4fa21a0746..538b12182ca 100644 --- a/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmInventory.java @@ -44,12 +44,4 @@ public java.sql.Timestamp getLastOpDate() { return this.lastOpDate; } - public java.util.List hostRefs; - public void setHostRefs(java.util.List hostRefs) { - this.hostRefs = hostRefs; - } - public java.util.List getHostRefs() { - return this.hostRefs; - } - } diff --git a/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmHostRefInventory.java b/sdk/src/main/java/org/zstack/sdk/vm/entity/VmHostFileInventory.java similarity index 57% rename from sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmHostRefInventory.java rename to sdk/src/main/java/org/zstack/sdk/vm/entity/VmHostFileInventory.java index 85db7ad9eb8..7c72bb8edb3 100644 --- a/sdk/src/main/java/org/zstack/sdk/tpm/entity/TpmHostRefInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/vm/entity/VmHostFileInventory.java @@ -1,23 +1,23 @@ -package org.zstack.sdk.tpm.entity; +package org.zstack.sdk.vm.entity; -public class TpmHostRefInventory { +public class VmHostFileInventory { - public long id; - public void setId(long id) { - this.id = id; + public java.lang.String uuid; + public void setUuid(java.lang.String uuid) { + this.uuid = uuid; } - public long getId() { - return this.id; + public java.lang.String getUuid() { + return this.uuid; } - public java.lang.String tpmUuid; - public void setTpmUuid(java.lang.String tpmUuid) { - this.tpmUuid = tpmUuid; + public java.lang.String vmInstanceUuid; + public void setVmInstanceUuid(java.lang.String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; } - public java.lang.String getTpmUuid() { - return this.tpmUuid; + public java.lang.String getVmInstanceUuid() { + return this.vmInstanceUuid; } public java.lang.String hostUuid; @@ -28,6 +28,14 @@ public java.lang.String getHostUuid() { return this.hostUuid; } + public java.lang.String type; + public void setType(java.lang.String type) { + this.type = type; + } + public java.lang.String getType() { + return this.type; + } + public java.lang.String path; public void setPath(java.lang.String path) { this.path = path; From 0fa68be1d57e5b6879a9f9d355c84d2bf87a484b Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 10 Mar 2026 15:59:11 +0800 Subject: [PATCH 19/64] [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 cae9533b7f2..575d54016c2 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -193,7 +193,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 d8852da83162b66e6bf7ab4f417da9683ebd53de Mon Sep 17 00:00:00 2001 From: "tao.yang" Date: Fri, 6 Mar 2026 17:43:07 +0800 Subject: [PATCH 20/64] [sdk]: rekey API and root key synchronization Resolves: ZSV-11331 Change-Id: I66626a707a6465616d70616171656469796d6476 --- .../vm/devices/VmTpmRekeyAssociation.java | 35 +++++++++++++++++++ conf/springConfigXml/VmInstanceManager.xml | 20 +++++++---- ...roviderRekeyAssociationExtensionPoint.java | 11 ++++++ .../api/RekeyKeyProviderRefsAction.java | 11 +++++- 4 files changed, 69 insertions(+), 8 deletions(-) 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 0e82353d042..55af623b940 100755 --- a/conf/springConfigXml/VmInstanceManager.xml +++ b/conf/springConfigXml/VmInstanceManager.xml @@ -283,10 +283,16 @@ - - - - - - - + + + + + + + + + + + + + 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); +} diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsAction.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsAction.java index 2d005f5c3dc..ba08306ec15 100644 --- a/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsAction.java +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsAction.java @@ -25,12 +25,21 @@ public Result throwExceptionIfError() { } } - @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + @Param(required = false, nonempty = true, nullElements = false, emptyString = false, noTrim = false) public java.util.List refIds; + @Param(required = false, nonempty = true, nullElements = false, emptyString = false, noTrim = false) + public java.util.List resourceUuids; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = false, noTrim = false) + public java.lang.String resourceType; + @Param(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false) public java.lang.String providerUuid; + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public boolean rekeyAll = false; + @Param(required = false) public java.util.List systemTags; From a4dcb78e9527a9691799aebeac1c7ac269463037 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 11 Mar 2026 12:32:01 +0800 Subject: [PATCH 21/64] [kvm]: update condition write VM host file * Refactors the error handling mechanism for host file reads within the Secure Boot extension. * Introduces an error aggregation strategy that accumulates failed file reads into a list instead of immediately terminating the process; overall success or failure is determined only after all paths have been processed. * A new `firstReadSuccess` flag is added to track the initial read status, and the skip condition logic for multiple stream steps is adjusted. Resolves: ZSV-11444 Related: ZSV-11436 Related: ZSV-11310 Change-Id: I6f636d7364787872716a6b7a6a7a76736a697762 --- .../kvm/efi/KvmSecureBootExtensions.java | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) 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 f9d8f923a76..83b8e8720c8 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 @@ -209,14 +209,16 @@ public void success(KvmResponseWrapper wrapper) { existsContentUuid = Collections.emptyList(); } + List errors = new ArrayList<>(); 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())); + errors.add(operr("failed to read file %s", path) + .withOpaque("path", path) + .withException(to.getError())); continue; } @@ -264,7 +266,11 @@ public void success(KvmResponseWrapper wrapper) { } } - completion.success(); + if (errors.isEmpty()) { + completion.success(); + } else { + completion.fail(operr("failed to read file content from host[uuid=%s]", context.hostUuid).withCause(errors)); + } } @Override @@ -406,6 +412,8 @@ public static class PrepareHostFileContext { // whether the NvRam is on the same host as before private boolean sameHost = false; + private boolean firstReadSuccess = false; + private boolean writeSuccess = false; private VmHostFileVO vmHostFile; } @@ -424,14 +432,6 @@ public void run(FlowTrigger trigger, Map data) { .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)); @@ -439,6 +439,7 @@ public void run(FlowTrigger trigger, Map data) { return; } + context.sameHost = vmHostFile.getHostUuid().equals(context.hostUuid); SyncVmHostFilesFromHostContext syncContext = new SyncVmHostFilesFromHostContext(); syncContext.hostUuid = vmHostFile.getHostUuid(); syncContext.vmUuid = context.vmUuid; @@ -454,12 +455,15 @@ public void run(FlowTrigger trigger, Map data) { syncVmHostFilesFromHost(syncContext, new Completion(trigger) { @Override public void success() { + context.firstReadSuccess = true; trigger.next(); } @Override public void fail(ErrorCode errorCode) { - trigger.fail(errorCode); + logger.warn(String.format("failed to read vm host file for VM[vmUuid=%s] but still continue: %s", + context.vmUuid, errorCode.getReadableDetails())); + trigger.next(); } }); } @@ -468,7 +472,7 @@ public void fail(ErrorCode errorCode) { @Override public boolean skip(Map data) { - return context.sameHost || context.vmHostFile == null; + return context.vmHostFile == null || (context.sameHost && context.firstReadSuccess); } @Override @@ -498,6 +502,7 @@ public void run(FlowTrigger trigger, Map data) { rewriteVmHostFiles(rewriteContext, new Completion(trigger) { @Override public void success() { + context.writeSuccess = true; trigger.next(); } @@ -512,8 +517,7 @@ public void fail(ErrorCode errorCode) { @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; + return !context.writeSuccess; } @Override From b224c4c7120984350e9c2f3783ae237160b6d5cc Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 14:10:15 +0800 Subject: [PATCH 22/64] Envelope wrapper dek to compute --- conf/db/zsv/V5.0.0__schema.sql | 1 + ...retCreateExt \342\200\216ensionPoint.java" | 19 ++ .../secret/SecretGetExtensionPoint.java | 19 ++ .../header/secret/SecretHostDefineMsg.java | 30 ++ .../header/secret/SecretHostDefineReply.java | 28 ++ plugin/kvm/pom.xml | 7 +- .../zstack/kvm/HostSecretEnvelopeCrypto.java | 132 +++++++++ .../java/org/zstack/kvm/KVMAgentCommands.java | 84 ++++++ .../main/java/org/zstack/kvm/KVMConstant.java | 6 + .../src/main/java/org/zstack/kvm/KVMHost.java | 266 ++++++++++++++++++ .../kvm/host/HostSecretCase.groovy | 149 ++++++++++ 11 files changed, 740 insertions(+), 1 deletion(-) 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/conf/db/zsv/V5.0.0__schema.sql b/conf/db/zsv/V5.0.0__schema.sql index f03734152a5..ac3b849ec1e 100644 --- a/conf/db/zsv/V5.0.0__schema.sql +++ b/conf/db/zsv/V5.0.0__schema.sql @@ -93,6 +93,7 @@ CREATE TABLE IF NOT EXISTS `zstack`.`HostKeyIdentityVO` ( `hostUuid` varchar(32) NOT NULL UNIQUE, `publicKey` text NOT NULL, `fingerprint` varchar(128) NOT NULL, + `verified` boolean NOT NULL DEFAULT FALSE, `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59', PRIMARY KEY (`hostUuid`), 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 4cabba3d897..c0f484d0bc7 100755 --- a/plugin/kvm/pom.xml +++ b/plugin/kvm/pom.xml @@ -4,11 +4,16 @@ plugin org.zstack - 4.10.0 + 4.10.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 63a2f9759e7..1efc52b0151 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -372,6 +372,90 @@ public void setQemuVersion(String qemuVersion) { } } + 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 Map configs; 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 575d54016c2..7355ed99f3b 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -122,6 +122,12 @@ public interface KVMConstant { String KVM_UPDATE_HOST_NQN_PATH = "/host/nqn/update"; String KVM_UPDATE_HOSTNAME_PATH = "/host/hostname/update"; + 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"; 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 adcfc545808..0a5b85056fc 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -55,6 +55,8 @@ import org.zstack.header.exception.CloudRuntimeException; import org.zstack.header.host.*; import org.zstack.header.host.MigrateVmOnHypervisorMsg.StorageMigrationPolicy; +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; @@ -748,6 +750,8 @@ protected void handleLocalMessage(Message msg) { handle((GetFileDownloadProgressMsg) msg); } else if (msg instanceof RestartKvmAgentMsg) { handle((RestartKvmAgentMsg) msg); + } else if (msg instanceof SecretHostDefineMsg) { + handle((SecretHostDefineMsg) msg); } else { super.handleLocalMessage(msg); } @@ -3861,6 +3865,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()); @@ -5283,6 +5304,21 @@ public void fail(ErrorCode errorCode) { } }); + flow(new NoRollbackFlow() { + String __name__ = "sync-secret-key-after-ping"; + + @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) { @@ -5300,6 +5336,236 @@ 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; + } + HostKeyIdentityVO vo = getHostKeyIdentity(hostUuid); + if (vo == null) { + vo = new HostKeyIdentityVO(); + vo.setHostUuid(hostUuid); + vo.setFingerprint(""); + vo.setCreateDate(new java.sql.Timestamp(System.currentTimeMillis())); + dbf.persist(vo); + } + vo.setPublicKey(publicKey.trim()); + 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 a11e7bfc14cd72b2de77b4158d38dc8678f0e88b Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 14:18:24 +0800 Subject: [PATCH 23/64] Add verified to HostKeyIdentityVO --- .../org/zstack/header/host/HostKeyIdentityVO.java | 11 +++++++++++ .../org/zstack/header/host/HostKeyIdentityVO_.java | 1 + 2 files changed, 12 insertions(+) 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/host/HostKeyIdentityVO_.java b/header/src/main/java/org/zstack/header/host/HostKeyIdentityVO_.java index a43d01375a0..a2bcfa7b850 100644 --- a/header/src/main/java/org/zstack/header/host/HostKeyIdentityVO_.java +++ b/header/src/main/java/org/zstack/header/host/HostKeyIdentityVO_.java @@ -9,6 +9,7 @@ public class HostKeyIdentityVO_ { public static volatile SingularAttribute hostUuid; public static volatile SingularAttribute publicKey; public static volatile SingularAttribute fingerprint; + public static volatile SingularAttribute verified; public static volatile SingularAttribute createDate; public static volatile SingularAttribute lastOpDate; } From 3dc5eb4cc625466d002eb5a24d738838ec214f43 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 14:21:13 +0800 Subject: [PATCH 24/64] Rename sync-envelope-public-key --- plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0a5b85056fc..3b31d82c4b9 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -5305,7 +5305,7 @@ public void fail(ErrorCode errorCode) { }); flow(new NoRollbackFlow() { - String __name__ = "sync-secret-key-after-ping"; + String __name__ = "sync-envelope-public-key"; @Override public boolean skip(Map data) { From c0ce171e7ef779e817d7b5450930607e53cc7915 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 14:43:34 +0800 Subject: [PATCH 25/64] Place holder public key --- plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 3b31d82c4b9..87b29a8b0a2 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -5433,15 +5433,19 @@ private void saveOrUpdateHostKeyIdentity(String hostUuid, String publicKey, bool 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(publicKey.trim()); + vo.setPublicKey(keyToSave); vo.setVerified(verified); dbf.update(vo); } From 8758247cb992c2997a2be941a052702170502456 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 14:44:41 +0800 Subject: [PATCH 26/64] Place holder public key --- plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 87b29a8b0a2..28a4bb07227 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -5438,9 +5438,9 @@ private void saveOrUpdateHostKeyIdentity(String hostUuid, String publicKey, bool if (vo == null) { vo = new HostKeyIdentityVO(); vo.setHostUuid(hostUuid); - vo.setPublicKey(keyToSave); + vo.setPublicKey(""); vo.setFingerprint(""); - vo.setVerified(verified); + vo.setVerified(false); vo.setCreateDate(new java.sql.Timestamp(System.currentTimeMillis())); dbf.persist(vo); return; From 25c78c24ae3a9e58244009025b8df27764078f97 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 14:45:39 +0800 Subject: [PATCH 27/64] Place holder public key --- plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 28a4bb07227..87b29a8b0a2 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -5438,9 +5438,9 @@ private void saveOrUpdateHostKeyIdentity(String hostUuid, String publicKey, bool if (vo == null) { vo = new HostKeyIdentityVO(); vo.setHostUuid(hostUuid); - vo.setPublicKey(""); + vo.setPublicKey(keyToSave); vo.setFingerprint(""); - vo.setVerified(false); + vo.setVerified(verified); vo.setCreateDate(new java.sql.Timestamp(System.currentTimeMillis())); dbf.persist(vo); return; From 2522d29bf945ce548f75c0bf4ca13eaba3fff508 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 15:30:24 +0800 Subject: [PATCH 28/64] Remove rotate public key in initial pingHook --- .../secret/SecretCreateExtensionPoint.java | 0 .../src/main/java/org/zstack/kvm/KVMHost.java | 24 ------------------- 2 files changed, 24 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/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index 87b29a8b0a2..607f65c5737 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -5376,30 +5376,6 @@ private void syncEnvelopeKeyAfterPing() { 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 { From 70728c38752556afaf763c04cb3c055a426b30f5 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 16:00:20 +0800 Subject: [PATCH 29/64] Add variable to SecretHostDefineCmd --- .../header/secret/SecretHostDefineMsg.java | 37 +++++++++++++++ .../java/org/zstack/kvm/KVMAgentCommands.java | 46 +++++++++++++++++-- .../src/main/java/org/zstack/kvm/KVMHost.java | 16 ++++++- 3 files changed, 93 insertions(+), 6 deletions(-) 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/KVMAgentCommands.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java index 1efc52b0151..eb121f58c32 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -425,13 +425,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 607f65c5737..1e841b7d2d1 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -3870,6 +3870,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()) { @@ -5433,6 +5436,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; @@ -5486,7 +5494,11 @@ private void handle(SecretHostDefineMsg msg) { try { String url = buildUrl(KVMConstant.KVM_ENSURE_SECRET_PATH); KVMAgentCommands.SecretHostDefineCmd cmd = new KVMAgentCommands.SecretHostDefineCmd(); - cmd.setEnvelopeDekBase64(envelopeDekBase64); + cmd.setEncryptedDek(envelopeDekBase64); + cmd.setVmUuid(msg.getVmUuid()); + cmd.setPurpose(msg.getPurpose()); + cmd.setProviderName(msg.getProviderName()); + cmd.setDescription(msg.getDescription() != null ? msg.getDescription() : ""); KVMAgentCommands.SecretHostDefineResponse rsp = restf.syncJsonPost(url, cmd, KVMAgentCommands.SecretHostDefineResponse.class); if (rsp.isSuccess()) { if (rsp.getSecretUuid() != null) { @@ -5525,7 +5537,7 @@ private void handle(SecretHostDefineMsg msg) { return; } String newEnvelopeDekBase64 = java.util.Base64.getEncoder().encodeToString(newEnvelope); - cmd.setEnvelopeDekBase64(newEnvelopeDekBase64); + cmd.setEncryptedDek(newEnvelopeDekBase64); rsp = restf.syncJsonPost(url, cmd, KVMAgentCommands.SecretHostDefineResponse.class); if (rsp.isSuccess()) { if (rsp.getSecretUuid() != null) { From 38b6d2a81fd40a83986de092c1129afe556f4b56 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 17:03:24 +0800 Subject: [PATCH 30/64] Remove rotate public key when define secret --- .../zstack/kvm/HostSecretEnvelopeCrypto.java | 37 ++++++++++++----- .../src/main/java/org/zstack/kvm/KVMHost.java | 40 ------------------- 2 files changed, 28 insertions(+), 49 deletions(-) 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/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index 1e841b7d2d1..45b3c91ba74 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -5507,46 +5507,6 @@ private void handle(SecretHostDefineMsg msg) { 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.setEncryptedDek(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()); From 7acb49c2bbf180d862381b9bc15ef9d5bd87a2b5 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 17:42:15 +0800 Subject: [PATCH 31/64] Envelope public key async call kvm agent --- .../src/main/java/org/zstack/kvm/KVMHost.java | 199 +++++++++++++----- 1 file changed, 144 insertions(+), 55 deletions(-) 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 45b3c91ba74..02f6f6e0246 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -5317,8 +5317,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(); + } + }); } }); @@ -5339,52 +5348,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; - } + 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(); } } @@ -5491,31 +5571,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.setEncryptedDek(envelopeDekBase64); - cmd.setVmUuid(msg.getVmUuid()); - cmd.setPurpose(msg.getPurpose()); - cmd.setProviderName(msg.getProviderName()); - cmd.setDescription(msg.getDescription() != null ? msg.getDescription() : ""); - 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); + } + + @Override + public void success(KVMAgentCommands.SecretHostDefineResponse rsp) { + if (rsp != null && rsp.isSuccess()) { + if (rsp.getSecretUuid() != null) { + 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()); + } } bus.reply(msg, reply); - return; } - 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 6acbbf3516cd6f5e6ef66c99e9fbedb6db22dfda Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 18:20:25 +0800 Subject: [PATCH 32/64] Limit dek length when define libvirt secret --- plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java | 8 ++++++++ 1 file changed, 8 insertions(+) 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 02f6f6e0246..a7dacc8e08e 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -5509,6 +5509,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())) { @@ -5549,6 +5551,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); From f2c226b8dbda2b687b8abf275342c141a4f76779 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Sat, 7 Mar 2026 19:57:13 +0800 Subject: [PATCH 33/64] Hide dek in log --- .../main/java/org/zstack/header/secret/SecretHostDefineMsg.java | 2 ++ 1 file changed, 2 insertions(+) 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; From a17cbb2056a5a929793c12f36a03a799300298a9 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Sat, 7 Mar 2026 20:02:09 +0800 Subject: [PATCH 34/64] Remove test point in stopVm --- .../src/main/java/org/zstack/kvm/KVMHost.java | 20 ------------------- 1 file changed, 20 deletions(-) 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 a7dacc8e08e..88d923de0b0 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -3865,26 +3865,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()); From 8679c5cc310c35ea0202e7b20e4abc8f85de2acf Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Sun, 8 Mar 2026 16:58:05 +0800 Subject: [PATCH 35/64] Improve HostSecretCase test --- .../kvm/host/HostSecretCase.groovy | 77 +++++++++++++++++-- 1 file changed, 70 insertions(+), 7 deletions(-) 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..0c00ae5f2c4 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,28 @@ 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.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.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.util.concurrent.atomic.AtomicInteger + /** * Integration test for host secret: create/get/rotate/verify public key on connect, * and SecretHostDefine (ensure secret on agent). @@ -28,6 +35,10 @@ 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=" @@ -62,6 +73,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 +83,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 +127,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 +149,41 @@ 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 only calls createEnvelopeKey; production does not GET and save the key after create. + // Persist HostKeyIdentity so SecretHostDefineMsg finds a public key (same value as KVM_GET_ENVELOPE_KEY_PATH simulator). + HostKeyIdentityVO keyVo = new HostKeyIdentityVO() + keyVo.hostUuid = addedHost.uuid + keyVo.publicKey = MOCK_PUBLIC_KEY_BASE64 + keyVo.fingerprint = "" + keyVo.verified = true + bean(DatabaseFacade.class).persist(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 +191,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 aff4bc677d9884fd03760c3cf6f2b405d79dfd8e Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Sun, 8 Mar 2026 18:12:25 +0800 Subject: [PATCH 36/64] Compute fingerprint of public key --- .../src/main/java/org/zstack/kvm/KVMHost.java | 178 +++++++++++++----- 1 file changed, 128 insertions(+), 50 deletions(-) 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 88d923de0b0..6a4a2b0d324 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -101,6 +101,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.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -5330,6 +5333,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(); @@ -5348,59 +5402,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); @@ -5429,8 +5445,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 @@ -5468,23 +5510,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); } @@ -5512,6 +5581,15 @@ private void handle(SecretHostDefineMsg msg) { bus.reply(msg, reply); return; } + String storedFingerprint = identity.getFingerprint(); + if (StringUtils.isNotBlank(storedFingerprint)) { + 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); From 0c197e42f94c3399e03dda68bd67f21a2df361fa Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Sun, 8 Mar 2026 18:28:12 +0800 Subject: [PATCH 37/64] Verify fingerprint in secret define --- .../src/main/java/org/zstack/kvm/KVMHost.java | 12 +++++------ .../kvm/host/HostSecretCase.groovy | 20 ++++++++++++++++++- 2 files changed, 24 insertions(+), 8 deletions(-) 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 6a4a2b0d324..bc4e9954b11 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -5582,13 +5582,11 @@ private void handle(SecretHostDefineMsg msg) { return; } String storedFingerprint = identity.getFingerprint(); - if (StringUtils.isNotBlank(storedFingerprint)) { - 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; - } + 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")); 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 0c00ae5f2c4..361b9221232 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 @@ -22,6 +22,7 @@ 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 /** @@ -42,6 +43,22 @@ class HostSecretCase extends SubCase { /** 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) @@ -165,10 +182,11 @@ class HostSecretCase extends SubCase { // Create/ping only calls createEnvelopeKey; production does not GET and save the key after create. // Persist HostKeyIdentity so SecretHostDefineMsg finds a public key (same value as KVM_GET_ENVELOPE_KEY_PATH simulator). + // Set fingerprint so the handle(SecretHostDefineMsg) fingerprint check is exercised (must match publicKey). HostKeyIdentityVO keyVo = new HostKeyIdentityVO() keyVo.hostUuid = addedHost.uuid keyVo.publicKey = MOCK_PUBLIC_KEY_BASE64 - keyVo.fingerprint = "" + keyVo.fingerprint = fingerprintFromPublicKey(MOCK_PUBLIC_KEY_BASE64) keyVo.verified = true bean(DatabaseFacade.class).persist(keyVo) } From 4835e5a05c2fbebc785a896007c32c4f74298aba Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Sun, 8 Mar 2026 18:33:14 +0800 Subject: [PATCH 38/64] Remove secret host get/create extension point --- .../secret/SecretCreateExtensionPoint.java | 19 ------------------- .../secret/SecretGetExtensionPoint.java | 19 ------------------- 2 files changed, 38 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); -} From 70a60ec735439843abb65ecf78c95b651e4823fc Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Mon, 9 Mar 2026 14:06:32 +0800 Subject: [PATCH 39/64] Fix primary key confict error --- .../kvm/host/HostSecretCase.groovy | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) 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 361b9221232..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 @@ -3,10 +3,13 @@ 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 @@ -180,15 +183,25 @@ class HostSecretCase extends SubCase { assert createEnvelopeKeyCallCount.get() >= 1 : "envelope key sync (KVM_CREATE_ENVELOPE_KEY_PATH) should be triggered at least once after add host" - // Create/ping only calls createEnvelopeKey; production does not GET and save the key after create. - // Persist HostKeyIdentity so SecretHostDefineMsg finds a public key (same value as KVM_GET_ENVELOPE_KEY_PATH simulator). - // Set fingerprint so the handle(SecretHostDefineMsg) fingerprint check is exercised (must match publicKey). - HostKeyIdentityVO keyVo = new HostKeyIdentityVO() - keyVo.hostUuid = addedHost.uuid - keyVo.publicKey = MOCK_PUBLIC_KEY_BASE64 - keyVo.fingerprint = fingerprintFromPublicKey(MOCK_PUBLIC_KEY_BASE64) - keyVo.verified = true - bean(DatabaseFacade.class).persist(keyVo) + // 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() { From e16d5a5f101794539fc1f3718f5e02005f11a1d1 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Mon, 9 Mar 2026 17:54:44 +0800 Subject: [PATCH 40/64] Use UTF-8 instead HPKE default charset --- .../org/zstack/kvm/HostSecretEnvelopeCrypto.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) 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); } From c10664a77c05ace256ab147c4bd997616efd867b Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Tue, 10 Mar 2026 14:06:15 +0800 Subject: [PATCH 41/64] Move some logic to premium * Move HPKE host secret envelope crypto to premium * Move logic of syncing public key to premium Resolves: ZSPHER-113 Related: http://dev.zstack.io:9080/zstackio/premium/-/merge_requests/13121 --- .../hostSecretEnvelopeCrypto.xml | 23 ++ conf/zstack.xml | 1 + .../zstack/header/host/HostKeyIdentityVO.java | 2 +- ...ostSecretEnvelopeCryptoExtensionPoint.java | 13 ++ .../header/secret/SecretHostDefineMsg.java | 2 +- .../header/secret/SecretHostDefineReply.java | 9 - plugin/kvm/pom.xml | 5 - .../zstack/kvm/HostSecretEnvelopeCrypto.java | 155 -------------- .../java/org/zstack/kvm/KVMAgentCommands.java | 2 +- .../src/main/java/org/zstack/kvm/KVMHost.java | 200 ++---------------- .../kvm/host/HostSecretCase.groovy | 21 +- ...tSecretEnvelopeCryptoExtensionPoint.groovy | 22 ++ ...SecretEnvelopeCryptoExtensionPointMock.xml | 15 ++ 13 files changed, 112 insertions(+), 358 deletions(-) create mode 100644 conf/springConfigXml/hostSecretEnvelopeCrypto.xml create mode 100644 header/src/main/java/org/zstack/header/secret/HostSecretEnvelopeCryptoExtensionPoint.java delete 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/MockHostSecretEnvelopeCryptoExtensionPoint.groovy create mode 100644 test/src/test/resources/springConfigXml/HostSecretEnvelopeCryptoExtensionPointMock.xml diff --git a/conf/springConfigXml/hostSecretEnvelopeCrypto.xml b/conf/springConfigXml/hostSecretEnvelopeCrypto.xml new file mode 100644 index 00000000000..69d61dd397e --- /dev/null +++ b/conf/springConfigXml/hostSecretEnvelopeCrypto.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + diff --git a/conf/zstack.xml b/conf/zstack.xml index a4bd0a088b8..61d6ae737a8 100755 --- a/conf/zstack.xml +++ b/conf/zstack.xml @@ -58,6 +58,7 @@ + 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/HostSecretEnvelopeCryptoExtensionPoint.java b/header/src/main/java/org/zstack/header/secret/HostSecretEnvelopeCryptoExtensionPoint.java new file mode 100644 index 00000000000..a166e3d1258 --- /dev/null +++ b/header/src/main/java/org/zstack/header/secret/HostSecretEnvelopeCryptoExtensionPoint.java @@ -0,0 +1,13 @@ +package org.zstack.header.secret; + +/** + * 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/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 c0f484d0bc7..977ff26f496 100755 --- a/plugin/kvm/pom.xml +++ b/plugin/kvm/pom.xml @@ -9,11 +9,6 @@ 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 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/KVMAgentCommands.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java index eb121f58c32..3f982544168 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -425,7 +425,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/KVMHost.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java index bc4e9954b11..7ccd0fe06f8 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -55,6 +55,7 @@ import org.zstack.header.exception.CloudRuntimeException; import org.zstack.header.host.*; import org.zstack.header.host.MigrateVmOnHypervisorMsg.StorageMigrationPolicy; +import org.zstack.header.secret.HostSecretEnvelopeCryptoExtensionPoint; import org.zstack.header.secret.SecretHostDefineMsg; import org.zstack.header.secret.SecretHostDefineReply; import org.zstack.header.message.APIMessage; @@ -5290,30 +5291,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) { @@ -5333,163 +5310,6 @@ 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(); - 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) { @@ -5626,10 +5446,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; @@ -5656,9 +5482,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); 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 afcdd213a77..685c94ade99 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 @@ -64,7 +64,26 @@ class HostSecretCase extends SubCase { @Override void setup() { - useSpring(KvmTest.springSpec) + // Use KvmTest spring spec plus mock HostSecretEnvelopeCryptoExtensionPoint (premium/crypto not on test classpath) + useSpring(makeSpring { + sftpBackupStorage() + localStorage() + nfsPrimaryStorage() + smp() + virtualRouter() + flatNetwork() + securityGroup() + kvm() + ceph() + vyos() + flatNetwork() + eip() + lb() + portForwarding() + include("LongJobManager.xml") + include("HostAllocateExtension.xml") + include("HostSecretEnvelopeCryptoExtensionPointMock.xml") + }) } @Override diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/host/MockHostSecretEnvelopeCryptoExtensionPoint.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/host/MockHostSecretEnvelopeCryptoExtensionPoint.groovy new file mode 100644 index 00000000000..c72ee05d8e0 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/kvm/host/MockHostSecretEnvelopeCryptoExtensionPoint.groovy @@ -0,0 +1,22 @@ +package org.zstack.test.integration.kvm.host + +import org.zstack.header.secret.HostSecretEnvelopeCryptoExtensionPoint + +/** + * Mock implementation for integration test when premium/crypto is not on classpath. + * Returns a fake envelope (32-byte enc + plaintext + 12 tag) so KVM proceeds to call the agent simulator. + */ +class MockHostSecretEnvelopeCryptoExtensionPoint implements HostSecretEnvelopeCryptoExtensionPoint { + @Override + byte[] seal(byte[] recipientPublicKey, byte[] plaintext) throws Exception { + if (recipientPublicKey == null || recipientPublicKey.length != 32 || plaintext == null) { + throw new IllegalArgumentException("recipientPublicKey must be 32 bytes, plaintext non-null") + } + int encLen = 32 + int tagLen = 12 + byte[] envelope = new byte[encLen + plaintext.length + tagLen] + System.arraycopy(recipientPublicKey, 0, envelope, 0, encLen) + System.arraycopy(plaintext, 0, envelope, encLen, plaintext.length) + return envelope + } +} diff --git a/test/src/test/resources/springConfigXml/HostSecretEnvelopeCryptoExtensionPointMock.xml b/test/src/test/resources/springConfigXml/HostSecretEnvelopeCryptoExtensionPointMock.xml new file mode 100644 index 00000000000..b4e41cf2794 --- /dev/null +++ b/test/src/test/resources/springConfigXml/HostSecretEnvelopeCryptoExtensionPointMock.xml @@ -0,0 +1,15 @@ + + + + + + + + + From 5c3ffc8fa167eee201b4f8b63444160ae7933277 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Tue, 10 Mar 2026 15:25:31 +0800 Subject: [PATCH 42/64] Remove redunrant extension config --- .../hostSecretEnvelopeCrypto.xml | 23 ------------------- conf/zstack.xml | 1 - 2 files changed, 24 deletions(-) delete mode 100644 conf/springConfigXml/hostSecretEnvelopeCrypto.xml diff --git a/conf/springConfigXml/hostSecretEnvelopeCrypto.xml b/conf/springConfigXml/hostSecretEnvelopeCrypto.xml deleted file mode 100644 index 69d61dd397e..00000000000 --- a/conf/springConfigXml/hostSecretEnvelopeCrypto.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/conf/zstack.xml b/conf/zstack.xml index 61d6ae737a8..a4bd0a088b8 100755 --- a/conf/zstack.xml +++ b/conf/zstack.xml @@ -58,7 +58,6 @@ - From e62ec6bf0501bd6756384f39cbbd3b9b29f78d72 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Tue, 10 Mar 2026 17:36:35 +0800 Subject: [PATCH 43/64] Add envelope test --- ...ostSecretEnvelopeCryptoExtensionPoint.java | 2 +- .../main/java/org/zstack/kvm/KVMConstant.java | 3 + .../src/main/java/org/zstack/kvm/KVMHost.java | 5 +- test/pom.xml | 2 +- .../kvm/host/HostSecretCase.groovy | 63 ++++--------------- ...stSecretEnvelopeCryptoTestExtension.groovy | 25 ++++++++ ...tSecretEnvelopeCryptoExtensionPoint.groovy | 22 ------- ...HostSecretEnvelopeCryptoTestExtension.xml} | 4 +- 8 files changed, 46 insertions(+), 80 deletions(-) rename {header/src/main/java/org/zstack/header/secret => plugin/kvm/src/main/java/org/zstack/kvm}/HostSecretEnvelopeCryptoExtensionPoint.java (94%) create mode 100644 test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretEnvelopeCryptoTestExtension.groovy delete mode 100644 test/src/test/groovy/org/zstack/test/integration/kvm/host/MockHostSecretEnvelopeCryptoExtensionPoint.groovy rename test/src/test/resources/springConfigXml/{HostSecretEnvelopeCryptoExtensionPointMock.xml => HostSecretEnvelopeCryptoTestExtension.xml} (66%) diff --git a/header/src/main/java/org/zstack/header/secret/HostSecretEnvelopeCryptoExtensionPoint.java b/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCryptoExtensionPoint.java similarity index 94% rename from header/src/main/java/org/zstack/header/secret/HostSecretEnvelopeCryptoExtensionPoint.java rename to plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCryptoExtensionPoint.java index a166e3d1258..252bffbf055 100644 --- a/header/src/main/java/org/zstack/header/secret/HostSecretEnvelopeCryptoExtensionPoint.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/HostSecretEnvelopeCryptoExtensionPoint.java @@ -1,4 +1,4 @@ -package org.zstack.header.secret; +package org.zstack.kvm; /** * Extension point for sealing plaintext (e.g. DEK) with recipient's X25519 public key for host secret (e.g. vTPM). 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 7355ed99f3b..95280978e0e 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -128,6 +128,9 @@ 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; + 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 7ccd0fe06f8..979a3715a48 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -55,7 +55,6 @@ import org.zstack.header.exception.CloudRuntimeException; import org.zstack.header.host.*; import org.zstack.header.host.MigrateVmOnHypervisorMsg.StorageMigrationPolicy; -import org.zstack.header.secret.HostSecretEnvelopeCryptoExtensionPoint; import org.zstack.header.secret.SecretHostDefineMsg; import org.zstack.header.secret.SecretHostDefineReply; import org.zstack.header.message.APIMessage; @@ -5308,8 +5307,6 @@ public void handle(ErrorCode errCode, Map data) { }).start(); } - private static final long ENVELOPE_KEY_HTTP_TIMEOUT_SEC = 5L; - private void setHostKeyIdentityVerified(String hostUuid, boolean verified) { HostKeyIdentityVO vo = getHostKeyIdentity(hostUuid); if (vo != null) { @@ -5498,7 +5495,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 e054377d284..3f81ded3240 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -9,7 +9,7 @@ zstack org.zstack - 4.10.0 + 4.10.0 .. test 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 685c94ade99..144c3a76613 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 @@ -29,9 +29,8 @@ 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). - * Uses simulated agent for all secret paths. + * Integration test for SecretHostDefine (ensure secret on agent) on KVM host. + * Uses simulated agent; envelope key sync-on-ping and HPKE seal are covered in premium tests. */ class HostSecretCase extends SubCase { EnvSpec env @@ -39,8 +38,7 @@ class HostSecretCase extends SubCase { CloudBus bus HostInventory addedHost - /** Counters for simulator call assertions (async secret sync / ensureSecret). */ - AtomicInteger createEnvelopeKeyCallCount + /** Counters for simulator call assertions (ensureSecret). */ AtomicInteger ensureSecretCallCount /** 32-byte X25519 public key (base64) for simulator; must be valid for HPKE seal. */ @@ -64,7 +62,8 @@ class HostSecretCase extends SubCase { @Override void setup() { - // Use KvmTest spring spec plus mock HostSecretEnvelopeCryptoExtensionPoint (premium/crypto not on test classpath) + // Run without premium/crypto; use test extension HostSecretEnvelopeCryptoTestExtension so that + // SecretHostDefine path can run and call the agent simulator. Sync-on-ping is covered in premium tests. useSpring(makeSpring { sftpBackupStorage() localStorage() @@ -82,7 +81,7 @@ class HostSecretCase extends SubCase { portForwarding() include("LongJobManager.xml") include("HostAllocateExtension.xml") - include("HostSecretEnvelopeCryptoExtensionPointMock.xml") + include("HostSecretEnvelopeCryptoTestExtension.xml") }) } @@ -95,7 +94,7 @@ class HostSecretCase extends SubCase { void test() { env.create { prepare() - testAddHostWithSecretSync() + prepareHostForSecretTests() testSecretHostDefineSuccess() testSecretHostDefineFailWhenNoDek() } @@ -112,7 +111,6 @@ class HostSecretCase extends SubCase { } void registerSecretSimulators() { - createEnvelopeKeyCallCount = new AtomicInteger(0) ensureSecretCallCount = new AtomicInteger(0) env.simulator(KVMConstant.KVM_CONNECT_PATH) { @@ -136,35 +134,6 @@ class HostSecretCase extends SubCase { 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(KVMConstant.KVM_CREATE_ENVELOPE_KEY_PATH) { - createEnvelopeKeyCallCount?.incrementAndGet() - 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) { ensureSecretCallCount?.incrementAndGet() def rsp = new KVMAgentCommands.SecretHostDefineResponse() @@ -173,7 +142,12 @@ class HostSecretCase extends SubCase { } } - void testAddHostWithSecretSync() { + /** + * Prepare a connected KVM host and corresponding HostKeyIdentityVO so that + * SecretHostDefineMsg can succeed. Envelope key sync-on-ping itself is + * covered by premium KVMEnvelopeKeySyncExtensionCase. + */ + void prepareHostForSecretTests() { registerSecretSimulators() AddKVMHostMsg amsg = new AddKVMHostMsg() @@ -192,17 +166,6 @@ class HostSecretCase extends SubCase { 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) diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretEnvelopeCryptoTestExtension.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretEnvelopeCryptoTestExtension.groovy new file mode 100644 index 00000000000..f15f7ce8b16 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretEnvelopeCryptoTestExtension.groovy @@ -0,0 +1,25 @@ +package org.zstack.test.integration.kvm.host.secret + +import org.zstack.kvm.HostSecretEnvelopeCryptoExtensionPoint + +/** + * Test-side mock of HostSecretEnvelopeCryptoExtensionPoint. + * Does NOT call premium crypto; just returns a fake envelope that looks structurally valid to the agent. + */ +class HostSecretEnvelopeCryptoTestExtension implements HostSecretEnvelopeCryptoExtensionPoint { + @Override + byte[] seal(byte[] recipientPublicKey, byte[] plaintext) throws Exception { + if (recipientPublicKey == null || recipientPublicKey.length != 32 || plaintext == null) { + throw new IllegalArgumentException("recipientPublicKey must be 32 bytes, plaintext non-null") + } + int encLen = 32 + int tagLen = 12 + byte[] envelope = new byte[encLen + plaintext.length + tagLen] + // Put the recipient public key into the "enc" slot so envelope[0..31] looks like a plausible X25519 public key. + System.arraycopy(recipientPublicKey, 0, envelope, 0, encLen) + // Copy plaintext in the middle; the simulator never decrypts it. + System.arraycopy(plaintext, 0, envelope, encLen, plaintext.length) + // Leave the last 12 bytes as zeros to mimic an AEAD tag. + return envelope + } +} diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/host/MockHostSecretEnvelopeCryptoExtensionPoint.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/host/MockHostSecretEnvelopeCryptoExtensionPoint.groovy deleted file mode 100644 index c72ee05d8e0..00000000000 --- a/test/src/test/groovy/org/zstack/test/integration/kvm/host/MockHostSecretEnvelopeCryptoExtensionPoint.groovy +++ /dev/null @@ -1,22 +0,0 @@ -package org.zstack.test.integration.kvm.host - -import org.zstack.header.secret.HostSecretEnvelopeCryptoExtensionPoint - -/** - * Mock implementation for integration test when premium/crypto is not on classpath. - * Returns a fake envelope (32-byte enc + plaintext + 12 tag) so KVM proceeds to call the agent simulator. - */ -class MockHostSecretEnvelopeCryptoExtensionPoint implements HostSecretEnvelopeCryptoExtensionPoint { - @Override - byte[] seal(byte[] recipientPublicKey, byte[] plaintext) throws Exception { - if (recipientPublicKey == null || recipientPublicKey.length != 32 || plaintext == null) { - throw new IllegalArgumentException("recipientPublicKey must be 32 bytes, plaintext non-null") - } - int encLen = 32 - int tagLen = 12 - byte[] envelope = new byte[encLen + plaintext.length + tagLen] - System.arraycopy(recipientPublicKey, 0, envelope, 0, encLen) - System.arraycopy(plaintext, 0, envelope, encLen, plaintext.length) - return envelope - } -} diff --git a/test/src/test/resources/springConfigXml/HostSecretEnvelopeCryptoExtensionPointMock.xml b/test/src/test/resources/springConfigXml/HostSecretEnvelopeCryptoTestExtension.xml similarity index 66% rename from test/src/test/resources/springConfigXml/HostSecretEnvelopeCryptoExtensionPointMock.xml rename to test/src/test/resources/springConfigXml/HostSecretEnvelopeCryptoTestExtension.xml index b4e41cf2794..9b4cb4b288f 100644 --- a/test/src/test/resources/springConfigXml/HostSecretEnvelopeCryptoExtensionPointMock.xml +++ b/test/src/test/resources/springConfigXml/HostSecretEnvelopeCryptoTestExtension.xml @@ -7,9 +7,9 @@ http://zstack.org/schema/zstack http://zstack.org/schema/zstack/plugin.xsd"> - + - + From 24342b53c165f58432a504098609677a1f922ac1 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Tue, 10 Mar 2026 18:31:52 +0800 Subject: [PATCH 44/64] Introduce host key helper --- .../org/zstack/kvm/HostKeyIdentityHelper.java | 123 ++++++++++++++++++ .../main/java/org/zstack/kvm/KVMConstant.java | 3 + .../src/main/java/org/zstack/kvm/KVMHost.java | 76 +---------- 3 files changed, 129 insertions(+), 73 deletions(-) create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/HostKeyIdentityHelper.java 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/KVMConstant.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java index 95280978e0e..c2cd54c184f 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -131,6 +131,9 @@ public interface KVMConstant { /** 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 979a3715a48..01635771110 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -5307,76 +5307,6 @@ public void handle(ErrorCode errCode, Map data) { }).start(); } - 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())) { @@ -5390,7 +5320,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) { @@ -5399,7 +5329,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); @@ -5424,7 +5354,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; From ba0a6eeedc470dce9346eb89e179937054b7e047 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Tue, 10 Mar 2026 20:49:46 +0800 Subject: [PATCH 45/64] Mova secret test to premium --- .../kvm/host/HostSecretCase.groovy | 225 ------------------ ...stSecretEnvelopeCryptoTestExtension.groovy | 25 -- .../HostSecretEnvelopeCryptoTestExtension.xml | 15 -- 3 files changed, 265 deletions(-) delete mode 100644 test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretCase.groovy delete mode 100644 test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretEnvelopeCryptoTestExtension.groovy delete mode 100644 test/src/test/resources/springConfigXml/HostSecretEnvelopeCryptoTestExtension.xml 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 deleted file mode 100644 index 144c3a76613..00000000000 --- a/test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretCase.groovy +++ /dev/null @@ -1,225 +0,0 @@ -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 SecretHostDefine (ensure secret on agent) on KVM host. - * Uses simulated agent; envelope key sync-on-ping and HPKE seal are covered in premium tests. - */ -class HostSecretCase extends SubCase { - EnvSpec env - def cluster - CloudBus bus - HostInventory addedHost - - /** Counters for simulator call assertions (ensureSecret). */ - 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() { - // Run without premium/crypto; use test extension HostSecretEnvelopeCryptoTestExtension so that - // SecretHostDefine path can run and call the agent simulator. Sync-on-ping is covered in premium tests. - useSpring(makeSpring { - sftpBackupStorage() - localStorage() - nfsPrimaryStorage() - smp() - virtualRouter() - flatNetwork() - securityGroup() - kvm() - ceph() - vyos() - flatNetwork() - eip() - lb() - portForwarding() - include("LongJobManager.xml") - include("HostAllocateExtension.xml") - include("HostSecretEnvelopeCryptoTestExtension.xml") - }) - } - - @Override - void environment() { - env = HostEnv.noHostBasicEnv() - } - - @Override - void test() { - env.create { - prepare() - prepareHostForSecretTests() - testSecretHostDefineSuccess() - testSecretHostDefineFailWhenNoDek() - } - } - - @Override - void clean() { - env.delete() - } - - void prepare() { - cluster = env.inventoryByName("cluster") - bus = bean(CloudBus.class) - } - - void registerSecretSimulators() { - ensureSecretCallCount = new AtomicInteger(0) - - 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 - } - // 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 - } - - env.simulator(KVMConstant.KVM_ENSURE_SECRET_PATH) { - ensureSecretCallCount?.incrementAndGet() - def rsp = new KVMAgentCommands.SecretHostDefineResponse() - rsp.secretUuid = Platform.uuid - return rsp - } - } - - /** - * Prepare a connected KVM host and corresponding HostKeyIdentityVO so that - * SecretHostDefineMsg can succeed. Envelope key sync-on-ping itself is - * covered by premium KVMEnvelopeKeySyncExtensionCase. - */ - void prepareHostForSecretTests() { - 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() : "AddHost failed: ${reply.error?.toString() ?: 'no error'}" - assert reply.inventory.status == HostStatus.Connected.toString() - addedHost = reply.inventory - - // 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 = "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) - assert reply != null - 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() { - 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 diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretEnvelopeCryptoTestExtension.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretEnvelopeCryptoTestExtension.groovy deleted file mode 100644 index f15f7ce8b16..00000000000 --- a/test/src/test/groovy/org/zstack/test/integration/kvm/host/HostSecretEnvelopeCryptoTestExtension.groovy +++ /dev/null @@ -1,25 +0,0 @@ -package org.zstack.test.integration.kvm.host.secret - -import org.zstack.kvm.HostSecretEnvelopeCryptoExtensionPoint - -/** - * Test-side mock of HostSecretEnvelopeCryptoExtensionPoint. - * Does NOT call premium crypto; just returns a fake envelope that looks structurally valid to the agent. - */ -class HostSecretEnvelopeCryptoTestExtension implements HostSecretEnvelopeCryptoExtensionPoint { - @Override - byte[] seal(byte[] recipientPublicKey, byte[] plaintext) throws Exception { - if (recipientPublicKey == null || recipientPublicKey.length != 32 || plaintext == null) { - throw new IllegalArgumentException("recipientPublicKey must be 32 bytes, plaintext non-null") - } - int encLen = 32 - int tagLen = 12 - byte[] envelope = new byte[encLen + plaintext.length + tagLen] - // Put the recipient public key into the "enc" slot so envelope[0..31] looks like a plausible X25519 public key. - System.arraycopy(recipientPublicKey, 0, envelope, 0, encLen) - // Copy plaintext in the middle; the simulator never decrypts it. - System.arraycopy(plaintext, 0, envelope, encLen, plaintext.length) - // Leave the last 12 bytes as zeros to mimic an AEAD tag. - return envelope - } -} diff --git a/test/src/test/resources/springConfigXml/HostSecretEnvelopeCryptoTestExtension.xml b/test/src/test/resources/springConfigXml/HostSecretEnvelopeCryptoTestExtension.xml deleted file mode 100644 index 9b4cb4b288f..00000000000 --- a/test/src/test/resources/springConfigXml/HostSecretEnvelopeCryptoTestExtension.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - From 754b572855e4eaec6775d31130588aa4104beb7b Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Wed, 11 Mar 2026 10:33:52 +0800 Subject: [PATCH 46/64] Remove unused envelopeDekBase64 --- plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java | 1 - 1 file changed, 1 deletion(-) 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 3f982544168..19d65295d22 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -424,7 +424,6 @@ public void setErrorCode(String errorCode) { } public static class SecretHostDefineCmd extends AgentCommand { - private String envelopeDekBase64; /** Base64 envelope of DEK; agent expects this field name (encryptedDek). */ private String encryptedDek; private String vmUuid; From c500de9ca469fa81d4ff50dd7838518ccde96b68 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Wed, 11 Mar 2026 10:38:28 +0800 Subject: [PATCH 47/64] Verify storedFingerprint is null --- plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 01635771110..65a3f682a11 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -5328,9 +5328,9 @@ private void handle(SecretHostDefineMsg msg) { bus.reply(msg, reply); return; } - String storedFingerprint = identity.getFingerprint(); + String storedFingerprint = StringUtils.trimToNull(identity.getFingerprint()); String computed = HostKeyIdentityHelper.fingerprintFromPublicKey(pubKey); - if (!storedFingerprint.equals(computed)) { + if (storedFingerprint == null || !StringUtils.equals(storedFingerprint, computed)) { reply.setError(operr("host public key fingerprint mismatch, key may be corrupted or tampered")); bus.reply(msg, reply); return; From 99876ecfda06b7fb6091608c94752eabd057da3d Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Wed, 11 Mar 2026 10:50:46 +0800 Subject: [PATCH 48/64] Remove self errCode --- .../java/org/zstack/kvm/KVMAgentCommands.java | 27 ------------------- .../src/main/java/org/zstack/kvm/KVMHost.java | 6 ++--- 2 files changed, 3 insertions(+), 30 deletions(-) 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 19d65295d22..e1be2dc98b0 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -383,7 +383,6 @@ public static class GetPublicKeyCmd extends AgentCommand { public static class GetPublicKeyResponse extends AgentResponse { private String publicKey; - private String errorCode; public String getPublicKey() { return publicKey; @@ -392,14 +391,6 @@ public String getPublicKey() { 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 { @@ -412,15 +403,6 @@ 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 { @@ -473,17 +455,8 @@ public void setDescription(String description) { } 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; } 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 65a3f682a11..a23773bbca2 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -5409,10 +5409,10 @@ public void success(KVMAgentCommands.SecretHostDefineResponse rsp) { reply.setSecretUuid(rsp.getSecretUuid()); } } else { - if (rsp != null && rsp.getErrorCode() != null) { + if (rsp != null && rsp.getError() != null) { ErrorCode err = new ErrorCode(); - err.setCode(rsp.getErrorCode()); - err.setDetails(rsp.getError() != null ? rsp.getError() : "ensure secret failed"); + err.setCode(rsp.getError()); + err.setDetails(rsp.getError()); reply.setError(err); } else { reply.setError(operr(rsp != null ? rsp.getError() : "ensure secret failed")); From 967e36a90907f2dfab284526e3ae13b4e89cafc3 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Wed, 11 Mar 2026 12:01:14 +0800 Subject: [PATCH 49/64] Fix some review suggestion --- .../main/java/org/zstack/header/secret/SecretHostDefineMsg.java | 2 +- .../kvm/src/main/java/org/zstack/kvm/HostKeyIdentityHelper.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 e8aeb6d9370..1997267b055 100644 --- a/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java +++ b/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java @@ -66,4 +66,4 @@ public String getDescription() { 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/HostKeyIdentityHelper.java b/plugin/kvm/src/main/java/org/zstack/kvm/HostKeyIdentityHelper.java index ffe65edca47..da59697a290 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/HostKeyIdentityHelper.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/HostKeyIdentityHelper.java @@ -33,7 +33,7 @@ public static String fingerprintFromPublicKey(String publicKeyBase64) { } try { byte[] keyBytes = Base64.getDecoder().decode(publicKeyBase64.trim()); - if (keyBytes == null || keyBytes.length == 0) { + if (keyBytes.length == 0) { return ""; } MessageDigest md = MessageDigest.getInstance("SHA-256"); From 567585e13ab0e4541a1fac18cbc6e956ad926ad4 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Wed, 11 Mar 2026 12:41:32 +0800 Subject: [PATCH 50/64] Fix db Duplicate entry --- .../org/zstack/kvm/HostKeyIdentityHelper.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/HostKeyIdentityHelper.java b/plugin/kvm/src/main/java/org/zstack/kvm/HostKeyIdentityHelper.java index da59697a290..4cc3fa82830 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/HostKeyIdentityHelper.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/HostKeyIdentityHelper.java @@ -7,10 +7,14 @@ import org.zstack.header.host.HostKeyIdentityVO; import org.zstack.header.host.HostKeyIdentityVO_; import org.zstack.header.secret.SecretHostDefineReply; +import org.zstack.utils.ExceptionDSL; import org.zstack.utils.logging.CLogger; +import javax.persistence.EntityExistsException; +import javax.persistence.PersistenceException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.sql.SQLIntegrityConstraintViolationException; import java.util.Base64; /** @@ -92,8 +96,19 @@ public static void saveOrUpdateHostKeyIdentity(DatabaseFacade dbf, String hostUu vo.setFingerprint(fingerprint); vo.setVerified(verified); vo.setCreateDate(new java.sql.Timestamp(System.currentTimeMillis())); - dbf.persist(vo); - return; + try { + dbf.persist(vo); + return; + } catch (EntityExistsException | PersistenceException e) { + if (!ExceptionDSL.isCausedBy(e, EntityExistsException.class) + && !ExceptionDSL.isCausedBy(e, SQLIntegrityConstraintViolationException.class, "Duplicate entry")) { + throw e; + } + vo = getHostKeyIdentity(dbf, hostUuid); + if (vo == null) { + throw e; + } + } } vo.setPublicKey(keyToSave); vo.setFingerprint(fingerprint); From 352845f3ec60f46c6f2ef3cc5bdccbe924c08c51 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Wed, 11 Mar 2026 13:20:10 +0800 Subject: [PATCH 51/64] Remove catching EntityExistsException --- .../kvm/src/main/java/org/zstack/kvm/HostKeyIdentityHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/HostKeyIdentityHelper.java b/plugin/kvm/src/main/java/org/zstack/kvm/HostKeyIdentityHelper.java index 4cc3fa82830..d100dbeee17 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/HostKeyIdentityHelper.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/HostKeyIdentityHelper.java @@ -99,7 +99,7 @@ public static void saveOrUpdateHostKeyIdentity(DatabaseFacade dbf, String hostUu try { dbf.persist(vo); return; - } catch (EntityExistsException | PersistenceException e) { + } catch (PersistenceException e) { if (!ExceptionDSL.isCausedBy(e, EntityExistsException.class) && !ExceptionDSL.isCausedBy(e, SQLIntegrityConstraintViolationException.class, "Duplicate entry")) { throw e; From edb0acb0714e4d248a8f02afe53082ff6dfe2d92 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 12 Mar 2026 16:03:33 +0800 Subject: [PATCH 52/64] [kvm]: introduce VmHostBackupFileVO Resolves: ZSV-11439 Related: ZSV-11310 Change-Id: I63706e6a7a75616366716574646a677a6e646276 --- conf/db/zsv/V5.0.0__schema.sql | 19 +++-- conf/persistence.xml | 1 + .../vm/additions/VmHostBackupFileVO.java | 80 +++++++++++++++++++ .../vm/additions/VmHostBackupFileVO_.java | 15 ++++ .../header/vm/additions/VmHostFileType.java | 2 - 5 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 header/src/main/java/org/zstack/header/vm/additions/VmHostBackupFileVO.java create mode 100644 header/src/main/java/org/zstack/header/vm/additions/VmHostBackupFileVO_.java diff --git a/conf/db/zsv/V5.0.0__schema.sql b/conf/db/zsv/V5.0.0__schema.sql index ac3b849ec1e..64eb64b56b0 100644 --- a/conf/db/zsv/V5.0.0__schema.sql +++ b/conf/db/zsv/V5.0.0__schema.sql @@ -13,26 +13,35 @@ CREATE TABLE IF NOT EXISTS `zstack`.`VmHostFileVO` ( `uuid` char(32) NOT NULL UNIQUE, `vmInstanceUuid` char(32) NOT NULL, `hostUuid` char(32) NOT NULL, - `type` varchar(64) NOT NULL COMMENT 'NvRam, TpmState, NvRamBackup, TpmStateBackup', + `type` varchar(64) NOT NULL COMMENT 'NvRam, TpmState', `path` varchar(1024) NOT NULL COMMENT 'Absolute path of the file on the host', `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59', PRIMARY KEY (`uuid`), INDEX `idxVmHostFileVOVmInstanceUuid` (`vmInstanceUuid`), INDEX `idxVmHostFileVOHostUuid` (`hostUuid`), - CONSTRAINT `fkVmHostFileVOVmInstanceVO` FOREIGN KEY (`vmInstanceUuid`) REFERENCES `VmInstanceEO` (`uuid`) ON DELETE CASCADE, - CONSTRAINT `fkVmHostFileVOHostVO` FOREIGN KEY (`hostUuid`) REFERENCES `HostEO` (`uuid`) ON DELETE CASCADE, UNIQUE KEY `ukVmHostFileVO` (`vmInstanceUuid`, `hostUuid`, `type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -CREATE TABLE IF NOT EXISTS `zstack`.`VmHostFileContentVO` ( +CREATE TABLE IF NOT EXISTS `zstack`.`VmHostBackupFileVO` ( `uuid` char(32) NOT NULL UNIQUE, + `vmInstanceUuid` char(32) NOT NULL, + `type` varchar(64) NOT NULL COMMENT 'NvRam, TpmState', + `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59', + PRIMARY KEY (`uuid`), + INDEX `idxVmHostBackupFileVOVmInstanceUuid` (`vmInstanceUuid`), + UNIQUE KEY `ukVmHostBackupFileVO` (`vmInstanceUuid`, `type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`VmHostFileContentVO` ( + `uuid` char(32) NOT NULL UNIQUE COMMENT 'VmHostFileVO.uuid or VmHostBackupFileVO.uuid', `content` MEDIUMBLOB DEFAULT NULL, `format` varchar(64) NOT NULL COMMENT 'Raw, TarballGzip', `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59', PRIMARY KEY (`uuid`), - CONSTRAINT `fkVmHostFileContentVOVmHostFileVO` FOREIGN KEY (`uuid`) REFERENCES `VmHostFileVO` (`uuid`) ON DELETE CASCADE + CONSTRAINT `fkVmHostFileContentVOResourceVO` FOREIGN KEY (`uuid`) REFERENCES `ResourceVO` (`uuid`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- Feature: KMS | ZSPHER-46, ZSPHER-60, ZSPHER-61, ZSPHER-62 diff --git a/conf/persistence.xml b/conf/persistence.xml index 0fa065a6a71..8f3b34b367c 100755 --- a/conf/persistence.xml +++ b/conf/persistence.xml @@ -20,6 +20,7 @@ org.zstack.header.managementnode.ManagementNodeContextVO org.zstack.header.tpm.entity.TpmVO org.zstack.header.vm.additions.VmHostFileVO + org.zstack.header.vm.additions.VmHostBackupFileVO org.zstack.header.vm.additions.VmHostFileContentVO org.zstack.header.zone.ZoneVO org.zstack.header.zone.ZoneEO diff --git a/header/src/main/java/org/zstack/header/vm/additions/VmHostBackupFileVO.java b/header/src/main/java/org/zstack/header/vm/additions/VmHostBackupFileVO.java new file mode 100644 index 00000000000..94662c612e0 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostBackupFileVO.java @@ -0,0 +1,80 @@ +package org.zstack.header.vm.additions; + +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 (Backup files) + * + * Include: NvRam / TpmState files + */ +@Entity +@Table +@EntityGraph( + friends = { + @EntityGraph.Neighbour(type = VmInstanceEO.class, myField = "vmInstanceUuid", targetField = "uuid"), + } +) +public class VmHostBackupFileVO extends ResourceVO { + @Column + @ForeignKey(parentEntityClass = VmInstanceEO.class, onDeleteAction = ForeignKey.ReferenceOption.CASCADE) + private String vmInstanceUuid; + @Column + @Enumerated(EnumType.STRING) + private VmHostFileType type; + @Column + private Timestamp createDate; + @Column + private Timestamp lastOpDate; + + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public VmHostFileType getType() { + return type; + } + + public void setType(VmHostFileType type) { + this.type = type; + } + + 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 "VmHostBackupFileVO{" + + "vmInstanceUuid='" + vmInstanceUuid + '\'' + + ", type=" + type + + ", createDate=" + createDate + + ", lastOpDate=" + lastOpDate + + '}'; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/additions/VmHostBackupFileVO_.java b/header/src/main/java/org/zstack/header/vm/additions/VmHostBackupFileVO_.java new file mode 100644 index 00000000000..e45e804f381 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostBackupFileVO_.java @@ -0,0 +1,15 @@ +package org.zstack.header.vm.additions; + +import org.zstack.header.vo.ResourceVO_; + +import javax.persistence.metamodel.SingularAttribute; +import javax.persistence.metamodel.StaticMetamodel; +import java.sql.Timestamp; + +@StaticMetamodel(VmHostBackupFileVO.class) +public class VmHostBackupFileVO_ extends ResourceVO_ { + public static volatile SingularAttribute vmInstanceUuid; + public static volatile SingularAttribute type; + public static volatile SingularAttribute createDate; + public static volatile SingularAttribute 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 index 16c493e6768..4416821e949 100644 --- a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileType.java +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileType.java @@ -3,6 +3,4 @@ public enum VmHostFileType { NvRam, TpmState, - NvRamBackup, - TpmStateBackup, } From 30d90f5da0d2cad9e7dc6ad5ade75a4f390a9b6d Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 12 Mar 2026 14:38:30 +0800 Subject: [PATCH 53/64] [kvm]: support clone for TPM VM * Added support for cross-VM cloning of host files (NvRam, TpmState) and TPM encryption key cloning * KvmSecureBootManager transitioned from a Component to a service * Introducing new message/response types, workflows, and TPM key backend interfaces along with their virtual implementations Resolves: ZSV-11439 Related: ZSV-11310 Change-Id: I796b77716b6c64657469697a7a797a7268636e6e --- .../compute/vm/devices/VmTpmManager.java | 16 + conf/springConfigXml/Kvm.xml | 3 + .../zstack/header/vm/VmInstanceConstant.java | 1 + .../zstack/kvm/efi/CloneVmHostFileMsg.java | 35 +++ .../zstack/kvm/efi/CloneVmHostFileReply.java | 6 + .../kvm/efi/KvmSecureBootExtensions.java | 4 + .../zstack/kvm/efi/KvmSecureBootManager.java | 283 +++++++++++++++++- .../org/zstack/kvm/tpm/CloneVmTpmMsg.java | 35 +++ .../org/zstack/kvm/tpm/CloneVmTpmReply.java | 18 ++ .../DummyTpmEncryptedResourceKeyBackend.java | 17 ++ .../org/zstack/kvm/tpm/KvmTpmManager.java | 110 +++++++ .../tpm/TpmEncryptedResourceKeyBackend.java | 31 ++ .../test/resources/springConfigXml/Kvm.xml | 3 + 13 files changed, 560 insertions(+), 2 deletions(-) create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/efi/CloneVmHostFileMsg.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/efi/CloneVmHostFileReply.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/tpm/CloneVmTpmMsg.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/tpm/CloneVmTpmReply.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/tpm/DummyTpmEncryptedResourceKeyBackend.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmEncryptedResourceKeyBackend.java 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 19099f290a7..70ba4beced6 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 @@ -3,14 +3,19 @@ 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.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); @@ -40,4 +45,15 @@ public static boolean isUefiBootMode(String bootMode) { return Objects.equals(bootMode, ImageBootMode.UEFI.toString()) || Objects.equals(bootMode, ImageBootMode.UEFI_WITH_CSM.toString()); } + + public boolean needRegisterNvRam(String vmUuid) { + boolean tpmExists = Q.New(TpmVO.class) + .eq(TpmVO_.vmInstanceUuid, vmUuid) + .isExists(); + if (tpmExists) { + return true; + } + ResourceConfig resourceConfig = resourceConfigFacade.getResourceConfig(ENABLE_UEFI_SECURE_BOOT.getIdentity()); + return resourceConfig.getResourceConfigValue(vmUuid, Boolean.class); + } } diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index 88886397a1f..d41d947234f 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -262,6 +262,7 @@ + @@ -271,4 +272,6 @@ + + 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 e767df877f1..43748d46dc1 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceConstant.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceConstant.java @@ -6,6 +6,7 @@ @PythonClass public interface VmInstanceConstant { String SERVICE_ID = "vmInstance"; + String SECURE_BOOT_SERVICE_ID = "secureBoot"; String ACTION_CATEGORY = "instance"; @PythonClass String USER_VM_TYPE = "UserVm"; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/CloneVmHostFileMsg.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/CloneVmHostFileMsg.java new file mode 100644 index 00000000000..d28226995e8 --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/CloneVmHostFileMsg.java @@ -0,0 +1,35 @@ +package org.zstack.kvm.efi; + +import org.zstack.header.message.NeedReplyMessage; + +import java.util.List; + +public class CloneVmHostFileMsg extends NeedReplyMessage { + private String srcVmUuid; + private List dstVmUuidList; + private Boolean resetTpm; + + public String getSrcVmUuid() { + return srcVmUuid; + } + + public void setSrcVmUuid(String srcVmUuid) { + this.srcVmUuid = srcVmUuid; + } + + public List getDstVmUuidList() { + return dstVmUuidList; + } + + public void setDstVmUuidList(List dstVmUuidList) { + this.dstVmUuidList = dstVmUuidList; + } + + public Boolean getResetTpm() { + return resetTpm; + } + + public void setResetTpm(Boolean resetTpm) { + this.resetTpm = resetTpm; + } +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/CloneVmHostFileReply.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/CloneVmHostFileReply.java new file mode 100644 index 00000000000..b026831c503 --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/CloneVmHostFileReply.java @@ -0,0 +1,6 @@ +package org.zstack.kvm.efi; + +import org.zstack.header.message.MessageReply; + +public class CloneVmHostFileReply extends MessageReply { +} 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 83b8e8720c8..82b2ad202d6 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 @@ -264,6 +264,10 @@ public void success(KvmResponseWrapper wrapper) { content.setLastOpDate(now); databaseFacade.persist(content); } + + if (logger.isTraceEnabled()) { + logger.trace(String.format("persist/update VmHostFileContentVO [uuid=%s]", file.getUuid())); + } } if (errors.isEmpty()) { 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 e6fc30333cc..1febc137e09 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 @@ -2,34 +2,69 @@ import org.springframework.beans.factory.annotation.Autowired; import org.zstack.compute.legacy.ComputeLegacyGlobalProperty; +import org.zstack.core.Platform; +import org.zstack.core.asyncbatch.While; +import org.zstack.core.cloudbus.CloudBus; 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.core.db.SQLBatch; +import org.zstack.core.workflow.SimpleFlowChain; +import org.zstack.header.AbstractService; import org.zstack.header.core.Completion; +import org.zstack.header.core.WhileDoneCompletion; +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.errorcode.ErrorCodeList; +import org.zstack.header.exception.CloudRuntimeException; +import org.zstack.header.message.Message; +import org.zstack.header.tpm.entity.TpmVO; +import org.zstack.header.tpm.entity.TpmVO_; import org.zstack.header.vm.VmCanonicalEvents; +import org.zstack.header.vm.VmInstanceConstant; import org.zstack.header.vm.VmInstanceVO; import org.zstack.header.vm.VmInstanceVO_; +import org.zstack.header.vm.additions.VmHostBackupFileVO; +import org.zstack.header.vm.additions.VmHostBackupFileVO_; +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.resourceconfig.ResourceConfig; +import org.zstack.resourceconfig.ResourceConfigFacade; +import org.zstack.utils.DebugUtils; import org.zstack.utils.Utils; 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.HashMap; import java.util.List; import java.util.Map; +import static org.zstack.compute.vm.VmGlobalConfig.ENABLE_UEFI_SECURE_BOOT; +import static org.zstack.compute.vm.VmGlobalConfig.RESET_TPM_AFTER_VM_CLONE; +import static org.zstack.kvm.efi.KvmSecureBootExtensions.*; import static org.zstack.utils.CollectionDSL.list; import static org.zstack.utils.CollectionUtils.findOneOrNull; +import static org.zstack.utils.CollectionUtils.transform; -public class KvmSecureBootManager implements Component { +public class KvmSecureBootManager extends AbstractService { private static final CLogger logger = Utils.getLogger(KvmSecureBootManager.class); + @Autowired + private CloudBus bus; @Autowired private EventFacadeImpl eventFacade; @Autowired + private ResourceConfigFacade resourceConfigFacade; + @Autowired private KvmSecureBootExtensions secureBootExtensions; @Override @@ -101,4 +136,248 @@ public void fail(ErrorCode errorCode) { } }); } + + @Override + public String getId() { + return bus.makeLocalServiceId(VmInstanceConstant.SECURE_BOOT_SERVICE_ID); + } + + @Override + public void handleMessage(Message msg) { + if (msg instanceof CloneVmHostFileMsg) { + handle((CloneVmHostFileMsg) msg); + } else { + bus.dealWithUnknownMessage(msg); + } + } + + static class CloneVmHostFileContext { + List typesNeedClone = new ArrayList<>(); + List files = new ArrayList<>(); + List backupFiles = new ArrayList<>(); + List syncContexts = new ArrayList<>(); + } + + @SuppressWarnings("rawtypes") + private void handle(CloneVmHostFileMsg msg) { + CloneVmHostFileReply reply = new CloneVmHostFileReply(); + + boolean hasTpm = Q.New(TpmVO.class) + .eq(TpmVO_.vmInstanceUuid, msg.getSrcVmUuid()) + .isExists(); + ResourceConfig resourceConfig = resourceConfigFacade.getResourceConfig(ENABLE_UEFI_SECURE_BOOT.getIdentity()); + boolean secureBoot = resourceConfig.getResourceConfigValue(msg.getSrcVmUuid(), Boolean.class); + if (!hasTpm && !secureBoot) { + bus.reply(msg, reply); + return; + } + + CloneVmHostFileContext context = new CloneVmHostFileContext(); + context.typesNeedClone.add(VmHostFileType.NvRam); + if (hasTpm) { + boolean resetTpm; + if (msg.getResetTpm() == null) { + resourceConfig = resourceConfigFacade.getResourceConfig(RESET_TPM_AFTER_VM_CLONE.getIdentity()); + resetTpm = resourceConfig.getResourceConfigValue(msg.getSrcVmUuid(), Boolean.class); + } else { + resetTpm = msg.getResetTpm(); + } + if (!resetTpm) { + context.typesNeedClone.add(VmHostFileType.TpmState); + } + } + logger.debug(String.format("clone VM[uuid=%s] host files for types: %s", msg.getSrcVmUuid(), context.typesNeedClone)); + + SimpleFlowChain chain = new SimpleFlowChain(); + chain.setName("clone-vm-host-file"); + chain.then(new NoRollbackFlow() { + String __name__ = "prepare-sync-vm-host-file-context-list"; + + @Override + public void run(FlowTrigger trigger, Map data) { + for (VmHostFileType type : context.typesNeedClone) { + VmHostFileVO file = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, msg.getSrcVmUuid()) + .eq(VmHostFileVO_.type, type) + .orderByDesc(VmHostFileVO_.lastOpDate) + .limit(1) + .find(); + if (file == null) { + logger.debug(String.format("skip to read/write %s host file for VM[vmUuid=%s]: file is not registered in MN", + type, msg.getSrcVmUuid())); + continue; + } + context.files.add(file); + } + + if (context.files.isEmpty()) { + trigger.next(); + return; + } + + Map contextMap = new HashMap<>(); + for (VmHostFileVO file : context.files) { + contextMap.computeIfAbsent(file.getHostUuid(), hostUuid -> { + SyncVmHostFilesFromHostContext syncContext = new SyncVmHostFilesFromHostContext(); + syncContext.hostUuid = hostUuid; + syncContext.vmUuid = msg.getSrcVmUuid(); + return syncContext; + }); + } + context.syncContexts.addAll(contextMap.values()); + + for (VmHostFileVO file : context.files) { + SyncVmHostFilesFromHostContext syncContext = contextMap.get(file.getHostUuid()); + if (file.getType() == VmHostFileType.NvRam) { + syncContext.nvRamPath = file.getPath(); + } else if (file.getType() == VmHostFileType.TpmState) { + syncContext.tpmStateFolder = file.getPath(); + } else { + throw new CloudRuntimeException("unsupported vm host file type: " + file.getType()); + } + } + + trigger.next(); + } + }).then(new NoRollbackFlow() { + String __name__ = "read-vm-host-file-from-origin-host"; + + @Override + public boolean skip(Map data) { + return context.syncContexts.isEmpty(); + } + + @Override + public void run(FlowTrigger trigger, Map data) { + new While<>(context.syncContexts).each((syncContext, whileContext) -> + secureBootExtensions.syncVmHostFilesFromHost(syncContext, new Completion(whileContext) { + @Override + public void success() { + whileContext.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + whileContext.addError(errorCode); + whileContext.done(); + } + }) + ).run(new WhileDoneCompletion(trigger) { + @Override + public void done(ErrorCodeList errorCodeList) { + if (!errorCodeList.isEmpty()) { + logger.warn(String.format("failed to sync host file for VM[uuid=%s] but still continue:\n%s", + msg.getSrcVmUuid(), + String.join("\n", transform(errorCodeList.getCauses(), ErrorCode::getReadableDetails)))); + } + trigger.next(); + } + }); + } + }).then(new NoRollbackFlow() { + String __name__ = "determine-content-uuid"; + + @Override + public void run(FlowTrigger trigger, Map data) { + List missingTypes = new ArrayList<>(context.typesNeedClone); + missingTypes.removeAll(transform(context.files, VmHostFileVO::getType)); + if (missingTypes.isEmpty()) { + trigger.next(); + return; + } + + context.backupFiles.addAll(Q.New(VmHostBackupFileVO.class) + .eq(VmHostBackupFileVO_.vmInstanceUuid, msg.getSrcVmUuid()) + .in(VmHostFileVO_.type, missingTypes) + .list()); + trigger.next(); + } + }).then(new NoRollbackFlow() { + String __name__ = "copy-host-content-database"; + + @Override + public boolean skip(Map data) { + return context.files.isEmpty() && context.backupFiles.isEmpty(); + } + + @Override + public void run(FlowTrigger trigger, Map data) { + List uuidList = transform(context.files, VmHostFileVO::getUuid); + List filesAfterSyncing = Q.New(VmHostFileVO.class) + .in(VmHostFileVO_.uuid, uuidList) + .list(); + uuidList.addAll(transform(context.backupFiles, VmHostBackupFileVO::getUuid)); + List contents = Q.New(VmHostFileContentVO.class) + .in(VmHostFileContentVO_.uuid, uuidList) + .list(); + + List filesNeedPersists = new ArrayList<>(); + List contentsNeedPersists = new ArrayList<>(); + + Timestamp now = Timestamp.from(Instant.now()); + for (String vmUuid : msg.getDstVmUuidList()) { + for (String uuid : uuidList) { + VmHostFileContentVO srcContent = findOneOrNull(contents, + item -> item.getUuid().equals(uuid)); + if (srcContent == null) { + continue; + } + + VmHostFileVO vmHostFile = findOneOrNull(filesAfterSyncing, + item -> item.getUuid().equals(uuid)); + VmHostBackupFileVO vmHostBackupFile = vmHostFile == null ? + findOneOrNull(context.backupFiles, item -> item.getUuid().equals(uuid)) : null; + DebugUtils.Assert(vmHostFile != null || vmHostBackupFile != null, + "vmHostFile or vmHostBackupFile cannot be null"); + + VmHostBackupFileVO file = new VmHostBackupFileVO(); + file.setUuid(Platform.getUuid()); + file.setVmInstanceUuid(vmUuid); + file.setType(vmHostFile == null ? vmHostBackupFile.getType() : vmHostFile.getType()); + file.setCreateDate(now); + file.setLastOpDate(now); + filesNeedPersists.add(file); + + VmHostFileContentVO content = new VmHostFileContentVO(); + content.setUuid(file.getUuid()); + content.setContent(srcContent.getContent()); + content.setFormat(srcContent.getFormat()); + content.setCreateDate(now); + content.setLastOpDate(now); + contentsNeedPersists.add(content); + } + } + + if (logger.isTraceEnabled()) { + logger.trace(String.format("persist VmHostFileContentVO [uuid=%s]", + transform(contentsNeedPersists, VmHostFileContentVO::getUuid))); + } + + new SQLBatch() { + @Override + protected void scripts() { + if (!filesNeedPersists.isEmpty()) { + databaseFacade.persistCollection(filesNeedPersists); + } + if (!contentsNeedPersists.isEmpty()) { + databaseFacade.persistCollection(contentsNeedPersists); + } + } + }.execute(); + + trigger.next(); + } + }).done(new FlowDoneHandler(msg) { + @Override + public void handle(Map data) { + bus.reply(msg, reply); + } + }).error(new FlowErrorHandler(msg) { + @Override + public void handle(ErrorCode errCode, Map data) { + reply.setError(errCode); + bus.reply(msg, reply); + } + }).start(); + } } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/CloneVmTpmMsg.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/CloneVmTpmMsg.java new file mode 100644 index 00000000000..69f0d6e5e16 --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/CloneVmTpmMsg.java @@ -0,0 +1,35 @@ +package org.zstack.kvm.tpm; + +import org.zstack.header.message.NeedReplyMessage; + +import java.util.List; + +public class CloneVmTpmMsg extends NeedReplyMessage { + private String srcVmUuid; + private List dstVmUuidList; + private Boolean resetTpm; + + public String getSrcVmUuid() { + return srcVmUuid; + } + + public void setSrcVmUuid(String srcVmUuid) { + this.srcVmUuid = srcVmUuid; + } + + public List getDstVmUuidList() { + return dstVmUuidList; + } + + public void setDstVmUuidList(List dstVmUuidList) { + this.dstVmUuidList = dstVmUuidList; + } + + public Boolean getResetTpm() { + return resetTpm; + } + + public void setResetTpm(Boolean resetTpm) { + this.resetTpm = resetTpm; + } +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/CloneVmTpmReply.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/CloneVmTpmReply.java new file mode 100644 index 00000000000..b2ad460b8f2 --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/CloneVmTpmReply.java @@ -0,0 +1,18 @@ +package org.zstack.kvm.tpm; + +import org.zstack.header.message.MessageReply; +import org.zstack.header.tpm.entity.TpmInventory; + +import java.util.List; + +public class CloneVmTpmReply extends MessageReply { + private List inventories; + + public List getInventories() { + return inventories; + } + + public void setInventories(List inventories) { + this.inventories = inventories; + } +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/DummyTpmEncryptedResourceKeyBackend.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/DummyTpmEncryptedResourceKeyBackend.java new file mode 100644 index 00000000000..a2ada84b8ae --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/DummyTpmEncryptedResourceKeyBackend.java @@ -0,0 +1,17 @@ +package org.zstack.kvm.tpm; + +import org.zstack.header.core.Completion; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +public class DummyTpmEncryptedResourceKeyBackend implements TpmEncryptedResourceKeyBackend { + private static final CLogger logger = Utils.getLogger(DummyTpmEncryptedResourceKeyBackend.class); + + @Override + public void cloneEncryptedResourceKey(CloneEncryptedResourceKeyContext context, Completion completion) { + // do nothing + logger.debug("ignore clone encrypted resource key request for TPM uuid " + + context.srcTpmUuid + " -> " + context.dstTpmUuid); + completion.success(); + } +} 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 4eb436a4b72..781faf4c186 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 @@ -2,6 +2,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.zstack.compute.vm.devices.VmTpmManager; +import org.zstack.core.asyncbatch.While; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.CloudBusCallBack; import org.zstack.core.cloudbus.MessageSafe; @@ -13,11 +14,15 @@ import org.zstack.core.workflow.SimpleFlowChain; import org.zstack.header.AbstractService; import org.zstack.header.core.Completion; +import org.zstack.header.core.WhileDoneCompletion; +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.errorcode.ErrorCodeList; import org.zstack.header.message.APIMessage; import org.zstack.header.message.Message; import org.zstack.header.message.MessageReply; @@ -44,14 +49,17 @@ import org.zstack.header.vm.additions.VmHostFileVO_; import org.zstack.resourceconfig.ResourceConfig; import org.zstack.resourceconfig.ResourceConfigFacade; +import org.zstack.utils.CollectionUtils; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import java.util.ArrayList; import java.util.List; 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.core.Platform.operr; 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; @@ -60,6 +68,7 @@ import static org.zstack.kvm.KVMSystemTags.SWTPM_VERSION_TOKEN; import static org.zstack.kvm.KVMSystemTags.VM_EDK; import static org.zstack.utils.CollectionDSL.list; +import static org.zstack.utils.CollectionUtils.transform; public class KvmTpmManager extends AbstractService { private static final CLogger logger = Utils.getLogger(KvmTpmManager.class); @@ -72,6 +81,8 @@ public class KvmTpmManager extends AbstractService { private ResourceConfigFacade resourceConfigFacade; @Autowired private VmTpmManager vmTpmManager; + @Autowired + private TpmEncryptedResourceKeyBackend tpmKeyBackend; @Override public boolean start() { @@ -106,6 +117,8 @@ private void handleLocalMessage(Message msg) { handle((AddTpmMsg) msg); } else if (msg instanceof RemoveTpmMsg) { handle((RemoveTpmMsg) msg); + } else if (msg instanceof CloneVmTpmMsg) { + handle((CloneVmTpmMsg) msg); } else { bus.dealWithUnknownMessage(msg); } @@ -309,6 +322,103 @@ public void handle(ErrorCode errorCode, Map data) { }).start(); } + @SuppressWarnings("rawtypes") + private void handle(CloneVmTpmMsg msg) { + CloneVmTpmReply reply = new CloneVmTpmReply(); + + String originTpmUuid = Q.New(TpmVO.class) + .eq(TpmVO_.vmInstanceUuid, msg.getSrcVmUuid()) + .select(TpmVO_.uuid) + .findValue(); + if (originTpmUuid == null) { + bus.reply(msg, reply); + return; + } + + SimpleFlowChain chain = new SimpleFlowChain(); + chain.setName("clone-VM-TPM"); + chain.then(new Flow() { + String __name__ = "persist-TPM-VO"; + + @Override + public void run(FlowTrigger trigger, Map data) { + reply.setInventories(new ArrayList<>()); + for (String dstVmUuid : msg.getDstVmUuidList()) { + TpmVO dstTpm = vmTpmManager.persistTpmVO(null, dstVmUuid); + reply.getInventories().add(TpmInventory.valueOf(dstTpm)); + } + trigger.next(); + } + + @Override + public void rollback(FlowRollback trigger, Map data) { + if (CollectionUtils.isEmpty(reply.getInventories())) { + trigger.rollback(); + return; + } + SQL.New(TpmVO.class) + .in(TpmVO_.uuid, transform(reply.getInventories(), TpmInventory::getUuid)) + .delete(); + trigger.rollback(); + } + }).then(new NoRollbackFlow() { + String __name__ = "clone-encrypted-resource-key-if-needed"; + + @Override + public void run(FlowTrigger trigger, Map data) { + boolean resetTpm; + if (msg.getResetTpm() == null) { + ResourceConfig resourceConfig = resourceConfigFacade.getResourceConfig(RESET_TPM_AFTER_VM_CLONE.getIdentity()); + resetTpm = resourceConfig.getResourceConfigValue(msg.getSrcVmUuid(), Boolean.class); + } else { + resetTpm = msg.getResetTpm(); + } + + new While<>(reply.getInventories()).each((inventory, whileCompletion) -> { + TpmEncryptedResourceKeyBackend.CloneEncryptedResourceKeyContext context = + new TpmEncryptedResourceKeyBackend.CloneEncryptedResourceKeyContext(); + context.srcTpmUuid = originTpmUuid; + context.dstTpmUuid = inventory.getUuid(); + context.resetTpm = resetTpm; + tpmKeyBackend.cloneEncryptedResourceKey(context, new Completion(whileCompletion) { + @Override + public void success() { + whileCompletion.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + whileCompletion.addError(errorCode); + whileCompletion.allDone(); + } + }); + }).run(new WhileDoneCompletion(trigger) { + @Override + public void done(ErrorCodeList errorCodeList) { + if (errorCodeList.isEmpty()) { + trigger.next(); + return; + } + trigger.fail(operr("Failed to clone encrypted resource key") + .withOpaque("src.tpm.uuid", originTpmUuid) + .withCause(errorCodeList)); + } + }); + } + }).done(new FlowDoneHandler(msg) { + @Override + public void handle(Map data) { + bus.reply(msg, reply); + } + }).error(new FlowErrorHandler(msg) { + @Override + public void handle(ErrorCode errCode, Map data) { + reply.setError(errCode); + bus.reply(msg, reply); + } + }).start(); + } + private void handle(APIGetTpmCapabilityMsg msg) { TpmCapabilityView view = new TpmCapabilityView(); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmEncryptedResourceKeyBackend.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmEncryptedResourceKeyBackend.java new file mode 100644 index 00000000000..4b34eb157fc --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmEncryptedResourceKeyBackend.java @@ -0,0 +1,31 @@ +package org.zstack.kvm.tpm; + +import org.zstack.header.core.Completion; + +/** + * Responsible for handling the replication or reset of encryption resource keys + * and other tasks in VM TPM cloning scenarios. + */ +public interface TpmEncryptedResourceKeyBackend { + static class CloneEncryptedResourceKeyContext { + public String srcTpmUuid; + public String dstTpmUuid; + + /** + * Whether to reset (regenerate) the key on the target TPM. + *
    + *
  • {@code true}:Regenerate the key for the target TPM + * without inheriting the encrypted data from the source TPM.
  • + *
  • {@code false}:Copy the existing keys from the source TPM + * to the target TPM to ensure they remain consistent.
  • + *
+ */ + public boolean resetTpm; + } + + /** + * In a VM cloning scenario, copy or reset the encryption resource key + * from the source TPM to the target TPM. + */ + void cloneEncryptedResourceKey(CloneEncryptedResourceKeyContext context, Completion completion); +} diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index 53cf63d951b..a1b8520ffca 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -261,6 +261,7 @@ + @@ -270,4 +271,6 @@
+ + From 29f4ad8731efb1938ad5470e7a00c3354689a02e Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 12 Mar 2026 16:35:10 +0800 Subject: [PATCH 54/64] [kvm]: support to start VM with VmHostBackupFileVO Resolves: ZSV-11439 Related: ZSV-11310 Change-Id: I626c6775707a67657570736778766d6b75736570 --- .../kvm/efi/KvmSecureBootExtensions.java | 57 +++++++++++++++---- 1 file changed, 46 insertions(+), 11 deletions(-) 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 82b2ad202d6..8da8b88b333 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 @@ -28,6 +28,8 @@ import org.zstack.header.vm.PreVmInstantiateResourceExtensionPoint; import org.zstack.header.vm.VmInstanceSpec; import org.zstack.header.vm.VmInstantiateResourceException; +import org.zstack.header.vm.additions.VmHostBackupFileVO; +import org.zstack.header.vm.additions.VmHostBackupFileVO_; import org.zstack.header.vm.additions.VmHostFileContentFormat; import org.zstack.header.vm.additions.VmHostFileContentVO; import org.zstack.header.vm.additions.VmHostFileContentVO_; @@ -414,11 +416,15 @@ public static class PrepareHostFileContext { public String vmUuid; public VmHostFileType type; + public String path; // whether the NvRam is on the same host as before private boolean sameHost = false; - private boolean firstReadSuccess = false; private boolean writeSuccess = false; private VmHostFileVO vmHostFile; + private VmHostBackupFileVO vmBackupFileVO; + + // property: VmHostFileVO (read success) > VmHostFileVO (read fail) > VmHostBackupFileVO + // Note: read VmHostBackupFileVO only if VmHostFileVO is not exist } @SuppressWarnings("rawtypes") @@ -430,7 +436,7 @@ public void prepareHostFileOnHost(PrepareHostFileContext context, Completion com @Override public void run(FlowTrigger trigger, Map data) { - VmHostFileVO vmHostFile = context.vmHostFile = Q.New(VmHostFileVO.class) + VmHostFileVO vmHostFile = Q.New(VmHostFileVO.class) .eq(VmHostFileVO_.type, context.type) .eq(VmHostFileVO_.vmInstanceUuid, context.vmUuid) .orderByDesc(VmHostFileVO_.lastOpDate) @@ -449,9 +455,9 @@ public void run(FlowTrigger trigger, Map data) { syncContext.vmUuid = context.vmUuid; if (vmHostFile.getType() == VmHostFileType.NvRam) { - syncContext.nvRamPath = vmHostFile.getPath(); + context.path = syncContext.nvRamPath = vmHostFile.getPath(); } else if (vmHostFile.getType() == VmHostFileType.TpmState) { - syncContext.tpmStateFolder = vmHostFile.getPath(); + context.path = syncContext.tpmStateFolder = vmHostFile.getPath(); } else { throw new CloudRuntimeException("unsupported vm host file type: " + vmHostFile.getType()); } @@ -459,7 +465,7 @@ public void run(FlowTrigger trigger, Map data) { syncVmHostFilesFromHost(syncContext, new Completion(trigger) { @Override public void success() { - context.firstReadSuccess = true; + context.vmHostFile = vmHostFile; trigger.next(); } @@ -471,18 +477,47 @@ public void fail(ErrorCode errorCode) { } }); } + }).then(new NoRollbackFlow() { + String __name__ = "read-vm-host-file-from-backup"; + + @Override + public boolean skip(Map data) { + return context.vmHostFile != null; + } + + @Override + public void run(FlowTrigger trigger, Map data) { + context.vmBackupFileVO = Q.New(VmHostBackupFileVO.class) + .eq(VmHostBackupFileVO_.type, context.type) + .eq(VmHostBackupFileVO_.vmInstanceUuid, context.vmUuid) + .orderByDesc(VmHostBackupFileVO_.lastOpDate) + .limit(1) + .find(); + if (context.vmBackupFileVO != null) { + logger.debug(String.format("use %s[type=%s] VM-host backup file for VM[uuid=%s]", + context.vmBackupFileVO.getUuid(), context.type, context.vmUuid)); + switch (context.type) { + case NvRam: context.path = buildNvramFilePath(context.vmUuid); break; + case TpmState: context.path = buildTpmStateFilePath(context.vmUuid); break; + } + } + trigger.next(); + } }).then(new NoRollbackFlow() { String __name__ = "write-vm-host-file-to-dest-host"; @Override public boolean skip(Map data) { - return context.vmHostFile == null || (context.sameHost && context.firstReadSuccess); + return (context.vmHostFile == null && context.vmBackupFileVO == null) + || (context.sameHost && context.vmHostFile != null); } @Override public void run(FlowTrigger trigger, Map data) { + String contentUuid = context.vmHostFile == null ? + context.vmBackupFileVO.getUuid() : context.vmHostFile.getUuid(); VmHostFileContentVO content = Q.New(VmHostFileContentVO.class) - .eq(VmHostFileContentVO_.uuid, context.vmHostFile.getUuid()) + .eq(VmHostFileContentVO_.uuid, contentUuid) .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", @@ -492,8 +527,8 @@ public void run(FlowTrigger trigger, Map data) { } VmHostFileTO to = new VmHostFileTO(); - to.setPath(context.vmHostFile.getPath()); - to.setType(context.vmHostFile.getType().toString()); + to.setPath(context.path); + to.setType(context.type.toString()); to.setFileFormat(content.getFormat().toString()); String contentBase64 = Base64.getEncoder().encodeToString(content.getContent()); @@ -532,9 +567,9 @@ public void run(FlowTrigger trigger, Map data) { syncBackContext.vmUuid = context.vmUuid; if (context.type == VmHostFileType.NvRam) { - syncBackContext.nvRamPath = context.vmHostFile.getPath(); + syncBackContext.nvRamPath = context.path; } else if (context.type == VmHostFileType.TpmState) { - syncBackContext.tpmStateFolder = context.vmHostFile.getPath(); + syncBackContext.tpmStateFolder = context.path; } syncVmHostFilesFromHost(syncBackContext, new Completion(trigger) { From 0c30218dc96a4c5fdaf7c6f329cface9a20f917a Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 12 Mar 2026 21:48:35 +0800 Subject: [PATCH 55/64] [kvm]: clean VmHostFileVO after VM destroy Resolves: ZSV-11439 Related: ZSV-11310 Change-Id: I68696b7776656f78677679686370707a70616665 --- .../zstack/compute/vm/VmCascadeExtension.java | 6 ++-- conf/springConfigXml/Kvm.xml | 1 + .../kvm/efi/KvmSecureBootExtensions.java | 31 ++++++++++++++++++- .../test/resources/springConfigXml/Kvm.xml | 1 + 4 files changed, 35 insertions(+), 4 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 05810e8bf53..dabbc36a653 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmCascadeExtension.java @@ -472,9 +472,9 @@ public void done(ErrorCodeList errorCodeList) { .flatMap(List::stream).map(VmCdRomInventory::getUuid) .collect(Collectors.toList()); dbf.removeByPrimaryKeys(cdRomUuids, VmCdRomVO.class); - dbf.removeByPrimaryKeys(vminvs.stream().map(p -> p.getInventory().getUuid()) - .collect(Collectors.toList()), - VmInstanceVO.class); + + List vmUuidList = transform(vminvs, p -> p.getInventory().getUuid()); + dbf.removeByPrimaryKeys(vmUuidList, VmInstanceVO.class); } completion.success(); diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index d41d947234f..7d4e03f86b2 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -270,6 +270,7 @@ + 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 8da8b88b333..9b747956163 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 @@ -26,6 +26,8 @@ import org.zstack.header.message.MessageReply; import org.zstack.header.vm.DiskAO; import org.zstack.header.vm.PreVmInstantiateResourceExtensionPoint; +import org.zstack.header.vm.VmInstanceDestroyExtensionPoint; +import org.zstack.header.vm.VmInstanceInventory; import org.zstack.header.vm.VmInstanceSpec; import org.zstack.header.vm.VmInstantiateResourceException; import org.zstack.header.vm.additions.VmHostBackupFileVO; @@ -76,7 +78,8 @@ import static org.zstack.utils.CollectionUtils.transform; public class KvmSecureBootExtensions implements KVMStartVmExtensionPoint, - PreVmInstantiateResourceExtensionPoint { + PreVmInstantiateResourceExtensionPoint, + VmInstanceDestroyExtensionPoint { private static final CLogger logger = Utils.getLogger(KvmSecureBootExtensions.class); @Autowired @@ -750,4 +753,30 @@ public void run(MessageReply reply) { } }); } + + @Override + public String preDestroyVm(VmInstanceInventory inv) { + return null; + } + + @Override + public void beforeDestroyVm(VmInstanceInventory inv) { + // do-nothing + } + + @Override + public void afterDestroyVm(VmInstanceInventory inv) { + String vmUuid = inv.getUuid(); + SQL.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) + .delete(); + SQL.New(VmHostBackupFileVO.class) + .eq(VmHostBackupFileVO_.vmInstanceUuid, vmUuid) + .delete(); + } + + @Override + public void failedToDestroyVm(VmInstanceInventory inv, ErrorCode reason) { + // do-nothing + } } diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index a1b8520ffca..730e9c79cae 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -269,6 +269,7 @@ +
From 10722a0fe34d3aaad01b02b807fb8adef858fe96 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Mon, 16 Mar 2026 13:37:08 +0800 Subject: [PATCH 56/64] [header]: fix reference error in VmHostFileContentVO Related: ZSV-11439 Related: ZSV-11310 Change-Id: I65616264756468686477647a70716f72626f6365 --- .../header/vm/additions/VmHostFileContentVO.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) 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 index 4995d5b0f38..e08e5a41d70 100644 --- a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileContentVO.java +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileContentVO.java @@ -1,7 +1,7 @@ package org.zstack.header.vm.additions; -import org.zstack.header.vo.EntityGraph; import org.zstack.header.vo.ForeignKey; +import org.zstack.header.vo.ResourceVO; import org.zstack.header.vo.SoftDeletionCascade; import org.zstack.header.vo.SoftDeletionCascades; @@ -19,17 +19,12 @@ @Entity @Table @SoftDeletionCascades({ - @SoftDeletionCascade(parent = VmHostFileVO.class, joinColumn = "uuid"), + @SoftDeletionCascade(parent = ResourceVO.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) + @ForeignKey(parentEntityClass = ResourceVO.class, onDeleteAction = ForeignKey.ReferenceOption.CASCADE) private String uuid; @Column private byte[] content; From 7e5ecd6f69b96a7944de8afe5c3b8a19365b222b Mon Sep 17 00:00:00 2001 From: "tao.yang" Date: Fri, 13 Mar 2026 18:41:11 +0800 Subject: [PATCH 57/64] [keyProvider]: reset default key provider after delete DBImpact Resolves: ZSV-11473 Change-Id: I676766646c6568687472776f6c776c6f72777279 --- conf/db/zsv/V5.0.0__schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/db/zsv/V5.0.0__schema.sql b/conf/db/zsv/V5.0.0__schema.sql index 64eb64b56b0..69ff903049c 100644 --- a/conf/db/zsv/V5.0.0__schema.sql +++ b/conf/db/zsv/V5.0.0__schema.sql @@ -122,7 +122,7 @@ CREATE TABLE IF NOT EXISTS `zstack`.`EncryptedResourceKeyRefVO` ( `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59', PRIMARY KEY (`id`), - INDEX `idxEncryptedResourceKeyRefVOResource` (`resourceUuid`, `resourceType`), + INDEX `idxEncryptedResourceKeyRefVOResource` (`resourceType`, `resourceUuid`), INDEX `idxEncryptedResourceKeyRefVOProviderUuid` (`providerUuid`), INDEX `idxEncryptedResourceKeyRefVOProviderName` (`providerName`), CONSTRAINT `fkEncryptedResourceKeyRefVOProviderUuid` FOREIGN KEY (`providerUuid`) REFERENCES `KeyProviderVO` (`uuid`) ON DELETE SET NULL From 504ff7567bb334af08d29adae623875087ff8699 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Mon, 16 Mar 2026 17:52:26 +0800 Subject: [PATCH 58/64] [compute]: persist resource key when creating VM * A new hook (afterRollbackPersistVmInstanceVO) has been added to the VM rollback path * introduce the TPM key provider management interface and implementation (attach/detach/find) have been introduced and extended Resolves: ZSV-11489 Related: ZSV-11310 Change-Id: I73646c617971626d77726d6f646467746f637075 --- .../compute/vm/VmInstanceManagerImpl.java | 16 ++- .../DummyTpmEncryptedResourceKeyBackend.java | 18 ++- .../TpmEncryptedResourceKeyBackend.java | 20 +++- .../compute/vm/devices/VmTpmExtensions.java | 46 +++++++- .../compute/vm/devices/VmTpmManager.java | 4 + conf/errorCodes/keyProvider.xml | 108 ++++++++++++++++++ conf/springConfigXml/Kvm.xml | 2 - conf/springConfigXml/VmInstanceManager.xml | 28 ++--- .../vm/VmInstanceCreateExtensionPoint.java | 7 ++ .../org/zstack/kvm/tpm/KvmTpmManager.java | 1 + .../test/resources/springConfigXml/Kvm.xml | 2 - 11 files changed, 223 insertions(+), 29 deletions(-) rename {plugin/kvm/src/main/java/org/zstack/kvm/tpm => compute/src/main/java/org/zstack/compute/vm/devices}/DummyTpmEncryptedResourceKeyBackend.java (53%) rename {plugin/kvm/src/main/java/org/zstack/kvm/tpm => compute/src/main/java/org/zstack/compute/vm/devices}/TpmEncryptedResourceKeyBackend.java (64%) create mode 100644 conf/errorCodes/keyProvider.xml 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 d3a6d6396b2..418cd0fbd5b 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceManagerImpl.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceManagerImpl.java @@ -1214,16 +1214,22 @@ public void setup() { flow(new Flow() { String __name__ = "call-after-persist-vm-extensions"; + List done = new ArrayList<>(); + @Override public void run(FlowTrigger trigger, Map data) { - pluginRgty.getExtensionList(VmInstanceCreateExtensionPoint.class).forEach( - extensionPoint -> extensionPoint.afterPersistVmInstanceVO(finalVo, msg)); + for (VmInstanceCreateExtensionPoint extension : pluginRgty.getExtensionList(VmInstanceCreateExtensionPoint.class)) { + done.add(extension); + extension.afterPersistVmInstanceVO(finalVo, msg); + } trigger.next(); } @Override public void rollback(FlowRollback trigger, Map data) { - // do nothing + Collections.reverse(done); + CollectionUtils.safeForEach(done, + extension -> extension.afterRollbackPersistVmInstanceVO(finalVo, msg)); trigger.rollback(); } }); @@ -1315,7 +1321,7 @@ public void run(FlowTrigger trigger, Map data) { smsg.setRootDiskOfferingUuid(rootDisk.getDiskOfferingUuid()); } else if (rootDisk.getSize() > 0) { dvo = new DiskOfferingVO(); - dvo.setUuid(Platform.getUuid()); + dvo.setUuid(getUuid()); dvo.setAccountUuid(msg.getAccountUuid()); dvo.setDiskSize(rootDisk.getSize()); dvo.setName("for-create-vm-" + finalVo.getUuid()); @@ -1381,7 +1387,7 @@ public void rollback(FlowRollback chain, Map data) { } DestroyVmInstanceMsg dmsg = new DestroyVmInstanceMsg(); dmsg.setVmInstanceUuid(finalVo.getUuid()); - dmsg.setDeletionPolicy(VmInstanceDeletionPolicyManager.VmInstanceDeletionPolicy.Direct); + dmsg.setDeletionPolicy(VmInstanceDeletionPolicy.Direct); bus.makeTargetServiceIdByResourceUuid(dmsg, VmInstanceConstant.SERVICE_ID, finalVo.getUuid()); bus.send(dmsg, new CloudBusCallBack(null) { @Override diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/DummyTpmEncryptedResourceKeyBackend.java b/compute/src/main/java/org/zstack/compute/vm/devices/DummyTpmEncryptedResourceKeyBackend.java similarity index 53% rename from plugin/kvm/src/main/java/org/zstack/kvm/tpm/DummyTpmEncryptedResourceKeyBackend.java rename to compute/src/main/java/org/zstack/compute/vm/devices/DummyTpmEncryptedResourceKeyBackend.java index a2ada84b8ae..911a78809a4 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/DummyTpmEncryptedResourceKeyBackend.java +++ b/compute/src/main/java/org/zstack/compute/vm/devices/DummyTpmEncryptedResourceKeyBackend.java @@ -1,4 +1,4 @@ -package org.zstack.kvm.tpm; +package org.zstack.compute.vm.devices; import org.zstack.header.core.Completion; import org.zstack.utils.Utils; @@ -7,6 +7,22 @@ public class DummyTpmEncryptedResourceKeyBackend implements TpmEncryptedResourceKeyBackend { private static final CLogger logger = Utils.getLogger(DummyTpmEncryptedResourceKeyBackend.class); + @Override + public void attachKeyProviderToTpm(String tpmUuid, String keyProviderUuid) { + logger.debug("ignore attach key provider to TPM request for TPM uuid " + tpmUuid + + " and key provider uuid " + keyProviderUuid); + } + + @Override + public void detachKeyProviderFromTpm(String tpmUuid) { + logger.debug("ignore detach key provider from TPM request for TPM uuid " + tpmUuid); + } + + @Override + public String findKeyProviderUuidByTpm(String tpmUuid) { + return null; + } + @Override public void cloneEncryptedResourceKey(CloneEncryptedResourceKeyContext context, Completion completion) { // do nothing diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmEncryptedResourceKeyBackend.java b/compute/src/main/java/org/zstack/compute/vm/devices/TpmEncryptedResourceKeyBackend.java similarity index 64% rename from plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmEncryptedResourceKeyBackend.java rename to compute/src/main/java/org/zstack/compute/vm/devices/TpmEncryptedResourceKeyBackend.java index 4b34eb157fc..cc043bd36ae 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmEncryptedResourceKeyBackend.java +++ b/compute/src/main/java/org/zstack/compute/vm/devices/TpmEncryptedResourceKeyBackend.java @@ -1,4 +1,4 @@ -package org.zstack.kvm.tpm; +package org.zstack.compute.vm.devices; import org.zstack.header.core.Completion; @@ -7,6 +7,24 @@ * and other tasks in VM TPM cloning scenarios. */ public interface TpmEncryptedResourceKeyBackend { + + /** + * Build relationship from {@link org.zstack.header.tpm.entity.TpmVO} to EncryptedResourceKeyRefVO + * Non-async call. + */ + void attachKeyProviderToTpm(String tpmUuid, String keyProviderUuid); + + /** + * Clean relationship from {@link org.zstack.header.tpm.entity.TpmVO} to EncryptedResourceKeyRefVO + * Non-async call. + */ + void detachKeyProviderFromTpm(String tpmUuid); + + /** + * maybe null (when crypto module is not installed) + */ + String findKeyProviderUuidByTpm(String tpmUuid); + static class CloneEncryptedResourceKeyContext { public String srcTpmUuid; public String dstTpmUuid; 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 39ce6fcb840..775fa9d6f16 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 @@ -4,6 +4,7 @@ import org.zstack.compute.vm.BuildVmSpecExtensionPoint; import org.zstack.compute.vm.VmSystemTags; import org.zstack.core.db.Q; +import org.zstack.core.db.SQLBatch; import org.zstack.header.tpm.entity.TpmSpec; import org.zstack.header.tpm.entity.TpmVO; import org.zstack.header.tpm.entity.TpmVO_; @@ -25,6 +26,8 @@ public class VmTpmExtensions implements VmInstanceCreateExtensionPoint, private VmTpmManager vmTpmManager; @Autowired private ResourceConfigFacade resourceConfigFacade; + @Autowired + private TpmEncryptedResourceKeyBackend resourceKeyBackend; @Override public void preCreateVmInstance(CreateVmInstanceMsg msg) { @@ -38,17 +41,49 @@ public void afterPersistVmInstanceVO(VmInstanceVO vo, CreateVmInstanceMsg msg) { return; } - vmTpmManager.persistTpmVO(null, vo.getUuid()); + new SQLBatch() { + @Override + protected void scripts() { + final TpmVO tpm = vmTpmManager.persistTpmVO(null, vo.getUuid()); + final String keyProviderUuid = spec.getTpm().getKeyProviderUuid(); + if (keyProviderUuid != null) { + resourceKeyBackend.attachKeyProviderToTpm(tpm.getUuid(), keyProviderUuid); + } + } + }.execute(); + } + + @Override + public void afterRollbackPersistVmInstanceVO(VmInstanceVO vo, CreateVmInstanceMsg msg) { + String tpmUuid = Q.New(TpmVO.class) + .eq(TpmVO_.vmInstanceUuid, vo.getUuid()) + .select(TpmVO_.uuid) + .findValue(); + if (tpmUuid == null) { + return; + } + + new SQLBatch() { + @Override + protected void scripts() { + try { + resourceKeyBackend.detachKeyProviderFromTpm(tpmUuid); + } finally { + vmTpmManager.deleteTpmVO(tpmUuid); + } + } + }.execute(); } @Override public void afterBuildVmSpec(VmInstanceSpec spec) { String vmUuid = spec.getVmInventory().getUuid(); - boolean tpmExists = Q.New(TpmVO.class) + String tpmUuid = Q.New(TpmVO.class) .eq(TpmVO_.vmInstanceUuid, vmUuid) - .isExists(); - boolean needRegisterNvRam = tpmExists; + .select(TpmVO_.uuid) + .findValue(); + boolean needRegisterNvRam = tpmUuid != null; if (!needRegisterNvRam) { String bootMode = VmSystemTags.BOOT_MODE.getTokenByResourceUuid(vmUuid, VmSystemTags.BOOT_MODE_TOKEN); if (vmTpmManager.isUefiBootMode(bootMode)) { @@ -64,12 +99,13 @@ public void afterBuildVmSpec(VmInstanceSpec spec) { spec.setNvRamSpec(nvRamSpec); } - if (tpmExists && (spec.getDevicesSpec() == null || spec.getDevicesSpec().getTpm() == null)) { + if (tpmUuid != null && (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); + devicesSpec.getTpm().setKeyProviderUuid(resourceKeyBackend.findKeyProviderUuidByTpm(tpmUuid)); } } } 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 70ba4beced6..7345e0e49d2 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 @@ -38,6 +38,10 @@ public TpmVO persistTpmVO(String tpmUuid, String vmUuid) { return tpm; } + public void deleteTpmVO(String tpmUuid) { + databaseFacade.removeByPrimaryKey(tpmUuid, TpmVO.class); + } + /** * @param bootMode boot mode, null is Legacy */ diff --git a/conf/errorCodes/keyProvider.xml b/conf/errorCodes/keyProvider.xml new file mode 100644 index 00000000000..12facc8ebf9 --- /dev/null +++ b/conf/errorCodes/keyProvider.xml @@ -0,0 +1,108 @@ + + KP + + + 1000 + ok + + + + 1001 + invalid content + + + + 1002 + internal error + + + + 1500 + backend unavailable + + + + 1506 + socket not found + + + + 1507 + socket not socket + + + + 1700 + kmip connect failed + + + + 1701 + kmip timeout + + + + 1702 + kmip tls handshake failed + + + + 1703 + kmip cert invalid + + + + 1704 + kmip operation failed + + + + 1600 + root key sha256 mismatch + + + + 1601 + root key sha256 file missing + + + + 1602 + zip data required + + + + 1603 + checksum mismatch + + + + 1604 + password invalid + + + + 1605 + root key extension missing + + + + 1900 + name duplicate + + + + 1901 + uuid duplicate + + + + 2000 + TPM related errors + + + + 2101 + TPM already attached key provider + + diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index 7d4e03f86b2..c8d357c9d37 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -273,6 +273,4 @@ - - diff --git a/conf/springConfigXml/VmInstanceManager.xml b/conf/springConfigXml/VmInstanceManager.xml index 55af623b940..3d84703cca4 100755 --- a/conf/springConfigXml/VmInstanceManager.xml +++ b/conf/springConfigXml/VmInstanceManager.xml @@ -283,16 +283,18 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + + 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 029eaa43334..a9d21d6b8e2 100644 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceCreateExtensionPoint.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceCreateExtensionPoint.java @@ -9,4 +9,11 @@ public interface VmInstanceCreateExtensionPoint { void preCreateVmInstance(CreateVmInstanceMsg msg); default void afterPersistVmInstanceVO(VmInstanceVO vo, CreateVmInstanceMsg msg) {} + + /** + * Invoked when VM creation rolls back after + * {`@link` `#afterPersistVmInstanceVO`(VmInstanceVO, CreateVmInstanceMsg)} so extensions can + * clean up any state created in that hook. Implementations should be idempotent. + */ + default void afterRollbackPersistVmInstanceVO(VmInstanceVO vo, CreateVmInstanceMsg msg) {} } 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 781faf4c186..f0b1dffb7a9 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,6 +1,7 @@ package org.zstack.kvm.tpm; import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.vm.devices.TpmEncryptedResourceKeyBackend; import org.zstack.compute.vm.devices.VmTpmManager; import org.zstack.core.asyncbatch.While; import org.zstack.core.cloudbus.CloudBus; diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index 730e9c79cae..8883bdb5ae9 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -272,6 +272,4 @@ - - From dbccca2c2d09d82f757c6e4490aff903484903a5 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 17 Mar 2026 19:09:22 +0800 Subject: [PATCH 59/64] [compute]: prepare DEK on instantiating VM resource Resolves: ZSV-11489 Change-Id: I6e626363656971726e68656c6a79676770696976 --- .../DummyTpmEncryptedResourceKeyBackend.java | 5 + .../TpmEncryptedResourceKeyBackend.java | 5 + .../compute/vm/devices/VmTpmExtensions.java | 21 ++- .../org/zstack/header/tpm/entity/TpmSpec.java | 20 +++ .../org/zstack/kvm/tpm/KvmTpmExtensions.java | 131 ++++++++++++++++-- .../main/java/org/zstack/kvm/tpm/TpmTO.java | 9 ++ 6 files changed, 171 insertions(+), 20 deletions(-) diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/DummyTpmEncryptedResourceKeyBackend.java b/compute/src/main/java/org/zstack/compute/vm/devices/DummyTpmEncryptedResourceKeyBackend.java index 911a78809a4..957dd7a0a98 100644 --- a/compute/src/main/java/org/zstack/compute/vm/devices/DummyTpmEncryptedResourceKeyBackend.java +++ b/compute/src/main/java/org/zstack/compute/vm/devices/DummyTpmEncryptedResourceKeyBackend.java @@ -23,6 +23,11 @@ public String findKeyProviderUuidByTpm(String tpmUuid) { return null; } + @Override + public String findKeyProviderNameByTpm(String tpmUuid) { + return null; + } + @Override public void cloneEncryptedResourceKey(CloneEncryptedResourceKeyContext context, Completion completion) { // do nothing diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/TpmEncryptedResourceKeyBackend.java b/compute/src/main/java/org/zstack/compute/vm/devices/TpmEncryptedResourceKeyBackend.java index cc043bd36ae..6a9bd8f7149 100644 --- a/compute/src/main/java/org/zstack/compute/vm/devices/TpmEncryptedResourceKeyBackend.java +++ b/compute/src/main/java/org/zstack/compute/vm/devices/TpmEncryptedResourceKeyBackend.java @@ -25,6 +25,11 @@ public interface TpmEncryptedResourceKeyBackend { */ String findKeyProviderUuidByTpm(String tpmUuid); + /** + * maybe null (when crypto module is not installed) + */ + String findKeyProviderNameByTpm(String tpmUuid); + static class CloneEncryptedResourceKeyContext { public String srcTpmUuid; public String dstTpmUuid; 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 775fa9d6f16..f41d290a450 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 @@ -99,13 +99,22 @@ public void afterBuildVmSpec(VmInstanceSpec spec) { spec.setNvRamSpec(nvRamSpec); } - if (tpmUuid != null && (spec.getDevicesSpec() == null || spec.getDevicesSpec().getTpm() == null)) { - VmDevicesSpec devicesSpec = spec.getDevicesSpec() == null ? new VmDevicesSpec() : spec.getDevicesSpec(); - spec.setDevicesSpec(devicesSpec); + if (tpmUuid != null) { + VmDevicesSpec devicesSpec = spec.getDevicesSpec(); + if (devicesSpec == null) { + devicesSpec = new VmDevicesSpec(); + spec.setDevicesSpec(devicesSpec); + } + + TpmSpec tpmSpec = devicesSpec.getTpm(); + if (tpmSpec == null) { + tpmSpec = new TpmSpec(); + devicesSpec.setTpm(tpmSpec); + } - devicesSpec.setTpm(new TpmSpec()); - devicesSpec.getTpm().setEnable(true); - devicesSpec.getTpm().setKeyProviderUuid(resourceKeyBackend.findKeyProviderUuidByTpm(tpmUuid)); + tpmSpec.setEnable(true); + tpmSpec.setTpmUuid(tpmUuid); + tpmSpec.setKeyProviderUuid(resourceKeyBackend.findKeyProviderUuidByTpm(tpmUuid)); } } } 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 index 60efd9a9df9..bce99ae23dd 100644 --- a/header/src/main/java/org/zstack/header/tpm/entity/TpmSpec.java +++ b/header/src/main/java/org/zstack/header/tpm/entity/TpmSpec.java @@ -1,10 +1,14 @@ package org.zstack.header.tpm.entity; +import org.zstack.header.rest.APINoSee; import org.zstack.utils.StringDSL; public class TpmSpec { private boolean enable = true; + private String tpmUuid; private String keyProviderUuid; + @APINoSee + private String secretUuid; public boolean isEnable() { return enable; @@ -14,6 +18,14 @@ public void setEnable(boolean enable) { this.enable = enable; } + public String getTpmUuid() { + return tpmUuid; + } + + public void setTpmUuid(String tpmUuid) { + this.tpmUuid = tpmUuid; + } + public String getKeyProviderUuid() { return keyProviderUuid; } @@ -22,6 +34,14 @@ public void setKeyProviderUuid(String keyProviderUuid) { this.keyProviderUuid = keyProviderUuid; } + public String getSecretUuid() { + return secretUuid; + } + + public void setSecretUuid(String secretUuid) { + this.secretUuid = secretUuid; + } + public static TpmSpec __example__() { TpmSpec tpm = new TpmSpec(); tpm.setKeyProviderUuid(StringDSL.createFixedUuid("keyProviderUuid")); 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 0d98a310d7a..2513703d37d 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,12 +1,24 @@ package org.zstack.kvm.tpm; import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.vm.devices.TpmEncryptedResourceKeyBackend; 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.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.host.HostConstant; +import org.zstack.header.message.MessageReply; +import org.zstack.header.secret.SecretHostDefineMsg; +import org.zstack.header.secret.SecretHostDefineReply; import org.zstack.header.vm.PreVmInstantiateResourceExtensionPoint; import org.zstack.header.vm.VmInstanceSpec; import org.zstack.header.vm.VmInstantiateResourceException; @@ -24,6 +36,7 @@ import java.sql.Timestamp; import java.time.Instant; +import java.util.Map; import static org.zstack.kvm.KVMConstant.*; @@ -35,6 +48,10 @@ public class KvmTpmExtensions implements KVMStartVmExtensionPoint, private KvmSecureBootExtensions secureBootExtensions; @Autowired private DatabaseFacade databaseFacade; + @Autowired + private TpmEncryptedResourceKeyBackend resourceKeyBackend; + @Autowired + private CloudBus bus; private final Object hostFileLock = new Object(); @@ -47,6 +64,7 @@ public void beforeStartVmOnKvm(KVMHostInventory host, VmInstanceSpec spec, KVMAg TpmTO tpm = new TpmTO(); tpm.setKeyProviderUuid(devicesSpec.getTpm().getKeyProviderUuid()); + tpm.setSecretUuid(devicesSpec.getTpm().getSecretUuid()); tpm.setInstallPath(buildTpmStateFilePath(cmd.getVmInstanceUuid())); cmd.setTpm(tpm); @@ -91,34 +109,119 @@ public void preBeforeInstantiateVmResource(VmInstanceSpec spec) throws VmInstant // do-nothing } - @Override - public void preInstantiateVmResource(VmInstanceSpec spec, Completion completion) { - prepareTpmStateHostFileOnHost(spec, completion); - } - - static class PrepareTpmStateHostFileContext { + static class PreInstantiateVmResourceContext { String hostUuid; String vmUuid; - - // whether the NvRam is on the same host as before - boolean sameHost = false; - VmHostFileVO tpmStateFile; + String tpmUuid; + String providerName; + String dekBase64; // secret key } - private void prepareTpmStateHostFileOnHost(VmInstanceSpec spec, Completion completion) { + @Override + @SuppressWarnings("rawtypes") + public void preInstantiateVmResource(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(); + PreInstantiateVmResourceContext context = new PreInstantiateVmResourceContext(); context.hostUuid = spec.getDestHost().getUuid(); context.vmUuid = spec.getVmInventory().getUuid(); - context.type = VmHostFileType.TpmState; - secureBootExtensions.prepareHostFileOnHost(context, completion); + context.tpmUuid = spec.getDevicesSpec().getTpm().getTpmUuid(); + context.providerName = resourceKeyBackend.findKeyProviderNameByTpm(context.tpmUuid); + + final SimpleFlowChain chain = new SimpleFlowChain(); + chain.setName("prepare-tpm-resources-for-vm-" + spec.getVmInventory().getUuid()); + chain.then(new NoRollbackFlow() { + String __name__ = "prepare-tpm-state-file-on-host"; + + @Override + public void run(FlowTrigger trigger, Map data) { + PrepareHostFileContext innerContext = new PrepareHostFileContext(); + innerContext.hostUuid = context.hostUuid; + innerContext.vmUuid = context.vmUuid; + innerContext.type = VmHostFileType.TpmState; + secureBootExtensions.prepareHostFileOnHost(innerContext, new Completion(trigger) { + @Override + public void success() { + trigger.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + trigger.fail(errorCode); + } + }); + } + }).then(new NoRollbackFlow() { + String __name__ = "create-dek"; + + @Override + public boolean skip(Map data) { + return context.providerName == null; + } + + @Override + public void run(FlowTrigger trigger, Map data) { + // TODO create DEK + context.dekBase64 = Platform.getUuid(); + trigger.next(); + } + }).then(new NoRollbackFlow() { + String __name__ = "define-secret-on-host"; + + @Override + public boolean skip(Map data) { + logger.warn("This is for test only, and coming soon"); // TODO + return true; + } + + @Override + public void run(FlowTrigger trigger, Map data) { + SecretHostDefineMsg innerMsg = new SecretHostDefineMsg(); + innerMsg.setHostUuid(context.hostUuid); + innerMsg.setVmUuid(context.vmUuid); + innerMsg.setDekBase64(context.dekBase64); + innerMsg.setPurpose("vtpm"); + innerMsg.setProviderName(context.providerName); + innerMsg.setDescription("Define secret for VM " + context.vmUuid); + bus.makeTargetServiceIdByResourceUuid(innerMsg, HostConstant.SERVICE_ID, innerMsg.getHostUuid()); + bus.send(innerMsg, new CloudBusCallBack(trigger) { + @Override + public void run(MessageReply reply) { + if (reply.isSuccess()) { + SecretHostDefineReply r = reply.castReply(); + spec.getDevicesSpec().getTpm().setSecretUuid(r.getSecretUuid()); + trigger.next(); + } else { + trigger.fail(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(); } + static class PrepareTpmStateHostFileContext { + String hostUuid; + String vmUuid; + + // whether the NvRam is on the same host as before + boolean sameHost = false; + VmHostFileVO tpmStateFile; + } @Override public void preReleaseVmResource(VmInstanceSpec spec, Completion completion) { 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 c1de0d42c1b..073257172f2 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 secretUuid; private String installPath; public String getKeyProviderUuid() { @@ -14,6 +15,14 @@ public void setKeyProviderUuid(String keyProviderUuid) { this.keyProviderUuid = keyProviderUuid; } + public String getSecretUuid() { + return secretUuid; + } + + public void setSecretUuid(String secretUuid) { + this.secretUuid = secretUuid; + } + public String getInstallPath() { return installPath; } From 1018662542b3b270dd64c37062baa6e89dc99978 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 18 Mar 2026 11:38:02 +0800 Subject: [PATCH 60/64] [conf]: move premium errors xml to premium module Related: ZSV-5936 Change-Id: I77736d726d726b6f776b70786a6c706173757563 --- conf/errorCodes/iam1.xml | 18 ------ conf/errorCodes/keyProvider.xml | 108 -------------------------------- conf/errorCodes/sso.xml | 12 ---- 3 files changed, 138 deletions(-) delete mode 100644 conf/errorCodes/iam1.xml delete mode 100644 conf/errorCodes/keyProvider.xml delete mode 100644 conf/errorCodes/sso.xml diff --git a/conf/errorCodes/iam1.xml b/conf/errorCodes/iam1.xml deleted file mode 100644 index 83920b66ec0..00000000000 --- a/conf/errorCodes/iam1.xml +++ /dev/null @@ -1,18 +0,0 @@ - - IAM1 - - - 1000 - IAM1 general error - - - - 2001 - Move account group to a wrong place - - - - 3001 - The specific resource is not a resource ensemble member - - diff --git a/conf/errorCodes/keyProvider.xml b/conf/errorCodes/keyProvider.xml deleted file mode 100644 index 12facc8ebf9..00000000000 --- a/conf/errorCodes/keyProvider.xml +++ /dev/null @@ -1,108 +0,0 @@ - - KP - - - 1000 - ok - - - - 1001 - invalid content - - - - 1002 - internal error - - - - 1500 - backend unavailable - - - - 1506 - socket not found - - - - 1507 - socket not socket - - - - 1700 - kmip connect failed - - - - 1701 - kmip timeout - - - - 1702 - kmip tls handshake failed - - - - 1703 - kmip cert invalid - - - - 1704 - kmip operation failed - - - - 1600 - root key sha256 mismatch - - - - 1601 - root key sha256 file missing - - - - 1602 - zip data required - - - - 1603 - checksum mismatch - - - - 1604 - password invalid - - - - 1605 - root key extension missing - - - - 1900 - name duplicate - - - - 1901 - uuid duplicate - - - - 2000 - TPM related errors - - - - 2101 - TPM already attached key provider - - diff --git a/conf/errorCodes/sso.xml b/conf/errorCodes/sso.xml deleted file mode 100644 index 844265d8c58..00000000000 --- a/conf/errorCodes/sso.xml +++ /dev/null @@ -1,12 +0,0 @@ - - SSO - - 1001 - invalid SSO token - - - - 2001 - SSO account not found - - From 5d8868a35f296b6dea44d832eb389a6943f57cd1 Mon Sep 17 00:00:00 2001 From: "tao.yang" Date: Wed, 18 Mar 2026 14:39:48 +0800 Subject: [PATCH 61/64] [kms]: distinguish kms trust state DBImpact Resolves: ZSV-11496 Change-Id: I706c63636363627a6a6b626573686e687a696b6c --- conf/db/zsv/V5.0.0__schema.sql | 2 +- sdk/src/main/java/org/zstack/sdk/KmsInventory.java | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/conf/db/zsv/V5.0.0__schema.sql b/conf/db/zsv/V5.0.0__schema.sql index 69ff903049c..808e3041120 100644 --- a/conf/db/zsv/V5.0.0__schema.sql +++ b/conf/db/zsv/V5.0.0__schema.sql @@ -65,7 +65,7 @@ CREATE TABLE IF NOT EXISTS `zstack`.`KmsVO` ( `kmipVersion` varchar(32) DEFAULT NULL, `username` varchar(255) DEFAULT NULL, `password` varchar(255) DEFAULT NULL, - `trusted` boolean NOT NULL DEFAULT FALSE, + `trustState` varchar(32) NOT NULL DEFAULT 'MUTUAL_UNTRUSTED', `activeIdentityUuid` varchar(32) DEFAULT NULL, `serverCertExpiredDate` timestamp NULL DEFAULT NULL, `serverCertPem` text DEFAULT NULL, diff --git a/sdk/src/main/java/org/zstack/sdk/KmsInventory.java b/sdk/src/main/java/org/zstack/sdk/KmsInventory.java index 65ee119c300..8da5d74fa61 100644 --- a/sdk/src/main/java/org/zstack/sdk/KmsInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/KmsInventory.java @@ -37,12 +37,12 @@ public java.lang.String getUsername() { return this.username; } - public boolean trusted; - public void setTrusted(boolean trusted) { - this.trusted = trusted; + public java.lang.String trustState; + public void setTrustState(java.lang.String trustState) { + this.trustState = trustState; } - public boolean getTrusted() { - return this.trusted; + public java.lang.String getTrustState() { + return this.trustState; } public java.lang.String activeIdentityUuid; From 559cb76d56c410621246d1e3dcf8f477680eb604 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 18 Mar 2026 18:29:52 +0800 Subject: [PATCH 62/64] [core]: improve SimpleFlowChain * Added named constructors, factory methods, `then/error/done` overloads, and asynchronous backup collection to SimpleFlowChain; * Added the FlowBuilder stream builder and the `Flow.of` factory method to Flow, allowing for the configuration of `skip/run/rollback` behavior and the generation of anonymous Flow implementations. Related: ZSV-5936 Change-Id: I776b7374716365687279697063726f7a7167736f --- .../zstack/core/workflow/SimpleFlowChain.java | 59 ++++++++++ .../org/zstack/header/core/workflow/Flow.java | 101 ++++++++++++++++++ 2 files changed, 160 insertions(+) diff --git a/core/src/main/java/org/zstack/core/workflow/SimpleFlowChain.java b/core/src/main/java/org/zstack/core/workflow/SimpleFlowChain.java index 4ee9498b1b9..683f2580154 100755 --- a/core/src/main/java/org/zstack/core/workflow/SimpleFlowChain.java +++ b/core/src/main/java/org/zstack/core/workflow/SimpleFlowChain.java @@ -8,6 +8,7 @@ import org.zstack.core.CoreGlobalProperty; import org.zstack.core.Platform; import org.zstack.core.errorcode.ErrorFacade; +import org.zstack.header.core.AsyncBackup; import org.zstack.header.core.progress.ProgressFlowChainProcessorFactory; import org.zstack.header.core.workflow.*; import org.zstack.header.errorcode.ErrorCode; @@ -23,6 +24,7 @@ import java.util.*; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; import java.util.function.Function; import static org.zstack.core.Platform.inerr; @@ -62,6 +64,7 @@ public class SimpleFlowChain implements FlowTrigger, FlowRollback, FlowChain, Fl private List> afterDone = new ArrayList<>(); private List> afterError = new ArrayList<>(); private List> afterFinal = new ArrayList<>(); + private List asyncBackups = new ArrayList<>(); @Autowired(required = false) ProgressFlowChainProcessorFactory progressFactory; @@ -131,11 +134,20 @@ public SimpleFlowChain() { id = "FCID_" + Platform.getUuid().substring(0, 8); } + public SimpleFlowChain(String chainName) { + this(); + this.name = chainName; + } + public SimpleFlowChain(Map data) { id = "FCID_" + Platform.getUuid().substring(0, 8); this.data.putAll(data); } + public static SimpleFlowChain of(String chainName) { + return new SimpleFlowChain(chainName); + } + @Override public List getFlows() { return flows; @@ -228,6 +240,12 @@ public SimpleFlowChain then(Flow flow) { return this; } + public SimpleFlowChain then(String flowName, Consumer consumer) { + return then(Flow.of(flowName) + .handle(consumer) + .build()); + } + public SimpleFlowChain ctxHandler(FlowContextHandler handler) { DebugUtils.Assert(contextHandler==null, "there has been an FlowContextHandler installed"); contextHandler = handler; @@ -240,6 +258,21 @@ public SimpleFlowChain error(FlowErrorHandler handler) { return this; } + @SuppressWarnings("rawtypes") + public SimpleFlowChain error(Consumer handler) { + AsyncBackup firstAsyncBackup = this.asyncBackups.isEmpty() ? null : this.asyncBackups.get(0); + AsyncBackup[] otherAsyncBackups = this.asyncBackups.isEmpty() ? new AsyncBackup[0] : + this.asyncBackups.subList(1, this.asyncBackups.size()).toArray(new AsyncBackup[0]); + + DebugUtils.Assert(handler != null, "handler of errorHandler should not be null"); + return error(new FlowErrorHandler(firstAsyncBackup, otherAsyncBackups) { + @Override + public void handle(ErrorCode errCode, Map data) { + handler.accept(errCode); + } + }); + } + @Override public FlowChain Finally(FlowFinallyHandler handler) { finallyHandler = handler; @@ -285,6 +318,17 @@ public Map getData() { return this.data; } + public SimpleFlowChain propagateExceptionTo(AsyncBackup... backups) { + DebugUtils.Assert(backups != null, "backups in methods propagateExceptionTo() must be not null"); + DebugUtils.Assert(Arrays.stream(backups).noneMatch(Objects::isNull), + "backups in propagateExceptionTo() should not contain null elements"); + DebugUtils.Assert(doneHandler == null, "propagateExceptionTo() must be called before SimpleFlowChain.done()"); + DebugUtils.Assert(errorHandler == null, "propagateExceptionTo() must be called before SimpleFlowChain.error()"); + DebugUtils.Assert(finallyHandler == null, "propagateExceptionTo() must be called before SimpleFlowChain.Finally()"); + this.asyncBackups.addAll(Arrays.asList(backups)); + return this; + } + @Override public SimpleFlowChain done(FlowDoneHandler handler) { DebugUtils.Assert(doneHandler==null, "there has been a FlowDoneHandler installed"); @@ -292,6 +336,21 @@ public SimpleFlowChain done(FlowDoneHandler handler) { return this; } + @SuppressWarnings("rawtypes") + public SimpleFlowChain done(Runnable runnable) { + AsyncBackup firstAsyncBackup = this.asyncBackups.isEmpty() ? null : this.asyncBackups.get(0); + AsyncBackup[] otherAsyncBackups = this.asyncBackups.isEmpty() ? new AsyncBackup[0] : + this.asyncBackups.subList(1, this.asyncBackups.size()).toArray(new AsyncBackup[0]); + + DebugUtils.Assert(runnable != null, "runnable of doneHandler should not be null"); + return done(new FlowDoneHandler(firstAsyncBackup, otherAsyncBackups) { + @Override + public void handle(Map data) { + runnable.run(); + } + }); + } + private void collectAfterRunnable(Flow flow) { List ad = FieldUtils.getAnnotatedFieldsOnThisClass(AfterDone.class, flow.getClass()); for (Field f : ad) { diff --git a/header/src/main/java/org/zstack/header/core/workflow/Flow.java b/header/src/main/java/org/zstack/header/core/workflow/Flow.java index 04f304af071..440769ecfe8 100755 --- a/header/src/main/java/org/zstack/header/core/workflow/Flow.java +++ b/header/src/main/java/org/zstack/header/core/workflow/Flow.java @@ -1,8 +1,12 @@ package org.zstack.header.core.workflow; +import org.zstack.utils.DebugUtils; import org.zstack.utils.FieldUtils; import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; public interface Flow { void run(FlowTrigger trigger, Map data); @@ -23,4 +27,101 @@ default String name() { } return String.format("%s", this.getClass().getSimpleName()); } + + @SuppressWarnings("rawtypes") + public static class FlowBuilder { + public final String flowName; + private Predicate skipPredicate; + private BiConsumer triggerConsumer; + private BiConsumer rollbackConsumer; + + private FlowBuilder(String flowName) { + DebugUtils.Assert(flowName != null, "flowName should not be null"); + this.flowName = flowName; + } + + public FlowBuilder withSkipPredicate(Predicate predicate) { + DebugUtils.Assert(predicate != null, "skipPredicate of FlowBuilder should not be null"); + this.skipPredicate = predicate; + return this; + } + + public FlowBuilder skipIf(Predicate predicate) { + return withSkipPredicate(predicate); + } + + public FlowBuilder runIf(Predicate predicate) { + DebugUtils.Assert(predicate != null, "predicate of FlowBuilder.runIf() should not be null"); + return withSkipPredicate(predicate.negate()); + } + + public FlowBuilder handle(BiConsumer consumer) { + DebugUtils.Assert(consumer != null, "consumer of FlowBuilder.handle() should not be null"); + this.triggerConsumer = consumer; + return this; + } + + public FlowBuilder handle(Consumer consumer) { + DebugUtils.Assert(consumer != null, "consumer of FlowBuilder.handle() should not be null"); + this.triggerConsumer = (trigger, data) -> consumer.accept(trigger); + return this; + } + + public FlowBuilder rollback(BiConsumer consumer) { + DebugUtils.Assert(consumer != null, "consumer of FlowBuilder.rollback() should not be null"); + this.rollbackConsumer = consumer; + return this; + } + + public FlowBuilder rollback(Consumer consumer) { + DebugUtils.Assert(consumer != null, "consumer of FlowBuilder.rollback() should not be null"); + this.rollbackConsumer = (trigger, data) -> consumer.accept(trigger); + return this; + } + + public Flow build() { + DebugUtils.Assert(triggerConsumer != null, "handle() must be called before build()"); + Predicate skipPredicateSnapshot = skipPredicate; + BiConsumer triggerConsumerSnapshot = triggerConsumer; + BiConsumer rollbackConsumerSnapshot = rollbackConsumer; + + return new Flow() { + @Override + public boolean skip(Map data) { + if (skipPredicateSnapshot == null) { + return false; + } + return skipPredicateSnapshot.test(data); + } + + @Override + public void run(FlowTrigger trigger, Map data) { + triggerConsumerSnapshot.accept(trigger, data); + } + + @Override + public void rollback(FlowRollback trigger, Map data) { + if (rollbackConsumerSnapshot == null) { + trigger.rollback(); + } else { + rollbackConsumerSnapshot.accept(trigger, data); + } + } + + @Override + public String name() { + return flowName; + } + + @Override + public String toString() { + return name(); + } + }; + } + } + + public static FlowBuilder of(String flowName) { + return new FlowBuilder(flowName); + } } From 56f525040b5940df7a1fcf33d46b638860aed6d6 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 18 Mar 2026 19:34:34 +0800 Subject: [PATCH 63/64] [kvm]: prepare nvram folder before migrate VM Before vTPM VM live migration, send a `PrepareOnly` command to the target host via the `VmPreMigrationExtensionPoint` to create only the NVRAM directory, thereby avoiding an error caused by the directory not existing during migration. Resolves: ZSV-11524 Related: ZSV-11438 Change-Id: I777875637a7071796d786c6566637875666a6970 --- conf/springConfigXml/Kvm.xml | 1 + .../java/org/zstack/kvm/KVMAgentCommands.java | 6 ++ .../kvm/efi/KvmSecureBootExtensions.java | 66 ++++++++++++++++++- .../test/resources/springConfigXml/Kvm.xml | 1 + 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index c8d357c9d37..92436934d60 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -271,6 +271,7 @@ + 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 e1be2dc98b0..08b36331d19 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -2824,6 +2824,8 @@ public static class StartVmResponse extends VmDevicesInfoResponse { } public static class VmHostFileTO { + public static final String FORMAT_PREPARE_ONLY = "PrepareOnly"; + private String path; /** * maybe "NvRam" or "TpmState" ... @@ -2832,9 +2834,13 @@ public static class VmHostFileTO { private String type; /** * maybe "Simple" or "TarballGzip" + * if prepare only, use {@link #FORMAT_PREPARE_ONLY} * @see VmHostFileContentFormat */ private String fileFormat; + /** + * null if fileFormat is {@link #FORMAT_PREPARE_ONLY} + */ @NoLogging private String contentBase64; private String error; 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 9b747956163..a2d815d96a1 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 @@ -3,6 +3,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.zstack.compute.legacy.ComputeLegacyGlobalProperty; import org.zstack.compute.vm.VmGlobalConfig; +import org.zstack.compute.vm.VmSystemTags; import org.zstack.compute.vm.devices.VmTpmManager; import org.zstack.core.Platform; import org.zstack.core.cloudbus.CloudBus; @@ -24,12 +25,16 @@ import org.zstack.header.identity.AccountResourceRefVO; import org.zstack.header.identity.AccountResourceRefVO_; import org.zstack.header.message.MessageReply; +import org.zstack.header.tpm.entity.TpmVO; +import org.zstack.header.tpm.entity.TpmVO_; import org.zstack.header.vm.DiskAO; import org.zstack.header.vm.PreVmInstantiateResourceExtensionPoint; import org.zstack.header.vm.VmInstanceDestroyExtensionPoint; import org.zstack.header.vm.VmInstanceInventory; import org.zstack.header.vm.VmInstanceSpec; import org.zstack.header.vm.VmInstantiateResourceException; +import org.zstack.header.vm.VmMigrationType; +import org.zstack.header.vm.VmPreMigrationExtensionPoint; import org.zstack.header.vm.additions.VmHostBackupFileVO; import org.zstack.header.vm.additions.VmHostBackupFileVO_; import org.zstack.header.vm.additions.VmHostFileContentFormat; @@ -72,14 +77,18 @@ import java.util.Map; import java.util.Objects; +import static org.zstack.compute.vm.VmGlobalConfig.ENABLE_UEFI_SECURE_BOOT; import static org.zstack.core.Platform.operr; +import static org.zstack.header.vm.VmMigrationType.HostMigration; import static org.zstack.kvm.KVMConstant.*; +import static org.zstack.utils.CollectionDSL.list; import static org.zstack.utils.CollectionUtils.findOneOrNull; import static org.zstack.utils.CollectionUtils.transform; public class KvmSecureBootExtensions implements KVMStartVmExtensionPoint, PreVmInstantiateResourceExtensionPoint, - VmInstanceDestroyExtensionPoint { + VmInstanceDestroyExtensionPoint, + VmPreMigrationExtensionPoint { private static final CLogger logger = Utils.getLogger(KvmSecureBootExtensions.class); @Autowired @@ -160,6 +169,61 @@ private void prepareNvRamToStartVmCmd(KVMAgentCommands.StartVmCmd cmd, DiskAO nv } } + @Override + public void preVmMigration(VmInstanceInventory vm, VmMigrationType type, String dstHostUuid, Completion completion) { + if (HostMigration != type) { + completion.success(); + return; + } + + String tpmUuid = Q.New(TpmVO.class) + .eq(TpmVO_.vmInstanceUuid, vm.getUuid()) + .select(TpmVO_.uuid) + .findValue(); + boolean needRegisterNvRam = tpmUuid != null; + if (!needRegisterNvRam) { + String bootMode = VmSystemTags.BOOT_MODE.getTokenByResourceUuid(vm.getUuid(), VmSystemTags.BOOT_MODE_TOKEN); + if (isUefiBootMode(bootMode)) { + ResourceConfig resourceConfig = resourceConfigFacade.getResourceConfig(ENABLE_UEFI_SECURE_BOOT.getIdentity()); + needRegisterNvRam = resourceConfig.getResourceConfigValue(vm.getUuid(), Boolean.class) == Boolean.TRUE; + } + + if (!needRegisterNvRam) { + completion.success(); + return; + } + } + + SimpleFlowChain.of("prepare-nvram-before-vm-" + vm.getUuid() + "-migrate") + .then("prepare-nvram-folder-on-dest-host", trigger -> { + VmHostFileTO to = new VmHostFileTO(); + to.setPath(buildNvramFilePath(vm.getUuid())); + to.setType(VmHostFileType.NvRam.toString()); + to.setFileFormat(VmHostFileTO.FORMAT_PREPARE_ONLY); + + RewriteVmHostFilesContext context = new RewriteVmHostFilesContext(); + context.hostUuid = dstHostUuid; + context.hostFiles = list(to); + + rewriteVmHostFiles(context, new Completion(trigger) { + @Override + public void success() { + trigger.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + trigger.fail(errorCode); + } + }); + }) + // TpmState folder is not needed to prepare + .propagateExceptionTo(completion) + .done(completion::success) + .error(completion::fail) + .start(); + } + public static class SyncVmHostFilesFromHostContext { public String hostUuid; public String vmUuid; diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index 8883bdb5ae9..cda756671f7 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -270,6 +270,7 @@ + From 41eaddc5f55de94762fa8ad46f11b37e2cf817dd Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Thu, 15 Jan 2026 14:06:14 +0800 Subject: [PATCH 64/64] [storage]: register and take over sblk APIImpact Resolves: ZSV-10000 Change-Id: I70637377776e777070676c6a6c616e74786b6667 --- conf/serviceConfig/primaryStorage.xml | 9 ++ .../APICheckPrimaryStorageConsistencyMsg.java | 35 ++++++ ...imaryStorageConsistencyMsgDoc_zh_cn.groovy | 58 ++++++++++ ...PICheckPrimaryStorageConsistencyReply.java | 42 ++++++++ ...aryStorageConsistencyReplyDoc_zh_cn.groovy | 41 +++++++ .../APITakeoverPrimaryStorageEvent.java | 60 +++++++++++ ...akeoverPrimaryStorageEventDoc_zh_cn.groovy | 44 ++++++++ .../primary/APITakeoverPrimaryStorageMsg.java | 40 +++++++ ...ITakeoverPrimaryStorageMsgDoc_zh_cn.groovy | 61 +++++++++++ .../primary/ConsistencyCheckStatus.java | 10 ++ .../storage/primary/ReconnectResult.java | 7 ++ sdk/src/main/java/SourceClassMap.java | 6 ++ .../CheckPrimaryStorageConsistencyAction.java | 95 ++++++++++++++++ .../CheckPrimaryStorageConsistencyResult.java | 28 +++++ .../zstack/sdk/ConsistencyCheckStatus.java | 7 ++ .../DiscoverSharedBlockGroupVgsAction.java | 95 ++++++++++++++++ .../DiscoverSharedBlockGroupVgsResult.java | 14 +++ .../java/org/zstack/sdk/ReconnectResult.java | 7 ++ .../zstack/sdk/SharedBlockGroupVgInfo.java | 31 ++++++ .../sdk/TakeoverPrimaryStorageAction.java | 101 ++++++++++++++++++ .../sdk/TakeoverPrimaryStorageResult.java | 31 ++++++ .../storage/primary/PrimaryStorageBase.java | 16 +++ .../java/org/zstack/testlib/ApiHelper.groovy | 81 ++++++++++++++ 23 files changed, 919 insertions(+) create mode 100644 header/src/main/java/org/zstack/header/storage/primary/APICheckPrimaryStorageConsistencyMsg.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/APICheckPrimaryStorageConsistencyMsgDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/storage/primary/APICheckPrimaryStorageConsistencyReply.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/APICheckPrimaryStorageConsistencyReplyDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/storage/primary/APITakeoverPrimaryStorageEvent.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/APITakeoverPrimaryStorageEventDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/storage/primary/APITakeoverPrimaryStorageMsg.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/APITakeoverPrimaryStorageMsgDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/storage/primary/ConsistencyCheckStatus.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/ReconnectResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/CheckPrimaryStorageConsistencyAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/CheckPrimaryStorageConsistencyResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/ConsistencyCheckStatus.java create mode 100644 sdk/src/main/java/org/zstack/sdk/DiscoverSharedBlockGroupVgsAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/DiscoverSharedBlockGroupVgsResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/ReconnectResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/SharedBlockGroupVgInfo.java create mode 100644 sdk/src/main/java/org/zstack/sdk/TakeoverPrimaryStorageAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/TakeoverPrimaryStorageResult.java diff --git a/conf/serviceConfig/primaryStorage.xml b/conf/serviceConfig/primaryStorage.xml index 337ce4eaac3..c928d2f6d8c 100755 --- a/conf/serviceConfig/primaryStorage.xml +++ b/conf/serviceConfig/primaryStorage.xml @@ -81,7 +81,16 @@ org.zstack.header.storage.primary.APICleanUpStorageTrashOnPrimaryStorageMsg + org.zstack.header.storage.primary.APIAddStorageProtocolMsg + + + org.zstack.header.storage.primary.APICheckPrimaryStorageConsistencyMsg + + + + org.zstack.header.storage.primary.APITakeoverPrimaryStorageMsg + diff --git a/header/src/main/java/org/zstack/header/storage/primary/APICheckPrimaryStorageConsistencyMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APICheckPrimaryStorageConsistencyMsg.java new file mode 100644 index 00000000000..e80bd4f9efc --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/APICheckPrimaryStorageConsistencyMsg.java @@ -0,0 +1,35 @@ +package org.zstack.header.storage.primary; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APISyncCallMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.rest.RestRequest; + +@RestRequest( + path = "/primary-storage/{uuid}/consistency", + responseClass = APICheckPrimaryStorageConsistencyReply.class, + method = HttpMethod.GET +) +public class APICheckPrimaryStorageConsistencyMsg extends APISyncCallMessage implements PrimaryStorageMessage { + @APIParam(resourceType = PrimaryStorageVO.class) + private String uuid; + + @Override + public String getPrimaryStorageUuid() { + return uuid; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public static APICheckPrimaryStorageConsistencyMsg __example__() { + APICheckPrimaryStorageConsistencyMsg msg = new APICheckPrimaryStorageConsistencyMsg(); + msg.setUuid(uuid(PrimaryStorageVO.class)); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/APICheckPrimaryStorageConsistencyMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/primary/APICheckPrimaryStorageConsistencyMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..e8a409b9718 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/APICheckPrimaryStorageConsistencyMsgDoc_zh_cn.groovy @@ -0,0 +1,58 @@ +package org.zstack.header.storage.primary + +import org.zstack.header.storage.primary.APICheckPrimaryStorageConsistencyReply + +doc { + title "CheckPrimaryStorageConsistency" + + category "storage.primary" + + desc """检查存储一致性""" + + rest { + request { + url "GET /v1/primary-storage/{uuid}/consistency" + + header (Authorization: 'OAuth the-session-uuid') + + clz APICheckPrimaryStorageConsistencyMsg.class + + desc """检查指定主存储的一致性状态""" + + params { + + column { + name "uuid" + enclosedIn "" + desc "主存储的UUID" + location "url" + type "String" + optional false + since "5.0.0" + } + column { + name "systemTags" + enclosedIn "" + desc "系统标签" + location "query" + type "List" + optional true + since "5.0.0" + } + column { + name "userTags" + enclosedIn "" + desc "用户标签" + location "query" + type "List" + optional true + since "5.0.0" + } + } + } + + response { + clz APICheckPrimaryStorageConsistencyReply.class + } + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/storage/primary/APICheckPrimaryStorageConsistencyReply.java b/header/src/main/java/org/zstack/header/storage/primary/APICheckPrimaryStorageConsistencyReply.java new file mode 100644 index 00000000000..864e2862b70 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/APICheckPrimaryStorageConsistencyReply.java @@ -0,0 +1,42 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.APIReply; +import org.zstack.header.rest.RestResponse; + +@RestResponse(fieldsTo = {"all"}) +public class APICheckPrimaryStorageConsistencyReply extends APIReply { + private boolean consistent; + private ConsistencyCheckStatus reason; + private String candidateVgUuid; + + public boolean isConsistent() { + return consistent; + } + + public void setConsistent(boolean consistent) { + this.consistent = consistent; + } + + public ConsistencyCheckStatus getReason() { + return reason; + } + + public void setReason(ConsistencyCheckStatus reason) { + this.reason = reason; + } + + public String getCandidateVgUuid() { + return candidateVgUuid; + } + + public void setCandidateVgUuid(String candidateVgUuid) { + this.candidateVgUuid = candidateVgUuid; + } + + public static APICheckPrimaryStorageConsistencyReply __example__() { + APICheckPrimaryStorageConsistencyReply reply = new APICheckPrimaryStorageConsistencyReply(); + reply.setConsistent(true); + reply.setReason(ConsistencyCheckStatus.CONSISTENT); + return reply; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/APICheckPrimaryStorageConsistencyReplyDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/primary/APICheckPrimaryStorageConsistencyReplyDoc_zh_cn.groovy new file mode 100644 index 00000000000..4487853f25c --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/APICheckPrimaryStorageConsistencyReplyDoc_zh_cn.groovy @@ -0,0 +1,41 @@ +package org.zstack.header.storage.primary + +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "检查存储一致性返回" + + field { + name "consistent" + desc "是否一致" + type "boolean" + since "5.0.0" + } + field { + name "reason" + desc "一致性检查结果: CONSISTENT(VG 存在且 UUID 一致)/ UUID_MISMATCH(VG 存在但 UUID 不一致,可执行接管)/ VG_NOT_FOUND(未找到 WWID 匹配的 VG)" + type "ConsistencyCheckStatus" + since "5.0.0" + } + field { + name "candidateVgUuid" + desc "reason 为 UUID_MISMATCH 时,存储上实际找到的 VG UUID(即接管候选);其他情况为 null" + type "String" + since "5.0.0" + } + field { + name "success" + desc "操作是否成功" + type "boolean" + since "5.0.0" + } + ref { + name "error" + path "org.zstack.header.storage.primary.APICheckPrimaryStorageConsistencyReply.error" + desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null" + type "ErrorCode" + since "5.0.0" + clz ErrorCode.class + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/APITakeoverPrimaryStorageEvent.java b/header/src/main/java/org/zstack/header/storage/primary/APITakeoverPrimaryStorageEvent.java new file mode 100644 index 00000000000..47b76fc5e21 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/APITakeoverPrimaryStorageEvent.java @@ -0,0 +1,60 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.APIEvent; +import org.zstack.header.rest.RestResponse; + +import java.util.Collections; + +@RestResponse(fieldsTo = {"all"}) +public class APITakeoverPrimaryStorageEvent extends APIEvent { + private PrimaryStorageInventory inventory; + + private ReconnectResult reconnectResult; + + private String reconnectError; + + public APITakeoverPrimaryStorageEvent() { + } + + public APITakeoverPrimaryStorageEvent(String apiId) { + super(apiId); + } + + public PrimaryStorageInventory getInventory() { + return inventory; + } + + public void setInventory(PrimaryStorageInventory inventory) { + this.inventory = inventory; + } + + public ReconnectResult getReconnectResult() { + return reconnectResult; + } + + public void setReconnectResult(ReconnectResult reconnectResult) { + this.reconnectResult = reconnectResult; + } + + public String getReconnectError() { + return reconnectError; + } + + public void setReconnectError(String reconnectError) { + this.reconnectError = reconnectError; + } + + public static APITakeoverPrimaryStorageEvent __example__() { + APITakeoverPrimaryStorageEvent event = new APITakeoverPrimaryStorageEvent(); + + PrimaryStorageInventory ps = new PrimaryStorageInventory(); + ps.setName("PS1"); + ps.setUrl("/zstack_ps"); + ps.setType("SharedBlock"); + ps.setAttachedClusterUuids(Collections.singletonList(uuid())); + + event.setInventory(ps); + event.setReconnectResult(ReconnectResult.SUCCESS); + return event; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/APITakeoverPrimaryStorageEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/primary/APITakeoverPrimaryStorageEventDoc_zh_cn.groovy new file mode 100644 index 00000000000..a40aad2cd59 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/APITakeoverPrimaryStorageEventDoc_zh_cn.groovy @@ -0,0 +1,44 @@ +package org.zstack.header.storage.primary + +import org.zstack.header.storage.primary.PrimaryStorageInventory +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "接管主存储返回" + + ref { + name "inventory" + path "org.zstack.header.storage.primary.APITakeoverPrimaryStorageEvent.inventory" + desc "主存储信息" + type "PrimaryStorageInventory" + since "5.0.0" + clz PrimaryStorageInventory.class + } + field { + name "success" + desc "操作是否成功" + type "boolean" + since "5.0.0" + } + ref { + name "error" + path "org.zstack.header.storage.primary.APITakeoverPrimaryStorageEvent.error" + desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null" + type "ErrorCode" + since "5.0.0" + clz ErrorCode.class + } + field { + name "reconnectResult" + desc "接管后重连结果,取值参见 ReconnectResult 枚举: SUCCESS(重连成功)/ FAILED(重连失败,但接管已完成且不可逆)/ NOT_ATTEMPTED(未尝试重连)" + type "ReconnectResult" + since "5.0.0" + } + field { + name "reconnectError" + desc "重连失败时的错误信息" + type "String" + since "5.0.0" + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/APITakeoverPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APITakeoverPrimaryStorageMsg.java new file mode 100644 index 00000000000..4fbc9e5e5c3 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/APITakeoverPrimaryStorageMsg.java @@ -0,0 +1,40 @@ +package org.zstack.header.storage.primary; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.message.DefaultTimeout; +import org.zstack.header.rest.RestRequest; + +import java.util.concurrent.TimeUnit; + +@RestRequest( + path = "/primary-storage/{uuid}/takeover", + responseClass = APITakeoverPrimaryStorageEvent.class, + method = HttpMethod.PUT, + isAction = true +) +@DefaultTimeout(timeunit = TimeUnit.HOURS, value = 1) +public class APITakeoverPrimaryStorageMsg extends APIMessage implements PrimaryStorageMessage { + @APIParam(resourceType = PrimaryStorageVO.class) + private String uuid; + + @Override + public String getPrimaryStorageUuid() { + return uuid; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public static APITakeoverPrimaryStorageMsg __example__() { + APITakeoverPrimaryStorageMsg msg = new APITakeoverPrimaryStorageMsg(); + msg.setUuid(uuid(PrimaryStorageVO.class)); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/APITakeoverPrimaryStorageMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/primary/APITakeoverPrimaryStorageMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..802d0902dc6 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/APITakeoverPrimaryStorageMsgDoc_zh_cn.groovy @@ -0,0 +1,61 @@ +package org.zstack.header.storage.primary + +import org.zstack.header.storage.primary.APITakeoverPrimaryStorageEvent + +doc { + title "TakeoverPrimaryStorage" + + category "storage.primary" + + desc """接管主存储。将其他ZStack平台的共享块主存储接管到当前平台。 +前置条件:主存储必须通过APICheckPrimaryStorageConsistencyMsg检查且consistent=false。 +接管操作不可逆(agent侧会执行VG rename、PV UUID reset、sanlock lockspace reset)。 +接管完成后自动触发reconnect,通过返回的reconnectResult/reconnectError获取重连状态。""" + + rest { + request { + url "PUT /v1/primary-storage/{uuid}/takeover" + + header (Authorization: 'OAuth the-session-uuid') + + clz APITakeoverPrimaryStorageMsg.class + + desc """接管指定主存储""" + + params { + + column { + name "uuid" + enclosedIn "takeoverPrimaryStorage" + desc "主存储的UUID" + location "url" + type "String" + optional false + since "5.0.0" + } + column { + name "systemTags" + enclosedIn "" + desc "系统标签" + location "body" + type "List" + optional true + since "5.0.0" + } + column { + name "userTags" + enclosedIn "" + desc "用户标签" + location "body" + type "List" + optional true + since "5.0.0" + } + } + } + + response { + clz APITakeoverPrimaryStorageEvent.class + } + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/storage/primary/ConsistencyCheckStatus.java b/header/src/main/java/org/zstack/header/storage/primary/ConsistencyCheckStatus.java new file mode 100644 index 00000000000..d09ca702a90 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/ConsistencyCheckStatus.java @@ -0,0 +1,10 @@ +package org.zstack.header.storage.primary; + +public enum ConsistencyCheckStatus { + /** VG found by WWID match, UUID matches the database — fully consistent */ + CONSISTENT, + /** VG found by WWID match, but its UUID differs from the database — takeover candidate */ + UUID_MISMATCH, + /** Hosts returned complete VG data, but no VG has a matching WWID set */ + VG_NOT_FOUND +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/ReconnectResult.java b/header/src/main/java/org/zstack/header/storage/primary/ReconnectResult.java new file mode 100644 index 00000000000..b4d7bb5241b --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/ReconnectResult.java @@ -0,0 +1,7 @@ +package org.zstack.header.storage.primary; + +public enum ReconnectResult { + SUCCESS, + FAILED, + NOT_ATTEMPTED +} diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 0970b63b62f..232283f4d80 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -249,9 +249,11 @@ public class SourceClassMap { put("org.zstack.header.storage.database.backup.DatabaseBackupStorageRefInventory", "org.zstack.sdk.databasebackup.DatabaseBackupStorageRefInventory"); put("org.zstack.header.storage.database.backup.DatabaseBackupStruct", "org.zstack.sdk.databasebackup.DatabaseBackupStruct"); put("org.zstack.header.storage.database.backup.DatabaseType", "org.zstack.sdk.databasebackup.DatabaseType"); + put("org.zstack.header.storage.primary.ConsistencyCheckStatus", "org.zstack.sdk.ConsistencyCheckStatus"); put("org.zstack.header.storage.primary.ImageCacheInventory", "org.zstack.sdk.ImageCacheInventory"); put("org.zstack.header.storage.primary.PrimaryStorageHostStatus", "org.zstack.sdk.PrimaryStorageHostStatus"); put("org.zstack.header.storage.primary.PrimaryStorageInventory", "org.zstack.sdk.PrimaryStorageInventory"); + put("org.zstack.header.storage.primary.ReconnectResult", "org.zstack.sdk.ReconnectResult"); put("org.zstack.header.storage.primary.UsageReport", "org.zstack.sdk.UsageReport"); put("org.zstack.header.storage.snapshot.BatchDeleteVolumeSnapshotStruct", "org.zstack.sdk.BatchDeleteVolumeSnapshotStruct"); put("org.zstack.header.storage.snapshot.ShrinkResult", "org.zstack.sdk.ShrinkResult"); @@ -564,6 +566,7 @@ public class SourceClassMap { put("org.zstack.storage.primary.sharedblock.SharedBlockGroupPrimaryStorageHostRefInventory", "org.zstack.sdk.SharedBlockGroupPrimaryStorageHostRefInventory"); put("org.zstack.storage.primary.sharedblock.SharedBlockGroupPrimaryStorageInventory", "org.zstack.sdk.SharedBlockGroupPrimaryStorageInventory"); put("org.zstack.storage.primary.sharedblock.SharedBlockGroupType", "org.zstack.sdk.SharedBlockGroupType"); + put("org.zstack.storage.primary.sharedblock.SharedBlockGroupVgInfo", "org.zstack.sdk.SharedBlockGroupVgInfo"); put("org.zstack.storage.primary.sharedblock.SharedBlockInventory", "org.zstack.sdk.SharedBlockInventory"); put("org.zstack.storage.primary.sharedblock.SharedBlockState", "org.zstack.sdk.SharedBlockState"); put("org.zstack.storage.primary.sharedblock.SharedBlockStatus", "org.zstack.sdk.SharedBlockStatus"); @@ -775,6 +778,7 @@ public class SourceClassMap { put("org.zstack.sdk.CloudFormationStackEventInventory", "org.zstack.header.cloudformation.CloudFormationStackEventInventory"); put("org.zstack.sdk.ClusterDRSInventory", "org.zstack.drs.entity.ClusterDRSInventory"); put("org.zstack.sdk.ClusterInventory", "org.zstack.header.cluster.ClusterInventory"); + put("org.zstack.sdk.ConsistencyCheckStatus", "org.zstack.header.storage.primary.ConsistencyCheckStatus"); put("org.zstack.sdk.ConsoleInventory", "org.zstack.header.console.ConsoleInventory"); put("org.zstack.sdk.ConsoleProxyAgentInventory", "org.zstack.header.console.ConsoleProxyAgentInventory"); put("org.zstack.sdk.ControlStrategy", "org.zstack.loginControl.entity.ControlStrategy"); @@ -1040,6 +1044,7 @@ public class SourceClassMap { put("org.zstack.sdk.QuotaUsage", "org.zstack.header.identity.Quota$QuotaUsage"); put("org.zstack.sdk.RaidControllerInventory", "org.zstack.storage.device.localRaid.RaidControllerInventory"); put("org.zstack.sdk.RaidPhysicalDriveInventory", "org.zstack.storage.device.localRaid.RaidPhysicalDriveInventory"); + put("org.zstack.sdk.ReconnectResult", "org.zstack.header.storage.primary.ReconnectResult"); put("org.zstack.sdk.RedirectUrlTemplate", "org.zstack.sso.header.RedirectUrlTemplate"); put("org.zstack.sdk.RemoteVtepInventory", "org.zstack.network.l2.vxlan.vtep.RemoteVtepInventory"); put("org.zstack.sdk.RemovalInstanceRuleInventory", "org.zstack.autoscaling.group.rule.RemovalInstanceRuleInventory"); @@ -1093,6 +1098,7 @@ public class SourceClassMap { put("org.zstack.sdk.SharedBlockGroupPrimaryStorageHostRefInventory", "org.zstack.storage.primary.sharedblock.SharedBlockGroupPrimaryStorageHostRefInventory"); put("org.zstack.sdk.SharedBlockGroupPrimaryStorageInventory", "org.zstack.storage.primary.sharedblock.SharedBlockGroupPrimaryStorageInventory"); put("org.zstack.sdk.SharedBlockGroupType", "org.zstack.storage.primary.sharedblock.SharedBlockGroupType"); + put("org.zstack.sdk.SharedBlockGroupVgInfo", "org.zstack.storage.primary.sharedblock.SharedBlockGroupVgInfo"); put("org.zstack.sdk.SharedBlockInventory", "org.zstack.storage.primary.sharedblock.SharedBlockInventory"); put("org.zstack.sdk.SharedBlockState", "org.zstack.storage.primary.sharedblock.SharedBlockState"); put("org.zstack.sdk.SharedBlockStatus", "org.zstack.storage.primary.sharedblock.SharedBlockStatus"); diff --git a/sdk/src/main/java/org/zstack/sdk/CheckPrimaryStorageConsistencyAction.java b/sdk/src/main/java/org/zstack/sdk/CheckPrimaryStorageConsistencyAction.java new file mode 100644 index 00000000000..5c86726b2d3 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/CheckPrimaryStorageConsistencyAction.java @@ -0,0 +1,95 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class CheckPrimaryStorageConsistencyAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.CheckPrimaryStorageConsistencyResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.CheckPrimaryStorageConsistencyResult value = res.getResult(org.zstack.sdk.CheckPrimaryStorageConsistencyResult.class); + ret.value = value == null ? new org.zstack.sdk.CheckPrimaryStorageConsistencyResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "GET"; + info.path = "/primary-storage/{uuid}/consistency"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/CheckPrimaryStorageConsistencyResult.java b/sdk/src/main/java/org/zstack/sdk/CheckPrimaryStorageConsistencyResult.java new file mode 100644 index 00000000000..f26ef81ce7e --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/CheckPrimaryStorageConsistencyResult.java @@ -0,0 +1,28 @@ +package org.zstack.sdk; + +public class CheckPrimaryStorageConsistencyResult { + public boolean consistent; + public void setConsistent(boolean consistent) { + this.consistent = consistent; + } + public boolean getConsistent() { + return this.consistent; + } + + public ConsistencyCheckStatus reason; + public void setReason(ConsistencyCheckStatus reason) { + this.reason = reason; + } + public ConsistencyCheckStatus getReason() { + return this.reason; + } + + public java.lang.String candidateVgUuid; + public void setCandidateVgUuid(java.lang.String candidateVgUuid) { + this.candidateVgUuid = candidateVgUuid; + } + public java.lang.String getCandidateVgUuid() { + return this.candidateVgUuid; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/ConsistencyCheckStatus.java b/sdk/src/main/java/org/zstack/sdk/ConsistencyCheckStatus.java new file mode 100644 index 00000000000..ea21d43736a --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/ConsistencyCheckStatus.java @@ -0,0 +1,7 @@ +package org.zstack.sdk; + +public enum ConsistencyCheckStatus { + CONSISTENT, + UUID_MISMATCH, + VG_NOT_FOUND, +} diff --git a/sdk/src/main/java/org/zstack/sdk/DiscoverSharedBlockGroupVgsAction.java b/sdk/src/main/java/org/zstack/sdk/DiscoverSharedBlockGroupVgsAction.java new file mode 100644 index 00000000000..e88feb2199e --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DiscoverSharedBlockGroupVgsAction.java @@ -0,0 +1,95 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class DiscoverSharedBlockGroupVgsAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.DiscoverSharedBlockGroupVgsResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String clusterUuid; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.DiscoverSharedBlockGroupVgsResult value = res.getResult(org.zstack.sdk.DiscoverSharedBlockGroupVgsResult.class); + ret.value = value == null ? new org.zstack.sdk.DiscoverSharedBlockGroupVgsResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "GET"; + info.path = "/primary-storage/sharedblockgroup/vgs"; + info.needSession = true; + info.needPoll = false; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/DiscoverSharedBlockGroupVgsResult.java b/sdk/src/main/java/org/zstack/sdk/DiscoverSharedBlockGroupVgsResult.java new file mode 100644 index 00000000000..9d23a4446e7 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/DiscoverSharedBlockGroupVgsResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + + + +public class DiscoverSharedBlockGroupVgsResult { + public java.util.Map vgInfos; + public void setVgInfos(java.util.Map vgInfos) { + this.vgInfos = vgInfos; + } + public java.util.Map getVgInfos() { + return this.vgInfos; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/ReconnectResult.java b/sdk/src/main/java/org/zstack/sdk/ReconnectResult.java new file mode 100644 index 00000000000..30eadb24630 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/ReconnectResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk; + +public enum ReconnectResult { + SUCCESS, + FAILED, + NOT_ATTEMPTED, +} diff --git a/sdk/src/main/java/org/zstack/sdk/SharedBlockGroupVgInfo.java b/sdk/src/main/java/org/zstack/sdk/SharedBlockGroupVgInfo.java new file mode 100644 index 00000000000..3bd931409ed --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/SharedBlockGroupVgInfo.java @@ -0,0 +1,31 @@ +package org.zstack.sdk; + + + +public class SharedBlockGroupVgInfo { + + public java.util.List candidateLuns; + public void setCandidateLuns(java.util.List candidateLuns) { + this.candidateLuns = candidateLuns; + } + public java.util.List getCandidateLuns() { + return this.candidateLuns; + } + + public boolean sharedGroupComplete; + public void setSharedGroupComplete(boolean sharedGroupComplete) { + this.sharedGroupComplete = sharedGroupComplete; + } + public boolean getSharedGroupComplete() { + return this.sharedGroupComplete; + } + + public java.util.Map existLunWwidsByHost; + public void setExistLunWwidsByHost(java.util.Map existLunWwidsByHost) { + this.existLunWwidsByHost = existLunWwidsByHost; + } + public java.util.Map getExistLunWwidsByHost() { + return this.existLunWwidsByHost; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/TakeoverPrimaryStorageAction.java b/sdk/src/main/java/org/zstack/sdk/TakeoverPrimaryStorageAction.java new file mode 100644 index 00000000000..f168002ef64 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/TakeoverPrimaryStorageAction.java @@ -0,0 +1,101 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class TakeoverPrimaryStorageAction extends AbstractAction { + + private static final HashMap parameterMap = new HashMap<>(); + + private static final HashMap nonAPIParameterMap = new HashMap<>(); + + public static class Result { + public ErrorCode error; + public org.zstack.sdk.TakeoverPrimaryStorageResult value; + + public Result throwExceptionIfError() { + if (error != null) { + throw new ApiException( + String.format("error[code: %s, description: %s, details: %s]", error.code, error.description, error.details) + ); + } + + return this; + } + } + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String uuid; + + @Param(required = false) + public java.util.List systemTags; + + @Param(required = false) + public java.util.List userTags; + + @Param(required = false) + public String sessionId; + + @Param(required = false) + public String accessKeyId; + + @Param(required = false) + public String accessKeySecret; + + @Param(required = false) + public String requestIp; + + @NonAPIParam + public long timeout = -1; + + @NonAPIParam + public long pollingInterval = -1; + + + private Result makeResult(ApiResult res) { + Result ret = new Result(); + if (res.error != null) { + ret.error = res.error; + return ret; + } + + org.zstack.sdk.TakeoverPrimaryStorageResult value = res.getResult(org.zstack.sdk.TakeoverPrimaryStorageResult.class); + ret.value = value == null ? new org.zstack.sdk.TakeoverPrimaryStorageResult() : value; + + return ret; + } + + public Result call() { + ApiResult res = ZSClient.call(this); + return makeResult(res); + } + + public void call(final Completion completion) { + ZSClient.call(this, new InternalCompletion() { + @Override + public void complete(ApiResult res) { + completion.complete(makeResult(res)); + } + }); + } + + protected Map getParameterMap() { + return parameterMap; + } + + protected Map getNonAPIParameterMap() { + return nonAPIParameterMap; + } + + protected RestInfo getRestInfo() { + RestInfo info = new RestInfo(); + info.httpMethod = "PUT"; + info.path = "/primary-storage/{uuid}/takeover"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "takeoverPrimaryStorage"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/TakeoverPrimaryStorageResult.java b/sdk/src/main/java/org/zstack/sdk/TakeoverPrimaryStorageResult.java new file mode 100644 index 00000000000..6b76df3b5bf --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/TakeoverPrimaryStorageResult.java @@ -0,0 +1,31 @@ +package org.zstack.sdk; + +import org.zstack.sdk.PrimaryStorageInventory; +import org.zstack.sdk.ReconnectResult; + +public class TakeoverPrimaryStorageResult { + public PrimaryStorageInventory inventory; + public void setInventory(PrimaryStorageInventory inventory) { + this.inventory = inventory; + } + public PrimaryStorageInventory getInventory() { + return this.inventory; + } + + public ReconnectResult reconnectResult; + public void setReconnectResult(ReconnectResult reconnectResult) { + this.reconnectResult = reconnectResult; + } + public ReconnectResult getReconnectResult() { + return this.reconnectResult; + } + + public java.lang.String reconnectError; + public void setReconnectError(java.lang.String reconnectError) { + this.reconnectError = reconnectError; + } + public java.lang.String getReconnectError() { + return this.reconnectError; + } + +} diff --git a/storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java b/storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java index b7f8cfbc24d..5798d7c6951 100755 --- a/storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java +++ b/storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java @@ -935,11 +935,27 @@ protected void handleApiMessage(APIMessage msg) { handle((APICleanUpStorageTrashOnPrimaryStorageMsg) msg); } else if (msg instanceof APIAddStorageProtocolMsg) { handle((APIAddStorageProtocolMsg) msg); + } else if (msg instanceof APITakeoverPrimaryStorageMsg) { + handle((APITakeoverPrimaryStorageMsg) msg); + } else if (msg instanceof APICheckPrimaryStorageConsistencyMsg) { + handle((APICheckPrimaryStorageConsistencyMsg) msg); } else { bus.dealWithUnknownMessage(msg); } } + protected void handle(APITakeoverPrimaryStorageMsg msg) { + APITakeoverPrimaryStorageEvent event = new APITakeoverPrimaryStorageEvent(msg.getId()); + event.setError(operr("takeover not supported for primary storage type[%s]", self.getType())); + bus.publish(event); + } + + protected void handle(APICheckPrimaryStorageConsistencyMsg msg) { + APICheckPrimaryStorageConsistencyReply reply = new APICheckPrimaryStorageConsistencyReply(); + reply.setError(operr("consistency check not supported for primary storage type[%s]", self.getType())); + bus.reply(msg, reply); + } + private void handle(APIAddStorageProtocolMsg msg) { APIAddStorageProtocolEvent evt = new APIAddStorageProtocolEvent(msg.getId()); addStorageProtocol(msg, new Completion(msg) { diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index 24fd79034bd..b5e57b47412 100644 --- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy @@ -5282,6 +5282,33 @@ abstract class ApiHelper { } + def checkPrimaryStorageConsistency(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.CheckPrimaryStorageConsistencyAction.class) Closure c) { + def a = new org.zstack.sdk.CheckPrimaryStorageConsistencyAction() + 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 checkScsiLunClusterStatus(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.CheckScsiLunClusterStatusAction.class) Closure c) { def a = new org.zstack.sdk.CheckScsiLunClusterStatusAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -17621,6 +17648,33 @@ abstract class ApiHelper { } + def discoverSharedBlockGroupVgs(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.DiscoverSharedBlockGroupVgsAction.class) Closure c) { + def a = new org.zstack.sdk.DiscoverSharedBlockGroupVgsAction() + 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 getSignatureServerEncryptPublicKey(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.GetSignatureServerEncryptPublicKeyAction.class) Closure c) { def a = new org.zstack.sdk.GetSignatureServerEncryptPublicKeyAction() @@ -30457,6 +30511,33 @@ abstract class ApiHelper { } + def takeoverPrimaryStorage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.TakeoverPrimaryStorageAction.class) Closure c) { + def a = new org.zstack.sdk.TakeoverPrimaryStorageAction() + 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 triggerGCJob(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.TriggerGCJobAction.class) Closure c) { def a = new org.zstack.sdk.TriggerGCJobAction() a.sessionId = Test.currentEnvSpec?.session?.uuid