From adf7220f270ee157f9dca8558b09779d6733223e Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 13 Jan 2026 14:57:37 +0800 Subject: [PATCH 001/129] [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 bba3f9203835ea387cef283f201ba84e075ed794 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 13 Jan 2026 17:27:47 +0800 Subject: [PATCH 002/129] [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 27aef222704a7682fc7ed6c48de6c51b6b0aa4c6 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 5 Feb 2026 10:40:16 +0800 Subject: [PATCH 003/129] [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 3056eacd8816a5df42d963ef219106dad915389f Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 3 Feb 2026 10:27:48 +0800 Subject: [PATCH 004/129] [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 7c044f986194089f2febbeda39098e794cab0d2c Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 4 Feb 2026 15:21:52 +0800 Subject: [PATCH 005/129] [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 3555fa0e3d02cbab8085c95e3fa527dfb99ab23e Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 4 Feb 2026 18:30:53 +0800 Subject: [PATCH 006/129] [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 c6e9edb14bdc78d2a818700a6ffcb8c029a2a6e3 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 11 Feb 2026 15:35:53 +0800 Subject: [PATCH 007/129] [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 718a3ea7e40bc2cb5d81b7881640d9639b074044 Mon Sep 17 00:00:00 2001 From: "tao.yang" Date: Fri, 6 Feb 2026 19:55:28 +0800 Subject: [PATCH 008/129] [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 7bdc854e12583e575a6d0cfa0c078570658ee7d7 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 11 Feb 2026 18:55:48 +0800 Subject: [PATCH 009/129] [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 02e8901cb4cd24c81a6072a9b15d9ae0f64bfc03 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 12 Feb 2026 14:40:34 +0800 Subject: [PATCH 010/129] [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 27816e8d570870da423c97269a2d0180539166d6 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 24 Feb 2026 16:15:59 +0800 Subject: [PATCH 011/129] [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 423c2936ef0798303b833ebb21b33889b00cba30 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 13 Feb 2026 14:59:35 +0800 Subject: [PATCH 012/129] [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 1abd76bb3f6622517cf5e0849628147b0c20fda6 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 13 Feb 2026 18:28:55 +0800 Subject: [PATCH 013/129] [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 dbe3f138760729369d3e34070b7bbea1be251e10 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 26 Feb 2026 11:13:35 +0800 Subject: [PATCH 014/129] [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 fa473add7a03a48dc7ff6715edce71514732f165 Mon Sep 17 00:00:00 2001 From: "tao.yang" Date: Thu, 26 Feb 2026 21:40:29 +0800 Subject: [PATCH 015/129] [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 8ddbb482fd0bc3f269e0b11c6e766c6af8011ed7 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 3 Mar 2026 14:34:39 +0800 Subject: [PATCH 016/129] [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 b0127a61834cb6d367222407db41903790ddacdc Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 5 Mar 2026 17:49:21 +0800 Subject: [PATCH 017/129] [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 8129f5ace484757511eac99a7c78365ea38f5d6f Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 6 Mar 2026 10:15:37 +0800 Subject: [PATCH 018/129] [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 78dde286bdc01112bd8b34ab7ce09c255c2bc5a5 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 14:10:15 +0800 Subject: [PATCH 019/129] 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 a01bae3a95f..813b56e2dc4 100644 --- a/conf/db/zsv/V5.0.0__schema.sql +++ b/conf/db/zsv/V5.0.0__schema.sql @@ -105,6 +105,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 cae9533b7f2..2a07596e767 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 ae6facb162495fb8fce7bf9023f3ceac81d8edde Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 14:18:24 +0800 Subject: [PATCH 020/129] 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 b3df320b006c6029ff0d950fdfb37974eb8d5e20 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 14:21:13 +0800 Subject: [PATCH 021/129] 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 b1a961b7c9993a825493bf3982bd578f0c606277 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 14:43:34 +0800 Subject: [PATCH 022/129] 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 7680570158e50fc2d6d33cd74df8d256b635b781 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 14:44:41 +0800 Subject: [PATCH 023/129] 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 189350f075dc83f7290f4f2e0c13f31fc64a5738 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 14:45:39 +0800 Subject: [PATCH 024/129] 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 35de65a318875f13a4007ea2a1d14cb553f8314b Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 15:30:24 +0800 Subject: [PATCH 025/129] 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 24f2540994b3dab4a5ea97b021d0c3d3dc037d69 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 16:00:20 +0800 Subject: [PATCH 026/129] 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 73a94a2a7419d05754caf9b2e1fb457557e32451 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 17:03:24 +0800 Subject: [PATCH 027/129] 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 c0e275915564718d57a25eaab24a1a131458ec79 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 17:42:15 +0800 Subject: [PATCH 028/129] 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 11c292c2b550951cc369f02213093027f1d8e327 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Fri, 6 Mar 2026 18:20:25 +0800 Subject: [PATCH 029/129] 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 881c597cf8f4a39ed7ff16db27a09f9e68ff5a76 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Sat, 7 Mar 2026 19:57:13 +0800 Subject: [PATCH 030/129] 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 fe36f412138b37964025474da5bb30dc088fa550 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Sat, 7 Mar 2026 20:02:09 +0800 Subject: [PATCH 031/129] 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 ee0de70ab37ef51d598183ec6b5c2f619c9021c5 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Sun, 8 Mar 2026 16:58:05 +0800 Subject: [PATCH 032/129] 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 6f4dd1333ae1055b095f5d43b198544b041ee7fa Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Sun, 8 Mar 2026 18:12:25 +0800 Subject: [PATCH 033/129] 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 48ea2ba7eae83a7acbb0f2f11fb41882372da21c Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Sun, 8 Mar 2026 18:28:12 +0800 Subject: [PATCH 034/129] 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 3672c7e68fadb6b01228a1334733bea35aa03423 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Sun, 8 Mar 2026 18:33:14 +0800 Subject: [PATCH 035/129] 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 bd4a432e0848cd8a0bf6156a5cdea6812f5b4a61 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Mon, 9 Mar 2026 14:06:32 +0800 Subject: [PATCH 036/129] 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 b36ace4aa92ca6b906cadb7f358c4ba71145842b Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Mon, 9 Mar 2026 17:54:44 +0800 Subject: [PATCH 037/129] 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 d0e2034a2a2317639cc80d0c464ce1f4ab9fa045 Mon Sep 17 00:00:00 2001 From: "tao.yang" Date: Fri, 6 Mar 2026 17:43:07 +0800 Subject: [PATCH 038/129] [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 b384d57bde2a327c5cc39f17cb975ea6779642e6 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Tue, 10 Mar 2026 14:06:15 +0800 Subject: [PATCH 039/129] 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 21350fae8721065e3285a81be4bc578fb0238441 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Tue, 10 Mar 2026 15:25:31 +0800 Subject: [PATCH 040/129] 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 1424cd7b85444e5575a8838483a4de4a03540bcf Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 10 Mar 2026 15:59:11 +0800 Subject: [PATCH 041/129] [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 01bd63d88d52de9bedf796cbecaff27adeabf38c Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Tue, 10 Mar 2026 17:36:35 +0800 Subject: [PATCH 042/129] 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 2a07596e767..55f35005d7b 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 158acc2e0b71ed8f45ea376ebe8d5d9f5521a301 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Tue, 10 Mar 2026 18:31:52 +0800 Subject: [PATCH 043/129] 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 55f35005d7b..d07b318cf72 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 a0ef764216a49f6a1460bc1eedbb2330c58f6536 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Tue, 10 Mar 2026 20:49:46 +0800 Subject: [PATCH 044/129] 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 504248326268b4b4ae05b78c769008184c5207b2 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Wed, 11 Mar 2026 10:33:52 +0800 Subject: [PATCH 045/129] 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 be3c6bb092eec293d727e12e7a46aea47b99d508 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Wed, 11 Mar 2026 10:38:28 +0800 Subject: [PATCH 046/129] 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 d6efecf64105ce8114956e9108c503b58b3d09e5 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Wed, 11 Mar 2026 10:50:46 +0800 Subject: [PATCH 047/129] 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 99f132dc1f2e29646b165773eb0d319c6e0db373 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Wed, 11 Mar 2026 12:01:14 +0800 Subject: [PATCH 048/129] 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 733d34b671af06c85fd00977b14dd6ff1273fa77 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Wed, 11 Mar 2026 12:41:32 +0800 Subject: [PATCH 049/129] 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 85d9d2a009b6b743290b3ce4820686d80bbbbe3c Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Wed, 11 Mar 2026 13:20:10 +0800 Subject: [PATCH 050/129] 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 95e9d4287a506f66d16ef6bfb7353e193ce24b36 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 11 Mar 2026 12:32:01 +0800 Subject: [PATCH 051/129] [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 736ad28af30cd6ce6f50268d1e38c8945157002c Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 12 Mar 2026 16:03:33 +0800 Subject: [PATCH 052/129] [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 f03734152a5..364995aa0fd 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 aa49c3a1389db13b27b6b75c609f500226bf3ce1 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 12 Mar 2026 14:38:30 +0800 Subject: [PATCH 053/129] [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 cc4835ecbad7d6b87a74eb9753da77f8de6a0cf4 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 12 Mar 2026 16:35:10 +0800 Subject: [PATCH 054/129] [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 572c93ada80e8fd7a4188b95892c7757f1f5e6d2 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 12 Mar 2026 21:48:35 +0800 Subject: [PATCH 055/129] [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 e25e748f4915934e6e0ed2c05a415ab95d0ab81d Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Mon, 16 Mar 2026 13:37:08 +0800 Subject: [PATCH 056/129] [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 9355fcea4ee56acf2b9dc146b928d9e8c3059c49 Mon Sep 17 00:00:00 2001 From: "tao.yang" Date: Fri, 13 Mar 2026 18:41:11 +0800 Subject: [PATCH 057/129] [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 ac3b849ec1e..503e45cce0f 100644 --- a/conf/db/zsv/V5.0.0__schema.sql +++ b/conf/db/zsv/V5.0.0__schema.sql @@ -113,7 +113,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 202913877d9c870dc97a63607352857981ed52b8 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Mon, 16 Mar 2026 17:52:26 +0800 Subject: [PATCH 058/129] [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 0eecfd99c9d2c19b8d18f732f01aa0392547acd6 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 17 Mar 2026 19:09:22 +0800 Subject: [PATCH 059/129] [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 8ad8e91c704e7b965efc24043f98d8a94732dacd Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 18 Mar 2026 11:38:02 +0800 Subject: [PATCH 060/129] [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 b2962379cf8cb326a649efebc30cda66e7f75086 Mon Sep 17 00:00:00 2001 From: "tao.yang" Date: Wed, 18 Mar 2026 14:39:48 +0800 Subject: [PATCH 061/129] [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 262d5dab9a06fa2dc80d7649cde2180ae5763f5f Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 18 Mar 2026 18:29:52 +0800 Subject: [PATCH 062/129] [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 3d09ff824c366484c168275e43ed11f83af4043b Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 18 Mar 2026 19:34:34 +0800 Subject: [PATCH 063/129] [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 0435fd72d0a57e44348dadf1fb17614700d139dc Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Mon, 23 Mar 2026 10:36:18 +0800 Subject: [PATCH 064/129] [kvm]: support take snapshot on VM offline * Refactor The VM host backup file management mechanism * Changing the foreign key of `VmHostBackupFileVO` from `vmInstanceUuid` to `resourceUuid` * New backup-related message classes and fields have been added, and the backup logic in the KVM and storage modules has been extended to support the backup of EFI/TPM files. Resolves: ZSV-11441 Related: ZSV-11310 Change-Id: I737964696f74707576766677686f697670777477 --- .../compute/vm/VmInstanceManagerImpl.java | 2 +- conf/db/zsv/V5.0.0__schema.sql | 5 +- .../snapshot/CreateVolumesSnapshotMsg.java | 10 ++ .../CreateVolumesSnapshotOverlayInnerMsg.java | 10 ++ ...reateVolumesSnapshotOverlayInnerReply.java | 9 + .../snapshot/CreateVolumesSnapshotReply.java | 9 + .../vm/additions/VmHostBackupFileVO.java | 16 +- .../vm/additions/VmHostBackupFileVO_.java | 2 +- .../zstack/kvm/efi/BackupVmHostFileMsg.java | 35 ++++ .../zstack/kvm/efi/BackupVmHostFileReply.java | 17 ++ .../kvm/efi/KvmSecureBootExtensions.java | 4 +- .../zstack/kvm/efi/KvmSecureBootManager.java | 157 +++++++++++------- .../group/VolumeSnapshotGroupBase.java | 62 ++++--- .../org/zstack/storage/volume/VolumeBase.java | 133 ++++++++++----- 14 files changed, 328 insertions(+), 143 deletions(-) create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileMsg.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileReply.java 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 418cd0fbd5b..2af241834d7 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceManagerImpl.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceManagerImpl.java @@ -895,7 +895,7 @@ private void handle(APICreateVmNicMsg msg) { FlowChain flowChain = FlowChainBuilder.newSimpleFlowChain(); flowChain.setName(String.format("create-nic-on-l3-network-%s", msg.getL3NetworkUuid())); flowChain.then(new NoRollbackFlow() { - String __name__ = "create-nic-and-presist-to-db"; + String __name__ = "create-nic-and-persist-to-db"; @Override public void run(FlowTrigger trigger, Map data) { diff --git a/conf/db/zsv/V5.0.0__schema.sql b/conf/db/zsv/V5.0.0__schema.sql index 808e3041120..3bdfbdcfaaf 100644 --- a/conf/db/zsv/V5.0.0__schema.sql +++ b/conf/db/zsv/V5.0.0__schema.sql @@ -25,13 +25,12 @@ CREATE TABLE IF NOT EXISTS `zstack`.`VmHostFileVO` ( CREATE TABLE IF NOT EXISTS `zstack`.`VmHostBackupFileVO` ( `uuid` char(32) NOT NULL UNIQUE, - `vmInstanceUuid` char(32) NOT NULL, + `resourceUuid` 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`) + UNIQUE KEY `ukVmHostBackupFileVO` (`resourceUuid`, `type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `zstack`.`VmHostFileContentVO` ( diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotMsg.java index 886f697c6df..919e0b9af8e 100755 --- a/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotMsg.java @@ -16,6 +16,8 @@ public class CreateVolumesSnapshotMsg extends NeedReplyMessage implements NeedQu private List volumeSnapshotJobs; + private boolean backupHostFileIfNeeded; + public String getAccountUuid() { return accountUuid; } @@ -44,4 +46,12 @@ public ConsistentType getConsistentType() { public void setConsistentType(ConsistentType consistentType) { this.consistentType = consistentType; } + + public boolean isBackupHostFileIfNeeded() { + return backupHostFileIfNeeded; + } + + public void setBackupHostFileIfNeeded(boolean backupHostFileIfNeeded) { + this.backupHostFileIfNeeded = backupHostFileIfNeeded; + } } diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotOverlayInnerMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotOverlayInnerMsg.java index 9e52d36b0d3..a9b4f6bdc62 100755 --- a/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotOverlayInnerMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotOverlayInnerMsg.java @@ -19,6 +19,8 @@ public class CreateVolumesSnapshotOverlayInnerMsg extends NeedReplyMessage imple private List lockedVolumeUuids; + private boolean backupHostFileIfNeeded; + public List getLockedVmInstanceUuids() { return lockedVmInstanceUuids; } @@ -63,4 +65,12 @@ public ConsistentType getConsistentType() { public void setConsistentType(ConsistentType consistentType) { this.consistentType = consistentType; } + + public boolean isBackupHostFileIfNeeded() { + return backupHostFileIfNeeded; + } + + public void setBackupHostFileIfNeeded(boolean backupHostFileIfNeeded) { + this.backupHostFileIfNeeded = backupHostFileIfNeeded; + } } diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotOverlayInnerReply.java b/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotOverlayInnerReply.java index a488079afc2..cd816f6014c 100644 --- a/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotOverlayInnerReply.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotOverlayInnerReply.java @@ -9,6 +9,7 @@ */ public class CreateVolumesSnapshotOverlayInnerReply extends MessageReply { private List inventories; + private List hostBackupFileUuidList; public List getInventories() { return inventories; @@ -17,4 +18,12 @@ public List getInventories() { public void setInventories(List inventories) { this.inventories = inventories; } + + public List getHostBackupFileUuidList() { + return hostBackupFileUuidList; + } + + public void setHostBackupFileUuidList(List hostBackupFileUuidList) { + this.hostBackupFileUuidList = hostBackupFileUuidList; + } } diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotReply.java b/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotReply.java index 41e1b0270f9..ac510f260b7 100755 --- a/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotReply.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/CreateVolumesSnapshotReply.java @@ -9,6 +9,7 @@ */ public class CreateVolumesSnapshotReply extends MessageReply { private List inventories; + private List hostBackupFileUuidList; public List getInventories() { return inventories; @@ -17,4 +18,12 @@ public List getInventories() { public void setInventories(List inventories) { this.inventories = inventories; } + + public List getHostBackupFileUuidList() { + return hostBackupFileUuidList; + } + + public void setHostBackupFileUuidList(List hostBackupFileUuidList) { + this.hostBackupFileUuidList = hostBackupFileUuidList; + } } 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 index 94662c612e0..383da0844a0 100644 --- a/header/src/main/java/org/zstack/header/vm/additions/VmHostBackupFileVO.java +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostBackupFileVO.java @@ -1,6 +1,5 @@ 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; @@ -21,13 +20,12 @@ @Table @EntityGraph( friends = { - @EntityGraph.Neighbour(type = VmInstanceEO.class, myField = "vmInstanceUuid", targetField = "uuid"), + @EntityGraph.Neighbour(type = ResourceVO.class, myField = "resourceUuid", targetField = "uuid"), } ) public class VmHostBackupFileVO extends ResourceVO { @Column - @ForeignKey(parentEntityClass = VmInstanceEO.class, onDeleteAction = ForeignKey.ReferenceOption.CASCADE) - private String vmInstanceUuid; + private String resourceUuid; @Column @Enumerated(EnumType.STRING) private VmHostFileType type; @@ -36,12 +34,12 @@ public class VmHostBackupFileVO extends ResourceVO { @Column private Timestamp lastOpDate; - public String getVmInstanceUuid() { - return vmInstanceUuid; + public String getResourceUuid() { + return resourceUuid; } - public void setVmInstanceUuid(String vmInstanceUuid) { - this.vmInstanceUuid = vmInstanceUuid; + public void setResourceUuid(String resourceUuid) { + this.resourceUuid = resourceUuid; } public VmHostFileType getType() { @@ -71,7 +69,7 @@ public void setLastOpDate(Timestamp lastOpDate) { @Override public String toString() { return "VmHostBackupFileVO{" + - "vmInstanceUuid='" + vmInstanceUuid + '\'' + + "resourceUuid='" + resourceUuid + '\'' + ", 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 index e45e804f381..355fb0661f9 100644 --- a/header/src/main/java/org/zstack/header/vm/additions/VmHostBackupFileVO_.java +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostBackupFileVO_.java @@ -8,7 +8,7 @@ @StaticMetamodel(VmHostBackupFileVO.class) public class VmHostBackupFileVO_ extends ResourceVO_ { - public static volatile SingularAttribute vmInstanceUuid; + public static volatile SingularAttribute resourceUuid; public static volatile SingularAttribute type; public static volatile SingularAttribute createDate; public static volatile SingularAttribute lastOpDate; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileMsg.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileMsg.java new file mode 100644 index 00000000000..1eafe523e1c --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileMsg.java @@ -0,0 +1,35 @@ +package org.zstack.kvm.efi; + +import org.zstack.header.message.NeedReplyMessage; + +import java.util.List; + +public class BackupVmHostFileMsg extends NeedReplyMessage { + private String vmUuid; + private String hostUuid; + private List toResourceUuidList; + + public String getVmUuid() { + return vmUuid; + } + + public void setVmUuid(String vmUuid) { + this.vmUuid = vmUuid; + } + + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + public List getToResourceUuidList() { + return toResourceUuidList; + } + + public void setToResourceUuidList(List toResourceUuidList) { + this.toResourceUuidList = toResourceUuidList; + } +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileReply.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileReply.java new file mode 100644 index 00000000000..a4d1c481c92 --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileReply.java @@ -0,0 +1,17 @@ +package org.zstack.kvm.efi; + +import org.zstack.header.message.MessageReply; + +import java.util.List; + +public class BackupVmHostFileReply extends MessageReply { + private List backupFileUuidList; + + public List getBackupFileUuidList() { + return backupFileUuidList; + } + + public void setBackupFileUuidList(List backupFileUuidList) { + this.backupFileUuidList = backupFileUuidList; + } +} 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 a2d815d96a1..69a977847ec 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 @@ -556,7 +556,7 @@ public boolean skip(Map data) { public void run(FlowTrigger trigger, Map data) { context.vmBackupFileVO = Q.New(VmHostBackupFileVO.class) .eq(VmHostBackupFileVO_.type, context.type) - .eq(VmHostBackupFileVO_.vmInstanceUuid, context.vmUuid) + .eq(VmHostBackupFileVO_.resourceUuid, context.vmUuid) .orderByDesc(VmHostBackupFileVO_.lastOpDate) .limit(1) .find(); @@ -835,7 +835,7 @@ public void afterDestroyVm(VmInstanceInventory inv) { .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) .delete(); SQL.New(VmHostBackupFileVO.class) - .eq(VmHostBackupFileVO_.vmInstanceUuid, vmUuid) + .eq(VmHostBackupFileVO_.resourceUuid, vmUuid) .delete(); } 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 1febc137e09..56a80935d37 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 @@ -7,6 +7,7 @@ import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.EventCallback; import org.zstack.core.cloudbus.EventFacadeImpl; +import org.zstack.core.cloudbus.MessageSafe; import org.zstack.core.db.Q; import org.zstack.core.db.SQLBatch; import org.zstack.core.workflow.SimpleFlowChain; @@ -143,9 +144,12 @@ public String getId() { } @Override + @MessageSafe public void handleMessage(Message msg) { if (msg instanceof CloneVmHostFileMsg) { handle((CloneVmHostFileMsg) msg); + } else if (msg instanceof BackupVmHostFileMsg) { + handle((BackupVmHostFileMsg) msg); } else { bus.dealWithUnknownMessage(msg); } @@ -287,7 +291,7 @@ public void run(FlowTrigger trigger, Map data) { } context.backupFiles.addAll(Q.New(VmHostBackupFileVO.class) - .eq(VmHostBackupFileVO_.vmInstanceUuid, msg.getSrcVmUuid()) + .eq(VmHostBackupFileVO_.resourceUuid, msg.getSrcVmUuid()) .in(VmHostFileVO_.type, missingTypes) .list()); trigger.next(); @@ -306,65 +310,7 @@ public void run(FlowTrigger trigger, Map data) { 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(); - + backupVmHostFile(filesAfterSyncing, context.backupFiles, msg.getDstVmUuidList()); trigger.next(); } }).done(new FlowDoneHandler(msg) { @@ -380,4 +326,95 @@ public void handle(ErrorCode errCode, Map data) { } }).start(); } + + private void handle(BackupVmHostFileMsg msg) { + BackupVmHostFileReply reply = new BackupVmHostFileReply(); + List filesNeedPersists = backupVmHostFile( + msg.getVmUuid(), msg.getHostUuid(), msg.getToResourceUuidList()); + reply.setBackupFileUuidList(transform(filesNeedPersists, VmHostBackupFileVO::getUuid)); + bus.reply(msg, reply); + } + + private List backupVmHostFile(String fromVmUuid, String hostUuid, List toResourceList) { + List hostFiles = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, fromVmUuid) + .eq(VmHostFileVO_.hostUuid, hostUuid) + .list(); + + if (hostFiles.isEmpty()) { + return new ArrayList<>(); + } + return backupVmHostFile(hostFiles, new ArrayList<>(), toResourceList); + } + + private List backupVmHostFile(List fileList, List backupFiles, List toResourceList) { + List uuidList = transform(fileList, VmHostFileVO::getUuid); + uuidList.addAll(transform(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 resourceUuid : toResourceList) { + for (String uuid : uuidList) { + VmHostFileContentVO srcContent = findOneOrNull(contents, + item -> item.getUuid().equals(uuid)); + if (srcContent == null) { + continue; + } + + VmHostFileVO vmHostFile = findOneOrNull(fileList, + item -> item.getUuid().equals(uuid)); + VmHostBackupFileVO vmHostBackupFile = vmHostFile == null ? + findOneOrNull(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.setResourceUuid(resourceUuid); + 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() { + for (VmHostBackupFileVO backupFile : filesNeedPersists) { + // resourceUuid + type must be unique in DB + sql(VmHostBackupFileVO.class) + .eq(VmHostBackupFileVO_.resourceUuid, backupFile.getResourceUuid()) + .eq(VmHostBackupFileVO_.type, backupFile.getType()) + .delete(); + } + + if (!filesNeedPersists.isEmpty()) { + databaseFacade.persistCollection(filesNeedPersists); + } + if (!contentsNeedPersists.isEmpty()) { + databaseFacade.persistCollection(contentsNeedPersists); + } + } + }.execute(); + return filesNeedPersists; + } } diff --git a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java index dbc130475e5..07881c552ed 100644 --- a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java @@ -31,6 +31,8 @@ import org.zstack.header.storage.snapshot.group.*; import org.zstack.header.vm.RestoreVmInstanceMsg; import org.zstack.header.vm.VmInstanceConstant; +import org.zstack.header.vm.additions.VmHostBackupFileVO; +import org.zstack.header.vm.additions.VmHostBackupFileVO_; import org.zstack.header.vm.devices.VmInstanceResourceMetadataManager; import org.zstack.header.volume.VolumeType; import org.zstack.header.volume.VolumeVO; @@ -142,6 +144,9 @@ public String getSyncSignature() { public void run(SyncTaskChain chain) { APIUngroupVolumeSnapshotGroupEvent evt = new APIUngroupVolumeSnapshotGroupEvent(msg.getId()); dbf.remove(self); + SQL.New(VmHostBackupFileVO.class) + .eq(VmHostBackupFileVO_.resourceUuid, msg.getUuid()) + .delete(); bus.publish(evt); chain.next(); } @@ -209,28 +214,43 @@ private void handle(DeleteVolumeSnapshotGroupInnerMsg msg) { logger.debug(String.format("skip snapshots not belong to origin vm[uuid:%s]", self.getVmInstanceUuid())); } - new While<>(snapshots).all((snapshot, compl) -> { - DeleteVolumeSnapshotMsg rmsg = new DeleteVolumeSnapshotMsg(); - rmsg.setSnapshotUuid(snapshot.getUuid()); - rmsg.setVolumeUuid(snapshot.getVolumeUuid()); - rmsg.setTreeUuid(snapshot.getTreeUuid()); - rmsg.setDeletionMode(msg.getDeletionMode()); - rmsg.setScope(msg.getScope()); - rmsg.setDirection(msg.getDirection()); - bus.makeTargetServiceIdByResourceUuid(rmsg, VolumeSnapshotConstant.SERVICE_ID, getResourceIdToRouteMsg(snapshot)); - bus.send(rmsg, new CloudBusCallBack(compl) { - @Override - public void run(MessageReply r) { - reply.addResult(new DeleteSnapshotGroupResult(rmsg.getSnapshotUuid(), rmsg.getVolumeUuid(), r.getError())); - compl.done(); - } - }); - }).run(new WhileDoneCompletion(msg) { - @Override - public void done(ErrorCodeList errorCodeList) { + SimpleFlowChain.of("delete-volume-snapshot-group") + .then("delete-volume-snapshots", (trigger) -> + new While<>(snapshots).step((snapshot, compl) -> { + DeleteVolumeSnapshotMsg rmsg = new DeleteVolumeSnapshotMsg(); + rmsg.setSnapshotUuid(snapshot.getUuid()); + rmsg.setVolumeUuid(snapshot.getVolumeUuid()); + rmsg.setTreeUuid(snapshot.getTreeUuid()); + rmsg.setDeletionMode(msg.getDeletionMode()); + rmsg.setScope(msg.getScope()); + rmsg.setDirection(msg.getDirection()); + bus.makeTargetServiceIdByResourceUuid(rmsg, VolumeSnapshotConstant.SERVICE_ID, getResourceIdToRouteMsg(snapshot)); + bus.send(rmsg, new CloudBusCallBack(compl) { + @Override + public void run(MessageReply r) { + reply.addResult(new DeleteSnapshotGroupResult(rmsg.getSnapshotUuid(), rmsg.getVolumeUuid(), r.getError())); + compl.done(); + } + }); + }, 5).run(new WhileDoneCompletion(msg) { + @Override + public void done(ErrorCodeList errorCodeList) { + trigger.next(); + } + })) + .then("delete-vm-host-backup-files", trigger -> { + SQL.New(VmHostBackupFileVO.class) + .eq(VmHostBackupFileVO_.resourceUuid, self.getUuid()) + .delete(); + trigger.next(); + }) + .propagateExceptionTo(msg) + .done(() -> bus.reply(msg, reply)) + .error(errorCode -> { + reply.setError(errorCode); bus.reply(msg, reply); - } - }); + }) + .start(); } private void handle(APIRevertVmFromSnapshotGroupMsg msg) { 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 e0ef4a6141a..f0cd6d4a511 100755 --- a/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java +++ b/storage/src/main/java/org/zstack/storage/volume/VolumeBase.java @@ -41,6 +41,8 @@ import org.zstack.header.tag.SystemTagVO; import org.zstack.header.tag.SystemTagVO_; import org.zstack.header.vm.*; +import org.zstack.header.vm.additions.VmHostBackupFileVO; +import org.zstack.header.vm.additions.VmHostBackupFileVO_; import org.zstack.header.vm.devices.VmInstanceResourceMetadataManager; import org.zstack.header.volume.*; import org.zstack.header.volume.VolumeConstant.Capability; @@ -62,6 +64,7 @@ import javax.persistence.TypedQuery; import java.util.*; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import static org.zstack.core.Platform.*; @@ -3084,6 +3087,7 @@ private void createSnapshotGroup(CreateVolumeSnapshotGroupMessage msg, ReturnVal CreateVolumesSnapshotMsg cmsg = new CreateVolumesSnapshotMsg(); List volumesSnapshotsJobs = new ArrayList<>(); cmsg.setAccountUuid(msg.getSession().getAccountUuid()); + cmsg.setBackupHostFileIfNeeded(true); VmInstanceInventory vm = msg.getVmInstance(); Map vols = vm.getAllVolumes().stream() @@ -3104,55 +3108,92 @@ private void createSnapshotGroup(CreateVolumeSnapshotGroupMessage msg, ReturnVal cmsg.setVolumeSnapshotJobs(volumesSnapshotsJobs); cmsg.setConsistentType(msg.getConsistentType()); - bus.makeTargetServiceIdByResourceUuid(cmsg, VolumeConstant.SERVICE_ID, msg.getRootVolumeUuid()); - bus.send(cmsg, new CloudBusCallBack(completion) { - @Override - public void run(MessageReply reply) { - if (!reply.isSuccess()) { - completion.fail(reply.getError()); - return; - } + List inventories = new ArrayList<>(); + List hostBackupFileUuidList = new ArrayList<>(); + AtomicReference groupRef = new AtomicReference<>(null); + String resourceUuid = msg.getResourceUuid() == null ? getUuid() : msg.getResourceUuid(); + SimpleFlowChain.of("create-snapshot-group") + .then("handle-create-snapshot-group-message", trigger -> { + bus.makeTargetServiceIdByResourceUuid(cmsg, VolumeConstant.SERVICE_ID, msg.getRootVolumeUuid()); + bus.send(cmsg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + trigger.fail(reply.getError()); + return; + } + CreateVolumesSnapshotReply r = reply.castReply(); + inventories.addAll(r.getInventories()); + if (!CollectionUtils.isEmpty(r.getHostBackupFileUuidList())) { + hostBackupFileUuidList.addAll(r.getHostBackupFileUuidList()); + } + trigger.next(); + } + }); + }) + .then(Flow.of("persist-snapshot-group") + .handle(trigger -> { + List refs = new ArrayList<>(); + VolumeSnapshotGroupVO group = new VolumeSnapshotGroupVO(); + group.setUuid(resourceUuid); + group.setSnapshotCount(cmsg.getVolumeSnapshotJobs().size()); + group.setName(msg.getName()); + group.setDescription(msg.getDescription()); + group.setVmInstanceUuid(vm.getUuid()); + group.setAccountUuid(msg.getSession().getAccountUuid()); + for (VolumeSnapshotInventory inv : inventories) { + VolumeSnapshotGroupRefVO ref = new VolumeSnapshotGroupRefVO(); + ref.setVolumeUuid(inv.getVolumeUuid()); + ref.setVolumeName(vols.get(inv.getVolumeUuid()).getName()); + ref.setVolumeType(inv.getVolumeType()); + ref.setVolumeSnapshotGroupUuid(group.getUuid()); + ref.setVolumeSnapshotUuid(inv.getUuid()); + ref.setVolumeSnapshotName(inv.getName()); + ref.setVolumeSnapshotInstallPath(inv.getPrimaryStorageInstallPath()); + ref.setDeviceId(vols.get(inv.getVolumeUuid()).getDeviceId()); + ref.setVolumeLastAttachDate(vols.get(inv.getVolumeUuid()).getLastAttachDate()); + refs.add(ref); + } + + new SQLBatch() { + @Override + protected void scripts() { + databaseFacade.persist(group); + databaseFacade.persistCollection(refs); + } + }.execute(); - CreateVolumesSnapshotReply r = reply.castReply(); - VolumeSnapshotGroupVO group = createGroup(r); + groupRef.set(group); + trigger.next(); + }) + .rollback(trigger -> { + SQL.New(VolumeSnapshotGroupVO.class) + .eq(VolumeSnapshotGroupVO_.uuid, resourceUuid) + .delete(); + // VolumeSnapshotGroupRefVO delete in cascade + trigger.rollback(); + }) + .build()) + .then(Flow.of("persist-vm-host-backup-file") + .handle(trigger -> { + if (!CollectionUtils.isEmpty(hostBackupFileUuidList)) { + SQL.New(VmHostBackupFileVO.class) + .in(VmHostBackupFileVO_.uuid, hostBackupFileUuidList) + .set(VmHostBackupFileVO_.resourceUuid, resourceUuid) + .update(); + } + trigger.next(); + }) + .build()) + .propagateExceptionTo(completion) + .done(() -> { logger.debug(String.format("created volume snapshot group[uuid:%s] for vm[uuid:%s]", - group.getUuid(), vm.getUuid())); - completion.success(VolumeSnapshotGroupInventory.valueOf(dbf.reload(group))); - } - - private VolumeSnapshotGroupVO createGroup(CreateVolumesSnapshotReply r) { - List refs = new ArrayList<>(); - VolumeSnapshotGroupVO group = new VolumeSnapshotGroupVO(); - if (msg.getResourceUuid() != null) { - group.setUuid(msg.getResourceUuid()); - } else { - group.setUuid(getUuid()); - } - group.setSnapshotCount(cmsg.getVolumeSnapshotJobs().size()); - group.setName(msg.getName()); - group.setDescription(msg.getDescription()); - group.setVmInstanceUuid(vm.getUuid()); - group.setAccountUuid(msg.getSession().getAccountUuid()); - for (VolumeSnapshotInventory inv : r.getInventories()) { - VolumeSnapshotGroupRefVO ref = new VolumeSnapshotGroupRefVO(); - ref.setVolumeUuid(inv.getVolumeUuid()); - ref.setVolumeName(vols.get(inv.getVolumeUuid()).getName()); - ref.setVolumeType(inv.getVolumeType()); - ref.setVolumeSnapshotGroupUuid(group.getUuid()); - ref.setVolumeSnapshotUuid(inv.getUuid()); - ref.setVolumeSnapshotName(inv.getName()); - ref.setVolumeSnapshotInstallPath(inv.getPrimaryStorageInstallPath()); - ref.setDeviceId(vols.get(inv.getVolumeUuid()).getDeviceId()); - ref.setVolumeLastAttachDate(vols.get(inv.getVolumeUuid()).getLastAttachDate()); - refs.add(ref); - } - - dbf.persist(group); - dbf.persistCollection(refs); - return group; - } - }); + groupRef.get().getUuid(), vm.getUuid())); + completion.success(VolumeSnapshotGroupInventory.valueOf(dbf.reload(groupRef.get()))); + }) + .error(completion::fail) + .start(); } private void handle(APIFlattenVolumeMsg msg) { From 515343258a963d19c55190e63ec3aac420fca78d Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Mon, 23 Mar 2026 19:01:06 +0800 Subject: [PATCH 065/129] [kvm]: introduce SyncVmHostFilesFromHostMsg Resolves: ZSV-11441 Related: ZSV-11310 Change-Id: I746d7a6c61757263776b6f64776570657166676a --- .../kvm/efi/KvmSecureBootExtensions.java | 192 +++------------- .../zstack/kvm/efi/KvmSecureBootManager.java | 214 ++++++++++++++---- .../kvm/efi/SyncVmHostFilesFromHostMsg.java | 42 ++++ .../kvm/efi/SyncVmHostFilesFromHostReply.java | 6 + 4 files changed, 254 insertions(+), 200 deletions(-) create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostMsg.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostReply.java 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 69a977847ec..e5904d4a291 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 @@ -29,6 +29,7 @@ import org.zstack.header.tpm.entity.TpmVO_; import org.zstack.header.vm.DiskAO; import org.zstack.header.vm.PreVmInstantiateResourceExtensionPoint; +import org.zstack.header.vm.VmInstanceConstant; import org.zstack.header.vm.VmInstanceDestroyExtensionPoint; import org.zstack.header.vm.VmInstanceInventory; import org.zstack.header.vm.VmInstanceSpec; @@ -37,7 +38,6 @@ 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; import org.zstack.header.vm.additions.VmHostFileContentVO; import org.zstack.header.vm.additions.VmHostFileContentVO_; import org.zstack.header.vm.additions.VmHostFileType; @@ -70,7 +70,6 @@ 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; @@ -82,8 +81,6 @@ 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, @@ -224,135 +221,6 @@ public void fail(ErrorCode errorCode) { .start(); } - 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(); - } - - 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) { - errors.add(operr("failed to read file %s", path) - .withOpaque("path", path) - .withException(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); - } - - if (logger.isTraceEnabled()) { - logger.trace(String.format("persist/update VmHostFileContentVO [uuid=%s]", file.getUuid())); - } - } - - if (errors.isEmpty()) { - completion.success(); - } else { - completion.fail(operr("failed to read file content from host[uuid=%s]", context.hostUuid).withCause(errors)); - } - } - - @Override - public void fail(ErrorCode errorCode) { - completion.fail(errorCode); - } - }); - } - public static class RewriteVmHostFilesContext { public String hostUuid; public List hostFiles; @@ -517,29 +385,30 @@ public void run(FlowTrigger trigger, Map data) { } context.sameHost = vmHostFile.getHostUuid().equals(context.hostUuid); - SyncVmHostFilesFromHostContext syncContext = new SyncVmHostFilesFromHostContext(); - syncContext.hostUuid = vmHostFile.getHostUuid(); - syncContext.vmUuid = context.vmUuid; + SyncVmHostFilesFromHostMsg syncMsg = new SyncVmHostFilesFromHostMsg(); + syncMsg.setHostUuid(vmHostFile.getHostUuid()); + syncMsg.setVmUuid(context.vmUuid); if (vmHostFile.getType() == VmHostFileType.NvRam) { - context.path = syncContext.nvRamPath = vmHostFile.getPath(); + context.path = vmHostFile.getPath(); + syncMsg.setNvRamPath(context.path); } else if (vmHostFile.getType() == VmHostFileType.TpmState) { - context.path = syncContext.tpmStateFolder = vmHostFile.getPath(); + context.path = vmHostFile.getPath(); + syncMsg.setTpmStateFolder(context.path); } else { throw new CloudRuntimeException("unsupported vm host file type: " + vmHostFile.getType()); } - syncVmHostFilesFromHost(syncContext, new Completion(trigger) { - @Override - public void success() { - context.vmHostFile = vmHostFile; - trigger.next(); - } - + bus.makeLocalServiceId(syncMsg, VmInstanceConstant.SECURE_BOOT_SERVICE_ID); + bus.send(syncMsg, new CloudBusCallBack(trigger) { @Override - public void fail(ErrorCode errorCode) { - logger.warn(String.format("failed to read vm host file for VM[vmUuid=%s] but still continue: %s", - context.vmUuid, errorCode.getReadableDetails())); + public void run(MessageReply reply) { + if (reply.isSuccess()) { + context.vmHostFile = vmHostFile; + } else { + logger.warn(String.format("failed to read vm host file for VM[vmUuid=%s] but still continue: %s", + context.vmUuid, reply.getError().getReadableDetails())); + } trigger.next(); } }); @@ -628,26 +497,25 @@ public boolean skip(Map data) { @Override public void run(FlowTrigger trigger, Map data) { - KvmSecureBootExtensions.SyncVmHostFilesFromHostContext syncBackContext = - new KvmSecureBootExtensions.SyncVmHostFilesFromHostContext(); - syncBackContext.hostUuid = context.hostUuid; - syncBackContext.vmUuid = context.vmUuid; + SyncVmHostFilesFromHostMsg syncMsg = new SyncVmHostFilesFromHostMsg(); + syncMsg.setHostUuid(context.hostUuid); + syncMsg.setVmUuid(context.vmUuid); if (context.type == VmHostFileType.NvRam) { - syncBackContext.nvRamPath = context.path; + syncMsg.setNvRamPath(context.path); } else if (context.type == VmHostFileType.TpmState) { - syncBackContext.tpmStateFolder = context.path; + syncMsg.setTpmStateFolder(context.path); } - syncVmHostFilesFromHost(syncBackContext, new Completion(trigger) { + bus.makeLocalServiceId(syncMsg, VmInstanceConstant.SECURE_BOOT_SERVICE_ID); + bus.send(syncMsg, new CloudBusCallBack(trigger) { @Override - public void success() { - trigger.next(); - } - - @Override - public void fail(ErrorCode errorCode) { - trigger.fail(errorCode); + public void run(MessageReply reply) { + if (reply.isSuccess()) { + trigger.next(); + } else { + trigger.fail(reply.getError()); + } } }); } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootManager.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootManager.java index 56a80935d37..41c4d564572 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 @@ -5,14 +5,17 @@ import org.zstack.core.Platform; import org.zstack.core.asyncbatch.While; import org.zstack.core.cloudbus.CloudBus; +import org.zstack.core.cloudbus.CloudBusCallBack; import org.zstack.core.cloudbus.EventCallback; import org.zstack.core.cloudbus.EventFacadeImpl; 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.db.SQLBatch; import org.zstack.core.workflow.SimpleFlowChain; import org.zstack.header.AbstractService; -import org.zstack.header.core.Completion; +import org.zstack.header.core.ReturnValueCompletion; import org.zstack.header.core.WhileDoneCompletion; import org.zstack.header.core.workflow.FlowDoneHandler; import org.zstack.header.core.workflow.FlowErrorHandler; @@ -22,6 +25,7 @@ import org.zstack.header.errorcode.ErrorCodeList; import org.zstack.header.exception.CloudRuntimeException; import org.zstack.header.message.Message; +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.VmCanonicalEvents; @@ -30,11 +34,15 @@ 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.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.kvm.KVMAgentCommands; +import org.zstack.kvm.KvmCommandSender; +import org.zstack.kvm.KvmResponseWrapper; import org.zstack.resourceconfig.ResourceConfig; import org.zstack.resourceconfig.ResourceConfigFacade; import org.zstack.utils.DebugUtils; @@ -45,13 +53,17 @@ import java.sql.Timestamp; import java.time.Instant; import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; 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.core.Platform.operr; +import static org.zstack.kvm.KVMConstant.READ_VM_HOST_FILE_PATH; import static org.zstack.utils.CollectionDSL.list; import static org.zstack.utils.CollectionUtils.findOneOrNull; import static org.zstack.utils.CollectionUtils.transform; @@ -62,11 +74,11 @@ public class KvmSecureBootManager extends AbstractService { @Autowired private CloudBus bus; @Autowired + private DatabaseFacade databaseFacade; + @Autowired private EventFacadeImpl eventFacade; @Autowired private ResourceConfigFacade resourceConfigFacade; - @Autowired - private KvmSecureBootExtensions secureBootExtensions; @Override public boolean start() { @@ -117,21 +129,22 @@ protected void run(Map tokens, Object data) { 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) { + SyncVmHostFilesFromHostMsg innerMessage = new SyncVmHostFilesFromHostMsg(); + innerMessage.setHostUuid(hostUuid); + innerMessage.setVmUuid(vmUuid); + innerMessage.setNvRamPath(nvRamFile == null ? null : nvRamFile.getPath()); + innerMessage.setTpmStateFolder(tpmStateFile == null ? null : tpmStateFile.getPath()); + bus.makeLocalServiceId(innerMessage, VmInstanceConstant.SECURE_BOOT_SERVICE_ID); + bus.send(innerMessage, new CloudBusCallBack(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())); + public void run(MessageReply reply) { + if (reply.isSuccess()) { + logger.info(String.format("success to read file content from host[uuid=%s]", + innerMessage.getHostUuid())); + } else { + logger.warn(String.format("failed to read file content from host[uuid=%s]: %s", + innerMessage.getHostUuid(), reply.getError().getReadableDetails())); + } } }); } @@ -146,7 +159,9 @@ public String getId() { @Override @MessageSafe public void handleMessage(Message msg) { - if (msg instanceof CloneVmHostFileMsg) { + if (msg instanceof SyncVmHostFilesFromHostMsg) { + handle((SyncVmHostFilesFromHostMsg) msg); + } else if (msg instanceof CloneVmHostFileMsg) { handle((CloneVmHostFileMsg) msg); } else if (msg instanceof BackupVmHostFileMsg) { handle((BackupVmHostFileMsg) msg); @@ -159,9 +174,134 @@ static class CloneVmHostFileContext { List typesNeedClone = new ArrayList<>(); List files = new ArrayList<>(); List backupFiles = new ArrayList<>(); - List syncContexts = new ArrayList<>(); + List syncContexts = new ArrayList<>(); } + private void handle(SyncVmHostFilesFromHostMsg msg) { + KvmCommandSender sender = new KvmCommandSender(msg.getHostUuid()); + + KVMAgentCommands.ReadVmHostFileContentCmd cmd = new KVMAgentCommands.ReadVmHostFileContentCmd(); + cmd.setHostFiles(new ArrayList<>()); + if (msg.getTpmStateFolder() != null) { + KVMAgentCommands.VmHostFileTO to = new KVMAgentCommands.VmHostFileTO(); + to.setPath(msg.getTpmStateFolder()); + to.setType(VmHostFileType.TpmState.toString()); + cmd.getHostFiles().add(to); + } + if (msg.getNvRamPath() != null) { + KVMAgentCommands.VmHostFileTO to = new KVMAgentCommands.VmHostFileTO(); + to.setPath(msg.getNvRamPath()); + to.setType(VmHostFileType.NvRam.toString()); + cmd.getHostFiles().add(to); + } + + SyncVmHostFilesFromHostReply reply = new SyncVmHostFilesFromHostReply(); + sender.send(cmd, READ_VM_HOST_FILE_PATH, wrapper -> { + KVMAgentCommands.ReadVmHostFileContentResponse readRsp = wrapper.getResponse(KVMAgentCommands.ReadVmHostFileContentResponse.class); + return readRsp.isSuccess() ? null : + operr("failed to read file content response").withException(readRsp.getError()); + }, new ReturnValueCompletion(msg) { + @Override + public void success(KvmResponseWrapper wrapper) { + KVMAgentCommands.ReadVmHostFileContentResponse readRsp = wrapper.getResponse(KVMAgentCommands.ReadVmHostFileContentResponse.class); + if (!readRsp.isSuccess()) { + reply.setError(operr("failed to read file content response").withException(readRsp.getError())); + bus.reply(msg, reply); + return; + } + + final List existsFiles = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, msg.getVmUuid()) + .eq(VmHostFileVO_.hostUuid, msg.getHostUuid()) + .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(); + } + + List errors = new ArrayList<>(); + for (String path : cmd.getPaths()) { + KVMAgentCommands.VmHostFileTO to = findOneOrNull(readRsp.getHostFiles(), item -> item.getPath().equals(path)); + if (to == null) { + continue; + } + if (to.getError() != null) { + errors.add(operr("failed to read file %s", path) + .withOpaque("path", path) + .withException(to.getError())); + continue; + } + + VmHostFileType type = Objects.equals(path, msg.getNvRamPath()) ? + 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(msg.getHostUuid()); + file.setVmInstanceUuid(msg.getVmUuid()); + file.setPath(path); + file.setType(type); + file.setCreateDate(now); + file.setLastOpDate(now); + file.setResourceName(String.format("%s file for %s", type, msg.getVmUuid())); + 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); + } + + if (logger.isTraceEnabled()) { + logger.trace(String.format("persist/update VmHostFileContentVO [uuid=%s]", file.getUuid())); + } + } + + if (!errors.isEmpty()) { + reply.setError(operr("failed to read file content from host[uuid=%s]", msg.getHostUuid()) + .withCause(errors)); + } + bus.reply(msg, reply); + } + + @Override + public void fail(ErrorCode errorCode) { + reply.setError(errorCode); + bus.reply(msg, reply); + } + }); + } + + @SuppressWarnings("rawtypes") private void handle(CloneVmHostFileMsg msg) { CloneVmHostFileReply reply = new CloneVmHostFileReply(); @@ -219,23 +359,23 @@ public void run(FlowTrigger trigger, Map data) { return; } - Map contextMap = new HashMap<>(); + 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(); + SyncVmHostFilesFromHostMsg syncContext = new SyncVmHostFilesFromHostMsg(); + syncContext.setHostUuid(hostUuid); + syncContext.setVmUuid(msg.getSrcVmUuid()); return syncContext; }); } context.syncContexts.addAll(contextMap.values()); for (VmHostFileVO file : context.files) { - SyncVmHostFilesFromHostContext syncContext = contextMap.get(file.getHostUuid()); + SyncVmHostFilesFromHostMsg syncContext = contextMap.get(file.getHostUuid()); if (file.getType() == VmHostFileType.NvRam) { - syncContext.nvRamPath = file.getPath(); + syncContext.setNvRamPath(file.getPath()); } else if (file.getType() == VmHostFileType.TpmState) { - syncContext.tpmStateFolder = file.getPath(); + syncContext.setTpmStateFolder(file.getPath()); } else { throw new CloudRuntimeException("unsupported vm host file type: " + file.getType()); } @@ -253,20 +393,18 @@ public boolean skip(Map data) { @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(); - } - + new While<>(context.syncContexts).each((syncContext, whileContext) -> { + bus.makeLocalServiceId(syncContext, VmInstanceConstant.SECURE_BOOT_SERVICE_ID); + bus.send(syncContext, new CloudBusCallBack(whileContext) { @Override - public void fail(ErrorCode errorCode) { - whileContext.addError(errorCode); + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + whileContext.addError(reply.getError()); + } whileContext.done(); } - }) - ).run(new WhileDoneCompletion(trigger) { + }); + }).run(new WhileDoneCompletion(trigger) { @Override public void done(ErrorCodeList errorCodeList) { if (!errorCodeList.isEmpty()) { diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostMsg.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostMsg.java new file mode 100644 index 00000000000..97ae8e54597 --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostMsg.java @@ -0,0 +1,42 @@ +package org.zstack.kvm.efi; + +import org.zstack.header.message.NeedReplyMessage; + +public class SyncVmHostFilesFromHostMsg extends NeedReplyMessage { + private String hostUuid; + private String vmUuid; + private String nvRamPath; + private String tpmStateFolder; + + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + public String getVmUuid() { + return vmUuid; + } + + public void setVmUuid(String vmUuid) { + this.vmUuid = vmUuid; + } + + public String getNvRamPath() { + return nvRamPath; + } + + public void setNvRamPath(String nvRamPath) { + this.nvRamPath = nvRamPath; + } + + public String getTpmStateFolder() { + return tpmStateFolder; + } + + public void setTpmStateFolder(String tpmStateFolder) { + this.tpmStateFolder = tpmStateFolder; + } +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostReply.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostReply.java new file mode 100644 index 00000000000..7509815a68e --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostReply.java @@ -0,0 +1,6 @@ +package org.zstack.kvm.efi; + +import org.zstack.header.message.MessageReply; + +public class SyncVmHostFilesFromHostReply extends MessageReply { +} From e593470dba7a6974f335f09e032f51a1f1ef831e Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Thu, 15 Jan 2026 14:06:14 +0800 Subject: [PATCH 066/129] [storage]: register and take over sblk APIImpact Resolves: ZSV-11559 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 | 30 ++++++ .../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, 921 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..41500468236 --- /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 status; + private String candidateVgUuid; + + public boolean isConsistent() { + return consistent; + } + + public void setConsistent(boolean consistent) { + this.consistent = consistent; + } + + public ConsistencyCheckStatus getStatus() { + return status; + } + + public void setStatus(ConsistencyCheckStatus status) { + this.status = status; + } + + 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.setStatus(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..38ff803ebec --- /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 "status" + desc "一致性检查结果: CONSISTENT(VG 存在且 UUID 一致)/ UUID_MISMATCH(VG 存在但 UUID 不一致,可执行接管)/ VG_NOT_FOUND(未找到 WWID 匹配的 VG)" + type "ConsistencyCheckStatus" + since "5.0.0" + } + field { + name "candidateVgUuid" + desc "status 为 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..f5c4b11d738 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/CheckPrimaryStorageConsistencyResult.java @@ -0,0 +1,30 @@ +package org.zstack.sdk; + +import org.zstack.sdk.ConsistencyCheckStatus; + +public class CheckPrimaryStorageConsistencyResult { + public boolean consistent; + public void setConsistent(boolean consistent) { + this.consistent = consistent; + } + public boolean getConsistent() { + return this.consistent; + } + + public ConsistencyCheckStatus status; + public void setStatus(ConsistencyCheckStatus status) { + this.status = status; + } + public ConsistencyCheckStatus getStatus() { + return this.status; + } + + 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 From 86ccf0b76312e0da701f0c192b14b3e95451162d Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 25 Mar 2026 10:27:07 +0800 Subject: [PATCH 067/129] [kvm]: reset TPM state after VM reimage Related: ZSV-11310 Change-Id: I706c6478786c61697476647a69727a6675636370 --- conf/springConfigXml/Kvm.xml | 1 + .../kvm/efi/KvmSecureBootExtensions.java | 22 ++++++++++++++++++- .../test/resources/springConfigXml/Kvm.xml | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index 92436934d60..2f857c32a28 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -272,6 +272,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 e5904d4a291..eb9d9797157 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 @@ -30,6 +30,7 @@ import org.zstack.header.vm.DiskAO; import org.zstack.header.vm.PreVmInstantiateResourceExtensionPoint; import org.zstack.header.vm.VmInstanceConstant; +import org.zstack.header.vm.AfterReimageVmInstanceExtensionPoint; import org.zstack.header.vm.VmInstanceDestroyExtensionPoint; import org.zstack.header.vm.VmInstanceInventory; import org.zstack.header.vm.VmInstanceSpec; @@ -85,7 +86,8 @@ public class KvmSecureBootExtensions implements KVMStartVmExtensionPoint, PreVmInstantiateResourceExtensionPoint, VmInstanceDestroyExtensionPoint, - VmPreMigrationExtensionPoint { + VmPreMigrationExtensionPoint, + AfterReimageVmInstanceExtensionPoint { private static final CLogger logger = Utils.getLogger(KvmSecureBootExtensions.class); @Autowired @@ -711,4 +713,22 @@ public void afterDestroyVm(VmInstanceInventory inv) { public void failedToDestroyVm(VmInstanceInventory inv, ErrorCode reason) { // do-nothing } + + @Override + public void afterReimageVmInstance(VolumeInventory inventory) { + String vmUuid = inventory.getVmInstanceUuid(); + if (vmUuid == null) { + return; + } + + SQL.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) + .delete(); + SQL.New(VmHostBackupFileVO.class) + .eq(VmHostBackupFileVO_.resourceUuid, vmUuid) + .delete(); + + logger.debug(String.format("reset TPM state for VM[uuid:%s] after reimage: " + + "deleted all VmHostFileVO and VmHostBackupFileVO records", vmUuid)); + } } diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index cda756671f7..b0a0350539d 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -271,6 +271,7 @@ + From 9f41218a3d7350b29f4d6eb9a8dc6d174c6a5460 Mon Sep 17 00:00:00 2001 From: "tao.yang" Date: Tue, 24 Mar 2026 07:10:55 +0800 Subject: [PATCH 068/129] [keyprovider]: Add DEK provisioning for TPM Resolves: ZSV-11489 Change-Id: I6c6772647177616e6c77756176766c7769796774 --- .../DummyEncryptedResourceKeyManager.java | 26 +++ .../compute/vm/devices/VmTpmExtensions.java | 1 - conf/springConfigXml/VmInstanceManager.xml | 1 + .../EncryptedResourceKeyManager.java | 150 ++++++++++++++++++ .../org/zstack/header/tpm/entity/TpmSpec.java | 20 +++ .../org/zstack/kvm/tpm/KvmTpmExtensions.java | 140 ++++++++++++---- .../org/zstack/testlib/KVMSimulator.groovy | 69 ++++++-- 7 files changed, 363 insertions(+), 44 deletions(-) create mode 100644 compute/src/main/java/org/zstack/compute/vm/devices/DummyEncryptedResourceKeyManager.java create mode 100644 header/src/main/java/org/zstack/header/keyprovider/EncryptedResourceKeyManager.java diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/DummyEncryptedResourceKeyManager.java b/compute/src/main/java/org/zstack/compute/vm/devices/DummyEncryptedResourceKeyManager.java new file mode 100644 index 00000000000..e7fb4cac4e2 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/devices/DummyEncryptedResourceKeyManager.java @@ -0,0 +1,26 @@ +package org.zstack.compute.vm.devices; + +import org.zstack.header.keyprovider.EncryptedResourceKeyManager; +import org.zstack.header.core.Completion; +import org.zstack.header.core.ReturnValueCompletion; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import static org.zstack.core.Platform.operr; + +public class DummyEncryptedResourceKeyManager implements EncryptedResourceKeyManager { + private static final CLogger logger = Utils.getLogger(DummyEncryptedResourceKeyManager.class); + + @Override + public void getOrCreateKey(GetOrCreateResourceKeyContext ctx, + ReturnValueCompletion completion) { + logger.warn(String.format("crypto module not installed, cannot create resource key for %s[uuid:%s]", + ctx.getResourceType(), ctx.getResourceUuid())); + completion.fail(operr("crypto module is not installed, cannot manage resource encryption keys")); + } + + @Override + public void rollbackCreatedKey(ResourceKeyResult result, Completion completion) { + completion.success(); + } +} 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 f41d290a450..5923d9a7795 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 @@ -114,7 +114,6 @@ public void afterBuildVmSpec(VmInstanceSpec spec) { tpmSpec.setEnable(true); tpmSpec.setTpmUuid(tpmUuid); - tpmSpec.setKeyProviderUuid(resourceKeyBackend.findKeyProviderUuidByTpm(tpmUuid)); } } } diff --git a/conf/springConfigXml/VmInstanceManager.xml b/conf/springConfigXml/VmInstanceManager.xml index 3d84703cca4..0b708ef5696 100755 --- a/conf/springConfigXml/VmInstanceManager.xml +++ b/conf/springConfigXml/VmInstanceManager.xml @@ -297,4 +297,5 @@ + diff --git a/header/src/main/java/org/zstack/header/keyprovider/EncryptedResourceKeyManager.java b/header/src/main/java/org/zstack/header/keyprovider/EncryptedResourceKeyManager.java new file mode 100644 index 00000000000..dcdd76851a7 --- /dev/null +++ b/header/src/main/java/org/zstack/header/keyprovider/EncryptedResourceKeyManager.java @@ -0,0 +1,150 @@ +package org.zstack.header.keyprovider; + +import org.zstack.header.core.ReturnValueCompletion; +import org.zstack.header.core.Completion; + +/** + * Unified resource key management service. + * Business layers (TPM, volume encryption, etc.) call this to create/retrieve DEKs + * without knowing gRPC/key-tools protocol details. + */ +public interface EncryptedResourceKeyManager { + + /** + * Get or create a resource encryption key. + *

+ * Semantically reuses the existing key record for the same {@code (resourceType, resourceUuid)} + * when one is already available; otherwise creates a new one and returns the plaintext DEK. + *

+ * This contract does not guarantee concurrent linearizability by itself. Callers must not assume + * the interface alone provides serialization, uniqueness enforcement, or transaction-level protection + * across concurrent create requests. + * + * @param ctx context describing the resource and key provider + * @param completion returns {@link ResourceKeyResult} containing the plaintext DEK (base64) + */ + void getOrCreateKey(GetOrCreateResourceKeyContext ctx, + ReturnValueCompletion completion); + + /** + * Roll back a newly created resource key during upper-layer workflow rollback. + *

+ * If the key record already existed before creation, implementation should restore it + * to its previous empty-placeholder state instead of deleting the relationship. + */ + void rollbackCreatedKey(ResourceKeyResult result, Completion completion); + + class GetOrCreateResourceKeyContext { + private String resourceUuid; + private String resourceType; + private String keyProviderUuid; + private String purpose; + + public String getResourceUuid() { + return resourceUuid; + } + + public void setResourceUuid(String resourceUuid) { + this.resourceUuid = resourceUuid; + } + + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + public String getKeyProviderUuid() { + return keyProviderUuid; + } + + public void setKeyProviderUuid(String keyProviderUuid) { + this.keyProviderUuid = keyProviderUuid; + } + + public String getPurpose() { + return purpose; + } + + public void setPurpose(String purpose) { + this.purpose = purpose; + } + } + + class ResourceKeyResult { + private String resourceUuid; + private String resourceType; + private String keyProviderUuid; + private String keyProviderName; + private String dekBase64; + private String secretRef; + private boolean createdNewKey; + private boolean refExistedBeforeCreate; + + public String getResourceUuid() { + return resourceUuid; + } + + public void setResourceUuid(String resourceUuid) { + this.resourceUuid = resourceUuid; + } + + public String getResourceType() { + return resourceType; + } + + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + public String getKeyProviderUuid() { + return keyProviderUuid; + } + + public void setKeyProviderUuid(String keyProviderUuid) { + this.keyProviderUuid = keyProviderUuid; + } + + public String getKeyProviderName() { + return keyProviderName; + } + + public void setKeyProviderName(String keyProviderName) { + this.keyProviderName = keyProviderName; + } + + public String getDekBase64() { + return dekBase64; + } + + public void setDekBase64(String dekBase64) { + this.dekBase64 = dekBase64; + } + + public String getSecretRef() { + return secretRef; + } + + public void setSecretRef(String secretRef) { + this.secretRef = secretRef; + } + + public boolean isCreatedNewKey() { + return createdNewKey; + } + + public void setCreatedNewKey(boolean createdNewKey) { + this.createdNewKey = createdNewKey; + } + + public boolean isRefExistedBeforeCreate() { + return refExistedBeforeCreate; + } + + public void setRefExistedBeforeCreate(boolean refExistedBeforeCreate) { + this.refExistedBeforeCreate = refExistedBeforeCreate; + } + } +} 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 bce99ae23dd..447f1d71a15 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 @@ -9,6 +9,10 @@ public class TpmSpec { private String keyProviderUuid; @APINoSee private String secretUuid; + @APINoSee + private boolean resourceKeyCreatedNew; + @APINoSee + private String resourceKeyProviderUuid; public boolean isEnable() { return enable; @@ -42,6 +46,22 @@ public void setSecretUuid(String secretUuid) { this.secretUuid = secretUuid; } + public boolean isResourceKeyCreatedNew() { + return resourceKeyCreatedNew; + } + + public void setResourceKeyCreatedNew(boolean resourceKeyCreatedNew) { + this.resourceKeyCreatedNew = resourceKeyCreatedNew; + } + + public String getResourceKeyProviderUuid() { + return resourceKeyProviderUuid; + } + + public void setResourceKeyProviderUuid(String resourceKeyProviderUuid) { + this.resourceKeyProviderUuid = resourceKeyProviderUuid; + } + 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 2513703d37d..84919356dbd 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,11 +1,13 @@ package org.zstack.kvm.tpm; +import org.apache.commons.lang.StringUtils; 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.header.core.ReturnValueCompletion; import org.zstack.core.db.Q; import org.zstack.core.db.SQL; import org.zstack.core.workflow.SimpleFlowChain; @@ -17,7 +19,12 @@ import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.host.HostConstant; import org.zstack.header.message.MessageReply; +import org.zstack.header.keyprovider.EncryptedResourceKeyManager; +import org.zstack.header.keyprovider.EncryptedResourceKeyManager.GetOrCreateResourceKeyContext; +import org.zstack.header.keyprovider.EncryptedResourceKeyManager.ResourceKeyResult; import org.zstack.header.secret.SecretHostDefineMsg; +import org.zstack.header.tpm.entity.TpmSpec; +import org.zstack.header.tpm.entity.TpmVO; import org.zstack.header.secret.SecretHostDefineReply; import org.zstack.header.vm.PreVmInstantiateResourceExtensionPoint; import org.zstack.header.vm.VmInstanceSpec; @@ -39,6 +46,7 @@ import java.util.Map; import static org.zstack.kvm.KVMConstant.*; +import static org.zstack.core.Platform.operr; public class KvmTpmExtensions implements KVMStartVmExtensionPoint, PreVmInstantiateResourceExtensionPoint { @@ -51,6 +59,8 @@ public class KvmTpmExtensions implements KVMStartVmExtensionPoint, @Autowired private TpmEncryptedResourceKeyBackend resourceKeyBackend; @Autowired + private EncryptedResourceKeyManager resourceKeyManager; + @Autowired private CloudBus bus; private final Object hostFileLock = new Object(); @@ -62,8 +72,13 @@ public void beforeStartVmOnKvm(KVMHostInventory host, VmInstanceSpec spec, KVMAg return; } + String keyProviderUuid = devicesSpec.getTpm().getKeyProviderUuid(); + if (StringUtils.isBlank(keyProviderUuid)) { + keyProviderUuid = resourceKeyBackend.findKeyProviderUuidByTpm(devicesSpec.getTpm().getTpmUuid()); + } + TpmTO tpm = new TpmTO(); - tpm.setKeyProviderUuid(devicesSpec.getTpm().getKeyProviderUuid()); + tpm.setKeyProviderUuid(keyProviderUuid); tpm.setSecretUuid(devicesSpec.getTpm().getSecretUuid()); tpm.setInstallPath(buildTpmStateFilePath(cmd.getVmInstanceUuid())); cmd.setTpm(tpm); @@ -96,12 +111,12 @@ public void beforeStartVmOnKvm(KVMHostInventory host, VmInstanceSpec spec, KVMAg @Override public void startVmOnKvmSuccess(KVMHostInventory host, VmInstanceSpec spec) { - // do-nothing + clearRollbackInfo(spec); } @Override public void startVmOnKvmFailed(KVMHostInventory host, VmInstanceSpec spec, ErrorCode err) { - // do-nothing + clearRollbackInfo(spec); } @Override @@ -109,14 +124,6 @@ public void preBeforeInstantiateVmResource(VmInstanceSpec spec) throws VmInstant // do-nothing } - static class PreInstantiateVmResourceContext { - String hostUuid; - String vmUuid; - String tpmUuid; - String providerName; - String dekBase64; // secret key - } - @Override @SuppressWarnings("rawtypes") public void preInstantiateVmResource(VmInstanceSpec spec, Completion completion) { @@ -126,10 +133,11 @@ public void preInstantiateVmResource(VmInstanceSpec spec, Completion completion) return; } - PreInstantiateVmResourceContext context = new PreInstantiateVmResourceContext(); - context.hostUuid = spec.getDestHost().getUuid(); - context.vmUuid = spec.getVmInventory().getUuid(); - context.tpmUuid = spec.getDevicesSpec().getTpm().getTpmUuid(); + TpmSpec tpmSpec = devicesSpec.getTpm(); + clearRollbackInfo(spec); + final PrepareTpmResourceContext context = new PrepareTpmResourceContext(); + context.tpmUuid = tpmSpec.getTpmUuid(); + context.providerUuid = resourceKeyBackend.findKeyProviderUuidByTpm(context.tpmUuid); context.providerName = resourceKeyBackend.findKeyProviderNameByTpm(context.tpmUuid); final SimpleFlowChain chain = new SimpleFlowChain(); @@ -140,8 +148,8 @@ public void preInstantiateVmResource(VmInstanceSpec spec, Completion completion) @Override public void run(FlowTrigger trigger, Map data) { PrepareHostFileContext innerContext = new PrepareHostFileContext(); - innerContext.hostUuid = context.hostUuid; - innerContext.vmUuid = context.vmUuid; + innerContext.hostUuid = spec.getDestHost().getUuid(); + innerContext.vmUuid = spec.getVmInventory().getUuid(); innerContext.type = VmHostFileType.TpmState; secureBootExtensions.prepareHostFileOnHost(innerContext, new Completion(trigger) { @Override @@ -160,33 +168,60 @@ public void fail(ErrorCode errorCode) { @Override public boolean skip(Map data) { - return context.providerName == null; + return false; } @Override public void run(FlowTrigger trigger, Map data) { - // TODO create DEK - context.dekBase64 = Platform.getUuid(); - trigger.next(); + if (StringUtils.isBlank(context.providerUuid) || StringUtils.isBlank(context.providerName)) { + trigger.fail(operr("missing TPM resource key binding for tpm[uuid:%s], attachKeyProviderToTpm must run before create-dek", context.tpmUuid)); + return; + } + + GetOrCreateResourceKeyContext keyCtx = new GetOrCreateResourceKeyContext(); + keyCtx.setResourceUuid(context.tpmUuid); + keyCtx.setResourceType(TpmVO.class.getSimpleName()); + keyCtx.setKeyProviderUuid(context.providerUuid); + keyCtx.setPurpose("vtpm"); + + resourceKeyManager.getOrCreateKey(keyCtx, new ReturnValueCompletion(trigger) { + @Override + public void success(ResourceKeyResult result) { + tpmSpec.setResourceKeyCreatedNew(result.isCreatedNewKey()); + tpmSpec.setResourceKeyProviderUuid(result.getKeyProviderUuid()); + context.providerName = result.getKeyProviderName(); + context.dekBase64 = result.getDekBase64(); + trigger.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + trigger.fail(errorCode); + } + }); } }).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; + return context.dekBase64 == null; } @Override public void run(FlowTrigger trigger, Map data) { + if (StringUtils.isBlank(context.providerName)) { + trigger.fail(operr("missing effective key provider name for tpm[uuid:%s] before define-secret-on-host", context.tpmUuid)); + return; + } + SecretHostDefineMsg innerMsg = new SecretHostDefineMsg(); - innerMsg.setHostUuid(context.hostUuid); - innerMsg.setVmUuid(context.vmUuid); + innerMsg.setHostUuid(spec.getDestHost().getUuid()); + innerMsg.setVmUuid(spec.getVmInventory().getUuid()); innerMsg.setDekBase64(context.dekBase64); innerMsg.setPurpose("vtpm"); innerMsg.setProviderName(context.providerName); - innerMsg.setDescription("Define secret for VM " + context.vmUuid); + innerMsg.setDescription("Define secret for VM " + spec.getVmInventory().getUuid()); bus.makeTargetServiceIdByResourceUuid(innerMsg, HostConstant.SERVICE_ID, innerMsg.getHostUuid()); bus.send(innerMsg, new CloudBusCallBack(trigger) { @Override @@ -194,8 +229,10 @@ public void run(MessageReply reply) { if (reply.isSuccess()) { SecretHostDefineReply r = reply.castReply(); spec.getDevicesSpec().getTpm().setSecretUuid(r.getSecretUuid()); + context.clearSensitiveData(); trigger.next(); } else { + context.clearSensitiveData(); trigger.fail(reply.getError()); } } @@ -204,11 +241,13 @@ public void run(MessageReply reply) { }).done(new FlowDoneHandler(completion) { @Override public void handle(Map data) { + context.clearSensitiveData(); completion.success(); } }).error(new FlowErrorHandler(completion) { @Override public void handle(ErrorCode errCode, Map data) { + context.clearSensitiveData(); completion.fail(errCode); } }).start(); @@ -223,8 +262,55 @@ static class PrepareTpmStateHostFileContext { VmHostFileVO tpmStateFile; } + static class PrepareTpmResourceContext { + String tpmUuid; + String providerUuid; + String providerName; + String dekBase64; + + void clearSensitiveData() { + dekBase64 = null; + } + } + @Override public void preReleaseVmResource(VmInstanceSpec spec, Completion completion) { - completion.success(); + TpmSpec tpmSpec = spec.getDevicesSpec() == null ? null : spec.getDevicesSpec().getTpm(); + if (tpmSpec == null || !tpmSpec.isResourceKeyCreatedNew()) { + completion.success(); + return; + } + + ResourceKeyResult result = new ResourceKeyResult(); + result.setResourceUuid(tpmSpec.getTpmUuid()); + result.setResourceType(TpmVO.class.getSimpleName()); + result.setKeyProviderUuid(tpmSpec.getResourceKeyProviderUuid()); + result.setCreatedNewKey(true); + // TPM resource key creation requires attachKeyProviderToTpm to create a placeholder ref first. + result.setRefExistedBeforeCreate(true); + + resourceKeyManager.rollbackCreatedKey(result, new Completion(completion) { + @Override + public void success() { + clearRollbackInfo(spec); + completion.success(); + } + + @Override + public void fail(ErrorCode errorCode) { + logger.warn(String.format("failed to rollback TPM resource key for tpm[uuid:%s]: %s", + tpmSpec.getTpmUuid(), errorCode != null ? errorCode.getDetails() : "")); + clearRollbackInfo(spec); + completion.success(); + } + }); + } + + private void clearRollbackInfo(VmInstanceSpec spec) { + if (spec.getDevicesSpec() == null || spec.getDevicesSpec().getTpm() == null) { + return; + } + spec.getDevicesSpec().getTpm().setResourceKeyCreatedNew(false); + spec.getDevicesSpec().getTpm().setResourceKeyProviderUuid(null); } } diff --git a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy index f7e20a79f80..360b0164fe0 100755 --- a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy +++ b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy @@ -1,7 +1,8 @@ package org.zstack.testlib -import org.springframework.http.HttpEntity -import org.zstack.core.db.Q +import org.springframework.http.HttpEntity +import org.zstack.core.Platform +import org.zstack.core.db.Q import org.zstack.core.db.SQL import org.zstack.core.db.SQLBatch import org.zstack.header.Constants @@ -36,9 +37,11 @@ import static org.zstack.kvm.KVMAgentCommands.* /** * Created by xing5 on 2017/6/6. */ -class KVMSimulator implements Simulator { - static ConcurrentHashMap connectCmdConcurrentHashMap = new ConcurrentHashMap<>() - private static Map takeSnapshotBackends = [:] +class KVMSimulator implements Simulator { + static ConcurrentHashMap connectCmdConcurrentHashMap = new ConcurrentHashMap<>() + static ConcurrentHashMap ensureSecretUuidCache = new ConcurrentHashMap<>() + static final String MOCK_ENVELOPE_PUBLIC_KEY_BASE64 = "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA=" + private static Map takeSnapshotBackends = [:] static { BeanUtils.reflections.getSubTypesOf(VFSPrimaryStorageTakeSnapshotBackend.class).each { clz -> @@ -309,23 +312,57 @@ class KVMSimulator implements Simulator { return rsp } - spec.simulator(KVMConstant.KVM_CONNECT_PATH) { HttpEntity e -> - Spec.checkHttpCallType(e, true) - KVMAgentCommands.ConnectCmd cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.ConnectCmd.class) + spec.simulator(KVMConstant.KVM_CONNECT_PATH) { HttpEntity e -> + Spec.checkHttpCallType(e, true) + KVMAgentCommands.ConnectCmd cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.ConnectCmd.class) connectCmdConcurrentHashMap.put(cmd.hostUuid, cmd) def rsp = new KVMAgentCommands.ConnectResponse() rsp.success = true rsp.libvirtVersion = "1.0.0" - rsp.qemuVersion = "1.3.0" - return rsp - } - - spec.simulator(KVMConstant.KVM_ECHO_PATH) { HttpEntity e -> - Spec.checkHttpCallType(e, true) - return [:] - } + rsp.qemuVersion = "1.3.0" + return rsp + } + + spec.simulator(KVMConstant.KVM_CREATE_ENVELOPE_KEY_PATH) { + def rsp = new CreatePublicKeyResponse() + rsp.success = true + return rsp + } + + spec.simulator(KVMConstant.KVM_GET_ENVELOPE_KEY_PATH) { + def rsp = new GetPublicKeyResponse() + rsp.success = true + rsp.publicKey = MOCK_ENVELOPE_PUBLIC_KEY_BASE64 + return rsp + } + + spec.simulator(KVMConstant.KVM_VERIFY_ENVELOPE_KEY_PATH) { + def rsp = new VerifyPublicKeyResponse() + rsp.success = true + return rsp + } + + spec.simulator(KVMConstant.KVM_ROTATE_ENVELOPE_KEY_PATH) { + def rsp = new RotatePublicKeyResponse() + rsp.success = true + return rsp + } + + spec.simulator(KVMConstant.KVM_ENSURE_SECRET_PATH) { HttpEntity e -> + String hostUuid = e.getHeaders().getFirst(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID) + SecretHostDefineCmd cmd = JSONObjectUtil.toObject(e.body, SecretHostDefineCmd.class) + String cacheKey = String.format("%s::%s", hostUuid ?: "", cmd?.vmUuid ?: "") + def rsp = new SecretHostDefineResponse() + rsp.secretUuid = ensureSecretUuidCache.computeIfAbsent(cacheKey) { Platform.uuid } + return rsp + } + + spec.simulator(KVMConstant.KVM_ECHO_PATH) { HttpEntity e -> + Spec.checkHttpCallType(e, true) + return [:] + } spec.simulator(KVMConstant.KVM_DETACH_VOLUME) { return new KVMAgentCommands.DetachDataVolumeResponse() From b012c35913d7048a0cf6d94ff7c48247cb9228d5 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 25 Mar 2026 11:12:21 +0800 Subject: [PATCH 069/129] [kvm]: refactor VmHostFileTO to support different operations Split fileFormat into format and operation fields. format only supports Raw and TarballGzip, operation supports Write, Prepare, and Delete. This allows better control over VM host file operations. Related: ZSV-11310 Change-Id: I7074677a656969746d6d71736f6d6d6e62627866 --- .../vm/additions/VmHostFileOperation.java | 7 ++++++ .../java/org/zstack/kvm/KVMAgentCommands.java | 24 +++++++++++++------ .../kvm/efi/KvmSecureBootExtensions.java | 4 +++- 3 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 header/src/main/java/org/zstack/header/vm/additions/VmHostFileOperation.java diff --git a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileOperation.java b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileOperation.java new file mode 100644 index 00000000000..8a7be311680 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileOperation.java @@ -0,0 +1,7 @@ +package org.zstack.header.vm.additions; + +public enum VmHostFileOperation { + Write, + Prepare, + Delete, +} \ 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 08b36331d19..b71cc075c89 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -11,7 +11,6 @@ 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; @@ -2824,8 +2823,6 @@ 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" ... @@ -2833,13 +2830,18 @@ public static class VmHostFileTO { */ private String type; /** - * maybe "Simple" or "TarballGzip" - * if prepare only, use {@link #FORMAT_PREPARE_ONLY} - * @see VmHostFileContentFormat + * file format: Raw or TarballGzip + * @see org.zstack.header.vm.additions.VmHostFileContentFormat */ private String fileFormat; /** - * null if fileFormat is {@link #FORMAT_PREPARE_ONLY} + * operation: Write, Prepare, or Delete + * only use in WriteVmHostFileContentCmd + * @see org.zstack.header.vm.additions.VmHostFileOperation + */ + private String operation; + /** + * null if operation is Prepare or Delete */ @NoLogging private String contentBase64; @@ -2869,6 +2871,14 @@ public void setFileFormat(String fileFormat) { this.fileFormat = fileFormat; } + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + public String getContentBase64() { return contentBase64; } 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 e5904d4a291..9834f4e68c2 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 @@ -40,6 +40,7 @@ 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.VmHostFileOperation; import org.zstack.header.vm.additions.VmHostFileType; import org.zstack.header.vm.additions.VmHostFileVO; import org.zstack.header.vm.additions.VmHostFileVO_; @@ -196,7 +197,7 @@ public void preVmMigration(VmInstanceInventory vm, VmMigrationType type, String VmHostFileTO to = new VmHostFileTO(); to.setPath(buildNvramFilePath(vm.getUuid())); to.setType(VmHostFileType.NvRam.toString()); - to.setFileFormat(VmHostFileTO.FORMAT_PREPARE_ONLY); + to.setOperation(VmHostFileOperation.Prepare.toString()); RewriteVmHostFilesContext context = new RewriteVmHostFilesContext(); context.hostUuid = dstHostUuid; @@ -466,6 +467,7 @@ public void run(FlowTrigger trigger, Map data) { to.setPath(context.path); to.setType(context.type.toString()); to.setFileFormat(content.getFormat().toString()); + to.setOperation(VmHostFileOperation.Write.toString()); String contentBase64 = Base64.getEncoder().encodeToString(content.getContent()); to.setContentBase64(contentBase64); From 237c7376c24697571efb4d41258608b40c97db35 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 24 Mar 2026 15:29:38 +0800 Subject: [PATCH 070/129] [kvm]: sync VM host files when releasing VM resource Resolves: ZSV-11439 Related: ZSV-11310 Change-Id: I6f666b7a676d79776b79636a766f6d676f767476 --- conf/springConfigXml/Kvm.xml | 1 + .../kvm/efi/KvmSecureBootExtensions.java | 51 ++++++++++++++++++- .../test/resources/springConfigXml/Kvm.xml | 1 + 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index 2f857c32a28..a033da22592 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -273,6 +273,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 eb9d9797157..2d4a5844149 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 @@ -37,6 +37,7 @@ import org.zstack.header.vm.VmInstantiateResourceException; import org.zstack.header.vm.VmMigrationType; import org.zstack.header.vm.VmPreMigrationExtensionPoint; +import org.zstack.header.vm.VmReleaseResourceExtensionPoint; import org.zstack.header.vm.additions.VmHostBackupFileVO; import org.zstack.header.vm.additions.VmHostBackupFileVO_; import org.zstack.header.vm.additions.VmHostFileContentVO; @@ -87,7 +88,8 @@ public class KvmSecureBootExtensions implements KVMStartVmExtensionPoint, PreVmInstantiateResourceExtensionPoint, VmInstanceDestroyExtensionPoint, VmPreMigrationExtensionPoint, - AfterReimageVmInstanceExtensionPoint { + AfterReimageVmInstanceExtensionPoint, + VmReleaseResourceExtensionPoint { private static final CLogger logger = Utils.getLogger(KvmSecureBootExtensions.class); @Autowired @@ -731,4 +733,51 @@ public void afterReimageVmInstance(VolumeInventory inventory) { logger.debug(String.format("reset TPM state for VM[uuid:%s] after reimage: " + "deleted all VmHostFileVO and VmHostBackupFileVO records", vmUuid)); } + + @Override + public void releaseVmResource(VmInstanceSpec spec, Completion completion) { + if (spec.getDestHost() == null) { + completion.success(); + return; + } + + String hostUuid = spec.getDestHost().getUuid(); + String vmUuid = spec.getVmInventory().getUuid(); + List vmHostFiles = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) + .eq(VmHostFileVO_.hostUuid, hostUuid) + .list(); + if (vmHostFiles.isEmpty()) { + completion.success(); + return; + } + + logger.info(String.format("try to sync VM host file[vmUuid=%s] from host[uuid=%s]", vmUuid, hostUuid)); + SyncVmHostFilesFromHostMsg syncMsg = new SyncVmHostFilesFromHostMsg(); + syncMsg.setHostUuid(hostUuid); + syncMsg.setVmUuid(vmUuid); + + for (VmHostFileVO file : vmHostFiles) { + if (file.getType() == VmHostFileType.NvRam) { + syncMsg.setNvRamPath(file.getPath()); + } else if (file.getType() == VmHostFileType.TpmState) { + syncMsg.setTpmStateFolder(file.getPath()); + } else { + logger.warn(String.format("unsupported vm host file type: %s, skip syncing for VM[uuid:%s] from host[uuid:%s]", + file.getType(), vmUuid, hostUuid)); + } + } + + bus.makeLocalServiceId(syncMsg, VmInstanceConstant.SECURE_BOOT_SERVICE_ID); + bus.send(syncMsg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + logger.warn(String.format("failed to sync VM host file[vmUuid=%s] from host[uuid=%s] but still continue: %s", + vmUuid, hostUuid, reply.getError().getReadableDetails())); + } + completion.success(); + } + }); + } } diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index b0a0350539d..2fd0f6855a8 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -272,6 +272,7 @@ + From 339a6040173e56ea3a767a9553903ff8f5615cea Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 25 Mar 2026 19:29:53 +0800 Subject: [PATCH 071/129] [kvm]: sync VM host files after migration Add VmInstanceMigrateExtensionPoint to KvmSecureBootExtensions. In afterMigrateVm, clean up stale VmHostFileVO/VmHostFileContentVO on dest host first, then sync host files (NvRam/TpmState) from the dest host using src host records as path reference. Errors are tolerated to avoid blocking the migration flow. Resolves: ZSV-11438 Related: ZSV-11310 Change-Id: I6a7375747776657a63756a7072796570647a6a64 --- conf/springConfigXml/Kvm.xml | 1 + .../kvm/efi/KvmSecureBootExtensions.java | 69 ++++++++++++++++++- .../test/resources/springConfigXml/Kvm.xml | 1 + 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index a033da22592..9c86e529b66 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -274,6 +274,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 2d4a5844149..20fe57d8598 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 @@ -13,6 +13,7 @@ import org.zstack.core.db.SQL; import org.zstack.core.workflow.SimpleFlowChain; import org.zstack.header.core.Completion; +import org.zstack.header.core.NoErrorCompletion; import org.zstack.header.core.ReturnValueCompletion; import org.zstack.header.core.workflow.Flow; import org.zstack.header.core.workflow.FlowDoneHandler; @@ -33,6 +34,7 @@ import org.zstack.header.vm.AfterReimageVmInstanceExtensionPoint; import org.zstack.header.vm.VmInstanceDestroyExtensionPoint; import org.zstack.header.vm.VmInstanceInventory; +import org.zstack.header.vm.VmInstanceMigrateExtensionPoint; import org.zstack.header.vm.VmInstanceSpec; import org.zstack.header.vm.VmInstantiateResourceException; import org.zstack.header.vm.VmMigrationType; @@ -89,7 +91,8 @@ public class KvmSecureBootExtensions implements KVMStartVmExtensionPoint, VmInstanceDestroyExtensionPoint, VmPreMigrationExtensionPoint, AfterReimageVmInstanceExtensionPoint, - VmReleaseResourceExtensionPoint { + VmReleaseResourceExtensionPoint, + VmInstanceMigrateExtensionPoint { private static final CLogger logger = Utils.getLogger(KvmSecureBootExtensions.class); @Autowired @@ -780,4 +783,68 @@ public void run(MessageReply reply) { } }); } + + @Override + public void afterMigrateVm(VmInstanceInventory inv, String srcHostUuid, NoErrorCompletion completion) { + String destHostUuid = inv.getHostUuid(); + String vmUuid = inv.getUuid(); + + List vmHostFiles = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) + .eq(VmHostFileVO_.hostUuid, srcHostUuid) + .list(); + if (vmHostFiles.isEmpty()) { + completion.done(); + return; + } + + // clean up stale VmHostFileVO/VmHostFileContentVO on dest host + // before sync creates new records + List staleFiles = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) + .eq(VmHostFileVO_.hostUuid, destHostUuid) + .list(); + if (!staleFiles.isEmpty()) { + List staleUuids = staleFiles.stream() + .map(VmHostFileVO::getUuid) + .collect(java.util.stream.Collectors.toList()); + SQL.New(VmHostFileContentVO.class) + .in(VmHostFileContentVO_.uuid, staleUuids) + .delete(); + SQL.New(VmHostFileVO.class) + .in(VmHostFileVO_.uuid, staleUuids) + .delete(); + logger.debug(String.format("cleaned up %d stale VmHostFileVO/Content on dest host[uuid=%s] for VM[uuid=%s]", + staleFiles.size(), destHostUuid, vmUuid)); + } + + logger.info(String.format("sync VM host file[vmUuid=%s] from dest host[uuid=%s] after migration", + vmUuid, destHostUuid)); + SyncVmHostFilesFromHostMsg syncMsg = new SyncVmHostFilesFromHostMsg(); + syncMsg.setHostUuid(destHostUuid); + syncMsg.setVmUuid(vmUuid); + + for (VmHostFileVO file : vmHostFiles) { + if (file.getType() == VmHostFileType.NvRam) { + syncMsg.setNvRamPath(file.getPath()); + } else if (file.getType() == VmHostFileType.TpmState) { + syncMsg.setTpmStateFolder(file.getPath()); + } else { + logger.warn(String.format("unsupported vm host file type: %s, skip syncing for VM[uuid:%s]", + file.getType(), vmUuid)); + } + } + + bus.makeLocalServiceId(syncMsg, VmInstanceConstant.SECURE_BOOT_SERVICE_ID); + bus.send(syncMsg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + logger.warn(String.format("failed to sync VM host file[vmUuid=%s] from host[uuid=%s] after migration, but tolerated: %s", + vmUuid, destHostUuid, reply.getError().getReadableDetails())); + } + completion.done(); + } + }); + } } diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index 2fd0f6855a8..dfc9db39c87 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -273,6 +273,7 @@ + From c60433be1a364cae8f87b368e6a0b3db94b6aa70 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 25 Mar 2026 17:07:50 +0800 Subject: [PATCH 072/129] [kvm]: reset TPM state on VM reimage * Introduce RestoreVmHostFileMsg for restoring host files from snapshot group backups during revert. * Refactor VolumeSnapshotGroupBase revert flow to use SimpleFlowChain with restore-vm-host-file step. Resolves: ZSV-11441 Related: ZSV-11310 Change-Id: I716b786c7366657077686561656c6e6f696b6577 --- .../vm/additions/RestoreVmHostFileMsg.java | 26 ++ .../vm/additions/RestoreVmHostFileReply.java | 6 + .../zstack/kvm/efi/KvmSecureBootManager.java | 239 ++++++++++++++++++ .../group/VolumeSnapshotGroupBase.java | 114 ++++++--- 4 files changed, 348 insertions(+), 37 deletions(-) create mode 100644 header/src/main/java/org/zstack/header/vm/additions/RestoreVmHostFileMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/additions/RestoreVmHostFileReply.java diff --git a/header/src/main/java/org/zstack/header/vm/additions/RestoreVmHostFileMsg.java b/header/src/main/java/org/zstack/header/vm/additions/RestoreVmHostFileMsg.java new file mode 100644 index 00000000000..1295c7474f4 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/additions/RestoreVmHostFileMsg.java @@ -0,0 +1,26 @@ +package org.zstack.header.vm.additions; + +import org.zstack.header.message.NeedReplyMessage; +import org.zstack.header.vm.VmInstanceMessage; + +public class RestoreVmHostFileMsg extends NeedReplyMessage implements VmInstanceMessage { + private String vmInstanceUuid; + private String snapshotGroupUuid; + + @Override + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public String getSnapshotGroupUuid() { + return snapshotGroupUuid; + } + + public void setSnapshotGroupUuid(String snapshotGroupUuid) { + this.snapshotGroupUuid = snapshotGroupUuid; + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/vm/additions/RestoreVmHostFileReply.java b/header/src/main/java/org/zstack/header/vm/additions/RestoreVmHostFileReply.java new file mode 100644 index 00000000000..602059a45dc --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/additions/RestoreVmHostFileReply.java @@ -0,0 +1,6 @@ +package org.zstack.header.vm.additions; + +import org.zstack.header.message.MessageReply; + +public class RestoreVmHostFileReply extends MessageReply { +} 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 41c4d564572..11a8b392926 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 @@ -17,6 +17,7 @@ import org.zstack.header.AbstractService; import org.zstack.header.core.ReturnValueCompletion; 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.FlowTrigger; @@ -32,11 +33,14 @@ import org.zstack.header.vm.VmInstanceConstant; import org.zstack.header.vm.VmInstanceVO; import org.zstack.header.vm.VmInstanceVO_; +import org.zstack.header.vm.additions.RestoreVmHostFileMsg; +import org.zstack.header.vm.additions.RestoreVmHostFileReply; 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_; +import org.zstack.header.vm.additions.VmHostFileOperation; import org.zstack.header.vm.additions.VmHostFileType; import org.zstack.header.vm.additions.VmHostFileVO; import org.zstack.header.vm.additions.VmHostFileVO_; @@ -56,14 +60,20 @@ import java.util.Base64; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; 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.core.Platform.operr; +import static org.zstack.kvm.KVMAgentCommands.*; import static org.zstack.kvm.KVMConstant.READ_VM_HOST_FILE_PATH; +import static org.zstack.kvm.KVMConstant.WRITE_VM_HOST_FILE_PATH; +import static org.zstack.kvm.KVMConstant.buildNvramFilePath; +import static org.zstack.kvm.KVMConstant.buildTpmStateFilePath; import static org.zstack.utils.CollectionDSL.list; import static org.zstack.utils.CollectionUtils.findOneOrNull; import static org.zstack.utils.CollectionUtils.transform; @@ -165,6 +175,8 @@ public void handleMessage(Message msg) { handle((CloneVmHostFileMsg) msg); } else if (msg instanceof BackupVmHostFileMsg) { handle((BackupVmHostFileMsg) msg); + } else if (msg instanceof RestoreVmHostFileMsg) { + handle((RestoreVmHostFileMsg) msg); } else { bus.dealWithUnknownMessage(msg); } @@ -555,4 +567,231 @@ protected void scripts() { }.execute(); return filesNeedPersists; } + + private void handle(RestoreVmHostFileMsg msg) { + RestoreVmHostFileReply reply = new RestoreVmHostFileReply(); + + List backupFiles = Q.New(VmHostBackupFileVO.class) + .eq(VmHostBackupFileVO_.resourceUuid, msg.getSnapshotGroupUuid()) + .list(); + + Tuple tuple = Q.New(VmInstanceVO.class) + .select(VmInstanceVO_.hostUuid, VmInstanceVO_.lastHostUuid) + .eq(VmInstanceVO_.uuid, msg.getVmInstanceUuid()) + .findTuple(); + if (tuple == null) { + reply.setError(operr("VM instance [uuid:%s] not found", msg.getVmInstanceUuid())); + bus.reply(msg, reply); + return; + } + + String hostUuid = tuple.get(0, String.class); + if (hostUuid == null) { + hostUuid = tuple.get(1, String.class); + } + if (hostUuid == null) { + reply.setError(operr("VM instance [uuid:%s] has no host", msg.getVmInstanceUuid())); + bus.reply(msg, reply); + return; + } + + List currentHostFiles = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, msg.getVmInstanceUuid()) + .eq(VmHostFileVO_.hostUuid, hostUuid) + .list(); + + Map currentFilesByType = new HashMap<>(); + for (VmHostFileVO file : currentHostFiles) { + currentFilesByType.put(file.getType(), file); + } + + Map backupFilesByType = new HashMap<>(); + for (VmHostBackupFileVO file : backupFiles) { + backupFilesByType.put(file.getType(), file); + } + + Set allTypes = new HashSet<>(); + allTypes.addAll(currentFilesByType.keySet()); + allTypes.addAll(backupFilesByType.keySet()); + + if (allTypes.isEmpty()) { + bus.reply(msg, reply); + return; + } + + List fileList = new ArrayList<>(); + for (VmHostFileType type : allTypes) { + VmHostFileTO to = new VmHostFileTO(); + to.setType(type.toString()); + + boolean hasCurrentFile = currentFilesByType.containsKey(type); + boolean hasBackupFile = backupFilesByType.containsKey(type); + + if (hasBackupFile) { + // Write operation + VmHostBackupFileVO backupFile = backupFilesByType.get(type); + VmHostFileContentVO content = Q.New(VmHostFileContentVO.class) + .eq(VmHostFileContentVO_.uuid, backupFile.getUuid()) + .find(); + if (content == null) { + logger.warn(String.format("backup file content [uuid:%s] not found for type %s", + backupFile.getUuid(), type)); + continue; + } + + if (type == VmHostFileType.NvRam) { + to.setPath(buildNvramFilePath(msg.getVmInstanceUuid())); + } else if (type == VmHostFileType.TpmState) { + to.setPath(buildTpmStateFilePath(msg.getVmInstanceUuid())); + } + + to.setFileFormat(content.getFormat().toString()); + to.setOperation(VmHostFileOperation.Write.toString()); + String contentBase64 = Base64.getEncoder().encodeToString(content.getContent()); + to.setContentBase64(contentBase64); + + fileList.add(to); + } else if (hasCurrentFile) { + // Delete operation + VmHostFileVO currentFile = currentFilesByType.get(type); + to.setPath(currentFile.getPath()); + to.setOperation(VmHostFileOperation.Delete.toString()); + + fileList.add(to); + } + } + + if (fileList.isEmpty()) { + bus.reply(msg, reply); + return; + } + + final String finalHostUuid = hostUuid; + SimpleFlowChain.of("restore-vm-host-file") + .then(Flow.of("send-cmd") + .handle(trigger -> { + KVMAgentCommands.WriteVmHostFileContentCmd cmd = new KVMAgentCommands.WriteVmHostFileContentCmd(); + cmd.setHostFiles(fileList); + + KvmCommandSender sender = new KvmCommandSender(finalHostUuid); + sender.send(cmd, WRITE_VM_HOST_FILE_PATH, wrapper -> { + KVMAgentCommands.WriteVmHostFileContentResponse writeRsp = wrapper.getResponse(KVMAgentCommands.WriteVmHostFileContentResponse.class); + return writeRsp.isSuccess() ? null : + operr("failed to write/delete host file response").withException(writeRsp.getError()); + }, new ReturnValueCompletion(msg) { + @Override + public void success(KvmResponseWrapper wrapper) { + KVMAgentCommands.WriteVmHostFileContentResponse writeRsp = wrapper.getResponse(KVMAgentCommands.WriteVmHostFileContentResponse.class); + if (writeRsp.isSuccess()) { + logger.info(String.format("success to restore host files for VM[uuid:%s] from snapshot group[uuid:%s]", + msg.getVmInstanceUuid(), msg.getSnapshotGroupUuid())); + trigger.next(); + return; + } + trigger.fail(operr("failed to write/delete host file") + .withException(writeRsp.getError())); + } + + @Override + public void fail(ErrorCode errorCode) { + trigger.fail(operr("failed to restore host files for VM[uuid:%s]", msg.getVmInstanceUuid()) + .withCause(errorCode)); + } + }); + }) + .build()) + .then(Flow.of("persist-content-in-db") + .handle(trigger -> { + Timestamp now = Timestamp.from(Instant.now()); + + for (VmHostFileType type : allTypes) { + boolean hasCurrentFile = currentFilesByType.containsKey(type); + boolean hasBackupFile = backupFilesByType.containsKey(type); + + if (hasBackupFile) { + VmHostBackupFileVO backupFile = backupFilesByType.get(type); + VmHostFileContentVO backupContent = Q.New(VmHostFileContentVO.class) + .eq(VmHostFileContentVO_.uuid, backupFile.getUuid()) + .find(); + if (backupContent == null) { + continue; + } + + if (hasCurrentFile) { + // update existing VmHostFileVO and VmHostFileContentVO + VmHostFileVO currentFile = currentFilesByType.get(type); + SQL.New(VmHostFileVO.class) + .eq(VmHostFileVO_.uuid, currentFile.getUuid()) + .set(VmHostFileVO_.lastOpDate, now) + .update(); + + VmHostFileContentVO existingContent = Q.New(VmHostFileContentVO.class) + .eq(VmHostFileContentVO_.uuid, currentFile.getUuid()) + .find(); + if (existingContent != null) { + SQL.New(VmHostFileContentVO.class) + .eq(VmHostFileContentVO_.uuid, currentFile.getUuid()) + .set(VmHostFileContentVO_.content, backupContent.getContent()) + .set(VmHostFileContentVO_.format, backupContent.getFormat()) + .set(VmHostFileContentVO_.lastOpDate, now) + .update(); + } else { + VmHostFileContentVO newContent = new VmHostFileContentVO(); + newContent.setUuid(currentFile.getUuid()); + newContent.setContent(backupContent.getContent()); + newContent.setFormat(backupContent.getFormat()); + newContent.setCreateDate(now); + newContent.setLastOpDate(now); + databaseFacade.persist(newContent); + } + } else { + // create new VmHostFileVO and VmHostFileContentVO + String path; + if (type == VmHostFileType.NvRam) { + path = buildNvramFilePath(msg.getVmInstanceUuid()); + } else { + path = buildTpmStateFilePath(msg.getVmInstanceUuid()); + } + + VmHostFileVO newFile = new VmHostFileVO(); + newFile.setUuid(Platform.getUuid()); + newFile.setVmInstanceUuid(msg.getVmInstanceUuid()); + newFile.setHostUuid(finalHostUuid); + newFile.setType(type); + newFile.setPath(path); + newFile.setCreateDate(now); + newFile.setLastOpDate(now); + databaseFacade.persist(newFile); + + VmHostFileContentVO newContent = new VmHostFileContentVO(); + newContent.setUuid(newFile.getUuid()); + newContent.setContent(backupContent.getContent()); + newContent.setFormat(backupContent.getFormat()); + newContent.setCreateDate(now); + newContent.setLastOpDate(now); + databaseFacade.persist(newContent); + } + } else if (hasCurrentFile) { + // delete VmHostFileVO and VmHostFileContentVO + VmHostFileVO currentFile = currentFilesByType.get(type); + SQL.New(VmHostFileContentVO.class) + .eq(VmHostFileContentVO_.uuid, currentFile.getUuid()) + .delete(); + SQL.New(VmHostFileVO.class) + .eq(VmHostFileVO_.uuid, currentFile.getUuid()) + .delete(); + } + } + + trigger.next(); + }) + .build()) + .propagateExceptionTo(msg) + .done(() -> bus.reply(msg, reply)) + .error(errorCode -> { + reply.setError(errorCode); + bus.reply(msg, reply); + }) + .start(); + } } diff --git a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java index 07881c552ed..1c808ced39e 100644 --- a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java @@ -29,6 +29,7 @@ import org.zstack.header.storage.snapshot.VolumeSnapshotConstant; import org.zstack.header.storage.snapshot.VolumeSnapshotVO; import org.zstack.header.storage.snapshot.group.*; +import org.zstack.header.vm.additions.RestoreVmHostFileMsg; import org.zstack.header.vm.RestoreVmInstanceMsg; import org.zstack.header.vm.VmInstanceConstant; import org.zstack.header.vm.additions.VmHostBackupFileVO; @@ -38,6 +39,7 @@ import org.zstack.header.volume.VolumeVO; import org.zstack.header.volume.VolumeVO_; import org.zstack.storage.snapshot.VolumeSnapshotGlobalConfig; +import org.zstack.utils.DebugUtils; import org.zstack.utils.TimeUtils; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; @@ -378,46 +380,84 @@ private void handle(RevertVmFromSnapshotGroupInnerMsg msg) { return; } - VolumeSnapshotGroupVO newGroup = null; - if (VolumeSnapshotGlobalConfig.SNAPSHOT_BEFORE_REVERTVOLUME.value(Boolean.class)) { - newGroup = new VolumeSnapshotGroupVO(); - newGroup.setUuid(Platform.getUuid()); - newGroup.setName(String.format("revert-vm-point-%s-%s", vmUuid, TimeUtils.getCurrentTimeStamp("yyyyMMddHHmmss"))); - newGroup.setDescription(String.format("save snapshot for revert vm [uuid:%s]", vmUuid)); - newGroup.setSnapshotCount(snapshots.size()); - newGroup.setVmInstanceUuid(vmUuid); - newGroup.setAccountUuid(msg.getSession().getAccountUuid()); - dbf.persist(newGroup); + class Context { + VolumeSnapshotGroupVO newGroup; } + Context context = new Context(); + + SimpleFlowChain.of("revert-vm-from-snapshot-group-inner") + .then(Flow.of("persist-before-revert-snapshot-in-db") + .runIf(data -> VolumeSnapshotGlobalConfig.SNAPSHOT_BEFORE_REVERTVOLUME.value(Boolean.class)) + .handle(trigger -> { + context.newGroup = new VolumeSnapshotGroupVO(); + context.newGroup.setUuid(Platform.getUuid()); + context.newGroup.setName(String.format("revert-vm-point-%s-%s", vmUuid, TimeUtils.getCurrentTimeStamp("yyyyMMddHHmmss"))); + context.newGroup.setDescription(String.format("save snapshot for revert vm [uuid:%s]", vmUuid)); + context.newGroup.setSnapshotCount(snapshots.size()); + context.newGroup.setVmInstanceUuid(vmUuid); + context.newGroup.setAccountUuid(msg.getSession().getAccountUuid()); + dbf.persist(context.newGroup); + trigger.next(); + }) + .build()) + .then(Flow.of("restore-vm-host-file") + .handle(trigger -> { + RestoreVmHostFileMsg restoreMsg = new RestoreVmHostFileMsg(); + restoreMsg.setVmInstanceUuid(vmUuid); + restoreMsg.setSnapshotGroupUuid(self.getUuid()); + bus.makeTargetServiceIdByResourceUuid(restoreMsg, VmInstanceConstant.SECURE_BOOT_SERVICE_ID, vmUuid); + bus.send(restoreMsg, new CloudBusCallBack(trigger) { + @Override + public void run(MessageReply reply) { + if (reply.isSuccess()) { + trigger.next(); + } else { + trigger.fail(reply.getError()); + } + } + }); + }) + .build()) + .then(Flow.of("revert-every-volumes") + .handle(trigger -> { + final String finalNewGroupUuid = context.newGroup == null ? null : context.newGroup.getUuid(); + new While<>(snapshots).each((snapshot, compl) -> { + if (Q.New(VolumeVO.class).eq(VolumeVO_.uuid, snapshot.getVolumeUuid()).eq(VolumeVO_.type, VolumeType.Memory).isExists()) { + compl.done(); + return; + } - final String finalNewGroupUuid = newGroup == null ? null : newGroup.getUuid(); - new While<>(snapshots).each((snapshot, compl) -> { - if (Q.New(VolumeVO.class).eq(VolumeVO_.uuid, snapshot.getVolumeUuid()).eq(VolumeVO_.type, VolumeType.Memory).isExists()) { - compl.done(); - return; - } - - RevertVolumeFromSnapshotGroupMsg rmsg = new RevertVolumeFromSnapshotGroupMsg(); - rmsg.setSnapshotUuid(snapshot.getUuid()); - rmsg.setVolumeUuid(snapshot.getVolumeUuid()); - rmsg.setTreeUuid(snapshot.getTreeUuid()); - rmsg.setSession(msg.getSession()); - rmsg.setNewSnapshotGroupUuid(finalNewGroupUuid); - - bus.makeTargetServiceIdByResourceUuid(rmsg, VolumeSnapshotConstant.SERVICE_ID, getResourceIdToRouteMsg(snapshot)); - bus.send(rmsg, new CloudBusCallBack(compl) { - @Override - public void run(MessageReply r) { - reply.addResult(new RevertSnapshotGroupResult(rmsg.getSnapshotUuid(), rmsg.getVolumeUuid(), r.getError())); - compl.done(); - } - }); - }).run(new WhileDoneCompletion(msg) { - @Override - public void done(ErrorCodeList errorCodeList) { + RevertVolumeFromSnapshotGroupMsg rmsg = new RevertVolumeFromSnapshotGroupMsg(); + rmsg.setSnapshotUuid(snapshot.getUuid()); + rmsg.setVolumeUuid(snapshot.getVolumeUuid()); + rmsg.setTreeUuid(snapshot.getTreeUuid()); + rmsg.setSession(msg.getSession()); + rmsg.setNewSnapshotGroupUuid(finalNewGroupUuid); + + bus.makeTargetServiceIdByResourceUuid(rmsg, VolumeSnapshotConstant.SERVICE_ID, getResourceIdToRouteMsg(snapshot)); + bus.send(rmsg, new CloudBusCallBack(compl) { + @Override + public void run(MessageReply r) { + reply.addResult(new RevertSnapshotGroupResult(rmsg.getSnapshotUuid(), rmsg.getVolumeUuid(), r.getError())); + compl.done(); + } + }); + }).run(new WhileDoneCompletion(msg) { + @Override + public void done(ErrorCodeList errorCodeList) { + DebugUtils.Assert(!errorCodeList.hasError(), "no errorCode expected"); + trigger.next(); + } + }); + }) + .build()) + .propagateExceptionTo(msg) + .done(() -> bus.reply(msg, reply)) + .error(errorCode -> { + reply.setError(errorCode); bus.reply(msg, reply); - } - }); + }) + .start(); } public List getSnapshots() { From a8f687b78d82cbda59be19cb6458b1d745048bc1 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 26 Mar 2026 01:15:23 +0800 Subject: [PATCH 073/129] [kvm]: batch query VM host file content * Optimize RestoreVmHostFileMsg handle method by batching database queries for VmHostFileContentVO to avoid N+1 query problem in loops * Add buildPathFor VmHostFileType helper method to unify path construction logic. Resolves: ZSV-11441 Related: ZSV-11310 Change-Id: I72726b67686471676f6b6b7977616e61636e6671 --- .../main/java/org/zstack/kvm/KVMConstant.java | 10 +++ .../kvm/efi/KvmSecureBootExtensions.java | 5 +- .../zstack/kvm/efi/KvmSecureBootManager.java | 72 +++++++++---------- .../group/VolumeSnapshotGroupBase.java | 2 +- 4 files changed, 47 insertions(+), 42 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 c2cd54c184f..00e1cb723be 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -1,7 +1,9 @@ package org.zstack.kvm; import org.zstack.header.configuration.PythonClass; +import org.zstack.header.exception.CloudRuntimeException; import org.zstack.header.vm.VmInstanceState; +import org.zstack.header.vm.additions.VmHostFileType; @PythonClass public interface KVMConstant { @@ -209,6 +211,14 @@ public static String buildTpmStateFilePath(String vmUuid) { return String.format(TPM_STATE_FILE_PATH_FORMAT, vmUuidWithHyphen); } + public static String buildPathForVmHostFileType(VmHostFileType type, String vmUuid) { + switch (type) { + case NvRam: return buildNvramFilePath(vmUuid); + case TpmState: return buildTpmStateFilePath(vmUuid); + default: throw new CloudRuntimeException("unsupported VmHostFileType: " + type); + } + } + 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/efi/KvmSecureBootExtensions.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java index faba888450b..c3738b900c4 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 @@ -440,10 +440,7 @@ public void run(FlowTrigger trigger, Map data) { 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; - } + context.path = buildPathForVmHostFileType(context.type, context.vmUuid); } trigger.next(); } 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 11a8b392926..777109f58d9 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 @@ -65,17 +65,16 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Function; 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.core.Platform.operr; import static org.zstack.kvm.KVMAgentCommands.*; -import static org.zstack.kvm.KVMConstant.READ_VM_HOST_FILE_PATH; -import static org.zstack.kvm.KVMConstant.WRITE_VM_HOST_FILE_PATH; -import static org.zstack.kvm.KVMConstant.buildNvramFilePath; -import static org.zstack.kvm.KVMConstant.buildTpmStateFilePath; +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.toMap; import static org.zstack.utils.CollectionUtils.transform; public class KvmSecureBootManager extends AbstractService { @@ -619,6 +618,16 @@ private void handle(RestoreVmHostFileMsg msg) { return; } + // Batch query all backup file content before loop + List backupUuids = transform(backupFiles, VmHostBackupFileVO::getUuid); + Map backupContentMap = new HashMap<>(); + if (!backupUuids.isEmpty()) { + List backupContents = Q.New(VmHostFileContentVO.class) + .in(VmHostFileContentVO_.uuid, backupUuids) + .list(); + backupContentMap.putAll(toMap(backupContents, VmHostFileContentVO::getUuid, Function.identity())); + } + List fileList = new ArrayList<>(); for (VmHostFileType type : allTypes) { VmHostFileTO to = new VmHostFileTO(); @@ -630,21 +639,14 @@ private void handle(RestoreVmHostFileMsg msg) { if (hasBackupFile) { // Write operation VmHostBackupFileVO backupFile = backupFilesByType.get(type); - VmHostFileContentVO content = Q.New(VmHostFileContentVO.class) - .eq(VmHostFileContentVO_.uuid, backupFile.getUuid()) - .find(); + VmHostFileContentVO content = backupContentMap.get(backupFile.getUuid()); if (content == null) { logger.warn(String.format("backup file content [uuid:%s] not found for type %s", backupFile.getUuid(), type)); continue; } - if (type == VmHostFileType.NvRam) { - to.setPath(buildNvramFilePath(msg.getVmInstanceUuid())); - } else if (type == VmHostFileType.TpmState) { - to.setPath(buildTpmStateFilePath(msg.getVmInstanceUuid())); - } - + to.setPath(buildPathForVmHostFileType(type, msg.getVmInstanceUuid())); to.setFileFormat(content.getFormat().toString()); to.setOperation(VmHostFileOperation.Write.toString()); String contentBase64 = Base64.getEncoder().encodeToString(content.getContent()); @@ -678,18 +680,12 @@ private void handle(RestoreVmHostFileMsg msg) { KVMAgentCommands.WriteVmHostFileContentResponse writeRsp = wrapper.getResponse(KVMAgentCommands.WriteVmHostFileContentResponse.class); return writeRsp.isSuccess() ? null : operr("failed to write/delete host file response").withException(writeRsp.getError()); - }, new ReturnValueCompletion(msg) { + }, new ReturnValueCompletion(trigger) { @Override public void success(KvmResponseWrapper wrapper) { - KVMAgentCommands.WriteVmHostFileContentResponse writeRsp = wrapper.getResponse(KVMAgentCommands.WriteVmHostFileContentResponse.class); - if (writeRsp.isSuccess()) { - logger.info(String.format("success to restore host files for VM[uuid:%s] from snapshot group[uuid:%s]", - msg.getVmInstanceUuid(), msg.getSnapshotGroupUuid())); - trigger.next(); - return; - } - trigger.fail(operr("failed to write/delete host file") - .withException(writeRsp.getError())); + logger.info(String.format("success to restore host files for VM[uuid:%s] from snapshot group[uuid:%s]", + msg.getVmInstanceUuid(), msg.getSnapshotGroupUuid())); + trigger.next(); } @Override @@ -704,15 +700,25 @@ public void fail(ErrorCode errorCode) { .handle(trigger -> { Timestamp now = Timestamp.from(Instant.now()); + List allUuids = new ArrayList<>(); + allUuids.addAll(transform(backupFilesByType.values(), VmHostBackupFileVO::getUuid)); + allUuids.addAll(transform(currentFilesByType.values(), VmHostFileVO::getUuid)); + + Map contentMap = new HashMap<>(); + if (!allUuids.isEmpty()) { + List contents = Q.New(VmHostFileContentVO.class) + .in(VmHostFileContentVO_.uuid, allUuids) + .list(); + contentMap.putAll(toMap(contents, VmHostFileContentVO::getUuid, Function.identity())); + } + for (VmHostFileType type : allTypes) { boolean hasCurrentFile = currentFilesByType.containsKey(type); boolean hasBackupFile = backupFilesByType.containsKey(type); if (hasBackupFile) { VmHostBackupFileVO backupFile = backupFilesByType.get(type); - VmHostFileContentVO backupContent = Q.New(VmHostFileContentVO.class) - .eq(VmHostFileContentVO_.uuid, backupFile.getUuid()) - .find(); + VmHostFileContentVO backupContent = contentMap.get(backupFile.getUuid()); if (backupContent == null) { continue; } @@ -725,9 +731,7 @@ public void fail(ErrorCode errorCode) { .set(VmHostFileVO_.lastOpDate, now) .update(); - VmHostFileContentVO existingContent = Q.New(VmHostFileContentVO.class) - .eq(VmHostFileContentVO_.uuid, currentFile.getUuid()) - .find(); + VmHostFileContentVO existingContent = contentMap.get(currentFile.getUuid()); if (existingContent != null) { SQL.New(VmHostFileContentVO.class) .eq(VmHostFileContentVO_.uuid, currentFile.getUuid()) @@ -746,19 +750,13 @@ public void fail(ErrorCode errorCode) { } } else { // create new VmHostFileVO and VmHostFileContentVO - String path; - if (type == VmHostFileType.NvRam) { - path = buildNvramFilePath(msg.getVmInstanceUuid()); - } else { - path = buildTpmStateFilePath(msg.getVmInstanceUuid()); - } - VmHostFileVO newFile = new VmHostFileVO(); newFile.setUuid(Platform.getUuid()); + newFile.setResourceName(String.format("%s file for %s", type, msg.getVmInstanceUuid())); newFile.setVmInstanceUuid(msg.getVmInstanceUuid()); newFile.setHostUuid(finalHostUuid); newFile.setType(type); - newFile.setPath(path); + newFile.setPath(buildPathForVmHostFileType(type, msg.getVmInstanceUuid())); newFile.setCreateDate(now); newFile.setLastOpDate(now); databaseFacade.persist(newFile); diff --git a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java index 1c808ced39e..a0730abb0e0 100644 --- a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java @@ -405,7 +405,7 @@ class Context { RestoreVmHostFileMsg restoreMsg = new RestoreVmHostFileMsg(); restoreMsg.setVmInstanceUuid(vmUuid); restoreMsg.setSnapshotGroupUuid(self.getUuid()); - bus.makeTargetServiceIdByResourceUuid(restoreMsg, VmInstanceConstant.SECURE_BOOT_SERVICE_ID, vmUuid); + bus.makeLocalServiceId(restoreMsg, VmInstanceConstant.SECURE_BOOT_SERVICE_ID); bus.send(restoreMsg, new CloudBusCallBack(trigger) { @Override public void run(MessageReply reply) { From 1eab431a44a12c5d2e3e65e1e8e8ed16909cc263 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 26 Mar 2026 03:34:17 +0800 Subject: [PATCH 074/129] [kvm]: reset TPM state after VM clone Introduce ResetVmTpmMsg to reset TPM state after VM clone. Add reset-vm-tpm flow step in VmInstanceBase to optionally reset TPM based on RESET_TPM_AFTER_VM_CLONE config. KvmTpmManager handles the reset by deleting VmHostFileVO and VmHostBackupFileVO for TpmState type. Resolves: ZSV-11440 Related: ZSV-11310 Change-Id: I6e63776a6e77706765727a66746f7a6c73766d62 --- .../org/zstack/compute/vm/VmInstanceBase.java | 36 +++++++++++++++++++ .../header/vm/additions/ResetVmTpmMsg.java | 15 ++++++++ .../header/vm/additions/ResetVmTpmReply.java | 6 ++++ .../org/zstack/kvm/tpm/KvmTpmManager.java | 22 ++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 header/src/main/java/org/zstack/header/vm/additions/ResetVmTpmMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/additions/ResetVmTpmReply.java 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 49b076a2fc6..ebb73ad1907 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java @@ -45,6 +45,7 @@ import org.zstack.header.message.*; import org.zstack.header.network.l3.*; import org.zstack.header.storage.primary.*; +import org.zstack.header.tpm.TpmConstants; import org.zstack.header.vm.*; import org.zstack.header.vm.ChangeVmMetaDataMsg.AtomicHostUuid; import org.zstack.header.vm.ChangeVmMetaDataMsg.AtomicVmState; @@ -56,6 +57,7 @@ import org.zstack.header.vm.VmInstanceSpec.CdRomSpec; import org.zstack.header.vm.VmInstanceSpec.HostName; import org.zstack.header.vm.VmInstanceSpec.IsoSpec; +import org.zstack.header.vm.additions.ResetVmTpmMsg; import org.zstack.header.vm.cdrom.*; import org.zstack.header.vm.devices.VmInstanceResourceMetadataManager; import org.zstack.header.vo.ResourceVO; @@ -3561,6 +3563,40 @@ public void run(MessageReply reply) { } }); + flow(new NoRollbackFlow() { + String __name__ = "reset-vm-tpm"; + + @Override + public boolean skip(Map data) { + boolean resetTpm; + if (msg.getResetTpm() == null) { + resetTpm = rcf.getResourceConfigValue( + VmGlobalConfig.RESET_TPM_AFTER_VM_CLONE, + msg.getVmInstanceUuid(), Boolean.class); + } else { + resetTpm = msg.getResetTpm(); + } + return !resetTpm; + } + + @Override + public void run(FlowTrigger trigger, Map map) { + ResetVmTpmMsg rmsg = new ResetVmTpmMsg(); + rmsg.setVmInstanceUuid(msg.getVmInstanceUuid()); + bus.makeLocalServiceId(rmsg, TpmConstants.SERVICE_ID); + bus.send(rmsg, new CloudBusCallBack(trigger) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + trigger.fail(reply.getError()); + return; + } + trigger.next(); + } + }); + } + }); + flow(new NoRollbackFlow() { String __name__ = "update-vm-name"; diff --git a/header/src/main/java/org/zstack/header/vm/additions/ResetVmTpmMsg.java b/header/src/main/java/org/zstack/header/vm/additions/ResetVmTpmMsg.java new file mode 100644 index 00000000000..0ce384c46c4 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/additions/ResetVmTpmMsg.java @@ -0,0 +1,15 @@ +package org.zstack.header.vm.additions; + +import org.zstack.header.message.NeedReplyMessage; + +public class ResetVmTpmMsg extends NeedReplyMessage { + private String vmInstanceUuid; + + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/additions/ResetVmTpmReply.java b/header/src/main/java/org/zstack/header/vm/additions/ResetVmTpmReply.java new file mode 100644 index 00000000000..0f100658fe3 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/additions/ResetVmTpmReply.java @@ -0,0 +1,6 @@ +package org.zstack.header.vm.additions; + +import org.zstack.header.message.MessageReply; + +public class ResetVmTpmReply extends MessageReply { +} 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 f0b1dffb7a9..b4744dffd33 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 @@ -44,6 +44,10 @@ 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.ResetVmTpmMsg; +import org.zstack.header.vm.additions.ResetVmTpmReply; +import org.zstack.header.vm.additions.VmHostBackupFileVO; +import org.zstack.header.vm.additions.VmHostBackupFileVO_; import org.zstack.header.vm.additions.VmHostFileInventory; import org.zstack.header.vm.additions.VmHostFileType; import org.zstack.header.vm.additions.VmHostFileVO; @@ -120,6 +124,8 @@ private void handleLocalMessage(Message msg) { handle((RemoveTpmMsg) msg); } else if (msg instanceof CloneVmTpmMsg) { handle((CloneVmTpmMsg) msg); + } else if (msg instanceof ResetVmTpmMsg) { + handle((ResetVmTpmMsg) msg); } else { bus.dealWithUnknownMessage(msg); } @@ -420,6 +426,22 @@ public void handle(ErrorCode errCode, Map data) { }).start(); } + private void handle(ResetVmTpmMsg msg) { + ResetVmTpmReply reply = new ResetVmTpmReply(); + + String vmUuid = msg.getVmInstanceUuid(); + SQL.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) + .eq(VmHostFileVO_.type, VmHostFileType.TpmState) + .delete(); + SQL.New(VmHostBackupFileVO.class) + .eq(VmHostBackupFileVO_.resourceUuid, vmUuid) + .eq(VmHostBackupFileVO_.type, VmHostFileType.TpmState) + .delete(); + + bus.reply(msg, reply); + } + private void handle(APIGetTpmCapabilityMsg msg) { TpmCapabilityView view = new TpmCapabilityView(); From a0f2ab5f679669561956dc5ad5558828f633cd0a Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 26 Mar 2026 09:33:08 +0800 Subject: [PATCH 075/129] [kvm]: use SQLBatch for TPM state reset Combine two separate delete operations for VmHostFileVO and VmHostBackupFileVO into a single SQLBatch transaction. This reduces database transaction overhead and improves performance during TPM state reset. Resolves: ZSV-11440 Related: ZSV-11310 Change-Id: I6b6f73786e6e736475686771747065686676726a --- .../org/zstack/kvm/tpm/KvmTpmManager.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) 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 b4744dffd33..cd7fc8e9c77 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 @@ -9,6 +9,7 @@ import org.zstack.core.cloudbus.MessageSafe; import org.zstack.core.db.Q; import org.zstack.core.db.SQL; +import org.zstack.core.db.SQLBatch; import org.zstack.core.thread.ChainTask; import org.zstack.core.thread.SyncTaskChain; import org.zstack.core.thread.ThreadFacade; @@ -430,14 +431,19 @@ private void handle(ResetVmTpmMsg msg) { ResetVmTpmReply reply = new ResetVmTpmReply(); String vmUuid = msg.getVmInstanceUuid(); - SQL.New(VmHostFileVO.class) - .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) - .eq(VmHostFileVO_.type, VmHostFileType.TpmState) - .delete(); - SQL.New(VmHostBackupFileVO.class) - .eq(VmHostBackupFileVO_.resourceUuid, vmUuid) - .eq(VmHostBackupFileVO_.type, VmHostFileType.TpmState) - .delete(); + new SQLBatch() { + @Override + protected void scripts() { + sql(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) + .eq(VmHostFileVO_.type, VmHostFileType.TpmState) + .delete(); + sql(VmHostBackupFileVO.class) + .eq(VmHostBackupFileVO_.resourceUuid, vmUuid) + .eq(VmHostBackupFileVO_.type, VmHostFileType.TpmState) + .delete(); + } + }.execute(); bus.reply(msg, reply); } From f5ae3c94e7d69505716d496ec73f34d560920390 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 26 Mar 2026 03:54:14 +0800 Subject: [PATCH 076/129] [kvm]: add VM host file periodic sync tracker * Introduce VmHostFileTracker component to periodically sync VM host files (NvRam/TpmState) from hosts for running VMs every 30 minutes * The tracker queries all VmHostFileVO records for VMs in Running state and groups the by vmUuid+hostUuid to trigger sync in parallel with concurrency control. Resolves: ZSV-11613 Related: ZSV-11310 Change-Id: I6b767073656d7474686d7a756871626278647167 --- conf/springConfigXml/Kvm.xml | 6 + .../org/zstack/kvm/efi/VmHostFileTracker.java | 147 ++++++++++++++++++ .../test/resources/springConfigXml/Kvm.xml | 6 + 3 files changed, 159 insertions(+) create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/efi/VmHostFileTracker.java diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index 9c86e529b66..8771289312e 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -277,4 +277,10 @@ + + + + + + diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/VmHostFileTracker.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/VmHostFileTracker.java new file mode 100644 index 00000000000..0d20e4a349b --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/VmHostFileTracker.java @@ -0,0 +1,147 @@ +package org.zstack.kvm.efi; + +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.core.CoreGlobalProperty; +import org.zstack.core.asyncbatch.While; +import org.zstack.core.cloudbus.CloudBus; +import org.zstack.core.cloudbus.CloudBusCallBack; +import org.zstack.core.cloudbus.ResourceDestinationMaker; +import org.zstack.core.db.Q; +import org.zstack.core.thread.PeriodicTask; +import org.zstack.core.thread.ThreadFacade; +import org.zstack.header.Component; +import org.zstack.header.core.WhileDoneCompletion; +import org.zstack.header.errorcode.ErrorCodeList; +import org.zstack.header.message.MessageReply; +import org.zstack.header.vm.VmInstanceAO_; +import org.zstack.header.vm.VmInstanceConstant; +import org.zstack.header.vm.VmInstanceState; +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 java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class VmHostFileTracker implements Component { + private static final CLogger logger = Utils.getLogger(VmHostFileTracker.class); + + private static final long SYNC_INTERVAL_SECONDS = TimeUnit.MINUTES.toSeconds(30); + private static final int SYNC_CONCURRENCY = 5; + + @Autowired + private ThreadFacade threadFacade; + @Autowired + private CloudBus bus; + @Autowired + private ResourceDestinationMaker destinationMaker; + + private Future trackerThread; + + @Override + public boolean start() { + if (CoreGlobalProperty.UNIT_TEST_ON) { + logger.info("VmHostFileTracker is disabled in unit test"); + return true; + } + + trackerThread = threadFacade.submitPeriodicTask(new PeriodicTask() { + @Override + public TimeUnit getTimeUnit() { + return TimeUnit.SECONDS; + } + + @Override + public long getInterval() { + return SYNC_INTERVAL_SECONDS; + } + + @Override + public String getName() { + return "vm-host-file-tracker"; + } + + @Override + public void run() { + logger.info("VmHostFileTracker: starting periodic sync of VM host files"); + syncVmHostFiles(); + } + }); + + return true; + } + + @Override + public boolean stop() { + if (trackerThread != null) { + trackerThread.cancel(true); + } + return true; + } + + private void syncVmHostFiles() { + List hostFiles = Q.New(VmHostFileVO.class, VmInstanceVO.class) + .table0() + .eq(VmHostFileVO_.vmInstanceUuid).table1(VmInstanceAO_.uuid) + .eq(VmHostFileVO_.hostUuid).table1(VmInstanceAO_.hostUuid) + .selectThisTable() + .table1() + .eq(VmInstanceAO_.state, VmInstanceState.Running) + .list(); + + if (hostFiles.isEmpty()) { + return; + } + + Map> grouped = hostFiles.stream() + .collect(Collectors.groupingBy(f -> f.getVmInstanceUuid() + "::" + f.getHostUuid())); + List> groups = new ArrayList<>(grouped.values()); + + new While<>(groups).step((group, whileCompletion) -> { + VmHostFileVO first = group.get(0); + String vmUuid = first.getVmInstanceUuid(); + String hostUuid = first.getHostUuid(); + + if (!destinationMaker.isManagedByUs(vmUuid)) { + whileCompletion.done(); + return; + } + + SyncVmHostFilesFromHostMsg syncMsg = new SyncVmHostFilesFromHostMsg(); + syncMsg.setHostUuid(hostUuid); + syncMsg.setVmUuid(vmUuid); + + for (VmHostFileVO file : group) { + if (file.getType() == VmHostFileType.NvRam) { + syncMsg.setNvRamPath(file.getPath()); + } else if (file.getType() == VmHostFileType.TpmState) { + syncMsg.setTpmStateFolder(file.getPath()); + } + } + + bus.makeLocalServiceId(syncMsg, VmInstanceConstant.SECURE_BOOT_SERVICE_ID); + bus.send(syncMsg, new CloudBusCallBack(whileCompletion) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + logger.warn(String.format("failed to sync VM host file[vmUuid=%s] from host[uuid=%s]: %s", + vmUuid, hostUuid, reply.getError().getReadableDetails())); + } + whileCompletion.done(); + } + }); + }, SYNC_CONCURRENCY).run(new WhileDoneCompletion(null) { + @Override + public void done(ErrorCodeList errorCodeList) { + // periodic sync round finished, empty callback + } + }); + } +} diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index dfc9db39c87..836260935cb 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -276,4 +276,10 @@ + + + + + + From 3039eb8a3aa4283478195d4ad80be392709af126 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 26 Mar 2026 10:52:53 +0800 Subject: [PATCH 077/129] [kvm]: make VM host file sync configurable Add two global configs to control VM host file periodic sync behavior. VM_HOST_FILE_SYNC_INTERVAL controls sync frequency (default 1800 seconds) and VM_HOST_FILE_SYNC_CONCURRENCY controls concurrent sync operations (default 5). Remove hardcoded constants and replace with configurable values. Add GlobalConfigUpdateExtensionPoint listener to restart tracker when configs change dynamically. This provides flexibility for tuning performance. Resolves: ZSV-11613 Related: ZSV-11310 Change-Id: I7273797a666a77617a72796f6e7a6b696373746f --- .../java/org/zstack/kvm/KVMGlobalConfig.java | 10 ++++ .../org/zstack/kvm/efi/VmHostFileTracker.java | 49 +++++++++++++------ 2 files changed, 44 insertions(+), 15 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 96caaa0e5b6..8a92b56202b 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java @@ -166,4 +166,14 @@ public class KVMGlobalConfig { 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"); + + @GlobalConfigValidation(numberGreaterThan = 1, numberLessThan = 86400) + @GlobalConfigDef(defaultValue = "1800", type = Long.class, + description = "Interval in seconds for syncing VM host files (NvRam, TpmState) from KVM hosts") + public static GlobalConfig VM_HOST_FILE_SYNC_INTERVAL = new GlobalConfig(CATEGORY, "vm.host.file.sync.interval"); + + @GlobalConfigValidation(numberGreaterThan = 1, numberLessThan = 30) + @GlobalConfigDef(defaultValue = "5", type = Integer.class, + description = "The concurrency level for syncing VM host files from KVM hosts") + public static GlobalConfig VM_HOST_FILE_SYNC_CONCURRENCY = new GlobalConfig(CATEGORY, "vm.host.file.sync.concurrency"); } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/VmHostFileTracker.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/VmHostFileTracker.java index 0d20e4a349b..203b27cef2b 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/VmHostFileTracker.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/VmHostFileTracker.java @@ -6,6 +6,7 @@ import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.CloudBusCallBack; import org.zstack.core.cloudbus.ResourceDestinationMaker; +import org.zstack.core.config.GlobalConfigUpdateExtensionPoint; import org.zstack.core.db.Q; import org.zstack.core.thread.PeriodicTask; import org.zstack.core.thread.ThreadFacade; @@ -20,6 +21,7 @@ import org.zstack.header.vm.additions.VmHostFileType; import org.zstack.header.vm.additions.VmHostFileVO; import org.zstack.header.vm.additions.VmHostFileVO_; +import org.zstack.kvm.KVMGlobalConfig; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; @@ -33,9 +35,6 @@ public class VmHostFileTracker implements Component { private static final CLogger logger = Utils.getLogger(VmHostFileTracker.class); - private static final long SYNC_INTERVAL_SECONDS = TimeUnit.MINUTES.toSeconds(30); - private static final int SYNC_CONCURRENCY = 5; - @Autowired private ThreadFacade threadFacade; @Autowired @@ -52,6 +51,34 @@ public boolean start() { return true; } + submitTrackerTask(); + + GlobalConfigUpdateExtensionPoint listener = (oldConfig, newConfig) -> { + logger.debug(String.format("%s changed from %s to %s, restarting vm-host-file-tracker", + newConfig.getCanonicalName(), oldConfig.value(), newConfig.value())); + submitTrackerTask(); + }; + + KVMGlobalConfig.VM_HOST_FILE_SYNC_INTERVAL.installUpdateExtension(listener); + KVMGlobalConfig.VM_HOST_FILE_SYNC_CONCURRENCY.installUpdateExtension(listener); + + return true; + } + + @Override + public boolean stop() { + if (trackerThread != null) { + trackerThread.cancel(true); + } + return true; + } + + private synchronized void submitTrackerTask() { + if (trackerThread != null) { + trackerThread.cancel(true); + } + + long interval = KVMGlobalConfig.VM_HOST_FILE_SYNC_INTERVAL.value(Long.class); trackerThread = threadFacade.submitPeriodicTask(new PeriodicTask() { @Override public TimeUnit getTimeUnit() { @@ -60,7 +87,7 @@ public TimeUnit getTimeUnit() { @Override public long getInterval() { - return SYNC_INTERVAL_SECONDS; + return interval; } @Override @@ -74,16 +101,6 @@ public void run() { syncVmHostFiles(); } }); - - return true; - } - - @Override - public boolean stop() { - if (trackerThread != null) { - trackerThread.cancel(true); - } - return true; } private void syncVmHostFiles() { @@ -104,6 +121,8 @@ private void syncVmHostFiles() { .collect(Collectors.groupingBy(f -> f.getVmInstanceUuid() + "::" + f.getHostUuid())); List> groups = new ArrayList<>(grouped.values()); + int concurrency = KVMGlobalConfig.VM_HOST_FILE_SYNC_CONCURRENCY.value(Integer.class); + new While<>(groups).step((group, whileCompletion) -> { VmHostFileVO first = group.get(0); String vmUuid = first.getVmInstanceUuid(); @@ -137,7 +156,7 @@ public void run(MessageReply reply) { whileCompletion.done(); } }); - }, SYNC_CONCURRENCY).run(new WhileDoneCompletion(null) { + }, concurrency).run(new WhileDoneCompletion(null) { @Override public void done(ErrorCodeList errorCodeList) { // periodic sync round finished, empty callback From 804ab6b6f2072600d69f1baf1d6aabd4732443a3 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 26 Mar 2026 11:32:35 +0800 Subject: [PATCH 078/129] [kvm]: add lastSyncReason to track VM host file sync Add lastSyncReason column to VmHostFileVO table to track the reason for each VM host file sync operation. This helps with debugging and auditing. Update SyncVmHostFilesFromHostMsg to include syncReason parameter and set it at various sync triggers: - VM start: on prepare host file - VM stop: on release vm resource - VM migration: on post-migration - VM clone: on clone vm - Libvirt shutdown: on libvirt shutdown event Update DB schema and all sync operation calls to properly record the sync reason for better traceability. Related: ZSV-11310 Change-Id: I7978696275717271667076676b79706a646c6a66 --- conf/db/zsv/V5.0.0__schema.sql | 1 + .../vm/additions/RestoreVmHostFileMsg.java | 9 ++++++ .../vm/additions/VmHostFileInventory.java | 11 +++++++ .../vm/additions/VmHostFileSyncReason.java | 31 +++++++++++++++++++ .../header/vm/additions/VmHostFileVO.java | 11 +++++++ .../header/vm/additions/VmHostFileVO_.java | 1 + .../kvm/efi/KvmSecureBootExtensions.java | 10 ++++++ .../zstack/kvm/efi/KvmSecureBootManager.java | 9 ++++++ .../kvm/efi/SyncVmHostFilesFromHostMsg.java | 9 ++++++ .../org/zstack/kvm/tpm/KvmTpmExtensions.java | 1 + .../sdk/vm/entity/VmHostFileInventory.java | 8 +++++ .../group/VolumeSnapshotGroupBase.java | 1 + 12 files changed, 102 insertions(+) create mode 100644 header/src/main/java/org/zstack/header/vm/additions/VmHostFileSyncReason.java diff --git a/conf/db/zsv/V5.0.0__schema.sql b/conf/db/zsv/V5.0.0__schema.sql index 3bdfbdcfaaf..f0934eda506 100644 --- a/conf/db/zsv/V5.0.0__schema.sql +++ b/conf/db/zsv/V5.0.0__schema.sql @@ -15,6 +15,7 @@ CREATE TABLE IF NOT EXISTS `zstack`.`VmHostFileVO` ( `hostUuid` char(32) NOT NULL, `type` varchar(64) NOT NULL COMMENT 'NvRam, TpmState', `path` varchar(1024) NOT NULL COMMENT 'Absolute path of the file on the host', + `lastSyncReason` varchar(255) DEFAULT NULL COMMENT 'The reason for the last sync operation', `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`), diff --git a/header/src/main/java/org/zstack/header/vm/additions/RestoreVmHostFileMsg.java b/header/src/main/java/org/zstack/header/vm/additions/RestoreVmHostFileMsg.java index 1295c7474f4..392e6d12e71 100644 --- a/header/src/main/java/org/zstack/header/vm/additions/RestoreVmHostFileMsg.java +++ b/header/src/main/java/org/zstack/header/vm/additions/RestoreVmHostFileMsg.java @@ -6,6 +6,7 @@ public class RestoreVmHostFileMsg extends NeedReplyMessage implements VmInstanceMessage { private String vmInstanceUuid; private String snapshotGroupUuid; + private String syncReason; @Override public String getVmInstanceUuid() { @@ -23,4 +24,12 @@ public String getSnapshotGroupUuid() { public void setSnapshotGroupUuid(String snapshotGroupUuid) { this.snapshotGroupUuid = snapshotGroupUuid; } + + public String getSyncReason() { + return syncReason; + } + + public void setSyncReason(String syncReason) { + this.syncReason = syncReason; + } } \ No newline at end of file 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 index fc5ecb9bb9f..6db0005be9c 100644 --- a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileInventory.java +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileInventory.java @@ -16,6 +16,7 @@ public class VmHostFileInventory { private String hostUuid; private String type; private String path; + private String lastSyncReason; private Timestamp createDate; private Timestamp lastOpDate; @@ -29,6 +30,7 @@ public static VmHostFileInventory valueOf(VmHostFileVO vo) { inv.setHostUuid(vo.getHostUuid()); inv.setType(vo.getType().toString()); inv.setPath(vo.getPath()); + inv.setLastSyncReason(vo.getLastSyncReason()); inv.setCreateDate(vo.getCreateDate()); inv.setLastOpDate(vo.getLastOpDate()); return inv; @@ -78,6 +80,14 @@ public void setPath(String path) { this.path = path; } + public String getLastSyncReason() { + return lastSyncReason; + } + + public void setLastSyncReason(String lastSyncReason) { + this.lastSyncReason = lastSyncReason; + } + public Timestamp getCreateDate() { return createDate; } @@ -101,6 +111,7 @@ public static VmHostFileInventory __example__() { ref.setHostUuid(DocUtils.createFixedUuid(HostVO.class)); ref.setType(VmHostFileType.TpmState.toString()); ref.setPath("/var/lib/libvirt/swtpm/" + ref.getHostUuid() + "/"); + ref.setLastSyncReason("on libvirt shutdown event"); ref.setCreateDate(DocUtils.timestamp()); ref.setLastOpDate(DocUtils.timestamp()); return ref; diff --git a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileSyncReason.java b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileSyncReason.java new file mode 100644 index 00000000000..82b8dd15957 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileSyncReason.java @@ -0,0 +1,31 @@ +package org.zstack.header.vm.additions; + +public enum VmHostFileSyncReason { + PrepareRead("on prepare host file (from origin host)"), + PrepareReRead("on prepare host file (from dest host)"), + ResourceRelease("on release vm resource"), + PostMigration("on post-migration (from dest host)"), + VmShutdown("on libvirt shutdown event"), + PostClone("on post-clone (from dest host)"), + Restore("restore"), + ; + + public final String detail; + + private VmHostFileSyncReason(String detail) { + this.detail = detail; + } + + @Override + public String toString() { + return detail; + } + + public String reason() { + return detail; + } + + public String reason(String description) { + return description == null || description.length() == 0 ? reason() : detail + ": " + description; + } +} 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 index c0691ad5dac..5a6b36c63dd 100644 --- a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileVO.java +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileVO.java @@ -40,6 +40,8 @@ public class VmHostFileVO extends ResourceVO { @Column private String path; @Column + private String lastSyncReason; + @Column private Timestamp createDate; @Column private Timestamp lastOpDate; @@ -76,6 +78,14 @@ public void setPath(String path) { this.path = path; } + public String getLastSyncReason() { + return lastSyncReason; + } + + public void setLastSyncReason(String lastSyncReason) { + this.lastSyncReason = lastSyncReason; + } + public Timestamp getCreateDate() { return createDate; } @@ -100,6 +110,7 @@ public String toString() { ", hostUuid='" + hostUuid + '\'' + ", type=" + type + ", path='" + path + '\'' + + ", lastSyncReason='" + lastSyncReason + '\'' + ", 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 index 39fdb742797..38ee885375f 100644 --- a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileVO_.java +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileVO_.java @@ -12,6 +12,7 @@ public class VmHostFileVO_ extends ResourceVO_ { public static volatile SingularAttribute hostUuid; public static volatile SingularAttribute type; public static volatile SingularAttribute path; + public static volatile SingularAttribute lastSyncReason; public static volatile SingularAttribute createDate; public static volatile SingularAttribute lastOpDate; } 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 c3738b900c4..87df5e65172 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 @@ -84,6 +84,10 @@ 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.header.vm.additions.VmHostFileSyncReason.PostMigration; +import static org.zstack.header.vm.additions.VmHostFileSyncReason.PrepareReRead; +import static org.zstack.header.vm.additions.VmHostFileSyncReason.PrepareRead; +import static org.zstack.header.vm.additions.VmHostFileSyncReason.ResourceRelease; import static org.zstack.kvm.KVMConstant.*; import static org.zstack.utils.CollectionDSL.list; @@ -358,6 +362,7 @@ public static class PrepareHostFileContext { public String hostUuid; public String vmUuid; public VmHostFileType type; + public String syncReason; public String path; // whether the NvRam is on the same host as before @@ -396,6 +401,7 @@ public void run(FlowTrigger trigger, Map data) { SyncVmHostFilesFromHostMsg syncMsg = new SyncVmHostFilesFromHostMsg(); syncMsg.setHostUuid(vmHostFile.getHostUuid()); syncMsg.setVmUuid(context.vmUuid); + syncMsg.setSyncReason(PrepareRead.reason(context.syncReason)); if (vmHostFile.getType() == VmHostFileType.NvRam) { context.path = vmHostFile.getPath(); @@ -506,6 +512,7 @@ public void run(FlowTrigger trigger, Map data) { SyncVmHostFilesFromHostMsg syncMsg = new SyncVmHostFilesFromHostMsg(); syncMsg.setHostUuid(context.hostUuid); syncMsg.setVmUuid(context.vmUuid); + syncMsg.setSyncReason(PrepareReRead.reason(context.syncReason)); if (context.type == VmHostFileType.NvRam) { syncMsg.setNvRamPath(context.path); @@ -549,6 +556,7 @@ private void prepareNvRamHostFileOnHost(VmInstanceSpec spec, Completion completi context.hostUuid = spec.getDestHost().getUuid(); context.vmUuid = spec.getVmInventory().getUuid(); context.type = VmHostFileType.NvRam; + context.syncReason = "pre-instantiate VM resource"; prepareHostFileOnHost(context, completion); } @@ -758,6 +766,7 @@ public void releaseVmResource(VmInstanceSpec spec, Completion completion) { SyncVmHostFilesFromHostMsg syncMsg = new SyncVmHostFilesFromHostMsg(); syncMsg.setHostUuid(hostUuid); syncMsg.setVmUuid(vmUuid); + syncMsg.setSyncReason(ResourceRelease.reason()); for (VmHostFileVO file : vmHostFiles) { if (file.getType() == VmHostFileType.NvRam) { @@ -822,6 +831,7 @@ public void afterMigrateVm(VmInstanceInventory inv, String srcHostUuid, NoErrorC SyncVmHostFilesFromHostMsg syncMsg = new SyncVmHostFilesFromHostMsg(); syncMsg.setHostUuid(destHostUuid); syncMsg.setVmUuid(vmUuid); + syncMsg.setSyncReason(PostMigration.reason()); for (VmHostFileVO file : vmHostFiles) { if (file.getType() == VmHostFileType.NvRam) { 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 777109f58d9..d0604d552e9 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 @@ -70,6 +70,9 @@ 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.core.Platform.operr; +import static org.zstack.header.vm.additions.VmHostFileSyncReason.PostClone; +import static org.zstack.header.vm.additions.VmHostFileSyncReason.Restore; +import static org.zstack.header.vm.additions.VmHostFileSyncReason.VmShutdown; import static org.zstack.kvm.KVMAgentCommands.*; import static org.zstack.kvm.KVMConstant.*; import static org.zstack.utils.CollectionDSL.list; @@ -143,6 +146,7 @@ protected void run(Map tokens, Object data) { innerMessage.setVmUuid(vmUuid); innerMessage.setNvRamPath(nvRamFile == null ? null : nvRamFile.getPath()); innerMessage.setTpmStateFolder(tpmStateFile == null ? null : tpmStateFile.getPath()); + innerMessage.setSyncReason(VmShutdown.reason()); bus.makeLocalServiceId(innerMessage, VmInstanceConstant.SECURE_BOOT_SERVICE_ID); bus.send(innerMessage, new CloudBusCallBack(null) { @Override @@ -260,6 +264,7 @@ public void success(KvmResponseWrapper wrapper) { SQL.New(VmHostFileVO.class) .eq(VmHostFileVO_.uuid, file.getUuid()) .set(VmHostFileVO_.lastOpDate, now) + .set(VmHostFileVO_.lastSyncReason, msg.getSyncReason()) .update(); } else { file = new VmHostFileVO(); @@ -268,6 +273,7 @@ public void success(KvmResponseWrapper wrapper) { file.setVmInstanceUuid(msg.getVmUuid()); file.setPath(path); file.setType(type); + file.setLastSyncReason(msg.getSyncReason()); file.setCreateDate(now); file.setLastOpDate(now); file.setResourceName(String.format("%s file for %s", type, msg.getVmUuid())); @@ -376,6 +382,7 @@ public void run(FlowTrigger trigger, Map data) { SyncVmHostFilesFromHostMsg syncContext = new SyncVmHostFilesFromHostMsg(); syncContext.setHostUuid(hostUuid); syncContext.setVmUuid(msg.getSrcVmUuid()); + syncContext.setSyncReason(PostClone.reason()); return syncContext; }); } @@ -728,6 +735,7 @@ public void fail(ErrorCode errorCode) { VmHostFileVO currentFile = currentFilesByType.get(type); SQL.New(VmHostFileVO.class) .eq(VmHostFileVO_.uuid, currentFile.getUuid()) + .set(VmHostFileVO_.lastSyncReason, Restore.reason(msg.getSyncReason())) .set(VmHostFileVO_.lastOpDate, now) .update(); @@ -757,6 +765,7 @@ public void fail(ErrorCode errorCode) { newFile.setHostUuid(finalHostUuid); newFile.setType(type); newFile.setPath(buildPathForVmHostFileType(type, msg.getVmInstanceUuid())); + newFile.setLastSyncReason(Restore.reason(msg.getSyncReason())); newFile.setCreateDate(now); newFile.setLastOpDate(now); databaseFacade.persist(newFile); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostMsg.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostMsg.java index 97ae8e54597..75e33f5791e 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostMsg.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostMsg.java @@ -7,6 +7,7 @@ public class SyncVmHostFilesFromHostMsg extends NeedReplyMessage { private String vmUuid; private String nvRamPath; private String tpmStateFolder; + private String syncReason; public String getHostUuid() { return hostUuid; @@ -39,4 +40,12 @@ public String getTpmStateFolder() { public void setTpmStateFolder(String tpmStateFolder) { this.tpmStateFolder = tpmStateFolder; } + + public String getSyncReason() { + return syncReason; + } + + public void setSyncReason(String syncReason) { + this.syncReason = syncReason; + } } 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 84919356dbd..a4976af0fba 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 @@ -151,6 +151,7 @@ public void run(FlowTrigger trigger, Map data) { innerContext.hostUuid = spec.getDestHost().getUuid(); innerContext.vmUuid = spec.getVmInventory().getUuid(); innerContext.type = VmHostFileType.TpmState; + innerContext.syncReason = "pre-instantiate VM resource"; secureBootExtensions.prepareHostFileOnHost(innerContext, new Completion(trigger) { @Override public void success() { diff --git a/sdk/src/main/java/org/zstack/sdk/vm/entity/VmHostFileInventory.java b/sdk/src/main/java/org/zstack/sdk/vm/entity/VmHostFileInventory.java index 7c72bb8edb3..35f14678602 100644 --- a/sdk/src/main/java/org/zstack/sdk/vm/entity/VmHostFileInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/vm/entity/VmHostFileInventory.java @@ -44,6 +44,14 @@ public java.lang.String getPath() { return this.path; } + public java.lang.String lastSyncReason; + public void setLastSyncReason(java.lang.String lastSyncReason) { + this.lastSyncReason = lastSyncReason; + } + public java.lang.String getLastSyncReason() { + return this.lastSyncReason; + } + public java.sql.Timestamp createDate; public void setCreateDate(java.sql.Timestamp createDate) { this.createDate = createDate; diff --git a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java index a0730abb0e0..2385428aed0 100644 --- a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java @@ -405,6 +405,7 @@ class Context { RestoreVmHostFileMsg restoreMsg = new RestoreVmHostFileMsg(); restoreMsg.setVmInstanceUuid(vmUuid); restoreMsg.setSnapshotGroupUuid(self.getUuid()); + restoreMsg.setSyncReason("revert snapshot"); bus.makeLocalServiceId(restoreMsg, VmInstanceConstant.SECURE_BOOT_SERVICE_ID); bus.send(restoreMsg, new CloudBusCallBack(trigger) { @Override From 99ae29b713f35e83c10d16fd7e9b36e1b6f72f81 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 26 Mar 2026 15:55:25 +0800 Subject: [PATCH 079/129] [kvm]: add key provider binding to TPM operations Refactor addTpmToVm and removeTpmFromVm methods to use new Flow API and add key provider binding support. Changes: - Migrate from SimpleFlowChain to new Flow API with lambda - Add keyProvider attach in addTpmToVm flow - Add keyProvider detach in removeTpmFromVm flow - Add proper rollback handling for failures - Track TPM creation state in context for cleanup This ensures key provider is properly associated with TPM during creation and detached during removal, with proper error handling and resource cleanup. Resolves: ZSV-11489 Related: ZSV-11310 Related: ZSV-11617 Change-Id: I7064796e6a76656469716a6f686570686b6a7773 --- .../org/zstack/kvm/tpm/KvmTpmManager.java | 190 ++++++++++-------- 1 file changed, 110 insertions(+), 80 deletions(-) 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 cd7fc8e9c77..59bfd84f5d1 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 @@ -60,8 +60,10 @@ import org.zstack.utils.logging.CLogger; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import static org.zstack.compute.vm.VmGlobalConfig.RESET_TPM_AFTER_VM_CLONE; import static org.zstack.core.Platform.err; @@ -189,6 +191,10 @@ static class AddTpmToVmContext { String vmInstanceUuid; String tpmUuid; + boolean tpmCreated; + boolean keyProviderAttached; + String createdTpmUuid; + static AddTpmToVmContext valueOf(AddTpmMsg msg) { AddTpmToVmContext context = new AddTpmToVmContext(); context.keyProviderUuid = msg.getKeyProviderUuid(); @@ -198,46 +204,55 @@ static AddTpmToVmContext valueOf(AddTpmMsg msg) { } } - @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) { - vmTpmManager.persistTpmVO(context.tpmUuid, context.vmInstanceUuid); - 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(); + SimpleFlowChain.of("add-tpm-to-vm-" + context.vmInstanceUuid) + .then(Flow.of("check-vm-status") + .handle(trigger -> { + 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(); + }) + .build()) + .then(Flow.of("create-tpm-db-records") + .handle(trigger -> { + try { + TpmVO tpm = vmTpmManager.persistTpmVO(context.tpmUuid, context.vmInstanceUuid); + context.createdTpmUuid = tpm.getUuid(); + context.tpmCreated = true; + if (context.keyProviderUuid != null) { + tpmKeyBackend.attachKeyProviderToTpm(context.createdTpmUuid, context.keyProviderUuid); + context.keyProviderAttached = true; + } + trigger.next(); + } catch (Exception e) { + trigger.fail(operr("failed to add TPM to vm[uuid:%s]: %s", context.vmInstanceUuid, e.getMessage())); + } + }) + .rollback(trigger -> { + try { + if (context.keyProviderAttached && context.createdTpmUuid != null) { + tpmKeyBackend.detachKeyProviderFromTpm(context.createdTpmUuid); + } + } finally { + if (context.tpmCreated && context.createdTpmUuid != null) { + vmTpmManager.deleteTpmVO(context.createdTpmUuid); + } + } + trigger.rollback(); + }) + .build()) + .propagateExceptionTo(completion) + .done(completion::success) + .error(completion::fail) + .start(); } private void handle(RemoveTpmMsg msg) { @@ -286,48 +301,63 @@ static RemoveTpmFromVmContext valueOf(RemoveTpmMsg msg) { } } - @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(); + SimpleFlowChain.of("remove-tpm-from-vm-" + context.vmInstanceUuid) + .then(Flow.of("check-vm-status") + .handle(trigger -> { + 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(); + }) + .build()) + .then(Flow.of("detach-resource-key") + .handle(trigger -> { + tpmKeyBackend.detachKeyProviderFromTpm(context.tpmUuid); + trigger.next(); + }) + .build()) + .then(Flow.of("remove-tpm-db-records") + .handle(trigger -> { + new SQLBatch() { + @Override + protected void scripts() { + Set types = new HashSet<>(); + types.add(VmHostFileType.TpmState); + + sql(TpmVO.class) + .eq(TpmVO_.uuid, context.tpmUuid) + .delete(); + + boolean needRegisterNvRam = vmTpmManager.needRegisterNvRam(context.vmInstanceUuid); + if (!needRegisterNvRam) { + types.add(VmHostFileType.NvRam); + } + + sql(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, context.vmInstanceUuid) + .in(VmHostFileVO_.type, types) + .delete(); + sql(VmHostBackupFileVO.class) + .eq(VmHostBackupFileVO_.resourceUuid, context.vmInstanceUuid) + .in(VmHostBackupFileVO_.type, types) + .delete(); + } + }.execute(); + trigger.next(); + }) + .build()) + .propagateExceptionTo(completion) + .done(completion::success) + .error(completion::fail) + .start(); } @SuppressWarnings("rawtypes") From 00ffc6ae6726522bb110143b48f22e228b1abc23 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 26 Mar 2026 17:17:59 +0800 Subject: [PATCH 080/129] [kvm]: move VM host file cleanup to new extension point Migrate VM host file cleanup from VmInstanceDestroyExtensionPoint to VmJustBeforeDeleteFromDbExtensionPoint. This ensures VmHostFileVO and VmHostBackupFileVO records are deleted before VM is removed from database. Changes: - Replace VmInstanceDestroyExtensionPoint with VmJustBeforeDeleteFromDbExtensionPoint - Remove unnecessary destroy lifecycle methods - Use SQLBatch for atomic deletion of both file records - Update Spring configuration in both production and test Resolves: ZSV-11436 Related: ZSV-11310 Change-Id: I6b766c6e757971796b696e6f7265766e76616871 --- conf/springConfigXml/Kvm.xml | 2 +- .../kvm/efi/KvmSecureBootExtensions.java | 38 ++++++++----------- .../test/resources/springConfigXml/Kvm.xml | 2 +- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index 8771289312e..05bb3903944 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -270,7 +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 87df5e65172..b0c4bf6b33c 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 @@ -11,6 +11,7 @@ import org.zstack.core.db.DatabaseFacade; import org.zstack.core.db.Q; import org.zstack.core.db.SQL; +import org.zstack.core.db.SQLBatch; import org.zstack.core.workflow.SimpleFlowChain; import org.zstack.header.core.Completion; import org.zstack.header.core.NoErrorCompletion; @@ -37,6 +38,7 @@ import org.zstack.header.vm.VmInstanceMigrateExtensionPoint; import org.zstack.header.vm.VmInstanceSpec; import org.zstack.header.vm.VmInstantiateResourceException; +import org.zstack.header.vm.VmJustBeforeDeleteFromDbExtensionPoint; import org.zstack.header.vm.VmMigrationType; import org.zstack.header.vm.VmPreMigrationExtensionPoint; import org.zstack.header.vm.VmReleaseResourceExtensionPoint; @@ -93,7 +95,7 @@ public class KvmSecureBootExtensions implements KVMStartVmExtensionPoint, PreVmInstantiateResourceExtensionPoint, - VmInstanceDestroyExtensionPoint, + VmJustBeforeDeleteFromDbExtensionPoint, VmPreMigrationExtensionPoint, AfterReimageVmInstanceExtensionPoint, VmReleaseResourceExtensionPoint, @@ -701,29 +703,19 @@ 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) { + public void vmJustBeforeDeleteFromDb(VmInstanceInventory inv) { String vmUuid = inv.getUuid(); - SQL.New(VmHostFileVO.class) - .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) - .delete(); - SQL.New(VmHostBackupFileVO.class) - .eq(VmHostBackupFileVO_.resourceUuid, vmUuid) - .delete(); - } - - @Override - public void failedToDestroyVm(VmInstanceInventory inv, ErrorCode reason) { - // do-nothing + new SQLBatch() { + @Override + protected void scripts() { + sql(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) + .delete(); + sql(VmHostBackupFileVO.class) + .eq(VmHostBackupFileVO_.resourceUuid, vmUuid) + .delete(); + } + }.execute(); } @Override diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index 836260935cb..07881cec10d 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -269,7 +269,7 @@ - + From 7ab925491d467b43e9572c26084d67a845614313 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 27 Mar 2026 01:52:30 +0800 Subject: [PATCH 081/129] [vm]: add TPM and NvRam restore support for snapshot group * Add NvRamSpec class for NvRam device configuration. * Add backupFileUuid field to TpmSpec for TPM state restore. * Add nvRam field to VmDevicesSpec for NvRam device. * Create SnapshotGroupRevertTpmHelper to handle TPM and NvRam restore when creating VM from volume snapshot group. The helper queries backup files from VmHostBackupFileVO and sets the appropriate backupFileUuid to device specifications based on resetTpm parameter and available backup file types. Resolves: ZSV-11441 Related: ZSV-11310 Change-Id: I737a687a6f6f706565657a677779666e62656566 --- .../org/zstack/header/tpm/entity/TpmSpec.java | 9 ++ .../zstack/header/vm/devices/NvRamSpec.java | 13 ++ .../header/vm/devices/VmDevicesSpec.java | 9 ++ .../kvm/tpm/SnapshotGroupRevertTpmHelper.java | 115 ++++++++++++++++++ 4 files changed, 146 insertions(+) create mode 100644 header/src/main/java/org/zstack/header/vm/devices/NvRamSpec.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/tpm/SnapshotGroupRevertTpmHelper.java 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 447f1d71a15..02195902e7c 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 @@ -9,6 +9,7 @@ public class TpmSpec { private String keyProviderUuid; @APINoSee private String secretUuid; + private String backupFileUuid; @APINoSee private boolean resourceKeyCreatedNew; @APINoSee @@ -38,6 +39,14 @@ public void setKeyProviderUuid(String keyProviderUuid) { this.keyProviderUuid = keyProviderUuid; } + public String getBackupFileUuid() { + return backupFileUuid; + } + + public void setBackupFileUuid(String backupFileUuid) { + this.backupFileUuid = backupFileUuid; + } + public String getSecretUuid() { return secretUuid; } diff --git a/header/src/main/java/org/zstack/header/vm/devices/NvRamSpec.java b/header/src/main/java/org/zstack/header/vm/devices/NvRamSpec.java new file mode 100644 index 00000000000..d9a5e5fdd03 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/devices/NvRamSpec.java @@ -0,0 +1,13 @@ +package org.zstack.header.vm.devices; + +public class NvRamSpec { + private String backupFileUuid; + + public String getBackupFileUuid() { + return backupFileUuid; + } + + public void setBackupFileUuid(String backupFileUuid) { + this.backupFileUuid = backupFileUuid; + } +} 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 index 6b24f045a8f..11fc43352fa 100644 --- a/header/src/main/java/org/zstack/header/vm/devices/VmDevicesSpec.java +++ b/header/src/main/java/org/zstack/header/vm/devices/VmDevicesSpec.java @@ -7,6 +7,7 @@ public class VmDevicesSpec { * Default value of tpm must be null, because tpm.enable is true by default. */ private TpmSpec tpm; + private NvRamSpec nvRam; public TpmSpec getTpm() { return tpm; @@ -16,6 +17,14 @@ public void setTpm(TpmSpec tpm) { this.tpm = tpm; } + public NvRamSpec getNvRam() { + return nvRam; + } + + public void setNvRam(NvRamSpec nvRam) { + this.nvRam = nvRam; + } + public static VmDevicesSpec __example__() { VmDevicesSpec spec = new VmDevicesSpec(); spec.setTpm(TpmSpec.__example__()); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/SnapshotGroupRevertTpmHelper.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/SnapshotGroupRevertTpmHelper.java new file mode 100644 index 00000000000..4e5a706e1a9 --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/SnapshotGroupRevertTpmHelper.java @@ -0,0 +1,115 @@ +package org.zstack.kvm.tpm; + +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Configurable; +import org.zstack.compute.vm.VmGlobalConfig; +import org.zstack.core.db.Q; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO_; +import org.zstack.header.tpm.entity.TpmSpec; +import org.zstack.header.vm.APICreateVmInstanceFromVolumeSnapshotGroupMsg; +import org.zstack.header.vm.CreateVmInstanceMsg; +import org.zstack.header.vm.additions.VmHostBackupFileVO; +import org.zstack.header.vm.additions.VmHostBackupFileVO_; +import org.zstack.header.vm.additions.VmHostFileType; +import org.zstack.header.vm.devices.NvRamSpec; +import org.zstack.header.vm.devices.VmDevicesSpec; +import org.zstack.resourceconfig.ResourceConfigFacade; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.util.List; + +@Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) +public class SnapshotGroupRevertTpmHelper { + private static final CLogger logger = Utils.getLogger(SnapshotGroupRevertTpmHelper.class); + + @Autowired + ResourceConfigFacade resourceConfigFacade; + + public void setupFromApi(APICreateVmInstanceFromVolumeSnapshotGroupMsg apiMsg, CreateVmInstanceMsg cmsg) { + String snapshotGroupUuid = apiMsg.getVolumeSnapshotGroupUuid(); + + boolean resetTpm; + if (apiMsg.getResetTpm() != null) { + resetTpm = apiMsg.getResetTpm(); + } else { + String vmInstanceUuid = Q.New(VolumeSnapshotGroupVO.class) + .select(VolumeSnapshotGroupVO_.vmInstanceUuid) + .eq(VolumeSnapshotGroupVO_.uuid, snapshotGroupUuid) + .findValue(); + resetTpm = resourceConfigFacade.getResourceConfigValue( + VmGlobalConfig.RESET_TPM_AFTER_VM_CLONE, vmInstanceUuid, Boolean.class); + logger.debug(String.format("resetTpm not specified in API, resolved from resource config " + + "RESET_TPM_AFTER_VM_CLONE for vm[uuid:%s]: %s", vmInstanceUuid, resetTpm)); + } + + List backupFiles = Q.New(VmHostBackupFileVO.class) + .eq(VmHostBackupFileVO_.resourceUuid, snapshotGroupUuid) + .list(); + + if (backupFiles.isEmpty()) { + logger.debug(String.format( + "no VmHostBackupFileVO found for volume snapshot group[uuid:%s], skip restoring TPM/NvRam", + snapshotGroupUuid)); + return; + } + + VmHostBackupFileVO tpmBackupFile = null; + VmHostBackupFileVO nvRamBackupFile = null; + for (VmHostBackupFileVO f : backupFiles) { + if (f.getType() == VmHostFileType.TpmState) { + tpmBackupFile = f; + } else if (f.getType() == VmHostFileType.NvRam) { + nvRamBackupFile = f; + } + } + + if (tpmBackupFile == null && nvRamBackupFile == null) { + logger.debug(String.format("no TpmState or NvRam backup file found for volume snapshot group[uuid:%s]", + snapshotGroupUuid)); + return; + } + + VmDevicesSpec devicesSpec = cmsg.getDevicesSpec(); + if (devicesSpec == null) { + devicesSpec = new VmDevicesSpec(); + cmsg.setDevicesSpec(devicesSpec); + } + + if (tpmBackupFile != null) { + TpmSpec tpmSpec = devicesSpec.getTpm(); + if (tpmSpec == null) { + tpmSpec = new TpmSpec(); + devicesSpec.setTpm(tpmSpec); + } + tpmSpec.setEnable(true); + + if (resetTpm) { + // resetTpm=true: reset secretUuid, generate a new one during VM creation + logger.debug(String.format("resetTpm is true for volume snapshot group[uuid:%s], " + + "will reset secretUuid, tpmBackupFileUuid:%s", snapshotGroupUuid, tpmBackupFile.getUuid())); + } else { + tpmSpec.setBackupFileUuid(tpmBackupFile.getUuid()); + // resetTpm=false: should reuse secretUuid + keyProviderUuid recorded in VolumeSnapshotGroup, + // but the recording step is not yet implemented, leave them empty for now + // TODO: retrieve secretUuid and keyProviderUuid from VolumeSnapshotGroup and set them here + logger.warn(String.format("resetTpm is false for volume snapshot group[uuid:%s], " + + "should restore secretUuid and keyProviderUuid but they are not yet recorded in snapshot group, " + + "leaving empty. tpmBackupFileUuid:%s", snapshotGroupUuid, tpmBackupFile.getUuid())); + } + } + + if (nvRamBackupFile != null) { + NvRamSpec nvRamSpec = devicesSpec.getNvRam(); + if (nvRamSpec == null) { + nvRamSpec = new NvRamSpec(); + devicesSpec.setNvRam(nvRamSpec); + } + nvRamSpec.setBackupFileUuid(nvRamBackupFile.getUuid()); + logger.debug(String.format("set NvRam restore info for volume snapshot group[uuid:%s], nvRamBackupFileUuid:%s", + snapshotGroupUuid, nvRamBackupFile.getUuid())); + } + } +} From 74e91e0b49a414b2203a30e52ea7acb8811adf57 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 27 Mar 2026 11:50:03 +0800 Subject: [PATCH 082/129] [kvm]: introduce VM host file base classes for backup tracking * Create abstract base classes for VM host file and backup file to provide extensible structure for different file types. * Add factory class KvmVmHostFileFactory to create appropriate base instances based on file type. * Implement base classes for NvRam and TpmState files. * Integrate factory into KvmSecureBootManager to call afterBackup hooks for tracking TPM key provider information. * Add TPM_KEY_PROVIDER_NAME system tag to record key provider association with backup files for snapshot restore scenarios. Resolves: ZSV-11441 Related: ZSV-11310 Change-Id: I7a7061647a6f6d636871707261656a6a6875626c --- conf/springConfigXml/Kvm.xml | 3 + .../java/org/zstack/kvm/KVMSystemTags.java | 5 ++ .../kvm/efi/AbstractVmHostBackupFileBase.java | 26 ++++++ .../kvm/efi/AbstractVmHostFileBase.java | 19 +++++ .../zstack/kvm/efi/KvmSecureBootManager.java | 20 +++++ .../zstack/kvm/efi/KvmVmHostFileFactory.java | 26 ++++++ .../kvm/efi/NvRamVmHostBackupFileBase.java | 15 ++++ .../zstack/kvm/efi/NvRamVmHostFileBase.java | 15 ++++ .../kvm/tpm/TpmStateVmHostBackupFileBase.java | 82 +++++++++++++++++++ .../kvm/tpm/TpmStateVmHostFileBase.java | 16 ++++ .../test/resources/springConfigXml/Kvm.xml | 3 + 11 files changed, 230 insertions(+) create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/efi/AbstractVmHostBackupFileBase.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/efi/AbstractVmHostFileBase.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmVmHostFileFactory.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/efi/NvRamVmHostBackupFileBase.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/efi/NvRamVmHostFileBase.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmStateVmHostBackupFileBase.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmStateVmHostFileBase.java diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index 05bb3903944..0080c90c528 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -283,4 +283,7 @@ + + + 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 8f0442f9246..709f069479f 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMSystemTags.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMSystemTags.java @@ -6,6 +6,7 @@ import org.zstack.header.tag.TagDefinition; import org.zstack.header.vm.VmInstanceVO; import org.zstack.header.vm.VmNicVO; +import org.zstack.header.vm.additions.VmHostBackupFileVO; import org.zstack.header.volume.VolumeVO; import org.zstack.tag.PatternedSystemTag; import org.zstack.tag.SystemTag; @@ -75,4 +76,8 @@ public class KVMSystemTags { public static final String SWTPM_VERSION_TOKEN = "version"; public static PatternedSystemTag SWTPM_VERSION = new PatternedSystemTag(String.format("swtpm::{%s}", SWTPM_VERSION_TOKEN), HostVO.class); + + public static final String TPM_KEY_PROVIDER_NAME_TOKEN = "keyProviderName"; + public static PatternedSystemTag TPM_KEY_PROVIDER_NAME = + new PatternedSystemTag(String.format("tpm::keyProvider::{%s}", TPM_KEY_PROVIDER_NAME_TOKEN), VmHostBackupFileVO.class); } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/AbstractVmHostBackupFileBase.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/AbstractVmHostBackupFileBase.java new file mode 100644 index 00000000000..c71bdc7db29 --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/AbstractVmHostBackupFileBase.java @@ -0,0 +1,26 @@ +package org.zstack.kvm.efi; + +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.factory.annotation.Configurable; +import org.zstack.header.vm.additions.VmHostBackupFileVO; +import org.zstack.header.vm.additions.VmHostFileType; +import org.zstack.header.vm.additions.VmHostFileVO; + +import java.util.Objects; + +@Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) +public abstract class AbstractVmHostBackupFileBase { + protected VmHostBackupFileVO self; + + protected AbstractVmHostBackupFileBase(VmHostBackupFileVO self) { + this.self = Objects.requireNonNull(self); + } + + public abstract VmHostFileType type(); + + public void afterBackup(VmHostBackupFileVO from) { + } + + public void afterBackup(VmHostFileVO from) { + } +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/AbstractVmHostFileBase.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/AbstractVmHostFileBase.java new file mode 100644 index 00000000000..7022d7a1d45 --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/AbstractVmHostFileBase.java @@ -0,0 +1,19 @@ +package org.zstack.kvm.efi; + +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.factory.annotation.Configurable; +import org.zstack.header.vm.additions.VmHostFileType; +import org.zstack.header.vm.additions.VmHostFileVO; + +import java.util.Objects; + +@Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) +public abstract class AbstractVmHostFileBase { + protected VmHostFileVO self; + + protected AbstractVmHostFileBase(VmHostFileVO self) { + this.self = Objects.requireNonNull(self); + } + + public abstract VmHostFileType type(); +} 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 d0604d552e9..6bc9c302c0c 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 @@ -91,6 +91,8 @@ public class KvmSecureBootManager extends AbstractService { private EventFacadeImpl eventFacade; @Autowired private ResourceConfigFacade resourceConfigFacade; + @Autowired + private KvmVmHostFileFactory vmHostFileFactory; @Override public boolean start() { @@ -512,6 +514,8 @@ private List backupVmHostFile(List fileList, L List filesNeedPersists = new ArrayList<>(); List contentsNeedPersists = new ArrayList<>(); + // value is VmHostBackupFileVO or VmHostFileVO + Map backupFromMap = new HashMap<>(); Timestamp now = Timestamp.from(Instant.now()); for (String resourceUuid : toResourceList) { @@ -536,6 +540,7 @@ private List backupVmHostFile(List fileList, L file.setCreateDate(now); file.setLastOpDate(now); filesNeedPersists.add(file); + backupFromMap.put(file, vmHostFile == null ? vmHostBackupFile : vmHostFile); VmHostFileContentVO content = new VmHostFileContentVO(); content.setUuid(file.getUuid()); @@ -571,6 +576,21 @@ protected void scripts() { } } }.execute(); + + for (VmHostBackupFileVO backup : filesNeedPersists) { + final Object backupFrom = backupFromMap.get(backup); + try { + if (backupFrom instanceof VmHostBackupFileVO) { + vmHostFileFactory.createBackupBase(backup).afterBackup((VmHostBackupFileVO) backupFrom); + } else if (backupFrom instanceof VmHostFileVO) { + vmHostFileFactory.createBackupBase(backup).afterBackup((VmHostFileVO) backupFrom); + } + } catch (Exception e) { + logger.warn(String.format("failed to execute afterBackup hook for VmHostBackupFileVO[uuid:%s, type:%s]: %s", + backup.getUuid(), backup.getType(), e.getMessage()), e); + } + } + return filesNeedPersists; } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmVmHostFileFactory.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmVmHostFileFactory.java new file mode 100644 index 00000000000..26cfe6e745f --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmVmHostFileFactory.java @@ -0,0 +1,26 @@ +package org.zstack.kvm.efi; + +import org.zstack.header.vm.additions.VmHostBackupFileVO; +import org.zstack.header.vm.additions.VmHostFileVO; +import org.zstack.kvm.tpm.TpmStateVmHostBackupFileBase; +import org.zstack.kvm.tpm.TpmStateVmHostFileBase; + +import static org.zstack.core.Platform.operr; + +public class KvmVmHostFileFactory { + public AbstractVmHostFileBase createBase(VmHostFileVO file) { + switch (file.getType()) { + case NvRam: return new NvRamVmHostFileBase(file); + case TpmState: return new TpmStateVmHostFileBase(file); + default: throw operr("invalid VM host file type: " + file.getType()).toException(); + } + } + + public AbstractVmHostBackupFileBase createBackupBase(VmHostBackupFileVO backupFile) { + switch (backupFile.getType()) { + case NvRam: return new NvRamVmHostBackupFileBase(backupFile); + case TpmState: return new TpmStateVmHostBackupFileBase(backupFile); + default: throw operr("invalid VM host file type: " + backupFile.getType()).toException(); + } + } +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/NvRamVmHostBackupFileBase.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/NvRamVmHostBackupFileBase.java new file mode 100644 index 00000000000..b2df7c91f9e --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/NvRamVmHostBackupFileBase.java @@ -0,0 +1,15 @@ +package org.zstack.kvm.efi; + +import org.zstack.header.vm.additions.VmHostBackupFileVO; +import org.zstack.header.vm.additions.VmHostFileType; + +public class NvRamVmHostBackupFileBase extends AbstractVmHostBackupFileBase { + public NvRamVmHostBackupFileBase(VmHostBackupFileVO self) { + super(self); + } + + @Override + public VmHostFileType type() { + return VmHostFileType.NvRam; + } +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/NvRamVmHostFileBase.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/NvRamVmHostFileBase.java new file mode 100644 index 00000000000..98f9ff37ee7 --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/NvRamVmHostFileBase.java @@ -0,0 +1,15 @@ +package org.zstack.kvm.efi; + +import org.zstack.header.vm.additions.VmHostFileType; +import org.zstack.header.vm.additions.VmHostFileVO; + +public class NvRamVmHostFileBase extends AbstractVmHostFileBase { + public NvRamVmHostFileBase(VmHostFileVO self) { + super(self); + } + + @Override + public VmHostFileType type() { + return VmHostFileType.NvRam; + } +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmStateVmHostBackupFileBase.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmStateVmHostBackupFileBase.java new file mode 100644 index 00000000000..ee3f8fa7669 --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmStateVmHostBackupFileBase.java @@ -0,0 +1,82 @@ +package org.zstack.kvm.tpm; + +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.vm.devices.TpmEncryptedResourceKeyBackend; +import org.zstack.core.db.Q; +import org.zstack.header.tpm.entity.TpmVO; +import org.zstack.header.tpm.entity.TpmVO_; +import org.zstack.header.vm.additions.VmHostBackupFileVO; +import org.zstack.header.vm.additions.VmHostFileType; +import org.zstack.header.vm.additions.VmHostFileVO; +import org.zstack.kvm.KVMSystemTags; +import org.zstack.kvm.efi.AbstractVmHostBackupFileBase; +import org.zstack.tag.SystemTagCreator; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import static org.zstack.utils.CollectionDSL.e; +import static org.zstack.utils.CollectionDSL.map; + +public class TpmStateVmHostBackupFileBase extends AbstractVmHostBackupFileBase { + private static final CLogger logger = Utils.getLogger(TpmStateVmHostBackupFileBase.class); + + @Autowired + private TpmEncryptedResourceKeyBackend resourceKeyBackend; + + public TpmStateVmHostBackupFileBase(VmHostBackupFileVO self) { + super(self); + } + + @Override + public VmHostFileType type() { + return VmHostFileType.TpmState; + } + + @Override + public void afterBackup(VmHostBackupFileVO from) { + String keyProviderName = KVMSystemTags.TPM_KEY_PROVIDER_NAME + .getTokenByResourceUuid(from.getUuid(), KVMSystemTags.TPM_KEY_PROVIDER_NAME_TOKEN); + if (keyProviderName == null) { + logger.debug(String.format("no tpm key provider name system tag found on source VmHostBackupFileVO[uuid:%s], skip copying", + from.getUuid())); + return; + } + + createKeyProviderNameTag(keyProviderName); + logger.debug(String.format("copied tpm key provider name[%s] from VmHostBackupFileVO[uuid:%s] to VmHostBackupFileVO[uuid:%s]", + keyProviderName, from.getUuid(), self.getUuid())); + } + + @Override + public void afterBackup(VmHostFileVO from) { + String tpmUuid = Q.New(TpmVO.class) + .select(TpmVO_.uuid) + .eq(TpmVO_.vmInstanceUuid, from.getVmInstanceUuid()) + .findValue(); + if (tpmUuid == null) { + logger.debug(String.format("no TpmVO found for vm[uuid:%s], skip creating key provider name tag on VmHostBackupFileVO[uuid:%s]", + from.getVmInstanceUuid(), self.getUuid())); + return; + } + + String keyProviderName = resourceKeyBackend.findKeyProviderNameByTpm(tpmUuid); + if (keyProviderName == null) { + logger.debug(String.format("no key provider name found for tpm[uuid:%s] of vm[uuid:%s], skip creating tag on VmHostBackupFileVO[uuid:%s]", + tpmUuid, from.getVmInstanceUuid(), self.getUuid())); + return; + } + + createKeyProviderNameTag(keyProviderName); + logger.debug(String.format("created tpm key provider name[%s] tag on VmHostBackupFileVO[uuid:%s] from tpm[uuid:%s] of vm[uuid:%s]", + keyProviderName, self.getUuid(), tpmUuid, from.getVmInstanceUuid())); + } + + @SuppressWarnings("unchecked") + private void createKeyProviderNameTag(String keyProviderName) { + SystemTagCreator creator = KVMSystemTags.TPM_KEY_PROVIDER_NAME.newSystemTagCreator(self.getUuid()); + creator.setTagByTokens(map(e(KVMSystemTags.TPM_KEY_PROVIDER_NAME_TOKEN, keyProviderName))); + creator.inherent = true; + creator.recreate = true; + creator.create(); + } +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmStateVmHostFileBase.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmStateVmHostFileBase.java new file mode 100644 index 00000000000..a32cb332b2a --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmStateVmHostFileBase.java @@ -0,0 +1,16 @@ +package org.zstack.kvm.tpm; + +import org.zstack.header.vm.additions.VmHostFileType; +import org.zstack.header.vm.additions.VmHostFileVO; +import org.zstack.kvm.efi.AbstractVmHostFileBase; + +public class TpmStateVmHostFileBase extends AbstractVmHostFileBase { + public TpmStateVmHostFileBase(VmHostFileVO self) { + super(self); + } + + @Override + public VmHostFileType type() { + return VmHostFileType.TpmState; + } +} diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index 07881cec10d..5034922b145 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -282,4 +282,7 @@ + + + From 2e3193e3ca631bd4d50b268f741328b54367de80 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 27 Mar 2026 12:30:18 +0800 Subject: [PATCH 083/129] [header]: add APINoSee to backupFileUuid fields Add @APINoSee annotation to backupFileUuid field in TpmSpec and NvRamSpec classes to hide these internal fields from API responses. These fields are used internally for TPM and NvRam state restore and should not be exposed to API clients. Related: ZSV-11310 Change-Id: I71666a6677657376727a6f7a736171736c67676e --- header/src/main/java/org/zstack/header/tpm/entity/TpmSpec.java | 1 + .../src/main/java/org/zstack/header/vm/devices/NvRamSpec.java | 3 +++ 2 files changed, 4 insertions(+) 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 02195902e7c..1798a775a88 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 @@ -9,6 +9,7 @@ public class TpmSpec { private String keyProviderUuid; @APINoSee private String secretUuid; + @APINoSee private String backupFileUuid; @APINoSee private boolean resourceKeyCreatedNew; diff --git a/header/src/main/java/org/zstack/header/vm/devices/NvRamSpec.java b/header/src/main/java/org/zstack/header/vm/devices/NvRamSpec.java index d9a5e5fdd03..d7f0bf6bd17 100644 --- a/header/src/main/java/org/zstack/header/vm/devices/NvRamSpec.java +++ b/header/src/main/java/org/zstack/header/vm/devices/NvRamSpec.java @@ -1,6 +1,9 @@ package org.zstack.header.vm.devices; +import org.zstack.header.rest.APINoSee; + public class NvRamSpec { + @APINoSee private String backupFileUuid; public String getBackupFileUuid() { From 73b053fadd2b03f17fd47775bb1780406f984e30 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 27 Mar 2026 14:25:22 +0800 Subject: [PATCH 084/129] [kvm]: remove NvRam volume supports Related: ZSV-11310 Change-Id: I6864696e76717971726776736272696479727662 --- .../zstack/compute/vm/VmDeleteVolumeFlow.java | 3 +- .../compute/vm/VmExpungeNvRamVolumeFlow.java | 83 ------ .../org/zstack/compute/vm/VmInstanceBase.java | 2 +- conf/springConfigXml/VmInstanceManager.xml | 1 - .../org/zstack/header/volume/VolumeType.java | 1 - .../ceph/primary/CephPrimaryStorageBase.java | 10 +- .../kvm/efi/KvmSecureBootExtensions.java | 248 +----------------- .../zstack/kvm/efi/KvmSecureBootManager.java | 5 - .../primary/local/LocalStorageKvmBackend.java | 7 +- .../nfs/NfsPrimaryStorageKVMBackend.java | 3 - .../storage/primary/smp/KvmBackend.java | 3 - 11 files changed, 13 insertions(+), 353 deletions(-) delete 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 a9dac33e70f..5c29c8e8292 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmDeleteVolumeFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmDeleteVolumeFlow.java @@ -65,8 +65,7 @@ public void run(final FlowTrigger trigger, Map data) { List volumeTypes = Arrays.asList( VolumeType.Root.toString(), VolumeType.Memory.toString(), - VolumeType.Cache.toString(), - VolumeType.NvRam.toString() + VolumeType.Cache.toString() ); List ctx = transformAndRemoveNull(spec.getVmInventory().getAllVolumes(), arg -> { if (VolumeType.Data.toString().equals(arg.getType()) && !deleteDataDisk && !templated) { diff --git a/compute/src/main/java/org/zstack/compute/vm/VmExpungeNvRamVolumeFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmExpungeNvRamVolumeFlow.java deleted file mode 100644 index 9b608817507..00000000000 --- a/compute/src/main/java/org/zstack/compute/vm/VmExpungeNvRamVolumeFlow.java +++ /dev/null @@ -1,83 +0,0 @@ -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 ebb73ad1907..bd929d84a98 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java @@ -2595,7 +2595,7 @@ protected void scripts() { sql(VmNicVO.class).eq(VmNicVO_.vmInstanceUuid, self.getUuid()).hardDelete(); sql(VolumeVO.class).eq(VolumeVO_.vmInstanceUuid, self.getUuid()) - .in(VolumeVO_.type, list(VolumeType.Root, VolumeType.NvRam)) + .eq(VolumeVO_.type, VolumeType.Root) .hardDelete(); sql(VmCdRomVO.class).eq(VmCdRomVO_.vmInstanceUuid, self.getUuid()).hardDelete(); sql(VmInstanceVO.class).eq(VmInstanceVO_.uuid, self.getUuid()).hardDelete(); diff --git a/conf/springConfigXml/VmInstanceManager.xml b/conf/springConfigXml/VmInstanceManager.xml index 0b708ef5696..8e6181c81c0 100755 --- a/conf/springConfigXml/VmInstanceManager.xml +++ b/conf/springConfigXml/VmInstanceManager.xml @@ -116,7 +116,6 @@ org.zstack.compute.vm.VmExpungeRootVolumeFlow - org.zstack.compute.vm.VmExpungeNvRamVolumeFlow org.zstack.compute.vm.VmExpungeMemoryVolumeFlow org.zstack.compute.vm.VmExpungeCacheVolumeFlow 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 4266a51ccec..6c14a18bd3d 100755 --- a/header/src/main/java/org/zstack/header/volume/VolumeType.java +++ b/header/src/main/java/org/zstack/header/volume/VolumeType.java @@ -5,5 +5,4 @@ public enum VolumeType { Data, Memory, Cache, - NvRam, } 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 68e311dbcb1..a04a9aba6e4 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 @@ -1734,13 +1734,9 @@ private void createEmptyVolume(final InstantiateVolumeOnPrimaryStorageMsg msg) { .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 ; - } + 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/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java index b0c4bf6b33c..add607f8990 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,7 +1,6 @@ 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.VmSystemTags; import org.zstack.compute.vm.devices.VmTpmManager; @@ -16,16 +15,12 @@ import org.zstack.header.core.Completion; import org.zstack.header.core.NoErrorCompletion; 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; -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.exception.CloudRuntimeException; -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_; @@ -33,7 +28,6 @@ import org.zstack.header.vm.PreVmInstantiateResourceExtensionPoint; import org.zstack.header.vm.VmInstanceConstant; import org.zstack.header.vm.AfterReimageVmInstanceExtensionPoint; -import org.zstack.header.vm.VmInstanceDestroyExtensionPoint; import org.zstack.header.vm.VmInstanceInventory; import org.zstack.header.vm.VmInstanceMigrateExtensionPoint; import org.zstack.header.vm.VmInstanceSpec; @@ -50,17 +44,7 @@ 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; -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.KVMAgentCommands.*; import org.zstack.kvm.KVMGlobalConfig; @@ -74,7 +58,6 @@ 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.Base64; @@ -131,23 +114,6 @@ public void beforeStartVmOnKvm(KVMHostInventory host, VmInstanceSpec spec, KVMAg } private void prepareNvRamToStartVmCmd(KVMAgentCommands.StartVmCmd cmd, DiskAO nvRamSpec, KVMHostInventory host) { - 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; - } - VolumeTO volume = new VolumeTO(); volume.setDeviceType(VolumeTO.FILE); volume.setInstallPath(buildNvramFilePath(cmd.getVmInstanceUuid())); @@ -288,76 +254,18 @@ 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; - - 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) { + if (nvRamSpec == 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(); - } - }); + PrepareHostFileContext context = new PrepareHostFileContext(); + context.hostUuid = spec.getDestHost().getUuid(); + context.vmUuid = spec.getVmInventory().getUuid(); + context.type = VmHostFileType.NvRam; + context.syncReason = "pre-instantiate VM resource"; + prepareHostFileOnHost(context, completion); } public static class PrepareHostFileContext { @@ -547,21 +455,6 @@ public void handle(ErrorCode errCode, Map data) { }).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; - context.syncReason = "pre-instantiate VM resource"; - prepareHostFileOnHost(context, completion); - } - @Override public void preReleaseVmResource(VmInstanceSpec spec, Completion completion) { completion.success(); @@ -575,133 +468,6 @@ static class NvRamVolumeContext { 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())); - } - }); - } - @Override public void vmJustBeforeDeleteFromDb(VmInstanceInventory inv) { String vmUuid = inv.getUuid(); 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 d0604d552e9..61b325a0813 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 @@ -1,7 +1,6 @@ package org.zstack.kvm.efi; 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; @@ -108,10 +107,6 @@ 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) 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 f45e9127817..ae4f66b9799 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 @@ -1293,9 +1293,7 @@ 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.setVolumeFormat(VolumeType.NvRam.toString().equals(volume.getType()) - ? ImageConstant.RAW_FORMAT_STRING - : ImageConstant.QCOW2_FORMAT_STRING); + cmd.setVolumeFormat(ImageConstant.QCOW2_FORMAT_STRING); } else { cmd.setVolumeFormat(ImageConstant.QCOW2_FORMAT_STRING); if (VolumeType.Root.toString().equals(volume.getType())) { @@ -1306,9 +1304,6 @@ 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.setVolumeFormat(ImageConstant.RAW_FORMAT_STRING); } } cmd.setName(volume.getName()); 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 b52fb127eda..46e6986abd2 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,9 +1107,6 @@ 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())); } 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 af5e39a76bb..a1e0d10bf8b 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 @@ -974,9 +974,6 @@ 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"); } From 32bb845c13dcb7cb349a47148dce0066b35a4b05 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 27 Mar 2026 15:38:08 +0800 Subject: [PATCH 085/129] [kvm]: support TPM key provider restore from snapshot group Add findKeyProviderUuidByName and defaultKeyProviderUuid methods to TpmEncryptedResourceKeyBackend interface for key provider lookup. Implement these methods in DummyTpmEncryptedResourceKeyBackend. Add backupUuid field to KvmSecureBootExtensions to support restoring from specific backup files instead of default latest backup. Update SnapshotGroupRevertTpmHelper to restore TPM key provider information from system tags on backup files, with fallback to default provider. Related: ZSV-11310 Resolves: ZSV-11441 Change-Id: I6d75766b6a6c616e73737a6d77626d6870706574 --- .../DummyTpmEncryptedResourceKeyBackend.java | 10 +++++ .../TpmEncryptedResourceKeyBackend.java | 10 +++++ .../kvm/efi/KvmSecureBootExtensions.java | 37 ++++++++++++++----- .../kvm/tpm/SnapshotGroupRevertTpmHelper.java | 36 +++++++++++++----- 4 files changed, 75 insertions(+), 18 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 957dd7a0a98..01b577d80ad 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,11 +23,21 @@ public String findKeyProviderUuidByTpm(String tpmUuid) { return null; } + @Override + public String findKeyProviderUuidByName(String providerName) { + return null; + } + @Override public String findKeyProviderNameByTpm(String tpmUuid) { return null; } + @Override + public String defaultKeyProviderUuid() { + 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 6a9bd8f7149..2992af5ec7a 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,11 +25,21 @@ public interface TpmEncryptedResourceKeyBackend { */ String findKeyProviderUuidByTpm(String tpmUuid); + /** + * maybe null (when crypto module is not installed) + */ + String findKeyProviderUuidByName(String providerName); + /** * maybe null (when crypto module is not installed) */ String findKeyProviderNameByTpm(String tpmUuid); + /** + * maybe null (when crypto module is not installed) + */ + String defaultKeyProviderUuid(); + static class CloneEncryptedResourceKeyContext { public String srcTpmUuid; public String dstTpmUuid; 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 b0c4bf6b33c..335fa77a19e 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,5 +1,6 @@ package org.zstack.kvm.efi; +import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.zstack.compute.legacy.ComputeLegacyGlobalProperty; import org.zstack.compute.vm.VmGlobalConfig; @@ -33,7 +34,6 @@ import org.zstack.header.vm.PreVmInstantiateResourceExtensionPoint; import org.zstack.header.vm.VmInstanceConstant; import org.zstack.header.vm.AfterReimageVmInstanceExtensionPoint; -import org.zstack.header.vm.VmInstanceDestroyExtensionPoint; import org.zstack.header.vm.VmInstanceInventory; import org.zstack.header.vm.VmInstanceMigrateExtensionPoint; import org.zstack.header.vm.VmInstanceSpec; @@ -364,6 +364,7 @@ public static class PrepareHostFileContext { public String hostUuid; public String vmUuid; public VmHostFileType type; + public String backupUuid; public String syncReason; public String path; @@ -373,8 +374,10 @@ public static class PrepareHostFileContext { private VmHostFileVO vmHostFile; private VmHostBackupFileVO vmBackupFileVO; - // property: VmHostFileVO (read success) > VmHostFileVO (read fail) > VmHostBackupFileVO - // Note: read VmHostBackupFileVO only if VmHostFileVO is not exist + // property: VmHostBackupFileVO (when "backupUuid" is set) + // > VmHostFileVO (read success) + // > VmHostFileVO (read fail) + // > VmHostBackupFileVO (vmInstanceUuid matched) <- read this only if VmHostFileVO is not exist } @SuppressWarnings("rawtypes") @@ -384,6 +387,11 @@ public void prepareHostFileOnHost(PrepareHostFileContext context, Completion com chain.then(new NoRollbackFlow() { String __name__ = "read-vm-host-file-from-origin-host"; + @Override + public boolean skip(Map data) { + return StringUtils.isNotBlank(context.backupUuid); + } + @Override public void run(FlowTrigger trigger, Map data) { VmHostFileVO vmHostFile = Q.New(VmHostFileVO.class) @@ -439,12 +447,23 @@ public boolean skip(Map data) { @Override public void run(FlowTrigger trigger, Map data) { - context.vmBackupFileVO = Q.New(VmHostBackupFileVO.class) - .eq(VmHostBackupFileVO_.type, context.type) - .eq(VmHostBackupFileVO_.resourceUuid, context.vmUuid) - .orderByDesc(VmHostBackupFileVO_.lastOpDate) - .limit(1) - .find(); + if (context.backupUuid == null) { + context.vmBackupFileVO = Q.New(VmHostBackupFileVO.class) + .eq(VmHostBackupFileVO_.type, context.type) + .eq(VmHostBackupFileVO_.resourceUuid, context.vmUuid) + .orderByDesc(VmHostBackupFileVO_.lastOpDate) + .limit(1) + .find(); + } else { + context.vmBackupFileVO = Q.New(VmHostBackupFileVO.class) + .eq(VmHostBackupFileVO_.uuid, context.backupUuid) + .find(); + if (context.vmBackupFileVO == null) { + trigger.fail(operr("cannot find matched vm-host backup file[backupUuid:%s]", + context.backupUuid)); + return; + } + } 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)); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/SnapshotGroupRevertTpmHelper.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/SnapshotGroupRevertTpmHelper.java index 4e5a706e1a9..26512f19d49 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/SnapshotGroupRevertTpmHelper.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/SnapshotGroupRevertTpmHelper.java @@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Configurable; import org.zstack.compute.vm.VmGlobalConfig; +import org.zstack.compute.vm.devices.TpmEncryptedResourceKeyBackend; import org.zstack.core.db.Q; import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO; import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO_; @@ -15,6 +16,7 @@ import org.zstack.header.vm.additions.VmHostFileType; import org.zstack.header.vm.devices.NvRamSpec; import org.zstack.header.vm.devices.VmDevicesSpec; +import org.zstack.kvm.KVMSystemTags; import org.zstack.resourceconfig.ResourceConfigFacade; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; @@ -26,7 +28,9 @@ public class SnapshotGroupRevertTpmHelper { private static final CLogger logger = Utils.getLogger(SnapshotGroupRevertTpmHelper.class); @Autowired - ResourceConfigFacade resourceConfigFacade; + private ResourceConfigFacade resourceConfigFacade; + @Autowired + private TpmEncryptedResourceKeyBackend tpmKeyBackend; public void setupFromApi(APICreateVmInstanceFromVolumeSnapshotGroupMsg apiMsg, CreateVmInstanceMsg cmsg) { String snapshotGroupUuid = apiMsg.getVolumeSnapshotGroupUuid(); @@ -87,17 +91,31 @@ public void setupFromApi(APICreateVmInstanceFromVolumeSnapshotGroupMsg apiMsg, C tpmSpec.setEnable(true); if (resetTpm) { - // resetTpm=true: reset secretUuid, generate a new one during VM creation + // resetTpm=true: reset generate a new one during VM creation logger.debug(String.format("resetTpm is true for volume snapshot group[uuid:%s], " + - "will reset secretUuid, tpmBackupFileUuid:%s", snapshotGroupUuid, tpmBackupFile.getUuid())); + "will reset tpmBackupFileUuid:%s", snapshotGroupUuid, tpmBackupFile.getUuid())); } else { tpmSpec.setBackupFileUuid(tpmBackupFile.getUuid()); - // resetTpm=false: should reuse secretUuid + keyProviderUuid recorded in VolumeSnapshotGroup, - // but the recording step is not yet implemented, leave them empty for now - // TODO: retrieve secretUuid and keyProviderUuid from VolumeSnapshotGroup and set them here - logger.warn(String.format("resetTpm is false for volume snapshot group[uuid:%s], " + - "should restore secretUuid and keyProviderUuid but they are not yet recorded in snapshot group, " + - "leaving empty. tpmBackupFileUuid:%s", snapshotGroupUuid, tpmBackupFile.getUuid())); + } + + String keyProviderName = KVMSystemTags.TPM_KEY_PROVIDER_NAME + .getTokenByResourceUuid(tpmBackupFile.getUuid(), KVMSystemTags.TPM_KEY_PROVIDER_NAME_TOKEN); + if (keyProviderName == null) { + logger.warn(String.format( + "failed to find keyProvider from snapshotGroup[uuid:%s] by tpmBackupFile[uuid:%s]", + snapshotGroupUuid, tpmBackupFile.getUuid())); + if (tpmSpec.getKeyProviderUuid() == null) { + tpmSpec.setKeyProviderUuid(tpmKeyBackend.defaultKeyProviderUuid()); // maybe null + } + } else { + String keyProviderUuid = tpmKeyBackend.findKeyProviderUuidByName(keyProviderName); + if (keyProviderUuid == null) { + logger.warn(String.format( + "failed to resolve keyProvider[name:%s] from snapshotGroup[uuid:%s] by tpmBackupFile[uuid:%s], keep keyProviderUuid unset", + keyProviderName, snapshotGroupUuid, tpmBackupFile.getUuid())); + } else { + tpmSpec.setKeyProviderUuid(keyProviderUuid); + } } } From f6e8b4168fb057590d66cb2238d36359d148a3ef Mon Sep 17 00:00:00 2001 From: "tao.yang" Date: Fri, 27 Mar 2026 18:15:36 +0800 Subject: [PATCH 086/129] [keyprovider]: Refine rekey validation and skip reporting Resolves: ZSV-11505 Change-Id: I6f756f7362636c7878676c786d6371746574746f --- sdk/src/main/java/SourceClassMap.java | 2 + .../api/RekeyKeyProviderRefsResult.java | 31 +++++++++++++++ .../keyprovider/api/RekeySkippedResource.java | 39 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeySkippedResource.java diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 232283f4d80..2260a0bea64 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -70,6 +70,7 @@ public class SourceClassMap { put("org.zstack.core.jsonlabel.JsonLabelInventory", "org.zstack.sdk.JsonLabelInventory"); put("org.zstack.crypto.ccs.CCSCertificateAccountRefInventory", "org.zstack.sdk.CCSCertificateAccountRefInventory"); put("org.zstack.crypto.ccs.CCSCertificateInventory", "org.zstack.sdk.CCSCertificateInventory"); + put("org.zstack.crypto.keyprovider.api.RekeySkippedResource", "org.zstack.sdk.keyprovider.api.RekeySkippedResource"); put("org.zstack.crypto.securitymachine.thirdparty.aisino.AiSiNoSecretResourcePoolInventory", "org.zstack.sdk.AiSiNoSecretResourcePoolInventory"); put("org.zstack.crypto.securitymachine.thirdparty.flkSec.FlkSecSecretResourcePoolInventory", "org.zstack.sdk.FlkSecSecretResourcePoolInventory"); put("org.zstack.crypto.securitymachine.thirdparty.flkSec.FlkSecSecurityMachineInventory", "org.zstack.sdk.FlkSecSecurityMachineInventory"); @@ -1264,6 +1265,7 @@ public class SourceClassMap { put("org.zstack.sdk.identity.ldap.entity.LdapServerInventory", "org.zstack.ldap.entity.LdapServerInventory"); put("org.zstack.sdk.identity.role.RoleAccountRefInventory", "org.zstack.header.identity.role.RoleAccountRefInventory"); put("org.zstack.sdk.identity.role.RoleInventory", "org.zstack.header.identity.role.RoleInventory"); + put("org.zstack.sdk.keyprovider.api.RekeySkippedResource", "org.zstack.crypto.keyprovider.api.RekeySkippedResource"); put("org.zstack.sdk.license.entity.LicenseHistoryInventory", "org.zstack.license.entity.LicenseHistoryInventory"); put("org.zstack.sdk.license.entity.LicenseUsageView", "org.zstack.license.entity.LicenseUsageView"); put("org.zstack.sdk.license.entity.UpdateLicenseView", "org.zstack.license.entity.UpdateLicenseView"); 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 index ffb0dd51c80..91f995ec6f6 100644 --- a/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsResult.java +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsResult.java @@ -3,5 +3,36 @@ public class RekeyKeyProviderRefsResult { + public int totalCount; + public void setTotalCount(int totalCount) { + this.totalCount = totalCount; + } + public int getTotalCount() { + return this.totalCount; + } + + public int successCount; + public void setSuccessCount(int successCount) { + this.successCount = successCount; + } + public int getSuccessCount() { + return this.successCount; + } + + public int skippedCount; + public void setSkippedCount(int skippedCount) { + this.skippedCount = skippedCount; + } + public int getSkippedCount() { + return this.skippedCount; + } + + public java.util.List skippedResources; + public void setSkippedResources(java.util.List skippedResources) { + this.skippedResources = skippedResources; + } + public java.util.List getSkippedResources() { + return this.skippedResources; + } } diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeySkippedResource.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeySkippedResource.java new file mode 100644 index 00000000000..70dce8675bf --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeySkippedResource.java @@ -0,0 +1,39 @@ +package org.zstack.sdk.keyprovider.api; + + + +public class RekeySkippedResource { + + public java.lang.Long keyRefId; + public void setKeyRefId(java.lang.Long keyRefId) { + this.keyRefId = keyRefId; + } + public java.lang.Long getKeyRefId() { + return this.keyRefId; + } + + public java.lang.String resourceType; + public void setResourceType(java.lang.String resourceType) { + this.resourceType = resourceType; + } + public java.lang.String getResourceType() { + return this.resourceType; + } + + public java.lang.String resourceUuid; + public void setResourceUuid(java.lang.String resourceUuid) { + this.resourceUuid = resourceUuid; + } + public java.lang.String getResourceUuid() { + return this.resourceUuid; + } + + public java.lang.String reason; + public void setReason(java.lang.String reason) { + this.reason = reason; + } + public java.lang.String getReason() { + return this.reason; + } + +} From fc22117e906c63e0b049cc4d9b03620398d52f65 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Sat, 28 Mar 2026 00:24:09 +0800 Subject: [PATCH 087/129] [kvm]: add online snapshot support for VM host files * Add VmHostFileBackupJob class to define VM host file backup tasks. * Add vmHostFileBackupJobs field to TakeVolumesSnapshotOnKvmMsg and hostBackupTempResourceUuid to TakeVolumesSnapshotOnKvmReply for tracking backup files during online snapshots. * Add snapshot backup path building methods in KVMConstant Implement VolumeSnapshotCreationExtensionPoint in KvmSecureBootExtensions to backup VM host files after online snapshot * Refactor sync logic in KvmSecureBootManager to support syncin to backup files * Add syncToBackup and backupResourceUuid fields to SyncVmHostFilesFromHostMsg. Related: ZSV-11310 Resolves: ZSV-11441 Change-Id: I6e6f7062697867706b616b6c7567707771676179 --- .../snapshot/TakeVolumesSnapshotOnKvmMsg.java | 11 + .../TakeVolumesSnapshotOnKvmReply.java | 9 + .../vm/additions/VmHostFileBackupJob.java | 36 +++ .../main/java/org/zstack/kvm/KVMConstant.java | 19 ++ .../kvm/efi/KvmSecureBootExtensions.java | 93 +++++- .../zstack/kvm/efi/KvmSecureBootManager.java | 278 +++++++++++++----- .../kvm/efi/SyncVmHostFilesFromHostMsg.java | 18 ++ 7 files changed, 385 insertions(+), 79 deletions(-) create mode 100644 header/src/main/java/org/zstack/header/vm/additions/VmHostFileBackupJob.java diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/TakeVolumesSnapshotOnKvmMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/TakeVolumesSnapshotOnKvmMsg.java index a54e8a2b8cf..c5199404ad7 100755 --- a/header/src/main/java/org/zstack/header/storage/snapshot/TakeVolumesSnapshotOnKvmMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/TakeVolumesSnapshotOnKvmMsg.java @@ -2,6 +2,7 @@ import org.zstack.header.host.HostMessage; import org.zstack.header.message.NeedReplyMessage; +import org.zstack.header.vm.additions.VmHostFileBackupJob; import java.util.List; @@ -11,6 +12,7 @@ public class TakeVolumesSnapshotOnKvmMsg extends NeedReplyMessage implements HostMessage { private List snapshotJobs; private String hostUuid; + private List vmHostFileBackupJobs; @Override public String getHostUuid() { @@ -28,4 +30,13 @@ public List getSnapshotJobs() { public void setSnapshotJobs(List snapshotJobs) { this.snapshotJobs = snapshotJobs; } + + public List getVmHostFileBackupJobs() { + return vmHostFileBackupJobs; + } + + public void setVmHostFileBackupJobs(List vmHostFileBackupJobs) { + this.vmHostFileBackupJobs = vmHostFileBackupJobs; + } + } diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/TakeVolumesSnapshotOnKvmReply.java b/header/src/main/java/org/zstack/header/storage/snapshot/TakeVolumesSnapshotOnKvmReply.java index 2ba73944a57..bb63fc9d324 100644 --- a/header/src/main/java/org/zstack/header/storage/snapshot/TakeVolumesSnapshotOnKvmReply.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/TakeVolumesSnapshotOnKvmReply.java @@ -9,6 +9,7 @@ */ public class TakeVolumesSnapshotOnKvmReply extends MessageReply { private List snapshotsResults; + private String hostBackupTempResourceUuid; public List getSnapshotsResults() { return snapshotsResults; @@ -17,4 +18,12 @@ public List getSnapshotsResults() { public void setSnapshotsResults(List snapshotsResults) { this.snapshotsResults = snapshotsResults; } + + public String getHostBackupTempResourceUuid() { + return hostBackupTempResourceUuid; + } + + public void setHostBackupTempResourceUuid(String hostBackupTempResourceUuid) { + this.hostBackupTempResourceUuid = hostBackupTempResourceUuid; + } } diff --git a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileBackupJob.java b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileBackupJob.java new file mode 100644 index 00000000000..8bff9b05c7e --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileBackupJob.java @@ -0,0 +1,36 @@ +package org.zstack.header.vm.additions; + +/** + * @author Zdream + * @date 2026-03-28 + * @since 0.0.5 + */ +public class VmHostFileBackupJob { + private String srcPath; + private String destPath; + private String type; + + public String getSrcPath() { + return srcPath; + } + + public void setSrcPath(String srcPath) { + this.srcPath = srcPath; + } + + public String getDestPath() { + return destPath; + } + + public void setDestPath(String destPath) { + this.destPath = destPath; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} 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 00e1cb723be..6657e2013a1 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -219,6 +219,25 @@ public static String buildPathForVmHostFileType(VmHostFileType type, String vmUu } } + public static final String NV_RAM_SNAPSHOT_BACKUP_FILE_PATH_FORMAT = "/var/lib/libvirt/qemu/nvram/%s-host-files/%s.fd.snapshot-backup"; + public static String buildNvramSnapshotBackupFilePath(String vmUuid) { + return String.format(NV_RAM_SNAPSHOT_BACKUP_FILE_PATH_FORMAT, vmUuid, vmUuid); + } + + public static final String TPM_STATE_SNAPSHOT_BACKUP_FILE_PATH_FORMAT = "/var/lib/libvirt/swtpm/%s.snapshot-backup/"; + public static String buildTpmStateSnapshotBackupFilePath(String 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_SNAPSHOT_BACKUP_FILE_PATH_FORMAT, vmUuidWithHyphen); + } + + public static String buildSnapshotBackupPathForVmHostFileType(VmHostFileType type, String vmUuid) { + switch (type) { + case NvRam: return buildNvramSnapshotBackupFilePath(vmUuid); + case TpmState: return buildTpmStateSnapshotBackupFilePath(vmUuid); + default: throw new CloudRuntimeException("unsupported VmHostFileType: " + type); + } + } + 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/efi/KvmSecureBootExtensions.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java index add607f8990..033e4dc8606 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.tpm.entity.TpmVO_; import org.zstack.header.vm.DiskAO; import org.zstack.header.vm.PreVmInstantiateResourceExtensionPoint; +import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.VmInstanceVO_; import org.zstack.header.vm.VmInstanceConstant; import org.zstack.header.vm.AfterReimageVmInstanceExtensionPoint; import org.zstack.header.vm.VmInstanceInventory; @@ -44,6 +46,12 @@ 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.storage.snapshot.ConsistentType; +import org.zstack.header.storage.snapshot.CreateVolumesSnapshotOverlayInnerMsg; +import org.zstack.header.storage.snapshot.TakeVolumesSnapshotOnKvmReply; +import org.zstack.header.storage.snapshot.VolumeSnapshotCreationExtensionPoint; +import org.zstack.header.storage.snapshot.VolumeSnapshotInventory; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupInventory; import org.zstack.header.volume.VolumeInventory; import org.zstack.kvm.KVMAgentCommands; import org.zstack.kvm.KVMAgentCommands.*; @@ -82,7 +90,8 @@ public class KvmSecureBootExtensions implements KVMStartVmExtensionPoint, VmPreMigrationExtensionPoint, AfterReimageVmInstanceExtensionPoint, VmReleaseResourceExtensionPoint, - VmInstanceMigrateExtensionPoint { + VmInstanceMigrateExtensionPoint, + VolumeSnapshotCreationExtensionPoint { private static final CLogger logger = Utils.getLogger(KvmSecureBootExtensions.class); @Autowired @@ -614,4 +623,86 @@ public void run(MessageReply reply) { } }); } + + @Override + public void afterVolumeLiveSnapshotGroupCreatedOnBackend(CreateVolumesSnapshotOverlayInnerMsg msg, + TakeVolumesSnapshotOnKvmReply treply, + Completion completion) { + if (treply == null || !treply.isSuccess()) { + completion.success(); + return; + } + + if (!msg.isBackupHostFileIfNeeded()) { + completion.success(); + return; + } + + String vmUuid = msg.getLockedVmInstanceUuids().get(0); + String hostUuid = Q.New(VmInstanceVO.class) + .select(VmInstanceVO_.hostUuid) + .eq(VmInstanceVO_.uuid, vmUuid) + .findValue(); + + List hostFiles = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) + .eq(VmHostFileVO_.hostUuid, hostUuid) + .list(); + + if (hostFiles.isEmpty()) { + completion.success(); + return; + } + + String tempResourceUuid = Platform.getUuid(); + + SyncVmHostFilesFromHostMsg syncMsg = new SyncVmHostFilesFromHostMsg(); + syncMsg.setVmUuid(vmUuid); + syncMsg.setHostUuid(hostUuid); + + for (VmHostFileVO file : hostFiles) { + if (file.getType() == VmHostFileType.NvRam) { + syncMsg.setNvRamPath(buildNvramSnapshotBackupFilePath(vmUuid)); + } else if (file.getType() == VmHostFileType.TpmState) { + syncMsg.setTpmStateFolder(buildTpmStateSnapshotBackupFilePath(vmUuid)); + } + } + + syncMsg.setSyncReason("snapshot-group-online-backup"); + syncMsg.setSyncToBackup(true); + syncMsg.setBackupResourceUuid(tempResourceUuid); + bus.makeLocalServiceId(syncMsg, VmInstanceConstant.SECURE_BOOT_SERVICE_ID); + bus.send(syncMsg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply reply) { + if (reply.isSuccess()) { + treply.setHostBackupTempResourceUuid(tempResourceUuid); + logger.debug(String.format("synced backup host files for vm[uuid:%s] to VmHostBackupFileVO[resourceUuid:%s]", + vmUuid, tempResourceUuid)); + } else { + logger.warn(String.format("failed to sync backup host files for vm[uuid:%s] during online snapshot, " + + "but tolerated: %s", vmUuid, reply.getError().getReadableDetails())); + } + completion.success(); + } + }); + } + + @Override + public void afterVolumeLiveSnapshotGroupCreationFailsOnBackend(CreateVolumesSnapshotOverlayInnerMsg msg, + TakeVolumesSnapshotOnKvmReply treply) { + // No cleanup needed — backup files on agent side are ephemeral + } + + @Override + public void afterVolumeSnapshotGroupCreated(VolumeSnapshotGroupInventory snapshotGroup, + ConsistentType consistentType, + Completion completion) { + completion.success(); + } + + @Override + public void afterVolumeSnapshotCreated(VolumeSnapshotInventory snapshot, 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 index d23467012d2..49f2fd2a150 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 @@ -44,6 +44,7 @@ import org.zstack.header.vm.additions.VmHostFileVO; import org.zstack.header.vm.additions.VmHostFileVO_; import org.zstack.kvm.KVMAgentCommands; +import org.zstack.kvm.KVMConstant; import org.zstack.kvm.KvmCommandSender; import org.zstack.kvm.KvmResponseWrapper; import org.zstack.resourceconfig.ResourceConfig; @@ -222,87 +223,15 @@ public void success(KvmResponseWrapper wrapper) { return; } - final List existsFiles = Q.New(VmHostFileVO.class) - .eq(VmHostFileVO_.vmInstanceUuid, msg.getVmUuid()) - .eq(VmHostFileVO_.hostUuid, msg.getHostUuid()) - .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(); + ErrorCode error; + if (msg.isSyncToBackup()) { + error = syncToBackupFiles(msg, readRsp); } else { - existsContentUuid = Collections.emptyList(); + error = syncToHostFiles(msg, cmd, readRsp); } - List errors = new ArrayList<>(); - for (String path : cmd.getPaths()) { - KVMAgentCommands.VmHostFileTO to = findOneOrNull(readRsp.getHostFiles(), item -> item.getPath().equals(path)); - if (to == null) { - continue; - } - if (to.getError() != null) { - errors.add(operr("failed to read file %s", path) - .withOpaque("path", path) - .withException(to.getError())); - continue; - } - - VmHostFileType type = Objects.equals(path, msg.getNvRamPath()) ? - 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) - .set(VmHostFileVO_.lastSyncReason, msg.getSyncReason()) - .update(); - } else { - file = new VmHostFileVO(); - file.setUuid(Platform.getUuid()); - file.setHostUuid(msg.getHostUuid()); - file.setVmInstanceUuid(msg.getVmUuid()); - file.setPath(path); - file.setType(type); - file.setLastSyncReason(msg.getSyncReason()); - file.setCreateDate(now); - file.setLastOpDate(now); - file.setResourceName(String.format("%s file for %s", type, msg.getVmUuid())); - 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); - } - - if (logger.isTraceEnabled()) { - logger.trace(String.format("persist/update VmHostFileContentVO [uuid=%s]", file.getUuid())); - } - } - - if (!errors.isEmpty()) { - reply.setError(operr("failed to read file content from host[uuid=%s]", msg.getHostUuid()) - .withCause(errors)); + if (error != null) { + reply.setError(error); } bus.reply(msg, reply); } @@ -315,6 +244,199 @@ public void fail(ErrorCode errorCode) { }); } + private ErrorCode syncToHostFiles(SyncVmHostFilesFromHostMsg msg, + KVMAgentCommands.ReadVmHostFileContentCmd cmd, + KVMAgentCommands.ReadVmHostFileContentResponse readRsp) { + final List existsFiles = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, msg.getVmUuid()) + .eq(VmHostFileVO_.hostUuid, msg.getHostUuid()) + .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(); + } + + List errors = new ArrayList<>(); + for (String path : cmd.getPaths()) { + KVMAgentCommands.VmHostFileTO to = findOneOrNull(readRsp.getHostFiles(), item -> item.getPath().equals(path)); + if (to == null) { + continue; + } + if (to.getError() != null) { + errors.add(operr("failed to read file %s", path) + .withOpaque("path", path) + .withException(to.getError())); + continue; + } + + VmHostFileType type = Objects.equals(path, msg.getNvRamPath()) ? + 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) + .set(VmHostFileVO_.lastSyncReason, msg.getSyncReason()) + .update(); + } else { + file = new VmHostFileVO(); + file.setUuid(Platform.getUuid()); + file.setHostUuid(msg.getHostUuid()); + file.setVmInstanceUuid(msg.getVmUuid()); + file.setPath(path); + file.setType(type); + file.setLastSyncReason(msg.getSyncReason()); + file.setCreateDate(now); + file.setLastOpDate(now); + file.setResourceName(String.format("%s file for %s", type, msg.getVmUuid())); + 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); + } + + if (logger.isTraceEnabled()) { + logger.trace(String.format("persist/update VmHostFileContentVO [uuid=%s]", file.getUuid())); + } + } + + if (errors.isEmpty()) { + return null; + } + + return operr("failed to read file content from host[uuid=%s]", msg.getHostUuid()) + .withCause(errors); + } + + private ErrorCode syncToBackupFiles(SyncVmHostFilesFromHostMsg msg, + KVMAgentCommands.ReadVmHostFileContentResponse readRsp) { + if (msg.getBackupResourceUuid() == null || msg.getBackupResourceUuid().isEmpty()) { + return operr("backupResourceUuid is required when syncToBackup is true"); + } + + // Query the source VmHostFileVO records for afterBackup callback + final List sourceHostFiles = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, msg.getVmUuid()) + .eq(VmHostFileVO_.hostUuid, msg.getHostUuid()) + .list(); + + List backupFilesToPersist = new ArrayList<>(); + List contentsToPersist = new ArrayList<>(); + Map backupFromMap = new HashMap<>(); + + List errors = new ArrayList<>(); + Timestamp now = Timestamp.from(Instant.now()); + + for (KVMAgentCommands.VmHostFileTO to : readRsp.getHostFiles()) { + if (to == null) { + continue; + } + if (to.getError() != null) { + errors.add(operr("failed to read backup file %s", to.getPath()) + .withOpaque("path", to.getPath()) + .withException(to.getError())); + continue; + } + if (to.getContentBase64() == null) { + errors.add(operr("backup file %s returns empty content", to.getPath()) + .withOpaque("path", to.getPath())); + continue; + } + + VmHostFileType type = VmHostFileType.valueOf(to.getType()); + String expectPath = KVMConstant.buildSnapshotBackupPathForVmHostFileType(type, msg.getVmUuid()); + if (!(Objects.equals(to.getPath(), expectPath))) { + errors.add(operr("unexpected path %s for backup file type %s", to.getPath(), to.getType()) + .withOpaque("path", to.getPath()) + .withOpaque("type", to.getType())); + continue; + } + + VmHostBackupFileVO backupFile = new VmHostBackupFileVO(); + backupFile.setUuid(Platform.getUuid()); + backupFile.setResourceUuid(msg.getBackupResourceUuid()); + backupFile.setType(type); + backupFile.setCreateDate(now); + backupFile.setLastOpDate(now); + backupFilesToPersist.add(backupFile); + + VmHostFileContentVO content = new VmHostFileContentVO(); + content.setUuid(backupFile.getUuid()); + content.setContent(Base64.getDecoder().decode(to.getContentBase64())); + content.setFormat(VmHostFileContentFormat.valueOf(to.getFileFormat())); + content.setCreateDate(now); + content.setLastOpDate(now); + contentsToPersist.add(content); + + VmHostFileVO sourceFile = findOneOrNull(sourceHostFiles, item -> item.getType() == type); + if (sourceFile != null) { + backupFromMap.put(backupFile, sourceFile); + } + } + + new SQLBatch() { + @Override + protected void scripts() { + for (VmHostBackupFileVO bf : backupFilesToPersist) { + sql(VmHostBackupFileVO.class) + .eq(VmHostBackupFileVO_.resourceUuid, bf.getResourceUuid()) + .eq(VmHostBackupFileVO_.type, bf.getType()) + .delete(); + } + + if (!backupFilesToPersist.isEmpty()) { + databaseFacade.persistCollection(backupFilesToPersist); + } + if (!contentsToPersist.isEmpty()) { + databaseFacade.persistCollection(contentsToPersist); + } + } + }.execute(); + + for (VmHostBackupFileVO backup : backupFilesToPersist) { + VmHostFileVO source = backupFromMap.get(backup); + if (source != null) { + try { + vmHostFileFactory.createBackupBase(backup).afterBackup(source); + } catch (Exception e) { + logger.warn(String.format("failed to execute afterBackup hook for VmHostBackupFileVO[uuid:%s, type:%s]: %s", + backup.getUuid(), backup.getType(), e.getMessage()), e); + } + } + } + + if (errors.isEmpty()) { + return null; + } + + return operr("failed to read backup file content from host[uuid=%s]", msg.getHostUuid()) + .withCause(errors); + } @SuppressWarnings("rawtypes") private void handle(CloneVmHostFileMsg msg) { diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostMsg.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostMsg.java index 75e33f5791e..8158110f596 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostMsg.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostMsg.java @@ -8,6 +8,8 @@ public class SyncVmHostFilesFromHostMsg extends NeedReplyMessage { private String nvRamPath; private String tpmStateFolder; private String syncReason; + private boolean syncToBackup; + private String backupResourceUuid; public String getHostUuid() { return hostUuid; @@ -48,4 +50,20 @@ public String getSyncReason() { public void setSyncReason(String syncReason) { this.syncReason = syncReason; } + + public boolean isSyncToBackup() { + return syncToBackup; + } + + public void setSyncToBackup(boolean syncToBackup) { + this.syncToBackup = syncToBackup; + } + + public String getBackupResourceUuid() { + return backupResourceUuid; + } + + public void setBackupResourceUuid(String backupResourceUuid) { + this.backupResourceUuid = backupResourceUuid; + } } From d9884ec9f44481fe9d8853bb59bc760f71a1d702 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Mon, 30 Mar 2026 11:32:59 +0800 Subject: [PATCH 088/129] [header]: remove invalid notes on VmHostFileBackupJob Related: ZSV-11310 Change-Id: I736e72657168776a6669727673756b6d6f6d6375 --- .../org/zstack/header/vm/additions/VmHostFileBackupJob.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileBackupJob.java b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileBackupJob.java index 8bff9b05c7e..4b82fc0b3b8 100644 --- a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileBackupJob.java +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileBackupJob.java @@ -1,10 +1,5 @@ package org.zstack.header.vm.additions; -/** - * @author Zdream - * @date 2026-03-28 - * @since 0.0.5 - */ public class VmHostFileBackupJob { private String srcPath; private String destPath; From d641dbe99d9c448f3190bd2c84d8d301352f4e71 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Mon, 30 Mar 2026 14:40:46 +0800 Subject: [PATCH 089/129] [kvm]: register VolumeSnapshotCreationExtensionPoint Register VolumeSnapshotCreationExtensionPoint in Kvm.xml to enable KvmSecureBootExtensions to implement volume snapshot creation extension point for VM host file backup. Related: ZSV-11310 Resolves: ZSV-11441 Change-Id: I726d6e6c64796b69726863776f6875757063626c --- conf/springConfigXml/Kvm.xml | 1 + test/src/test/resources/springConfigXml/Kvm.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index 0080c90c528..42d903ba3d1 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -275,6 +275,7 @@ + diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index 5034922b145..9a3c87d2ba5 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -274,6 +274,7 @@ + From f5da7247c68352fd7f344230cc97649fdc7a153c Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Mon, 30 Mar 2026 18:46:14 +0800 Subject: [PATCH 090/129] [kvm]: add hostfile backup agent command Add BACKUP_VM_HOST_FILE_PATH constant, BackupVmHostFileCmd / BackupVmHostFileResponse in KVMAgentCommands, and BackupVmHostFileOnHypervisorMsg / Reply. Implement the handler in KvmSecureBootManager to send the backup command to the KVM agent via KvmCommandSender. Related: ZSV-11310 Resolves: ZSV-11441 Change-Id: I72746d6272746761776f66776862726f78676976 --- .../java/org/zstack/kvm/KVMAgentCommands.java | 16 + .../main/java/org/zstack/kvm/KVMConstant.java | 1 + .../efi/BackupVmHostFileOnHypervisorMsg.java | 29 + .../BackupVmHostFileOnHypervisorReply.java | 6 + .../zstack/kvm/efi/KvmSecureBootManager.java | 29 + .../org/zstack/testlib/KVMSimulator.groovy | 1382 +++++++++-------- 6 files changed, 774 insertions(+), 689 deletions(-) create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileOnHypervisorMsg.java create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileOnHypervisorReply.java 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 b71cc075c89..0f02858071d 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.VmHostFileBackupJob; import org.zstack.header.vm.devices.DeviceAddress; import org.zstack.header.vm.devices.VirtualDeviceInfo; import org.zstack.kvm.tpm.TpmTO; @@ -3015,6 +3016,21 @@ public void setHostFiles(List hostFiles) { public static class WriteVmHostFileContentResponse extends AgentResponse { } + public static class BackupVmHostFileCmd extends AgentCommand { + private List vmHostFileBackupJobs; + + public List getVmHostFileBackupJobs() { + return vmHostFileBackupJobs; + } + + public void setVmHostFileBackupJobs(List vmHostFileBackupJobs) { + this.vmHostFileBackupJobs = vmHostFileBackupJobs; + } + } + + public static class BackupVmHostFileResponse 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 6657e2013a1..1f7fd4731e0 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -89,6 +89,7 @@ public interface KVMConstant { 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 BACKUP_VM_HOST_FILE_PATH = "/vm/hostfile/backup"; String ISO_TO = "kvm.isoto"; String ANSIBLE_PLAYBOOK_NAME = "kvm.py"; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileOnHypervisorMsg.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileOnHypervisorMsg.java new file mode 100644 index 00000000000..803503e2553 --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileOnHypervisorMsg.java @@ -0,0 +1,29 @@ +package org.zstack.kvm.efi; + +import org.zstack.header.host.HostMessage; +import org.zstack.header.message.NeedReplyMessage; +import org.zstack.header.vm.additions.VmHostFileBackupJob; + +import java.util.List; + +public class BackupVmHostFileOnHypervisorMsg extends NeedReplyMessage implements HostMessage { + private String hostUuid; + private List vmHostFileBackupJobs; + + @Override + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + public List getVmHostFileBackupJobs() { + return vmHostFileBackupJobs; + } + + public void setVmHostFileBackupJobs(List vmHostFileBackupJobs) { + this.vmHostFileBackupJobs = vmHostFileBackupJobs; + } +} diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileOnHypervisorReply.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileOnHypervisorReply.java new file mode 100644 index 00000000000..7670f13fa87 --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileOnHypervisorReply.java @@ -0,0 +1,6 @@ +package org.zstack.kvm.efi; + +import org.zstack.header.message.MessageReply; + +public class BackupVmHostFileOnHypervisorReply extends MessageReply { +} 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 49f2fd2a150..6d7a3758293 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 @@ -178,6 +178,8 @@ public void handleMessage(Message msg) { handle((BackupVmHostFileMsg) msg); } else if (msg instanceof RestoreVmHostFileMsg) { handle((RestoreVmHostFileMsg) msg); + } else if (msg instanceof BackupVmHostFileOnHypervisorMsg) { + handle((BackupVmHostFileOnHypervisorMsg) msg); } else { bus.dealWithUnknownMessage(msg); } @@ -938,4 +940,31 @@ public void fail(ErrorCode errorCode) { }) .start(); } + + private void handle(BackupVmHostFileOnHypervisorMsg msg) { + KvmCommandSender sender = new KvmCommandSender(msg.getHostUuid()); + + KVMAgentCommands.BackupVmHostFileCmd cmd = new KVMAgentCommands.BackupVmHostFileCmd(); + cmd.setVmHostFileBackupJobs(msg.getVmHostFileBackupJobs()); + + BackupVmHostFileOnHypervisorReply reply = new BackupVmHostFileOnHypervisorReply(); + sender.send(cmd, BACKUP_VM_HOST_FILE_PATH, wrapper -> { + KVMAgentCommands.BackupVmHostFileResponse rsp = wrapper.getResponse(KVMAgentCommands.BackupVmHostFileResponse.class); + return rsp.isSuccess() ? null : + operr("failed to backup vm host file on hypervisor[hostUuid=%s]", msg.getHostUuid()) + .withOpaque("host.uuid", msg.getHostUuid()) + .withException(rsp.getError()); + }, new ReturnValueCompletion(msg) { + @Override + public void success(KvmResponseWrapper wrapper) { + bus.reply(msg, reply); + } + + @Override + public void fail(ErrorCode errorCode) { + reply.setError(errorCode); + bus.reply(msg, reply); + } + }); + } } diff --git a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy index 360b0164fe0..c9b975069cd 100755 --- a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy +++ b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy @@ -1,326 +1,326 @@ -package org.zstack.testlib - +package org.zstack.testlib + import org.springframework.http.HttpEntity import org.zstack.core.Platform import org.zstack.core.db.Q -import org.zstack.core.db.SQL -import org.zstack.core.db.SQLBatch -import org.zstack.header.Constants -import org.zstack.header.storage.primary.PrimaryStorageVO -import org.zstack.header.storage.primary.PrimaryStorageVO_ -import org.zstack.header.storage.snapshot.TakeSnapshotsOnKvmJobStruct -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 -import org.zstack.header.volume.VolumeVO -import org.zstack.header.volume.VolumeVO_ -import org.zstack.kvm.KVMAgentCommands -import org.zstack.kvm.KVMConstant -import org.zstack.kvm.VolumeTO -import org.zstack.testlib.vfs.Qcow2 -import org.zstack.testlib.vfs.VFS -import org.zstack.testlib.vfs.extensions.VFSPrimaryStorageTakeSnapshotBackend -import org.zstack.testlib.vfs.extensions.VFSSnapshot -import org.zstack.utils.BeanUtils -import org.zstack.utils.data.SizeUnit -import org.zstack.utils.gson.JSONObjectUtil - -import javax.persistence.Tuple -import java.util.concurrent.ConcurrentHashMap - -import static org.zstack.kvm.KVMAgentCommands.* -/** - * Created by xing5 on 2017/6/6. - */ +import org.zstack.core.db.SQL +import org.zstack.core.db.SQLBatch +import org.zstack.header.Constants +import org.zstack.header.storage.primary.PrimaryStorageVO +import org.zstack.header.storage.primary.PrimaryStorageVO_ +import org.zstack.header.storage.snapshot.TakeSnapshotsOnKvmJobStruct +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 +import org.zstack.header.volume.VolumeVO +import org.zstack.header.volume.VolumeVO_ +import org.zstack.kvm.KVMAgentCommands +import org.zstack.kvm.KVMConstant +import org.zstack.kvm.VolumeTO +import org.zstack.testlib.vfs.Qcow2 +import org.zstack.testlib.vfs.VFS +import org.zstack.testlib.vfs.extensions.VFSPrimaryStorageTakeSnapshotBackend +import org.zstack.testlib.vfs.extensions.VFSSnapshot +import org.zstack.utils.BeanUtils +import org.zstack.utils.data.SizeUnit +import org.zstack.utils.gson.JSONObjectUtil + +import javax.persistence.Tuple +import java.util.concurrent.ConcurrentHashMap + +import static org.zstack.kvm.KVMAgentCommands.* +/** + * Created by xing5 on 2017/6/6. + */ class KVMSimulator implements Simulator { static ConcurrentHashMap connectCmdConcurrentHashMap = new ConcurrentHashMap<>() static ConcurrentHashMap ensureSecretUuidCache = new ConcurrentHashMap<>() static final String MOCK_ENVELOPE_PUBLIC_KEY_BASE64 = "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA=" private static Map takeSnapshotBackends = [:] - - static { - BeanUtils.reflections.getSubTypesOf(VFSPrimaryStorageTakeSnapshotBackend.class).each { clz -> - VFSPrimaryStorageTakeSnapshotBackend backend = clz.getConstructor().newInstance() - VFSPrimaryStorageTakeSnapshotBackend old = takeSnapshotBackends[backend.primaryStorageType] - assert old == null : "duplicated VFSPrimaryStorageTakeSnapshotBackend[type: ${backend.primaryStorageType}]," + - " ${backend} and ${old}" - takeSnapshotBackends[backend.primaryStorageType] = backend - } - } - - static List takeSnapshotByPrimaryStorage(HttpEntity e, EnvSpec spec, List snapshotJobs) { - Map> jobMap = new HashMap<>() - Map primaryStorageTypeMap = new HashMap<>() - snapshotJobs.each { job -> - Tuple tuple = SQL.New("select pri.uuid, pri.type from PrimaryStorageVO pri, VolumeVO vol where" + - " pri.uuid = vol.primaryStorageUuid and vol.uuid = :volUuid", Tuple.class).param("volUuid", job.volumeUuid) - .find() - assert tuple.get(1) : "cannot find primary storage of volume[uuid: ${job.volumeUuid}]" - - jobMap.putIfAbsent((String) tuple.get(0), new ArrayList<>()) - jobMap.get((String) tuple.get(0)).add(job) - primaryStorageTypeMap.putIfAbsent((String) tuple.get(0), (String) tuple.get(1)) - } - - List results = new ArrayList<>() - jobMap.entrySet().each { entry -> - String psUuid = entry.key - List jobs = entry.value - VFSPrimaryStorageTakeSnapshotBackend bkd = getVFSPrimaryStorageTakeSnapshotBackend(primaryStorageTypeMap.get(psUuid)) - results.addAll(bkd.takeSnapshotsOnVolumes(psUuid, e, spec, jobs)) - } - - - return results - } - - static VFSPrimaryStorageTakeSnapshotBackend getVFSPrimaryStorageTakeSnapshotBackend(String primaryStorageType) { - VFSPrimaryStorageTakeSnapshotBackend bkd = takeSnapshotBackends[primaryStorageType] - assert bkd != null : "cannot find VFSPrimaryStorageTakeSnapshotBackend[type: ${primaryStorageType}]" - return bkd - } - - @Override - void registerSimulators(EnvSpec spec) { - spec.simulator(KVMConstant.KVM_HOST_CAPACITY_PATH) { HttpEntity e, EnvSpec espec -> - def rsp = new KVMAgentCommands.HostCapacityResponse() - - KVMHostSpec kspec = espec.specByUuid(e.getHeaders().getFirst(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID)) - rsp.success = true - - if (kspec == null) { - rsp.usedCpu = 0 - rsp.cpuNum = 8 - rsp.totalMemory = SizeUnit.GIGABYTE.toByte(32) - rsp.usedMemory = 0 - rsp.cpuSpeed = 1 - rsp.cpuSockets = 2 - rsp.cpuCoreNum = 4 - } else { - rsp.usedCpu = kspec.usedCpu - rsp.cpuNum = kspec.totalCpu - rsp.totalMemory = kspec.totalMem - rsp.usedMemory = kspec.usedMem - rsp.cpuSpeed = 1 - rsp.cpuSockets = kspec.cpuSockets - rsp.cpuCoreNum = kspec.cpuCores - } - - return rsp - } - - spec.simulator(KVMConstant.KVM_HARDEN_CONSOLE_PATH) { - return new KVMAgentCommands.AgentResponse() - } - - spec.simulator(KVMConstant.KVM_DELETE_CONSOLE_FIREWALL_PATH) { - return new KVMAgentCommands.AgentResponse() - } - - spec.simulator(KVMConstant.GET_VM_DEVICE_ADDRESS_PATH) { HttpEntity e -> - def cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.GetVmDeviceAddressCmd.class) - def rsp = new KVMAgentCommands.GetVmDeviceAddressRsp() - if (cmd.deviceTOs.keySet().contains(VolumeVO.class.simpleName)) { - rsp.addresses = ["VolumeVO": []] - for (Object o : cmd.deviceTOs.get(VolumeVO.class.simpleName)) { - VolumeTO to = JSONObjectUtil.rehashObject(o, VolumeTO.class) - rsp.addresses[VolumeVO.class.simpleName].add(new KVMAgentCommands.VmDeviceAddressTO( - addressType: "pci", - address: String.format("0000:%02d:00:0", to.deviceId), - deviceType: "disk", - uuid: to.volumeUuid - )) - } - } - - return rsp - } - - spec.simulator(KVMConstant.GET_VIRTUALIZER_INFO_PATH) { HttpEntity e -> - def rsp = new GetVirtualizerInfoRsp() - rsp.hostInfo = new VirtualizerInfoTO() - rsp.hostInfo.version = "4.2.0-627.g36ee592.el7" - rsp.hostInfo.virtualizer = "qemu-kvm" - String hostUuid = e.getHeaders().getFirst(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID) - rsp.hostInfo.uuid = hostUuid - - def cmd = JSONObjectUtil.toObject(e.body, GetVirtualizerInfoCmd.class) - rsp.vmInfoList = cmd.vmUuids.collect { vmUuid -> - def to = new VirtualizerInfoTO() - to.uuid = vmUuid - to.version = "4.2.0-627.g36ee592.el7" - to.virtualizer = "qemu-kvm" - return to - } - - return rsp - } - - spec.simulator(KVMConstant.KVM_HOST_FACT_PATH) { HttpEntity e -> - def hostUuid = e.getHeaders().getFirst(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID) - def rsp = new HostFactResponse() - - rsp.osDistribution = "zstack" - rsp.osRelease = "kvmSimulator" - rsp.osVersion = "0.1" - rsp.qemuImgVersion = "2.0.0" - rsp.libvirtVersion = "1.2.9" - rsp.cpuModelName = "Broadwell" - rsp.cpuProcessorNum = 10 - rsp.cpuThreadsPerCore = 2 - rsp.cpuCoresPerSocket = 5 - rsp.cpuSockets = 1 - rsp.cpuGHz = "2.10" - rsp.hostCpuModelName = "Broadwell @ 2.10GHz" - rsp.ipmiAddress = "None" - rsp.eptFlag = "ept" - rsp.libvirtCapabilities = ["incrementaldrivemirror", "blockcopynetworktarget"] - rsp.powerSupplyModelName = "" - rsp.powerSupplyManufacturer = "" - rsp.hvmCpuFlag = "" - rsp.cpuCache = "64.0,4096.0,16384.0" - rsp.nqn = "nqn.2014-08.org.nvmexpress:uuid:748d0363-8366-44db-803b-146effb96988" - rsp.hostname = "hostname" - rsp.iscsiInitiatorName = "iqn.1994-05.com.redhat:" + hostUuid.substring(0, 12) - rsp.virtualizerInfo = new VirtualizerInfoTO() - rsp.virtualizerInfo.version = "4.2.0-627.g36ee592.el7" - rsp.virtualizerInfo.virtualizer = "qemu-kvm" - return rsp - } - - spec.simulator(KVMConstant.KVM_VM_UPDATE_CPU_QUOTA_PATH) { - return new KVMAgentCommands.UpdateVmCpuQuotaRsp() - } - - spec.simulator(KVMConstant.KVM_VM_UPDATE_PRIORITY_PATH) { - return new KVMAgentCommands.UpdateVmPriorityRsp() - } - - spec.simulator(KVMConstant.KVM_VM_CHECK_STATE) { HttpEntity e -> - KVMAgentCommands.CheckVmStateCmd cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.CheckVmStateCmd.class) - List vms = Q.New(VmInstanceVO.class).in(VmInstanceVO_.uuid, cmd.vmUuids).list() - KVMAgentCommands.CheckVmStateRsp rsp = new KVMAgentCommands.CheckVmStateRsp() - rsp.states = [:] - vms.each { - def kstate = KVMConstant.KvmVmState.fromVmInstanceState(it.state) - if (kstate != null) { - rsp.states[(it.uuid)] = kstate.toString() - } else { - rsp.states[(it.uuid)] = KVMConstant.KvmVmState.Shutdown.toString() - } - } - - return rsp - } - - spec.simulator(KVMConstant.KVM_ATTACH_NIC_PATH) { HttpEntity e -> - KVMAgentCommands.AttachNicCommand cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.AttachNicCommand.class) - KVMAgentCommands.AttachNicResponse rsp = new KVMAgentCommands.AttachNicResponse() - VirtualDeviceInfo deviceInfo = new VirtualDeviceInfo() - deviceInfo.setResourceUuid(cmd.nic.getUuid()) - rsp.setVirtualDeviceInfoList(Arrays.asList(deviceInfo)) - return rsp - } - - spec.simulator(KVMConstant.KVM_DETACH_NIC_PATH) { - return new KVMAgentCommands.DetachNicRsp() - } - - spec.simulator(KVMConstant.KVM_UPDATE_NIC_PATH) { - return new KVMAgentCommands.UpdateNicRsp() - } - - spec.simulator(KVMConstant.KVM_ATTACH_ISO_PATH) { - return new KVMAgentCommands.AttachIsoRsp() - } - - spec.simulator(KVMConstant.KVM_DETACH_ISO_PATH) { - return new KVMAgentCommands.DetachIsoRsp() - } - - spec.simulator(KVMConstant.KVM_MERGE_SNAPSHOT_PATH) { HttpEntity e, EnvSpec espec -> - return new KVMAgentCommands.MergeSnapshotRsp() - } - - VFS.vfsHook(KVMConstant.KVM_MERGE_SNAPSHOT_PATH, spec) { rsp, HttpEntity e, EnvSpec espec -> - KVMAgentCommands.MergeSnapshotCmd cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.MergeSnapshotCmd.class) - VolumeInventory volume = null - String primaryStorageType = null - - new SQLBatch() { - @Override - protected void scripts() { - VolumeVO vol = sql("select vol from VolumeVO vol where vol.vmInstanceUuid = :vmUuid " + - " and vol.deviceId = :deviceId", VolumeVO.class) - .param("vmUuid", cmd.vmUuid) - .param("deviceId", cmd.volume.getDeviceId()) - .find() - - assert vol: "cannot find dest volume[path: ${cmd.destPath}, deviceId: ${cmd.volume.getDeviceId()}] of VM[uuid: ${cmd.vmUuid}] in database" - volume = vol.toInventory() - - primaryStorageType = q(PrimaryStorageVO.class).select(PrimaryStorageVO_.type).eq(PrimaryStorageVO_.uuid, vol.primaryStorageUuid).findValue() - assert primaryStorageType: "cannot find primary storage[uuid: ${vol.primaryStorageUuid}]" - } - }.execute() - - VFSPrimaryStorageTakeSnapshotBackend bkd = getVFSPrimaryStorageTakeSnapshotBackend(primaryStorageType) - bkd.mergeSnapshots(e, espec, cmd, volume) - } - - spec.simulator(KVMConstant.KVM_TAKE_VOLUME_SNAPSHOT_PATH) { HttpEntity e, EnvSpec espec -> - return new KVMAgentCommands.TakeSnapshotResponse() - } - - VFS.vfsHook(KVMConstant.KVM_TAKE_VOLUME_SNAPSHOT_PATH, spec) { rsp, HttpEntity e, EnvSpec espec -> - KVMAgentCommands.TakeSnapshotCmd cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.TakeSnapshotCmd.class) - - VolumeVO volume = Q.New(VolumeVO.class).eq(VolumeVO_.uuid, cmd.volumeUuid).find() - assert volume: "cannot find volume[uuid: ${cmd.volumeUuid}]" - String primaryStorageType = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.type) - .eq(PrimaryStorageVO_.uuid, volume.primaryStorageUuid).findValue() - assert primaryStorageType: "cannot find primary storage[uuid: ${volume.primaryStorageUuid}] from volume[uuid: ${volume.uuid}, name: ${volume.name}]" - VFSPrimaryStorageTakeSnapshotBackend bkd = getVFSPrimaryStorageTakeSnapshotBackend(primaryStorageType) - - VFSSnapshot snapshot = bkd.takeSnapshot(e, espec, cmd, volume.toInventory() as VolumeInventory) - rsp.newVolumeInstallPath = snapshot.installPath - rsp.snapshotInstallPath = cmd.volumeInstallPath - // size required greater than 0, if no mock value set keep size return 1 - rsp.size = snapshot.size == null || snapshot.size == 0 ? 1 : snapshot.size - return rsp - } - - spec.simulator(KVMConstant.KVM_PING_PATH) { HttpEntity e, EnvSpec espec -> - KVMAgentCommands.PingCmd cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.PingCmd.class) - assert null != cmd - assert null != cmd.hostUuid - - def rsp = new KVMAgentCommands.PingResponse() - rsp.hostUuid = cmd.hostUuid - rsp.version = connectCmdConcurrentHashMap.get(rsp.hostUuid).version - rsp.sendCommandUrl = connectCmdConcurrentHashMap.get(rsp.hostUuid).sendCommandUrl - return rsp - } - - spec.simulator(KVMConstant.KVM_UPDATE_HOST_CONFIGURATION_PATH) { HttpEntity e, EnvSpec espec -> - def rsp = new KVMAgentCommands.UpdateHostConfigurationResponse() - return rsp - } - + + static { + BeanUtils.reflections.getSubTypesOf(VFSPrimaryStorageTakeSnapshotBackend.class).each { clz -> + VFSPrimaryStorageTakeSnapshotBackend backend = clz.getConstructor().newInstance() + VFSPrimaryStorageTakeSnapshotBackend old = takeSnapshotBackends[backend.primaryStorageType] + assert old == null : "duplicated VFSPrimaryStorageTakeSnapshotBackend[type: ${backend.primaryStorageType}]," + + " ${backend} and ${old}" + takeSnapshotBackends[backend.primaryStorageType] = backend + } + } + + static List takeSnapshotByPrimaryStorage(HttpEntity e, EnvSpec spec, List snapshotJobs) { + Map> jobMap = new HashMap<>() + Map primaryStorageTypeMap = new HashMap<>() + snapshotJobs.each { job -> + Tuple tuple = SQL.New("select pri.uuid, pri.type from PrimaryStorageVO pri, VolumeVO vol where" + + " pri.uuid = vol.primaryStorageUuid and vol.uuid = :volUuid", Tuple.class).param("volUuid", job.volumeUuid) + .find() + assert tuple.get(1) : "cannot find primary storage of volume[uuid: ${job.volumeUuid}]" + + jobMap.putIfAbsent((String) tuple.get(0), new ArrayList<>()) + jobMap.get((String) tuple.get(0)).add(job) + primaryStorageTypeMap.putIfAbsent((String) tuple.get(0), (String) tuple.get(1)) + } + + List results = new ArrayList<>() + jobMap.entrySet().each { entry -> + String psUuid = entry.key + List jobs = entry.value + VFSPrimaryStorageTakeSnapshotBackend bkd = getVFSPrimaryStorageTakeSnapshotBackend(primaryStorageTypeMap.get(psUuid)) + results.addAll(bkd.takeSnapshotsOnVolumes(psUuid, e, spec, jobs)) + } + + + return results + } + + static VFSPrimaryStorageTakeSnapshotBackend getVFSPrimaryStorageTakeSnapshotBackend(String primaryStorageType) { + VFSPrimaryStorageTakeSnapshotBackend bkd = takeSnapshotBackends[primaryStorageType] + assert bkd != null : "cannot find VFSPrimaryStorageTakeSnapshotBackend[type: ${primaryStorageType}]" + return bkd + } + + @Override + void registerSimulators(EnvSpec spec) { + spec.simulator(KVMConstant.KVM_HOST_CAPACITY_PATH) { HttpEntity e, EnvSpec espec -> + def rsp = new KVMAgentCommands.HostCapacityResponse() + + KVMHostSpec kspec = espec.specByUuid(e.getHeaders().getFirst(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID)) + rsp.success = true + + if (kspec == null) { + rsp.usedCpu = 0 + rsp.cpuNum = 8 + rsp.totalMemory = SizeUnit.GIGABYTE.toByte(32) + rsp.usedMemory = 0 + rsp.cpuSpeed = 1 + rsp.cpuSockets = 2 + rsp.cpuCoreNum = 4 + } else { + rsp.usedCpu = kspec.usedCpu + rsp.cpuNum = kspec.totalCpu + rsp.totalMemory = kspec.totalMem + rsp.usedMemory = kspec.usedMem + rsp.cpuSpeed = 1 + rsp.cpuSockets = kspec.cpuSockets + rsp.cpuCoreNum = kspec.cpuCores + } + + return rsp + } + + spec.simulator(KVMConstant.KVM_HARDEN_CONSOLE_PATH) { + return new KVMAgentCommands.AgentResponse() + } + + spec.simulator(KVMConstant.KVM_DELETE_CONSOLE_FIREWALL_PATH) { + return new KVMAgentCommands.AgentResponse() + } + + spec.simulator(KVMConstant.GET_VM_DEVICE_ADDRESS_PATH) { HttpEntity e -> + def cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.GetVmDeviceAddressCmd.class) + def rsp = new KVMAgentCommands.GetVmDeviceAddressRsp() + if (cmd.deviceTOs.keySet().contains(VolumeVO.class.simpleName)) { + rsp.addresses = ["VolumeVO": []] + for (Object o : cmd.deviceTOs.get(VolumeVO.class.simpleName)) { + VolumeTO to = JSONObjectUtil.rehashObject(o, VolumeTO.class) + rsp.addresses[VolumeVO.class.simpleName].add(new KVMAgentCommands.VmDeviceAddressTO( + addressType: "pci", + address: String.format("0000:%02d:00:0", to.deviceId), + deviceType: "disk", + uuid: to.volumeUuid + )) + } + } + + return rsp + } + + spec.simulator(KVMConstant.GET_VIRTUALIZER_INFO_PATH) { HttpEntity e -> + def rsp = new GetVirtualizerInfoRsp() + rsp.hostInfo = new VirtualizerInfoTO() + rsp.hostInfo.version = "4.2.0-627.g36ee592.el7" + rsp.hostInfo.virtualizer = "qemu-kvm" + String hostUuid = e.getHeaders().getFirst(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID) + rsp.hostInfo.uuid = hostUuid + + def cmd = JSONObjectUtil.toObject(e.body, GetVirtualizerInfoCmd.class) + rsp.vmInfoList = cmd.vmUuids.collect { vmUuid -> + def to = new VirtualizerInfoTO() + to.uuid = vmUuid + to.version = "4.2.0-627.g36ee592.el7" + to.virtualizer = "qemu-kvm" + return to + } + + return rsp + } + + spec.simulator(KVMConstant.KVM_HOST_FACT_PATH) { HttpEntity e -> + def hostUuid = e.getHeaders().getFirst(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID) + def rsp = new HostFactResponse() + + rsp.osDistribution = "zstack" + rsp.osRelease = "kvmSimulator" + rsp.osVersion = "0.1" + rsp.qemuImgVersion = "2.0.0" + rsp.libvirtVersion = "1.2.9" + rsp.cpuModelName = "Broadwell" + rsp.cpuProcessorNum = 10 + rsp.cpuThreadsPerCore = 2 + rsp.cpuCoresPerSocket = 5 + rsp.cpuSockets = 1 + rsp.cpuGHz = "2.10" + rsp.hostCpuModelName = "Broadwell @ 2.10GHz" + rsp.ipmiAddress = "None" + rsp.eptFlag = "ept" + rsp.libvirtCapabilities = ["incrementaldrivemirror", "blockcopynetworktarget"] + rsp.powerSupplyModelName = "" + rsp.powerSupplyManufacturer = "" + rsp.hvmCpuFlag = "" + rsp.cpuCache = "64.0,4096.0,16384.0" + rsp.nqn = "nqn.2014-08.org.nvmexpress:uuid:748d0363-8366-44db-803b-146effb96988" + rsp.hostname = "hostname" + rsp.iscsiInitiatorName = "iqn.1994-05.com.redhat:" + hostUuid.substring(0, 12) + rsp.virtualizerInfo = new VirtualizerInfoTO() + rsp.virtualizerInfo.version = "4.2.0-627.g36ee592.el7" + rsp.virtualizerInfo.virtualizer = "qemu-kvm" + return rsp + } + + spec.simulator(KVMConstant.KVM_VM_UPDATE_CPU_QUOTA_PATH) { + return new KVMAgentCommands.UpdateVmCpuQuotaRsp() + } + + spec.simulator(KVMConstant.KVM_VM_UPDATE_PRIORITY_PATH) { + return new KVMAgentCommands.UpdateVmPriorityRsp() + } + + spec.simulator(KVMConstant.KVM_VM_CHECK_STATE) { HttpEntity e -> + KVMAgentCommands.CheckVmStateCmd cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.CheckVmStateCmd.class) + List vms = Q.New(VmInstanceVO.class).in(VmInstanceVO_.uuid, cmd.vmUuids).list() + KVMAgentCommands.CheckVmStateRsp rsp = new KVMAgentCommands.CheckVmStateRsp() + rsp.states = [:] + vms.each { + def kstate = KVMConstant.KvmVmState.fromVmInstanceState(it.state) + if (kstate != null) { + rsp.states[(it.uuid)] = kstate.toString() + } else { + rsp.states[(it.uuid)] = KVMConstant.KvmVmState.Shutdown.toString() + } + } + + return rsp + } + + spec.simulator(KVMConstant.KVM_ATTACH_NIC_PATH) { HttpEntity e -> + KVMAgentCommands.AttachNicCommand cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.AttachNicCommand.class) + KVMAgentCommands.AttachNicResponse rsp = new KVMAgentCommands.AttachNicResponse() + VirtualDeviceInfo deviceInfo = new VirtualDeviceInfo() + deviceInfo.setResourceUuid(cmd.nic.getUuid()) + rsp.setVirtualDeviceInfoList(Arrays.asList(deviceInfo)) + return rsp + } + + spec.simulator(KVMConstant.KVM_DETACH_NIC_PATH) { + return new KVMAgentCommands.DetachNicRsp() + } + + spec.simulator(KVMConstant.KVM_UPDATE_NIC_PATH) { + return new KVMAgentCommands.UpdateNicRsp() + } + + spec.simulator(KVMConstant.KVM_ATTACH_ISO_PATH) { + return new KVMAgentCommands.AttachIsoRsp() + } + + spec.simulator(KVMConstant.KVM_DETACH_ISO_PATH) { + return new KVMAgentCommands.DetachIsoRsp() + } + + spec.simulator(KVMConstant.KVM_MERGE_SNAPSHOT_PATH) { HttpEntity e, EnvSpec espec -> + return new KVMAgentCommands.MergeSnapshotRsp() + } + + VFS.vfsHook(KVMConstant.KVM_MERGE_SNAPSHOT_PATH, spec) { rsp, HttpEntity e, EnvSpec espec -> + KVMAgentCommands.MergeSnapshotCmd cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.MergeSnapshotCmd.class) + VolumeInventory volume = null + String primaryStorageType = null + + new SQLBatch() { + @Override + protected void scripts() { + VolumeVO vol = sql("select vol from VolumeVO vol where vol.vmInstanceUuid = :vmUuid " + + " and vol.deviceId = :deviceId", VolumeVO.class) + .param("vmUuid", cmd.vmUuid) + .param("deviceId", cmd.volume.getDeviceId()) + .find() + + assert vol: "cannot find dest volume[path: ${cmd.destPath}, deviceId: ${cmd.volume.getDeviceId()}] of VM[uuid: ${cmd.vmUuid}] in database" + volume = vol.toInventory() + + primaryStorageType = q(PrimaryStorageVO.class).select(PrimaryStorageVO_.type).eq(PrimaryStorageVO_.uuid, vol.primaryStorageUuid).findValue() + assert primaryStorageType: "cannot find primary storage[uuid: ${vol.primaryStorageUuid}]" + } + }.execute() + + VFSPrimaryStorageTakeSnapshotBackend bkd = getVFSPrimaryStorageTakeSnapshotBackend(primaryStorageType) + bkd.mergeSnapshots(e, espec, cmd, volume) + } + + spec.simulator(KVMConstant.KVM_TAKE_VOLUME_SNAPSHOT_PATH) { HttpEntity e, EnvSpec espec -> + return new KVMAgentCommands.TakeSnapshotResponse() + } + + VFS.vfsHook(KVMConstant.KVM_TAKE_VOLUME_SNAPSHOT_PATH, spec) { rsp, HttpEntity e, EnvSpec espec -> + KVMAgentCommands.TakeSnapshotCmd cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.TakeSnapshotCmd.class) + + VolumeVO volume = Q.New(VolumeVO.class).eq(VolumeVO_.uuid, cmd.volumeUuid).find() + assert volume: "cannot find volume[uuid: ${cmd.volumeUuid}]" + String primaryStorageType = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.type) + .eq(PrimaryStorageVO_.uuid, volume.primaryStorageUuid).findValue() + assert primaryStorageType: "cannot find primary storage[uuid: ${volume.primaryStorageUuid}] from volume[uuid: ${volume.uuid}, name: ${volume.name}]" + VFSPrimaryStorageTakeSnapshotBackend bkd = getVFSPrimaryStorageTakeSnapshotBackend(primaryStorageType) + + VFSSnapshot snapshot = bkd.takeSnapshot(e, espec, cmd, volume.toInventory() as VolumeInventory) + rsp.newVolumeInstallPath = snapshot.installPath + rsp.snapshotInstallPath = cmd.volumeInstallPath + // size required greater than 0, if no mock value set keep size return 1 + rsp.size = snapshot.size == null || snapshot.size == 0 ? 1 : snapshot.size + return rsp + } + + spec.simulator(KVMConstant.KVM_PING_PATH) { HttpEntity e, EnvSpec espec -> + KVMAgentCommands.PingCmd cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.PingCmd.class) + assert null != cmd + assert null != cmd.hostUuid + + def rsp = new KVMAgentCommands.PingResponse() + rsp.hostUuid = cmd.hostUuid + rsp.version = connectCmdConcurrentHashMap.get(rsp.hostUuid).version + rsp.sendCommandUrl = connectCmdConcurrentHashMap.get(rsp.hostUuid).sendCommandUrl + return rsp + } + + spec.simulator(KVMConstant.KVM_UPDATE_HOST_CONFIGURATION_PATH) { HttpEntity e, EnvSpec espec -> + def rsp = new KVMAgentCommands.UpdateHostConfigurationResponse() + return rsp + } + spec.simulator(KVMConstant.KVM_CONNECT_PATH) { HttpEntity e -> Spec.checkHttpCallType(e, true) KVMAgentCommands.ConnectCmd cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.ConnectCmd.class) - - connectCmdConcurrentHashMap.put(cmd.hostUuid, cmd) - - def rsp = new KVMAgentCommands.ConnectResponse() - rsp.success = true - rsp.libvirtVersion = "1.0.0" + + connectCmdConcurrentHashMap.put(cmd.hostUuid, cmd) + + def rsp = new KVMAgentCommands.ConnectResponse() + rsp.success = true + rsp.libvirtVersion = "1.0.0" rsp.qemuVersion = "1.3.0" return rsp } @@ -363,380 +363,384 @@ class KVMSimulator implements Simulator { Spec.checkHttpCallType(e, true) return [:] } - - spec.simulator(KVMConstant.KVM_DETACH_VOLUME) { - return new KVMAgentCommands.DetachDataVolumeResponse() - } - - spec.simulator(KVMConstant.KVM_VM_SYNC_PATH) { HttpEntity e -> - def hostUuid = e.getHeaders().getFirst(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID) - - List states = Q.New(VmInstanceVO.class) - .select(VmInstanceVO_.uuid, VmInstanceVO_.state) - .in(VmInstanceVO_.state, [VmInstanceState.Running, VmInstanceState.Unknown]) - .eq(VmInstanceVO_.hostUuid, hostUuid).listTuple() - - def rsp = new KVMAgentCommands.VmSyncResponse() - rsp.states = [:] - states.each { - String vmUuid = it.get(0, String.class) - VmInstanceState state = it.get(1, VmInstanceState.class) - if (state == VmInstanceState.Unknown) { - // host reconnecting will set VMs to Unknown in DB - // the spec.simulator treat them as Running by default - rsp.states[(vmUuid)] = KVMConstant.KvmVmState.Running.toString() - } else { - rsp.states[(vmUuid)] = KVMConstant.KvmVmState.fromVmInstanceState(state).toString() - } - } - rsp.setVmInShutdowns(new ArrayList()) - return rsp - } - - spec.simulator(KVMConstant.KVM_VOLUME_SYNC_PATH) { - return new VolumeSyncRsp() - } - - spec.simulator(KVMConstant.KVM_ATTACH_VOLUME) { HttpEntity e -> - def cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.AttachDataVolumeCmd.class) - // assume all data volumes has same deviceType. - if (Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.type).listValues().stream().distinct().count() == 1) { - assert (cmd.addons["attachedDataVolumes"] as List).stream() - .allMatch({ vol -> vol.deviceType == cmd.volume.deviceType }) - } - return new KVMAgentCommands.AttachDataVolumeResponse() - } - - spec.simulator(KVMConstant.KVM_CHECK_PHYSICAL_NETWORK_INTERFACE_PATH) { HttpEntity e -> - Spec.checkHttpCallType(e, true) - return new KVMAgentCommands.CheckPhysicalNetworkInterfaceResponse() - } - - spec.simulator(KVMConstant.KVM_ADD_INTERFACE_TO_BRIDGE_PATH) { - return new KVMAgentCommands.AddInterfaceToBridgeResponse() - } - - spec.simulator(KVMConstant.KVM_REALIZE_L2NOVLAN_NETWORK_PATH) { - return new KVMAgentCommands.CreateBridgeResponse() - } - - spec.simulator(KVMConstant.KVM_MIGRATE_VM_PATH) { - return new KVMAgentCommands.MigrateVmResponse() - } - - spec.simulator(KVMConstant.KVM_GET_CPU_XML_PATH) { - return new KVMAgentCommands.VmGetCpuXmlResponse() - } - - spec.simulator(KVMConstant.KVM_COMPARE_CPU_FUNCTION_PATH) { - return new KVMAgentCommands.VmCompareCpuFunctionResponse() - } - - spec.simulator(KVMConstant.KVM_UPDATE_L2VLAN_NETWORK_PATH) { - return new KVMAgentCommands.UpdateL2NetworkResponse() - } - - spec.simulator(KVMConstant.KVM_UPDATE_L2VXLAN_NETWORK_PATH) { - return new KVMAgentCommands.UpdateL2NetworkResponse() - } - - spec.simulator(KVMConstant.KVM_CHECK_L2NOVLAN_NETWORK_PATH) { - return new KVMAgentCommands.CheckBridgeResponse() - } - - spec.simulator(KVMConstant.KVM_CHECK_L2VLAN_NETWORK_PATH) { - return new KVMAgentCommands.CheckVlanBridgeResponse() - } - - spec.simulator(KVMConstant.KVM_REALIZE_L2VLAN_NETWORK_PATH) { - return new KVMAgentCommands.CreateVlanBridgeResponse() - } - - spec.simulator(KVMConstant.KVM_CHECK_OVSDPDK_NETWORK_PATH) { - return new KVMAgentCommands.AgentResponse() - } - - spec.simulator(KVMConstant.KVM_REALIZE_OVSDPDK_NETWORK_PATH) { - return new KVMAgentCommands.CreateBridgeResponse() - } - - spec.simulator(KVMConstant.KVM_GENERATE_VDPA_PATH) { HttpEntity e -> - KVMAgentCommands.GenerateVdpaCmd cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.GenerateVdpaCmd.class) - def rsp = new KVMAgentCommands.GenerateVdpaResponse() - - ArrayList nics = cmd.getNics() - def vdpaPaths = new ArrayList() - - nics.each { KVMAgentCommands.NicTO it -> - vdpaPaths.add("/var/run/zstack-vdpa/" + it.bridgeName + "/" + it.nicInternalName) - } - rsp.setVdpaPaths(vdpaPaths) - - return rsp - } - - spec.simulator(KVMConstant.KVM_DELETE_VDPA_PATH) { - return new KVMAgentCommands.AgentResponse() - } - - spec.simulator(KVMConstant.KVM_GENERATE_VHOST_USER_CLIENT_PATH) { - return new KVMAgentCommands.AgentResponse() - } - - spec.simulator(KVMConstant.KVM_DELETE_VHOST_USER_CLIENT_PATH) { - return new KVMAgentCommands.AgentResponse() - } - - spec.simulator(KVMConstant.KVM_SYNC_VM_DEVICEINFO_PATH) { HttpEntity e -> - SyncVmDeviceInfoCmd cmd = JSONObjectUtil.toObject(e.body, SyncVmDeviceInfoCmd.class) - def rsp = new SyncVmDeviceInfoResponse() - - rsp.virtualizerInfo = new VirtualizerInfoTO() - rsp.virtualizerInfo.uuid = cmd.vmInstanceUuid - rsp.virtualizerInfo.virtualizer = "qemu-kvm" - rsp.virtualizerInfo.version = "4.2.0-632.g6a6222b.el7" - - return rsp - } - - spec.simulator(KVMConstant.KVM_START_VM_PATH) { HttpEntity e -> - StartVmCmd cmd = JSONObjectUtil.toObject(e.body, StartVmCmd.class) - assert new HashSet<>(cmd.dataVolumes.deviceId).size() == cmd.dataVolumes.size() - StartVmResponse rsp = new StartVmResponse() - rsp.virtualDeviceInfoList = [] - List pciInfo = new ArrayList() - pciInfo.add(cmd.rootVolume) - pciInfo.addAll(cmd.dataVolumes) - - Integer counter = 0 - pciInfo.each { to -> - VirtualDeviceInfo info = new VirtualDeviceInfo() - info.resourceUuid = to.volumeUuid - info.deviceAddress = new DeviceAddress() - info.deviceAddress.domain = "0000" - info.deviceAddress.bus = "00" - info.deviceAddress.slot = Integer.toHexString(counter) - info.deviceAddress.function = "0" - - counter++ - - rsp.virtualDeviceInfoList.add(info) - } - - rsp.virtualizerInfo = new VirtualizerInfoTO() - 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 - } - - spec.simulator(KVMConstant.KVM_STOP_VM_PATH) { - return new KVMAgentCommands.StopVmResponse() - } - - spec.simulator(KVMConstant.KVM_PAUSE_VM_PATH) { - return new KVMAgentCommands.PauseVmResponse() - } - - spec.simulator(KVMConstant.KVM_RESUME_VM_PATH) { - return new KVMAgentCommands.ResumeVmResponse() - } - - spec.simulator(KVMConstant.KVM_REBOOT_VM_PATH) { - return new KVMAgentCommands.RebootVmResponse() - } - - spec.simulator(KVMConstant.KVM_DESTROY_VM_PATH) { - return new KVMAgentCommands.DestroyVmResponse() - } - - spec.simulator(KVMConstant.KVM_GET_VNC_PORT_PATH) { - def rsp = new KVMAgentCommands.GetVncPortResponse() - rsp.port = 5900 - return rsp - } - - spec.simulator(KVMConstant.KVM_LOGOUT_ISCSI_PATH) { - return new KVMAgentCommands.LogoutIscsiTargetRsp() - } - - spec.simulator(KVMConstant.KVM_LOGIN_ISCSI_PATH) { - return new KVMAgentCommands.LoginIscsiTargetRsp() - } - - spec.simulator(KVMConstant.KVM_VM_ONLINE_INCREASE_CPU) { - return new KVMAgentCommands.IncreaseCpuResponse() - } - - spec.simulator(KVMConstant.KVM_VM_ONLINE_INCREASE_MEMORY) { - return new KVMAgentCommands.IncreaseMemoryResponse() - } - - spec.simulator(KVMConstant.KVM_UPDATE_HOST_OS_PATH) { - return new KVMAgentCommands.UpdateHostOSRsp() - } - - spec.simulator(KVMConstant.KVM_HOST_UPDATE_DEPENDENCY_PATH) { - return new KVMAgentCommands.UpdateDependencyRsp() - } - - spec.simulator(KVMConstant.HOST_UPDATE_SPICE_CHANNEL_CONFIG_PATH) { - return new KVMAgentCommands.UpdateSpiceChannelConfigResponse() - } - - spec.simulator(KVMConstant.KVM_SCAN_VM_PORT_STATUS) { - return new KVMAgentCommands.ScanVmPortResponse() - } - - spec.simulator(KVMConstant.GET_DEV_CAPACITY) { - KVMAgentCommands.GetDevCapacityResponse rsp = new KVMAgentCommands.GetDevCapacityResponse() - rsp.totalSize = SizeUnit.GIGABYTE.toByte(100) - rsp.availableSize = SizeUnit.GIGABYTE.toByte(80) - return rsp - } - - spec.simulator(KVMConstant.KVM_CONFIG_PRIMARY_VM_PATH) { - return new KVMAgentCommands.AgentResponse() - } - - spec.simulator(KVMConstant.KVM_CONFIG_SECONDARY_VM_PATH) { - return new KVMAgentCommands.AgentResponse() - } - - spec.simulator(KVMConstant.KVM_START_COLO_SYNC_PATH) { - return new KVMAgentCommands.AgentResponse() - } - - spec.simulator(KVMConstant.KVM_REGISTER_PRIMARY_VM_HEARTBEAT) { - return new KVMAgentCommands.AgentResponse() - } - - spec.simulator(KVMConstant.KVM_HOST_CHECK_FILE_PATH) { HttpEntity e -> - KVMAgentCommands.CheckFileOnHostCmd cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.CheckFileOnHostCmd.class) - KVMAgentCommands.CheckFileOnHostResponse response = new KVMAgentCommands.CheckFileOnHostResponse() - response.existPaths = new HashMap<>() - cmd.paths.forEach({ path -> response.existPaths.put(path, "") }) - return response - } - - spec.simulator(KVMConstant.KVM_HOST_NUMA_PATH) { - def rsp = new KVMAgentCommands.GetHostNUMATopologyResponse() - return rsp - } - - spec.simulator(KVMConstant.KVM_HOST_ATTACH_VOLUME_PATH) { - def rsp = new KVMAgentCommands.AttachVolumeRsp() - rsp.device = "/dev/nbd0" - return rsp - } - - spec.simulator(KVMConstant.KVM_HOST_DETACH_VOLUME_PATH) { - def rsp = new KVMAgentCommands.DetachVolumeRsp() - return rsp - } - - spec.simulator(KVMConstant.KVM_BLOCK_COMMIT_VOLUME_PATH) { HttpEntity e -> - def rsp = new BlockCommitResponse() - rsp.size = 1 - return rsp - } - - VFS.vfsHook(KVMConstant.KVM_BLOCK_COMMIT_VOLUME_PATH, spec) { rsp, HttpEntity e, EnvSpec espec -> - BlockCommitCmd cmd = JSONObjectUtil.toObject(e.body, BlockCommitCmd.class) - - VolumeVO volume = Q.New(VolumeVO.class).eq(VolumeVO_.uuid, cmd.volume.getVolumeUuid()).find() - assert volume: "cannot find volume[uuid: ${cmd.volume.getVolumeUuid()}]" - - String primaryStorageType = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.type) - .eq(PrimaryStorageVO_.uuid, volume.primaryStorageUuid).findValue() - assert primaryStorageType: "cannot find primary storage[uuid: ${volume.primaryStorageUuid}] " + - "from volume[uuid: ${volume.uuid}, name: ${volume.name}]" - - VFSPrimaryStorageTakeSnapshotBackend bkd = getVFSPrimaryStorageTakeSnapshotBackend(primaryStorageType) - bkd.blockCommit(e, espec, cmd, volume.toInventory() as VolumeInventory) - return rsp - } - - spec.simulator(KVMConstant.KVM_BLOCK_PULL_VOLUME_PATH) { HttpEntity e -> - def rsp = new BlockPullResponse() - rsp.size = 1 - return rsp - } - - VFS.vfsHook(KVMConstant.KVM_BLOCK_PULL_VOLUME_PATH, spec) { BlockPullResponse rsp, HttpEntity e, EnvSpec espec -> - BlockPullCmd cmd = JSONObjectUtil.toObject(e.body, BlockPullCmd.class) - - VolumeVO volume = Q.New(VolumeVO.class).eq(VolumeVO_.uuid, cmd.volume.getVolumeUuid()).find() - assert volume: "cannot find volume[uuid: ${cmd.getVolume().getVolumeUuid()}]" - - String primaryStorageType = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.type) - .eq(PrimaryStorageVO_.uuid, volume.primaryStorageUuid).findValue() - assert primaryStorageType: "cannot find primary storage[uuid: ${volume.primaryStorageUuid}] " + - "from volume[uuid: ${volume.uuid}, name: ${volume.name}]" - - VFSPrimaryStorageTakeSnapshotBackend bkd = getVFSPrimaryStorageTakeSnapshotBackend(primaryStorageType) - - Qcow2 newInstallPath = bkd.blockPull(e, espec, cmd, volume.toInventory() as VolumeInventory) - rsp.size = newInstallPath.actualSize == 0 ? 1 : newInstallPath.actualSize - return rsp - } - - spec.simulator(KVMConstant.TAKE_VM_CONSOLE_SCREENSHOT_PATH) { - def rsp = new KVMAgentCommands.TakeVmConsoleScreenshotRsp() - rsp.imageData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAABECAIAAADFvmZTAAACA0lEQVR4nO3VMYviQBQH8Hkvk5hE2ZUEU+juFlbKauH3/wYWNhYWKiiIRBDcZDckJJmZK6Zb7q44uJW7/f+qCcOQzP89XoSAL0FPT09VVTFzEARhGKZp2rat67qO4xhjlFJ5nidJcr1eHx8fPc+r65qZ27Zt2zbP83t//z9DRlFUVRURGWN83/c8z/O8Xq/HzFEUvb29aa211sxsgxZCKKWIKMuyPM9tSWypHMdRSmmtP72DmTudTlmWRCSl1Forpe5x2XuSSZJcLpeiKPr9flEUdV0LIZqm+fj4GI/H+/1+Npsdj8fX11chRFVV5/N5MBgIIZg5juP5fN7tdpfL5WAwiKJovV4Ph8MgCDabzWQyKcvycDgsFoskSVarlZTy+fl5t9udz2db2jvf/gsRETGz1tp13bZtbT8yszHGdqiNw66Z2W4ZY7TWUkqbl5TSGGOPSymFEHb+2Oa1u47jEFHTNFrrbxWxRQ8PD1LKuq7Lsux2u8zcNE1d10EQFEXR6XRc1yWiPM9937cxua6rlPrplIBfkS8vL2EYZlmWpul0OvV9/3Q6vb+/j0aj4/EYx3EYhkqp7XY7HA6TJDHG3G43IjocDnbsfsP2/OuIyP4P4U8Q0W8eAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgf/QDviYImvcayGAAAAAASUVORK5CYII=" - return rsp - } - - spec.simulator(KVMConstant.KVM_UPDATE_HOST_NQN_PATH) { - def rsp = new KVMAgentCommands.UpdateHostNqnRsp() - return rsp - } - - spec.simulator(KVMConstant.KVM_HOST_FILE_DOWNLOAD_PATH) { - DownloadFileResponse rsp = new DownloadFileResponse() - rsp.md5sum = "00df1327d49e4631a21f4467aa729c11" - rsp.size = 1024 - return rsp - } - - spec.simulator(KVMConstant.KVM_HOST_FILE_UPLOAD_PATH) { - UploadFileResponse rsp = new UploadFileResponse() - rsp.directUploadPath = "http://172.1.1.1:7070/host/file/direct-upload" - return rsp - } - - spec.simulator(KVMConstant.KVM_HOST_FILE_DOWNLOAD_PROGRESS_PATH) { - GetDownloadFileProgressResponse rsp = new GetDownloadFileProgressResponse() - rsp.completed = false - rsp.downloadSize = 1 - rsp.size = 1024 - rsp.lastOpTime = System.currentTimeMillis() - return rsp - } - - 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() - } - } -} + + spec.simulator(KVMConstant.KVM_DETACH_VOLUME) { + return new KVMAgentCommands.DetachDataVolumeResponse() + } + + spec.simulator(KVMConstant.KVM_VM_SYNC_PATH) { HttpEntity e -> + def hostUuid = e.getHeaders().getFirst(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID) + + List states = Q.New(VmInstanceVO.class) + .select(VmInstanceVO_.uuid, VmInstanceVO_.state) + .in(VmInstanceVO_.state, [VmInstanceState.Running, VmInstanceState.Unknown]) + .eq(VmInstanceVO_.hostUuid, hostUuid).listTuple() + + def rsp = new KVMAgentCommands.VmSyncResponse() + rsp.states = [:] + states.each { + String vmUuid = it.get(0, String.class) + VmInstanceState state = it.get(1, VmInstanceState.class) + if (state == VmInstanceState.Unknown) { + // host reconnecting will set VMs to Unknown in DB + // the spec.simulator treat them as Running by default + rsp.states[(vmUuid)] = KVMConstant.KvmVmState.Running.toString() + } else { + rsp.states[(vmUuid)] = KVMConstant.KvmVmState.fromVmInstanceState(state).toString() + } + } + rsp.setVmInShutdowns(new ArrayList()) + return rsp + } + + spec.simulator(KVMConstant.KVM_VOLUME_SYNC_PATH) { + return new VolumeSyncRsp() + } + + spec.simulator(KVMConstant.KVM_ATTACH_VOLUME) { HttpEntity e -> + def cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.AttachDataVolumeCmd.class) + // assume all data volumes has same deviceType. + if (Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.type).listValues().stream().distinct().count() == 1) { + assert (cmd.addons["attachedDataVolumes"] as List).stream() + .allMatch({ vol -> vol.deviceType == cmd.volume.deviceType }) + } + return new KVMAgentCommands.AttachDataVolumeResponse() + } + + spec.simulator(KVMConstant.KVM_CHECK_PHYSICAL_NETWORK_INTERFACE_PATH) { HttpEntity e -> + Spec.checkHttpCallType(e, true) + return new KVMAgentCommands.CheckPhysicalNetworkInterfaceResponse() + } + + spec.simulator(KVMConstant.KVM_ADD_INTERFACE_TO_BRIDGE_PATH) { + return new KVMAgentCommands.AddInterfaceToBridgeResponse() + } + + spec.simulator(KVMConstant.KVM_REALIZE_L2NOVLAN_NETWORK_PATH) { + return new KVMAgentCommands.CreateBridgeResponse() + } + + spec.simulator(KVMConstant.KVM_MIGRATE_VM_PATH) { + return new KVMAgentCommands.MigrateVmResponse() + } + + spec.simulator(KVMConstant.KVM_GET_CPU_XML_PATH) { + return new KVMAgentCommands.VmGetCpuXmlResponse() + } + + spec.simulator(KVMConstant.KVM_COMPARE_CPU_FUNCTION_PATH) { + return new KVMAgentCommands.VmCompareCpuFunctionResponse() + } + + spec.simulator(KVMConstant.KVM_UPDATE_L2VLAN_NETWORK_PATH) { + return new KVMAgentCommands.UpdateL2NetworkResponse() + } + + spec.simulator(KVMConstant.KVM_UPDATE_L2VXLAN_NETWORK_PATH) { + return new KVMAgentCommands.UpdateL2NetworkResponse() + } + + spec.simulator(KVMConstant.KVM_CHECK_L2NOVLAN_NETWORK_PATH) { + return new KVMAgentCommands.CheckBridgeResponse() + } + + spec.simulator(KVMConstant.KVM_CHECK_L2VLAN_NETWORK_PATH) { + return new KVMAgentCommands.CheckVlanBridgeResponse() + } + + spec.simulator(KVMConstant.KVM_REALIZE_L2VLAN_NETWORK_PATH) { + return new KVMAgentCommands.CreateVlanBridgeResponse() + } + + spec.simulator(KVMConstant.KVM_CHECK_OVSDPDK_NETWORK_PATH) { + return new KVMAgentCommands.AgentResponse() + } + + spec.simulator(KVMConstant.KVM_REALIZE_OVSDPDK_NETWORK_PATH) { + return new KVMAgentCommands.CreateBridgeResponse() + } + + spec.simulator(KVMConstant.KVM_GENERATE_VDPA_PATH) { HttpEntity e -> + KVMAgentCommands.GenerateVdpaCmd cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.GenerateVdpaCmd.class) + def rsp = new KVMAgentCommands.GenerateVdpaResponse() + + ArrayList nics = cmd.getNics() + def vdpaPaths = new ArrayList() + + nics.each { KVMAgentCommands.NicTO it -> + vdpaPaths.add("/var/run/zstack-vdpa/" + it.bridgeName + "/" + it.nicInternalName) + } + rsp.setVdpaPaths(vdpaPaths) + + return rsp + } + + spec.simulator(KVMConstant.KVM_DELETE_VDPA_PATH) { + return new KVMAgentCommands.AgentResponse() + } + + spec.simulator(KVMConstant.KVM_GENERATE_VHOST_USER_CLIENT_PATH) { + return new KVMAgentCommands.AgentResponse() + } + + spec.simulator(KVMConstant.KVM_DELETE_VHOST_USER_CLIENT_PATH) { + return new KVMAgentCommands.AgentResponse() + } + + spec.simulator(KVMConstant.KVM_SYNC_VM_DEVICEINFO_PATH) { HttpEntity e -> + SyncVmDeviceInfoCmd cmd = JSONObjectUtil.toObject(e.body, SyncVmDeviceInfoCmd.class) + def rsp = new SyncVmDeviceInfoResponse() + + rsp.virtualizerInfo = new VirtualizerInfoTO() + rsp.virtualizerInfo.uuid = cmd.vmInstanceUuid + rsp.virtualizerInfo.virtualizer = "qemu-kvm" + rsp.virtualizerInfo.version = "4.2.0-632.g6a6222b.el7" + + return rsp + } + + spec.simulator(KVMConstant.KVM_START_VM_PATH) { HttpEntity e -> + StartVmCmd cmd = JSONObjectUtil.toObject(e.body, StartVmCmd.class) + assert new HashSet<>(cmd.dataVolumes.deviceId).size() == cmd.dataVolumes.size() + StartVmResponse rsp = new StartVmResponse() + rsp.virtualDeviceInfoList = [] + List pciInfo = new ArrayList() + pciInfo.add(cmd.rootVolume) + pciInfo.addAll(cmd.dataVolumes) + + Integer counter = 0 + pciInfo.each { to -> + VirtualDeviceInfo info = new VirtualDeviceInfo() + info.resourceUuid = to.volumeUuid + info.deviceAddress = new DeviceAddress() + info.deviceAddress.domain = "0000" + info.deviceAddress.bus = "00" + info.deviceAddress.slot = Integer.toHexString(counter) + info.deviceAddress.function = "0" + + counter++ + + rsp.virtualDeviceInfoList.add(info) + } + + rsp.virtualizerInfo = new VirtualizerInfoTO() + 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 + } + + spec.simulator(KVMConstant.KVM_STOP_VM_PATH) { + return new KVMAgentCommands.StopVmResponse() + } + + spec.simulator(KVMConstant.KVM_PAUSE_VM_PATH) { + return new KVMAgentCommands.PauseVmResponse() + } + + spec.simulator(KVMConstant.KVM_RESUME_VM_PATH) { + return new KVMAgentCommands.ResumeVmResponse() + } + + spec.simulator(KVMConstant.KVM_REBOOT_VM_PATH) { + return new KVMAgentCommands.RebootVmResponse() + } + + spec.simulator(KVMConstant.KVM_DESTROY_VM_PATH) { + return new KVMAgentCommands.DestroyVmResponse() + } + + spec.simulator(KVMConstant.KVM_GET_VNC_PORT_PATH) { + def rsp = new KVMAgentCommands.GetVncPortResponse() + rsp.port = 5900 + return rsp + } + + spec.simulator(KVMConstant.KVM_LOGOUT_ISCSI_PATH) { + return new KVMAgentCommands.LogoutIscsiTargetRsp() + } + + spec.simulator(KVMConstant.KVM_LOGIN_ISCSI_PATH) { + return new KVMAgentCommands.LoginIscsiTargetRsp() + } + + spec.simulator(KVMConstant.KVM_VM_ONLINE_INCREASE_CPU) { + return new KVMAgentCommands.IncreaseCpuResponse() + } + + spec.simulator(KVMConstant.KVM_VM_ONLINE_INCREASE_MEMORY) { + return new KVMAgentCommands.IncreaseMemoryResponse() + } + + spec.simulator(KVMConstant.KVM_UPDATE_HOST_OS_PATH) { + return new KVMAgentCommands.UpdateHostOSRsp() + } + + spec.simulator(KVMConstant.KVM_HOST_UPDATE_DEPENDENCY_PATH) { + return new KVMAgentCommands.UpdateDependencyRsp() + } + + spec.simulator(KVMConstant.HOST_UPDATE_SPICE_CHANNEL_CONFIG_PATH) { + return new KVMAgentCommands.UpdateSpiceChannelConfigResponse() + } + + spec.simulator(KVMConstant.KVM_SCAN_VM_PORT_STATUS) { + return new KVMAgentCommands.ScanVmPortResponse() + } + + spec.simulator(KVMConstant.GET_DEV_CAPACITY) { + KVMAgentCommands.GetDevCapacityResponse rsp = new KVMAgentCommands.GetDevCapacityResponse() + rsp.totalSize = SizeUnit.GIGABYTE.toByte(100) + rsp.availableSize = SizeUnit.GIGABYTE.toByte(80) + return rsp + } + + spec.simulator(KVMConstant.KVM_CONFIG_PRIMARY_VM_PATH) { + return new KVMAgentCommands.AgentResponse() + } + + spec.simulator(KVMConstant.KVM_CONFIG_SECONDARY_VM_PATH) { + return new KVMAgentCommands.AgentResponse() + } + + spec.simulator(KVMConstant.KVM_START_COLO_SYNC_PATH) { + return new KVMAgentCommands.AgentResponse() + } + + spec.simulator(KVMConstant.KVM_REGISTER_PRIMARY_VM_HEARTBEAT) { + return new KVMAgentCommands.AgentResponse() + } + + spec.simulator(KVMConstant.KVM_HOST_CHECK_FILE_PATH) { HttpEntity e -> + KVMAgentCommands.CheckFileOnHostCmd cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.CheckFileOnHostCmd.class) + KVMAgentCommands.CheckFileOnHostResponse response = new KVMAgentCommands.CheckFileOnHostResponse() + response.existPaths = new HashMap<>() + cmd.paths.forEach({ path -> response.existPaths.put(path, "") }) + return response + } + + spec.simulator(KVMConstant.KVM_HOST_NUMA_PATH) { + def rsp = new KVMAgentCommands.GetHostNUMATopologyResponse() + return rsp + } + + spec.simulator(KVMConstant.KVM_HOST_ATTACH_VOLUME_PATH) { + def rsp = new KVMAgentCommands.AttachVolumeRsp() + rsp.device = "/dev/nbd0" + return rsp + } + + spec.simulator(KVMConstant.KVM_HOST_DETACH_VOLUME_PATH) { + def rsp = new KVMAgentCommands.DetachVolumeRsp() + return rsp + } + + spec.simulator(KVMConstant.KVM_BLOCK_COMMIT_VOLUME_PATH) { HttpEntity e -> + def rsp = new BlockCommitResponse() + rsp.size = 1 + return rsp + } + + VFS.vfsHook(KVMConstant.KVM_BLOCK_COMMIT_VOLUME_PATH, spec) { rsp, HttpEntity e, EnvSpec espec -> + BlockCommitCmd cmd = JSONObjectUtil.toObject(e.body, BlockCommitCmd.class) + + VolumeVO volume = Q.New(VolumeVO.class).eq(VolumeVO_.uuid, cmd.volume.getVolumeUuid()).find() + assert volume: "cannot find volume[uuid: ${cmd.volume.getVolumeUuid()}]" + + String primaryStorageType = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.type) + .eq(PrimaryStorageVO_.uuid, volume.primaryStorageUuid).findValue() + assert primaryStorageType: "cannot find primary storage[uuid: ${volume.primaryStorageUuid}] " + + "from volume[uuid: ${volume.uuid}, name: ${volume.name}]" + + VFSPrimaryStorageTakeSnapshotBackend bkd = getVFSPrimaryStorageTakeSnapshotBackend(primaryStorageType) + bkd.blockCommit(e, espec, cmd, volume.toInventory() as VolumeInventory) + return rsp + } + + spec.simulator(KVMConstant.KVM_BLOCK_PULL_VOLUME_PATH) { HttpEntity e -> + def rsp = new BlockPullResponse() + rsp.size = 1 + return rsp + } + + VFS.vfsHook(KVMConstant.KVM_BLOCK_PULL_VOLUME_PATH, spec) { BlockPullResponse rsp, HttpEntity e, EnvSpec espec -> + BlockPullCmd cmd = JSONObjectUtil.toObject(e.body, BlockPullCmd.class) + + VolumeVO volume = Q.New(VolumeVO.class).eq(VolumeVO_.uuid, cmd.volume.getVolumeUuid()).find() + assert volume: "cannot find volume[uuid: ${cmd.getVolume().getVolumeUuid()}]" + + String primaryStorageType = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.type) + .eq(PrimaryStorageVO_.uuid, volume.primaryStorageUuid).findValue() + assert primaryStorageType: "cannot find primary storage[uuid: ${volume.primaryStorageUuid}] " + + "from volume[uuid: ${volume.uuid}, name: ${volume.name}]" + + VFSPrimaryStorageTakeSnapshotBackend bkd = getVFSPrimaryStorageTakeSnapshotBackend(primaryStorageType) + + Qcow2 newInstallPath = bkd.blockPull(e, espec, cmd, volume.toInventory() as VolumeInventory) + rsp.size = newInstallPath.actualSize == 0 ? 1 : newInstallPath.actualSize + return rsp + } + + spec.simulator(KVMConstant.TAKE_VM_CONSOLE_SCREENSHOT_PATH) { + def rsp = new KVMAgentCommands.TakeVmConsoleScreenshotRsp() + rsp.imageData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAABECAIAAADFvmZTAAACA0lEQVR4nO3VMYviQBQH8Hkvk5hE2ZUEU+juFlbKauH3/wYWNhYWKiiIRBDcZDckJJmZK6Zb7q44uJW7/f+qCcOQzP89XoSAL0FPT09VVTFzEARhGKZp2rat67qO4xhjlFJ5nidJcr1eHx8fPc+r65qZ27Zt2zbP83t//z9DRlFUVRURGWN83/c8z/O8Xq/HzFEUvb29aa211sxsgxZCKKWIKMuyPM9tSWypHMdRSmmtP72DmTudTlmWRCSl1Forpe5x2XuSSZJcLpeiKPr9flEUdV0LIZqm+fj4GI/H+/1+Npsdj8fX11chRFVV5/N5MBgIIZg5juP5fN7tdpfL5WAwiKJovV4Ph8MgCDabzWQyKcvycDgsFoskSVarlZTy+fl5t9udz2db2jvf/gsRETGz1tp13bZtbT8yszHGdqiNw66Z2W4ZY7TWUkqbl5TSGGOPSymFEHb+2Oa1u47jEFHTNFrrbxWxRQ8PD1LKuq7Lsux2u8zcNE1d10EQFEXR6XRc1yWiPM9937cxua6rlPrplIBfkS8vL2EYZlmWpul0OvV9/3Q6vb+/j0aj4/EYx3EYhkqp7XY7HA6TJDHG3G43IjocDnbsfsP2/OuIyP4P4U8Q0W8eAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgf/QDviYImvcayGAAAAAASUVORK5CYII=" + return rsp + } + + spec.simulator(KVMConstant.KVM_UPDATE_HOST_NQN_PATH) { + def rsp = new KVMAgentCommands.UpdateHostNqnRsp() + return rsp + } + + spec.simulator(KVMConstant.KVM_HOST_FILE_DOWNLOAD_PATH) { + DownloadFileResponse rsp = new DownloadFileResponse() + rsp.md5sum = "00df1327d49e4631a21f4467aa729c11" + rsp.size = 1024 + return rsp + } + + spec.simulator(KVMConstant.KVM_HOST_FILE_UPLOAD_PATH) { + UploadFileResponse rsp = new UploadFileResponse() + rsp.directUploadPath = "http://172.1.1.1:7070/host/file/direct-upload" + return rsp + } + + spec.simulator(KVMConstant.KVM_HOST_FILE_DOWNLOAD_PROGRESS_PATH) { + GetDownloadFileProgressResponse rsp = new GetDownloadFileProgressResponse() + rsp.completed = false + rsp.downloadSize = 1 + rsp.size = 1024 + rsp.lastOpTime = System.currentTimeMillis() + return rsp + } + + 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() + } + + spec.simulator(KVMConstant.BACKUP_VM_HOST_FILE_PATH) { HttpEntity e -> + return new BackupVmHostFileResponse() + } + } +} From a6ac23f4381b5fa1dbaa60bd473a00b2e338bcff Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 31 Mar 2026 11:34:36 +0800 Subject: [PATCH 091/129] [kvm]: fix invalid edk version tag error Related: ZSV-11310 Change-Id: I737178716776676c766e7164736b6679756c7065 --- .../src/main/java/org/zstack/kvm/BootKvmStartVmExtension.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/BootKvmStartVmExtension.java b/plugin/kvm/src/main/java/org/zstack/kvm/BootKvmStartVmExtension.java index 9da3a907662..41ab1f49b51 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/BootKvmStartVmExtension.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/BootKvmStartVmExtension.java @@ -1,5 +1,6 @@ package org.zstack.kvm; +import org.apache.commons.lang.StringUtils; import org.zstack.compute.vm.VmSystemTags; import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.vm.VmInstanceInventory; @@ -47,7 +48,7 @@ public void afterReceiveVmDeviceInfoResponse(VmInstanceInventory vm, KVMAgentCom @SuppressWarnings("unchecked") private void saveVmEdkStatesFromCommand(String vmUuid, KVMAgentCommands.VmDevicesInfoResponse rsp) { - if (rsp.getEdkRpm() == null) { + if (StringUtils.isEmpty(rsp.getEdkRpm())) { VM_EDK.deleteInherentTag(vmUuid); return; } From cbd001ca091ec44ba79e6dfa499a9559a460393b Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 31 Mar 2026 16:43:04 +0800 Subject: [PATCH 092/129] [kvm]: reorganize vm host file management packages Move common vm host file classes from efi to vmfiles package Move message classes to message subdirectories Update all import statements to reference new package paths Update Spring configuration files with new class locations Update test configuration files to match new package paths Related: ZSV-11310 Change-Id: I7362626862666d717265766979786b687270776d --- conf/springConfigXml/Kvm.xml | 2 +- .../java/org/zstack/kvm/efi/KvmSecureBootExtensions.java | 1 + .../java/org/zstack/kvm/efi/KvmSecureBootManager.java | 8 ++++++++ .../java/org/zstack/kvm/efi/KvmVmHostFileFactory.java | 2 ++ .../org/zstack/kvm/efi/NvRamVmHostBackupFileBase.java | 1 + .../main/java/org/zstack/kvm/efi/NvRamVmHostFileBase.java | 1 + .../src/main/java/org/zstack/kvm/tpm/KvmTpmManager.java | 2 ++ .../org/zstack/kvm/tpm/TpmStateVmHostBackupFileBase.java | 2 +- .../java/org/zstack/kvm/tpm/TpmStateVmHostFileBase.java | 2 +- .../org/zstack/kvm/tpm/{ => message}/CloneVmTpmMsg.java | 2 +- .../org/zstack/kvm/tpm/{ => message}/CloneVmTpmReply.java | 2 +- .../{efi => vmfiles}/AbstractVmHostBackupFileBase.java | 2 +- .../kvm/{efi => vmfiles}/AbstractVmHostFileBase.java | 2 +- .../zstack/kvm/{efi => vmfiles}/VmHostFileTracker.java | 3 ++- .../kvm/{efi => vmfiles/message}/BackupVmHostFileMsg.java | 2 +- .../message}/BackupVmHostFileOnHypervisorMsg.java | 2 +- .../message}/BackupVmHostFileOnHypervisorReply.java | 2 +- .../{efi => vmfiles/message}/BackupVmHostFileReply.java | 2 +- .../kvm/{efi => vmfiles/message}/CloneVmHostFileMsg.java | 2 +- .../{efi => vmfiles/message}/CloneVmHostFileReply.java | 2 +- .../message}/SyncVmHostFilesFromHostMsg.java | 2 +- .../message}/SyncVmHostFilesFromHostReply.java | 2 +- test/src/test/resources/springConfigXml/Kvm.xml | 2 +- 23 files changed, 33 insertions(+), 17 deletions(-) rename plugin/kvm/src/main/java/org/zstack/kvm/tpm/{ => message}/CloneVmTpmMsg.java (95%) rename plugin/kvm/src/main/java/org/zstack/kvm/tpm/{ => message}/CloneVmTpmReply.java (92%) rename plugin/kvm/src/main/java/org/zstack/kvm/{efi => vmfiles}/AbstractVmHostBackupFileBase.java (96%) rename plugin/kvm/src/main/java/org/zstack/kvm/{efi => vmfiles}/AbstractVmHostFileBase.java (94%) rename plugin/kvm/src/main/java/org/zstack/kvm/{efi => vmfiles}/VmHostFileTracker.java (98%) rename plugin/kvm/src/main/java/org/zstack/kvm/{efi => vmfiles/message}/BackupVmHostFileMsg.java (95%) rename plugin/kvm/src/main/java/org/zstack/kvm/{efi => vmfiles/message}/BackupVmHostFileOnHypervisorMsg.java (95%) rename plugin/kvm/src/main/java/org/zstack/kvm/{efi => vmfiles/message}/BackupVmHostFileOnHypervisorReply.java (75%) rename plugin/kvm/src/main/java/org/zstack/kvm/{efi => vmfiles/message}/BackupVmHostFileReply.java (90%) rename plugin/kvm/src/main/java/org/zstack/kvm/{efi => vmfiles/message}/CloneVmHostFileMsg.java (94%) rename plugin/kvm/src/main/java/org/zstack/kvm/{efi => vmfiles/message}/CloneVmHostFileReply.java (72%) rename plugin/kvm/src/main/java/org/zstack/kvm/{efi => vmfiles/message}/SyncVmHostFilesFromHostMsg.java (97%) rename plugin/kvm/src/main/java/org/zstack/kvm/{efi => vmfiles/message}/SyncVmHostFilesFromHostReply.java (74%) diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index 42d903ba3d1..13625bb3ae2 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -279,7 +279,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 c242eb3eef8..46c02aa856e 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 @@ -62,6 +62,7 @@ import org.zstack.kvm.KvmCommandSender; import org.zstack.kvm.KvmResponseWrapper; import org.zstack.kvm.VolumeTO; +import org.zstack.kvm.vmfiles.message.SyncVmHostFilesFromHostMsg; import org.zstack.resourceconfig.ResourceConfig; import org.zstack.resourceconfig.ResourceConfigFacade; import org.zstack.utils.Utils; 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 6d7a3758293..68dc9379c60 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 @@ -47,6 +47,14 @@ import org.zstack.kvm.KVMConstant; import org.zstack.kvm.KvmCommandSender; import org.zstack.kvm.KvmResponseWrapper; +import org.zstack.kvm.vmfiles.message.BackupVmHostFileMsg; +import org.zstack.kvm.vmfiles.message.BackupVmHostFileOnHypervisorMsg; +import org.zstack.kvm.vmfiles.message.BackupVmHostFileOnHypervisorReply; +import org.zstack.kvm.vmfiles.message.BackupVmHostFileReply; +import org.zstack.kvm.vmfiles.message.CloneVmHostFileMsg; +import org.zstack.kvm.vmfiles.message.CloneVmHostFileReply; +import org.zstack.kvm.vmfiles.message.SyncVmHostFilesFromHostMsg; +import org.zstack.kvm.vmfiles.message.SyncVmHostFilesFromHostReply; import org.zstack.resourceconfig.ResourceConfig; import org.zstack.resourceconfig.ResourceConfigFacade; import org.zstack.utils.DebugUtils; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmVmHostFileFactory.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmVmHostFileFactory.java index 26cfe6e745f..0aaaad0cc9a 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmVmHostFileFactory.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmVmHostFileFactory.java @@ -4,6 +4,8 @@ import org.zstack.header.vm.additions.VmHostFileVO; import org.zstack.kvm.tpm.TpmStateVmHostBackupFileBase; import org.zstack.kvm.tpm.TpmStateVmHostFileBase; +import org.zstack.kvm.vmfiles.AbstractVmHostBackupFileBase; +import org.zstack.kvm.vmfiles.AbstractVmHostFileBase; import static org.zstack.core.Platform.operr; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/NvRamVmHostBackupFileBase.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/NvRamVmHostBackupFileBase.java index b2df7c91f9e..8d78b9ac0d6 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/NvRamVmHostBackupFileBase.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/NvRamVmHostBackupFileBase.java @@ -2,6 +2,7 @@ import org.zstack.header.vm.additions.VmHostBackupFileVO; import org.zstack.header.vm.additions.VmHostFileType; +import org.zstack.kvm.vmfiles.AbstractVmHostBackupFileBase; public class NvRamVmHostBackupFileBase extends AbstractVmHostBackupFileBase { public NvRamVmHostBackupFileBase(VmHostBackupFileVO self) { diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/NvRamVmHostFileBase.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/NvRamVmHostFileBase.java index 98f9ff37ee7..cf539ee2278 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/NvRamVmHostFileBase.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/NvRamVmHostFileBase.java @@ -2,6 +2,7 @@ import org.zstack.header.vm.additions.VmHostFileType; import org.zstack.header.vm.additions.VmHostFileVO; +import org.zstack.kvm.vmfiles.AbstractVmHostFileBase; public class NvRamVmHostFileBase extends AbstractVmHostFileBase { public NvRamVmHostFileBase(VmHostFileVO self) { 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 59bfd84f5d1..e77d675637d 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 @@ -53,6 +53,8 @@ import org.zstack.header.vm.additions.VmHostFileType; import org.zstack.header.vm.additions.VmHostFileVO; import org.zstack.header.vm.additions.VmHostFileVO_; +import org.zstack.kvm.tpm.message.CloneVmTpmMsg; +import org.zstack.kvm.tpm.message.CloneVmTpmReply; import org.zstack.resourceconfig.ResourceConfig; import org.zstack.resourceconfig.ResourceConfigFacade; import org.zstack.utils.CollectionUtils; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmStateVmHostBackupFileBase.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmStateVmHostBackupFileBase.java index ee3f8fa7669..31caa26f219 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmStateVmHostBackupFileBase.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmStateVmHostBackupFileBase.java @@ -9,7 +9,7 @@ import org.zstack.header.vm.additions.VmHostFileType; import org.zstack.header.vm.additions.VmHostFileVO; import org.zstack.kvm.KVMSystemTags; -import org.zstack.kvm.efi.AbstractVmHostBackupFileBase; +import org.zstack.kvm.vmfiles.AbstractVmHostBackupFileBase; import org.zstack.tag.SystemTagCreator; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmStateVmHostFileBase.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmStateVmHostFileBase.java index a32cb332b2a..32f00975a41 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmStateVmHostFileBase.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/TpmStateVmHostFileBase.java @@ -2,7 +2,7 @@ import org.zstack.header.vm.additions.VmHostFileType; import org.zstack.header.vm.additions.VmHostFileVO; -import org.zstack.kvm.efi.AbstractVmHostFileBase; +import org.zstack.kvm.vmfiles.AbstractVmHostFileBase; public class TpmStateVmHostFileBase extends AbstractVmHostFileBase { public TpmStateVmHostFileBase(VmHostFileVO self) { diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/CloneVmTpmMsg.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/message/CloneVmTpmMsg.java similarity index 95% rename from plugin/kvm/src/main/java/org/zstack/kvm/tpm/CloneVmTpmMsg.java rename to plugin/kvm/src/main/java/org/zstack/kvm/tpm/message/CloneVmTpmMsg.java index 69f0d6e5e16..60722841ed2 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/CloneVmTpmMsg.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/message/CloneVmTpmMsg.java @@ -1,4 +1,4 @@ -package org.zstack.kvm.tpm; +package org.zstack.kvm.tpm.message; import org.zstack.header.message.NeedReplyMessage; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/CloneVmTpmReply.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/message/CloneVmTpmReply.java similarity index 92% rename from plugin/kvm/src/main/java/org/zstack/kvm/tpm/CloneVmTpmReply.java rename to plugin/kvm/src/main/java/org/zstack/kvm/tpm/message/CloneVmTpmReply.java index b2ad460b8f2..0c9be5fb4f3 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/CloneVmTpmReply.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/message/CloneVmTpmReply.java @@ -1,4 +1,4 @@ -package org.zstack.kvm.tpm; +package org.zstack.kvm.tpm.message; import org.zstack.header.message.MessageReply; import org.zstack.header.tpm.entity.TpmInventory; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/AbstractVmHostBackupFileBase.java b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/AbstractVmHostBackupFileBase.java similarity index 96% rename from plugin/kvm/src/main/java/org/zstack/kvm/efi/AbstractVmHostBackupFileBase.java rename to plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/AbstractVmHostBackupFileBase.java index c71bdc7db29..1e4e7bae71b 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/AbstractVmHostBackupFileBase.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/AbstractVmHostBackupFileBase.java @@ -1,4 +1,4 @@ -package org.zstack.kvm.efi; +package org.zstack.kvm.vmfiles; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Configurable; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/AbstractVmHostFileBase.java b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/AbstractVmHostFileBase.java similarity index 94% rename from plugin/kvm/src/main/java/org/zstack/kvm/efi/AbstractVmHostFileBase.java rename to plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/AbstractVmHostFileBase.java index 7022d7a1d45..8e87dcf70cd 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/AbstractVmHostFileBase.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/AbstractVmHostFileBase.java @@ -1,4 +1,4 @@ -package org.zstack.kvm.efi; +package org.zstack.kvm.vmfiles; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.Configurable; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/VmHostFileTracker.java b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java similarity index 98% rename from plugin/kvm/src/main/java/org/zstack/kvm/efi/VmHostFileTracker.java rename to plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java index 203b27cef2b..ecc16b91583 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/VmHostFileTracker.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java @@ -1,4 +1,4 @@ -package org.zstack.kvm.efi; +package org.zstack.kvm.vmfiles; import org.springframework.beans.factory.annotation.Autowired; import org.zstack.core.CoreGlobalProperty; @@ -22,6 +22,7 @@ import org.zstack.header.vm.additions.VmHostFileVO; import org.zstack.header.vm.additions.VmHostFileVO_; import org.zstack.kvm.KVMGlobalConfig; +import org.zstack.kvm.vmfiles.message.SyncVmHostFilesFromHostMsg; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileMsg.java b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/BackupVmHostFileMsg.java similarity index 95% rename from plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileMsg.java rename to plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/BackupVmHostFileMsg.java index 1eafe523e1c..18a0930d7fc 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileMsg.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/BackupVmHostFileMsg.java @@ -1,4 +1,4 @@ -package org.zstack.kvm.efi; +package org.zstack.kvm.vmfiles.message; import org.zstack.header.message.NeedReplyMessage; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileOnHypervisorMsg.java b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/BackupVmHostFileOnHypervisorMsg.java similarity index 95% rename from plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileOnHypervisorMsg.java rename to plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/BackupVmHostFileOnHypervisorMsg.java index 803503e2553..1ac815f8ff9 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileOnHypervisorMsg.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/BackupVmHostFileOnHypervisorMsg.java @@ -1,4 +1,4 @@ -package org.zstack.kvm.efi; +package org.zstack.kvm.vmfiles.message; import org.zstack.header.host.HostMessage; import org.zstack.header.message.NeedReplyMessage; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileOnHypervisorReply.java b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/BackupVmHostFileOnHypervisorReply.java similarity index 75% rename from plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileOnHypervisorReply.java rename to plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/BackupVmHostFileOnHypervisorReply.java index 7670f13fa87..f3268a43f83 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileOnHypervisorReply.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/BackupVmHostFileOnHypervisorReply.java @@ -1,4 +1,4 @@ -package org.zstack.kvm.efi; +package org.zstack.kvm.vmfiles.message; import org.zstack.header.message.MessageReply; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileReply.java b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/BackupVmHostFileReply.java similarity index 90% rename from plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileReply.java rename to plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/BackupVmHostFileReply.java index a4d1c481c92..f2a22b3d920 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/BackupVmHostFileReply.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/BackupVmHostFileReply.java @@ -1,4 +1,4 @@ -package org.zstack.kvm.efi; +package org.zstack.kvm.vmfiles.message; import org.zstack.header.message.MessageReply; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/CloneVmHostFileMsg.java b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/CloneVmHostFileMsg.java similarity index 94% rename from plugin/kvm/src/main/java/org/zstack/kvm/efi/CloneVmHostFileMsg.java rename to plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/CloneVmHostFileMsg.java index d28226995e8..ece11bc11fc 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/CloneVmHostFileMsg.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/CloneVmHostFileMsg.java @@ -1,4 +1,4 @@ -package org.zstack.kvm.efi; +package org.zstack.kvm.vmfiles.message; import org.zstack.header.message.NeedReplyMessage; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/CloneVmHostFileReply.java b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/CloneVmHostFileReply.java similarity index 72% rename from plugin/kvm/src/main/java/org/zstack/kvm/efi/CloneVmHostFileReply.java rename to plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/CloneVmHostFileReply.java index b026831c503..b29e604ed39 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/CloneVmHostFileReply.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/CloneVmHostFileReply.java @@ -1,4 +1,4 @@ -package org.zstack.kvm.efi; +package org.zstack.kvm.vmfiles.message; import org.zstack.header.message.MessageReply; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostMsg.java b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/SyncVmHostFilesFromHostMsg.java similarity index 97% rename from plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostMsg.java rename to plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/SyncVmHostFilesFromHostMsg.java index 8158110f596..93b21797f14 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostMsg.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/SyncVmHostFilesFromHostMsg.java @@ -1,4 +1,4 @@ -package org.zstack.kvm.efi; +package org.zstack.kvm.vmfiles.message; import org.zstack.header.message.NeedReplyMessage; diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostReply.java b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/SyncVmHostFilesFromHostReply.java similarity index 74% rename from plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostReply.java rename to plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/SyncVmHostFilesFromHostReply.java index 7509815a68e..e920c800985 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/SyncVmHostFilesFromHostReply.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/message/SyncVmHostFilesFromHostReply.java @@ -1,4 +1,4 @@ -package org.zstack.kvm.efi; +package org.zstack.kvm.vmfiles.message; import org.zstack.header.message.MessageReply; diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index 9a3c87d2ba5..f209b116efb 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -278,7 +278,7 @@ - + From 39fa0478dc13f4ac3a05210dbe6e22201e28309f Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 31 Mar 2026 20:56:20 +0800 Subject: [PATCH 093/129] [kvm]: add os spec machine type support Add OperatingSystemBootingSpec to VmInstanceSpec for machine type Support setting machine type through os spec instead of system tag Auto set q35 machine type for TPM/NvRam scenarios when needed Update KVMHost to prioritize spec machine type over system tag Remove unused imports from KVMHost Resolves: ZSV-11371 Related: ZSV-11310 Change-Id: I6f6d6a677374656d6c7467686e6369786f6f646f --- .../compute/vm/devices/VmTpmExtensions.java | 11 +++++++++ .../org/zstack/header/vm/VmInstanceSpec.java | 24 +++++++++++++++++++ .../src/main/java/org/zstack/kvm/KVMHost.java | 14 ++++++----- 3 files changed, 43 insertions(+), 6 deletions(-) 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 5923d9a7795..e873084b281 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 @@ -13,15 +13,20 @@ import org.zstack.header.vm.VmInstanceCreateExtensionPoint; import org.zstack.header.vm.VmInstanceSpec; import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.VmMachineType; import org.zstack.header.vm.devices.VmDevicesSpec; import org.zstack.resourceconfig.ResourceConfig; import org.zstack.resourceconfig.ResourceConfigFacade; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; 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 { + private static final CLogger logger = Utils.getLogger(VmTpmExtensions.class); + @Autowired private VmTpmManager vmTpmManager; @Autowired @@ -115,5 +120,11 @@ public void afterBuildVmSpec(VmInstanceSpec spec) { tpmSpec.setEnable(true); tpmSpec.setTpmUuid(tpmUuid); } + + if (needRegisterNvRam && spec.getOsSpec().getMachineType() == null) { + spec.getOsSpec().setMachineType(VmMachineType.q35.toString()); + logger.debug(String.format( + "auto-set machineType to q35 for VM[uuid:%s] because NvRam/TPM registration is needed", vmUuid)); + } } } 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 95ccdc22bf2..198e2497b6c 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java @@ -406,6 +406,7 @@ public void setCandidatePrimaryStorageUuidsForDataVolume(List candidateP private VmCustomSpecificationStruct vmCustomSpecification; private VmDevicesSpec devicesSpec; private DiskAO nvRamSpec; + private OperatingSystemBootingSpec osSpec = new OperatingSystemBootingSpec(); public DiskAO getRootDisk() { return rootDisk; @@ -455,6 +456,14 @@ public void setNvRamSpec(DiskAO nvRamSpec) { this.nvRamSpec = nvRamSpec; } + public OperatingSystemBootingSpec getOsSpec() { + return osSpec; + } + + public void setOsSpec(OperatingSystemBootingSpec osSpec) { + this.osSpec = osSpec; + } + public boolean isSkipIpAllocation() { return skipIpAllocation; } @@ -924,4 +933,19 @@ public String getVolumeFormatFromImage() { return ImageConstant.QCOW2_FORMAT_STRING; } } + + public static class OperatingSystemBootingSpec implements Serializable { + /** + * @see VmMachineType + */ + private String machineType; + + public String getMachineType() { + return machineType; + } + + public void setMachineType(String machineType) { + this.machineType = machineType; + } + } } 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 a23773bbca2..d71c2789e4a 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -101,9 +101,6 @@ 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; @@ -4439,9 +4436,14 @@ protected void startVm(final VmInstanceSpec spec, final NeedReplyMessage msg, fi cmd.setChassisAssetTag(NetworkGlobalProperty.CHASSIS_ASSET_TAG); } - String machineType = VmSystemTags.MACHINE_TYPE.getTokenByResourceUuid(cmd.getVmInstanceUuid(), - VmInstanceVO.class, VmSystemTags.MACHINE_TYPE_TOKEN); - cmd.setMachineType(StringUtils.isNotEmpty(machineType) ? machineType : "pc"); + if (spec.getOsSpec().getMachineType() == null) { + String machineType = VmSystemTags.MACHINE_TYPE.getTokenByResourceUuid(cmd.getVmInstanceUuid(), + VmInstanceVO.class, VmSystemTags.MACHINE_TYPE_TOKEN); + cmd.setMachineType(StringUtils.isNotEmpty(machineType) ? machineType : "pc"); + } else { + cmd.setMachineType(spec.getOsSpec().getMachineType()); + } + String machineType = cmd.getMachineType(); if (KVMSystemTags.VM_PREDEFINED_PCI_BRIDGE_NUM.hasTag(spec.getVmInventory().getUuid())) { cmd.setPredefinedPciBridgeNum(Integer.valueOf(KVMSystemTags.VM_PREDEFINED_PCI_BRIDGE_NUM.getTokenByResourceUuid(spec.getVmInventory().getUuid(), KVMSystemTags.VM_PREDEFINED_PCI_BRIDGE_NUM_TOKEN))); From 458fcb816bd221900a98fe937a8a8277cc51f51d Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 1 Apr 2026 10:48:16 +0800 Subject: [PATCH 094/129] [kvm]: persist machine type to system tag Persist machine type to system tag after VM creation This ensures machine type configuration is saved in database Recreate existing tag if present to update machine type value Resolves: ZSV-11371 Related: ZSV-11310 Change-Id: I7a6a78626f6978677a7a7562796b707864736267 --- .../java/org/zstack/kvm/BootKvmStartVmExtension.java | 11 +++++++++++ plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java | 1 + 2 files changed, 12 insertions(+) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/BootKvmStartVmExtension.java b/plugin/kvm/src/main/java/org/zstack/kvm/BootKvmStartVmExtension.java index 41ab1f49b51..9bbc737a1f7 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/BootKvmStartVmExtension.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/BootKvmStartVmExtension.java @@ -29,6 +29,17 @@ public void startVmOnKvmSuccess(KVMHostInventory host, VmInstanceSpec spec) { VmSystemTags.BOOT_ORDER.delete(spec.getVmInventory().getUuid()); VmSystemTags.CDROM_BOOT_ONCE.delete(spec.getVmInventory().getUuid()); } + + final String machineType = spec.getOsSpec().getMachineType(); + if (StringUtils.isEmpty(machineType)) { + VmSystemTags.MACHINE_TYPE.delete(spec.getVmInventory().getUuid()); + } else { + SystemTagCreator creator = VmSystemTags.MACHINE_TYPE.newSystemTagCreator(spec.getVmInventory().getUuid()); + creator.setTagByTokens(map(e(VmSystemTags.MACHINE_TYPE_TOKEN, machineType))); + creator.inherent = false; + creator.recreate = true; + creator.create(); + } } @Override 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 d71c2789e4a..256988fd0df 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -4440,6 +4440,7 @@ protected void startVm(final VmInstanceSpec spec, final NeedReplyMessage msg, fi String machineType = VmSystemTags.MACHINE_TYPE.getTokenByResourceUuid(cmd.getVmInstanceUuid(), VmInstanceVO.class, VmSystemTags.MACHINE_TYPE_TOKEN); cmd.setMachineType(StringUtils.isNotEmpty(machineType) ? machineType : "pc"); + spec.getOsSpec().setMachineType(cmd.getMachineType()); } else { cmd.setMachineType(spec.getOsSpec().getMachineType()); } From d3af48a8403bbdd56a597f1a88b4416f440fe008 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 1 Apr 2026 15:14:01 +0800 Subject: [PATCH 095/129] [vm]: move nvram to devices spec and add backup support Move nvRamSpec from VmInstanceSpec to VmDevicesSpec for better device management consistency with TPM. Add needRegister flag to NvRamSpec for explicit registration control. Remove unused NV_RAM_DEFAULT_SIZE constant. Update VmTpmExtensions and KvmSecureBootExtensions to use new NvRamSpec structure. Add backupFileUuid support for TPM and NvRam resource synchronization. Related: ZSV-11310 Resolves: ZSV-11653 Change-Id: I6c7177767470757575746b77696477756d716f6e --- .../compute/vm/devices/VmTpmExtensions.java | 26 ++++++++++--------- .../zstack/header/vm/VmInstanceConstant.java | 2 -- .../org/zstack/header/vm/VmInstanceSpec.java | 11 +------- .../zstack/header/vm/devices/NvRamSpec.java | 9 +++++++ .../kvm/efi/KvmSecureBootExtensions.java | 16 +++++++----- .../org/zstack/kvm/tpm/KvmTpmExtensions.java | 3 +++ 6 files changed, 37 insertions(+), 30 deletions(-) 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 e873084b281..fe0535f27c6 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 @@ -9,11 +9,11 @@ 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.VmMachineType; +import org.zstack.header.vm.devices.NvRamSpec; import org.zstack.header.vm.devices.VmDevicesSpec; import org.zstack.resourceconfig.ResourceConfig; import org.zstack.resourceconfig.ResourceConfigFacade; @@ -21,7 +21,6 @@ import org.zstack.utils.logging.CLogger; 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 { @@ -97,20 +96,23 @@ public void afterBuildVmSpec(VmInstanceSpec spec) { } } - if (needRegisterNvRam) { - DiskAO nvRamSpec = new DiskAO(); - nvRamSpec.setSize(NV_RAM_DEFAULT_SIZE); - nvRamSpec.setName("NvRam-of-VM-" + vmUuid); - spec.setNvRamSpec(nvRamSpec); + VmDevicesSpec devicesSpec = spec.getDevicesSpec(); + if (devicesSpec == null) { + devicesSpec = new VmDevicesSpec(); + spec.setDevicesSpec(devicesSpec); } - if (tpmUuid != null) { - VmDevicesSpec devicesSpec = spec.getDevicesSpec(); - if (devicesSpec == null) { - devicesSpec = new VmDevicesSpec(); - spec.setDevicesSpec(devicesSpec); + if (needRegisterNvRam) { + NvRamSpec nvRamSpec = devicesSpec.getNvRam(); + if (nvRamSpec == null) { + nvRamSpec = new NvRamSpec(); + devicesSpec.setNvRam(nvRamSpec); } + nvRamSpec.setNeedRegister(true); + } + + if (tpmUuid != null) { TpmSpec tpmSpec = devicesSpec.getTpm(); if (tpmSpec == null) { tpmSpec = new TpmSpec(); 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 43748d46dc1..e09a1cefc52 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceConstant.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceConstant.java @@ -27,8 +27,6 @@ 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 198e2497b6c..ac1d8ca3d66 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceSpec.java @@ -404,8 +404,7 @@ public void setCandidatePrimaryStorageUuidsForDataVolume(List candidateP private List dataDisks; private List deprecatedDisksSpecs = new ArrayList<>(); private VmCustomSpecificationStruct vmCustomSpecification; - private VmDevicesSpec devicesSpec; - private DiskAO nvRamSpec; + private VmDevicesSpec devicesSpec = new VmDevicesSpec(); private OperatingSystemBootingSpec osSpec = new OperatingSystemBootingSpec(); public DiskAO getRootDisk() { @@ -448,14 +447,6 @@ public void setDevicesSpec(VmDevicesSpec devicesSpec) { this.devicesSpec = devicesSpec; } - public DiskAO getNvRamSpec() { - return nvRamSpec; - } - - public void setNvRamSpec(DiskAO nvRamSpec) { - this.nvRamSpec = nvRamSpec; - } - public OperatingSystemBootingSpec getOsSpec() { return osSpec; } diff --git a/header/src/main/java/org/zstack/header/vm/devices/NvRamSpec.java b/header/src/main/java/org/zstack/header/vm/devices/NvRamSpec.java index d7f0bf6bd17..81eca6330bc 100644 --- a/header/src/main/java/org/zstack/header/vm/devices/NvRamSpec.java +++ b/header/src/main/java/org/zstack/header/vm/devices/NvRamSpec.java @@ -3,9 +3,18 @@ import org.zstack.header.rest.APINoSee; public class NvRamSpec { + private boolean needRegister; @APINoSee private String backupFileUuid; + public boolean isNeedRegister() { + return needRegister; + } + + public void setNeedRegister(boolean needRegister) { + this.needRegister = needRegister; + } + public String getBackupFileUuid() { return backupFileUuid; } 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 46c02aa856e..8f9fffe87ec 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 @@ -53,6 +53,7 @@ import org.zstack.header.storage.snapshot.VolumeSnapshotCreationExtensionPoint; import org.zstack.header.storage.snapshot.VolumeSnapshotInventory; import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupInventory; +import org.zstack.header.vm.devices.NvRamSpec; import org.zstack.header.volume.VolumeInventory; import org.zstack.kvm.KVMAgentCommands; import org.zstack.kvm.KVMAgentCommands.*; @@ -119,12 +120,13 @@ public void beforeStartVmOnKvm(KVMHostInventory host, VmInstanceSpec spec, KVMAg } } - if (spec.getNvRamSpec() != null) { - prepareNvRamToStartVmCmd(cmd, spec.getNvRamSpec(), host); + final NvRamSpec nvRam = spec.getDevicesSpec().getNvRam(); + if (nvRam != null && nvRam.isNeedRegister()) { + prepareNvRamToStartVmCmd(cmd, nvRam, host); } } - private void prepareNvRamToStartVmCmd(KVMAgentCommands.StartVmCmd cmd, DiskAO nvRamSpec, KVMHostInventory host) { + private void prepareNvRamToStartVmCmd(KVMAgentCommands.StartVmCmd cmd, NvRamSpec nvRam, KVMHostInventory host) { VolumeTO volume = new VolumeTO(); volume.setDeviceType(VolumeTO.FILE); volume.setInstallPath(buildNvramFilePath(cmd.getVmInstanceUuid())); @@ -132,6 +134,7 @@ private void prepareNvRamToStartVmCmd(KVMAgentCommands.StartVmCmd cmd, DiskAO nv cmd.setNvRam(volume); synchronized (hostFileLock) { + final Timestamp now = Timestamp.from(Instant.now()); VmHostFileVO nvRamFile = Q.New(VmHostFileVO.class) .eq(VmHostFileVO_.vmInstanceUuid, cmd.getVmInstanceUuid()) .eq(VmHostFileVO_.type, VmHostFileType.NvRam) @@ -144,14 +147,14 @@ private void prepareNvRamToStartVmCmd(KVMAgentCommands.StartVmCmd cmd, DiskAO nv nvRamFile.setVmInstanceUuid(cmd.getVmInstanceUuid()); nvRamFile.setType(VmHostFileType.NvRam); nvRamFile.setPath(volume.getInstallPath()); - nvRamFile.setCreateDate(Timestamp.from(Instant.now())); + nvRamFile.setCreateDate(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())) + .set(VmHostFileVO_.lastOpDate, now) .update(); } } @@ -265,7 +268,7 @@ public void preBeforeInstantiateVmResource(VmInstanceSpec spec) throws VmInstant @Override public void preInstantiateVmResource(VmInstanceSpec spec, Completion completion) { - final DiskAO nvRamSpec = spec.getNvRamSpec(); + final NvRamSpec nvRamSpec = spec.getDevicesSpec() == null ? null : spec.getDevicesSpec().getNvRam(); if (nvRamSpec == null) { completion.success(); return; @@ -275,6 +278,7 @@ public void preInstantiateVmResource(VmInstanceSpec spec, Completion completion) context.hostUuid = spec.getDestHost().getUuid(); context.vmUuid = spec.getVmInventory().getUuid(); context.type = VmHostFileType.NvRam; + context.backupUuid = nvRamSpec.getBackupFileUuid(); context.syncReason = "pre-instantiate VM resource"; prepareHostFileOnHost(context, completion); } 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 a4976af0fba..5aed336b5d9 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 @@ -137,6 +137,7 @@ public void preInstantiateVmResource(VmInstanceSpec spec, Completion completion) clearRollbackInfo(spec); final PrepareTpmResourceContext context = new PrepareTpmResourceContext(); context.tpmUuid = tpmSpec.getTpmUuid(); + context.backupFileUuid = tpmSpec.getBackupFileUuid(); // maybe null context.providerUuid = resourceKeyBackend.findKeyProviderUuidByTpm(context.tpmUuid); context.providerName = resourceKeyBackend.findKeyProviderNameByTpm(context.tpmUuid); @@ -151,6 +152,7 @@ public void run(FlowTrigger trigger, Map data) { innerContext.hostUuid = spec.getDestHost().getUuid(); innerContext.vmUuid = spec.getVmInventory().getUuid(); innerContext.type = VmHostFileType.TpmState; + innerContext.backupUuid = context.backupFileUuid; innerContext.syncReason = "pre-instantiate VM resource"; secureBootExtensions.prepareHostFileOnHost(innerContext, new Completion(trigger) { @Override @@ -265,6 +267,7 @@ static class PrepareTpmStateHostFileContext { static class PrepareTpmResourceContext { String tpmUuid; + String backupFileUuid; String providerUuid; String providerName; String dekBase64; From 1d7126c5596107fc04ee9403a417cb3f67314880 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 1 Apr 2026 19:10:12 +0800 Subject: [PATCH 096/129] [compute]: add allowed.tpm.vm.without.kms global config Related: ZSV-11310 Change-Id: I67787561736968637666626a627366757775696a --- .../main/java/org/zstack/compute/vm/VmGlobalConfig.java | 4 ++++ .../main/java/org/zstack/kvm/tpm/KvmTpmExtensions.java | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) 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 d588936ee53..fa1b3808c44 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java @@ -139,4 +139,8 @@ public class VmGlobalConfig { @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"); + + @GlobalConfigDef(defaultValue = "false", type = Boolean.class, description = "allowed TPM VM start without KMS") + @GlobalConfigValidation(validValues = {"true", "false"}) + public static GlobalConfig ALLOWED_TPM_VM_WITHOUT_KMS = new GlobalConfig(CATEGORY, "allowed.tpm.vm.without.kms"); } 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 5aed336b5d9..5d98121f66a 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 @@ -2,6 +2,7 @@ import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.vm.VmGlobalConfig; import org.zstack.compute.vm.devices.TpmEncryptedResourceKeyBackend; import org.zstack.core.Platform; import org.zstack.core.cloudbus.CloudBus; @@ -171,7 +172,12 @@ public void fail(ErrorCode errorCode) { @Override public boolean skip(Map data) { - return false; + boolean shouldSkip = VmGlobalConfig.ALLOWED_TPM_VM_WITHOUT_KMS.value(Boolean.class) && + (StringUtils.isBlank(context.providerUuid) || StringUtils.isBlank(context.providerName)); + if (shouldSkip) { + logger.info("skip create-dek: allowed.tpm.vm.without.kms is enabled and no KMS provider bound"); + } + return shouldSkip; } @Override From adea70054e2f3d3a2e36811ac179d2455524e940 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 2 Apr 2026 10:48:07 +0800 Subject: [PATCH 097/129] [kvm]: support secure boot for primary storage migration Modify the preVmMigration method to include PrimaryStorageMigration in the migration type check, ensuring secure boot settings are properly handled during primary storage migration. Related: ZSV-11310 Resolves: ZSV-11524 Change-Id: I776b74707765776778636176627076776e6c6771 --- .../main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 8f9fffe87ec..5951591ce2a 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 @@ -80,6 +80,7 @@ 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.header.vm.VmMigrationType.PrimaryStorageMigration; import static org.zstack.header.vm.additions.VmHostFileSyncReason.PostMigration; import static org.zstack.header.vm.additions.VmHostFileSyncReason.PrepareReRead; import static org.zstack.header.vm.additions.VmHostFileSyncReason.PrepareRead; @@ -162,7 +163,7 @@ private void prepareNvRamToStartVmCmd(KVMAgentCommands.StartVmCmd cmd, NvRamSpec @Override public void preVmMigration(VmInstanceInventory vm, VmMigrationType type, String dstHostUuid, Completion completion) { - if (HostMigration != type) { + if (HostMigration != type && PrimaryStorageMigration != type) { completion.success(); return; } From 89b3e036f4482a91005ae988d714be62d7d81e93 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 2 Apr 2026 15:05:20 +0800 Subject: [PATCH 098/129] [kvm]: add periodic cleanup for expired VM host files Add a periodic cleanup task that runs every 30 minutes to clean up expired VM host files and backup files. Implements different cleanup strategies for active VMs, deleted VMs, and expunged VMs. - For active VMs: keep latest file, delete files older than 1 day - For deleted VMs: keep latest for 90 days, others for 7 days - For expunged VMs: delete all records immediately - For backup files: keep based on resource type and age - Send KVM delete commands before removing DB records - Handle host deletion scenarios properly Related: ZSV-11310 Change-Id: I7a78676c6775746d637762716367716c68617a74 --- .../zstack/kvm/vmfiles/VmHostFileTracker.java | 344 +++++++++++++++++- 1 file changed, 338 insertions(+), 6 deletions(-) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java index ecc16b91583..3a52a09f73c 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java @@ -7,43 +7,72 @@ import org.zstack.core.cloudbus.CloudBusCallBack; import org.zstack.core.cloudbus.ResourceDestinationMaker; import org.zstack.core.config.GlobalConfigUpdateExtensionPoint; +import org.zstack.core.db.DatabaseFacade; import org.zstack.core.db.Q; +import org.zstack.core.db.SQLBatch; import org.zstack.core.thread.PeriodicTask; import org.zstack.core.thread.ThreadFacade; +import org.zstack.core.timeout.TimeHelper; import org.zstack.header.Component; +import org.zstack.header.core.FutureReturnValueCompletion; import org.zstack.header.core.WhileDoneCompletion; import org.zstack.header.errorcode.ErrorCodeList; +import org.zstack.header.host.HostVO; +import org.zstack.header.host.HostVO_; import org.zstack.header.message.MessageReply; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO; import org.zstack.header.vm.VmInstanceAO_; import org.zstack.header.vm.VmInstanceConstant; +import org.zstack.header.vm.VmInstanceEO; import org.zstack.header.vm.VmInstanceState; 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.header.vm.VmInstanceVO_; +import org.zstack.header.vm.additions.*; +import org.zstack.header.vo.ResourceVO; +import org.zstack.header.vo.ResourceVO_; +import org.zstack.kvm.KVMAgentCommands; +import org.zstack.kvm.KVMConstant; import org.zstack.kvm.KVMGlobalConfig; +import org.zstack.kvm.KvmCommandSender; import org.zstack.kvm.vmfiles.message.SyncVmHostFilesFromHostMsg; +import org.zstack.utils.CollectionUtils; import org.zstack.utils.Utils; +import org.zstack.utils.data.Pair; import org.zstack.utils.logging.CLogger; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import javax.persistence.Tuple; +import java.sql.Timestamp; +import java.util.*; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static org.zstack.core.Platform.operr; +import static org.zstack.utils.CollectionUtils.toMap; +import static org.zstack.utils.CollectionUtils.transformToSet; + public class VmHostFileTracker implements Component { private static final CLogger logger = Utils.getLogger(VmHostFileTracker.class); + private static final long CLEANUP_INTERVAL_SECONDS = 1800; + private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1); + private static final long SEVEN_DAYS_MS = TimeUnit.DAYS.toMillis(7); + private static final long NINETY_DAYS_MS = TimeUnit.DAYS.toMillis(90); + private static final long KVM_CMD_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(3); + @Autowired private ThreadFacade threadFacade; @Autowired private CloudBus bus; @Autowired private ResourceDestinationMaker destinationMaker; + @Autowired + private DatabaseFacade dbf; + @Autowired + private TimeHelper timeHelper; private Future trackerThread; + private Future cleanupThread; @Override public boolean start() { @@ -53,6 +82,7 @@ public boolean start() { } submitTrackerTask(); + submitCleanupTask(); GlobalConfigUpdateExtensionPoint listener = (oldConfig, newConfig) -> { logger.debug(String.format("%s changed from %s to %s, restarting vm-host-file-tracker", @@ -71,6 +101,9 @@ public boolean stop() { if (trackerThread != null) { trackerThread.cancel(true); } + if (cleanupThread != null) { + cleanupThread.cancel(true); + } return true; } @@ -104,6 +137,44 @@ public void run() { }); } + private synchronized void submitCleanupTask() { + if (cleanupThread != null) { + cleanupThread.cancel(true); + } + + cleanupThread = threadFacade.submitPeriodicTask(new PeriodicTask() { + @Override + public TimeUnit getTimeUnit() { + return TimeUnit.SECONDS; + } + + @Override + public long getInterval() { + return CLEANUP_INTERVAL_SECONDS; + } + + @Override + public String getName() { + return "vm-host-file-cleanup-tracker"; + } + + @Override + public void run() { + logger.info("VmHostFileTracker: starting periodic cleanup of expired VM host files"); + try { + cleanupExpiredVmHostFiles(); + } catch (Throwable t) { + logger.warn("error during VmHostFile cleanup", t); + } + try { + cleanupExpiredVmHostBackupFiles(); + } catch (Throwable t) { + logger.warn("error during VmHostBackupFile cleanup", t); + } + } + }); + } + private void syncVmHostFiles() { List hostFiles = Q.New(VmHostFileVO.class, VmInstanceVO.class) .table0() @@ -164,4 +235,265 @@ public void done(ErrorCodeList errorCodeList) { } }); } + + /** + * active VM: In each type, max(lastOpDate) will not be deleted; + * delete lastOpDate > 1d if delete command success + * VmInstanceEO(deleted): delete lastOpDate > 90d if record is latest; + * delete lastOpDate > 7d if record is not latest even if delete command failed + * delete lastOpDate < 7d if record is not latest and delete command success + * VmInstanceEO(not exists): delete + * Host not exists: delete and not send command + */ + private void cleanupExpiredVmHostFiles() { + List vmTypeList = Q.New(VmHostFileVO.class) + .select(VmHostFileVO_.vmInstanceUuid, VmHostFileVO_.type) + .listTuple(); + if (vmTypeList.isEmpty()) { + return; + } + + Set> pairs = new HashSet<>(); + for (Tuple tuple : vmTypeList) { + pairs.add(new Pair<>(tuple.get(0, String.class), tuple.get(1, VmHostFileType.class))); + } + + long now = timeHelper.getCurrentTimeMillis(); + CollectionUtils.safeForEach(pairs, pair -> { + String vmUuid = pair.first(); + if (!destinationMaker.isManagedByUs(vmUuid)) { + return; + } + + VmHostFileType type = pair.second(); + List files = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) + .eq(VmHostFileVO_.type, type) + .list(); + if (files.isEmpty()) { + return; // should not be here + } + files.sort((a, b) -> b.getLastOpDate().compareTo(a.getLastOpDate())); + + // Re-check VM status each iteration as per user's requirement + VmInstanceEO eo = Q.New(VmInstanceEO.class).eq(VmInstanceVO_.uuid, vmUuid).find(); + boolean vmActive = eo != null && eo.getDeleted() == null; + + if (vmActive) { + cleanupActiveVmHostFiles(files, now); + } else if (eo != null && eo.getDeleted() != null) { + cleanupDeletedVmHostFiles(files, now); + } else { + for (VmHostFileVO file : files) { + logger.info(String.format("deleting VmHostFileVO[uuid=%s] for expunged VM[uuid=%s]", + file.getUuid(), vmUuid)); + deleteVmHostFileFromDb(file.getUuid()); + } + } + }); + } + + private void cleanupActiveVmHostFiles(List files, long now) { + if (files.size() <= 1) { + return; + } + + VmHostFileVO latestWithContent = null; + for (VmHostFileVO file : files) { + if (hasContent(file.getUuid())) { + latestWithContent = file; + break; + } + } + + boolean allOlderThanOneDay = files.stream() + .allMatch(f -> now - f.getLastOpDate().getTime() > ONE_DAY_MS); + + for (int i = 1; i < files.size(); i++) { + VmHostFileVO file = files.get(i); + long age = now - file.getLastOpDate().getTime(); + + if (age <= ONE_DAY_MS) { + continue; + } + + if (allOlderThanOneDay && latestWithContent != null + && latestWithContent.getUuid().equals(file.getUuid()) + && !latestWithContent.getUuid().equals(files.get(0).getUuid())) { + continue; + } + + if (hasContent(file.getUuid())) { + boolean latestHasContent = hasContent(files.get(0).getUuid()); + if (!latestHasContent && latestWithContent != null + && latestWithContent.getUuid().equals(file.getUuid())) { + continue; + } + } + + boolean cmdSuccess = sendDeleteCommandToHost(file); + if (cmdSuccess) { + logger.info(String.format("deleting expired VmHostFileVO[uuid=%s, vm=%s, type=%s, age=%d hours]", + file.getUuid(), file.getVmInstanceUuid(), file.getType(), TimeUnit.MILLISECONDS.toHours(age))); + deleteVmHostFileFromDb(file.getUuid()); + } else { + logger.warn(String.format("failed to delete host file for VmHostFileVO[uuid=%s], skipping DB deletion", + file.getUuid())); + } + } + } + + private void cleanupDeletedVmHostFiles(List files, long now) { + for (int i = 0; i < files.size(); i++) { + VmHostFileVO file = files.get(i); + long age = now - file.getLastOpDate().getTime(); + + if (i == 0 && age <= NINETY_DAYS_MS) { + continue; + } + + if (age <= SEVEN_DAYS_MS) { + boolean cmdSuccess = sendDeleteCommandToHost(file); + if (!cmdSuccess) { + logger.warn(String.format( + "KVM delete failed for VmHostFileVO[uuid=%s] of deleted VM, lastOpDate within 7 days, skipping", + file.getUuid())); + continue; + } + } else { + sendDeleteCommandToHost(file); + } + + logger.info(String.format("deleting VmHostFileVO[uuid=%s] for deleted VM[uuid=%s, type=%s, age=%d days]", + file.getUuid(), file.getVmInstanceUuid(), file.getType(), TimeUnit.MILLISECONDS.toDays(age))); + deleteVmHostFileFromDb(file.getUuid()); + } + } + + /** + * lastOpDate <= 7d: do not delete + * resourceVO no longer exists: delete + * -> VolumeSnapshotGroupVO: do not delete + * -> VmInstanceEO (deleted): delete if VM has deleted >= 90d + * -> Other: do not delete + */ + private void cleanupExpiredVmHostBackupFiles() { + long now = timeHelper.getCurrentTimeMillis(); + Timestamp daysBefore = new Timestamp(now - SEVEN_DAYS_MS); + + List allBackupFiles = Q.New(VmHostBackupFileVO.class) + .lt(VmHostBackupFileVO_.lastOpDate, daysBefore) + .list(); + if (allBackupFiles.isEmpty()) { + return; + } + + List resourceTypeTuples = Q.New(ResourceVO.class) + .in(ResourceVO_.uuid, transformToSet(allBackupFiles, VmHostBackupFileVO::getResourceUuid)) + .select(ResourceVO_.uuid, ResourceVO_.resourceType) + .listTuple(); + Map uuidTypeMap = toMap(resourceTypeTuples, + tuple -> tuple.get(0, String.class), + tuple -> tuple.get(1, String.class)); + + CollectionUtils.safeForEach(allBackupFiles, backupFile -> { + String resourceUuid = backupFile.getResourceUuid(); + if (!destinationMaker.isManagedByUs(resourceUuid)) { + return; + } + + String resourceType = uuidTypeMap.get(resourceUuid); + if (resourceType == null) { + logger.info(String.format( + "deleting VmHostBackupFileVO[uuid=%s] -> referenced resource[uuid=%s] no longer exists", + backupFile.getUuid(), resourceUuid)); + deleteVmHostBackupFileFromDb(backupFile.getUuid()); + return; + } + + if (VolumeSnapshotGroupVO.class.getSimpleName().equals(resourceType)) { + return; + } + + if (VmInstanceVO.class.getSimpleName().equals(resourceType)) { + VmInstanceEO eo = dbf.findByUuid(resourceUuid, VmInstanceEO.class); + if (eo != null && eo.getDeleted() != null) { + long deletedAge; + try { + Timestamp deletedTime = Timestamp.valueOf(eo.getDeleted()); + deletedAge = now - deletedTime.getTime(); + } catch (IllegalArgumentException e) { + deletedAge = now - eo.getLastOpDate().getTime(); + } + + try { + if (deletedAge > NINETY_DAYS_MS) { + logger.info(String.format( + "deleting VmHostBackupFileVO[uuid=%s] -> referenced VM[uuid=%s] was deleted %d days ago", + backupFile.getUuid(), resourceUuid, TimeUnit.MILLISECONDS.toDays(deletedAge))); + deleteVmHostBackupFileFromDb(backupFile.getUuid()); + } + } catch (Exception e) { + logger.warn(String.format("failed to parse deleted timestamp[%s] for VM[uuid=%s]", + eo.getDeleted(), resourceUuid), e); + } + } + } + }); + } + + /** + * Send a KVM delete command to the host for the given file. + * Returns true if the command succeeded or the host doesn't exist (allowing DB cleanup). + */ + private boolean sendDeleteCommandToHost(VmHostFileVO file) { + String hostUuid = file.getHostUuid(); + + // If host is gone (soft-deleted or expunged), allow direct DB cleanup + if (!Q.New(HostVO.class).eq(HostVO_.uuid, hostUuid).isExists()) { + return true; + } + + KVMAgentCommands.WriteVmHostFileContentCmd cmd = new KVMAgentCommands.WriteVmHostFileContentCmd(); + KVMAgentCommands.VmHostFileTO to = new KVMAgentCommands.VmHostFileTO(); + to.setPath(file.getPath()); + to.setType(file.getType().toString()); + to.setOperation(VmHostFileOperation.Delete.toString()); + cmd.setHostFiles(Collections.singletonList(to)); + + FutureReturnValueCompletion future = new FutureReturnValueCompletion(null); + new KvmCommandSender(hostUuid).send(cmd, KVMConstant.WRITE_VM_HOST_FILE_PATH, wrapper -> { + KVMAgentCommands.WriteVmHostFileContentResponse rsp = + wrapper.getResponse(KVMAgentCommands.WriteVmHostFileContentResponse.class); + return rsp.isSuccess() ? null : operr("failed to delete host file[path=%s] on host[uuid=%s]", + file.getPath(), hostUuid); + }, future); + + future.await(KVM_CMD_TIMEOUT_MS); + return future.isSuccess(); + } + + private boolean hasContent(String fileUuid) { + return Q.New(VmHostFileContentVO.class).eq(VmHostFileContentVO_.uuid, fileUuid).isExists(); + } + + private void deleteVmHostFileFromDb(String fileUuid) { + new SQLBatch() { + @Override + protected void scripts() { + sql(VmHostFileContentVO.class).eq(VmHostFileContentVO_.uuid, fileUuid).delete(); + sql(VmHostFileVO.class).eq(VmHostFileVO_.uuid, fileUuid).delete(); + } + }.execute(); + } + + private void deleteVmHostBackupFileFromDb(String fileUuid) { + new SQLBatch() { + @Override + protected void scripts() { + sql(VmHostFileContentVO.class).eq(VmHostFileContentVO_.uuid, fileUuid).delete(); + sql(VmHostBackupFileVO.class).eq(VmHostBackupFileVO_.uuid, fileUuid).delete(); + } + }.execute(); + } } From efc4fc22f8e5582cc728c3b17b60ff54417a3cf0 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 2 Apr 2026 15:39:32 +0800 Subject: [PATCH 099/129] [kvm]: improve TPM removal by cleaning host files before DB deletion Refactor the removeTpmFromVm method to split the DB removal flow into three steps for better resource cleanup: 1. Collect VmHostFileVO and VmHostBackupFileVO for TPM state files 2. Send delete commands to hosts to clean up files on disk 3. Remove database records regardless of command success This ensures host files are cleaned up before DB records are deleted. Removed NvRam file deletion to avoid conflicts with secure boot. Related: ZSV-11310 Resolves: ZSV-11622 Change-Id: I7865626a687376746c746a706b74676b6b7a6a71 --- .../org/zstack/kvm/tpm/KvmTpmManager.java | 83 +++++++++++++++---- 1 file changed, 69 insertions(+), 14 deletions(-) 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 e77d675637d..eff3303b20f 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 @@ -53,6 +53,12 @@ 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.additions.VmHostFileOperation; +import org.zstack.header.core.ReturnValueCompletion; +import org.zstack.kvm.KVMConstant; +import org.zstack.kvm.KVMAgentCommands; +import org.zstack.kvm.KvmCommandSender; +import org.zstack.kvm.KvmResponseWrapper; import org.zstack.kvm.tpm.message.CloneVmTpmMsg; import org.zstack.kvm.tpm.message.CloneVmTpmReply; import org.zstack.resourceconfig.ResourceConfig; @@ -62,10 +68,9 @@ import org.zstack.utils.logging.CLogger; import java.util.ArrayList; -import java.util.HashSet; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import static org.zstack.compute.vm.VmGlobalConfig.RESET_TPM_AFTER_VM_CLONE; import static org.zstack.core.Platform.err; @@ -294,6 +299,7 @@ public String getName() { static class RemoveTpmFromVmContext { String vmInstanceUuid; String tpmUuid; + List hostFiles; static RemoveTpmFromVmContext valueOf(RemoveTpmMsg msg) { RemoveTpmFromVmContext context = new RemoveTpmFromVmContext(); @@ -320,36 +326,85 @@ private void removeTpmFromVm(RemoveTpmFromVmContext context, Completion completi trigger.next(); }) .build()) + .then(Flow.of("collect-vm-host-files") + .handle(trigger -> { + // DO NOT delete NvRam type VmHostFile: Maybe secure boot or other component related. + context.hostFiles = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, context.vmInstanceUuid) + .eq(VmHostFileVO_.type, VmHostFileType.TpmState) + .list(); + trigger.next(); + }) + .build()) + .then(Flow.of("send-delete-commands-to-hosts") + .skipIf(data -> context.hostFiles.isEmpty()) + .handle(trigger -> { + Map> filesByHost = new HashMap<>(); + for (VmHostFileVO file : context.hostFiles) { + filesByHost.computeIfAbsent(file.getHostUuid(), k -> new ArrayList<>()).add(file); + } + + new While<>(filesByHost.entrySet()).each((entry, whileCompletion) -> { + String hostUuid = entry.getKey(); + List files = entry.getValue(); + + KVMAgentCommands.WriteVmHostFileContentCmd cmd = new KVMAgentCommands.WriteVmHostFileContentCmd(); + List fileTOs = new ArrayList<>(); + for (VmHostFileVO file : files) { + KVMAgentCommands.VmHostFileTO to = new KVMAgentCommands.VmHostFileTO(); + to.setPath(file.getPath()); + to.setType(file.getType().toString()); + to.setOperation(VmHostFileOperation.Delete.toString()); + fileTOs.add(to); + } + cmd.setHostFiles(fileTOs); + + new KvmCommandSender(hostUuid).send(cmd, KVMConstant.WRITE_VM_HOST_FILE_PATH, wrapper -> { + KVMAgentCommands.WriteVmHostFileContentResponse rsp = + wrapper.getResponse(KVMAgentCommands.WriteVmHostFileContentResponse.class); + return rsp.isSuccess() ? null : operr("failed to delete host files on host[uuid=%s]", hostUuid); + }, new ReturnValueCompletion(whileCompletion) { + @Override + public void success(KvmResponseWrapper wrapper) { + whileCompletion.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + logger.warn(String.format("failed to delete host files on host[uuid=%s], but continuing with DB cleanup: %s", + hostUuid, errorCode.getDetails())); + whileCompletion.done(); + } + }); + }).run(new WhileDoneCompletion(trigger) { + @Override + public void done(ErrorCodeList errorCodeList) { + trigger.next(); + } + }); + }) + .build()) .then(Flow.of("detach-resource-key") .handle(trigger -> { tpmKeyBackend.detachKeyProviderFromTpm(context.tpmUuid); trigger.next(); }) .build()) - .then(Flow.of("remove-tpm-db-records") + .then(Flow.of("remove-db-records") .handle(trigger -> { new SQLBatch() { @Override protected void scripts() { - Set types = new HashSet<>(); - types.add(VmHostFileType.TpmState); - sql(TpmVO.class) .eq(TpmVO_.uuid, context.tpmUuid) .delete(); - - boolean needRegisterNvRam = vmTpmManager.needRegisterNvRam(context.vmInstanceUuid); - if (!needRegisterNvRam) { - types.add(VmHostFileType.NvRam); - } - sql(VmHostFileVO.class) .eq(VmHostFileVO_.vmInstanceUuid, context.vmInstanceUuid) - .in(VmHostFileVO_.type, types) + .eq(VmHostFileVO_.type, VmHostFileType.TpmState) .delete(); sql(VmHostBackupFileVO.class) .eq(VmHostBackupFileVO_.resourceUuid, context.vmInstanceUuid) - .in(VmHostBackupFileVO_.type, types) + .eq(VmHostBackupFileVO_.type, VmHostFileType.TpmState) .delete(); } }.execute(); From f48ebc552758323fcea7e7b68371f069eabb6aa4 Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Thu, 2 Apr 2026 17:57:38 +0800 Subject: [PATCH 100/129] [vm]: add vm metadata header definitions Resolves: ZSV-11559 Change-Id: I3cd9fa26b6af7bd47787ab88c5050683a3356059 --- ...stanceMetadataFromPrimaryStorageEvent.java | 33 +++++ ...ataFromPrimaryStorageEventDoc_zh_cn.groovy | 32 ++++ ...InstanceMetadataFromPrimaryStorageMsg.java | 35 +++++ ...adataFromPrimaryStorageMsgDoc_zh_cn.groovy | 58 ++++++++ ...VmInstanceMetadataOnPrimaryStorageMsg.java | 52 +++++++ ...InstanceMetadataOnPrimaryStorageReply.java | 6 + ...InstanceMetadataFromPrimaryStorageMsg.java | 44 ++++++ ...stanceMetadataFromPrimaryStorageReply.java | 15 ++ .../primary/ReadVmInstanceMetadataMsg.java | 21 +++ .../primary/ReadVmInstanceMetadataReply.java | 15 ++ ...eVolumeBackingFileOnPrimaryStorageMsg.java | 70 +++++++++ ...olumeBackingFileOnPrimaryStorageReply.java | 15 ++ ...InstanceMetadataFromPrimaryStorageMsg.java | 25 ++++ ...stanceMetadataFromPrimaryStorageReply.java | 18 +++ .../storage/primary/VmMetadataScanEntry.java | 94 ++++++++++++ .../VmMetadataScanEntryDoc_zh_cn.groovy | 67 +++++++++ .../vm/APICleanupVmInstanceMetadataEvent.java | 53 +++++++ ...nupVmInstanceMetadataEventDoc_zh_cn.groovy | 42 ++++++ .../vm/APICleanupVmInstanceMetadataMsg.java | 33 +++++ ...eanupVmInstanceMetadataMsgDoc_zh_cn.groovy | 58 ++++++++ ...stanceMetadataFromPrimaryStorageEvent.java | 30 ++++ ...ataFromPrimaryStorageEventDoc_zh_cn.groovy | 29 ++++ ...InstanceMetadataFromPrimaryStorageMsg.java | 35 +++++ ...adataFromPrimaryStorageMsgDoc_zh_cn.groovy | 58 ++++++++ ...PIRegisterVmInstanceFromMetadataEvent.java | 34 +++++ ...mInstanceFromMetadataEventDoc_zh_cn.groovy | 32 ++++ .../APIRegisterVmInstanceFromMetadataMsg.java | 102 +++++++++++++ ...rVmInstanceFromMetadataMsgDoc_zh_cn.groovy | 112 ++++++++++++++ .../vm/APIUpdateVmInstanceMetadataEvent.java | 19 +++ ...ateVmInstanceMetadataEventDoc_zh_cn.groovy | 23 +++ .../vm/APIUpdateVmInstanceMetadataMsg.java | 34 +++++ ...pdateVmInstanceMetadataMsgDoc_zh_cn.groovy | 58 ++++++++ .../org/zstack/header/vm/VmInstanceState.java | 7 +- .../header/vm/metadata/MetadataImpact.java | 34 +++++ .../header/vm/metadata/ResourceMetadata.java | 40 +++++ .../metadata/UpdateVmInstanceMetadataMsg.java | 26 ++++ ...VmInstanceMetadataOnPrimaryStorageMsg.java | 98 +++++++++++++ ...InstanceMetadataOnPrimaryStorageReply.java | 6 + .../UpdateVmInstanceMetadataReply.java | 15 ++ .../metadata/VmInstanceMetadataConstants.java | 30 ++++ .../vm/metadata/VmInstanceMetadataDTO.java | 78 ++++++++++ .../vm/metadata/VmMetadataCategory.java | 7 + .../vm/metadata/VmMetadataDirtyService.java | 29 ++++ .../header/vm/metadata/VmMetadataDirtyVO.java | 120 +++++++++++++++ .../vm/metadata/VmMetadataDirtyVO_.java | 18 +++ .../vm/metadata/VmMetadataFlushStateVO.java | 137 ++++++++++++++++++ .../vm/metadata/VmMetadataFlushStateVO_.java | 16 ++ .../VmMetadataPathBuildExtensionPoint.java | 11 ++ ...MetadataPathReplacementExtensionPoint.java | 42 ++++++ ...MetadataResourcePersistExtensionPoint.java | 16 ++ .../vm/metadata/VmUuidFromApiResolver.java | 25 ++++ .../vm/metadata/VolumeResourceMetadata.java | 22 +++ 52 files changed, 2128 insertions(+), 1 deletion(-) create mode 100644 header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEvent.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEventDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsg.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageMsg.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageReply.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageMsg.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageReply.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataMsg.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataReply.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageMsg.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageReply.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageMsg.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageReply.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/VmMetadataScanEntry.java create mode 100644 header/src/main/java/org/zstack/header/storage/primary/VmMetadataScanEntryDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataEvent.java create mode 100644 header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataEventDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataMsgDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/vm/APIGetVmInstanceMetadataFromPrimaryStorageEvent.java create mode 100644 header/src/main/java/org/zstack/header/vm/APIGetVmInstanceMetadataFromPrimaryStorageEventDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/vm/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/APIGetVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataEvent.java create mode 100644 header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataEventDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataMsgDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMetadataEvent.java create mode 100644 header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMetadataEventDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMetadataMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMetadataMsgDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/MetadataImpact.java create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/ResourceMetadata.java create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageMsg.java create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageReply.java create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataReply.java create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataDTO.java create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/VmMetadataCategory.java create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyService.java create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO.java create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO_.java create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/VmMetadataFlushStateVO.java create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/VmMetadataFlushStateVO_.java create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathBuildExtensionPoint.java create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathReplacementExtensionPoint.java create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/VmMetadataResourcePersistExtensionPoint.java create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java create mode 100644 header/src/main/java/org/zstack/header/vm/metadata/VolumeResourceMetadata.java diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEvent.java b/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEvent.java new file mode 100644 index 00000000000..f3c5adad2fc --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEvent.java @@ -0,0 +1,33 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.APIEvent; +import org.zstack.header.rest.RestResponse; + +import java.util.ArrayList; +import java.util.List; + +@RestResponse(fieldsTo = {"all"}) +public class APIScanVmInstanceMetadataFromPrimaryStorageEvent extends APIEvent { + private List vmInstanceMetadata = new ArrayList<>(); + + public APIScanVmInstanceMetadataFromPrimaryStorageEvent() { + super(null); + } + + public APIScanVmInstanceMetadataFromPrimaryStorageEvent(String apiId) { + super(apiId); + } + + public List getVmInstanceMetadata() { + return vmInstanceMetadata; + } + + public void setVmInstanceMetadata(List vmInstanceMetadata) { + this.vmInstanceMetadata = vmInstanceMetadata == null ? new ArrayList<>() : vmInstanceMetadata; + } + + public static APIScanVmInstanceMetadataFromPrimaryStorageEvent __example__() { + APIScanVmInstanceMetadataFromPrimaryStorageEvent evt = new APIScanVmInstanceMetadataFromPrimaryStorageEvent(); + return evt; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEventDoc_zh_cn.groovy new file mode 100644 index 00000000000..692e48f08a0 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEventDoc_zh_cn.groovy @@ -0,0 +1,32 @@ +package org.zstack.header.storage.primary + + +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "扫描主存储上的云主机元数据返回" + + ref { + name "vmInstanceMetadata" + path "org.zstack.header.storage.primary.APIScanVmInstanceMetadataFromPrimaryStorageEvent.vmInstanceMetadata" + desc "云主机元数据摘要列表" + type "List" + since "5.0.0" + clz VmMetadataScanEntry.class + } + field { + name "success" + desc "操作是否成功" + type "boolean" + since "5.0.0" + } + ref { + name "error" + path "org.zstack.header.storage.primary.APIScanVmInstanceMetadataFromPrimaryStorageEvent.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/APIScanVmInstanceMetadataFromPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsg.java new file mode 100644 index 00000000000..411a568d5a0 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsg.java @@ -0,0 +1,35 @@ +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.rest.RestRequest; + +@RestRequest( + path = "/primary-storage/vm-instances/metadata/scan", + method = HttpMethod.GET, + responseClass = APIScanVmInstanceMetadataFromPrimaryStorageEvent.class +) +public class APIScanVmInstanceMetadataFromPrimaryStorageMsg extends APIMessage implements PrimaryStorageMessage { + @APIParam(resourceType = PrimaryStorageVO.class) + private String uuid; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + @Override + public String getPrimaryStorageUuid() { + return uuid; + } + + public static APIScanVmInstanceMetadataFromPrimaryStorageMsg __example__() { + APIScanVmInstanceMetadataFromPrimaryStorageMsg msg = new APIScanVmInstanceMetadataFromPrimaryStorageMsg(); + msg.setUuid(uuid()); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..090f2a36075 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy @@ -0,0 +1,58 @@ +package org.zstack.header.storage.primary + +import org.zstack.header.storage.primary.APIScanVmInstanceMetadataFromPrimaryStorageEvent + +doc { + title "扫描主存储上的云主机元数据" + + category "主存储" + + desc """扫描指定主存储上所有云主机元数据文件,返回元数据摘要列表""" + + rest { + request { + url "GET /v1/primary-storage/vm-instances/metadata/scan" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIScanVmInstanceMetadataFromPrimaryStorageMsg.class + + desc """""" + + params { + + column { + name "uuid" + enclosedIn "" + desc "主存储UUID" + location "query" + 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 APIScanVmInstanceMetadataFromPrimaryStorageEvent.class + } + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageMsg.java new file mode 100644 index 00000000000..4e508032775 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageMsg.java @@ -0,0 +1,52 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.NeedReplyMessage; + +public class CleanupVmInstanceMetadataOnPrimaryStorageMsg extends NeedReplyMessage implements PrimaryStorageMessage { + private String primaryStorageUuid; + private String vmInstanceUuid; + private String rootVolumeUuid; + private String metadataPath; + private String hostUuid; + + @Override + public String getPrimaryStorageUuid() { + return primaryStorageUuid; + } + + public void setPrimaryStorageUuid(String primaryStorageUuid) { + this.primaryStorageUuid = primaryStorageUuid; + } + + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public String getRootVolumeUuid() { + return rootVolumeUuid; + } + + public void setRootVolumeUuid(String rootVolumeUuid) { + this.rootVolumeUuid = rootVolumeUuid; + } + + public String getMetadataPath() { + return metadataPath; + } + + public void setMetadataPath(String metadataPath) { + this.metadataPath = metadataPath; + } + + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageReply.java b/header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageReply.java new file mode 100644 index 00000000000..05bba3ac430 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageReply.java @@ -0,0 +1,6 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.MessageReply; + +public class CleanupVmInstanceMetadataOnPrimaryStorageReply extends MessageReply { +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageMsg.java new file mode 100644 index 00000000000..67ea49e5e18 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageMsg.java @@ -0,0 +1,44 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.NeedReplyMessage; + + +public class GetVmInstanceMetadataFromPrimaryStorageMsg extends NeedReplyMessage implements PrimaryStorageMessage { + private String primaryStorageUuid; + private String metadataPath; + private String rootVolumeUuid; + private String hostUuid; + + @Override + public String getPrimaryStorageUuid() { + return primaryStorageUuid; + } + + public void setPrimaryStorageUuid(String primaryStorageUuid) { + this.primaryStorageUuid = primaryStorageUuid; + } + + public String getMetadataPath() { + return metadataPath; + } + + public void setMetadataPath(String metadataPath) { + this.metadataPath = metadataPath; + } + + public String getRootVolumeUuid() { + return rootVolumeUuid; + } + + public void setRootVolumeUuid(String rootVolumeUuid) { + this.rootVolumeUuid = rootVolumeUuid; + } + + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageReply.java b/header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageReply.java new file mode 100644 index 00000000000..c164a99792d --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageReply.java @@ -0,0 +1,15 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.MessageReply; + +public class GetVmInstanceMetadataFromPrimaryStorageReply extends MessageReply { + private String metadata; + + public String getMetadata() { + return metadata; + } + + public void setMetadata(String metadata) { + this.metadata = metadata; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataMsg.java b/header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataMsg.java new file mode 100644 index 00000000000..ed09f15eb4d --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataMsg.java @@ -0,0 +1,21 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.NeedReplyMessage; +import org.zstack.header.vm.VmInstanceMessage; + +public class ReadVmInstanceMetadataMsg extends NeedReplyMessage implements VmInstanceMessage { + private String uuid; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + @Override + public String getVmInstanceUuid() { + return uuid; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataReply.java b/header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataReply.java new file mode 100644 index 00000000000..04462f849ad --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataReply.java @@ -0,0 +1,15 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.MessageReply; + +public class ReadVmInstanceMetadataReply extends MessageReply { + private String vmMetadata; + + public String getVmMetadata() { + return vmMetadata; + } + + public void setVmMetadata(String vmMetadata) { + this.vmMetadata = vmMetadata; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageMsg.java new file mode 100644 index 00000000000..f9dd68d2934 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageMsg.java @@ -0,0 +1,70 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.NeedReplyMessage; + +import java.util.List; + +/** + * 请求目标主存储对指定文件做 backing file 前缀替换(prefix rebase)。 + *

+ * 各存储插件(LocalStorage / SharedBlock / NFS)自行选择 host、构造 agent command 并发送。 + */ +public class RebaseVolumeBackingFileOnPrimaryStorageMsg extends NeedReplyMessage implements PrimaryStorageMessage { + private String primaryStorageUuid; + + /** + * volume + snapshot 的 installPath 列表(已做路径替换的逻辑路径)。 + * LocalStorage / NFS 下即绝对路径;SharedBlock 下为 sharedblock:// scheme 路径,由插件内部转绝对路径。 + */ + private List installPaths; + + /** 旧路径前缀 */ + private String oldPrefix; + + /** 新路径前缀 */ + private String newPrefix; + + /** 注册请求指定的 hostUuid(LocalStorage 需要,其他存储可忽略) */ + private String hostUuid; + + @Override + public String getPrimaryStorageUuid() { + return primaryStorageUuid; + } + + public void setPrimaryStorageUuid(String primaryStorageUuid) { + this.primaryStorageUuid = primaryStorageUuid; + } + + public List getInstallPaths() { + return installPaths; + } + + public void setInstallPaths(List installPaths) { + this.installPaths = installPaths; + } + + public String getOldPrefix() { + return oldPrefix; + } + + public void setOldPrefix(String oldPrefix) { + this.oldPrefix = oldPrefix; + } + + public String getNewPrefix() { + return newPrefix; + } + + public void setNewPrefix(String newPrefix) { + this.newPrefix = newPrefix; + } + + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageReply.java b/header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageReply.java new file mode 100644 index 00000000000..44043421f09 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageReply.java @@ -0,0 +1,15 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.MessageReply; + +public class RebaseVolumeBackingFileOnPrimaryStorageReply extends MessageReply { + private int rebasedCount; + + public int getRebasedCount() { + return rebasedCount; + } + + public void setRebasedCount(int rebasedCount) { + this.rebasedCount = rebasedCount; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageMsg.java new file mode 100644 index 00000000000..052e77f7507 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageMsg.java @@ -0,0 +1,25 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.NeedReplyMessage; + +public class ScanVmInstanceMetadataFromPrimaryStorageMsg extends NeedReplyMessage implements PrimaryStorageMessage { + private String primaryStorageUuid; + private String metadataDir; + + @Override + public String getPrimaryStorageUuid() { + return primaryStorageUuid; + } + + public void setPrimaryStorageUuid(String primaryStorageUuid) { + this.primaryStorageUuid = primaryStorageUuid; + } + + public String getMetadataDir() { + return metadataDir; + } + + public void setMetadataDir(String metadataDir) { + this.metadataDir = metadataDir; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageReply.java b/header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageReply.java new file mode 100644 index 00000000000..59ecf8a9ce5 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageReply.java @@ -0,0 +1,18 @@ +package org.zstack.header.storage.primary; + +import org.zstack.header.message.MessageReply; + +import java.util.ArrayList; +import java.util.List; + +public class ScanVmInstanceMetadataFromPrimaryStorageReply extends MessageReply { + private List vmInstanceMetadata = new ArrayList<>(); + + public List getVmInstanceMetadata() { + return vmInstanceMetadata; + } + + public void setVmInstanceMetadata(List vmInstanceMetadata) { + this.vmInstanceMetadata = vmInstanceMetadata == null ? new ArrayList<>() : new ArrayList<>(vmInstanceMetadata); + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/VmMetadataScanEntry.java b/header/src/main/java/org/zstack/header/storage/primary/VmMetadataScanEntry.java new file mode 100644 index 00000000000..9859bc2aca7 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/VmMetadataScanEntry.java @@ -0,0 +1,94 @@ +package org.zstack.header.storage.primary; + +public class VmMetadataScanEntry { + private String vmUuid; + private String vmName; + private String vmCategory; + private String architecture; + private String schemaVersion; + private String metadataPath; + private String hostUuid; + private long sizeBytes; + private long lastUpdateTime; + private boolean incomplete; + + public String getVmUuid() { + return vmUuid; + } + + public void setVmUuid(String vmUuid) { + this.vmUuid = vmUuid; + } + + public String getVmName() { + return vmName; + } + + public void setVmName(String vmName) { + this.vmName = vmName; + } + + public String getVmCategory() { + return vmCategory; + } + + public void setVmCategory(String vmCategory) { + this.vmCategory = vmCategory; + } + + public String getArchitecture() { + return architecture; + } + + public void setArchitecture(String architecture) { + this.architecture = architecture; + } + + public String getSchemaVersion() { + return schemaVersion; + } + + public void setSchemaVersion(String schemaVersion) { + this.schemaVersion = schemaVersion; + } + + public String getMetadataPath() { + return metadataPath; + } + + public void setMetadataPath(String metadataPath) { + this.metadataPath = metadataPath; + } + + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + public long getSizeBytes() { + return sizeBytes; + } + + public void setSizeBytes(long sizeBytes) { + this.sizeBytes = sizeBytes; + } + + public long getLastUpdateTime() { + return lastUpdateTime; + } + + public void setLastUpdateTime(long lastUpdateTime) { + this.lastUpdateTime = lastUpdateTime; + } + + public boolean isIncomplete() { + return incomplete; + } + + public void setIncomplete(boolean incomplete) { + this.incomplete = incomplete; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/primary/VmMetadataScanEntryDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/primary/VmMetadataScanEntryDoc_zh_cn.groovy new file mode 100644 index 00000000000..4a2e0fdf060 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/primary/VmMetadataScanEntryDoc_zh_cn.groovy @@ -0,0 +1,67 @@ +package org.zstack.header.storage.primary + +doc { + + title "虚拟机元数据扫描数据" + + field { + name "vmUuid" + desc "" + type "String" + since "5.0.0" + } + field { + name "vmName" + desc "" + type "String" + since "5.0.0" + } + field { + name "vmCategory" + desc "" + type "String" + since "5.0.0" + } + field { + name "architecture" + desc "" + type "String" + since "5.0.0" + } + field { + name "schemaVersion" + desc "" + type "String" + since "5.0.0" + } + field { + name "metadataPath" + desc "" + type "String" + since "5.0.0" + } + field { + name "hostUuid" + desc "" + type "String" + since "5.0.0" + } + field { + name "sizeBytes" + desc "" + type "Long" + since "5.0.0" + } + field { + name "lastUpdateTime" + desc "" + type "Long" + since "5.0.0" + } + field { + name "incomplete" + desc "" + type "boolean" + since "5.0.0" + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataEvent.java b/header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataEvent.java new file mode 100644 index 00000000000..60a441a626d --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataEvent.java @@ -0,0 +1,53 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.APIEvent; +import org.zstack.header.rest.RestResponse; + +import java.util.List; + +@RestResponse(fieldsTo = {"all"}) +public class APICleanupVmInstanceMetadataEvent extends APIEvent { + private Integer totalCleaned; + private Integer totalFailed; + private List failedVmUuids; + + public APICleanupVmInstanceMetadataEvent() { + super(null); + } + + public APICleanupVmInstanceMetadataEvent(String apiId) { + super(apiId); + } + + public Integer getTotalCleaned() { + return totalCleaned; + } + + public void setTotalCleaned(Integer totalCleaned) { + this.totalCleaned = totalCleaned; + } + + public Integer getTotalFailed() { + return totalFailed; + } + + public void setTotalFailed(Integer totalFailed) { + this.totalFailed = totalFailed; + } + + public List getFailedVmUuids() { + return failedVmUuids; + } + + public void setFailedVmUuids(List failedVmUuids) { + this.failedVmUuids = failedVmUuids; + } + + public static APICleanupVmInstanceMetadataEvent __example__() { + APICleanupVmInstanceMetadataEvent evt = new APICleanupVmInstanceMetadataEvent(); + evt.totalCleaned = 5; + evt.totalFailed = 0; + evt.failedVmUuids = java.util.Collections.emptyList(); + return evt; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataEventDoc_zh_cn.groovy new file mode 100644 index 00000000000..2855bb47b72 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataEventDoc_zh_cn.groovy @@ -0,0 +1,42 @@ +package org.zstack.header.vm + + +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "清理云主机元数据返回" + + field { + name "totalCleaned" + desc "成功清理的元数据数量" + type "Integer" + since "5.0.0" + } + field { + name "totalFailed" + desc "清理失败的元数据数量" + type "Integer" + since "5.0.0" + } + field { + name "failedVmUuids" + desc "清理失败的云主机UUID列表" + type "List" + since "5.0.0" + } + field { + name "success" + desc "操作是否成功" + type "boolean" + since "5.0.0" + } + ref { + name "error" + path "org.zstack.header.vm.APICleanupVmInstanceMetadataEvent.error" + desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null" + type "ErrorCode" + since "5.0.0" + clz ErrorCode.class + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataMsg.java b/header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataMsg.java new file mode 100644 index 00000000000..e75d807c3c0 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataMsg.java @@ -0,0 +1,33 @@ +package org.zstack.header.vm; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.rest.RestRequest; + +import java.util.List; + +@RestRequest( + path = "/vm-instances/metadata/cleanup", + method = HttpMethod.PUT, + responseClass = APICleanupVmInstanceMetadataEvent.class, + isAction = true +) +public class APICleanupVmInstanceMetadataMsg extends APIMessage { + @APIParam(resourceType = VmInstanceVO.class, nonempty = true) + private List vmUuids; + + public List getVmUuids() { + return vmUuids; + } + + public void setVmUuids(List vmUuids) { + this.vmUuids = vmUuids; + } + + public static APICleanupVmInstanceMetadataMsg __example__() { + APICleanupVmInstanceMetadataMsg msg = new APICleanupVmInstanceMetadataMsg(); + msg.vmUuids = java.util.Arrays.asList(uuid(), uuid()); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..6f7bd0827f4 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APICleanupVmInstanceMetadataMsgDoc_zh_cn.groovy @@ -0,0 +1,58 @@ +package org.zstack.header.vm + +import org.zstack.header.vm.APICleanupVmInstanceMetadataEvent + +doc { + title "清理云主机元数据" + + category "云主机" + + desc """清理指定云主机在主存储上的元数据文件""" + + rest { + request { + url "PUT /v1/vm-instances/metadata/cleanup" + + header (Authorization: 'OAuth the-session-uuid') + + clz APICleanupVmInstanceMetadataMsg.class + + desc """""" + + params { + + column { + name "vmUuids" + enclosedIn "cleanupVmInstanceMetadata" + desc "需要清理元数据的云主机UUID列表" + location "body" + type "List" + 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 APICleanupVmInstanceMetadataEvent.class + } + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmInstanceMetadataFromPrimaryStorageEvent.java b/header/src/main/java/org/zstack/header/vm/APIGetVmInstanceMetadataFromPrimaryStorageEvent.java new file mode 100644 index 00000000000..89087ba98cd --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmInstanceMetadataFromPrimaryStorageEvent.java @@ -0,0 +1,30 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.APIEvent; +import org.zstack.header.rest.RestResponse; + +@RestResponse(fieldsTo = {"all"}) +public class APIGetVmInstanceMetadataFromPrimaryStorageEvent extends APIEvent { + private String metadata; + + public APIGetVmInstanceMetadataFromPrimaryStorageEvent() { + super(null); + } + + public APIGetVmInstanceMetadataFromPrimaryStorageEvent(String apiId) { + super(apiId); + } + + public String getMetadata() { + return metadata; + } + + public void setMetadata(String metadata) { + this.metadata = metadata; + } + + public static APIGetVmInstanceMetadataFromPrimaryStorageEvent __example__() { + APIGetVmInstanceMetadataFromPrimaryStorageEvent evt = new APIGetVmInstanceMetadataFromPrimaryStorageEvent(); + return evt; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmInstanceMetadataFromPrimaryStorageEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/vm/APIGetVmInstanceMetadataFromPrimaryStorageEventDoc_zh_cn.groovy new file mode 100644 index 00000000000..8521f4ca708 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmInstanceMetadataFromPrimaryStorageEventDoc_zh_cn.groovy @@ -0,0 +1,29 @@ +package org.zstack.header.vm + +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "获取云主机元数据返回" + + field { + name "metadata" + desc "云主机元数据内容" + type "String" + since "5.0.0" + } + field { + name "success" + desc "操作是否成功" + type "boolean" + since "5.0.0" + } + ref { + name "error" + path "org.zstack.header.vm.APIGetVmInstanceMetadataFromPrimaryStorageEvent.error" + desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null" + type "ErrorCode" + since "5.0.0" + clz ErrorCode.class + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/vm/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java new file mode 100644 index 00000000000..686bb1ba587 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java @@ -0,0 +1,35 @@ +package org.zstack.header.vm; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.rest.RestRequest; + +@RestRequest( + path = "/primary-storage/vm-instances/metadata", + method = HttpMethod.GET, + responseClass = APIGetVmInstanceMetadataFromPrimaryStorageEvent.class +) +public class APIGetVmInstanceMetadataFromPrimaryStorageMsg extends APIMessage implements VmInstanceMessage { + @APIParam(resourceType = VmInstanceVO.class) + private String uuid; + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + @Override + public String getVmInstanceUuid() { + return uuid; + } + + public static APIGetVmInstanceMetadataFromPrimaryStorageMsg __example__() { + APIGetVmInstanceMetadataFromPrimaryStorageMsg msg = new APIGetVmInstanceMetadataFromPrimaryStorageMsg(); + msg.setUuid(uuid(VmInstanceVO.class)); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIGetVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/vm/APIGetVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..560a4494010 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIGetVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy @@ -0,0 +1,58 @@ +package org.zstack.header.vm + +import org.zstack.header.vm.APIGetVmInstanceMetadataFromPrimaryStorageEvent + +doc { + title "获取云主机元数据" + + category "主存储" + + desc """从主存储获取指定云主机的元数据内容""" + + rest { + request { + url "GET /v1/primary-storage/vm-instances/metadata" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIGetVmInstanceMetadataFromPrimaryStorageMsg.class + + desc """""" + + params { + + column { + name "uuid" + enclosedIn "" + desc "云主机UUID" + location "query" + 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 APIGetVmInstanceMetadataFromPrimaryStorageEvent.class + } + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataEvent.java b/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataEvent.java new file mode 100644 index 00000000000..b2d721412ab --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataEvent.java @@ -0,0 +1,34 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.APIEvent; +import org.zstack.header.rest.RestResponse; + +@RestResponse(fieldsTo = {"all"}) +public class APIRegisterVmInstanceFromMetadataEvent extends APIEvent { + private VmInstanceInventory inventory; + + public APIRegisterVmInstanceFromMetadataEvent() { + super(null); + } + + public APIRegisterVmInstanceFromMetadataEvent(String apiId) { + super(apiId); + } + + public VmInstanceInventory getInventory() { + return inventory; + } + + public void setInventory(VmInstanceInventory inventory) { + this.inventory = inventory; + } + + public static APIRegisterVmInstanceFromMetadataEvent __example__() { + APIRegisterVmInstanceFromMetadataEvent evt = new APIRegisterVmInstanceFromMetadataEvent(); + VmInstanceInventory vm = new VmInstanceInventory(); + vm.setUuid(uuid()); + vm.setName("recovered-vm"); + evt.setInventory(vm); + return evt; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataEventDoc_zh_cn.groovy new file mode 100644 index 00000000000..3ce91baf280 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataEventDoc_zh_cn.groovy @@ -0,0 +1,32 @@ +package org.zstack.header.vm + + +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "从元数据注册云主机返回" + + ref { + name "inventory" + path "org.zstack.header.vm.APIRegisterVmInstanceFromMetadataEvent.inventory" + desc "云主机详情" + type "VmInstanceInventory" + since "5.0.0" + clz VmInstanceInventory.class + } + field { + name "success" + desc "操作是否成功" + type "boolean" + since "5.0.0" + } + ref { + name "error" + path "org.zstack.header.vm.APIRegisterVmInstanceFromMetadataEvent.error" + desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null" + type "ErrorCode" + since "5.0.0" + clz ErrorCode.class + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataMsg.java b/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataMsg.java new file mode 100644 index 00000000000..f900a635429 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataMsg.java @@ -0,0 +1,102 @@ +package org.zstack.header.vm; + +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 org.zstack.header.storage.primary.PrimaryStorageVO; +import org.zstack.header.zone.ZoneVO; +import org.zstack.header.cluster.ClusterVO; +import org.zstack.header.host.HostVO; +import org.zstack.header.tag.TagResourceType; + +import java.util.concurrent.TimeUnit; + +@TagResourceType(VmInstanceVO.class) +@RestRequest( + path = "/vm-instances/metadata/register", + method = HttpMethod.POST, + responseClass = APIRegisterVmInstanceFromMetadataEvent.class, + parameterName = "params" +) +@DefaultTimeout(timeunit = TimeUnit.HOURS, value = 1) +public class APIRegisterVmInstanceFromMetadataMsg extends APIMessage { + @APIParam(nonempty = true, maxLength = 2048) + private String metadataPath; + + @APIParam(resourceType = PrimaryStorageVO.class) + private String primaryStorageUuid; + + @APIParam(resourceType = ZoneVO.class) + private String zoneUuid; + + @APIParam(resourceType = ClusterVO.class) + private String clusterUuid; + + @APIParam(required = false, resourceType = HostVO.class) + private String hostUuid; + + @APIParam(required = false, maxLength = 255) + private String name; + + public String getMetadataPath() { + return metadataPath; + } + + public void setMetadataPath(String metadataPath) { + this.metadataPath = metadataPath; + } + + public String getPrimaryStorageUuid() { + return primaryStorageUuid; + } + + public void setPrimaryStorageUuid(String primaryStorageUuid) { + this.primaryStorageUuid = primaryStorageUuid; + } + + public String getZoneUuid() { + return zoneUuid; + } + + public void setZoneUuid(String zoneUuid) { + this.zoneUuid = zoneUuid; + } + + public String getClusterUuid() { + return clusterUuid; + } + + public void setClusterUuid(String clusterUuid) { + this.clusterUuid = clusterUuid; + } + + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public static APIRegisterVmInstanceFromMetadataMsg __example__() { + APIRegisterVmInstanceFromMetadataMsg msg = new APIRegisterVmInstanceFromMetadataMsg(); + String vmUuid = uuid(VmInstanceVO.class); + msg.metadataPath = String.format("/mnt/ps/vm-metadata/%s.vmmeta", vmUuid); + msg.primaryStorageUuid = uuid(PrimaryStorageVO.class); + msg.zoneUuid = uuid(ZoneVO.class); + msg.clusterUuid = uuid(ClusterVO.class); + msg.hostUuid = uuid(HostVO.class); + msg.name = "my-restored-vm"; + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..26f687524ac --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIRegisterVmInstanceFromMetadataMsgDoc_zh_cn.groovy @@ -0,0 +1,112 @@ +package org.zstack.header.vm + +import org.zstack.header.vm.APIRegisterVmInstanceFromMetadataEvent + +doc { + title "从元数据注册云主机(RegisterVmInstanceFromMetadata)" + + category "云主机" + + desc """根据主存储上的元数据文件注册(恢复)一台云主机""" + + rest { + request { + url "POST /v1/vm-instances/metadata/register" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIRegisterVmInstanceFromMetadataMsg.class + + desc """""" + + params { + + column { + name "metadataPath" + enclosedIn "params" + desc "元数据文件在主存储上的路径" + location "body" + type "String" + optional false + since "5.0.0" + } + column { + name "primaryStorageUuid" + enclosedIn "params" + desc "目标主存储UUID" + location "body" + type "String" + optional false + since "5.0.0" + } + column { + name "zoneUuid" + enclosedIn "params" + desc "区域UUID" + location "body" + type "String" + optional false + since "5.0.0" + } + column { + name "clusterUuid" + enclosedIn "params" + desc "集群UUID" + location "body" + type "String" + optional false + since "5.0.0" + } + column { + name "hostUuid" + enclosedIn "params" + desc "物理机UUID" + location "body" + type "String" + optional true + since "5.0.0" + } + column { + name "name" + enclosedIn "params" + desc "云主机名称" + 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 APIRegisterVmInstanceFromMetadataEvent.class + } + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMetadataEvent.java b/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMetadataEvent.java new file mode 100644 index 00000000000..4736470890e --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMetadataEvent.java @@ -0,0 +1,19 @@ +package org.zstack.header.vm; + +import org.zstack.header.message.APIEvent; +import org.zstack.header.rest.RestResponse; + +@RestResponse(fieldsTo = {"all"}) +public class APIUpdateVmInstanceMetadataEvent extends APIEvent { + public APIUpdateVmInstanceMetadataEvent() { + super(null); + } + + public APIUpdateVmInstanceMetadataEvent(String apiId) { + super(apiId); + } + + public static APIUpdateVmInstanceMetadataEvent __example__() { + return new APIUpdateVmInstanceMetadataEvent(); + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMetadataEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMetadataEventDoc_zh_cn.groovy new file mode 100644 index 00000000000..5c133eed8df --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMetadataEventDoc_zh_cn.groovy @@ -0,0 +1,23 @@ +package org.zstack.header.vm + +import org.zstack.header.errorcode.ErrorCode + +doc { + + title "更新云主机元数据返回" + + field { + name "success" + desc "操作是否成功" + type "boolean" + since "5.0.0" + } + ref { + name "error" + path "org.zstack.header.vm.APIUpdateVmInstanceMetadataEvent.error" + desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null" + type "ErrorCode" + since "5.0.0" + clz ErrorCode.class + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMetadataMsg.java b/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMetadataMsg.java new file mode 100644 index 00000000000..e4ffabdadcc --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMetadataMsg.java @@ -0,0 +1,34 @@ +package org.zstack.header.vm; + +import org.springframework.http.HttpMethod; +import org.zstack.header.message.APIMessage; +import org.zstack.header.message.APIParam; +import org.zstack.header.rest.RestRequest; + +import java.util.Collections; +import java.util.List; + +@RestRequest( + path = "/vm-instances/metadata/actions", + method = HttpMethod.PUT, + responseClass = APIUpdateVmInstanceMetadataEvent.class, + isAction = true +) +public class APIUpdateVmInstanceMetadataMsg extends APIMessage { + @APIParam(resourceType = VmInstanceVO.class, nonempty = true) + private List vmUuids; + + public List getVmUuids() { + return vmUuids; + } + + public void setVmUuids(List vmUuids) { + this.vmUuids = vmUuids; + } + + public static APIUpdateVmInstanceMetadataMsg __example__() { + APIUpdateVmInstanceMetadataMsg msg = new APIUpdateVmInstanceMetadataMsg(); + msg.vmUuids = Collections.singletonList(uuid()); + return msg; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMetadataMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMetadataMsgDoc_zh_cn.groovy new file mode 100644 index 00000000000..3d4db5d283a --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMetadataMsgDoc_zh_cn.groovy @@ -0,0 +1,58 @@ +package org.zstack.header.vm + +import org.zstack.header.vm.APIUpdateVmInstanceMetadataEvent + +doc { + title "更新云主机元数据" + + category "云主机" + + desc """立即触发指定云主机的元数据更新""" + + rest { + request { + url "PUT /v1/vm-instances/metadata/actions" + + header (Authorization: 'OAuth the-session-uuid') + + clz APIUpdateVmInstanceMetadataMsg.class + + desc """""" + + params { + + column { + name "vmUuids" + enclosedIn "updateVmInstanceMetadata" + desc "需要更新元数据的云主机UUID列表" + location "body" + type "List" + 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 APIUpdateVmInstanceMetadataEvent.class + } + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceState.java b/header/src/main/java/org/zstack/header/vm/VmInstanceState.java index b71d8f3be45..314ac3d8b0b 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceState.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceState.java @@ -30,7 +30,8 @@ public enum VmInstanceState { Error(null), NoState(VmInstanceStateEvent.noState), Unknown(VmInstanceStateEvent.unknown), - Crashed(VmInstanceStateEvent.crashed); + Crashed(VmInstanceStateEvent.crashed), + Registering(null); public static List intermediateStates = new ArrayList<>(); @@ -52,6 +53,7 @@ public enum VmInstanceState { offlineStates.add(Destroyed); offlineStates.add(VolumeMigrating); offlineStates.add(Crashed); + offlineStates.add(Registering); Created.transactions( new Transaction(VmInstanceStateEvent.starting, VmInstanceState.Starting), @@ -200,6 +202,9 @@ public enum VmInstanceState { new Transaction(VmInstanceStateEvent.noState, VmInstanceState.NoState), new Transaction(VmInstanceStateEvent.destroying, VmInstanceState.Destroying) ); + Registering.transactions( + new Transaction(VmInstanceStateEvent.stopped, VmInstanceState.Stopped) + ); } private VmInstanceState(VmInstanceStateEvent drivenEvent) { diff --git a/header/src/main/java/org/zstack/header/vm/metadata/MetadataImpact.java b/header/src/main/java/org/zstack/header/vm/metadata/MetadataImpact.java new file mode 100644 index 00000000000..96d6dc3238a --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/MetadataImpact.java @@ -0,0 +1,34 @@ +package org.zstack.header.vm.metadata; + +import java.lang.annotation.*; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MetadataImpact { + Impact value(); + + /** + * Spring bean id of the {@code VmUuidFromApiResolver} implementation. + * Required when value is CONFIG or STORAGE; leave default for NONE. + */ + String resolver() default ""; + + /** + * The API message field that the resolver reads to extract the VM UUID. + * Required when value is CONFIG or STORAGE; leave default for NONE. + */ + String field() default ""; + + /** + * Whether to update metadata even if the API execution fails. + * Required when value is CONFIG or STORAGE; leave default for NONE. + */ + boolean updateOnFailure() default false; + + enum Impact { + NONE, + CONFIG, + STORAGE + } +} diff --git a/header/src/main/java/org/zstack/header/vm/metadata/ResourceMetadata.java b/header/src/main/java/org/zstack/header/vm/metadata/ResourceMetadata.java new file mode 100644 index 00000000000..e450d65f3f6 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/ResourceMetadata.java @@ -0,0 +1,40 @@ +package org.zstack.header.vm.metadata; + +public class ResourceMetadata { + private String resourceUuid; + private String vo; + private String systemTags; + private String resourceConfigs; + + public String getResourceUuid() { + return resourceUuid; + } + + public void setResourceUuid(String resourceUuid) { + this.resourceUuid = resourceUuid; + } + + public String getVo() { + return vo; + } + + public void setVo(String vo) { + this.vo = vo; + } + + public String getSystemTags() { + return systemTags; + } + + public void setSystemTags(String systemTags) { + this.systemTags = systemTags; + } + + public String getResourceConfigs() { + return resourceConfigs; + } + + public void setResourceConfigs(String resourceConfigs) { + this.resourceConfigs = resourceConfigs; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataMsg.java b/header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataMsg.java new file mode 100644 index 00000000000..bf3956f41fc --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataMsg.java @@ -0,0 +1,26 @@ +package org.zstack.header.vm.metadata; + +import org.zstack.header.message.NeedReplyMessage; +import org.zstack.header.vm.VmInstanceMessage; + +public class UpdateVmInstanceMetadataMsg extends NeedReplyMessage implements VmInstanceMessage { + private String vmInstanceUuid; + private boolean storageStructureChange; + + @Override + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public boolean isStorageStructureChange() { + return storageStructureChange; + } + + public void setStorageStructureChange(boolean storageStructureChange) { + this.storageStructureChange = storageStructureChange; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageMsg.java new file mode 100644 index 00000000000..ba12e4b918e --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageMsg.java @@ -0,0 +1,98 @@ +package org.zstack.header.vm.metadata; + +import org.zstack.header.message.NeedReplyMessage; +import org.zstack.header.storage.primary.PrimaryStorageMessage; + +public class UpdateVmInstanceMetadataOnPrimaryStorageMsg extends NeedReplyMessage implements PrimaryStorageMessage { + private String primaryStorageUuid; + private String vmInstanceUuid; + private String rootVolumeUuid; + private String vmInstanceName; + private String vmCategory; + private String architecture; + private String metadata; + private String schemaVersion; + private boolean storageStructureChange; + private String metadataPath; + + @Override + public String getPrimaryStorageUuid() { + return primaryStorageUuid; + } + + public void setPrimaryStorageUuid(String primaryStorageUuid) { + this.primaryStorageUuid = primaryStorageUuid; + } + + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public String getRootVolumeUuid() { + return rootVolumeUuid; + } + + public void setRootVolumeUuid(String rootVolumeUuid) { + this.rootVolumeUuid = rootVolumeUuid; + } + + public String getVmInstanceName() { + return vmInstanceName; + } + + public void setVmInstanceName(String vmInstanceName) { + this.vmInstanceName = vmInstanceName; + } + + public String getVmCategory() { + return vmCategory; + } + + public void setVmCategory(String vmCategory) { + this.vmCategory = vmCategory; + } + + public String getArchitecture() { + return architecture; + } + + public void setArchitecture(String architecture) { + this.architecture = architecture; + } + + public String getMetadata() { + return metadata; + } + + public void setMetadata(String metadata) { + this.metadata = metadata; + } + + public String getSchemaVersion() { + return schemaVersion; + } + + public void setSchemaVersion(String schemaVersion) { + this.schemaVersion = schemaVersion; + } + + public boolean isStorageStructureChange() { + return storageStructureChange; + } + + public void setStorageStructureChange(boolean storageStructureChange) { + this.storageStructureChange = storageStructureChange; + } + + public String getMetadataPath() { + return metadataPath; + } + + public void setMetadataPath(String metadataPath) { + this.metadataPath = metadataPath; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageReply.java b/header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageReply.java new file mode 100644 index 00000000000..d8323171909 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageReply.java @@ -0,0 +1,6 @@ +package org.zstack.header.vm.metadata; + +import org.zstack.header.message.MessageReply; + +public class UpdateVmInstanceMetadataOnPrimaryStorageReply extends MessageReply { +} diff --git a/header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataReply.java b/header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataReply.java new file mode 100644 index 00000000000..98296a42b3c --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataReply.java @@ -0,0 +1,15 @@ +package org.zstack.header.vm.metadata; + +import org.zstack.header.message.MessageReply; + +public class UpdateVmInstanceMetadataReply extends MessageReply { + private String metadata; + + public String getMetadata() { + return metadata; + } + + public void setMetadata(String metadata) { + this.metadata = metadata; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java b/header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java new file mode 100644 index 00000000000..efdeb5f8bb2 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java @@ -0,0 +1,30 @@ +package org.zstack.header.vm.metadata; + +public class VmInstanceMetadataConstants { + private VmInstanceMetadataConstants() { + } + + public static final String SBLK_METADATA_LV_SUFFIX = "_vmmeta"; + public static final String FILE_METADATA_SUFFIX = ".vmmeta"; + public static final String METADATA_DIR_NAME = "vm-metadata"; + + private static final String[] STORAGE_PATH_MARKERS = { + "/rootVolumes/", "/dataVolumes/", "/volumeSnapshots/", "/memory/" + }; + + public static String extractOldPrefix(String path) { + if (path == null || !path.startsWith("/")) { + return null; + } + int earliest = Integer.MAX_VALUE; + String foundMarker = null; + for (String marker : STORAGE_PATH_MARKERS) { + int idx = path.indexOf(marker); + if (idx >= 0 && idx < earliest) { + earliest = idx; + foundMarker = marker; + } + } + return foundMarker == null ? null : path.substring(0, earliest + foundMarker.length()); + } +} \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataDTO.java b/header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataDTO.java new file mode 100644 index 00000000000..311ddbde784 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataDTO.java @@ -0,0 +1,78 @@ +package org.zstack.header.vm.metadata; + +import java.util.List; + +public class VmInstanceMetadataDTO { + private String schemaVersion; + private VmMetadataCategory vmCategory; + private ResourceMetadata vm; + private List volumes; + private List nics; + private List snapshots; + private List snapshotGroups; + private List snapshotGroupRefs; + + public String getSchemaVersion() { + return schemaVersion; + } + + public void setSchemaVersion(String schemaVersion) { + this.schemaVersion = schemaVersion; + } + + public VmMetadataCategory getVmCategory() { + return vmCategory; + } + + public void setVmCategory(VmMetadataCategory vmCategory) { + this.vmCategory = vmCategory; + } + + public ResourceMetadata getVm() { + return vm; + } + + public void setVm(ResourceMetadata vm) { + this.vm = vm; + } + + public List getVolumes() { + return volumes; + } + + public void setVolumes(List volumes) { + this.volumes = volumes; + } + + public List getNics() { + return nics; + } + + public void setNics(List nics) { + this.nics = nics; + } + + public List getSnapshots() { + return snapshots; + } + + public void setSnapshots(List snapshots) { + this.snapshots = snapshots; + } + + public List getSnapshotGroups() { + return snapshotGroups; + } + + public void setSnapshotGroups(List snapshotGroups) { + this.snapshotGroups = snapshotGroups; + } + + public List getSnapshotGroupRefs() { + return snapshotGroupRefs; + } + + public void setSnapshotGroupRefs(List snapshotGroupRefs) { + this.snapshotGroupRefs = snapshotGroupRefs; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataCategory.java b/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataCategory.java new file mode 100644 index 00000000000..a2a09028d27 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataCategory.java @@ -0,0 +1,7 @@ +package org.zstack.header.vm.metadata; + +public enum VmMetadataCategory { + VM, + VM_TEMPLATE, + VM_TEMPLATE_CACHE +} diff --git a/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyService.java b/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyService.java new file mode 100644 index 00000000000..ff17ffcf7f4 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyService.java @@ -0,0 +1,29 @@ +package org.zstack.header.vm.metadata; + +/** + * Service interface for marking VM metadata as dirty. + *

+ * Implementations live in the premium module (VmMetadataDirtyMarker). + * The zstack core module uses this interface via {@code @Autowired(required = false)} + * so that the feature degrades gracefully when the premium module is not loaded. + */ +public interface VmMetadataDirtyService { + /** + * Mark the VM's metadata as dirty so that it will be flushed + * to primary storage on the next poll cycle. + * + * @param vmInstanceUuid the VM whose metadata changed + * @return true if the mark was actually written (feature enabled), false otherwise + */ + boolean markDirty(String vmInstanceUuid); + + /** + * Mark the VM's metadata as dirty, optionally flagging a storage-structure change + * (e.g. volume attach/detach, snapshot, migration). + * + * @param vmInstanceUuid the VM whose metadata changed + * @param storageStructureChange true if the change affects storage topology + * @return true if the mark was actually written (feature enabled), false otherwise + */ + boolean markDirty(String vmInstanceUuid, boolean storageStructureChange); +} diff --git a/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO.java b/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO.java new file mode 100644 index 00000000000..d894bb756c3 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO.java @@ -0,0 +1,120 @@ +package org.zstack.header.vm.metadata; + +import org.zstack.header.managementnode.ManagementNodeVO; +import org.zstack.header.vm.VmInstanceEO; +import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vo.ForeignKey; +import org.zstack.header.vo.ForeignKey.ReferenceOption; + +import javax.persistence.*; +import java.sql.Timestamp; + +@Entity +public class VmMetadataDirtyVO { + @Id + @Column + @ForeignKey(parentEntityClass = VmInstanceEO.class, onDeleteAction = ReferenceOption.CASCADE) + private String vmInstanceUuid; + + @Column + @ForeignKey(parentEntityClass = ManagementNodeVO.class, onDeleteAction = ReferenceOption.SET_NULL) + private String managementNodeUuid; + + @Column + private long dirtyVersion = 1; + + @Column + private Timestamp lastClaimTime; + + @Column + private boolean storageStructureChange; + + @Column + private int retryCount; + + @Column + private Timestamp nextRetryTime; + + @Column + private Timestamp createDate; + + @Column + private Timestamp lastOpDate; + + @PreUpdate + private void preUpdate() { + lastOpDate = null; + } + + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public String getManagementNodeUuid() { + return managementNodeUuid; + } + + public void setManagementNodeUuid(String managementNodeUuid) { + this.managementNodeUuid = managementNodeUuid; + } + + public long getDirtyVersion() { + return dirtyVersion; + } + + public void setDirtyVersion(long dirtyVersion) { + this.dirtyVersion = dirtyVersion; + } + + public Timestamp getLastClaimTime() { + return lastClaimTime; + } + + public void setLastClaimTime(Timestamp lastClaimTime) { + this.lastClaimTime = lastClaimTime; + } + + public boolean isStorageStructureChange() { + return storageStructureChange; + } + + public void setStorageStructureChange(boolean storageStructureChange) { + this.storageStructureChange = storageStructureChange; + } + + public int getRetryCount() { + return retryCount; + } + + public void setRetryCount(int retryCount) { + this.retryCount = retryCount; + } + + public Timestamp getNextRetryTime() { + return nextRetryTime; + } + + public void setNextRetryTime(Timestamp nextRetryTime) { + this.nextRetryTime = nextRetryTime; + } + + 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/vm/metadata/VmMetadataDirtyVO_.java b/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO_.java new file mode 100644 index 00000000000..059ff6ad20f --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO_.java @@ -0,0 +1,18 @@ +package org.zstack.header.vm.metadata; + +import javax.persistence.metamodel.SingularAttribute; +import javax.persistence.metamodel.StaticMetamodel; +import java.sql.Timestamp; + +@StaticMetamodel(VmMetadataDirtyVO.class) +public class VmMetadataDirtyVO_ { + public static volatile SingularAttribute vmInstanceUuid; + public static volatile SingularAttribute managementNodeUuid; + public static volatile SingularAttribute dirtyVersion; + public static volatile SingularAttribute storageStructureChange; + public static volatile SingularAttribute retryCount; + public static volatile SingularAttribute nextRetryTime; + public static volatile SingularAttribute lastClaimTime; + public static volatile SingularAttribute createDate; + public static volatile SingularAttribute lastOpDate; +} diff --git a/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataFlushStateVO.java b/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataFlushStateVO.java new file mode 100644 index 00000000000..fdf9d8f300a --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataFlushStateVO.java @@ -0,0 +1,137 @@ +package org.zstack.header.vm.metadata; + +import org.zstack.header.vm.VmInstanceEO; +import org.zstack.header.vo.ForeignKey; +import org.zstack.header.vo.ForeignKey.ReferenceOption; + +import javax.persistence.*; +import java.sql.Timestamp; + +/** + * Records the metadata flush state for each VM instance, and tracks + * the stale-recovery lifecycle when a flush fails permanently. + * + *

Lifecycle

+ *
    + *
  1. Normal flush success: {@code metadataSnapshot} is updated, + * {@code pendingStaleRecovery} and {@code staleRecoveryCount} are reset to 0.
  2. + *
  3. Flush failure (retry exhausted): {@code pendingStaleRecovery} is set to {@code true}, + * the dirty row is deleted, and the VM enters the stale-recovery queue.
  4. + *
  5. Stale recovery task picks up the VM, calls {@code markDirty} to re-enter + * the flush flow, and increments {@code staleRecoveryCount}.
  6. + *
  7. Circuit breaker: when {@code staleRecoveryCount >= maxCycles}, + * both fields are reset to 0 (permanent-stale), requiring manual intervention + * via {@code APIUpdateVmInstanceMetadataMsg}.
  8. + *
+ * + *

Content drift detection

+ * A periodic task compares live-computed metadata against {@code metadataSnapshot}. + * If they differ, the VM is marked dirty for re-flush. + */ +@Entity +@Table +public class VmMetadataFlushStateVO { + @Id + @Column + @ForeignKey(parentEntityClass = VmInstanceEO.class, onDeleteAction = ReferenceOption.CASCADE) + private String vmInstanceUuid; + + /** + * Timestamp when the last successful flush completed. + * Null if the VM has never been flushed. + */ + @Column + private Timestamp lastFlushFinishTime; + + /** + * Whether this VM is waiting for the stale-recovery periodic task to + * re-queue it into the flush pipeline. Set to {@code true} when flush + * retry is exhausted; cleared on next successful flush or circuit break. + */ + @Column + private boolean pendingStaleRecovery; + + /** + * Number of stale-recovery cycles attempted so far. + * Incremented each time the recovery task re-queues this VM. + * When it reaches {@code maxCycles}, the circuit breaker fires + * and resets both this field and {@code pendingStaleRecovery} to 0. + */ + @Column + private int staleRecoveryCount; + + /** + * JSON snapshot of the VM metadata written during the last successful flush. + * Used by the content-drift detector to compare against live-computed metadata. + */ + @Column + @Lob + private String metadataSnapshot; + + @Column + private Timestamp createDate; + + @Column + private Timestamp lastOpDate; + + @PreUpdate + private void preUpdate() { + lastOpDate = null; + } + + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public Timestamp getLastFlushFinishTime() { + return lastFlushFinishTime; + } + + public void setLastFlushFinishTime(Timestamp lastFlushFinishTime) { + this.lastFlushFinishTime = lastFlushFinishTime; + } + + public boolean isPendingStaleRecovery() { + return pendingStaleRecovery; + } + + public void setPendingStaleRecovery(boolean pendingStaleRecovery) { + this.pendingStaleRecovery = pendingStaleRecovery; + } + + public int getStaleRecoveryCount() { + return staleRecoveryCount; + } + + public void setStaleRecoveryCount(int staleRecoveryCount) { + this.staleRecoveryCount = staleRecoveryCount; + } + + public String getMetadataSnapshot() { + return metadataSnapshot; + } + + public void setMetadataSnapshot(String metadataSnapshot) { + this.metadataSnapshot = metadataSnapshot; + } + + 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/vm/metadata/VmMetadataFlushStateVO_.java b/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataFlushStateVO_.java new file mode 100644 index 00000000000..945a4557523 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataFlushStateVO_.java @@ -0,0 +1,16 @@ +package org.zstack.header.vm.metadata; + +import javax.persistence.metamodel.SingularAttribute; +import javax.persistence.metamodel.StaticMetamodel; +import java.sql.Timestamp; + +@StaticMetamodel(VmMetadataFlushStateVO.class) +public class VmMetadataFlushStateVO_ { + public static volatile SingularAttribute vmInstanceUuid; + public static volatile SingularAttribute lastFlushFinishTime; + public static volatile SingularAttribute pendingStaleRecovery; + public static volatile SingularAttribute staleRecoveryCount; + public static volatile SingularAttribute metadataSnapshot; + public static volatile SingularAttribute createDate; + public static volatile SingularAttribute lastOpDate; +} diff --git a/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathBuildExtensionPoint.java b/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathBuildExtensionPoint.java new file mode 100644 index 00000000000..b386c900e54 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathBuildExtensionPoint.java @@ -0,0 +1,11 @@ +package org.zstack.header.vm.metadata; + +public interface VmMetadataPathBuildExtensionPoint { + String getPrimaryStorageType(); + String buildVmMetadataPath(String primaryStorageUuid, String vmInstanceUuid); + String buildMetadataDir(String primaryStorageUuid); + String validateMetadataPath(String primaryStorageUuid, String path); + default boolean requireHostForCleanup() { + return false; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathReplacementExtensionPoint.java b/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathReplacementExtensionPoint.java new file mode 100644 index 00000000000..989175ffe97 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathReplacementExtensionPoint.java @@ -0,0 +1,42 @@ +package org.zstack.header.vm.metadata; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public interface VmMetadataPathReplacementExtensionPoint { + String getPrimaryStorageType(); + PathReplacementResult calculatePathReplacements(String targetPsUuid, List allOldPaths); + class PathReplacementResult { + private Map metadataToCurrentPathMap = new HashMap<>(); + private String oldPrefix; + private String newPrefix; + + public PathReplacementResult() { + } + + public Map getMetadataToCurrentPathMap() { + return metadataToCurrentPathMap; + } + + public void setMetadataToCurrentPathMap(Map metadataToCurrentPathMap) { + this.metadataToCurrentPathMap = metadataToCurrentPathMap; + } + + public String getOldPrefix() { + return oldPrefix; + } + + public void setOldPrefix(String oldPrefix) { + this.oldPrefix = oldPrefix; + } + + public String getNewPrefix() { + return newPrefix; + } + + public void setNewPrefix(String newPrefix) { + this.newPrefix = newPrefix; + } + } +} diff --git a/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataResourcePersistExtensionPoint.java b/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataResourcePersistExtensionPoint.java new file mode 100644 index 00000000000..1819560c93e --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/VmMetadataResourcePersistExtensionPoint.java @@ -0,0 +1,16 @@ +package org.zstack.header.vm.metadata; + +import java.sql.Timestamp; +import java.util.List; + +public interface VmMetadataResourcePersistExtensionPoint { + String getPrimaryStorageType(); + + void afterVolumePersist(String primaryStorageUuid, String resourceUuid, + String resourceType, String hostUuid, long size, Timestamp now); + + void afterSnapshotPersist(String primaryStorageUuid, String resourceUuid, + String resourceType, String hostUuid, long size, Timestamp now); + + default void afterRegistrationRollback(List resourceUuids) {} +} diff --git a/header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java b/header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java new file mode 100644 index 00000000000..afc0f4f475d --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java @@ -0,0 +1,25 @@ +package org.zstack.header.vm.metadata; + +import java.util.ArrayList; +import java.util.List; + +public interface VmUuidFromApiResolver { + String resolveVmUuid(String fieldValue); + + default List batchResolveVmUuids(List fieldValues) { + List result = new ArrayList<>(); + if (fieldValues == null || fieldValues.isEmpty()) { + return result; + } + for (String v : fieldValues) { + if (v == null) { + continue; + } + String vmUuid = resolveVmUuid(v); + if (vmUuid != null) { + result.add(vmUuid); + } + } + return result; + } +} diff --git a/header/src/main/java/org/zstack/header/vm/metadata/VolumeResourceMetadata.java b/header/src/main/java/org/zstack/header/vm/metadata/VolumeResourceMetadata.java new file mode 100644 index 00000000000..76ff75474e1 --- /dev/null +++ b/header/src/main/java/org/zstack/header/vm/metadata/VolumeResourceMetadata.java @@ -0,0 +1,22 @@ +package org.zstack.header.vm.metadata; + +public class VolumeResourceMetadata extends ResourceMetadata { + private String snapshotReference; + private String snapshotReferenceTree; + + public String getSnapshotReference() { + return snapshotReference; + } + + public void setSnapshotReference(String snapshotReference) { + this.snapshotReference = snapshotReference; + } + + public String getSnapshotReferenceTree() { + return snapshotReferenceTree; + } + + public void setSnapshotReferenceTree(String snapshotReferenceTree) { + this.snapshotReferenceTree = snapshotReferenceTree; + } +} From 986d8027f39bbf2ed0439cec02b8381340b63b1c Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Thu, 2 Apr 2026 17:58:19 +0800 Subject: [PATCH 101/129] [vm]: annotate API msgs with MetadataImpact Resolves: ZSV-11559 Change-Id: I91c9593bfced1a4c323841f275a8164d3373e133 --- .../header/storage/snapshot/APIDeleteVolumeSnapshotMsg.java | 2 ++ .../header/storage/snapshot/APIRevertVolumeFromSnapshotMsg.java | 2 ++ .../header/storage/snapshot/APIShrinkVolumeSnapshotMsg.java | 2 ++ .../header/storage/snapshot/APIUpdateVolumeSnapshotMsg.java | 2 ++ .../storage/snapshot/group/APIDeleteVolumeSnapshotGroupMsg.java | 2 ++ .../storage/snapshot/group/APIRevertVmFromSnapshotGroupMsg.java | 2 ++ .../snapshot/group/APIUngroupVolumeSnapshotGroupMsg.java | 2 ++ .../storage/snapshot/group/APIUpdateVolumeSnapshotGroupMsg.java | 2 ++ .../main/java/org/zstack/header/tag/APICreateSystemTagMsg.java | 2 ++ .../main/java/org/zstack/header/tag/APICreateSystemTagsMsg.java | 2 ++ header/src/main/java/org/zstack/header/tag/APIDeleteTagMsg.java | 2 ++ .../main/java/org/zstack/header/tag/APIUpdateSystemTagMsg.java | 2 ++ .../java/org/zstack/header/vm/APIAttachIsoToVmInstanceMsg.java | 2 ++ .../java/org/zstack/header/vm/APIAttachL3NetworkToVmMsg.java | 2 ++ .../main/java/org/zstack/header/vm/APIAttachVmNicToVmMsg.java | 2 ++ .../java/org/zstack/header/vm/APIChangeInstanceOfferingMsg.java | 2 ++ .../java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java | 2 ++ .../main/java/org/zstack/header/vm/APIChangeVmNicStateMsg.java | 2 ++ .../header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsg.java | 2 ++ .../header/vm/APIConvertVmInstanceToTemplatedVmInstanceMsg.java | 2 ++ .../main/java/org/zstack/header/vm/APIDeleteVmBootModeMsg.java | 2 ++ .../org/zstack/header/vm/APIDeleteVmConsolePasswordMsg.java | 2 ++ .../main/java/org/zstack/header/vm/APIDeleteVmHostnameMsg.java | 2 ++ .../main/java/org/zstack/header/vm/APIDeleteVmSshKeyMsg.java | 2 ++ .../main/java/org/zstack/header/vm/APIDeleteVmStaticIpMsg.java | 2 ++ .../main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java | 2 ++ .../org/zstack/header/vm/APIDetachIsoFromVmInstanceMsg.java | 2 ++ .../java/org/zstack/header/vm/APIDetachL3NetworkFromVmMsg.java | 2 ++ header/src/main/java/org/zstack/header/vm/APIMigrateVmMsg.java | 2 ++ .../main/java/org/zstack/header/vm/APIRecoverVmInstanceMsg.java | 2 ++ .../main/java/org/zstack/header/vm/APIReimageVmInstanceMsg.java | 2 ++ .../src/main/java/org/zstack/header/vm/APISetVmBootModeMsg.java | 2 ++ .../main/java/org/zstack/header/vm/APISetVmBootOrderMsg.java | 2 ++ .../main/java/org/zstack/header/vm/APISetVmBootVolumeMsg.java | 2 ++ .../main/java/org/zstack/header/vm/APISetVmClockTrackMsg.java | 2 ++ .../java/org/zstack/header/vm/APISetVmConsolePasswordMsg.java | 2 ++ .../src/main/java/org/zstack/header/vm/APISetVmHostnameMsg.java | 2 ++ .../main/java/org/zstack/header/vm/APISetVmQxlMemoryMsg.java | 2 ++ .../main/java/org/zstack/header/vm/APISetVmSoundTypeMsg.java | 2 ++ .../src/main/java/org/zstack/header/vm/APISetVmSshKeyMsg.java | 2 ++ .../src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java | 2 ++ .../main/java/org/zstack/header/vm/APIUpdateVmInstanceMsg.java | 2 ++ .../main/java/org/zstack/header/vm/APIUpdateVmNicDriverMsg.java | 2 ++ .../main/java/org/zstack/header/vm/APIUpdateVmPriorityMsg.java | 2 ++ .../zstack/header/vm/cdrom/APISetVmInstanceDefaultCdRomMsg.java | 2 ++ .../org/zstack/header/volume/APIAttachDataVolumeToVmMsg.java | 2 ++ .../java/org/zstack/header/volume/APIChangeVolumeStateMsg.java | 2 ++ .../zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java | 2 ++ .../org/zstack/header/volume/APICreateVolumeSnapshotMsg.java | 2 ++ .../java/org/zstack/header/volume/APIDeleteDataVolumeMsg.java | 2 ++ .../org/zstack/header/volume/APIDetachDataVolumeFromVmMsg.java | 2 ++ .../java/org/zstack/header/volume/APIExpungeDataVolumeMsg.java | 2 ++ .../main/java/org/zstack/header/volume/APIFlattenVolumeMsg.java | 2 ++ .../java/org/zstack/header/volume/APIRecoverDataVolumeMsg.java | 2 ++ .../java/org/zstack/header/volume/APISyncVolumeSizeMsg.java | 2 ++ .../org/zstack/header/volume/APIUndoSnapshotCreationMsg.java | 2 ++ .../main/java/org/zstack/header/volume/APIUpdateVolumeMsg.java | 2 ++ .../storage/primary/local/APILocalStorageMigrateVolumeMsg.java | 2 ++ .../org/zstack/resourceconfig/APIDeleteResourceConfigMsg.java | 2 ++ .../org/zstack/resourceconfig/APIUpdateResourceConfigMsg.java | 2 ++ 60 files changed, 120 insertions(+) diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/APIDeleteVolumeSnapshotMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/APIDeleteVolumeSnapshotMsg.java index 966a7d1030c..4711fa5de74 100755 --- a/header/src/main/java/org/zstack/header/storage/snapshot/APIDeleteVolumeSnapshotMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/APIDeleteVolumeSnapshotMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.*; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; import java.util.List; import java.util.concurrent.TimeUnit; @@ -44,6 +45,7 @@ responseClass = APIDeleteVolumeSnapshotEvent.class ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 6) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "SnapshotUuidToVmUuidResolver", field = "uuid", updateOnFailure = true) public class APIDeleteVolumeSnapshotMsg extends APIDeleteMessage implements DeleteVolumeSnapshotMessage { /** * @desc volume snapshot uuid diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/APIRevertVolumeFromSnapshotMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/APIRevertVolumeFromSnapshotMsg.java index 744f13038b6..6ae3008e03e 100755 --- a/header/src/main/java/org/zstack/header/storage/snapshot/APIRevertVolumeFromSnapshotMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/APIRevertVolumeFromSnapshotMsg.java @@ -8,6 +8,7 @@ import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; import org.zstack.header.volume.VolumeVO; import java.util.concurrent.TimeUnit; @@ -46,6 +47,7 @@ responseClass = APIRevertVolumeFromSnapshotEvent.class ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 24) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "SnapshotUuidToVmUuidResolver", field = "uuid", updateOnFailure = true) public class APIRevertVolumeFromSnapshotMsg extends APIMessage implements RevertVolumeSnapshotMessage, APIAuditor { /** * @desc volume snapshot uuid diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/APIShrinkVolumeSnapshotMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/APIShrinkVolumeSnapshotMsg.java index d5f00be9237..f8a1a670023 100644 --- a/header/src/main/java/org/zstack/header/storage/snapshot/APIShrinkVolumeSnapshotMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/APIShrinkVolumeSnapshotMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.DefaultTimeout; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; import java.util.concurrent.TimeUnit; @@ -20,6 +21,7 @@ isAction = true ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 24) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "SnapshotUuidToVmUuidResolver", field = "uuid") public class APIShrinkVolumeSnapshotMsg extends APIMessage implements VolumeSnapshotMessage { @APIParam(resourceType = VolumeSnapshotVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/APIUpdateVolumeSnapshotMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/APIUpdateVolumeSnapshotMsg.java index 870a9ceacd4..6daf7b61649 100755 --- a/header/src/main/java/org/zstack/header/storage/snapshot/APIUpdateVolumeSnapshotMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/APIUpdateVolumeSnapshotMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by frank on 6/14/2015. @@ -15,6 +16,7 @@ isAction = true, responseClass = APIUpdateVolumeSnapshotEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "SnapshotUuidToVmUuidResolver", field = "uuid") public class APIUpdateVolumeSnapshotMsg extends APIMessage implements VolumeSnapshotMessage { @APIParam(resourceType = VolumeSnapshotVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/group/APIDeleteVolumeSnapshotGroupMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/group/APIDeleteVolumeSnapshotGroupMsg.java index 4afc5170734..cb1f8dde454 100644 --- a/header/src/main/java/org/zstack/header/storage/snapshot/group/APIDeleteVolumeSnapshotGroupMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/APIDeleteVolumeSnapshotGroupMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.snapshot.SnapshotBackendOperation; +import org.zstack.header.vm.metadata.MetadataImpact; import java.util.concurrent.TimeUnit; @@ -19,6 +20,7 @@ responseClass = APIDeleteVolumeSnapshotGroupEvent.class ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 3) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "SnapshotGroupUuidToVmUuidResolver", field = "uuid", updateOnFailure = true) public class APIDeleteVolumeSnapshotGroupMsg extends APIDeleteMessage implements VolumeSnapshotGroupMessage { @APIParam(resourceType = VolumeSnapshotGroupVO.class, successIfResourceNotExisting = true) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/group/APIRevertVmFromSnapshotGroupMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/group/APIRevertVmFromSnapshotGroupMsg.java index d042ee6229f..28752daf8b0 100644 --- a/header/src/main/java/org/zstack/header/storage/snapshot/group/APIRevertVmFromSnapshotGroupMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/APIRevertVmFromSnapshotGroupMsg.java @@ -14,6 +14,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.snapshot.SnapshotBackendOperation; import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.metadata.MetadataImpact; import java.util.concurrent.TimeUnit; @@ -25,6 +26,7 @@ ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 24) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "SnapshotGroupUuidToVmUuidResolver", field = "uuid", updateOnFailure = true) public class APIRevertVmFromSnapshotGroupMsg extends APIMessage implements VolumeSnapshotGroupMessage, APIAuditor { @APIParam(resourceType = VolumeSnapshotGroupVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/group/APIUngroupVolumeSnapshotGroupMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/group/APIUngroupVolumeSnapshotGroupMsg.java index daa39380ac1..6428bde5269 100644 --- a/header/src/main/java/org/zstack/header/storage/snapshot/group/APIUngroupVolumeSnapshotGroupMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/APIUngroupVolumeSnapshotGroupMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.snapshot.SnapshotBackendOperation; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by MaJin on 2019/7/9. @@ -14,6 +15,7 @@ method = HttpMethod.DELETE, responseClass = APIUngroupVolumeSnapshotGroupEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "SnapshotGroupUuidToVmUuidResolver", field = "uuid") public class APIUngroupVolumeSnapshotGroupMsg extends APIMessage implements VolumeSnapshotGroupMessage { @APIParam(resourceType = VolumeSnapshotGroupVO.class, successIfResourceNotExisting = true) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/group/APIUpdateVolumeSnapshotGroupMsg.java b/header/src/main/java/org/zstack/header/storage/snapshot/group/APIUpdateVolumeSnapshotGroupMsg.java index 0cee28cd6ea..68deeb2b635 100644 --- a/header/src/main/java/org/zstack/header/storage/snapshot/group/APIUpdateVolumeSnapshotGroupMsg.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/APIUpdateVolumeSnapshotGroupMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; import org.zstack.header.storage.snapshot.SnapshotBackendOperation; /** @@ -15,6 +16,7 @@ isAction = true, responseClass = APIUpdateVolumeSnapshotGroupEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "SnapshotGroupUuidToVmUuidResolver", field = "uuid") public class APIUpdateVolumeSnapshotGroupMsg extends APIMessage implements VolumeSnapshotGroupMessage { @APIParam(required = false) private String name; diff --git a/header/src/main/java/org/zstack/header/tag/APICreateSystemTagMsg.java b/header/src/main/java/org/zstack/header/tag/APICreateSystemTagMsg.java index a02a6001cf1..aaa350d471a 100755 --- a/header/src/main/java/org/zstack/header/tag/APICreateSystemTagMsg.java +++ b/header/src/main/java/org/zstack/header/tag/APICreateSystemTagMsg.java @@ -2,6 +2,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** */ @@ -11,6 +12,7 @@ responseClass = APICreateSystemTagEvent.class, parameterName = "params" ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "ResourceUuidToVmUuidResolver", field = "resourceUuid") public class APICreateSystemTagMsg extends APIAbstractCreateTagMsg { public static APICreateSystemTagMsg __example__() { diff --git a/header/src/main/java/org/zstack/header/tag/APICreateSystemTagsMsg.java b/header/src/main/java/org/zstack/header/tag/APICreateSystemTagsMsg.java index 170076094d9..c2fa8085b91 100644 --- a/header/src/main/java/org/zstack/header/tag/APICreateSystemTagsMsg.java +++ b/header/src/main/java/org/zstack/header/tag/APICreateSystemTagsMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; import org.zstack.header.vo.ResourceVO; import java.util.List; @@ -16,6 +17,7 @@ responseClass = APICreateSystemTagsEvent.class, parameterName = "params" ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "ResourceUuidToVmUuidResolver", field = "resourceUuid") public class APICreateSystemTagsMsg extends APIMessage { @APIParam private String resourceType; diff --git a/header/src/main/java/org/zstack/header/tag/APIDeleteTagMsg.java b/header/src/main/java/org/zstack/header/tag/APIDeleteTagMsg.java index b1159b20945..59597ac6901 100755 --- a/header/src/main/java/org/zstack/header/tag/APIDeleteTagMsg.java +++ b/header/src/main/java/org/zstack/header/tag/APIDeleteTagMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIDeleteMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** */ @@ -12,6 +13,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteTagEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "SystemTagUuidToVmUuidResolver", field = "uuid") public class APIDeleteTagMsg extends APIDeleteMessage { @APIParam private String uuid; diff --git a/header/src/main/java/org/zstack/header/tag/APIUpdateSystemTagMsg.java b/header/src/main/java/org/zstack/header/tag/APIUpdateSystemTagMsg.java index 2962ce07061..65173855747 100755 --- a/header/src/main/java/org/zstack/header/tag/APIUpdateSystemTagMsg.java +++ b/header/src/main/java/org/zstack/header/tag/APIUpdateSystemTagMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by frank on 8/17/2015. @@ -14,6 +15,7 @@ isAction = true, method = HttpMethod.PUT ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "SystemTagUuidToVmUuidResolver", field = "uuid") public class APIUpdateSystemTagMsg extends APIMessage { @APIParam(resourceType = SystemTagVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIAttachIsoToVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIAttachIsoToVmInstanceMsg.java index c28f55a3c6c..e630c877e5c 100755 --- a/header/src/main/java/org/zstack/header/vm/APIAttachIsoToVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIAttachIsoToVmInstanceMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by frank on 10/17/2015. @@ -16,6 +17,7 @@ responseClass = APIAttachIsoToVmInstanceEvent.class, parameterName = "null" ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "vmInstanceUuid") public class APIAttachIsoToVmInstanceMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String vmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmMsg.java b/header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmMsg.java index 7bab9be06f9..681a04c6b51 100755 --- a/header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.network.l3.L3NetworkVO; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; import org.zstack.utils.network.NicIpAddressInfo; import java.util.List; @@ -45,6 +46,7 @@ method = HttpMethod.POST, responseClass = APIAttachL3NetworkToVmEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "vmInstanceUuid") public class APIAttachL3NetworkToVmMsg extends APIMessage implements VmInstanceMessage { /** * @desc vm uuid diff --git a/header/src/main/java/org/zstack/header/vm/APIAttachVmNicToVmMsg.java b/header/src/main/java/org/zstack/header/vm/APIAttachVmNicToVmMsg.java index 05b86a2bedf..5313e83aa0e 100644 --- a/header/src/main/java/org/zstack/header/vm/APIAttachVmNicToVmMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIAttachVmNicToVmMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; @RestRequest( path = "/vm-instances/{vmInstanceUuid}/nices/{vmNicUuid}", @@ -11,6 +12,7 @@ responseClass = APIAttachVmNicToVmEvent.class, parameterName = "params" ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "vmInstanceUuid") public class APIAttachVmNicToVmMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmNicVO.class) diff --git a/header/src/main/java/org/zstack/header/vm/APIChangeInstanceOfferingMsg.java b/header/src/main/java/org/zstack/header/vm/APIChangeInstanceOfferingMsg.java index aa0e68f9211..d157868793f 100755 --- a/header/src/main/java/org/zstack/header/vm/APIChangeInstanceOfferingMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIChangeInstanceOfferingMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by frank on 7/16/2015. @@ -15,6 +16,7 @@ method = HttpMethod.PUT, responseClass = APIChangeInstanceOfferingEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "vmInstanceUuid") public class APIChangeInstanceOfferingMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String vmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java b/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java index 60ded8149d2..bfedeb43306 100644 --- a/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.network.l3.L3NetworkVO; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; import java.util.List; import java.util.Map; @@ -16,6 +17,7 @@ method = HttpMethod.POST, responseClass = APIChangeVmNicNetworkEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "NicUuidToVmUuidResolver", field = "vmNicUuid") public class APIChangeVmNicNetworkMsg extends APIMessage implements VmInstanceMessage{ @APIParam(resourceType = VmNicVO.class) private String vmNicUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIChangeVmNicStateMsg.java b/header/src/main/java/org/zstack/header/vm/APIChangeVmNicStateMsg.java index 6c1594fed23..1a38b709b25 100644 --- a/header/src/main/java/org/zstack/header/vm/APIChangeVmNicStateMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIChangeVmNicStateMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.other.APIMultiAuditor; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; import java.util.ArrayList; import java.util.List; @@ -22,6 +23,7 @@ responseClass = APIChangeVmNicStateEvent.class, isAction = true ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "NicUuidToVmUuidResolver", field = "vmNicUuid") public class APIChangeVmNicStateMsg extends APIMessage implements VmInstanceMessage, APIMultiAuditor { @APIParam(resourceType = VmNicVO.class) private String vmNicUuid; 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 46e443c35cf..dc1be500331 100644 --- a/header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; @RestRequest( path = "/vm-instances/{templatedVmInstanceUuid}/convert-to-vmInstance", @@ -14,6 +15,7 @@ responseClass = APIConvertTemplatedVmInstanceToVmInstanceEvent.class, parameterName = "params" ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "templatedVmInstanceUuid") public class APIConvertTemplatedVmInstanceToVmInstanceMsg extends APIMessage implements VmInstanceMessage, APIAuditor { @APIParam(resourceType = TemplatedVmInstanceVO.class) private String templatedVmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIConvertVmInstanceToTemplatedVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIConvertVmInstanceToTemplatedVmInstanceMsg.java index 2b9191824b7..d83709a1703 100644 --- a/header/src/main/java/org/zstack/header/vm/APIConvertVmInstanceToTemplatedVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIConvertVmInstanceToTemplatedVmInstanceMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; @RestRequest( path = "/vm-instances/{vmInstanceUuid}/convert-to-templatedVmInstance", @@ -13,6 +14,7 @@ responseClass = APIConvertVmInstanceToTemplatedVmInstanceEvent.class, parameterName = "params" ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "vmInstanceUuid") public class APIConvertVmInstanceToTemplatedVmInstanceMsg extends APIMessage implements VmInstanceMessage, APIAuditor { @APIParam(resourceType = VmInstanceVO.class) private String vmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIDeleteVmBootModeMsg.java b/header/src/main/java/org/zstack/header/vm/APIDeleteVmBootModeMsg.java index a2b73a69527..27d27afa839 100644 --- a/header/src/main/java/org/zstack/header/vm/APIDeleteVmBootModeMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIDeleteVmBootModeMsg.java @@ -4,12 +4,14 @@ import org.zstack.header.message.APIDeleteMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; @RestRequest( path = "/vm-instances/{uuid}/bootmode", method = HttpMethod.DELETE, responseClass = APIDeleteVmBootModeEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "uuid") public class APIDeleteVmBootModeMsg extends APIDeleteMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIDeleteVmConsolePasswordMsg.java b/header/src/main/java/org/zstack/header/vm/APIDeleteVmConsolePasswordMsg.java index e9d6152c7ef..f7b0cb056a1 100755 --- a/header/src/main/java/org/zstack/header/vm/APIDeleteVmConsolePasswordMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIDeleteVmConsolePasswordMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by root on 8/2/16. @@ -13,6 +14,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteVmConsolePasswordEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "uuid") public class APIDeleteVmConsolePasswordMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIDeleteVmHostnameMsg.java b/header/src/main/java/org/zstack/header/vm/APIDeleteVmHostnameMsg.java index 611c1c13da4..89aaa4a1e95 100755 --- a/header/src/main/java/org/zstack/header/vm/APIDeleteVmHostnameMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIDeleteVmHostnameMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIDeleteMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by frank on 2/26/2016. @@ -13,6 +14,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteVmHostnameEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "uuid") public class APIDeleteVmHostnameMsg extends APIDeleteMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIDeleteVmSshKeyMsg.java b/header/src/main/java/org/zstack/header/vm/APIDeleteVmSshKeyMsg.java index 0372c7526ab..f10c7ed7e83 100755 --- a/header/src/main/java/org/zstack/header/vm/APIDeleteVmSshKeyMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIDeleteVmSshKeyMsg.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APIMessage; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by luchukun on 8/4/16. @@ -12,6 +13,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteVmSshKeyEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "uuid") public class APIDeleteVmSshKeyMsg extends APIMessage implements VmInstanceMessage { private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIDeleteVmStaticIpMsg.java b/header/src/main/java/org/zstack/header/vm/APIDeleteVmStaticIpMsg.java index 15d11f96647..2f1312fb903 100755 --- a/header/src/main/java/org/zstack/header/vm/APIDeleteVmStaticIpMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIDeleteVmStaticIpMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIDeleteMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by frank on 2/26/2016. @@ -13,6 +14,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteVmStaticIpEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "vmInstanceUuid") public class APIDeleteVmStaticIpMsg extends APIDeleteMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String vmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java index b93c5eb4361..0f02d85c6f0 100755 --- a/header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIDeleteMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; import java.util.List; @@ -41,6 +42,7 @@ method = HttpMethod.DELETE, responseClass = APIDestroyVmInstanceEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "VmUuidDirectResolver", field = "uuid", updateOnFailure = true) public class APIDestroyVmInstanceMsg extends APIDeleteMessage implements VmInstanceMessage { /** * @desc vm uuid diff --git a/header/src/main/java/org/zstack/header/vm/APIDetachIsoFromVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIDetachIsoFromVmInstanceMsg.java index 52d17b61fec..3cffd78123a 100755 --- a/header/src/main/java/org/zstack/header/vm/APIDetachIsoFromVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIDetachIsoFromVmInstanceMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by frank on 10/17/2015. @@ -16,6 +17,7 @@ method = HttpMethod.DELETE, responseClass = APIDetachIsoFromVmInstanceEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "vmInstanceUuid") public class APIDetachIsoFromVmInstanceMsg extends APIMessage implements VmInstanceMessage, APIAuditor { @APIParam(resourceType = VmInstanceVO.class) private String vmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIDetachL3NetworkFromVmMsg.java b/header/src/main/java/org/zstack/header/vm/APIDetachL3NetworkFromVmMsg.java index 0d2c7d2d38f..e226e8f962b 100755 --- a/header/src/main/java/org/zstack/header/vm/APIDetachL3NetworkFromVmMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIDetachL3NetworkFromVmMsg.java @@ -9,6 +9,7 @@ import org.zstack.header.other.APIMultiAuditor; import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; import java.util.ArrayList; import java.util.List; @@ -21,6 +22,7 @@ method = HttpMethod.DELETE, responseClass = APIDetachL3NetworkFromVmEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "NicUuidToVmUuidResolver", field = "vmNicUuid") public class APIDetachL3NetworkFromVmMsg extends APIMessage implements VmInstanceMessage, APIMultiAuditor { @APIParam(resourceType = VmNicVO.class) private String vmNicUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIMigrateVmMsg.java b/header/src/main/java/org/zstack/header/vm/APIMigrateVmMsg.java index e6ed3bbf44b..1fa0f34f2f5 100755 --- a/header/src/main/java/org/zstack/header/vm/APIMigrateVmMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIMigrateVmMsg.java @@ -6,6 +6,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.message.DefaultTimeout; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; import java.util.concurrent.TimeUnit; @@ -44,6 +45,7 @@ ) @SkipVmTracer(replyClass = APIMigrateVmEvent.class) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 1) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "VmUuidDirectResolver", field = "vmInstanceUuid") public class APIMigrateVmMsg extends APIMessage implements VmInstanceMessage, MigrateVmMessage, CheckAttachedVolumesMessage { /** * @desc vm uuid diff --git a/header/src/main/java/org/zstack/header/vm/APIRecoverVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIRecoverVmInstanceMsg.java index 7de84b5dccd..fd0f4c44622 100755 --- a/header/src/main/java/org/zstack/header/vm/APIRecoverVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIRecoverVmInstanceMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by frank on 11/12/2015. @@ -14,6 +15,7 @@ method = HttpMethod.PUT, responseClass = APIRecoverVmInstanceEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "uuid") public class APIRecoverVmInstanceMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceMsg.java index 53ad2c26f4f..dc78a69c631 100755 --- a/header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by miao on 11/3/16. @@ -15,6 +16,7 @@ responseClass = APIReimageVmInstanceEvent.class, category = "vmInstance" ) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "VmUuidDirectResolver", field = "vmInstanceUuid") public class APIReimageVmInstanceMsg extends APIMessage implements VmInstanceMessage { public String getVmInstanceUuid() { return vmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APISetVmBootModeMsg.java b/header/src/main/java/org/zstack/header/vm/APISetVmBootModeMsg.java index 0aa6e025c3c..e96b38cd64b 100644 --- a/header/src/main/java/org/zstack/header/vm/APISetVmBootModeMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APISetVmBootModeMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; @RestRequest( path = "/vm-instances/{uuid}/actions", @@ -11,6 +12,7 @@ isAction = true, responseClass = APISetVmBootModeEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "uuid") public class APISetVmBootModeMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APISetVmBootOrderMsg.java b/header/src/main/java/org/zstack/header/vm/APISetVmBootOrderMsg.java index fb3d980e807..7400901f35c 100755 --- a/header/src/main/java/org/zstack/header/vm/APISetVmBootOrderMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APISetVmBootOrderMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; import java.util.List; @@ -18,6 +19,7 @@ method = HttpMethod.PUT, responseClass = APISetVmBootOrderEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "uuid") public class APISetVmBootOrderMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APISetVmBootVolumeMsg.java b/header/src/main/java/org/zstack/header/vm/APISetVmBootVolumeMsg.java index a72619161e0..bf1d090f845 100644 --- a/header/src/main/java/org/zstack/header/vm/APISetVmBootVolumeMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APISetVmBootVolumeMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; import org.zstack.header.volume.VolumeVO; /** @@ -16,6 +17,7 @@ method = HttpMethod.PUT, responseClass = APISetVmBootVolumeEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "VmUuidDirectResolver", field = "vmInstanceUuid") public class APISetVmBootVolumeMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String vmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APISetVmClockTrackMsg.java b/header/src/main/java/org/zstack/header/vm/APISetVmClockTrackMsg.java index 1f25eead1db..2aa0026eb79 100644 --- a/header/src/main/java/org/zstack/header/vm/APISetVmClockTrackMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APISetVmClockTrackMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; @RestRequest( path = "/vm-instances/{uuid}/actions", @@ -11,6 +12,7 @@ method = HttpMethod.PUT, responseClass = APISetVmClockTrackEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "uuid") public class APISetVmClockTrackMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APISetVmConsolePasswordMsg.java b/header/src/main/java/org/zstack/header/vm/APISetVmConsolePasswordMsg.java index 0812289e799..30c886d2833 100755 --- a/header/src/main/java/org/zstack/header/vm/APISetVmConsolePasswordMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APISetVmConsolePasswordMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; import java.io.Serializable; @@ -18,6 +19,7 @@ method = HttpMethod.PUT, responseClass = APISetVmConsolePasswordEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "uuid") public class APISetVmConsolePasswordMsg extends APIMessage implements VmInstanceMessage, Serializable { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APISetVmHostnameMsg.java b/header/src/main/java/org/zstack/header/vm/APISetVmHostnameMsg.java index 2b2e54afabc..2ea204e2f29 100755 --- a/header/src/main/java/org/zstack/header/vm/APISetVmHostnameMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APISetVmHostnameMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by frank on 2/26/2016. @@ -14,6 +15,7 @@ isAction = true, responseClass = APISetVmHostnameEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "uuid") public class APISetVmHostnameMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APISetVmQxlMemoryMsg.java b/header/src/main/java/org/zstack/header/vm/APISetVmQxlMemoryMsg.java index c5d7ebd8e72..e83d4662015 100644 --- a/header/src/main/java/org/zstack/header/vm/APISetVmQxlMemoryMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APISetVmQxlMemoryMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; @RestRequest( path = "/vm-instances/{uuid}/actions", @@ -11,6 +12,7 @@ method = HttpMethod.PUT, responseClass = APISetVmQxlMemoryEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "uuid") public class APISetVmQxlMemoryMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APISetVmSoundTypeMsg.java b/header/src/main/java/org/zstack/header/vm/APISetVmSoundTypeMsg.java index 686697b009c..3c348568865 100644 --- a/header/src/main/java/org/zstack/header/vm/APISetVmSoundTypeMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APISetVmSoundTypeMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; @RestRequest( path = "/vm-instances/{uuid}/actions", @@ -11,6 +12,7 @@ method = HttpMethod.PUT, responseClass = APISetVmSoundTypeEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "uuid") public class APISetVmSoundTypeMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APISetVmSshKeyMsg.java b/header/src/main/java/org/zstack/header/vm/APISetVmSshKeyMsg.java index 3b2d315ea06..fc3bdf81f03 100755 --- a/header/src/main/java/org/zstack/header/vm/APISetVmSshKeyMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APISetVmSshKeyMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by luchukun on 8/4/16. @@ -15,6 +16,7 @@ method = HttpMethod.PUT, responseClass = APISetVmSshKeyEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "uuid") public class APISetVmSshKeyMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java b/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java index 094f9d4a54f..c82b4064519 100755 --- a/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.network.l3.L3NetworkVO; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by frank on 2/26/2016. @@ -15,6 +16,7 @@ method = HttpMethod.PUT, responseClass = APISetVmStaticIpEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "vmInstanceUuid") public class APISetVmStaticIpMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String vmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMsg.java index 60e9343ff38..d9b9d333198 100755 --- a/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.network.l3.L3NetworkVO; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by frank on 6/14/2015. @@ -15,6 +16,7 @@ isAction = true, responseClass = APIUpdateVmInstanceEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "uuid") public class APIUpdateVmInstanceMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIUpdateVmNicDriverMsg.java b/header/src/main/java/org/zstack/header/vm/APIUpdateVmNicDriverMsg.java index de2fd27289c..1863f7c224e 100644 --- a/header/src/main/java/org/zstack/header/vm/APIUpdateVmNicDriverMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIUpdateVmNicDriverMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * @ Author : yh.w @@ -15,6 +16,7 @@ isAction = true, responseClass = APIUpdateVmNicDriverEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "NicUuidToVmUuidResolver", field = "vmNicUuid") public class APIUpdateVmNicDriverMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String vmInstanceUuid; diff --git a/header/src/main/java/org/zstack/header/vm/APIUpdateVmPriorityMsg.java b/header/src/main/java/org/zstack/header/vm/APIUpdateVmPriorityMsg.java index a8c6f821450..bab95b4fafa 100644 --- a/header/src/main/java/org/zstack/header/vm/APIUpdateVmPriorityMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIUpdateVmPriorityMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; @RestRequest( path = "/vm-instances/{uuid}/actions", @@ -11,6 +12,7 @@ isAction = true, responseClass = APIUpdateVmPriorityEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "uuid") public class APIUpdateVmPriorityMsg extends APIMessage implements VmInstanceMessage { @APIParam(resourceType = VmInstanceVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/vm/cdrom/APISetVmInstanceDefaultCdRomMsg.java b/header/src/main/java/org/zstack/header/vm/cdrom/APISetVmInstanceDefaultCdRomMsg.java index f5f9b0afa76..0e1a8c93430 100644 --- a/header/src/main/java/org/zstack/header/vm/cdrom/APISetVmInstanceDefaultCdRomMsg.java +++ b/header/src/main/java/org/zstack/header/vm/cdrom/APISetVmInstanceDefaultCdRomMsg.java @@ -8,6 +8,7 @@ import org.zstack.header.rest.RestRequest; import org.zstack.header.vm.VmInstanceMessage; import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Create by lining at 2018/12/29 @@ -18,6 +19,7 @@ isAction = true, responseClass = APISetVmInstanceDefaultCdRomEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "vmInstanceUuid") public class APISetVmInstanceDefaultCdRomMsg extends APIMessage implements VmInstanceMessage, APIAuditor { @APIParam(resourceType = VmCdRomVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToVmMsg.java b/header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToVmMsg.java index 6ae817723f4..b01ff703a4c 100755 --- a/header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToVmMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToVmMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.metadata.MetadataImpact; /** * @api api event for message :ref:`APIAttachVolumeToVmEvent` @@ -39,6 +40,7 @@ parameterName = "params", responseClass = APIAttachDataVolumeToVmEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "VmUuidDirectResolver", field = "vmInstanceUuid") public class APIAttachDataVolumeToVmMsg extends APIMessage implements VolumeMessage { /** * @desc vm uuid. see :ref:`VmInstanceInventory` diff --git a/header/src/main/java/org/zstack/header/volume/APIChangeVolumeStateMsg.java b/header/src/main/java/org/zstack/header/volume/APIChangeVolumeStateMsg.java index 30478454c67..464436a6345 100755 --- a/header/src/main/java/org/zstack/header/volume/APIChangeVolumeStateMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIChangeVolumeStateMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * @api change data volume state @@ -38,6 +39,7 @@ method = HttpMethod.PUT, responseClass = APIChangeVolumeStateEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VolumeUuidToVmUuidResolver", field = "uuid") public class APIChangeVolumeStateMsg extends APIMessage implements VolumeMessage { /** * @desc data volume uuid diff --git a/header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java b/header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java index 9b497a73fe4..13434495343 100644 --- a/header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java @@ -13,6 +13,7 @@ import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO; import org.zstack.header.vm.VmInstanceInventory; import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.metadata.MetadataImpact; import java.util.ArrayList; import java.util.List; @@ -27,6 +28,7 @@ responseClass = APICreateVolumeSnapshotGroupEvent.class, parameterName = "params" ) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "VolumeUuidToVmUuidResolver", field = "rootVolumeUuid", updateOnFailure = true) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 3) public class APICreateVolumeSnapshotGroupMsg extends APICreateMessage implements VolumeMessage, CreateVolumeSnapshotGroupMessage, APIMultiAuditor { /** diff --git a/header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotMsg.java b/header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotMsg.java index 35f57d205f6..ca92ecf5034 100755 --- a/header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.snapshot.VolumeSnapshotVO; +import org.zstack.header.vm.metadata.MetadataImpact; import java.util.concurrent.TimeUnit; @@ -45,6 +46,7 @@ parameterName = "params" ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 3) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "VolumeUuidToVmUuidResolver", field = "volumeUuid", updateOnFailure = true) public class APICreateVolumeSnapshotMsg extends APICreateMessage implements VolumeMessage, APIAuditor { /** * @desc volume uuid. See :ref:`VolumeInventory` diff --git a/header/src/main/java/org/zstack/header/volume/APIDeleteDataVolumeMsg.java b/header/src/main/java/org/zstack/header/volume/APIDeleteDataVolumeMsg.java index fdbb1be9847..bde41313f8e 100755 --- a/header/src/main/java/org/zstack/header/volume/APIDeleteDataVolumeMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIDeleteDataVolumeMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIDeleteMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; import java.util.List; @@ -42,6 +43,7 @@ method = HttpMethod.DELETE, responseClass = APIDeleteDataVolumeEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "VolumeUuidToVmUuidResolver", field = "uuid") public class APIDeleteDataVolumeMsg extends APIDeleteMessage implements VolumeMessage { /** * @desc data volume uuid diff --git a/header/src/main/java/org/zstack/header/volume/APIDetachDataVolumeFromVmMsg.java b/header/src/main/java/org/zstack/header/volume/APIDetachDataVolumeFromVmMsg.java index 5164ffca076..65374886a04 100755 --- a/header/src/main/java/org/zstack/header/volume/APIDetachDataVolumeFromVmMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIDetachDataVolumeFromVmMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.metadata.MetadataImpact; import org.zstack.header.rest.RestRequest; /** @@ -36,6 +37,7 @@ method = HttpMethod.DELETE, responseClass = APIDetachDataVolumeFromVmEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "VolumeUuidToVmUuidResolver", field = "uuid") public class APIDetachDataVolumeFromVmMsg extends APIMessage implements VolumeMessage { /** * @desc data volume uuid. See :ref:`VolumeInventory` diff --git a/header/src/main/java/org/zstack/header/volume/APIExpungeDataVolumeMsg.java b/header/src/main/java/org/zstack/header/volume/APIExpungeDataVolumeMsg.java index 272036c9378..53071f40ae8 100755 --- a/header/src/main/java/org/zstack/header/volume/APIExpungeDataVolumeMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIExpungeDataVolumeMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by frank on 11/16/2015. @@ -14,6 +15,7 @@ method = HttpMethod.PUT, responseClass = APIExpungeDataVolumeEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "VolumeUuidToVmUuidResolver", field = "uuid") public class APIExpungeDataVolumeMsg extends APIMessage implements VolumeMessage { @APIParam(resourceType = VolumeVO.class, successIfResourceNotExisting = true) private String uuid; diff --git a/header/src/main/java/org/zstack/header/volume/APIFlattenVolumeMsg.java b/header/src/main/java/org/zstack/header/volume/APIFlattenVolumeMsg.java index daeb56b44eb..31a76c2451c 100644 --- a/header/src/main/java/org/zstack/header/volume/APIFlattenVolumeMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIFlattenVolumeMsg.java @@ -7,6 +7,7 @@ import org.zstack.header.message.DefaultTimeout; import org.zstack.header.other.APIAuditor; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; import java.util.concurrent.TimeUnit; @@ -17,6 +18,7 @@ method = HttpMethod.PUT, responseClass = APIFlattenVolumeEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "VolumeUuidToVmUuidResolver", field = "uuid", updateOnFailure = true) public class APIFlattenVolumeMsg extends APIMessage implements VolumeMessage, APIAuditor { @APIParam(resourceType = VolumeVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/volume/APIRecoverDataVolumeMsg.java b/header/src/main/java/org/zstack/header/volume/APIRecoverDataVolumeMsg.java index eec77334a37..11fc833074d 100755 --- a/header/src/main/java/org/zstack/header/volume/APIRecoverDataVolumeMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIRecoverDataVolumeMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by frank on 11/12/2015. @@ -14,6 +15,7 @@ method = HttpMethod.PUT, responseClass = APIRecoverDataVolumeEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "VolumeUuidToVmUuidResolver", field = "uuid") public class APIRecoverDataVolumeMsg extends APIMessage implements VolumeMessage { @APIParam(resourceType = VolumeVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/volume/APISyncVolumeSizeMsg.java b/header/src/main/java/org/zstack/header/volume/APISyncVolumeSizeMsg.java index d4780c73be0..0eb69ce5dc4 100755 --- a/header/src/main/java/org/zstack/header/volume/APISyncVolumeSizeMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APISyncVolumeSizeMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by xing5 on 2016/4/24. @@ -14,6 +15,7 @@ responseClass = APISyncVolumeSizeEvent.class, isAction = true ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VolumeUuidToVmUuidResolver", field = "uuid") public class APISyncVolumeSizeMsg extends APIMessage implements VolumeMessage { @APIParam(resourceType = VolumeVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/volume/APIUndoSnapshotCreationMsg.java b/header/src/main/java/org/zstack/header/volume/APIUndoSnapshotCreationMsg.java index 0d27cfaa18a..6c6b5513525 100644 --- a/header/src/main/java/org/zstack/header/volume/APIUndoSnapshotCreationMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIUndoSnapshotCreationMsg.java @@ -5,6 +5,7 @@ import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.snapshot.VolumeSnapshotVO; +import org.zstack.header.vm.metadata.MetadataImpact; /** * @ Author : yh.w @@ -16,6 +17,7 @@ method = HttpMethod.PUT, responseClass = APIUndoSnapshotCreationEvent.class ) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "VolumeUuidToVmUuidResolver", field = "uuid", updateOnFailure = true) public class APIUndoSnapshotCreationMsg extends APIMessage implements VolumeMessage { @APIParam(resourceType = VolumeVO.class) diff --git a/header/src/main/java/org/zstack/header/volume/APIUpdateVolumeMsg.java b/header/src/main/java/org/zstack/header/volume/APIUpdateVolumeMsg.java index 785cf824abd..a3d67b10f73 100755 --- a/header/src/main/java/org/zstack/header/volume/APIUpdateVolumeMsg.java +++ b/header/src/main/java/org/zstack/header/volume/APIUpdateVolumeMsg.java @@ -4,6 +4,7 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; /** * Created by frank on 6/14/2015. @@ -14,6 +15,7 @@ responseClass = APIUpdateVolumeEvent.class, isAction = true ) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VolumeUuidToVmUuidResolver", field = "uuid") public class APIUpdateVolumeMsg extends APIMessage implements VolumeMessage { @APIParam(resourceType = VolumeVO.class) private String uuid; diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APILocalStorageMigrateVolumeMsg.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APILocalStorageMigrateVolumeMsg.java index e1864931171..a586da2bf48 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APILocalStorageMigrateVolumeMsg.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APILocalStorageMigrateVolumeMsg.java @@ -10,6 +10,7 @@ import org.zstack.header.rest.APINoSee; import org.zstack.header.rest.RestRequest; import org.zstack.header.storage.primary.PrimaryStorageMessage; +import org.zstack.header.vm.metadata.MetadataImpact; import org.zstack.header.storage.primary.PrimaryStorageVO; import org.zstack.header.volume.VolumeVO; @@ -25,6 +26,7 @@ isAction = true ) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 24) +@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "VolumeUuidToVmUuidResolver", field = "volumeUuid", updateOnFailure = true) public class APILocalStorageMigrateVolumeMsg extends APIMessage implements PrimaryStorageMessage, APIAuditor { @APIParam(resourceType = VolumeVO.class) private String volumeUuid; diff --git a/resourceconfig/src/main/java/org/zstack/resourceconfig/APIDeleteResourceConfigMsg.java b/resourceconfig/src/main/java/org/zstack/resourceconfig/APIDeleteResourceConfigMsg.java index fd43d66dac3..7733f7702b0 100644 --- a/resourceconfig/src/main/java/org/zstack/resourceconfig/APIDeleteResourceConfigMsg.java +++ b/resourceconfig/src/main/java/org/zstack/resourceconfig/APIDeleteResourceConfigMsg.java @@ -5,11 +5,13 @@ import org.zstack.header.message.APIDeleteMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; import org.zstack.header.vo.ResourceVO; @RestRequest(path = "/resource-configurations/{category}/{name}/{resourceUuid}", method = HttpMethod.DELETE, responseClass = APIDeleteResourceConfigEvent.class) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "ResourceUuidToVmUuidResolver", field = "resourceUuid") public class APIDeleteResourceConfigMsg extends APIDeleteMessage implements ResourceConfigMessage { @APIParam private String category; diff --git a/resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigMsg.java b/resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigMsg.java index 1b8437d2404..fc30ba61e3b 100644 --- a/resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigMsg.java +++ b/resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigMsg.java @@ -5,12 +5,14 @@ import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; import org.zstack.header.rest.RestRequest; +import org.zstack.header.vm.metadata.MetadataImpact; import org.zstack.header.vo.ResourceVO; @RestRequest(path = "/resource-configurations/{category}/{name}/{resourceUuid}/actions", method = HttpMethod.PUT, isAction = true, responseClass = APIUpdateResourceConfigEvent.class) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "ResourceUuidToVmUuidResolver", field = "resourceUuid") public class APIUpdateResourceConfigMsg extends APIMessage implements ResourceConfigMessage { @APIParam private String category; From 1734662c4e9e8b571b95a7e4b3a82f1cf590ff8a Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Thu, 2 Apr 2026 17:58:42 +0800 Subject: [PATCH 102/129] [vm]: add metadata compute core and config Resolves: ZSV-11559 Change-Id: I4ad67f16ba0a37ec0f5add571f4f74752bcfbee5 --- ...pVmInstanceMetadataOnPrimaryStorageGC.java | 106 ++++++++++++++ .../compute/vm/VmExpungeMetadataFlow.java | 138 ++++++++++++++++++ .../org/zstack/compute/vm/VmGlobalConfig.java | 48 ++++++ .../compute/vm/VmInstanceApiInterceptor.java | 71 +++++++++ .../org/zstack/compute/vm/VmSystemTags.java | 4 + conf/db/zsv/V5.0.0__schema.sql | 27 ++++ conf/globalConfig/vm.xml | 129 ++++++++++++++++ conf/persistence.xml | 2 + conf/serviceConfig/primaryStorage.xml | 4 + conf/serviceConfig/vmInstance.xml | 12 ++ conf/springConfigXml/VmInstanceManager.xml | 1 + .../storage/primary/PrimaryStorageBase.java | 99 +++++++++++++ 12 files changed, 641 insertions(+) create mode 100644 compute/src/main/java/org/zstack/compute/vm/CleanupVmInstanceMetadataOnPrimaryStorageGC.java create mode 100644 compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java diff --git a/compute/src/main/java/org/zstack/compute/vm/CleanupVmInstanceMetadataOnPrimaryStorageGC.java b/compute/src/main/java/org/zstack/compute/vm/CleanupVmInstanceMetadataOnPrimaryStorageGC.java new file mode 100644 index 00000000000..3b480ffbce9 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/CleanupVmInstanceMetadataOnPrimaryStorageGC.java @@ -0,0 +1,106 @@ +package org.zstack.compute.vm; + +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.core.cloudbus.CloudBusCallBack; +import org.zstack.core.componentloader.PluginRegistry; +import org.zstack.core.db.Q; +import org.zstack.core.gc.GC; +import org.zstack.core.gc.GCCompletion; +import org.zstack.core.gc.TimeBasedGarbageCollector; +import org.zstack.header.host.HostVO; +import org.zstack.header.message.MessageReply; +import org.zstack.header.storage.primary.CleanupVmInstanceMetadataOnPrimaryStorageMsg; +import org.zstack.header.storage.primary.PrimaryStorageConstant; +import org.zstack.header.storage.primary.PrimaryStorageVO; +import org.zstack.header.storage.primary.PrimaryStorageVO_; +import org.zstack.header.vm.metadata.VmMetadataPathBuildExtensionPoint; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +public class CleanupVmInstanceMetadataOnPrimaryStorageGC extends TimeBasedGarbageCollector { + private static final CLogger logger = Utils.getLogger(CleanupVmInstanceMetadataOnPrimaryStorageGC.class); + + @Autowired + private PluginRegistry pluginRgty; + + @GC + public String primaryStorageUuid; + @GC + public String vmUuid; + @GC + public String rootVolumeUuid; + @GC + public String metadataPath; + @GC + public String hostUuid; + + public static String getGCName(String vmUuid) { + return String.format("gc-cleanup-vm-metadata-%s", vmUuid); + } + + @Override + protected void triggerNow(GCCompletion completion) { + if (!dbf.isExist(primaryStorageUuid, PrimaryStorageVO.class)) { + logger.debug(String.format("[MetadataCleanupGC] primary storage[uuid:%s] no longer exists, " + + "cancel gc for vm[uuid:%s]", primaryStorageUuid, vmUuid)); + completion.cancel(); + return; + } + + String psType = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.type).eq(PrimaryStorageVO_.uuid, primaryStorageUuid).findValue(); + if (psType == null) { + logger.debug(String.format("[MetadataCleanupGC] primary storage[uuid:%s] type not found, " + + "cancel gc for vm[uuid:%s]", primaryStorageUuid, vmUuid)); + completion.cancel(); + return; + } + + VmMetadataPathBuildExtensionPoint ext = pluginRgty.getExtensionFromMap(psType, VmMetadataPathBuildExtensionPoint.class); + boolean requireHost = ext != null && ext.requireHostForCleanup(); + + // Determine effective hostUuid based on whether the PS type requires a host for cleanup. + String effectiveHostUuid = hostUuid; + if (!requireHost) { + effectiveHostUuid = null; + } else { + if (effectiveHostUuid == null) { + logger.debug(String.format("[MetadataCleanupGC] hostUuid is null and ps[uuid:%s, type:%s] " + + "requires host for cleanup, cancel gc for vm[uuid:%s]", + primaryStorageUuid, psType, vmUuid)); + completion.cancel(); + return; + } + if (!dbf.isExist(effectiveHostUuid, HostVO.class)) { + logger.debug(String.format("[MetadataCleanupGC] host[uuid:%s] no longer exists " + + "and ps[uuid:%s, type:%s] requires host for cleanup, " + + "metadata is unreachable, cancel gc for vm[uuid:%s]", + effectiveHostUuid, primaryStorageUuid, psType, vmUuid)); + completion.cancel(); + return; + } + } + + CleanupVmInstanceMetadataOnPrimaryStorageMsg msg = new CleanupVmInstanceMetadataOnPrimaryStorageMsg(); + msg.setPrimaryStorageUuid(primaryStorageUuid); + msg.setVmInstanceUuid(vmUuid); + msg.setRootVolumeUuid(rootVolumeUuid); + msg.setMetadataPath(metadataPath); + msg.setHostUuid(effectiveHostUuid); + + bus.makeTargetServiceIdByResourceUuid(msg, PrimaryStorageConstant.SERVICE_ID, primaryStorageUuid); + bus.send(msg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply reply) { + if (reply.isSuccess()) { + logger.info(String.format("[MetadataCleanupGC] successfully cleaned up metadata " + + "for vm[uuid:%s] on ps[uuid:%s]", vmUuid, primaryStorageUuid)); + completion.success(); + } else { + logger.warn(String.format("[MetadataCleanupGC] failed to clean up metadata " + + "for vm[uuid:%s] on ps[uuid:%s]: %s", vmUuid, primaryStorageUuid, reply.getError())); + completion.fail(reply.getError()); + } + } + }); + } +} diff --git a/compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java new file mode 100644 index 00000000000..00daedea93f --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java @@ -0,0 +1,138 @@ +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.cloudbus.CloudBus; +import org.zstack.core.cloudbus.CloudBusCallBack; +import org.zstack.core.componentloader.PluginRegistry; +import org.zstack.core.db.Q; +import org.zstack.header.core.workflow.FlowTrigger; +import org.zstack.header.core.workflow.NoRollbackFlow; +import org.zstack.header.message.MessageReply; +import org.zstack.header.storage.primary.CleanupVmInstanceMetadataOnPrimaryStorageMsg; +import org.zstack.header.storage.primary.PrimaryStorageConstant; +import org.zstack.header.storage.primary.PrimaryStorageVO; +import org.zstack.header.storage.primary.PrimaryStorageVO_; +import org.zstack.header.vm.VmInstanceConstant; +import org.zstack.header.vm.VmInstanceSpec; +import org.zstack.header.vm.metadata.VmMetadataPathBuildExtensionPoint; +import org.zstack.header.volume.VolumeInventory; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) +public class VmExpungeMetadataFlow extends NoRollbackFlow { + private static final CLogger logger = Utils.getLogger(VmExpungeMetadataFlow.class); + + @Autowired + private CloudBus bus; + @Autowired + private PluginRegistry pluginRgty; + + @Override + public void run(FlowTrigger trigger, Map data) { + final VmInstanceSpec spec = (VmInstanceSpec) data.get(VmInstanceConstant.Params.VmInstanceSpec.toString()); + if (spec == null || spec.getVmInventory() == null) { + logger.warn("[MetadataExpunge] missing VmInstanceSpec or VmInventory, skip metadata cleanup"); + trigger.next(); + return; + } + + final String vmUuid = spec.getVmInventory().getUuid(); + + VolumeInventory rootVolume = spec.getVmInventory().getRootVolume(); + String psUuid = rootVolume != null ? rootVolume.getPrimaryStorageUuid() : null; + if (psUuid == null) { + logger.debug(String.format("[MetadataExpunge] vm[uuid:%s] root volume has no primaryStorageUuid, " + + "skipping metadata cleanup", vmUuid)); + trigger.next(); + return; + } + + + String psType = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.type).eq(PrimaryStorageVO_.uuid, psUuid).findValue(); + if (psType == null) { + logger.warn(String.format("[MetadataExpunge] primary storage[uuid:%s] not found for vm[uuid:%s], " + + "skip metadata cleanup", psUuid, vmUuid)); + trigger.next(); + return; + } + + VmMetadataPathBuildExtensionPoint ext = pluginRgty.getExtensionFromMap(psType, VmMetadataPathBuildExtensionPoint.class); + if (ext == null) { + logger.warn(String.format("[MetadataExpunge] no VmMetadataPathBuildExtensionPoint found for ps[uuid:%s, type:%s], " + + "skip metadata cleanup", psUuid, psType)); + trigger.next(); + return; + } + final String metadataPath; + try { + metadataPath = ext.buildVmMetadataPath(psUuid, vmUuid); + } catch (Exception e) { + logger.warn(String.format("[MetadataExpunge] failed to build metadata path for vm[uuid:%s] on ps[uuid:%s], " + + "skip metadata cleanup: %s", vmUuid, psUuid, e.getMessage())); + trigger.next(); + return; + } + + String hostUuid = null; + if (ext.requireHostForCleanup()) { + hostUuid = spec.getVmInventory().getHostUuid(); + if (hostUuid == null) { + hostUuid = spec.getVmInventory().getLastHostUuid(); + } + + if (hostUuid == null) { + logger.warn(String.format("[MetadataExpunge] vm[uuid:%s] hostUuid is null, " + + "ps[uuid:%s, type:%s] requires host for cleanup, skip without submitting GC", + vmUuid, psUuid, psType)); + trigger.next(); + return; + } + } + + String rootVolumeUuid = rootVolume.getUuid(); + CleanupVmInstanceMetadataOnPrimaryStorageMsg cmsg = new CleanupVmInstanceMetadataOnPrimaryStorageMsg(); + cmsg.setPrimaryStorageUuid(psUuid); + cmsg.setVmInstanceUuid(vmUuid); + cmsg.setMetadataPath(metadataPath); + cmsg.setRootVolumeUuid(rootVolumeUuid); + cmsg.setHostUuid(hostUuid); + final String finalPsUuid = psUuid; + final String finalHostUuid = hostUuid; + + bus.makeTargetServiceIdByResourceUuid(cmsg, PrimaryStorageConstant.SERVICE_ID, psUuid); + bus.send(cmsg, new CloudBusCallBack(trigger) { + @Override + public void run(MessageReply reply) { + if (reply.isSuccess()) { + logger.info(String.format("[MetadataExpunge] successfully deleted metadata for vm[uuid:%s] on ps[uuid:%s]", + vmUuid, finalPsUuid)); + } else { + logger.warn(String.format("[MetadataExpunge] failed to delete metadata for vm[uuid:%s] on ps[uuid:%s]: %s, " + + "submitting GC job for retry", vmUuid, finalPsUuid, reply.getError())); + submitGC(finalPsUuid, vmUuid, rootVolumeUuid, metadataPath, finalHostUuid); + } + trigger.next(); + } + }); + } + + private void submitGC(String psUuid, String vmUuid, String rootVolumeUuid, String metadataPath, String hostUuid) { + CleanupVmInstanceMetadataOnPrimaryStorageGC gc = new CleanupVmInstanceMetadataOnPrimaryStorageGC(); + gc.NAME = CleanupVmInstanceMetadataOnPrimaryStorageGC.getGCName(vmUuid); + gc.primaryStorageUuid = psUuid; + gc.vmUuid = vmUuid; + gc.rootVolumeUuid = rootVolumeUuid; + gc.metadataPath = metadataPath; + gc.hostUuid = hostUuid; + long gcIntervalSec = TimeUnit.HOURS.toSeconds(VmGlobalConfig.VM_METADATA_CLEANUP_GC_INTERVAL.value(Long.class)); + gc.deduplicateSubmit(gcIntervalSec, TimeUnit.SECONDS); + + logger.info(String.format("[MetadataExpunge] submitted GC job [%s] for vm[uuid:%s] on ps[uuid:%s]", gc.NAME, vmUuid, psUuid)); + } +} 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 fa1b3808c44..fa72dcbe396 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java @@ -143,4 +143,52 @@ public class VmGlobalConfig { @GlobalConfigDef(defaultValue = "false", type = Boolean.class, description = "allowed TPM VM start without KMS") @GlobalConfigValidation(validValues = {"true", "false"}) public static GlobalConfig ALLOWED_TPM_VM_WITHOUT_KMS = new GlobalConfig(CATEGORY, "allowed.tpm.vm.without.kms"); + + @GlobalConfigValidation(validValues = {"true", "false"}) + public static GlobalConfig VM_METADATA_ENABLED = new GlobalConfig(CATEGORY, "vm.metadata.enabled"); + + @GlobalConfigValidation() + public static GlobalConfig VM_METADATA_LAST_REFRESH_VERSION = new GlobalConfig(CATEGORY, "vm.metadata.lastRefreshVersion"); + + @GlobalConfigValidation(numberGreaterThan = 0, numberLessThan = 100) + public static GlobalConfig VM_METADATA_FLUSH_CONCURRENCY = new GlobalConfig(CATEGORY, "vm.metadata.flush.concurrency"); + + @GlobalConfigValidation(numberGreaterThan = 0, numberLessThan = 300) + public static GlobalConfig VM_METADATA_FLUSH_POLL_INTERVAL = new GlobalConfig(CATEGORY, "vm.metadata.flush.pollInterval"); + + @GlobalConfigValidation(numberGreaterThan = 0, numberLessThan = 1000) + public static GlobalConfig VM_METADATA_FLUSH_BATCH_SIZE = new GlobalConfig(CATEGORY, "vm.metadata.flush.batchSize"); + + @GlobalConfigValidation(numberGreaterThan = 0, numberLessThan = 168) + public static GlobalConfig VM_METADATA_CLEANUP_GC_INTERVAL = new GlobalConfig(CATEGORY, "vm.metadata.cleanup.gc.interval"); + + @GlobalConfigValidation(numberGreaterThan = 0, numberLessThan = 10) + public static GlobalConfig VM_METADATA_FLUSH_MAX_RETRY = new GlobalConfig(CATEGORY, "vm.metadata.flush.maxRetry"); + + @GlobalConfigValidation(numberGreaterThan = 0, numberLessThan = 120) + public static GlobalConfig VM_METADATA_FLUSH_ZOMBIE_CLAIM_THRESHOLD = new GlobalConfig(CATEGORY, "vm.metadata.flush.zombieClaimThreshold"); + + @GlobalConfigValidation(numberGreaterThan = 21599, numberLessThan = 172801) + public static GlobalConfig VM_METADATA_MAINTENANCE_CONTENT_DRIFT_INTERVAL = new GlobalConfig(CATEGORY, "vm.metadata.maintenance.contentDriftInterval"); + + @GlobalConfigValidation(numberGreaterThan = 0, numberLessThan = 86400) + public static GlobalConfig VM_METADATA_MAINTENANCE_STALE_RECOVERY_INTERVAL = new GlobalConfig(CATEGORY, "vm.metadata.maintenance.staleRecoveryInterval"); + + @GlobalConfigValidation(numberGreaterThan = 0, numberLessThan = 1000) + public static GlobalConfig VM_METADATA_MAINTENANCE_STALE_RECOVERY_MAX_CYCLES = new GlobalConfig(CATEGORY, "vm.metadata.maintenance.staleRecoveryMaxCycles"); + + @GlobalConfigValidation(numberGreaterThan = 0) + public static GlobalConfig VM_METADATA_PAYLOAD_REJECT_THRESHOLD = new GlobalConfig(CATEGORY, "vm.metadata.payload.rejectThreshold"); + + @GlobalConfigValidation(numberGreaterThan = 0, numberLessThan = 86400) + public static GlobalConfig VM_METADATA_MAINTENANCE_ORPHAN_CHECK_INTERVAL = new GlobalConfig(CATEGORY, "vm.metadata.maintenance.orphanCheckInterval"); + + @GlobalConfigValidation(numberGreaterThan = 0, numberLessThan = 20) + public static GlobalConfig VM_METADATA_MAINTENANCE_STALE_RECOVERY_BATCH_SIZE = new GlobalConfig(CATEGORY, "vm.metadata.maintenance.staleRecoveryBatchSize"); + + @GlobalConfigValidation(numberGreaterThan = 9, numberLessThan = 201) + public static GlobalConfig VM_METADATA_MAINTENANCE_CONTENT_DRIFT_BATCH_SIZE = new GlobalConfig(CATEGORY, "vm.metadata.maintenance.contentDriftBatchSize"); + + @GlobalConfigValidation(numberGreaterThan = 0, numberLessThan = 31) + public static GlobalConfig VM_METADATA_MAINTENANCE_CONTENT_DRIFT_BATCH_SLEEP_SEC = new GlobalConfig(CATEGORY, "vm.metadata.maintenance.contentDriftBatchSleepSec"); } 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 9d110a31e46..57169124127 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java @@ -23,14 +23,22 @@ import org.zstack.header.network.l3.*; import org.zstack.header.storage.primary.PrimaryStorageClusterRefVO; import org.zstack.header.storage.primary.PrimaryStorageClusterRefVO_; +import org.zstack.header.storage.primary.PrimaryStorageVO; +import org.zstack.header.storage.primary.PrimaryStorageVO_; import org.zstack.header.storage.snapshot.VolumeSnapshotVO; import org.zstack.header.storage.snapshot.VolumeSnapshotVO_; import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO; import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO_; +import org.zstack.header.host.HostState; +import org.zstack.header.host.HostStatus; +import org.zstack.header.host.HostVO; +import org.zstack.header.host.HostVO_; import org.zstack.header.vm.*; import org.zstack.header.vm.cdrom.*; +import org.zstack.header.vm.APIRegisterVmInstanceFromMetadataMsg; import org.zstack.header.vm.devices.VmInstanceResourceMetadataGroupVO; import org.zstack.header.vm.devices.VmInstanceResourceMetadataGroupVO_; +import org.zstack.header.vm.metadata.VmMetadataPathBuildExtensionPoint; import org.zstack.header.volume.*; import org.zstack.network.l2.L2NetworkHostUtils; import org.zstack.resourceconfig.ResourceConfigFacade; @@ -166,6 +174,8 @@ else if (msg instanceof APIAttachVmNicToVmMsg) { validate((APIConvertTemplatedVmInstanceToVmInstanceMsg) msg); } else if (msg instanceof APIDeleteTemplatedVmInstanceMsg) { validate((APIDeleteTemplatedVmInstanceMsg) msg); + } else if (msg instanceof APIRegisterVmInstanceFromMetadataMsg) { + validate((APIRegisterVmInstanceFromMetadataMsg) msg); } if (msg instanceof NewVmInstanceMessage2) { @@ -1319,4 +1329,65 @@ private void validate(APIFstrimVmMsg msg) { } msg.setHostUuid(t.get(1, String.class)); } + + private void validate(APIRegisterVmInstanceFromMetadataMsg msg) { + String path = msg.getMetadataPath(); + if (StringUtils.isEmpty(path)) { + throw new ApiMessageInterceptionException(argerr("metadataPath cannot be empty or null")); + } + + // Delegate path validation to the storage-type-specific extension + String psUuid = msg.getPrimaryStorageUuid(); + String psType = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.type).eq(PrimaryStorageVO_.uuid, psUuid).findValue(); + if (psType == null) { + throw new ApiMessageInterceptionException(argerr( + "primary storage[uuid:%s] not found", psUuid)); + } + + VmMetadataPathBuildExtensionPoint ext = pluginRgty.getExtensionFromMap(psType, VmMetadataPathBuildExtensionPoint.class); + if (ext == null) { + throw new ApiMessageInterceptionException(argerr( + "primary storage[uuid:%s, type:%s] does not support vm metadata", psUuid, psType)); + } + + String error = ext.validateMetadataPath(psUuid, path); + if (error != null) { + throw new ApiMessageInterceptionException(argerr("%s", error)); + } + + boolean psAttachedToCluster = Q.New(PrimaryStorageClusterRefVO.class) + .eq(PrimaryStorageClusterRefVO_.primaryStorageUuid, psUuid) + .eq(PrimaryStorageClusterRefVO_.clusterUuid, msg.getClusterUuid()) + .isExists(); + if (!psAttachedToCluster) { + throw new ApiMessageInterceptionException(argerr( + "primary storage[uuid:%s] is not attached to cluster[uuid:%s]", + psUuid, msg.getClusterUuid())); + } + + if (msg.getHostUuid() != null) { + boolean hostAvailable = Q.New(HostVO.class) + .eq(HostVO_.uuid, msg.getHostUuid()) + .eq(HostVO_.clusterUuid, msg.getClusterUuid()) + .eq(HostVO_.state, HostState.Enabled) + .eq(HostVO_.status, HostStatus.Connected) + .isExists(); + if (!hostAvailable) { + throw new ApiMessageInterceptionException(argerr( + "host[uuid:%s] is not in cluster[uuid:%s] or not Enabled/Connected", msg.getHostUuid(), msg.getClusterUuid())); + } + } else { + boolean hasHost = Q.New(HostVO.class) + .eq(HostVO_.clusterUuid, msg.getClusterUuid()) + .eq(HostVO_.state, HostState.Enabled) + .eq(HostVO_.status, HostStatus.Connected) + .isExists(); + if (!hasHost) { + throw new ApiMessageInterceptionException(argerr( + "no available host found in cluster[uuid:%s], " + + "please specify hostUuid or ensure there is at least one connected host in the cluster", + msg.getClusterUuid())); + } + } + } } diff --git a/compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java b/compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java index 52aa2282c87..89b56e921b7 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java @@ -307,4 +307,8 @@ public String desensitizeTag(SystemTag systemTag, String tag) { } public static PatternedSystemTag VM_STATE_PAUSED_AFTER_MIGRATE = new PatternedSystemTag(("vmPausedAfterMigrate"), VmInstanceVO.class); + + public static String VM_METADATA_REGISTERING_MN_UUID_TOKEN = "registeringMnUuid"; + public static PatternedSystemTag VM_METADATA_REGISTERING_MN_UUID = new PatternedSystemTag( + String.format("vmMetadata::registeringMnUuid::{%s}", VM_METADATA_REGISTERING_MN_UUID_TOKEN), VmInstanceVO.class); } diff --git a/conf/db/zsv/V5.0.0__schema.sql b/conf/db/zsv/V5.0.0__schema.sql index f0934eda506..1dd6aeee2a9 100644 --- a/conf/db/zsv/V5.0.0__schema.sql +++ b/conf/db/zsv/V5.0.0__schema.sql @@ -127,3 +127,30 @@ CREATE TABLE IF NOT EXISTS `zstack`.`EncryptedResourceKeyRefVO` ( INDEX `idxEncryptedResourceKeyRefVOProviderName` (`providerName`), CONSTRAINT `fkEncryptedResourceKeyRefVOProviderUuid` FOREIGN KEY (`providerUuid`) REFERENCES `KeyProviderVO` (`uuid`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`VmMetadataDirtyVO` ( + `vmInstanceUuid` VARCHAR(32) NOT NULL, + `managementNodeUuid` VARCHAR(32) DEFAULT NULL, + `dirtyVersion` BIGINT NOT NULL DEFAULT 1, + `lastClaimTime` TIMESTAMP NULL DEFAULT NULL, + `storageStructureChange` TINYINT(1) NOT NULL DEFAULT 0, + `retryCount` INT NOT NULL DEFAULT 0, + `nextRetryTime` 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 (`vmInstanceUuid`), + CONSTRAINT `fkVmMetadataDirtyVOVmInstanceEO` FOREIGN KEY (`vmInstanceUuid`) REFERENCES `VmInstanceEO` (`uuid`) ON DELETE CASCADE, + CONSTRAINT `fkVmMetadataDirtyVOManagementNodeVO` FOREIGN KEY (`managementNodeUuid`) REFERENCES `ManagementNodeVO` (`uuid`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `zstack`.`VmMetadataFlushStateVO` ( + `vmInstanceUuid` VARCHAR(32) NOT NULL, + `metadataSnapshot` LONGTEXT, + `lastFlushFinishTime` TIMESTAMP NULL DEFAULT NULL, + `pendingStaleRecovery` TINYINT(1) NOT NULL DEFAULT 0, + `staleRecoveryCount` INT NOT NULL DEFAULT 0, + `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59', + PRIMARY KEY (`vmInstanceUuid`), + CONSTRAINT `fkVmMetadataFlushStateVOVmInstanceEO` FOREIGN KEY (`vmInstanceUuid`) REFERENCES `VmInstanceEO` (`uuid`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/conf/globalConfig/vm.xml b/conf/globalConfig/vm.xml index 2542bc2cb03..900d91a7749 100755 --- a/conf/globalConfig/vm.xml +++ b/conf/globalConfig/vm.xml @@ -317,4 +317,133 @@ java.lang.Boolean false + + + vm + vm.metadata.enabled + enable vm metadata + java.lang.Boolean + false + + + + vm + vm.metadata.lastRefreshVersion + Last completed upgrade refresh version, prevents duplicate triggers across MNs. Internal use only + java.lang.String + 5.0.0 + + + + vm + vm.metadata.flush.concurrency + Max concurrent metadata flush tasks per management node + java.lang.Integer + 10 + + + + vm + vm.metadata.flush.pollInterval + Metadata dirty poller interval in seconds + java.lang.Long + 5 + + + + vm + vm.metadata.flush.batchSize + Max dirty rows claimed per poll cycle + java.lang.Integer + 20 + + + + vm + vm.metadata.cleanup.gc.interval + Metadata cleanup GC retry interval in hours + java.lang.Long + 8 + + + + vm + vm.metadata.flush.maxRetry + Max flush retry count before marking VM metadata as stale + java.lang.Integer + 5 + + + + vm + vm.metadata.flush.zombieClaimThreshold + Minutes before an uncompleted flush claim is considered zombie and released + java.lang.Long + 15 + + + + vm + vm.metadata.maintenance.contentDriftInterval + Content drift detection interval in seconds. Drift detection is a fallback safety net; the primary mechanism is dirty marking via interceptors. + java.lang.Long + 86400 + + + + vm + vm.metadata.maintenance.staleRecoveryInterval + Stale VM metadata recovery interval in seconds + java.lang.Long + 600 + + + + vm + vm.metadata.maintenance.staleRecoveryMaxCycles + Max stale recovery cycles before entering permanent-stale state + java.lang.Integer + 10 + + + + vm + vm.metadata.payload.rejectThreshold + Max allowed VM metadata payload size in bytes. VMs exceeding this are skipped. + java.lang.Long + 33554432 + + + + vm + vm.metadata.maintenance.orphanCheckInterval + Orphan dirty-row detector interval in seconds. Releases rows claimed by dead management nodes. + java.lang.Long + 3600 + + + + vm + vm.metadata.maintenance.staleRecoveryBatchSize + Max stale VMs processed per recovery cycle + java.lang.Integer + 10 + + + + vm + vm.metadata.maintenance.contentDriftBatchSize + Number of VMs checked per batch during content drift detection + java.lang.Integer + 10 + + + + vm + vm.metadata.maintenance.contentDriftBatchSleepSec + Sleep time in seconds between content drift detection batches to limit DB pressure + java.lang.Long + 1 + + diff --git a/conf/persistence.xml b/conf/persistence.xml index 8f3b34b367c..e0b9cccdb32 100755 --- a/conf/persistence.xml +++ b/conf/persistence.xml @@ -221,5 +221,7 @@ org.zstack.header.resourceattribute.entity.ResourceAttributeKeyResourceTypeVO org.zstack.header.resourceattribute.entity.ResourceAttributeConstraintVO org.zstack.softwarePackage.header.SoftwarePackageVO + org.zstack.header.vm.metadata.VmMetadataDirtyVO + org.zstack.header.vm.metadata.VmMetadataFlushStateVO diff --git a/conf/serviceConfig/primaryStorage.xml b/conf/serviceConfig/primaryStorage.xml index c928d2f6d8c..dcac7c151c0 100755 --- a/conf/serviceConfig/primaryStorage.xml +++ b/conf/serviceConfig/primaryStorage.xml @@ -93,4 +93,8 @@ org.zstack.header.storage.primary.APITakeoverPrimaryStorageMsg + + + org.zstack.header.storage.primary.APIScanVmInstanceMetadataFromPrimaryStorageMsg + diff --git a/conf/serviceConfig/vmInstance.xml b/conf/serviceConfig/vmInstance.xml index ab73fb6bea9..730951e0ece 100755 --- a/conf/serviceConfig/vmInstance.xml +++ b/conf/serviceConfig/vmInstance.xml @@ -264,4 +264,16 @@ org.zstack.header.vm.APIDeleteTemplatedVmInstanceMsg + + org.zstack.header.vm.APICleanupVmInstanceMetadataMsg + + + org.zstack.header.vm.APIRegisterVmInstanceFromMetadataMsg + + + org.zstack.header.vm.APIUpdateVmInstanceMetadataMsg + + + org.zstack.header.vm.APIGetVmInstanceMetadataFromPrimaryStorageMsg + diff --git a/conf/springConfigXml/VmInstanceManager.xml b/conf/springConfigXml/VmInstanceManager.xml index 8e6181c81c0..1b284284d29 100755 --- a/conf/springConfigXml/VmInstanceManager.xml +++ b/conf/springConfigXml/VmInstanceManager.xml @@ -118,6 +118,7 @@ org.zstack.compute.vm.VmExpungeRootVolumeFlow org.zstack.compute.vm.VmExpungeMemoryVolumeFlow org.zstack.compute.vm.VmExpungeCacheVolumeFlow + org.zstack.compute.vm.VmExpungeMetadataFlow 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 5798d7c6951..0312aa7c028 100755 --- a/storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java +++ b/storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java @@ -6,6 +6,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Configurable; import org.springframework.transaction.annotation.Transactional; +import org.zstack.core.Platform; import org.zstack.core.asyncbatch.While; import org.zstack.core.cascade.CascadeConstant; import org.zstack.core.cascade.CascadeFacade; @@ -50,6 +51,7 @@ import org.zstack.header.storage.primary.PrimaryStorageCanonicalEvent.PrimaryStorageStatusChangedData; import org.zstack.header.storage.snapshot.*; import org.zstack.header.vm.*; +import org.zstack.header.vm.metadata.*; import org.zstack.header.volume.*; import org.zstack.storage.volume.VolumeUtils; import org.zstack.utils.CollectionDSL; @@ -318,6 +320,9 @@ private void checkPrimaryStatus(Message msg) { new PrimaryStorageValidater().disable().maintenance() .validate(); } + } else if (msg instanceof RebaseVolumeBackingFileOnPrimaryStorageMsg) { + new PrimaryStorageValidater().maintenance() + .validate(); } } @@ -417,6 +422,16 @@ protected void handleLocalMessage(Message msg) { handle((DeleteVolumeChainOnPrimaryStorageMsg) msg); } else if (msg instanceof CleanUpStorageTrashOnPrimaryStorageMsg) { handle((CleanUpStorageTrashOnPrimaryStorageMsg)msg); + } else if (msg instanceof UpdateVmInstanceMetadataOnPrimaryStorageMsg) { + handle((UpdateVmInstanceMetadataOnPrimaryStorageMsg) msg); + } else if (msg instanceof ScanVmInstanceMetadataFromPrimaryStorageMsg) { + handle((ScanVmInstanceMetadataFromPrimaryStorageMsg) msg); + } else if (msg instanceof GetVmInstanceMetadataFromPrimaryStorageMsg) { + handle((GetVmInstanceMetadataFromPrimaryStorageMsg) msg); + } else if (msg instanceof CleanupVmInstanceMetadataOnPrimaryStorageMsg) { + handle((CleanupVmInstanceMetadataOnPrimaryStorageMsg) msg); + } else if (msg instanceof RebaseVolumeBackingFileOnPrimaryStorageMsg) { + handle((RebaseVolumeBackingFileOnPrimaryStorageMsg) msg); } else { bus.dealWithUnknownMessage(msg); } @@ -939,6 +954,8 @@ protected void handleApiMessage(APIMessage msg) { handle((APITakeoverPrimaryStorageMsg) msg); } else if (msg instanceof APICheckPrimaryStorageConsistencyMsg) { handle((APICheckPrimaryStorageConsistencyMsg) msg); + } else if (msg instanceof APIScanVmInstanceMetadataFromPrimaryStorageMsg) { + handle((APIScanVmInstanceMetadataFromPrimaryStorageMsg) msg); } else { bus.dealWithUnknownMessage(msg); } @@ -1789,6 +1806,36 @@ protected void handle(UnlinkBitsOnPrimaryStorageMsg msg) { bus.reply(msg, reply); }; + protected void handle(UpdateVmInstanceMetadataOnPrimaryStorageMsg msg) { + UpdateVmInstanceMetadataOnPrimaryStorageReply reply = new UpdateVmInstanceMetadataOnPrimaryStorageReply(); + reply.setError(operr("operation not supported")); + bus.reply(msg, reply); + } + + protected void handle(GetVmInstanceMetadataFromPrimaryStorageMsg msg) { + GetVmInstanceMetadataFromPrimaryStorageReply reply = new GetVmInstanceMetadataFromPrimaryStorageReply(); + reply.setError(operr("operation not supported")); + bus.reply(msg, reply); + } + + protected void handle(ScanVmInstanceMetadataFromPrimaryStorageMsg msg) { + ScanVmInstanceMetadataFromPrimaryStorageReply reply = new ScanVmInstanceMetadataFromPrimaryStorageReply(); + reply.setError(operr("operation not supported")); + bus.reply(msg, reply); + } + + protected void handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg msg) { + CleanupVmInstanceMetadataOnPrimaryStorageReply reply = new CleanupVmInstanceMetadataOnPrimaryStorageReply(); + reply.setError(operr("operation not supported")); + bus.reply(msg, reply); + } + + protected void handle(RebaseVolumeBackingFileOnPrimaryStorageMsg msg) { + RebaseVolumeBackingFileOnPrimaryStorageReply reply = new RebaseVolumeBackingFileOnPrimaryStorageReply(); + reply.setError(operr("operation not supported")); + bus.reply(msg, reply); + } + // don't attach any cluster public boolean isUnmounted() { long count = Q.New(PrimaryStorageClusterRefVO.class) @@ -1828,4 +1875,56 @@ protected ImageCacheVO createTemporaryImageCacheFromVolumeSnapshot(ImageInventor private static String getDeduplicateError(String operationName) { return String.format("an other %s task is running, cancel this operation", operationName); } + + private void handle(APIScanVmInstanceMetadataFromPrimaryStorageMsg msg) { + APIScanVmInstanceMetadataFromPrimaryStorageEvent evt = new APIScanVmInstanceMetadataFromPrimaryStorageEvent(msg.getId()); + + if (self.getStatus() != PrimaryStorageStatus.Connected) { + evt.setError(Platform.operr("primary storage[uuid:%s] is not Connected (status=%s), cannot scan metadata", + self.getUuid(), self.getStatus())); + bus.publish(evt); + return; + } + + String psType = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.type).eq(PrimaryStorageVO_.uuid, msg.getPrimaryStorageUuid()).findValue(); + if (psType == null) { + evt.setError(Platform.operr("primary storage[uuid:%s] not found", msg.getPrimaryStorageUuid())); + bus.publish(evt); + return; + } + VmMetadataPathBuildExtensionPoint ext = pluginRgty.getExtensionFromMap(psType, VmMetadataPathBuildExtensionPoint.class); + if (ext == null) { + evt.setError(Platform.operr("primary storage type %s does not support metadata", psType)); + bus.publish(evt); + return; + } + String metadataDir = ext.buildMetadataDir(msg.getPrimaryStorageUuid()); + + ScanVmInstanceMetadataFromPrimaryStorageMsg gmsg = new ScanVmInstanceMetadataFromPrimaryStorageMsg(); + gmsg.setPrimaryStorageUuid(msg.getPrimaryStorageUuid()); + gmsg.setMetadataDir(metadataDir); + bus.makeTargetServiceIdByResourceUuid(gmsg, PrimaryStorageConstant.SERVICE_ID, msg.getPrimaryStorageUuid()); + bus.send(gmsg, new CloudBusCallBack(msg) { + @Override + public void run(MessageReply r) { + if (!r.isSuccess()) { + evt.setError(r.getError()); + bus.publish(evt); + return; + } + ScanVmInstanceMetadataFromPrimaryStorageReply re = r.castReply(); + List metadata = re.getVmInstanceMetadata(); + if (metadata == null) { + metadata = Collections.emptyList(); + } + List filtered = metadata.stream() + .filter(Objects::nonNull) + .filter(e -> e.getVmCategory() != null) + .filter(e -> !VmMetadataCategory.VM_TEMPLATE_CACHE.name().equals(e.getVmCategory())) + .collect(Collectors.toList()); + evt.setVmInstanceMetadata(filtered); + bus.publish(evt); + } + }); + } } From 2aa69f43f960da55c53f7a6a74ab75e46d9d0288 Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Thu, 2 Apr 2026 17:59:06 +0800 Subject: [PATCH 103/129] [vm]: add metadata local and nfs storage backend Resolves: ZSV-11559 Change-Id: Idc34906c7c767f5fdf5213d751b293ec951a9b31 --- .../primary/local/LocalStorageBase.java | 296 +++++++++++++++++- .../local/LocalStorageHypervisorBackend.java | 12 + .../primary/local/LocalStorageKvmBackend.java | 174 +++++++++- .../primary/local/LocalStorageSimulator.java | 48 +++ .../local/LocalStorageSimulatorConfig.java | 5 + .../LocalStorageVmMetadataExtension.java | 179 +++++++++++ .../primary/nfs/NfsPrimaryStorage.java | 177 ++++++++++- .../primary/nfs/NfsPrimaryStorageBackend.java | 12 + .../nfs/NfsPrimaryStorageKVMBackend.java | 181 ++++++++++- .../NfsPrimaryStorageKVMBackendCommands.java | 48 +++ .../primary/nfs/NfsVmMetadataExtension.java | 125 ++++++++ 11 files changed, 1245 insertions(+), 12 deletions(-) create mode 100644 plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageVmMetadataExtension.java create mode 100644 plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsVmMetadataExtension.java diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java index d4665a86a06..1642c1c1097 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java @@ -36,9 +36,13 @@ import org.zstack.header.message.MessageReply; import org.zstack.header.message.OverlayMessage; import org.zstack.header.storage.primary.*; +import org.zstack.header.storage.primary.CleanupVmInstanceMetadataOnPrimaryStorageMsg; +import org.zstack.header.storage.primary.CleanupVmInstanceMetadataOnPrimaryStorageReply; import org.zstack.header.storage.primary.VolumeSnapshotCapability.VolumeSnapshotArrangementType; import org.zstack.header.storage.snapshot.*; import org.zstack.header.vm.*; +import org.zstack.header.vm.metadata.UpdateVmInstanceMetadataOnPrimaryStorageMsg; +import org.zstack.header.vm.metadata.UpdateVmInstanceMetadataOnPrimaryStorageReply; import org.zstack.header.vo.ResourceVO; import org.zstack.header.volume.*; import org.zstack.storage.primary.*; @@ -65,7 +69,8 @@ import static org.zstack.core.Platform.*; import static org.zstack.storage.primary.local.LocalStorageUtils.getHostUuidFromInstallUrl; import static org.zstack.utils.CollectionDSL.*; -import static org.zstack.utils.CollectionUtils.*; +import static org.zstack.utils.CollectionUtils.toMap; +import static org.zstack.utils.CollectionUtils.transformAndRemoveNull; /** * Created by frank on 6/30/2015. @@ -1640,6 +1645,33 @@ public void fail(ErrorCode errorCode) { }); } + protected void handle(RebaseVolumeBackingFileOnPrimaryStorageMsg msg) { + final LocalStorageHypervisorBackend bkd; + try { + LocalStorageHypervisorFactory f = getHypervisorBackendFactoryByHostUuid(msg.getHostUuid()); + bkd = f.getHypervisorBackend(self); + } catch (Exception e) { + RebaseVolumeBackingFileOnPrimaryStorageReply reply = new RebaseVolumeBackingFileOnPrimaryStorageReply(); + reply.setError(operr("failed to resolve host/backend for rebase on local primary storage[uuid:%s], hostUuid[%s]: %s", + self.getUuid(), msg.getHostUuid(), e.getMessage())); + bus.reply(msg, reply); + return; + } + bkd.handle(msg, msg.getHostUuid(), new ReturnValueCompletion(msg) { + @Override + public void success(RebaseVolumeBackingFileOnPrimaryStorageReply returnValue) { + bus.reply(msg, returnValue); + } + + @Override + public void fail(ErrorCode errorCode) { + RebaseVolumeBackingFileOnPrimaryStorageReply reply = new RebaseVolumeBackingFileOnPrimaryStorageReply(); + reply.setError(errorCode); + bus.reply(msg, reply); + } + }); + } + private void handle(RemoveHostFromLocalStorageMsg msg) { RemoveHostFromLocalStorageReply reply = new RemoveHostFromLocalStorageReply(); thdf.chainSubmit(new ChainTask(msg) { @@ -3329,4 +3361,266 @@ public void fail(ErrorCode errorCode) { public static class LocalStoragePhysicalCapacityUsage extends PrimaryStorageBase.PhysicalCapacityUsage { public long localStorageUsedSize; } + + @Override + protected void handle(final UpdateVmInstanceMetadataOnPrimaryStorageMsg msg) { + thdf.chainSubmit(new ChainTask(msg) { + @Override + public String getSyncSignature() { + return String.format("update-metadata-on-ps-%s", self.getUuid()); + } + + @Override + public int getSyncLevel() { + return 10; + } + + @Override + public void run(SyncTaskChain chain) { + final String hostUuid; + final LocalStorageHypervisorBackend bkd; + try { + hostUuid = getHostUuidByResourceUuid(msg.getRootVolumeUuid()); + LocalStorageHypervisorFactory f = getHypervisorBackendFactoryByHostUuid(hostUuid); + bkd = f.getHypervisorBackend(self); + } catch (Exception e) { + UpdateVmInstanceMetadataOnPrimaryStorageReply reply = new UpdateVmInstanceMetadataOnPrimaryStorageReply(); + reply.setError(operr("failed to resolve host for vm metadata update on local primary storage[uuid:%s], rootVolumeUuid[%s]: %s", + self.getUuid(), msg.getRootVolumeUuid(), e.getMessage())); + bus.reply(msg, reply); + chain.next(); + return; + } + bkd.handle(msg, hostUuid, new ReturnValueCompletion(msg, chain) { + @Override + public void success(UpdateVmInstanceMetadataOnPrimaryStorageReply returnValue) { + bus.reply(msg, returnValue); + chain.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + UpdateVmInstanceMetadataOnPrimaryStorageReply reply = new UpdateVmInstanceMetadataOnPrimaryStorageReply(); + reply.setError(errorCode); + bus.reply(msg, reply); + chain.next(); + } + }); + } + + @Override + public String getName() { + return String.format("update-metadata-on-ps-%s", self.getUuid()); + } + }); + } + + @Override + protected void handle(final GetVmInstanceMetadataFromPrimaryStorageMsg msg) { + GetVmInstanceMetadataFromPrimaryStorageReply reply = new GetVmInstanceMetadataFromPrimaryStorageReply(); + + String hostUuid = null; + if (msg.getHostUuid() != null) { + hostUuid = msg.getHostUuid(); + } + + if (hostUuid == null) { + if (msg.getRootVolumeUuid() == null) { + reply.setError(operr("cannot determine host for vm metadata get on local primary storage[uuid:%s]," + + " rootVolumeUuid is null", self.getUuid())); + bus.reply(msg, reply); + return; + } + try { + hostUuid = getHostUuidByResourceUuid(msg.getRootVolumeUuid()); + } catch (Exception e) { + reply.setError(operr("cannot determine host for vm metadata get on local primary storage[uuid:%s], rootVolumeUuid[%s]: %s", + self.getUuid(), msg.getRootVolumeUuid(), e.getMessage())); + bus.reply(msg, reply); + return; + } + } + + final LocalStorageHypervisorBackend bkd; + try { + LocalStorageHypervisorFactory f = getHypervisorBackendFactoryByHostUuid(hostUuid); + bkd = f.getHypervisorBackend(self); + } catch (Exception e) { + reply.setError(operr("failed to resolve host/backend for vm metadata get on local primary storage[uuid:%s], hostUuid[%s]: %s", + self.getUuid(), hostUuid, e.getMessage())); + bus.reply(msg, reply); + return; + } + bkd.handle(msg, hostUuid, new ReturnValueCompletion(msg) { + @Override + public void success(GetVmInstanceMetadataFromPrimaryStorageReply returnValue) { + bus.reply(msg, returnValue); + } + + @Override + public void fail(ErrorCode errorCode) { + reply.setError(errorCode); + bus.reply(msg, reply); + } + }); + } + + @Override + protected void handle(final ScanVmInstanceMetadataFromPrimaryStorageMsg msg) { + ScanVmInstanceMetadataFromPrimaryStorageReply reply = new ScanVmInstanceMetadataFromPrimaryStorageReply(); + + List allHostUuids = SQL.New( + "select h.hostUuid from LocalStorageHostRefVO h, HostVO host" + + " where h.primaryStorageUuid = :psUuid" + + " and h.hostUuid = host.uuid", String.class) + .param("psUuid", self.getUuid()) + .list(); + if (allHostUuids.isEmpty()) { + reply.setError(operr("no host found for local primary storage[uuid:%s]", self.getUuid())); + bus.reply(msg, reply); + return; + } + + List connectedHostUuids = SQL.New( + "select h.hostUuid from LocalStorageHostRefVO h, HostVO host" + + " where h.primaryStorageUuid = :psUuid" + + " and h.hostUuid = host.uuid" + + " and host.status = :hstatus", String.class) + .param("psUuid", self.getUuid()) + .param("hstatus", HostStatus.Connected) + .list(); + if (connectedHostUuids.isEmpty()) { + reply.setError(operr("no connected host found for local primary storage[uuid:%s], " + + "total hosts=%d", self.getUuid(), allHostUuids.size())); + bus.reply(msg, reply); + return; + } + + // Disconnected hosts cannot be scanned; treat them as failed upfront + List disconnectedHostUuids = new ArrayList<>(allHostUuids); + disconnectedHostUuids.removeAll(connectedHostUuids); + + List allSummaries = Collections.synchronizedList(new ArrayList<>()); + List failedHosts = Collections.synchronizedList(new ArrayList<>(disconnectedHostUuids)); + if (!disconnectedHostUuids.isEmpty()) { + logger.warn(String.format("local primary storage[uuid:%s] has %d disconnected hosts that cannot be scanned: %s", + self.getUuid(), disconnectedHostUuids.size(), disconnectedHostUuids)); + } + + new While<>(connectedHostUuids).all((hostUuid, com) -> { + final LocalStorageHypervisorBackend bkd; + try { + LocalStorageHypervisorFactory f = getHypervisorBackendFactoryByHostUuid(hostUuid); + bkd = f.getHypervisorBackend(self); + } catch (Exception e) { + failedHosts.add(hostUuid); + com.addError(operr("failed to prepare vm metadata scan on host[uuid:%s]: %s", + hostUuid, e.getMessage())); + com.done(); + return; + } + bkd.handle(msg, hostUuid, new ReturnValueCompletion(com) { + @Override + public void success(ScanVmInstanceMetadataFromPrimaryStorageReply returnValue) { + if (returnValue.getVmInstanceMetadata() != null) { + allSummaries.addAll(returnValue.getVmInstanceMetadata()); + } + com.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + logger.warn(String.format("failed to scan vm metadata from host[uuid:%s] on local primary storage[uuid:%s]: %s", + hostUuid, self.getUuid(), errorCode)); + failedHosts.add(hostUuid); + com.addError(errorCode); + com.done(); + } + }); + }).run(new WhileDoneCompletion(msg) { + @Override + public void done(ErrorCodeList errorCodeList) { + if (!errorCodeList.getCauses().isEmpty() && errorCodeList.getCauses().size() == connectedHostUuids.size()) { + reply.setError(operr("failed to scan vm metadata from all hosts on local primary storage[uuid:%s], causes: %s", + self.getUuid(), errorCodeList)); + } else { + reply.setVmInstanceMetadata(new ArrayList<>(allSummaries)); + if (!failedHosts.isEmpty()) { + logger.warn(String.format("partial scan failure on local primary storage[uuid:%s], " + + "failedHosts=%s, successHosts=%d/%d", self.getUuid(), failedHosts, + connectedHostUuids.size() - failedHosts.size(), connectedHostUuids.size())); + } + } + bus.reply(msg, reply); + } + }); + } + + @Override + protected void handle(final CleanupVmInstanceMetadataOnPrimaryStorageMsg msg) { + thdf.chainSubmit(new ChainTask(msg) { + @Override + public String getSyncSignature() { + return String.format("update-metadata-on-ps-%s-%s", self.getUuid(), msg.getVmInstanceUuid()); + } + + @Override + public int getSyncLevel() { + return 1; + } + + @Override + public void run(SyncTaskChain chain) { + CleanupVmInstanceMetadataOnPrimaryStorageReply reply = new CleanupVmInstanceMetadataOnPrimaryStorageReply(); + + String hostUuid = msg.getHostUuid(); + if (hostUuid == null && msg.getRootVolumeUuid() != null) { + try { + hostUuid = getHostUuidByResourceUuid(msg.getRootVolumeUuid()); + } catch (Exception e) { + logger.warn(String.format("failed to get host by rootVolumeUuid[%s]: %s", msg.getRootVolumeUuid(), e.getMessage())); + } + } + + if (hostUuid == null) { + reply.setError(operr("cannot determine host for vm metadata cleanup on local primary storage[uuid:%s]," + + " rootVolumeUuid[%s], hostUuid is null", self.getUuid(), msg.getRootVolumeUuid())); + bus.reply(msg, reply); + chain.next(); + return; + } + + final LocalStorageHypervisorBackend bkd; + try { + LocalStorageHypervisorFactory f = getHypervisorBackendFactoryByHostUuid(hostUuid); + bkd = f.getHypervisorBackend(self); + } catch (Exception e) { + reply.setError(operr("failed to resolve host/backend for vm metadata cleanup on local primary storage[uuid:%s], hostUuid[%s]: %s", + self.getUuid(), hostUuid, e.getMessage())); + bus.reply(msg, reply); + chain.next(); + return; + } + bkd.handle(msg, hostUuid, new ReturnValueCompletion(msg, chain) { + @Override + public void success(CleanupVmInstanceMetadataOnPrimaryStorageReply returnValue) { + bus.reply(msg, returnValue); + chain.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + reply.setError(errorCode); + bus.reply(msg, reply); + chain.next(); + } + }); + } + + @Override + public String getName() { + return String.format("cleanup-metadata-on-ps-%s-%s", self.getUuid(), msg.getVmInstanceUuid()); + } + }); + } } diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageHypervisorBackend.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageHypervisorBackend.java index 7760e28de93..d8226d932b2 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageHypervisorBackend.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageHypervisorBackend.java @@ -7,6 +7,8 @@ import org.zstack.header.image.ImageInventory; import org.zstack.header.storage.primary.*; import org.zstack.header.storage.snapshot.VolumeSnapshotInventory; +import org.zstack.header.vm.metadata.UpdateVmInstanceMetadataOnPrimaryStorageMsg; +import org.zstack.header.vm.metadata.UpdateVmInstanceMetadataOnPrimaryStorageReply; import org.zstack.header.volume.*; import org.zstack.storage.primary.EstimateVolumeTemplateSizeOnPrimaryStorageMsg; import org.zstack.storage.primary.EstimateVolumeTemplateSizeOnPrimaryStorageReply; @@ -121,4 +123,14 @@ public LocalStorageHypervisorBackend(PrimaryStorageVO self) { abstract void handle(CommitVolumeSnapshotOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion); abstract void handle(PullVolumeSnapshotOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion); + + abstract void handle(UpdateVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion); + + abstract void handle(GetVmInstanceMetadataFromPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion); + + abstract void handle(ScanVmInstanceMetadataFromPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion); + + abstract void handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion); + + abstract void handle(RebaseVolumeBackingFileOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion); } 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 ae4f66b9799..93b88a397c9 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 @@ -43,11 +43,15 @@ import org.zstack.header.rest.RESTFacade; import org.zstack.header.storage.backup.*; import org.zstack.header.storage.primary.*; -import org.zstack.header.storage.snapshot.*; +import org.zstack.header.storage.snapshot.VolumeSnapshotConstant; +import org.zstack.header.storage.snapshot.VolumeSnapshotInventory; +import org.zstack.header.storage.snapshot.VolumeSnapshotVO; import org.zstack.header.vm.VmInstanceSpec.ImageSpec; import org.zstack.header.vm.VmInstanceState; import org.zstack.header.vm.VmInstanceVO; import org.zstack.header.vm.VmInstanceVO_; +import org.zstack.header.vm.metadata.UpdateVmInstanceMetadataOnPrimaryStorageMsg; +import org.zstack.header.vm.metadata.UpdateVmInstanceMetadataOnPrimaryStorageReply; import org.zstack.header.volume.*; import org.zstack.identity.AccountManager; import org.zstack.kvm.*; @@ -68,9 +72,7 @@ import java.util.*; import java.util.stream.Collectors; -import static org.zstack.core.Platform.inerr; -import static org.zstack.core.Platform.multiErr; -import static org.zstack.core.Platform.operr; +import static org.zstack.core.Platform.*; import static org.zstack.utils.CollectionDSL.list; import static org.zstack.utils.CollectionUtils.transformAndRemoveNull; @@ -913,6 +915,52 @@ public void setHashValue(String hashValue) { } } + public static class WriteVmMetadataCmd extends AgentCommand { + public String metadata; + public String metadataPath; + public String vmUuid; + public String vmName; + public String vmCategory; + public String architecture; + public String schemaVersion; + } + + public static class WriteVmMetadataRsp extends AgentResponse { + } + + public static class GetVmInstanceMetadataCmd extends AgentCommand { + public String metadataPath; + } + + public static class GetVmInstanceMetadataRsp extends AgentResponse { + public String metadata; + } + + public static class ScanVmMetadataCmd extends AgentCommand { + public String metadataDir; + } + + public static class ScanVmMetadataRsp extends AgentResponse { + public List metadataEntries = new ArrayList<>(); + } + + public static class CleanupVmMetadataCmd extends AgentCommand { + public String metadataPath; + } + + public static class CleanupVmMetadataRsp extends AgentResponse { + } + + public static class PrefixRebaseBackingFilesCmd extends LocalStorageKvmBackend.AgentCommand { + public List filePaths; + public String oldPrefix; + public String newPrefix; + } + + public static class PrefixRebaseBackingFilesRsp extends LocalStorageKvmBackend.AgentResponse { + public int rebasedCount; + } + public static final String INIT_PATH = "/localstorage/init"; public static final String GET_PHYSICAL_CAPACITY_PATH = "/localstorage/getphysicalcapacity"; public static final String CREATE_EMPTY_VOLUME_PATH = "/localstorage/volume/createempty"; @@ -945,6 +993,11 @@ public void setHashValue(String hashValue) { public static final String CANCEL_DOWNLOAD_BITS_FROM_KVM_HOST_PATH = "/localstorage/kvmhost/download/cancel"; public static final String GET_DOWNLOAD_BITS_FROM_KVM_HOST_PROGRESS_PATH = "/localstorage/kvmhost/download/progress"; public static final String GET_QCOW2_HASH_VALUE_PATH = "/localstorage/getqcow2hash"; + public static final String WRITE_VM_METADATA_PATH = "/localstorage/vm/metadata/write"; + public static final String GET_VM_INSTANCE_METADATA_PATH = "/localstorage/vm/metadata/get"; + public static final String SCAN_VM_METADATA_PATH = "/localstorage/vm/metadata/scan"; + public static final String CLEANUP_VM_METADATA_PATH = "/localstorage/vm/metadata/cleanup"; + public static final String PREFIX_REBASE_BACKING_FILES_PATH = "/localstorage/snapshot/prefixrebasebackingfiles"; public LocalStorageKvmBackend() { } @@ -3815,4 +3868,117 @@ public void fail(ErrorCode errorCode) { } }); } + + @Override + void handle(UpdateVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion) { + WriteVmMetadataCmd cmd = new WriteVmMetadataCmd(); + cmd.metadata = msg.getMetadata(); + cmd.metadataPath = msg.getMetadataPath(); + cmd.vmUuid = msg.getVmInstanceUuid(); + cmd.vmName = msg.getVmInstanceName(); + cmd.vmCategory = msg.getVmCategory(); + cmd.architecture = msg.getArchitecture(); + cmd.schemaVersion = msg.getSchemaVersion(); + + httpCall(WRITE_VM_METADATA_PATH, hostUuid, cmd, WriteVmMetadataRsp.class, new ReturnValueCompletion(completion) { + @Override + public void success(WriteVmMetadataRsp rsp) { + completion.success(new UpdateVmInstanceMetadataOnPrimaryStorageReply()); + } + + @Override + public void fail(ErrorCode errorCode) { + completion.fail(errorCode); + } + }); + } + + @Override + void handle(GetVmInstanceMetadataFromPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion) { + GetVmInstanceMetadataCmd cmd = new GetVmInstanceMetadataCmd(); + cmd.metadataPath = msg.getMetadataPath(); + + httpCall(GET_VM_INSTANCE_METADATA_PATH, hostUuid, cmd, GetVmInstanceMetadataRsp.class, new ReturnValueCompletion(completion) { + @Override + public void success(GetVmInstanceMetadataRsp rsp) { + GetVmInstanceMetadataFromPrimaryStorageReply reply = new GetVmInstanceMetadataFromPrimaryStorageReply(); + reply.setMetadata(rsp.metadata); + completion.success(reply); + } + + @Override + public void fail(ErrorCode errorCode) { + completion.fail(errorCode); + } + }); + } + + @Override + void handle(ScanVmInstanceMetadataFromPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion) { + ScanVmMetadataCmd cmd = new ScanVmMetadataCmd(); + cmd.metadataDir = msg.getMetadataDir(); + + httpCall(SCAN_VM_METADATA_PATH, hostUuid, cmd, ScanVmMetadataRsp.class, new ReturnValueCompletion(completion) { + @Override + public void success(ScanVmMetadataRsp rsp) { + ScanVmInstanceMetadataFromPrimaryStorageReply reply = new ScanVmInstanceMetadataFromPrimaryStorageReply(); + if (rsp.metadataEntries != null) { + List entries = rsp.metadataEntries.stream() + .filter(Objects::nonNull) + .peek(entry -> entry.setHostUuid(hostUuid)) + .collect(Collectors.toList()); + reply.setVmInstanceMetadata(entries); + } else { + reply.setVmInstanceMetadata(Collections.emptyList()); + } + completion.success(reply); + } + + @Override + public void fail(ErrorCode errorCode) { + completion.fail(errorCode); + } + }); + } + + @Override + void handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion) { + CleanupVmMetadataCmd cmd = new CleanupVmMetadataCmd(); + cmd.metadataPath = msg.getMetadataPath(); + + httpCall(CLEANUP_VM_METADATA_PATH, hostUuid, cmd, CleanupVmMetadataRsp.class, new ReturnValueCompletion(completion) { + @Override + public void success(CleanupVmMetadataRsp rsp) { + CleanupVmInstanceMetadataOnPrimaryStorageReply reply = new CleanupVmInstanceMetadataOnPrimaryStorageReply(); + completion.success(reply); + } + + @Override + public void fail(ErrorCode errorCode) { + completion.fail(errorCode); + } + }); + } + + @Override + void handle(RebaseVolumeBackingFileOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion) { + PrefixRebaseBackingFilesCmd cmd = new PrefixRebaseBackingFilesCmd(); + cmd.filePaths = msg.getInstallPaths(); + cmd.oldPrefix = msg.getOldPrefix(); + cmd.newPrefix = msg.getNewPrefix(); + + httpCall(PREFIX_REBASE_BACKING_FILES_PATH, hostUuid, cmd, PrefixRebaseBackingFilesRsp.class, new ReturnValueCompletion(completion) { + @Override + public void success(PrefixRebaseBackingFilesRsp rsp) { + RebaseVolumeBackingFileOnPrimaryStorageReply reply = new RebaseVolumeBackingFileOnPrimaryStorageReply(); + reply.setRebasedCount(rsp.rebasedCount); + completion.success(reply); + } + + @Override + public void fail(ErrorCode errorCode) { + completion.fail(errorCode); + } + }); + } } diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java index dbe774888f0..f186aac0dab 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java @@ -334,6 +334,54 @@ String offlineMerge(HttpEntity entity) { return null; } + @RequestMapping(value=LocalStorageKvmBackend.WRITE_VM_METADATA_PATH, method= RequestMethod.POST) + public @ResponseBody + String writeVmMetadata(HttpEntity entity) { + WriteVmMetadataCmd cmd = JSONObjectUtil.toObject(entity.getBody(), WriteVmMetadataCmd.class); + config.writeVmMetadataCmds.add(cmd); + reply(entity, new WriteVmMetadataRsp()); + return null; + } + + @RequestMapping(value=LocalStorageKvmBackend.GET_VM_INSTANCE_METADATA_PATH, method= RequestMethod.POST) + public @ResponseBody + String getVmInstanceMetadata(HttpEntity entity) { + GetVmInstanceMetadataCmd cmd = JSONObjectUtil.toObject(entity.getBody(), GetVmInstanceMetadataCmd.class); + config.getVmInstanceMetadataCmds.add(cmd); + GetVmInstanceMetadataRsp rsp = new GetVmInstanceMetadataRsp(); + reply(entity, rsp); + return null; + } + + @RequestMapping(value=LocalStorageKvmBackend.SCAN_VM_METADATA_PATH, method= RequestMethod.POST) + public @ResponseBody + String scanVmMetadata(HttpEntity entity) { + ScanVmMetadataCmd cmd = JSONObjectUtil.toObject(entity.getBody(), ScanVmMetadataCmd.class); + config.scanVmMetadataCmds.add(cmd); + ScanVmMetadataRsp rsp = new ScanVmMetadataRsp(); + reply(entity, rsp); + return null; + } + + @RequestMapping(value=LocalStorageKvmBackend.CLEANUP_VM_METADATA_PATH, method= RequestMethod.POST) + public @ResponseBody + String cleanupVmMetadata(HttpEntity entity) { + CleanupVmMetadataCmd cmd = JSONObjectUtil.toObject(entity.getBody(), CleanupVmMetadataCmd.class); + config.cleanupVmMetadataCmds.add(cmd); + reply(entity, new CleanupVmMetadataRsp()); + return null; + } + + @RequestMapping(value=LocalStorageKvmBackend.PREFIX_REBASE_BACKING_FILES_PATH, method= RequestMethod.POST) + public @ResponseBody + String prefixRebaseBackingFiles(HttpEntity entity) { + PrefixRebaseBackingFilesCmd cmd = JSONObjectUtil.toObject(entity.getBody(), PrefixRebaseBackingFilesCmd.class); + PrefixRebaseBackingFilesRsp rsp = new PrefixRebaseBackingFilesRsp(); + rsp.rebasedCount = cmd.filePaths == null ? 0 : cmd.filePaths.size(); + reply(entity, rsp); + return null; + } + @ExceptionHandler(Exception.class) public ModelAndView handleAllException(Exception ex) { logger.warn(ex.getMessage(), ex); diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulatorConfig.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulatorConfig.java index 53a02339833..34463b46c6e 100755 --- a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulatorConfig.java +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulatorConfig.java @@ -59,4 +59,9 @@ public static class Capacity { public List getQCOW2ReferenceCmds = new ArrayList<>(); public List getQCOW2ReferenceCmdReference = new ArrayList<>(); + + public List writeVmMetadataCmds = new ArrayList<>(); + public List getVmInstanceMetadataCmds = new ArrayList<>(); + public List scanVmMetadataCmds = new ArrayList<>(); + public List cleanupVmMetadataCmds = new ArrayList<>(); } diff --git a/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageVmMetadataExtension.java b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageVmMetadataExtension.java new file mode 100644 index 00000000000..3f89d4dedff --- /dev/null +++ b/plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageVmMetadataExtension.java @@ -0,0 +1,179 @@ +package org.zstack.storage.primary.local; + +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.db.DatabaseFacade; +import org.zstack.core.db.Q; +import org.zstack.core.db.SQL; +import org.zstack.header.exception.CloudRuntimeException; +import org.zstack.header.storage.primary.PrimaryStorageVO; +import org.zstack.header.storage.primary.PrimaryStorageVO_; +import org.zstack.header.vm.metadata.VmInstanceMetadataConstants; +import org.zstack.header.vm.metadata.VmMetadataPathBuildExtensionPoint; +import org.zstack.header.vm.metadata.VmMetadataPathReplacementExtensionPoint; +import org.zstack.header.vm.metadata.VmMetadataResourcePersistExtensionPoint; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.sql.Timestamp; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) +public class LocalStorageVmMetadataExtension implements VmMetadataPathBuildExtensionPoint, + VmMetadataPathReplacementExtensionPoint, VmMetadataResourcePersistExtensionPoint { + private static final CLogger logger = Utils.getLogger(LocalStorageVmMetadataExtension.class); + + @Autowired + private DatabaseFacade dbf; + + @Override + public String getPrimaryStorageType() { + return LocalStorageConstants.LOCAL_STORAGE_TYPE; + } + + @Override + public String buildMetadataDir(String primaryStorageUuid) { + String url = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.url).eq(PrimaryStorageVO_.uuid, primaryStorageUuid).findValue(); + if (url == null) { + throw new CloudRuntimeException(String.format("cannot find url for primary storage[uuid:%s]", primaryStorageUuid)); + } + return String.format("%s/%s", normalizeBaseDir(url), VmInstanceMetadataConstants.METADATA_DIR_NAME); + } + + @Override + public String buildVmMetadataPath(String primaryStorageUuid, String vmInstanceUuid) { + String url = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.url).eq(PrimaryStorageVO_.uuid, primaryStorageUuid).findValue(); + if (url == null) { + throw new CloudRuntimeException(String.format("cannot find url for primary storage[uuid:%s]", primaryStorageUuid)); + } + return String.format("%s/%s/%s%s", normalizeBaseDir(url), VmInstanceMetadataConstants.METADATA_DIR_NAME, vmInstanceUuid, VmInstanceMetadataConstants.FILE_METADATA_SUFFIX); + } + + @Override + public PathReplacementResult calculatePathReplacements(String targetPsUuid, List allOldPaths) { + if (allOldPaths == null || allOldPaths.isEmpty()) { + PathReplacementResult result = new PathReplacementResult(); + result.setMetadataToCurrentPathMap(Collections.emptyMap()); + return result; + } + + String baseDir = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.url).eq(PrimaryStorageVO_.uuid, targetPsUuid).findValue(); + if (baseDir == null) { + logger.warn(String.format("LocalStorage PS[uuid:%s] has no url, path replacement disabled", targetPsUuid)); + PathReplacementResult result = new PathReplacementResult(); + result.setMetadataToCurrentPathMap(Collections.emptyMap()); + return result; + } + String newPrefix = normalizeBaseDir(baseDir) + "/"; + + // Extract old prefix from the first recognizable path + String oldPrefix = null; + for (String path : allOldPaths) { + oldPrefix = VmInstanceMetadataConstants.extractOldPrefix(path); + if (oldPrefix != null) break; + } + + Map pathMap = new LinkedHashMap<>(); + if (oldPrefix != null) { + for (String oldPath : allOldPaths) { + if (oldPath != null && oldPath.startsWith(oldPrefix)) { + pathMap.put(oldPath, newPrefix + oldPath.substring(oldPrefix.length())); + } + } + } else { + logger.warn(String.format("cannot extract old path prefix from any path for LocalStorage PS[uuid:%s], " + + "path replacement disabled", targetPsUuid)); + } + + PathReplacementResult result = new PathReplacementResult(); + result.setMetadataToCurrentPathMap(pathMap); + result.setOldPrefix(oldPrefix); + result.setNewPrefix(newPrefix); + return result; + } + + @Override + public boolean requireHostForCleanup() { + return true; + } + + @Override + public String validateMetadataPath(String primaryStorageUuid, String path) { + if (path == null) { + return "metadataPath cannot be null"; + } + + // Expected format: /.vmmeta + // e.g. /local_ps/vm-metadata/a1b2c3d4e5f6...vmmeta + final String metadataDir; + try { + metadataDir = buildMetadataDir(primaryStorageUuid); + } catch (CloudRuntimeException e) { + return String.format("cannot resolve metadata dir for primary storage[uuid:%s]: %s", + primaryStorageUuid, e.getMessage()); + } + + String prefix = metadataDir + "/"; + String suffix = VmInstanceMetadataConstants.FILE_METADATA_SUFFIX; + + if (!path.startsWith(prefix) || !path.endsWith(suffix)) { + return String.format("metadataPath[%s] does not match expected format: %s/%s", + path, metadataDir, suffix); + } + + String uuidPart = path.substring(prefix.length(), path.length() - suffix.length()); + if (!uuidPart.matches("[0-9a-fA-F]{32}")) { + return String.format("metadataPath[%s] contains invalid uuid[%s], expected 32-char hex", path, uuidPart); + } + return null; + } + + private String normalizeBaseDir(String url) { + return url.endsWith("/") ? url.substring(0, url.length() - 1) : url; + } + + @Override + public void afterVolumePersist(String primaryStorageUuid, String resourceUuid, + String resourceType, String hostUuid, long size, Timestamp now) { + createResourceRef(primaryStorageUuid, resourceUuid, resourceType, hostUuid, size, now); + } + + @Override + public void afterSnapshotPersist(String primaryStorageUuid, String resourceUuid, + String resourceType, String hostUuid, long size, Timestamp now) { + createResourceRef(primaryStorageUuid, resourceUuid, resourceType, hostUuid, size, now); + } + + private void createResourceRef(String primaryStorageUuid, String resourceUuid, + String resourceType, String hostUuid, long size, Timestamp now) { + boolean exists = Q.New(LocalStorageResourceRefVO.class).eq(LocalStorageResourceRefVO_.resourceUuid, resourceUuid).isExists(); + if (exists) { + logger.debug(String.format("LocalStorageResourceRefVO for resource[uuid:%s] already exists, skip creation", resourceUuid)); + return; + } + + LocalStorageResourceRefVO ref = new LocalStorageResourceRefVO(); + ref.setPrimaryStorageUuid(primaryStorageUuid); + ref.setResourceUuid(resourceUuid); + ref.setResourceType(resourceType); + ref.setHostUuid(hostUuid); + ref.setSize(size); + ref.setCreateDate(now); + ref.setLastOpDate(now); + dbf.persist(ref); + } + + @Override + public void afterRegistrationRollback(List resourceUuids) { + if (resourceUuids == null || resourceUuids.isEmpty()) { + return; + } + SQL.New(LocalStorageResourceRefVO.class) + .in(LocalStorageResourceRefVO_.resourceUuid, resourceUuids) + .hardDelete(); + } +} diff --git a/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java index abe9ac152b6..6b0142e5a63 100755 --- a/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java +++ b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java @@ -36,7 +36,6 @@ import org.zstack.header.storage.backup.BackupStorageVO; import org.zstack.header.storage.primary.*; import org.zstack.header.storage.primary.VolumeSnapshotCapability.VolumeSnapshotArrangementType; -import org.zstack.header.storage.snapshot.DeleteVolumeSnapshotDirection; import org.zstack.header.storage.snapshot.ShrinkVolumeSnapshotOnPrimaryStorageMsg; import org.zstack.header.storage.snapshot.VolumeSnapshotConstant; import org.zstack.header.storage.snapshot.VolumeSnapshotInventory; @@ -44,23 +43,27 @@ import org.zstack.header.vm.VmInstanceState; import org.zstack.header.vm.VmInstanceVO; import org.zstack.header.vm.VmInstanceVO_; +import org.zstack.header.vm.metadata.*; import org.zstack.header.volume.*; -import org.zstack.kvm.*; -import org.zstack.storage.primary.*; +import org.zstack.kvm.KVMConstant; +import org.zstack.storage.primary.EstimateVolumeTemplateSizeOnPrimaryStorageMsg; +import org.zstack.storage.primary.EstimateVolumeTemplateSizeOnPrimaryStorageReply; +import org.zstack.storage.primary.PrimaryStorageBase; +import org.zstack.storage.primary.PrimaryStorageCapacityUpdater; import org.zstack.storage.snapshot.reference.VolumeSnapshotReferenceUtils; import org.zstack.storage.volume.VolumeErrors; import org.zstack.storage.volume.VolumeSystemTags; import org.zstack.tag.SystemTagCreator; -import org.zstack.utils.CollectionUtils; import org.zstack.utils.DebugUtils; import org.zstack.utils.Utils; -import org.zstack.utils.function.Function; import org.zstack.utils.logging.CLogger; import javax.persistence.Tuple; import javax.persistence.TypedQuery; import java.io.File; -import java.util.*; +import java.util.Collection; +import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -131,6 +134,8 @@ protected void handleLocalMessage(Message msg) { handle((CommitVolumeSnapshotOnPrimaryStorageMsg) msg); } else if (msg instanceof PullVolumeSnapshotOnPrimaryStorageMsg) { handle((PullVolumeSnapshotOnPrimaryStorageMsg) msg); + } else if (msg instanceof RebaseVolumeBackingFileOnPrimaryStorageMsg) { + handle((RebaseVolumeBackingFileOnPrimaryStorageMsg) msg); } else { super.handleLocalMessage(msg); } @@ -1896,6 +1901,9 @@ public void fail(ErrorCode errorCode) { private String getHostUuidFromVolume(String volumeUuid) { VolumeVO vol = dbf.findByUuid(volumeUuid, VolumeVO.class); + if (vol == null) { + return ""; + } String hostUuid = ""; List connectedHost = factory.getConnectedHostForOperation(getSelfInventory()); @@ -1924,4 +1932,161 @@ private String getHostUuidFromVolume(String volumeUuid) { return hostUuid; } + + @Override + protected void handle(UpdateVmInstanceMetadataOnPrimaryStorageMsg msg) { + thdf.chainSubmit(new ChainTask(msg) { + @Override + public String getSyncSignature() { + return String.format("update-metadata-on-ps-%s", self.getUuid()); + } + + @Override + public int getSyncLevel() { + return 10; + } + + @Override + public void run(SyncTaskChain chain) { + UpdateVmInstanceMetadataOnPrimaryStorageReply reply = new UpdateVmInstanceMetadataOnPrimaryStorageReply(); + + String hostUuid = getHostUuidFromVolume(msg.getRootVolumeUuid()); + if (hostUuid == null || hostUuid.isEmpty()) { + reply.setError(operr("no host found for volume[uuid:%s]", msg.getRootVolumeUuid())); + bus.reply(msg, reply); + chain.next(); + return; + } + + final NfsPrimaryStorageBackend backend = getBackendByHostUuid(hostUuid); + backend.handle(msg, hostUuid, new ReturnValueCompletion(msg, chain) { + @Override + public void success(UpdateVmInstanceMetadataOnPrimaryStorageReply r) { + bus.reply(msg, r); + chain.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + reply.setError(errorCode); + bus.reply(msg, reply); + chain.next(); + } + }); + } + + @Override + public String getName() { + return String.format("update-metadata-on-ps-%s", self.getUuid()); + } + }); + } + + @Override + protected void handle(GetVmInstanceMetadataFromPrimaryStorageMsg msg) { + GetVmInstanceMetadataFromPrimaryStorageReply reply = new GetVmInstanceMetadataFromPrimaryStorageReply(); + List connectedHosts = factory.getConnectedHostForOperation(getSelfInventory()); + if (connectedHosts.isEmpty()) { + reply.setError(operr("no connected host found for NFS primary storage[uuid:%s]", self.getUuid())); + bus.reply(msg, reply); + return; + } + String hostUuid = msg.getHostUuid(); + String finalHostUuid = hostUuid; + if (hostUuid == null || !connectedHosts.stream().anyMatch(h -> h.getUuid().equals(finalHostUuid))) { + hostUuid = connectedHosts.get(0).getUuid(); + } + final NfsPrimaryStorageBackend backend = getBackendByHostUuid(hostUuid); + backend.handle(msg, hostUuid, new ReturnValueCompletion(msg) { + @Override + public void success(GetVmInstanceMetadataFromPrimaryStorageReply r) { + bus.reply(msg, r); + } + + @Override + public void fail(ErrorCode errorCode) { + reply.setError(errorCode); + bus.reply(msg, reply); + } + }); + } + + @Override + protected void handle(ScanVmInstanceMetadataFromPrimaryStorageMsg msg) { + ScanVmInstanceMetadataFromPrimaryStorageReply reply = new ScanVmInstanceMetadataFromPrimaryStorageReply(); + List connectedHosts = factory.getConnectedHostForOperation(getSelfInventory()); + if (connectedHosts.isEmpty()) { + reply.setError(operr("no connected host found for NFS primary storage[uuid:%s]", self.getUuid())); + bus.reply(msg, reply); + return; + } + String hostUuid = connectedHosts.get(0).getUuid(); + final NfsPrimaryStorageBackend backend = getBackendByHostUuid(hostUuid); + backend.handle(msg, hostUuid, new ReturnValueCompletion(msg) { + @Override + public void success(ScanVmInstanceMetadataFromPrimaryStorageReply r) { + bus.reply(msg, r); + } + + @Override + public void fail(ErrorCode errorCode) { + reply.setError(errorCode); + bus.reply(msg, reply); + } + }); + } + + @Override + protected void handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg msg) { + CleanupVmInstanceMetadataOnPrimaryStorageReply reply = new CleanupVmInstanceMetadataOnPrimaryStorageReply(); + List connectedHosts = factory.getConnectedHostForOperation(getSelfInventory()); + if (connectedHosts.isEmpty()) { + reply.setError(operr("no connected host found for NFS primary storage[uuid:%s]", self.getUuid())); + bus.reply(msg, reply); + return; + } + String hostUuid = msg.getHostUuid(); + String finalHostUuid = hostUuid; + if (hostUuid == null || !connectedHosts.stream().anyMatch(h -> h.getUuid().equals(finalHostUuid))) { + hostUuid = connectedHosts.get(0).getUuid(); + } + final NfsPrimaryStorageBackend backend = getBackendByHostUuid(hostUuid); + backend.handle(msg, hostUuid, new ReturnValueCompletion(msg) { + @Override + public void success(CleanupVmInstanceMetadataOnPrimaryStorageReply r) { + bus.reply(msg, r); + } + + @Override + public void fail(ErrorCode errorCode) { + reply.setError(errorCode); + bus.reply(msg, reply); + } + }); + } + + @Override + protected void handle(RebaseVolumeBackingFileOnPrimaryStorageMsg msg) { + RebaseVolumeBackingFileOnPrimaryStorageReply reply = new RebaseVolumeBackingFileOnPrimaryStorageReply(); + List connectedHosts = factory.getConnectedHostForOperation(getSelfInventory()); + if (connectedHosts.isEmpty()) { + reply.setError(operr("no connected host found for NFS primary storage[uuid:%s]", self.getUuid())); + bus.reply(msg, reply); + return; + } + String hostUuid = connectedHosts.get(0).getUuid(); + final NfsPrimaryStorageBackend backend = getBackendByHostUuid(hostUuid); + backend.handle(msg, hostUuid, new ReturnValueCompletion(msg) { + @Override + public void success(RebaseVolumeBackingFileOnPrimaryStorageReply r) { + bus.reply(msg, r); + } + + @Override + public void fail(ErrorCode errorCode) { + reply.setError(errorCode); + bus.reply(msg, reply); + } + }); + } } diff --git a/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageBackend.java b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageBackend.java index 459023d7c17..35a240a6221 100755 --- a/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageBackend.java +++ b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageBackend.java @@ -7,6 +7,8 @@ import org.zstack.header.image.ImageInventory; import org.zstack.header.storage.primary.*; import org.zstack.header.storage.snapshot.VolumeSnapshotInventory; +import org.zstack.header.vm.metadata.UpdateVmInstanceMetadataOnPrimaryStorageMsg; +import org.zstack.header.vm.metadata.UpdateVmInstanceMetadataOnPrimaryStorageReply; import org.zstack.header.volume.VolumeStats; import org.zstack.header.volume.BatchSyncVolumeSizeOnPrimaryStorageMsg; import org.zstack.header.volume.BatchSyncVolumeSizeOnPrimaryStorageReply; @@ -91,6 +93,16 @@ public interface NfsPrimaryStorageBackend { void updateMountPoint(PrimaryStorageInventory pinv, String clusterUuid, String oldMountPoint, String newMountPoint, Completion completion); + void handle(UpdateVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion); + + void handle(GetVmInstanceMetadataFromPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion); + + void handle(ScanVmInstanceMetadataFromPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion); + + void handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion); + + void handle(RebaseVolumeBackingFileOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion); + class BitsInfo { private String installPath; private long size; 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 46e6986abd2..445497e0871 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 @@ -21,7 +21,9 @@ import org.zstack.core.timeout.ApiTimeoutManager; import org.zstack.core.trash.StorageTrash; import org.zstack.header.core.*; -import org.zstack.header.core.workflow.*; +import org.zstack.header.core.workflow.Flow; +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.errorcode.OperationFailureException; @@ -39,6 +41,8 @@ import org.zstack.header.vm.VmInstanceState; import org.zstack.header.vm.VmInstanceVO; import org.zstack.header.vm.VmInstanceVO_; +import org.zstack.header.vm.metadata.UpdateVmInstanceMetadataOnPrimaryStorageMsg; +import org.zstack.header.vm.metadata.UpdateVmInstanceMetadataOnPrimaryStorageReply; import org.zstack.header.volume.*; import org.zstack.identity.AccountManager; import org.zstack.kvm.*; @@ -129,7 +133,12 @@ public class NfsPrimaryStorageKVMBackend implements NfsPrimaryStorageBackend, public static final String GET_DOWNLOAD_BITS_FROM_KVM_HOST_PROGRESS_PATH = "/nfsprimarystorage/kvmhost/download/progress"; public static final String CREATE_VOLUME_FROM_TEMPLATE_PATH = "/nfsprimarystorage/sftp/createvolumefromtemplate"; public static final String GET_QCOW2_HASH_VALUE_PATH = "/nfsprimarystorage/getqcow2hash"; + public static final String WRITE_VM_METADATA_PATH = "/nfsprimarystorage/vm/metadata/write"; + public static final String GET_VM_INSTANCE_METADATA_PATH = "/nfsprimarystorage/vm/metadata/get"; + public static final String SCAN_VM_METADATA_PATH = "/nfsprimarystorage/vm/metadata/scan"; + public static final String CLEANUP_VM_METADATA_PATH = "/nfsprimarystorage/vm/metadata/cleanup"; + public static final String NFS_PREFIX_REBASE_BACKING_FILES_PATH = "/nfsprimarystorage/snapshot/prefixrebasebackingfiles"; //////////////// For unit test ////////////////////////// private boolean syncGetCapacity = false; @@ -2051,4 +2060,174 @@ public void run(MessageReply r) { } }); } + + @Override + public void handle(UpdateVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion) { + WriteVmMetadataCmd cmd = new WriteVmMetadataCmd(); + cmd.setUuid(msg.getPrimaryStorageUuid()); + cmd.metadata = msg.getMetadata(); + cmd.metadataPath = msg.getMetadataPath(); + cmd.vmUuid = msg.getVmInstanceUuid(); + cmd.vmName = msg.getVmInstanceName(); + cmd.vmCategory = msg.getVmCategory(); + cmd.architecture = msg.getArchitecture(); + cmd.schemaVersion = msg.getSchemaVersion(); + + KVMHostAsyncHttpCallMsg hmsg = new KVMHostAsyncHttpCallMsg(); + hmsg.setCommand(cmd); + hmsg.setPath(WRITE_VM_METADATA_PATH); + hmsg.setHostUuid(hostUuid); + bus.makeTargetServiceIdByResourceUuid(hmsg, HostConstant.SERVICE_ID, hostUuid); + bus.send(hmsg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + completion.fail(reply.getError()); + return; + } + + WriteVmMetadataRsp rsp = ((KVMHostAsyncHttpCallReply) reply).toResponse(WriteVmMetadataRsp.class); + if (!rsp.isSuccess()) { + completion.fail(operr("failed to write vm metadata on nfs via host[uuid:%s]: %s", hostUuid, rsp.getError())); + return; + } + + completion.success(new UpdateVmInstanceMetadataOnPrimaryStorageReply()); + } + }); + } + + @Override + public void handle(GetVmInstanceMetadataFromPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion) { + NfsPrimaryStorageKVMBackendCommands.GetVmInstanceMetadataCmd cmd = new NfsPrimaryStorageKVMBackendCommands.GetVmInstanceMetadataCmd(); + cmd.setUuid(msg.getPrimaryStorageUuid()); + cmd.metadataPath = msg.getMetadataPath(); + + KVMHostAsyncHttpCallMsg hmsg = new KVMHostAsyncHttpCallMsg(); + hmsg.setCommand(cmd); + hmsg.setPath(GET_VM_INSTANCE_METADATA_PATH); + hmsg.setHostUuid(hostUuid); + bus.makeTargetServiceIdByResourceUuid(hmsg, HostConstant.SERVICE_ID, hostUuid); + bus.send(hmsg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + completion.fail(reply.getError()); + return; + } + + GetVmInstanceMetadataRsp rsp = ((KVMHostAsyncHttpCallReply) reply).toResponse(GetVmInstanceMetadataRsp.class); + if (!rsp.isSuccess()) { + completion.fail(operr("failed to get vm instance metadata on nfs via host[uuid:%s]: %s", hostUuid, rsp.getError())); + return; + } + + GetVmInstanceMetadataFromPrimaryStorageReply r = new GetVmInstanceMetadataFromPrimaryStorageReply(); + r.setMetadata(rsp.metadata); + completion.success(r); + } + }); + } + + @Override + public void handle(ScanVmInstanceMetadataFromPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion) { + NfsPrimaryStorageKVMBackendCommands.ScanVmMetadataCmd cmd = new NfsPrimaryStorageKVMBackendCommands.ScanVmMetadataCmd(); + cmd.setUuid(msg.getPrimaryStorageUuid()); + cmd.metadataDir = msg.getMetadataDir(); + + KVMHostAsyncHttpCallMsg hmsg = new KVMHostAsyncHttpCallMsg(); + hmsg.setCommand(cmd); + hmsg.setPath(SCAN_VM_METADATA_PATH); + hmsg.setHostUuid(hostUuid); + bus.makeTargetServiceIdByResourceUuid(hmsg, HostConstant.SERVICE_ID, hostUuid); + bus.send(hmsg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + completion.fail(reply.getError()); + return; + } + + NfsPrimaryStorageKVMBackendCommands.ScanVmMetadataRsp rsp = ((KVMHostAsyncHttpCallReply) reply).toResponse(NfsPrimaryStorageKVMBackendCommands.ScanVmMetadataRsp.class); + if (!rsp.isSuccess()) { + completion.fail(operr("failed to scan vm instance metadata on nfs via host[uuid:%s]: %s", hostUuid, rsp.getError())); + return; + } + + ScanVmInstanceMetadataFromPrimaryStorageReply r = new ScanVmInstanceMetadataFromPrimaryStorageReply(); + if (rsp.metadataEntries != null) { + r.setVmInstanceMetadata(rsp.metadataEntries.stream().filter(Objects::nonNull).collect(Collectors.toList())); + } else { + r.setVmInstanceMetadata(Collections.emptyList()); + } + completion.success(r); + } + }); + } + + @Override + public void handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion) { + CleanupVmMetadataCmd cmd = new CleanupVmMetadataCmd(); + cmd.setUuid(msg.getPrimaryStorageUuid()); + cmd.metadataPath = msg.getMetadataPath(); + + KVMHostAsyncHttpCallMsg hmsg = new KVMHostAsyncHttpCallMsg(); + hmsg.setCommand(cmd); + hmsg.setPath(CLEANUP_VM_METADATA_PATH); + hmsg.setHostUuid(hostUuid); + bus.makeTargetServiceIdByResourceUuid(hmsg, HostConstant.SERVICE_ID, hostUuid); + bus.send(hmsg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + completion.fail(reply.getError()); + return; + } + + CleanupVmMetadataRsp rsp = ((KVMHostAsyncHttpCallReply) reply).toResponse(CleanupVmMetadataRsp.class); + if (!rsp.isSuccess()) { + completion.fail(operr("failed to cleanup vm metadata on nfs via host[uuid:%s]: %s", hostUuid, rsp.getError())); + return; + } + + CleanupVmInstanceMetadataOnPrimaryStorageReply r = new CleanupVmInstanceMetadataOnPrimaryStorageReply(); + completion.success(r); + } + }); + } + + @Override + public void handle(RebaseVolumeBackingFileOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion completion) { + NfsPrimaryStorageKVMBackendCommands.PrefixRebaseBackingFilesCmd cmd = new NfsPrimaryStorageKVMBackendCommands.PrefixRebaseBackingFilesCmd(); + cmd.filePaths = msg.getInstallPaths(); + cmd.oldPrefix = msg.getOldPrefix(); + cmd.newPrefix = msg.getNewPrefix(); + cmd.setUuid(msg.getPrimaryStorageUuid()); + + KVMHostAsyncHttpCallMsg hmsg = new KVMHostAsyncHttpCallMsg(); + hmsg.setCommand(cmd); + hmsg.setPath(NFS_PREFIX_REBASE_BACKING_FILES_PATH); + hmsg.setHostUuid(hostUuid); + bus.makeTargetServiceIdByResourceUuid(hmsg, HostConstant.SERVICE_ID, hostUuid); + bus.send(hmsg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + completion.fail(reply.getError()); + return; + } + + NfsPrimaryStorageKVMBackendCommands.PrefixRebaseBackingFilesRsp rsp = + ((KVMHostAsyncHttpCallReply) reply).toResponse(NfsPrimaryStorageKVMBackendCommands.PrefixRebaseBackingFilesRsp.class); + if (!rsp.isSuccess()) { + completion.fail(operr("failed to prefix rebase backing files on nfs via host[uuid:%s]: %s", hostUuid, rsp.getError())); + return; + } + + RebaseVolumeBackingFileOnPrimaryStorageReply r = new RebaseVolumeBackingFileOnPrimaryStorageReply(); + r.setRebasedCount(rsp.rebasedCount); + completion.success(r); + } + }); + } } 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 a105552fdd9..0901af4cadb 100755 --- a/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackendCommands.java +++ b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackendCommands.java @@ -3,6 +3,7 @@ import org.zstack.header.HasThreadContext; import org.zstack.header.core.validation.Validation; import org.zstack.header.volume.VolumeConstant; +import org.zstack.header.storage.primary.VmMetadataScanEntry; import org.zstack.kvm.KVMAgentCommands; import org.zstack.kvm.KVMAgentCommands.AgentCommand; import org.zstack.kvm.KVMAgentCommands.AgentResponse; @@ -956,4 +957,51 @@ public void setHashValue(String hashValue) { this.hashValue = hashValue; } } + + public static class WriteVmMetadataCmd extends NfsPrimaryStorageAgentCommand { + public String metadata; + public String metadataPath; + public String vmUuid; + public String vmName; + public String vmCategory; + public String architecture; + public String schemaVersion; + } + + public static class WriteVmMetadataRsp extends NfsPrimaryStorageAgentResponse { + } + + + public static class GetVmInstanceMetadataCmd extends NfsPrimaryStorageAgentCommand { + public String metadataPath; + } + + public static class GetVmInstanceMetadataRsp extends NfsPrimaryStorageAgentResponse { + public String metadata; + } + + public static class ScanVmMetadataCmd extends NfsPrimaryStorageAgentCommand { + public String metadataDir; + } + + public static class ScanVmMetadataRsp extends NfsPrimaryStorageAgentResponse { + public List metadataEntries = new ArrayList<>(); + } + + public static class CleanupVmMetadataCmd extends NfsPrimaryStorageAgentCommand { + public String metadataPath; + } + + public static class CleanupVmMetadataRsp extends NfsPrimaryStorageAgentResponse { + } + + public static class PrefixRebaseBackingFilesCmd extends NfsPrimaryStorageAgentCommand { + public List filePaths; + public String oldPrefix; + public String newPrefix; + } + + public static class PrefixRebaseBackingFilesRsp extends NfsPrimaryStorageAgentResponse { + public int rebasedCount; + } } diff --git a/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsVmMetadataExtension.java b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsVmMetadataExtension.java new file mode 100644 index 00000000000..db66e11dfe3 --- /dev/null +++ b/plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsVmMetadataExtension.java @@ -0,0 +1,125 @@ +package org.zstack.storage.primary.nfs; + +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.factory.annotation.Configurable; +import org.zstack.core.db.Q; +import org.zstack.header.exception.CloudRuntimeException; +import org.zstack.header.storage.primary.PrimaryStorageVO; +import org.zstack.header.storage.primary.PrimaryStorageVO_; +import org.zstack.header.vm.metadata.VmInstanceMetadataConstants; +import org.zstack.header.vm.metadata.VmMetadataPathBuildExtensionPoint; +import org.zstack.header.vm.metadata.VmMetadataPathReplacementExtensionPoint; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) +public class NfsVmMetadataExtension implements VmMetadataPathBuildExtensionPoint, VmMetadataPathReplacementExtensionPoint { + private static final CLogger logger = Utils.getLogger(NfsVmMetadataExtension.class); + + @Override + public String getPrimaryStorageType() { + return NfsPrimaryStorageConstant.NFS_PRIMARY_STORAGE_TYPE; + } + + private String resolveBaseDir(String primaryStorageUuid) { + // mountPath is the host-local mount point (e.g. /mnt/pss); + // url is the remote NFS export (e.g. 192.168.1.1:/nfs/share) and must NOT be used + // as a file-system path — it would generate paths the agent cannot access. + String baseDir = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.mountPath).eq(PrimaryStorageVO_.uuid, primaryStorageUuid).findValue(); + if (baseDir == null) { + throw new CloudRuntimeException(String.format("cannot find mountPath for NFS primary storage[uuid:%s]", primaryStorageUuid)); + } + return baseDir.endsWith("/") ? baseDir.substring(0, baseDir.length() - 1) : baseDir; + } + + @Override + public String buildMetadataDir(String primaryStorageUuid) { + String baseDir = resolveBaseDir(primaryStorageUuid); + return String.format("%s/%s", baseDir, VmInstanceMetadataConstants.METADATA_DIR_NAME); + } + + @Override + public String buildVmMetadataPath(String primaryStorageUuid, String vmInstanceUuid) { + String baseDir = resolveBaseDir(primaryStorageUuid); + return String.format("%s/%s/%s%s", baseDir, VmInstanceMetadataConstants.METADATA_DIR_NAME, vmInstanceUuid, VmInstanceMetadataConstants.FILE_METADATA_SUFFIX); + } + + @Override + public String validateMetadataPath(String primaryStorageUuid, String path) { + if (path == null) { + return "metadataPath cannot be null"; + } + + // Expected format: /.vmmeta + // e.g. /mnt/pss/vm-metadata/a1b2c3d4e5f6...vmmeta + final String metadataDir; + try { + metadataDir = buildMetadataDir(primaryStorageUuid); + } catch (CloudRuntimeException e) { + return String.format("cannot resolve metadata dir for primary storage[uuid:%s]: %s", + primaryStorageUuid, e.getMessage()); + } + + String prefix = metadataDir + "/"; + String suffix = VmInstanceMetadataConstants.FILE_METADATA_SUFFIX; + + if (!path.startsWith(prefix) || !path.endsWith(suffix)) { + return String.format("metadataPath[%s] does not match expected format: %s/%s", + path, metadataDir, suffix); + } + + String uuidPart = path.substring(prefix.length(), path.length() - suffix.length()); + if (!uuidPart.matches("[0-9a-fA-F]{32}")) { + return String.format("metadataPath[%s] contains invalid uuid[%s], expected 32-char hex", path, uuidPart); + } + return null; + } + + @Override + public PathReplacementResult calculatePathReplacements(String targetPsUuid, List allOldPaths) { + if (allOldPaths == null || allOldPaths.isEmpty()) { + PathReplacementResult result = new PathReplacementResult(); + result.setMetadataToCurrentPathMap(Collections.emptyMap()); + return result; + } + + String baseDir = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.mountPath).eq(PrimaryStorageVO_.uuid, targetPsUuid).findValue(); + if (baseDir == null) { + logger.warn(String.format("NFS PS[uuid:%s] has no mountPath, path replacement disabled", targetPsUuid)); + PathReplacementResult result = new PathReplacementResult(); + result.setMetadataToCurrentPathMap(Collections.emptyMap()); + return result; + } + String newPrefix = baseDir.endsWith("/") ? baseDir : baseDir + "/"; + + // Extract old prefix from the first recognizable path + String oldPrefix = null; + for (String path : allOldPaths) { + oldPrefix = VmInstanceMetadataConstants.extractOldPrefix(path); + if (oldPrefix != null) break; + } + + Map pathMap = new LinkedHashMap<>(); + if (oldPrefix != null) { + for (String oldPath : allOldPaths) { + if (oldPath != null && oldPath.startsWith(oldPrefix)) { + pathMap.put(oldPath, newPrefix + oldPath.substring(oldPrefix.length())); + } + } + } else { + logger.warn(String.format("cannot extract old path prefix from any path for NFS PS[uuid:%s], " + + "path replacement disabled", targetPsUuid)); + } + + PathReplacementResult result = new PathReplacementResult(); + result.setMetadataToCurrentPathMap(pathMap); + result.setOldPrefix(oldPrefix); + result.setNewPrefix(newPrefix); + return result; + } +} From 2d3633c5fd4cc0ace3d95578e589719c6becd42b Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 3 Apr 2026 10:21:40 +0800 Subject: [PATCH 104/129] [log]: add LongText type for logging long content safely Add LongText type to NoLogging annotation to handle long content like base64-encoded image data in logs. When content length >= 20 characters, only show first 10 and last 10 characters with ... in between to improve log readability. Applied to: - APITakeVmConsoleScreenshotEvent.imageData - TakeVmConsoleScreenshotReply.imageData - KVMAgentCommands.VmHostFileTO.contentBase64 - KVMAgentCommands.TakeVmConsoleScreenshotRsp.imageData Related: ZSV-11310 Resolves: ZSV-11443 Change-Id: I747a68626a717563727373767975647863786977 --- core/src/main/java/org/zstack/core/log/LogSafeGson.java | 4 ++++ header/src/main/java/org/zstack/header/log/NoLogging.java | 8 +++++++- .../zstack/header/vm/APITakeVmConsoleScreenshotEvent.java | 2 ++ .../zstack/header/vm/TakeVmConsoleScreenshotReply.java | 2 ++ .../src/main/java/org/zstack/kvm/KVMAgentCommands.java | 3 ++- 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/zstack/core/log/LogSafeGson.java b/core/src/main/java/org/zstack/core/log/LogSafeGson.java index c3dcd26626c..8f0a1197821 100644 --- a/core/src/main/java/org/zstack/core/log/LogSafeGson.java +++ b/core/src/main/java/org/zstack/core/log/LogSafeGson.java @@ -99,6 +99,10 @@ String getMaskedValue(String raw) { return "*****"; } else if (annotation.type().tag()) { return tagInfoHider.call(raw); + } else if (annotation.type().longText()) { + if (raw == null) + return null; + return (raw.length() < 20) ? raw : raw.substring(0, 10) + "..." + raw.substring(raw.length() - 10); } else { return Utils.getLogMaskWords().getOrDefault(raw, uriPattern.matcher(raw).replaceFirst(":*****@")); } diff --git a/header/src/main/java/org/zstack/header/log/NoLogging.java b/header/src/main/java/org/zstack/header/log/NoLogging.java index b53c90f6333..5454acc849d 100644 --- a/header/src/main/java/org/zstack/header/log/NoLogging.java +++ b/header/src/main/java/org/zstack/header/log/NoLogging.java @@ -26,7 +26,9 @@ public boolean mask() { enum Type { Simple, Tag, - Uri; + Uri, + LongText, + ; public boolean simple() { return this == Simple; @@ -39,6 +41,10 @@ public boolean tag() { public boolean uri() { return this == Uri; } + + public boolean longText() { + return this == LongText; + } } Behavior behavior() default Behavior.Mask; diff --git a/header/src/main/java/org/zstack/header/vm/APITakeVmConsoleScreenshotEvent.java b/header/src/main/java/org/zstack/header/vm/APITakeVmConsoleScreenshotEvent.java index b03cad02a3f..a7ee8d9da57 100644 --- a/header/src/main/java/org/zstack/header/vm/APITakeVmConsoleScreenshotEvent.java +++ b/header/src/main/java/org/zstack/header/vm/APITakeVmConsoleScreenshotEvent.java @@ -1,5 +1,6 @@ package org.zstack.header.vm; +import org.zstack.header.log.NoLogging; import org.zstack.header.message.APIEvent; import org.zstack.header.rest.RestResponse; @@ -9,6 +10,7 @@ */ @RestResponse(fieldsTo = {"imageData"}) public class APITakeVmConsoleScreenshotEvent extends APIEvent { + @NoLogging(type = NoLogging.Type.LongText) private String imageData; public APITakeVmConsoleScreenshotEvent() { diff --git a/header/src/main/java/org/zstack/header/vm/TakeVmConsoleScreenshotReply.java b/header/src/main/java/org/zstack/header/vm/TakeVmConsoleScreenshotReply.java index 69ad7def25f..b66ceb3e96e 100644 --- a/header/src/main/java/org/zstack/header/vm/TakeVmConsoleScreenshotReply.java +++ b/header/src/main/java/org/zstack/header/vm/TakeVmConsoleScreenshotReply.java @@ -1,5 +1,6 @@ package org.zstack.header.vm; +import org.zstack.header.log.NoLogging; import org.zstack.header.message.MessageReply; /** @@ -7,6 +8,7 @@ * @date 2023-09-11 */ public class TakeVmConsoleScreenshotReply extends MessageReply { + @NoLogging(type = NoLogging.Type.LongText) private String imageData; public String getImageData() { 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 0f02858071d..6ffc636a090 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -2844,7 +2844,7 @@ public static class VmHostFileTO { /** * null if operation is Prepare or Delete */ - @NoLogging + @NoLogging(type = NoLogging.Type.LongText) private String contentBase64; private String error; @@ -4760,6 +4760,7 @@ public void setVmUuid(String vmUuid) { } public static class TakeVmConsoleScreenshotRsp extends AgentResponse { + @NoLogging(type = NoLogging.Type.LongText) private String imageData; public String getImageData() { From ab1b9a428cdf22385425169917aa806f72d41bb8 Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Thu, 2 Apr 2026 17:59:37 +0800 Subject: [PATCH 105/129] [vm]: add metadata SDK actions and testlib Resolves: ZSV-11559 Change-Id: Ie71ae4242af1031164e34d3f55bf8d379a9f74ec --- .../compute/vm/VmExpungeMetadataFlow.java | 5 + conf/db/zsv/V5.0.0__schema.sql | 1 + .../header/vm/APIDestroyVmInstanceMsg.java | 2 +- .../org/zstack/header/vm/APIMigrateVmMsg.java | 2 +- sdk/src/main/java/SourceClassMap.java | 2 + .../sdk/CleanupVmInstanceMetadataAction.java | 101 +++++++++++++ .../sdk/CleanupVmInstanceMetadataResult.java | 30 ++++ ...tanceMetadataFromPrimaryStorageAction.java | 101 +++++++++++++ ...tanceMetadataFromPrimaryStorageResult.java | 14 ++ .../RegisterVmInstanceFromMetadataAction.java | 116 +++++++++++++++ .../RegisterVmInstanceFromMetadataResult.java | 14 ++ ...tanceMetadataFromPrimaryStorageAction.java | 101 +++++++++++++ ...tanceMetadataFromPrimaryStorageResult.java | 14 ++ .../sdk/UpdateVmInstanceMetadataAction.java | 101 +++++++++++++ .../sdk/UpdateVmInstanceMetadataResult.java | 7 + .../org/zstack/sdk/VmMetadataScanEntry.java | 87 +++++++++++ .../java/org/zstack/testlib/ApiHelper.groovy | 135 ++++++++++++++++++ .../zstack/testlib/LocalStorageSpec.groovy | 20 +++ .../testlib/NfsPrimaryStorageSpec.groovy | 20 +++ 19 files changed, 871 insertions(+), 2 deletions(-) create mode 100644 sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataAction.java create mode 100644 sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataResult.java create mode 100644 sdk/src/main/java/org/zstack/sdk/VmMetadataScanEntry.java diff --git a/compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java b/compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java index 00daedea93f..8baf8b5ae86 100644 --- a/compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java @@ -35,6 +35,11 @@ public class VmExpungeMetadataFlow extends NoRollbackFlow { @Override public void run(FlowTrigger trigger, Map data) { + if (!VmGlobalConfig.VM_METADATA_ENABLED.value(Boolean.class)) { + trigger.next(); + return; + } + final VmInstanceSpec spec = (VmInstanceSpec) data.get(VmInstanceConstant.Params.VmInstanceSpec.toString()); if (spec == null || spec.getVmInventory() == null) { logger.warn("[MetadataExpunge] missing VmInstanceSpec or VmInventory, skip metadata cleanup"); diff --git a/conf/db/zsv/V5.0.0__schema.sql b/conf/db/zsv/V5.0.0__schema.sql index 1dd6aeee2a9..6e7ffc1f37c 100644 --- a/conf/db/zsv/V5.0.0__schema.sql +++ b/conf/db/zsv/V5.0.0__schema.sql @@ -139,6 +139,7 @@ CREATE TABLE IF NOT EXISTS `zstack`.`VmMetadataDirtyVO` ( `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59', PRIMARY KEY (`vmInstanceUuid`), + INDEX `idx_VmMetadataDirtyVO_unclaimed` (`managementNodeUuid`, `nextRetryTime`, `lastOpDate`), CONSTRAINT `fkVmMetadataDirtyVOVmInstanceEO` FOREIGN KEY (`vmInstanceUuid`) REFERENCES `VmInstanceEO` (`uuid`) ON DELETE CASCADE, CONSTRAINT `fkVmMetadataDirtyVOManagementNodeVO` FOREIGN KEY (`managementNodeUuid`) REFERENCES `ManagementNodeVO` (`uuid`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java b/header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java index 0f02d85c6f0..9fa37d94788 100755 --- a/header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java @@ -42,7 +42,7 @@ method = HttpMethod.DELETE, responseClass = APIDestroyVmInstanceEvent.class ) -@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "VmUuidDirectResolver", field = "uuid", updateOnFailure = true) +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "uuid") public class APIDestroyVmInstanceMsg extends APIDeleteMessage implements VmInstanceMessage { /** * @desc vm uuid diff --git a/header/src/main/java/org/zstack/header/vm/APIMigrateVmMsg.java b/header/src/main/java/org/zstack/header/vm/APIMigrateVmMsg.java index 1fa0f34f2f5..76e400e7dd7 100755 --- a/header/src/main/java/org/zstack/header/vm/APIMigrateVmMsg.java +++ b/header/src/main/java/org/zstack/header/vm/APIMigrateVmMsg.java @@ -45,7 +45,7 @@ ) @SkipVmTracer(replyClass = APIMigrateVmEvent.class) @DefaultTimeout(timeunit = TimeUnit.HOURS, value = 1) -@MetadataImpact(value = MetadataImpact.Impact.STORAGE, resolver = "VmUuidDirectResolver", field = "vmInstanceUuid") +@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "VmUuidDirectResolver", field = "vmInstanceUuid") public class APIMigrateVmMsg extends APIMessage implements VmInstanceMessage, MigrateVmMessage, CheckAttachedVolumesMessage { /** * @desc vm uuid diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 232283f4d80..a1ea03d9432 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -255,6 +255,7 @@ public class SourceClassMap { 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.primary.VmMetadataScanEntry", "org.zstack.sdk.VmMetadataScanEntry"); put("org.zstack.header.storage.snapshot.BatchDeleteVolumeSnapshotStruct", "org.zstack.sdk.BatchDeleteVolumeSnapshotStruct"); put("org.zstack.header.storage.snapshot.ShrinkResult", "org.zstack.sdk.ShrinkResult"); put("org.zstack.header.storage.snapshot.VolumeSnapshotBackupStorageRefInventory", "org.zstack.sdk.VolumeSnapshotBackupStorageRefInventory"); @@ -1184,6 +1185,7 @@ public class SourceClassMap { put("org.zstack.sdk.VmInstanceResourceMetadataGroupInventory", "org.zstack.header.vm.devices.VmInstanceResourceMetadataGroupInventory"); put("org.zstack.sdk.VmMemoryBillingInventory", "org.zstack.billing.generator.vm.memory.VmMemoryBillingInventory"); put("org.zstack.sdk.VmMemorySpendingDetails", "org.zstack.billing.spendingcalculator.vm.VmMemorySpendingDetails"); + put("org.zstack.sdk.VmMetadataScanEntry", "org.zstack.header.storage.primary.VmMetadataScanEntry"); put("org.zstack.sdk.VmNicBandwidthSpendingDetails", "org.zstack.billing.spendingcalculator.vmnic.VmNicBandwidthSpendingDetails"); put("org.zstack.sdk.VmNicConflictEntry", "org.zstack.header.storage.snapshot.group.VmNicConflictEntry"); put("org.zstack.sdk.VmNicInventory", "org.zstack.header.vm.VmNicInventory"); diff --git a/sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataAction.java b/sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataAction.java new file mode 100644 index 00000000000..46586cab3c4 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataAction.java @@ -0,0 +1,101 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class CleanupVmInstanceMetadataAction 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.CleanupVmInstanceMetadataResult 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 = true, nullElements = false, emptyString = true, noTrim = false) + public java.util.List vmUuids; + + @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.CleanupVmInstanceMetadataResult value = res.getResult(org.zstack.sdk.CleanupVmInstanceMetadataResult.class); + ret.value = value == null ? new org.zstack.sdk.CleanupVmInstanceMetadataResult() : 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 = "/vm-instances/metadata/cleanup"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "cleanupVmInstanceMetadata"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataResult.java b/sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataResult.java new file mode 100644 index 00000000000..879a409cc59 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataResult.java @@ -0,0 +1,30 @@ +package org.zstack.sdk; + + + +public class CleanupVmInstanceMetadataResult { + public java.lang.Integer totalCleaned; + public void setTotalCleaned(java.lang.Integer totalCleaned) { + this.totalCleaned = totalCleaned; + } + public java.lang.Integer getTotalCleaned() { + return this.totalCleaned; + } + + public java.lang.Integer totalFailed; + public void setTotalFailed(java.lang.Integer totalFailed) { + this.totalFailed = totalFailed; + } + public java.lang.Integer getTotalFailed() { + return this.totalFailed; + } + + public java.util.List failedVmUuids; + public void setFailedVmUuids(java.util.List failedVmUuids) { + this.failedVmUuids = failedVmUuids; + } + public java.util.List getFailedVmUuids() { + return this.failedVmUuids; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageAction.java b/sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageAction.java new file mode 100644 index 00000000000..a0d62c682a4 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageAction.java @@ -0,0 +1,101 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class GetVmInstanceMetadataFromPrimaryStorageAction 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.GetVmInstanceMetadataFromPrimaryStorageResult 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.GetVmInstanceMetadataFromPrimaryStorageResult value = res.getResult(org.zstack.sdk.GetVmInstanceMetadataFromPrimaryStorageResult.class); + ret.value = value == null ? new org.zstack.sdk.GetVmInstanceMetadataFromPrimaryStorageResult() : 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/vm-instances/metadata"; + info.needSession = true; + info.needPoll = true; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageResult.java b/sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageResult.java new file mode 100644 index 00000000000..d22099fae76 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + + + +public class GetVmInstanceMetadataFromPrimaryStorageResult { + public java.lang.String metadata; + public void setMetadata(java.lang.String metadata) { + this.metadata = metadata; + } + public java.lang.String getMetadata() { + return this.metadata; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataAction.java b/sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataAction.java new file mode 100644 index 00000000000..0c7d43cec6b --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataAction.java @@ -0,0 +1,116 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class RegisterVmInstanceFromMetadataAction 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.RegisterVmInstanceFromMetadataResult 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 = 2048, nonempty = true, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String metadataPath; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String primaryStorageUuid; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String zoneUuid; + + @Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String clusterUuid; + + @Param(required = false, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String hostUuid; + + @Param(required = false, maxLength = 255, nonempty = false, nullElements = false, emptyString = true, noTrim = false) + public java.lang.String name; + + @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.RegisterVmInstanceFromMetadataResult value = res.getResult(org.zstack.sdk.RegisterVmInstanceFromMetadataResult.class); + ret.value = value == null ? new org.zstack.sdk.RegisterVmInstanceFromMetadataResult() : 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 = "/vm-instances/metadata/register"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "params"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataResult.java b/sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataResult.java new file mode 100644 index 00000000000..11634dcf3f1 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + +import org.zstack.sdk.VmInstanceInventory; + +public class RegisterVmInstanceFromMetadataResult { + public VmInstanceInventory inventory; + public void setInventory(VmInstanceInventory inventory) { + this.inventory = inventory; + } + public VmInstanceInventory getInventory() { + return this.inventory; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageAction.java b/sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageAction.java new file mode 100644 index 00000000000..bdb11d011bc --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageAction.java @@ -0,0 +1,101 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class ScanVmInstanceMetadataFromPrimaryStorageAction 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.ScanVmInstanceMetadataFromPrimaryStorageResult 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.ScanVmInstanceMetadataFromPrimaryStorageResult value = res.getResult(org.zstack.sdk.ScanVmInstanceMetadataFromPrimaryStorageResult.class); + ret.value = value == null ? new org.zstack.sdk.ScanVmInstanceMetadataFromPrimaryStorageResult() : 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/vm-instances/metadata/scan"; + info.needSession = true; + info.needPoll = true; + info.parameterName = ""; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageResult.java b/sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageResult.java new file mode 100644 index 00000000000..4abb18ceae6 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageResult.java @@ -0,0 +1,14 @@ +package org.zstack.sdk; + + + +public class ScanVmInstanceMetadataFromPrimaryStorageResult { + public java.util.List vmInstanceMetadata; + public void setVmInstanceMetadata(java.util.List vmInstanceMetadata) { + this.vmInstanceMetadata = vmInstanceMetadata; + } + public java.util.List getVmInstanceMetadata() { + return this.vmInstanceMetadata; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataAction.java b/sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataAction.java new file mode 100644 index 00000000000..2f2620e38e2 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataAction.java @@ -0,0 +1,101 @@ +package org.zstack.sdk; + +import java.util.HashMap; +import java.util.Map; +import org.zstack.sdk.*; + +public class UpdateVmInstanceMetadataAction 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.UpdateVmInstanceMetadataResult 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 = true, nullElements = false, emptyString = true, noTrim = false) + public java.util.List vmUuids; + + @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.UpdateVmInstanceMetadataResult value = res.getResult(org.zstack.sdk.UpdateVmInstanceMetadataResult.class); + ret.value = value == null ? new org.zstack.sdk.UpdateVmInstanceMetadataResult() : 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 = "/vm-instances/metadata/actions"; + info.needSession = true; + info.needPoll = true; + info.parameterName = "updateVmInstanceMetadata"; + return info; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataResult.java b/sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataResult.java new file mode 100644 index 00000000000..d09f1fe7bf7 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataResult.java @@ -0,0 +1,7 @@ +package org.zstack.sdk; + + + +public class UpdateVmInstanceMetadataResult { + +} diff --git a/sdk/src/main/java/org/zstack/sdk/VmMetadataScanEntry.java b/sdk/src/main/java/org/zstack/sdk/VmMetadataScanEntry.java new file mode 100644 index 00000000000..fc5fc13f644 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/VmMetadataScanEntry.java @@ -0,0 +1,87 @@ +package org.zstack.sdk; + + + +public class VmMetadataScanEntry { + + public java.lang.String vmUuid; + public void setVmUuid(java.lang.String vmUuid) { + this.vmUuid = vmUuid; + } + public java.lang.String getVmUuid() { + return this.vmUuid; + } + + public java.lang.String vmName; + public void setVmName(java.lang.String vmName) { + this.vmName = vmName; + } + public java.lang.String getVmName() { + return this.vmName; + } + + public java.lang.String vmCategory; + public void setVmCategory(java.lang.String vmCategory) { + this.vmCategory = vmCategory; + } + public java.lang.String getVmCategory() { + return this.vmCategory; + } + + public java.lang.String architecture; + public void setArchitecture(java.lang.String architecture) { + this.architecture = architecture; + } + public java.lang.String getArchitecture() { + return this.architecture; + } + + public java.lang.String schemaVersion; + public void setSchemaVersion(java.lang.String schemaVersion) { + this.schemaVersion = schemaVersion; + } + public java.lang.String getSchemaVersion() { + return this.schemaVersion; + } + + public java.lang.String metadataPath; + public void setMetadataPath(java.lang.String metadataPath) { + this.metadataPath = metadataPath; + } + public java.lang.String getMetadataPath() { + return this.metadataPath; + } + + public java.lang.String hostUuid; + public void setHostUuid(java.lang.String hostUuid) { + this.hostUuid = hostUuid; + } + public java.lang.String getHostUuid() { + return this.hostUuid; + } + + public long sizeBytes; + public void setSizeBytes(long sizeBytes) { + this.sizeBytes = sizeBytes; + } + public long getSizeBytes() { + return this.sizeBytes; + } + + public long lastUpdateTime; + public void setLastUpdateTime(long lastUpdateTime) { + this.lastUpdateTime = lastUpdateTime; + } + public long getLastUpdateTime() { + return this.lastUpdateTime; + } + + public boolean incomplete; + public void setIncomplete(boolean incomplete) { + this.incomplete = incomplete; + } + public boolean getIncomplete() { + return this.incomplete; + } + +} diff --git a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy index b5e57b47412..17a0473b8e0 100644 --- a/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy +++ b/testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy @@ -5633,6 +5633,33 @@ abstract class ApiHelper { } + def cleanupVmInstanceMetadata(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.CleanupVmInstanceMetadataAction.class) Closure c) { + def a = new org.zstack.sdk.CleanupVmInstanceMetadataAction() + 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 cloneVmInstance(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.CloneVmInstanceAction.class) Closure c) { def a = new org.zstack.sdk.CloneVmInstanceAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -18593,6 +18620,33 @@ abstract class ApiHelper { } + def getVmInstanceMetadataFromPrimaryStorage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.GetVmInstanceMetadataFromPrimaryStorageAction.class) Closure c) { + def a = new org.zstack.sdk.GetVmInstanceMetadataFromPrimaryStorageAction() + 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 getVmInstanceProtectedRecoveryPoints(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.GetVmInstanceProtectedRecoveryPointsAction.class) Closure c) { def a = new org.zstack.sdk.GetVmInstanceProtectedRecoveryPointsAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -27433,6 +27487,33 @@ abstract class ApiHelper { } + def registerVmInstanceFromMetadata(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.RegisterVmInstanceFromMetadataAction.class) Closure c) { + def a = new org.zstack.sdk.RegisterVmInstanceFromMetadataAction() + 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 reimageVmInstance(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.ReimageVmInstanceAction.class) Closure c) { def a = new org.zstack.sdk.ReimageVmInstanceAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -28648,6 +28729,33 @@ abstract class ApiHelper { } + def scanVmInstanceMetadataFromPrimaryStorage(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.ScanVmInstanceMetadataFromPrimaryStorageAction.class) Closure c) { + def a = new org.zstack.sdk.ScanVmInstanceMetadataFromPrimaryStorageAction() + 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 securityMachineDetectSync(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.SecurityMachineDetectSyncAction.class) Closure c) { def a = new org.zstack.sdk.SecurityMachineDetectSyncAction() a.sessionId = Test.currentEnvSpec?.session?.uuid @@ -34264,6 +34372,33 @@ abstract class ApiHelper { } + def updateVmInstanceMetadata(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UpdateVmInstanceMetadataAction.class) Closure c) { + def a = new org.zstack.sdk.UpdateVmInstanceMetadataAction() + 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 updateVmNicDriver(@DelegatesTo(strategy = Closure.OWNER_FIRST, value = org.zstack.sdk.UpdateVmNicDriverAction.class) Closure c) { def a = new org.zstack.sdk.UpdateVmNicDriverAction() a.sessionId = Test.currentEnvSpec?.session?.uuid diff --git a/testlib/src/main/java/org/zstack/testlib/LocalStorageSpec.groovy b/testlib/src/main/java/org/zstack/testlib/LocalStorageSpec.groovy index 747ca4388b1..8997d7d27e6 100755 --- a/testlib/src/main/java/org/zstack/testlib/LocalStorageSpec.groovy +++ b/testlib/src/main/java/org/zstack/testlib/LocalStorageSpec.groovy @@ -600,6 +600,26 @@ class LocalStorageSpec extends PrimaryStorageSpec { rsp.hashValue = cmd.installPath return rsp } + + simulator(LocalStorageKvmBackend.WRITE_VM_METADATA_PATH) { + return new LocalStorageKvmBackend.WriteVmMetadataRsp() + } + + simulator(LocalStorageKvmBackend.GET_VM_INSTANCE_METADATA_PATH) { + return new LocalStorageKvmBackend.GetVmInstanceMetadataRsp() + } + + simulator(LocalStorageKvmBackend.SCAN_VM_METADATA_PATH) { + return new LocalStorageKvmBackend.ScanVmMetadataRsp() + } + + simulator(LocalStorageKvmBackend.CLEANUP_VM_METADATA_PATH) { + return new LocalStorageKvmBackend.CleanupVmMetadataRsp() + } + + simulator(LocalStorageKvmBackend.PREFIX_REBASE_BACKING_FILES_PATH) { + return new LocalStorageKvmBackend.PrefixRebaseBackingFilesRsp() + } } } diff --git a/testlib/src/main/java/org/zstack/testlib/NfsPrimaryStorageSpec.groovy b/testlib/src/main/java/org/zstack/testlib/NfsPrimaryStorageSpec.groovy index 0dbe59bfc01..3b7d7ad105e 100755 --- a/testlib/src/main/java/org/zstack/testlib/NfsPrimaryStorageSpec.groovy +++ b/testlib/src/main/java/org/zstack/testlib/NfsPrimaryStorageSpec.groovy @@ -522,6 +522,26 @@ class NfsPrimaryStorageSpec extends PrimaryStorageSpec { return rsp } + simulator(NfsPrimaryStorageKVMBackend.WRITE_VM_METADATA_PATH) { + return new NfsPrimaryStorageKVMBackendCommands.WriteVmMetadataRsp() + } + + simulator(NfsPrimaryStorageKVMBackend.GET_VM_INSTANCE_METADATA_PATH) { + return new NfsPrimaryStorageKVMBackendCommands.GetVmInstanceMetadataRsp() + } + + simulator(NfsPrimaryStorageKVMBackend.SCAN_VM_METADATA_PATH) { + return new NfsPrimaryStorageKVMBackendCommands.ScanVmMetadataRsp() + } + + simulator(NfsPrimaryStorageKVMBackend.CLEANUP_VM_METADATA_PATH) { + return new NfsPrimaryStorageKVMBackendCommands.CleanupVmMetadataRsp() + } + + simulator(NfsPrimaryStorageKVMBackend.NFS_PREFIX_REBASE_BACKING_FILES_PATH) { + return new NfsPrimaryStorageKVMBackendCommands.PrefixRebaseBackingFilesRsp() + } + VFS.vfsHook(NfsPrimaryStorageKVMBackend.NFS_REBASE_VOLUME_BACKING_FILE_PATH, xspec) { rsp, HttpEntity e, EnvSpec spec -> def cmd = JSONObjectUtil.toObject(e.body, NfsPrimaryStorageKVMBackendCommands.NfsRebaseVolumeBackingFileCmd.class) From 677c96e55f61714120ac6694f02e2c52c6c0698e Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 3 Apr 2026 14:04:47 +0800 Subject: [PATCH 106/129] [kvm]: add null check for dstHostUuid in secure boot Add a null check for dstHostUuid before querying TPM info in KvmSecureBootExtensions to prevent NullPointerException when destination host is not specified. Resolves: ZSV-11731 Related: ZSV-11310 Change-Id: I75706a697969776f617667686864617376717274 --- .../java/org/zstack/kvm/efi/KvmSecureBootExtensions.java | 5 +++++ 1 file changed, 5 insertions(+) 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 5951591ce2a..8cd857c8edf 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 @@ -168,6 +168,11 @@ public void preVmMigration(VmInstanceInventory vm, VmMigrationType type, String return; } + if (dstHostUuid == null) { + completion.success(); + return; + } + String tpmUuid = Q.New(TpmVO.class) .eq(TpmVO_.vmInstanceUuid, vm.getUuid()) .select(TpmVO_.uuid) From cd69046d5a3b7203861b080919cf71d1cdcefd53 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 3 Apr 2026 15:54:17 +0800 Subject: [PATCH 107/129] [core]: enforce propagateExceptionTo before lambda handlers Add assertion in done(Runnable) and error(Consumer) to require propagateExceptionTo() being called first. This prevents uncaught exceptions from being silently swallowed when using lambda-style handlers. - Allow first element of asyncBackups to be null for cases where no async backup is needed. propagateExceptionTo will remove all null elements. - Remove redundant isEmpty ternary guards since the new assertion guarantees asyncBackups is non-empty Related: ZSV-11310 Change-Id: I696870737a6f6b79747a63686c73656363686a6a --- .../zstack/core/workflow/SimpleFlowChain.java | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) 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 683f2580154..a13a7cd6742 100755 --- a/core/src/main/java/org/zstack/core/workflow/SimpleFlowChain.java +++ b/core/src/main/java/org/zstack/core/workflow/SimpleFlowChain.java @@ -26,6 +26,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Function; +import java.util.stream.Collectors; import static org.zstack.core.Platform.inerr; @@ -260,9 +261,11 @@ public SimpleFlowChain error(FlowErrorHandler handler) { @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(!asyncBackups.isEmpty(), + "propagateExceptionTo() must be called before error(Consumer)." + + " Call .propagateExceptionTo(completion) or .propagateExceptionTo((AsyncBackup) null) if you don't need async backup"); + AsyncBackup firstAsyncBackup = this.asyncBackups.get(0); + AsyncBackup[] otherAsyncBackups = 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) { @@ -320,12 +323,16 @@ public Map getData() { 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)); + this.asyncBackups.removeIf(Objects::isNull); + + if (this.asyncBackups.isEmpty()) { + this.asyncBackups.add(null); + } return this; } @@ -338,9 +345,11 @@ public SimpleFlowChain done(FlowDoneHandler handler) { @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(!asyncBackups.isEmpty(), + "propagateExceptionTo() must be called before done(Runnable)." + + " Call .propagateExceptionTo(completion) or .propagateExceptionTo((AsyncBackup) null) if you don't need async backup"); + AsyncBackup firstAsyncBackup = this.asyncBackups.get(0); + AsyncBackup[] otherAsyncBackups = 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) { From 9c9e2e45e5f6eb4f39735614aca44cc16e482b2d Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 3 Apr 2026 14:53:00 +0800 Subject: [PATCH 108/129] [kvm]: reset TPM via data plane before DB cleanup Refactor handle(ResetVmTpmMsg) to use SimpleFlowChain.of pattern with two flow steps: 1. Send delete commands to KVM hosts via rewriteVmHostFiles with operation=Delete, grouped by hostUuid. Failures are propagated to the caller. 2. Remove VmHostFileVO and VmHostBackupFileVO DB records only after data plane deletion succeeds. Previously this handler only deleted DB records without notifying KVM agents to remove actual files on hosts. Resolves: ZSV-11739 Related: ZSV-11310 Change-Id: I697075746b71777978747863747276736a777663 --- .../org/zstack/kvm/tpm/KvmTpmManager.java | 197 ++++++++++++++++-- 1 file changed, 184 insertions(+), 13 deletions(-) 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 eff3303b20f..fa9c1dbebfd 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 @@ -59,6 +59,7 @@ import org.zstack.kvm.KVMAgentCommands; import org.zstack.kvm.KvmCommandSender; import org.zstack.kvm.KvmResponseWrapper; +import org.zstack.kvm.efi.KvmSecureBootExtensions; import org.zstack.kvm.tpm.message.CloneVmTpmMsg; import org.zstack.kvm.tpm.message.CloneVmTpmReply; import org.zstack.resourceconfig.ResourceConfig; @@ -98,6 +99,8 @@ public class KvmTpmManager extends AbstractService { private VmTpmManager vmTpmManager; @Autowired private TpmEncryptedResourceKeyBackend tpmKeyBackend; + @Autowired + private KvmSecureBootExtensions secureBootExtensions; @Override public boolean start() { @@ -514,25 +517,193 @@ public void handle(ErrorCode errCode, Map data) { }).start(); } + static class ResetVmTpmContext { + String vmInstanceUuid; + + List hostFiles; + VmHostFileVO hostFileToDeleteLast; + List hostFileUuidListDeleteSuccessfully = new ArrayList<>(); + ErrorCodeList errorsOnSendCmd = new ErrorCodeList(); + + static ResetVmTpmContext valueOf(ResetVmTpmMsg msg) { + ResetVmTpmContext context = new ResetVmTpmContext(); + context.vmInstanceUuid = msg.getVmInstanceUuid(); + return context; + } + } + private void handle(ResetVmTpmMsg msg) { ResetVmTpmReply reply = new ResetVmTpmReply(); + threadFacade.chainSubmit(new ChainTask(msg) { + @Override + public void run(SyncTaskChain chain) { + ResetVmTpmContext context = ResetVmTpmContext.valueOf(msg); + resetVmTpm(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); + } + }); + } - String vmUuid = msg.getVmInstanceUuid(); - new SQLBatch() { @Override - protected void scripts() { - sql(VmHostFileVO.class) - .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) - .eq(VmHostFileVO_.type, VmHostFileType.TpmState) - .delete(); - sql(VmHostBackupFileVO.class) - .eq(VmHostBackupFileVO_.resourceUuid, vmUuid) - .eq(VmHostBackupFileVO_.type, VmHostFileType.TpmState) - .delete(); + public String getSyncSignature() { + return tpmQueueSyncSignature(msg.getVmInstanceUuid()); } - }.execute(); - bus.reply(msg, reply); + @Override + public String getName() { + return "queue-of-reset-tpm-from-vm-" + msg.getVmInstanceUuid(); + } + }); + } + + private void resetVmTpm(ResetVmTpmContext context, Completion completion) { + String vmUuid = context.vmInstanceUuid; + + SimpleFlowChain.of("reset-vm-tpm-" + vmUuid) + .then(Flow.of("collect-vm-host-files") + .handle(trigger -> { + context.hostFiles = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) + .eq(VmHostFileVO_.type, VmHostFileType.TpmState) + .orderByAsc(VmHostFileVO_.lastOpDate) + .list(); + if (!context.hostFiles.isEmpty()) { + // We should delete it in last turn: + context.hostFileToDeleteLast = context.hostFiles.get(context.hostFiles.size() - 1); + context.hostFiles.remove(context.hostFiles.size() - 1); + } + trigger.next(); + }) + .build()) + .then(Flow.of("send-delete-commands-to-hosts-exclude-last-modified") + .skipIf(data -> context.hostFiles.isEmpty()) + .handle(trigger -> { + Map> filesByHost = new HashMap<>(); + for (VmHostFileVO file : context.hostFiles) { + filesByHost.computeIfAbsent(file.getHostUuid(), k -> new ArrayList<>()).add(file); + } + + new While<>(filesByHost.entrySet()).each((entry, whileCompletion) -> { + List fileTOs = new ArrayList<>(); + for (VmHostFileVO file : entry.getValue()) { + KVMAgentCommands.VmHostFileTO to = new KVMAgentCommands.VmHostFileTO(); + to.setPath(file.getPath()); + to.setType(file.getType().toString()); + to.setOperation(VmHostFileOperation.Delete.toString()); + fileTOs.add(to); + } + + KvmSecureBootExtensions.RewriteVmHostFilesContext ctx = + new KvmSecureBootExtensions.RewriteVmHostFilesContext(); + ctx.hostUuid = entry.getKey(); + ctx.hostFiles = fileTOs; + + secureBootExtensions.rewriteVmHostFiles(ctx, new Completion(whileCompletion) { + @Override + public void success() { + context.hostFileUuidListDeleteSuccessfully.addAll( + transform(entry.getValue(), VmHostFileVO::getUuid)); + whileCompletion.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + context.errorsOnSendCmd.add(errorCode.withOpaque("host.uuid", entry.getKey())); + whileCompletion.done(); + } + }); + }).run(new WhileDoneCompletion(trigger) { + @Override + public void done(ErrorCodeList errorCodeList) { + trigger.next(); + } + }); + }) + .build()) + .then(Flow.of("remove-db-records") + .skipIf(data -> context.hostFileUuidListDeleteSuccessfully.isEmpty()) + .handle(trigger -> { + SQL.New(VmHostFileVO.class) + .in(VmHostFileVO_.uuid, context.hostFileUuidListDeleteSuccessfully) + .delete(); + trigger.next(); + }) + .build()) + .then(Flow.of("check-if-any-error-in-command-sending") + .handle(trigger -> { + // If any host failed to delete, abort the chain to preserve + // the last-modified TPM record as a recovery point. + if (context.errorsOnSendCmd.hasError()) { + if (context.errorsOnSendCmd.size() == 1) { + trigger.fail(context.errorsOnSendCmd.getCauses().get(0)); + } else { + trigger.fail(operr("failed to delete TPM files on multiple hosts") + .withOpaque("vm.uuid", vmUuid) + .withCause(context.errorsOnSendCmd.getCauses())); + } + return; + } + trigger.next(); + }) + .build()) + .then(Flow.of("send-delete-commands-to-hosts-for-last-modified") + .skipIf(data -> context.hostFileToDeleteLast == null) + .handle(trigger -> { + KVMAgentCommands.VmHostFileTO to = new KVMAgentCommands.VmHostFileTO(); + to.setPath(context.hostFileToDeleteLast.getPath()); + to.setType(context.hostFileToDeleteLast.getType().toString()); + to.setOperation(VmHostFileOperation.Delete.toString()); + + KvmSecureBootExtensions.RewriteVmHostFilesContext ctx = + new KvmSecureBootExtensions.RewriteVmHostFilesContext(); + ctx.hostUuid = context.hostFileToDeleteLast.getHostUuid(); + ctx.hostFiles = list(to); + + secureBootExtensions.rewriteVmHostFiles(ctx, new Completion(trigger) { + @Override + public void success() { + trigger.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + trigger.fail(errorCode.withOpaque("host.uuid", ctx.hostUuid)); + } + }); + }) + .build()) + .then(Flow.of("remove-db-records-for-remains") + .skipIf(data -> context.hostFileToDeleteLast == null) + .handle(trigger -> { + new SQLBatch() { + @Override + protected void scripts() { + sql(VmHostFileVO.class) + .eq(VmHostFileVO_.uuid, context.hostFileToDeleteLast.getUuid()) + .delete(); + sql(VmHostBackupFileVO.class) + .eq(VmHostBackupFileVO_.resourceUuid, vmUuid) + .eq(VmHostBackupFileVO_.type, VmHostFileType.TpmState) + .delete(); + } + }.execute(); + trigger.next(); + }) + .build()) + .propagateExceptionTo(completion) + .done(completion::success) + .error(completion::fail) + .start(); } private void handle(APIGetTpmCapabilityMsg msg) { From 2785e194c5d603b76544e054ae1236eba24b86e1 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 3 Apr 2026 22:42:00 +0800 Subject: [PATCH 109/129] [kvm]: skip key provider attach when no KMS Split the "create-tpm-db-records" flow into two separate flows: one for persisting TpmVO and another for attaching the key provider to TPM. The new "attach-key-provider-to-tpm" flow uses skipIf to skip execution when ALLOWED_TPM_VM_WITHOUT_KMS global config is set to true, so that tpmKeyBackend.attachKeyProviderToTpm is not called. Resolves: ZSV-11729 Related: ZSV-11310 Change-Id: I72757569696e73707a666b746d776a786e716574 --- .../org/zstack/kvm/tpm/KvmTpmManager.java | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) 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 eff3303b20f..fa3569cdb89 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.VmGlobalConfig; import org.zstack.compute.vm.devices.TpmEncryptedResourceKeyBackend; import org.zstack.compute.vm.devices.VmTpmManager; import org.zstack.core.asyncbatch.While; @@ -230,28 +231,30 @@ private void addTpmToVm(AddTpmToVmContext context, Completion completion) { .build()) .then(Flow.of("create-tpm-db-records") .handle(trigger -> { - try { - TpmVO tpm = vmTpmManager.persistTpmVO(context.tpmUuid, context.vmInstanceUuid); - context.createdTpmUuid = tpm.getUuid(); - context.tpmCreated = true; - if (context.keyProviderUuid != null) { - tpmKeyBackend.attachKeyProviderToTpm(context.createdTpmUuid, context.keyProviderUuid); - context.keyProviderAttached = true; - } - trigger.next(); - } catch (Exception e) { - trigger.fail(operr("failed to add TPM to vm[uuid:%s]: %s", context.vmInstanceUuid, e.getMessage())); + TpmVO tpm = vmTpmManager.persistTpmVO(context.tpmUuid, context.vmInstanceUuid); + context.createdTpmUuid = tpm.getUuid(); + context.tpmCreated = true; + trigger.next(); + }) + .rollback(trigger -> { + if (context.tpmCreated && context.createdTpmUuid != null) { + vmTpmManager.deleteTpmVO(context.createdTpmUuid); + } + trigger.rollback(); + }) + .build()) + .then(Flow.of("attach-key-provider-to-tpm") + .skipIf(data -> VmGlobalConfig.ALLOWED_TPM_VM_WITHOUT_KMS.value(Boolean.class)) + .handle(trigger -> { + if (context.keyProviderUuid != null) { + tpmKeyBackend.attachKeyProviderToTpm(context.createdTpmUuid, context.keyProviderUuid); + context.keyProviderAttached = true; } + trigger.next(); }) .rollback(trigger -> { - try { - if (context.keyProviderAttached && context.createdTpmUuid != null) { - tpmKeyBackend.detachKeyProviderFromTpm(context.createdTpmUuid); - } - } finally { - if (context.tpmCreated && context.createdTpmUuid != null) { - vmTpmManager.deleteTpmVO(context.createdTpmUuid); - } + if (context.keyProviderAttached && context.createdTpmUuid != null) { + tpmKeyBackend.detachKeyProviderFromTpm(context.createdTpmUuid); } trigger.rollback(); }) From fdcff04ea5ab5ef3f664c8310de1c61fa94b8446 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Sat, 4 Apr 2026 22:41:19 +0800 Subject: [PATCH 110/129] [kvm]: use enum for vm host file sync reasons Replace hardcoded strings with VmHostFileSyncReason enum for vm host file synchronization operations. Add new enum values for snapshot group online backup, revert snapshot and volume backup scenarios. Update KvmSecureBootExtensions and VolumeSnapshotGroupBase to use enum values instead of hardcoded strings. Resolves: ZSV-11560 Related: ZSV-11310 Change-Id: I6f7867777a616777636c64666a6b646d6d6e7a70 --- .../org/zstack/header/vm/additions/VmHostFileSyncReason.java | 3 +++ .../main/java/org/zstack/kvm/efi/KvmSecureBootExtensions.java | 3 ++- .../zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileSyncReason.java b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileSyncReason.java index 82b8dd15957..d9755e05fe6 100644 --- a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileSyncReason.java +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileSyncReason.java @@ -8,6 +8,9 @@ public enum VmHostFileSyncReason { VmShutdown("on libvirt shutdown event"), PostClone("on post-clone (from dest host)"), Restore("restore"), + SnapshotGroupOnlineBackup("snapshot group online backup"), + RevertSnapshot("revert snapshot"), + VolumeBackup("volume backup"), ; public final String detail; 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 8cd857c8edf..35b7f6992a1 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 @@ -85,6 +85,7 @@ import static org.zstack.header.vm.additions.VmHostFileSyncReason.PrepareReRead; import static org.zstack.header.vm.additions.VmHostFileSyncReason.PrepareRead; import static org.zstack.header.vm.additions.VmHostFileSyncReason.ResourceRelease; +import static org.zstack.header.vm.additions.VmHostFileSyncReason.SnapshotGroupOnlineBackup; import static org.zstack.kvm.KVMConstant.*; import static org.zstack.utils.CollectionDSL.list; @@ -699,7 +700,7 @@ public void afterVolumeLiveSnapshotGroupCreatedOnBackend(CreateVolumesSnapshotOv } } - syncMsg.setSyncReason("snapshot-group-online-backup"); + syncMsg.setSyncReason(SnapshotGroupOnlineBackup.reason()); syncMsg.setSyncToBackup(true); syncMsg.setBackupResourceUuid(tempResourceUuid); bus.makeLocalServiceId(syncMsg, VmInstanceConstant.SECURE_BOOT_SERVICE_ID); diff --git a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java index 2385428aed0..294f3e39f33 100644 --- a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupBase.java @@ -34,6 +34,7 @@ import org.zstack.header.vm.VmInstanceConstant; import org.zstack.header.vm.additions.VmHostBackupFileVO; import org.zstack.header.vm.additions.VmHostBackupFileVO_; +import org.zstack.header.vm.additions.VmHostFileSyncReason; import org.zstack.header.vm.devices.VmInstanceResourceMetadataManager; import org.zstack.header.volume.VolumeType; import org.zstack.header.volume.VolumeVO; @@ -405,7 +406,7 @@ class Context { RestoreVmHostFileMsg restoreMsg = new RestoreVmHostFileMsg(); restoreMsg.setVmInstanceUuid(vmUuid); restoreMsg.setSnapshotGroupUuid(self.getUuid()); - restoreMsg.setSyncReason("revert snapshot"); + restoreMsg.setSyncReason(VmHostFileSyncReason.RevertSnapshot.reason()); bus.makeLocalServiceId(restoreMsg, VmInstanceConstant.SECURE_BOOT_SERVICE_ID); bus.send(restoreMsg, new CloudBusCallBack(trigger) { @Override From c4fb10cbe24177f1d08a09e54566ae8c5fc525f2 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Sun, 5 Apr 2026 12:38:15 +0800 Subject: [PATCH 111/129] [kvm]: prepare NvRam host-file before migration Resolves: ZSV-11524 Related: ZSV-11430 Related: ZSV-11310 Change-Id: I6e736c676b726a776f7961756b6f636f6b797369 --- .../java/org/zstack/kvm/efi/KvmSecureBootExtensions.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 35b7f6992a1..cc7084e7cf6 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 @@ -162,6 +162,11 @@ private void prepareNvRamToStartVmCmd(KVMAgentCommands.StartVmCmd cmd, NvRamSpec } } + @Override + public void preMigrateVm(VmInstanceInventory inv, String destHostUuid, Completion completion) { + completion.success(); // use preVmMigration instead of preMigrateVm to prevent from handle twice + } + @Override public void preVmMigration(VmInstanceInventory vm, VmMigrationType type, String dstHostUuid, Completion completion) { if (HostMigration != type && PrimaryStorageMigration != type) { @@ -169,6 +174,10 @@ public void preVmMigration(VmInstanceInventory vm, VmMigrationType type, String return; } + prepareNvRamBeforeMigration(vm, dstHostUuid, completion); + } + + private void prepareNvRamBeforeMigration(VmInstanceInventory vm, String dstHostUuid, Completion completion) { if (dstHostUuid == null) { completion.success(); return; From 932c8baf86e9bdd2fb913efa235e501db8de63d4 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Mon, 6 Apr 2026 21:24:07 +0800 Subject: [PATCH 112/129] [kvm]: sync VM host files before HA start Add BeforeHaStartVmInstanceExtensionPoint to sync NvRam and TPM state files from last host when HA starts a VM. This ensures that secure boot state is preserved during HA failover. Changes: - Implement BeforeHaStartVmInstanceExtensionPoint in KvmSecureBootExtensions - Add BeforeHaStart sync reason for VmHostFileSyncReason - Register extension point in Kvm.xml spring config - Sync NvRam and TPM state files from last host before HA start Resolves: ZSV-11767 Related: ZSV-11613 Related: ZSV-11310 Change-Id: I706a757376666b67706a6a666e7271776e7a776f --- conf/springConfigXml/Kvm.xml | 1 + .../vm/additions/VmHostFileSyncReason.java | 1 + .../kvm/efi/KvmSecureBootExtensions.java | 66 ++++++++++++++++++- .../test/resources/springConfigXml/Kvm.xml | 1 + 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index 13625bb3ae2..3417a5272c0 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -276,6 +276,7 @@ +
diff --git a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileSyncReason.java b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileSyncReason.java index d9755e05fe6..8745a8b3779 100644 --- a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileSyncReason.java +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileSyncReason.java @@ -11,6 +11,7 @@ public enum VmHostFileSyncReason { SnapshotGroupOnlineBackup("snapshot group online backup"), RevertSnapshot("revert snapshot"), VolumeBackup("volume backup"), + BeforeHaStart("on before HA start (from last host)"), ; public final String detail; 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 cc7084e7cf6..b84bd996211 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 @@ -31,6 +31,7 @@ import org.zstack.header.vm.VmInstanceVO_; import org.zstack.header.vm.VmInstanceConstant; import org.zstack.header.vm.AfterReimageVmInstanceExtensionPoint; +import org.zstack.header.vm.BeforeHaStartVmInstanceExtensionPoint; import org.zstack.header.vm.VmInstanceInventory; import org.zstack.header.vm.VmInstanceMigrateExtensionPoint; import org.zstack.header.vm.VmInstanceSpec; @@ -69,6 +70,7 @@ 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.Base64; @@ -82,6 +84,7 @@ import static org.zstack.header.vm.VmMigrationType.HostMigration; import static org.zstack.header.vm.VmMigrationType.PrimaryStorageMigration; import static org.zstack.header.vm.additions.VmHostFileSyncReason.PostMigration; +import static org.zstack.header.vm.additions.VmHostFileSyncReason.BeforeHaStart; import static org.zstack.header.vm.additions.VmHostFileSyncReason.PrepareReRead; import static org.zstack.header.vm.additions.VmHostFileSyncReason.PrepareRead; import static org.zstack.header.vm.additions.VmHostFileSyncReason.ResourceRelease; @@ -96,7 +99,8 @@ public class KvmSecureBootExtensions implements KVMStartVmExtensionPoint, AfterReimageVmInstanceExtensionPoint, VmReleaseResourceExtensionPoint, VmInstanceMigrateExtensionPoint, - VolumeSnapshotCreationExtensionPoint { + VolumeSnapshotCreationExtensionPoint, + BeforeHaStartVmInstanceExtensionPoint { private static final CLogger logger = Utils.getLogger(KvmSecureBootExtensions.class); @Autowired @@ -600,6 +604,66 @@ public void run(MessageReply reply) { }); } + @Override + public void beforeHaStartVmInstance(String vmUuid, String judgerClassName, List softAvoidHostUuids, Completion completion) { + Tuple hostUuidTuple = Q.New(VmInstanceVO.class) + .select(VmInstanceVO_.hostUuid, VmInstanceVO_.lastHostUuid) + .eq(VmInstanceVO_.uuid, vmUuid) + .findTuple(); + if (hostUuidTuple == null) { + completion.success(); + return; + } + + String hostUuid = hostUuidTuple.get(0, String.class); + final String finalHostUuid = (hostUuid == null) ? hostUuidTuple.get(1, String.class) : hostUuid; + if (finalHostUuid == null) { + logger.debug("skip HA file sync: VM has no host/lastHost record"); + completion.success(); + return; + } + + List vmHostFiles = Q.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) + .eq(VmHostFileVO_.hostUuid, finalHostUuid) + .list(); + if (vmHostFiles.isEmpty()) { + completion.success(); + return; + } + + logger.info(String.format("try to sync VM host file[vmUuid=%s] from last host[uuid=%s] before HA start", + vmUuid, finalHostUuid)); + SyncVmHostFilesFromHostMsg syncMsg = new SyncVmHostFilesFromHostMsg(); + syncMsg.setHostUuid(finalHostUuid); + syncMsg.setVmUuid(vmUuid); + syncMsg.setSyncReason(BeforeHaStart.reason()); + + for (VmHostFileVO file : vmHostFiles) { + if (file.getType() == VmHostFileType.NvRam) { + syncMsg.setNvRamPath(file.getPath()); + } else if (file.getType() == VmHostFileType.TpmState) { + syncMsg.setTpmStateFolder(file.getPath()); + } else { + logger.warn(String.format( + "unsupported vm host file type: %s, skip syncing for VM[uuid:%s] from host[uuid:%s]", + file.getType(), vmUuid, finalHostUuid)); + } + } + + bus.makeLocalServiceId(syncMsg, VmInstanceConstant.SECURE_BOOT_SERVICE_ID); + bus.send(syncMsg, new CloudBusCallBack(completion) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + logger.warn(String.format("failed to sync VM host file[vmUuid=%s] from last host[uuid=%s] before HA start, but tolerated: %s", + vmUuid, finalHostUuid, reply.getError().getReadableDetails())); + } + completion.success(); + } + }); + } + @Override public void afterMigrateVm(VmInstanceInventory inv, String srcHostUuid, NoErrorCompletion completion) { String destHostUuid = inv.getHostUuid(); diff --git a/test/src/test/resources/springConfigXml/Kvm.xml b/test/src/test/resources/springConfigXml/Kvm.xml index f209b116efb..2b43a4ba787 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -275,6 +275,7 @@ +
From 3a674556fea3b774fb15b7743ec65da296f9b784 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Tue, 7 Apr 2026 11:58:38 +0800 Subject: [PATCH 113/129] [crypto]: keep TPM key ref until VM removed from DB Delay/trash deletion no longer drops EncryptedResourceKeyRef; cleanup runs in vmJustBeforeDeleteFromDb (expunge/direct delete). Centralize ref delete in VmTpmManager; wire RemoveTpm/add-tpm rollback and drop unused destroy extension from VmTpmExtensions. Resolves: ZSV-11721 Change-Id: I00d3201a733026a6c82edc281dcb6fa760e6ddfd --- .../org/zstack/compute/vm/VmInstanceBase.java | 24 ++++++++++++ .../compute/vm/devices/VmTpmManager.java | 7 ++++ .../EncryptedResourceKeyManager.java | 9 +++++ .../org/zstack/kvm/tpm/KvmTpmExtensions.java | 6 ++- sdk/src/main/java/SourceClassMap.java | 2 + .../keyprovider/api/RekeyFailedResource.java | 39 +++++++++++++++++++ .../api/RekeyKeyProviderRefsResult.java | 16 ++++++++ 7 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyFailedResource.java 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 bd929d84a98..f44eb637f66 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java @@ -7,6 +7,8 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.transaction.annotation.Transactional; import org.zstack.compute.allocator.HostAllocatorManager; +import org.zstack.compute.vm.devices.TpmEncryptedResourceKeyBackend; +import org.zstack.compute.vm.devices.VmTpmManager; import org.zstack.core.Platform; import org.zstack.core.asyncbatch.While; import org.zstack.core.cascade.CascadeConstant; @@ -142,6 +144,8 @@ public class VmInstanceBase extends AbstractVmInstance { private VmInstanceResourceMetadataManager vidm; @Autowired private NetworkServiceManager nwServiceMgr; + @Autowired + private TpmEncryptedResourceKeyBackend tpmKeyBackend; protected VmInstanceVO self; protected VmInstanceVO originalCopy; @@ -149,6 +153,19 @@ public class VmInstanceBase extends AbstractVmInstance { private final static StaticIpOperator ipOperator = new StaticIpOperator(); private final static VmConfigSyncHelper vmConfigSyncHelper = new VmConfigSyncHelper(); + private void detachTpmKeyProviderBestEffort(String tpmUuid) { + if (tpmUuid == null) { + return; + } + try { + tpmKeyBackend.detachKeyProviderFromTpm(tpmUuid); + } catch (Throwable t) { + logger.warn(String.format( + "failed to detach key provider from TPM[uuid:%s]: %s", + tpmUuid, t.getMessage()), t); + } + } + protected void checkState(final String hostUuid, final NoErrorCompletion completion) { CheckVmStateOnHypervisorMsg msg = new CheckVmStateOnHypervisorMsg(); msg.setVmInstanceUuids(list(self.getUuid())); @@ -1272,6 +1289,8 @@ public void handle(Map data) { CollectionUtils.safeForEach(pluginRgty.getExtensionList(VmAfterExpungeExtensionPoint.class), arg -> arg.vmAfterExpunge(inv)); + final String tpmUuidForEncryptedKeyRef = VmTpmManager.findTpmUuidForVmOrNull(self.getUuid()); + callVmJustBeforeDeleteFromDbExtensionPoint(); dbf.reload(self); @@ -1283,6 +1302,7 @@ public void handle(Map data) { if (inv.getRootVolumeUuid() != null) { dbf.eoCleanup(VolumeVO.class, inv.getRootVolumeUuid()); } + detachTpmKeyProviderBestEffort(tpmUuidForEncryptedKeyRef); completion.success(); } }).error(new FlowErrorHandler(completion) { @@ -2582,12 +2602,15 @@ public void success() { if (self.getState() != VmInstanceState.Destroyed) { changeVmStateInDb(VmInstanceStateEvent.destroyed); } + final String tpmUuidForEncryptedKeyRef = VmTpmManager.findTpmUuidForVmOrNull(self.getUuid()); callVmJustBeforeDeleteFromDbExtensionPoint(); dbf.removeCollection(self.getVmCdRoms(), VmCdRomVO.class); dbf.remove(getSelf()); dbf.eoCleanup(VmInstanceVO.class, self.getUuid()); + detachTpmKeyProviderBestEffort(tpmUuidForEncryptedKeyRef); } else if (deletionPolicy == VmInstanceDeletionPolicy.DBOnly || deletionPolicy == VmInstanceDeletionPolicy.KeepVolume) { String accountUuid = acntMgr.getOwnerAccountUuidOfResource(inv.getUuid()); + final String tpmUuidForEncryptedKeyRef = VmTpmManager.findTpmUuidForVmOrNull(self.getUuid()); new SQLBatch() { @Override protected void scripts() { @@ -2601,6 +2624,7 @@ protected void scripts() { sql(VmInstanceVO.class).eq(VmInstanceVO_.uuid, self.getUuid()).hardDelete(); } }.execute(); + detachTpmKeyProviderBestEffort(tpmUuidForEncryptedKeyRef); callVmJustAfterDeleteFromDbExtensionPoint(inv, accountUuid); } else if (deletionPolicy == VmInstanceDeletionPolicy.Delay) { changeVmStateInDb(VmInstanceStateEvent.destroyed); 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 7345e0e49d2..757696c6c1d 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 @@ -42,6 +42,13 @@ public void deleteTpmVO(String tpmUuid) { databaseFacade.removeByPrimaryKey(tpmUuid, TpmVO.class); } + public static String findTpmUuidForVmOrNull(String vmInstanceUuid) { + return Q.New(TpmVO.class) + .eq(TpmVO_.vmInstanceUuid, vmInstanceUuid) + .select(TpmVO_.uuid) + .findValue(); + } + /** * @param bootMode boot mode, null is Legacy */ diff --git a/header/src/main/java/org/zstack/header/keyprovider/EncryptedResourceKeyManager.java b/header/src/main/java/org/zstack/header/keyprovider/EncryptedResourceKeyManager.java index dcdd76851a7..ecf37e08b94 100644 --- a/header/src/main/java/org/zstack/header/keyprovider/EncryptedResourceKeyManager.java +++ b/header/src/main/java/org/zstack/header/keyprovider/EncryptedResourceKeyManager.java @@ -38,6 +38,7 @@ class GetOrCreateResourceKeyContext { private String resourceUuid; private String resourceType; private String keyProviderUuid; + private String keyProviderName; private String purpose; public String getResourceUuid() { @@ -64,6 +65,14 @@ public void setKeyProviderUuid(String keyProviderUuid) { this.keyProviderUuid = keyProviderUuid; } + public String getKeyProviderName() { + return keyProviderName; + } + + public void setKeyProviderName(String keyProviderName) { + this.keyProviderName = keyProviderName; + } + public String getPurpose() { return purpose; } 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 5d98121f66a..11ba25bf55f 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 @@ -173,7 +173,7 @@ public void fail(ErrorCode errorCode) { @Override public boolean skip(Map data) { boolean shouldSkip = VmGlobalConfig.ALLOWED_TPM_VM_WITHOUT_KMS.value(Boolean.class) && - (StringUtils.isBlank(context.providerUuid) || StringUtils.isBlank(context.providerName)); + (StringUtils.isBlank(context.providerUuid) && StringUtils.isBlank(context.providerName)); if (shouldSkip) { logger.info("skip create-dek: allowed.tpm.vm.without.kms is enabled and no KMS provider bound"); } @@ -182,7 +182,7 @@ public boolean skip(Map data) { @Override public void run(FlowTrigger trigger, Map data) { - if (StringUtils.isBlank(context.providerUuid) || StringUtils.isBlank(context.providerName)) { + if (StringUtils.isBlank(context.providerUuid) && StringUtils.isBlank(context.providerName)) { trigger.fail(operr("missing TPM resource key binding for tpm[uuid:%s], attachKeyProviderToTpm must run before create-dek", context.tpmUuid)); return; } @@ -191,6 +191,7 @@ public void run(FlowTrigger trigger, Map data) { keyCtx.setResourceUuid(context.tpmUuid); keyCtx.setResourceType(TpmVO.class.getSimpleName()); keyCtx.setKeyProviderUuid(context.providerUuid); + keyCtx.setKeyProviderName(context.providerName); keyCtx.setPurpose("vtpm"); resourceKeyManager.getOrCreateKey(keyCtx, new ReturnValueCompletion(trigger) { @@ -198,6 +199,7 @@ public void run(FlowTrigger trigger, Map data) { public void success(ResourceKeyResult result) { tpmSpec.setResourceKeyCreatedNew(result.isCreatedNewKey()); tpmSpec.setResourceKeyProviderUuid(result.getKeyProviderUuid()); + context.providerUuid = result.getKeyProviderUuid(); context.providerName = result.getKeyProviderName(); context.dekBase64 = result.getDekBase64(); trigger.next(); diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 99ce7738939..6820b518a7f 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -70,6 +70,7 @@ public class SourceClassMap { put("org.zstack.core.jsonlabel.JsonLabelInventory", "org.zstack.sdk.JsonLabelInventory"); put("org.zstack.crypto.ccs.CCSCertificateAccountRefInventory", "org.zstack.sdk.CCSCertificateAccountRefInventory"); put("org.zstack.crypto.ccs.CCSCertificateInventory", "org.zstack.sdk.CCSCertificateInventory"); + put("org.zstack.crypto.keyprovider.api.RekeyFailedResource", "org.zstack.sdk.keyprovider.api.RekeyFailedResource"); put("org.zstack.crypto.keyprovider.api.RekeySkippedResource", "org.zstack.sdk.keyprovider.api.RekeySkippedResource"); put("org.zstack.crypto.securitymachine.thirdparty.aisino.AiSiNoSecretResourcePoolInventory", "org.zstack.sdk.AiSiNoSecretResourcePoolInventory"); put("org.zstack.crypto.securitymachine.thirdparty.flkSec.FlkSecSecretResourcePoolInventory", "org.zstack.sdk.FlkSecSecretResourcePoolInventory"); @@ -1267,6 +1268,7 @@ public class SourceClassMap { put("org.zstack.sdk.identity.ldap.entity.LdapServerInventory", "org.zstack.ldap.entity.LdapServerInventory"); put("org.zstack.sdk.identity.role.RoleAccountRefInventory", "org.zstack.header.identity.role.RoleAccountRefInventory"); put("org.zstack.sdk.identity.role.RoleInventory", "org.zstack.header.identity.role.RoleInventory"); + put("org.zstack.sdk.keyprovider.api.RekeyFailedResource", "org.zstack.crypto.keyprovider.api.RekeyFailedResource"); put("org.zstack.sdk.keyprovider.api.RekeySkippedResource", "org.zstack.crypto.keyprovider.api.RekeySkippedResource"); put("org.zstack.sdk.license.entity.LicenseHistoryInventory", "org.zstack.license.entity.LicenseHistoryInventory"); put("org.zstack.sdk.license.entity.LicenseUsageView", "org.zstack.license.entity.LicenseUsageView"); diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyFailedResource.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyFailedResource.java new file mode 100644 index 00000000000..293d667dd6a --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyFailedResource.java @@ -0,0 +1,39 @@ +package org.zstack.sdk.keyprovider.api; + + + +public class RekeyFailedResource { + + public java.lang.Long keyRefId; + public void setKeyRefId(java.lang.Long keyRefId) { + this.keyRefId = keyRefId; + } + public java.lang.Long getKeyRefId() { + return this.keyRefId; + } + + public java.lang.String resourceType; + public void setResourceType(java.lang.String resourceType) { + this.resourceType = resourceType; + } + public java.lang.String getResourceType() { + return this.resourceType; + } + + public java.lang.String resourceUuid; + public void setResourceUuid(java.lang.String resourceUuid) { + this.resourceUuid = resourceUuid; + } + public java.lang.String getResourceUuid() { + return this.resourceUuid; + } + + 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/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsResult.java index 91f995ec6f6..b697dc044c3 100644 --- a/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsResult.java +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsResult.java @@ -27,6 +27,14 @@ public int getSkippedCount() { return this.skippedCount; } + public int failedCount; + public void setFailedCount(int failedCount) { + this.failedCount = failedCount; + } + public int getFailedCount() { + return this.failedCount; + } + public java.util.List skippedResources; public void setSkippedResources(java.util.List skippedResources) { this.skippedResources = skippedResources; @@ -35,4 +43,12 @@ public java.util.List getSkippedResources() { return this.skippedResources; } + public java.util.List failedResources; + public void setFailedResources(java.util.List failedResources) { + this.failedResources = failedResources; + } + public java.util.List getFailedResources() { + return this.failedResources; + } + } From 195a8e0c5df96e441a4d86e44a75598151543232 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 7 Apr 2026 16:31:06 +0800 Subject: [PATCH 114/129] [kvm]: periodic check and sync for VM host files Implement periodic VM host file checking with dirty detection and force sync mechanism. This improves the sync interval from 30 minutes to 15 seconds, allowing faster detection and sync of file changes. Changes: - Add changeDate and lastSyncDate fields to VmHostFileVO for tracking - Add PeriodicDirtyCheck and PeriodicForceSync sync reasons - Add VM_HOST_FILE_CHANGED canonical event for file change reporting - Update VmHostFileTracker to check files periodically and sync when changed or force sync after long period without changes - Update vm.host.file.sync.interval default from 1800 to 15 seconds - Update config description from 'syncing' to 'checking' - Implement event listener to mark files as changed when reported Resolves: ZSV-11371 Related: ZSV-11310 Change-Id: I6f62727061686b646f617463737672686d667578 --- conf/db/zsv/V5.0.0__schema.sql | 2 + .../zstack/header/vm/VmCanonicalEvents.java | 34 ++++++++ .../vm/additions/VmHostFileInventory.java | 20 +++++ .../vm/additions/VmHostFileSyncReason.java | 2 + .../header/vm/additions/VmHostFileVO.java | 22 ++++++ .../header/vm/additions/VmHostFileVO_.java | 2 + .../java/org/zstack/kvm/KVMGlobalConfig.java | 4 +- .../zstack/kvm/efi/KvmSecureBootManager.java | 50 ++++++++---- .../zstack/kvm/vmfiles/VmHostFileTracker.java | 79 ++++++++++++++++++- .../sdk/vm/entity/VmHostFileInventory.java | 16 ++++ 10 files changed, 213 insertions(+), 18 deletions(-) diff --git a/conf/db/zsv/V5.0.0__schema.sql b/conf/db/zsv/V5.0.0__schema.sql index 6e7ffc1f37c..ecbb6f112ac 100644 --- a/conf/db/zsv/V5.0.0__schema.sql +++ b/conf/db/zsv/V5.0.0__schema.sql @@ -16,6 +16,8 @@ CREATE TABLE IF NOT EXISTS `zstack`.`VmHostFileVO` ( `type` varchar(64) NOT NULL COMMENT 'NvRam, TpmState', `path` varchar(1024) NOT NULL COMMENT 'Absolute path of the file on the host', `lastSyncReason` varchar(255) DEFAULT NULL COMMENT 'The reason for the last sync operation', + `changeDate` timestamp NULL DEFAULT NULL COMMENT 'Timestamp when file was reported changed, null after sync', + `lastSyncDate` timestamp NULL DEFAULT NULL COMMENT 'Timestamp of the last successful sync', `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`), diff --git a/header/src/main/java/org/zstack/header/vm/VmCanonicalEvents.java b/header/src/main/java/org/zstack/header/vm/VmCanonicalEvents.java index bde3985ce8d..330eccdaea8 100755 --- a/header/src/main/java/org/zstack/header/vm/VmCanonicalEvents.java +++ b/header/src/main/java/org/zstack/header/vm/VmCanonicalEvents.java @@ -3,6 +3,7 @@ import org.zstack.header.message.NeedJsonSchema; import org.zstack.header.errorcode.ErrorCode; import java.util.Date; +import java.util.List; import java.time.LocalDateTime; /** @@ -19,6 +20,8 @@ public class VmCanonicalEvents { public static final String VM_NIC_INFO_CHANGED_PATH = "/vm/nicinfo/change"; public static final String VM_NIC_INFO_DUPLICATE_PATH = "/vm/nicinfo/duplicate"; public static final String VM_NIC_INFO_IPRANGE_CONFLICT_PATH = "/vm/nicinfo/iprangeConflict"; + public static final String VM_HOST_FILE_CHANGED_PATH = "/vm/hostfile/changed"; + public static final String KVM_REPORT_VM_HOST_FILE_CHANGED = "/kvm/reportvmhostfilechanged"; @NeedJsonSchema public static class VmCrashReportData { @@ -317,4 +320,35 @@ public void setInternalIp(String internalIp) { this.internalIp = internalIp; } } + + @NeedJsonSchema + public static class VmHostFileChangedData { + private String hostUuid; + private String vmUuid; + private List types; + + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + public String getVmUuid() { + return vmUuid; + } + + public void setVmUuid(String vmUuid) { + this.vmUuid = vmUuid; + } + + public List getTypes() { + return types; + } + + public void setTypes(List types) { + this.types = types; + } + } } 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 index 6db0005be9c..e229c49024c 100644 --- a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileInventory.java +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileInventory.java @@ -17,6 +17,8 @@ public class VmHostFileInventory { private String type; private String path; private String lastSyncReason; + private Timestamp changeDate; + private Timestamp lastSyncDate; private Timestamp createDate; private Timestamp lastOpDate; @@ -31,6 +33,8 @@ public static VmHostFileInventory valueOf(VmHostFileVO vo) { inv.setType(vo.getType().toString()); inv.setPath(vo.getPath()); inv.setLastSyncReason(vo.getLastSyncReason()); + inv.setChangeDate(vo.getChangeDate()); + inv.setLastSyncDate(vo.getLastSyncDate()); inv.setCreateDate(vo.getCreateDate()); inv.setLastOpDate(vo.getLastOpDate()); return inv; @@ -88,6 +92,22 @@ public void setLastSyncReason(String lastSyncReason) { this.lastSyncReason = lastSyncReason; } + public Timestamp getChangeDate() { + return changeDate; + } + + public void setChangeDate(Timestamp changeDate) { + this.changeDate = changeDate; + } + + public Timestamp getLastSyncDate() { + return lastSyncDate; + } + + public void setLastSyncDate(Timestamp lastSyncDate) { + this.lastSyncDate = lastSyncDate; + } + public Timestamp getCreateDate() { return createDate; } diff --git a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileSyncReason.java b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileSyncReason.java index 8745a8b3779..852f08e1aa1 100644 --- a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileSyncReason.java +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileSyncReason.java @@ -12,6 +12,8 @@ public enum VmHostFileSyncReason { RevertSnapshot("revert snapshot"), VolumeBackup("volume backup"), BeforeHaStart("on before HA start (from last host)"), + PeriodicDirtyCheck("on periodic dirty check"), + PeriodicForceSync("on periodic force sync"), ; public final String detail; 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 index 5a6b36c63dd..44f3d5423d1 100644 --- a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileVO.java +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileVO.java @@ -42,6 +42,10 @@ public class VmHostFileVO extends ResourceVO { @Column private String lastSyncReason; @Column + private Timestamp changeDate; + @Column + private Timestamp lastSyncDate; + @Column private Timestamp createDate; @Column private Timestamp lastOpDate; @@ -86,6 +90,22 @@ public void setLastSyncReason(String lastSyncReason) { this.lastSyncReason = lastSyncReason; } + public Timestamp getChangeDate() { + return changeDate; + } + + public void setChangeDate(Timestamp changeDate) { + this.changeDate = changeDate; + } + + public Timestamp getLastSyncDate() { + return lastSyncDate; + } + + public void setLastSyncDate(Timestamp lastSyncDate) { + this.lastSyncDate = lastSyncDate; + } + public Timestamp getCreateDate() { return createDate; } @@ -111,6 +131,8 @@ public String toString() { ", type=" + type + ", path='" + path + '\'' + ", lastSyncReason='" + lastSyncReason + '\'' + + ", changeDate=" + changeDate + + ", lastSyncDate=" + lastSyncDate + ", 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 index 38ee885375f..ab3546aca02 100644 --- a/header/src/main/java/org/zstack/header/vm/additions/VmHostFileVO_.java +++ b/header/src/main/java/org/zstack/header/vm/additions/VmHostFileVO_.java @@ -13,6 +13,8 @@ public class VmHostFileVO_ extends ResourceVO_ { public static volatile SingularAttribute type; public static volatile SingularAttribute path; public static volatile SingularAttribute lastSyncReason; + public static volatile SingularAttribute changeDate; + public static volatile SingularAttribute lastSyncDate; public static volatile SingularAttribute createDate; public static volatile SingularAttribute lastOpDate; } 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 8a92b56202b..4c2ff3a9175 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMGlobalConfig.java @@ -168,8 +168,8 @@ public class KVMGlobalConfig { public static GlobalConfig VM_EDK_VERSION_CONFIG = new GlobalConfig(CATEGORY, "vm.edk.version"); @GlobalConfigValidation(numberGreaterThan = 1, numberLessThan = 86400) - @GlobalConfigDef(defaultValue = "1800", type = Long.class, - description = "Interval in seconds for syncing VM host files (NvRam, TpmState) from KVM hosts") + @GlobalConfigDef(defaultValue = "15", type = Long.class, + description = "Interval in seconds for checking VM host files (NvRam, TpmState) on KVM hosts") public static GlobalConfig VM_HOST_FILE_SYNC_INTERVAL = new GlobalConfig(CATEGORY, "vm.host.file.sync.interval"); @GlobalConfigValidation(numberGreaterThan = 1, numberLessThan = 30) 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 68dc9379c60..45480bb7aed 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 @@ -12,6 +12,7 @@ import org.zstack.core.db.Q; import org.zstack.core.db.SQL; import org.zstack.core.db.SQLBatch; +import org.zstack.core.timeout.TimeHelper; import org.zstack.core.workflow.SimpleFlowChain; import org.zstack.header.AbstractService; import org.zstack.header.core.ReturnValueCompletion; @@ -101,6 +102,8 @@ public class KvmSecureBootManager extends AbstractService { private ResourceConfigFacade resourceConfigFacade; @Autowired private KvmVmHostFileFactory vmHostFileFactory; + @Autowired + private TimeHelper timeHelper; @Override public boolean start() { @@ -217,6 +220,7 @@ private void handle(SyncVmHostFilesFromHostMsg msg) { to.setType(VmHostFileType.NvRam.toString()); cmd.getHostFiles().add(to); } + long now = timeHelper.getCurrentTimeMillis(); SyncVmHostFilesFromHostReply reply = new SyncVmHostFilesFromHostReply(); sender.send(cmd, READ_VM_HOST_FILE_PATH, wrapper -> { @@ -237,7 +241,7 @@ public void success(KvmResponseWrapper wrapper) { if (msg.isSyncToBackup()) { error = syncToBackupFiles(msg, readRsp); } else { - error = syncToHostFiles(msg, cmd, readRsp); + error = syncToHostFiles(msg, cmd, readRsp, now); } if (error != null) { @@ -256,7 +260,8 @@ public void fail(ErrorCode errorCode) { private ErrorCode syncToHostFiles(SyncVmHostFilesFromHostMsg msg, KVMAgentCommands.ReadVmHostFileContentCmd cmd, - KVMAgentCommands.ReadVmHostFileContentResponse readRsp) { + KVMAgentCommands.ReadVmHostFileContentResponse readRsp, + long timeBeforeSync) { final List existsFiles = Q.New(VmHostFileVO.class) .eq(VmHostFileVO_.vmInstanceUuid, msg.getVmUuid()) .eq(VmHostFileVO_.hostUuid, msg.getHostUuid()) @@ -272,6 +277,7 @@ private ErrorCode syncToHostFiles(SyncVmHostFilesFromHostMsg msg, existsContentUuid = Collections.emptyList(); } + Timestamp syncTime = new Timestamp(timeBeforeSync); List errors = new ArrayList<>(); for (String path : cmd.getPaths()) { KVMAgentCommands.VmHostFileTO to = findOneOrNull(readRsp.getHostFiles(), item -> item.getPath().equals(path)); @@ -291,13 +297,25 @@ private ErrorCode syncToHostFiles(SyncVmHostFilesFromHostMsg msg, 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) - .set(VmHostFileVO_.lastSyncReason, msg.getSyncReason()) - .update(); + String fileUuid = file.getUuid(); + new SQLBatch() { + @Override + protected void scripts() { + sql(VmHostFileVO.class) + .eq(VmHostFileVO_.uuid, fileUuid) + .set(VmHostFileVO_.lastSyncReason, msg.getSyncReason()) + .set(VmHostFileVO_.lastSyncDate, syncTime) + .set(VmHostFileVO_.lastOpDate, syncTime) + .update(); + sql(VmHostFileVO.class) + .eq(VmHostFileVO_.uuid, fileUuid) + .lt(VmHostFileVO_.changeDate, syncTime) + .set(VmHostFileVO_.changeDate, null) // CAS update + .update(); + } + }.execute(); + } else { file = new VmHostFileVO(); file.setUuid(Platform.getUuid()); @@ -306,8 +324,10 @@ private ErrorCode syncToHostFiles(SyncVmHostFilesFromHostMsg msg, file.setPath(path); file.setType(type); file.setLastSyncReason(msg.getSyncReason()); - file.setCreateDate(now); - file.setLastOpDate(now); + file.setLastSyncDate(syncTime); + file.setChangeDate(null); + file.setLastOpDate(syncTime); + file.setCreateDate(syncTime); file.setResourceName(String.format("%s file for %s", type, msg.getVmUuid())); databaseFacade.persist(file); } @@ -318,15 +338,15 @@ private ErrorCode syncToHostFiles(SyncVmHostFilesFromHostMsg msg, .eq(VmHostFileContentVO_.uuid, file.getUuid()) .set(VmHostFileContentVO_.content, bytes) .set(VmHostFileContentVO_.format, VmHostFileContentFormat.valueOf(to.getFileFormat())) - .set(VmHostFileContentVO_.lastOpDate, now) + .set(VmHostFileContentVO_.lastOpDate, syncTime) .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); + content.setCreateDate(syncTime); + content.setLastOpDate(syncTime); databaseFacade.persist(content); } @@ -489,7 +509,7 @@ public void run(FlowTrigger trigger, Map data) { VmHostFileVO file = Q.New(VmHostFileVO.class) .eq(VmHostFileVO_.vmInstanceUuid, msg.getSrcVmUuid()) .eq(VmHostFileVO_.type, type) - .orderByDesc(VmHostFileVO_.lastOpDate) + .orderByDesc(VmHostFileVO_.lastSyncDate) .limit(1) .find(); if (file == null) { @@ -884,6 +904,7 @@ public void fail(ErrorCode errorCode) { .eq(VmHostFileVO_.uuid, currentFile.getUuid()) .set(VmHostFileVO_.lastSyncReason, Restore.reason(msg.getSyncReason())) .set(VmHostFileVO_.lastOpDate, now) + .set(VmHostFileVO_.lastSyncDate, now) .update(); VmHostFileContentVO existingContent = contentMap.get(currentFile.getUuid()); @@ -913,6 +934,7 @@ public void fail(ErrorCode errorCode) { newFile.setType(type); newFile.setPath(buildPathForVmHostFileType(type, msg.getVmInstanceUuid())); newFile.setLastSyncReason(Restore.reason(msg.getSyncReason())); + newFile.setLastSyncDate(now); newFile.setCreateDate(now); newFile.setLastOpDate(now); databaseFacade.persist(newFile); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java index 3a52a09f73c..4ba9269aba4 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java @@ -5,10 +5,13 @@ import org.zstack.core.asyncbatch.While; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.CloudBusCallBack; +import org.zstack.core.cloudbus.EventCallback; +import org.zstack.core.cloudbus.EventFacade; import org.zstack.core.cloudbus.ResourceDestinationMaker; import org.zstack.core.config.GlobalConfigUpdateExtensionPoint; import org.zstack.core.db.DatabaseFacade; import org.zstack.core.db.Q; +import org.zstack.core.db.SQL; import org.zstack.core.db.SQLBatch; import org.zstack.core.thread.PeriodicTask; import org.zstack.core.thread.ThreadFacade; @@ -21,6 +24,7 @@ import org.zstack.header.host.HostVO_; import org.zstack.header.message.MessageReply; import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO; +import org.zstack.header.vm.VmCanonicalEvents; import org.zstack.header.vm.VmInstanceAO_; import org.zstack.header.vm.VmInstanceConstant; import org.zstack.header.vm.VmInstanceEO; @@ -70,6 +74,8 @@ public class VmHostFileTracker implements Component { private DatabaseFacade dbf; @Autowired private TimeHelper timeHelper; + @Autowired + private EventFacade eventFacade; private Future trackerThread; private Future cleanupThread; @@ -83,6 +89,7 @@ public boolean start() { submitTrackerTask(); submitCleanupTask(); + setupCanonicalEvents(); GlobalConfigUpdateExtensionPoint listener = (oldConfig, newConfig) -> { logger.debug(String.format("%s changed from %s to %s, restarting vm-host-file-tracker", @@ -107,6 +114,52 @@ public boolean stop() { return true; } + private void setupCanonicalEvents() { + eventFacade.on(VmCanonicalEvents.VM_HOST_FILE_CHANGED_PATH, new EventCallback() { + @Override + protected void run(Map tokens, Object data) { + if (!(data instanceof VmCanonicalEvents.VmHostFileChangedData)) { + return; + } + + VmCanonicalEvents.VmHostFileChangedData d = (VmCanonicalEvents.VmHostFileChangedData) data; + String hostUuid = d.getHostUuid(); + String vmUuid = d.getVmUuid(); + List types = d.getTypes(); + + if (hostUuid == null || vmUuid == null || types == null || types.isEmpty()) { + logger.warn(String.format("received VM_HOST_FILE_CHANGED event with incomplete data: hostUuid=%s, vmUuid=%s", hostUuid, vmUuid)); + return; + } + + Timestamp now = new Timestamp(timeHelper.getCurrentTimeMillis()); + for (String type : types) { + VmHostFileType fileType; + try { + fileType = VmHostFileType.valueOf(type); + } catch (Exception e) { + logger.warn(String.format("ignore invalid vm host file type in event: vmUuid=%s, hostUuid=%s, type=%s", + vmUuid, hostUuid, type), e); + continue; + } + + long updated = SQL.New(VmHostFileVO.class) + .eq(VmHostFileVO_.hostUuid, hostUuid) + .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) + .eq(VmHostFileVO_.type, fileType) + .set(VmHostFileVO_.changeDate, now) + .update(); + + if (updated > 0) { + logger.debug(String.format("marked VmHostFile changed: vmUuid=%s, hostUuid=%s, type=%s", vmUuid, hostUuid, type)); + } else { + logger.warn(String.format("no VmHostFileVO found for vmUuid=%s, hostUuid=%s, type=%s", vmUuid, hostUuid, type)); + } + } + } + }); + } + private synchronized void submitTrackerTask() { if (trackerThread != null) { trackerThread.cancel(true); @@ -131,7 +184,7 @@ public String getName() { @Override public void run() { - logger.info("VmHostFileTracker: starting periodic sync of VM host files"); + logger.info("VmHostFileTracker: starting periodic check of VM host files"); syncVmHostFiles(); } }); @@ -193,6 +246,9 @@ private void syncVmHostFiles() { .collect(Collectors.groupingBy(f -> f.getVmInstanceUuid() + "::" + f.getHostUuid())); List> groups = new ArrayList<>(grouped.values()); + long now = timeHelper.getCurrentTimeMillis(); + long checkIntervalMs = KVMGlobalConfig.VM_HOST_FILE_SYNC_INTERVAL.value(Long.class) * 1000; + long forceSyncThresholdMs = checkIntervalMs * 100; int concurrency = KVMGlobalConfig.VM_HOST_FILE_SYNC_CONCURRENCY.value(Integer.class); new While<>(groups).step((group, whileCompletion) -> { @@ -205,9 +261,28 @@ private void syncVmHostFiles() { return; } + boolean hasChanged = group.stream().anyMatch(f -> f.getChangeDate() != null); + String syncReason; + if (hasChanged) { + syncReason = VmHostFileSyncReason.PeriodicDirtyCheck.reason(); + } else { + // check if force sync is needed based on lastSyncDate + Timestamp oldestLastSync = group.stream() + .map(VmHostFileVO::getLastSyncDate) + .min(Comparator.nullsFirst(Comparator.naturalOrder())) + .orElse(null); + + if (oldestLastSync != null && (now - oldestLastSync.getTime()) < forceSyncThresholdMs) { + whileCompletion.done(); + return; + } + syncReason = VmHostFileSyncReason.PeriodicForceSync.reason(); + } + SyncVmHostFilesFromHostMsg syncMsg = new SyncVmHostFilesFromHostMsg(); syncMsg.setHostUuid(hostUuid); syncMsg.setVmUuid(vmUuid); + syncMsg.setSyncReason(syncReason); for (VmHostFileVO file : group) { if (file.getType() == VmHostFileType.NvRam) { @@ -231,7 +306,7 @@ public void run(MessageReply reply) { }, concurrency).run(new WhileDoneCompletion(null) { @Override public void done(ErrorCodeList errorCodeList) { - // periodic sync round finished, empty callback + // periodic check round finished, empty callback } }); } diff --git a/sdk/src/main/java/org/zstack/sdk/vm/entity/VmHostFileInventory.java b/sdk/src/main/java/org/zstack/sdk/vm/entity/VmHostFileInventory.java index 35f14678602..555cf4b8d45 100644 --- a/sdk/src/main/java/org/zstack/sdk/vm/entity/VmHostFileInventory.java +++ b/sdk/src/main/java/org/zstack/sdk/vm/entity/VmHostFileInventory.java @@ -52,6 +52,22 @@ public java.lang.String getLastSyncReason() { return this.lastSyncReason; } + public java.sql.Timestamp changeDate; + public void setChangeDate(java.sql.Timestamp changeDate) { + this.changeDate = changeDate; + } + public java.sql.Timestamp getChangeDate() { + return this.changeDate; + } + + public java.sql.Timestamp lastSyncDate; + public void setLastSyncDate(java.sql.Timestamp lastSyncDate) { + this.lastSyncDate = lastSyncDate; + } + public java.sql.Timestamp getLastSyncDate() { + return this.lastSyncDate; + } + public java.sql.Timestamp createDate; public void setCreateDate(java.sql.Timestamp createDate) { this.createDate = createDate; From ab338a6086362d0590d0423043184888f31419ec Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 7 Apr 2026 10:44:19 +0800 Subject: [PATCH 115/129] [kvm]: remove TPM from VM individually instead of batch delete Change the TPM deletion logic from batch SQL delete to individual deletion using While loop. This ensures proper cleanup for each TPM by calling removeTpmFromVm method. Added error handling to continue even if deletion fails for individual TPM. Changes: - Replace batch SQL delete with While loop iteration - Call removeTpmFromVm for each TPM to ensure proper cleanup - Add error handling to continue on individual failures - Log warnings for failed TPM deletions Resolves: ZSV-11439 Related: ZSV-11310 Change-Id: I616d746d6a6f677a6772796f63676e6177676371 --- .../org/zstack/kvm/tpm/KvmTpmManager.java | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) 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 4bbc5ba1477..a06a03aa82e 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 @@ -305,6 +305,10 @@ public String getName() { static class RemoveTpmFromVmContext { String vmInstanceUuid; String tpmUuid; + + // enable when TPM delete/VM delete ... + boolean force; + List hostFiles; static RemoveTpmFromVmContext valueOf(RemoveTpmMsg msg) { @@ -318,6 +322,7 @@ static RemoveTpmFromVmContext valueOf(RemoveTpmMsg msg) { private void removeTpmFromVm(RemoveTpmFromVmContext context, Completion completion) { SimpleFlowChain.of("remove-tpm-from-vm-" + context.vmInstanceUuid) .then(Flow.of("check-vm-status") + .skipIf(data -> context.force) .handle(trigger -> { VmInstanceVO vm = Q.New(VmInstanceVO.class) .eq(VmInstanceVO_.uuid, context.vmInstanceUuid) @@ -457,10 +462,31 @@ public void rollback(FlowRollback trigger, Map data) { trigger.rollback(); return; } - SQL.New(TpmVO.class) - .in(TpmVO_.uuid, transform(reply.getInventories(), TpmInventory::getUuid)) - .delete(); - trigger.rollback(); + + new While<>(reply.getInventories()).each((tpm, whileCompletion) -> { + RemoveTpmFromVmContext removeContext = new RemoveTpmFromVmContext(); + removeContext.vmInstanceUuid = tpm.getVmInstanceUuid(); + removeContext.tpmUuid = tpm.getUuid(); + removeContext.force = true; + removeTpmFromVm(removeContext, new Completion(whileCompletion) { + @Override + public void success() { + whileCompletion.done(); + } + + @Override + public void fail(ErrorCode errorCode) { + logger.warn(String.format("failed to delete tpm for VM[%s] but still continue: %s", + tpm.getVmInstanceUuid(), errorCode.getReadableDetails())); + whileCompletion.done(); + } + }); + }).run(new WhileDoneCompletion(trigger) { + @Override + public void done(ErrorCodeList errorCodeList) { + trigger.rollback(); + } + }); } }).then(new NoRollbackFlow() { String __name__ = "clone-encrypted-resource-key-if-needed"; From 866fc0f2bd0bd7cf032ec116c90b1317c6b047b6 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Tue, 7 Apr 2026 18:45:44 +0800 Subject: [PATCH 116/129] [kvm]: support TPM revert without KMS Support TPM snapshot group revert when ALLOWED_TPM_VM_WITHOUT_KMS config is enabled. Skip key provider resolution to allow TPM recovery without KMS dependency. Changes: - Add ALLOWED_TPM_VM_WITHOUT_KMS config check - Skip key provider resolution when config is true Resolves: ZSV-11489 Related: ZSV-11310 Change-Id: I6470766e66656f6464767a7776696a7a6979706b --- .../kvm/tpm/SnapshotGroupRevertTpmHelper.java | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/SnapshotGroupRevertTpmHelper.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/SnapshotGroupRevertTpmHelper.java index 26512f19d49..00592c249d6 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/SnapshotGroupRevertTpmHelper.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/SnapshotGroupRevertTpmHelper.java @@ -23,6 +23,8 @@ import java.util.List; +import static org.zstack.compute.vm.VmGlobalConfig.ALLOWED_TPM_VM_WITHOUT_KMS; + @Configurable(preConstruction = true, autowire = Autowire.BY_TYPE) public class SnapshotGroupRevertTpmHelper { private static final CLogger logger = Utils.getLogger(SnapshotGroupRevertTpmHelper.class); @@ -98,23 +100,25 @@ public void setupFromApi(APICreateVmInstanceFromVolumeSnapshotGroupMsg apiMsg, C tpmSpec.setBackupFileUuid(tpmBackupFile.getUuid()); } - String keyProviderName = KVMSystemTags.TPM_KEY_PROVIDER_NAME - .getTokenByResourceUuid(tpmBackupFile.getUuid(), KVMSystemTags.TPM_KEY_PROVIDER_NAME_TOKEN); - if (keyProviderName == null) { - logger.warn(String.format( - "failed to find keyProvider from snapshotGroup[uuid:%s] by tpmBackupFile[uuid:%s]", - snapshotGroupUuid, tpmBackupFile.getUuid())); - if (tpmSpec.getKeyProviderUuid() == null) { - tpmSpec.setKeyProviderUuid(tpmKeyBackend.defaultKeyProviderUuid()); // maybe null - } - } else { - String keyProviderUuid = tpmKeyBackend.findKeyProviderUuidByName(keyProviderName); - if (keyProviderUuid == null) { + if (ALLOWED_TPM_VM_WITHOUT_KMS.value(Boolean.class) != Boolean.TRUE) { + String keyProviderName = KVMSystemTags.TPM_KEY_PROVIDER_NAME + .getTokenByResourceUuid(tpmBackupFile.getUuid(), KVMSystemTags.TPM_KEY_PROVIDER_NAME_TOKEN); + if (keyProviderName == null) { logger.warn(String.format( - "failed to resolve keyProvider[name:%s] from snapshotGroup[uuid:%s] by tpmBackupFile[uuid:%s], keep keyProviderUuid unset", - keyProviderName, snapshotGroupUuid, tpmBackupFile.getUuid())); + "failed to find keyProvider from snapshotGroup[uuid:%s] by tpmBackupFile[uuid:%s]", + snapshotGroupUuid, tpmBackupFile.getUuid())); + if (tpmSpec.getKeyProviderUuid() == null) { + tpmSpec.setKeyProviderUuid(tpmKeyBackend.defaultKeyProviderUuid()); // maybe null + } } else { - tpmSpec.setKeyProviderUuid(keyProviderUuid); + String keyProviderUuid = tpmKeyBackend.findKeyProviderUuidByName(keyProviderName); + if (keyProviderUuid == null) { + logger.warn(String.format( + "failed to resolve keyProvider[name:%s] from snapshotGroup[uuid:%s] by tpmBackupFile[uuid:%s], keep keyProviderUuid unset", + keyProviderName, snapshotGroupUuid, tpmBackupFile.getUuid())); + } else { + tpmSpec.setKeyProviderUuid(keyProviderUuid); + } } } } From 4f8ecd126d2c40da4237fe39b93aa67ea6143fdb Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Tue, 7 Apr 2026 20:38:43 +0800 Subject: [PATCH 117/129] [metadata]: extractOldPrefix use lastIndexOf Resolves: ZSV-11771 Change-Id: I801a5f4d8ba84e47a642be4043043af88979dd4e --- .../metadata/VmInstanceMetadataConstants.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java b/header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java index efdeb5f8bb2..498449d22c9 100644 --- a/header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java +++ b/header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java @@ -12,19 +12,28 @@ private VmInstanceMetadataConstants() { "/rootVolumes/", "/dataVolumes/", "/volumeSnapshots/", "/memory/" }; + /** + * Extract the storage mount-point prefix from a volume install path. + *
+     *   /mnt/ps/rootVolumes/acct-xxx/vol-xxx/xxx.qcow2  →  /mnt/ps/
+     *   /local_ps/dataVolumes/acct-xxx/vol-xxx/xxx.qcow2 →  /local_ps/
+     * 
+ * Uses {@code lastIndexOf} so that mount paths which themselves contain a + * marker string (e.g. {@code /rootVolumes/rootVolumes/…}) are handled + * correctly — the last occurrence is always the real subdirectory boundary. + */ public static String extractOldPrefix(String path) { if (path == null || !path.startsWith("/")) { return null; } - int earliest = Integer.MAX_VALUE; - String foundMarker = null; + + int latest = -1; for (String marker : STORAGE_PATH_MARKERS) { - int idx = path.indexOf(marker); - if (idx >= 0 && idx < earliest) { - earliest = idx; - foundMarker = marker; + int idx = path.lastIndexOf(marker); + if (idx > latest) { + latest = idx; } } - return foundMarker == null ? null : path.substring(0, earliest + foundMarker.length()); + return latest < 0 ? null : path.substring(0, latest + 1); } } \ No newline at end of file From 3ffe6906e81c768204b73b5bda71c48b221b02f3 Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Wed, 8 Apr 2026 11:13:36 +0800 Subject: [PATCH 118/129] [storage]: update APIScanVmInstanceMetadataFromPrimaryStorageMsg Let APIScanVmInstanceMetadataFromPrimaryStorageMsg extend APISyncCallMessage APIImpact Resolves: ZSV-11790 Change-Id: I6b796372747178757767646b77676d646361636f --- ...InstanceMetadataFromPrimaryStorageMsg.java | 6 +++-- ...adataFromPrimaryStorageMsgDoc_zh_cn.groovy | 4 ++-- ...tanceMetadataFromPrimaryStorageReply.java} | 16 ++++---------- ...taFromPrimaryStorageReplyDoc_zh_cn.groovy} | 5 ++--- ...tanceMetadataFromPrimaryStorageAction.java | 8 +------ .../storage/primary/PrimaryStorageBase.java | 22 +++++++++---------- 6 files changed, 24 insertions(+), 37 deletions(-) rename header/src/main/java/org/zstack/header/storage/primary/{APIScanVmInstanceMetadataFromPrimaryStorageEvent.java => APIScanVmInstanceMetadataFromPrimaryStorageReply.java} (52%) rename header/src/main/java/org/zstack/header/storage/primary/{APIScanVmInstanceMetadataFromPrimaryStorageEventDoc_zh_cn.groovy => APIScanVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy} (89%) diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsg.java b/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsg.java index 411a568d5a0..af40d1d7ad7 100644 --- a/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsg.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsg.java @@ -3,14 +3,16 @@ import org.springframework.http.HttpMethod; import org.zstack.header.message.APIMessage; import org.zstack.header.message.APIParam; +import org.zstack.header.message.APISyncCallMessage; +import org.zstack.header.message.DefaultTimeout; import org.zstack.header.rest.RestRequest; @RestRequest( path = "/primary-storage/vm-instances/metadata/scan", method = HttpMethod.GET, - responseClass = APIScanVmInstanceMetadataFromPrimaryStorageEvent.class + responseClass = APIScanVmInstanceMetadataFromPrimaryStorageReply.class ) -public class APIScanVmInstanceMetadataFromPrimaryStorageMsg extends APIMessage implements PrimaryStorageMessage { +public class APIScanVmInstanceMetadataFromPrimaryStorageMsg extends APISyncCallMessage implements PrimaryStorageMessage { @APIParam(resourceType = PrimaryStorageVO.class) private String uuid; diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy index 090f2a36075..ccb81445cf1 100644 --- a/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy +++ b/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy @@ -1,6 +1,6 @@ package org.zstack.header.storage.primary -import org.zstack.header.storage.primary.APIScanVmInstanceMetadataFromPrimaryStorageEvent +import org.zstack.header.storage.primary.APIScanVmInstanceMetadataFromPrimaryStorageReply doc { title "扫描主存储上的云主机元数据" @@ -52,7 +52,7 @@ doc { } response { - clz APIScanVmInstanceMetadataFromPrimaryStorageEvent.class + clz APIScanVmInstanceMetadataFromPrimaryStorageReply.class } } } \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEvent.java b/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageReply.java similarity index 52% rename from header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEvent.java rename to header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageReply.java index f3c5adad2fc..182687a89cf 100644 --- a/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEvent.java +++ b/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageReply.java @@ -1,23 +1,15 @@ package org.zstack.header.storage.primary; -import org.zstack.header.message.APIEvent; +import org.zstack.header.message.APIReply; import org.zstack.header.rest.RestResponse; import java.util.ArrayList; import java.util.List; @RestResponse(fieldsTo = {"all"}) -public class APIScanVmInstanceMetadataFromPrimaryStorageEvent extends APIEvent { +public class APIScanVmInstanceMetadataFromPrimaryStorageReply extends APIReply { private List vmInstanceMetadata = new ArrayList<>(); - public APIScanVmInstanceMetadataFromPrimaryStorageEvent() { - super(null); - } - - public APIScanVmInstanceMetadataFromPrimaryStorageEvent(String apiId) { - super(apiId); - } - public List getVmInstanceMetadata() { return vmInstanceMetadata; } @@ -26,8 +18,8 @@ public void setVmInstanceMetadata(List vmInstanceMetadata) this.vmInstanceMetadata = vmInstanceMetadata == null ? new ArrayList<>() : vmInstanceMetadata; } - public static APIScanVmInstanceMetadataFromPrimaryStorageEvent __example__() { - APIScanVmInstanceMetadataFromPrimaryStorageEvent evt = new APIScanVmInstanceMetadataFromPrimaryStorageEvent(); + public static APIScanVmInstanceMetadataFromPrimaryStorageReply __example__() { + APIScanVmInstanceMetadataFromPrimaryStorageReply evt = new APIScanVmInstanceMetadataFromPrimaryStorageReply(); return evt; } } diff --git a/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEventDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy similarity index 89% rename from header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEventDoc_zh_cn.groovy rename to header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy index 692e48f08a0..3db2650faff 100644 --- a/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEventDoc_zh_cn.groovy +++ b/header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy @@ -1,6 +1,5 @@ package org.zstack.header.storage.primary - import org.zstack.header.errorcode.ErrorCode doc { @@ -9,7 +8,7 @@ doc { ref { name "vmInstanceMetadata" - path "org.zstack.header.storage.primary.APIScanVmInstanceMetadataFromPrimaryStorageEvent.vmInstanceMetadata" + path "org.zstack.header.storage.primary.APIScanVmInstanceMetadataFromPrimaryStorageReply.vmInstanceMetadata" desc "云主机元数据摘要列表" type "List" since "5.0.0" @@ -23,7 +22,7 @@ doc { } ref { name "error" - path "org.zstack.header.storage.primary.APIScanVmInstanceMetadataFromPrimaryStorageEvent.error" + path "org.zstack.header.storage.primary.APIScanVmInstanceMetadataFromPrimaryStorageReply.error" desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null" type "ErrorCode" since "5.0.0" diff --git a/sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageAction.java b/sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageAction.java index bdb11d011bc..64984c03149 100644 --- a/sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageAction.java +++ b/sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageAction.java @@ -46,12 +46,6 @@ public Result throwExceptionIfError() { @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(); @@ -93,7 +87,7 @@ protected RestInfo getRestInfo() { info.httpMethod = "GET"; info.path = "/primary-storage/vm-instances/metadata/scan"; info.needSession = true; - info.needPoll = true; + info.needPoll = false; info.parameterName = ""; return info; } 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 0312aa7c028..1aa4922a99e 100755 --- a/storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java +++ b/storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java @@ -1877,25 +1877,25 @@ private static String getDeduplicateError(String operationName) { } private void handle(APIScanVmInstanceMetadataFromPrimaryStorageMsg msg) { - APIScanVmInstanceMetadataFromPrimaryStorageEvent evt = new APIScanVmInstanceMetadataFromPrimaryStorageEvent(msg.getId()); + APIScanVmInstanceMetadataFromPrimaryStorageReply reply = new APIScanVmInstanceMetadataFromPrimaryStorageReply(); if (self.getStatus() != PrimaryStorageStatus.Connected) { - evt.setError(Platform.operr("primary storage[uuid:%s] is not Connected (status=%s), cannot scan metadata", + reply.setError(Platform.operr("primary storage[uuid:%s] is not Connected (status=%s), cannot scan metadata", self.getUuid(), self.getStatus())); - bus.publish(evt); + bus.reply(msg, reply); return; } String psType = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.type).eq(PrimaryStorageVO_.uuid, msg.getPrimaryStorageUuid()).findValue(); if (psType == null) { - evt.setError(Platform.operr("primary storage[uuid:%s] not found", msg.getPrimaryStorageUuid())); - bus.publish(evt); + reply.setError(Platform.operr("primary storage[uuid:%s] not found", msg.getPrimaryStorageUuid())); + bus.reply(msg, reply); return; } VmMetadataPathBuildExtensionPoint ext = pluginRgty.getExtensionFromMap(psType, VmMetadataPathBuildExtensionPoint.class); if (ext == null) { - evt.setError(Platform.operr("primary storage type %s does not support metadata", psType)); - bus.publish(evt); + reply.setError(Platform.operr("primary storage type %s does not support metadata", psType)); + bus.reply(msg, reply); return; } String metadataDir = ext.buildMetadataDir(msg.getPrimaryStorageUuid()); @@ -1908,8 +1908,8 @@ private void handle(APIScanVmInstanceMetadataFromPrimaryStorageMsg msg) { @Override public void run(MessageReply r) { if (!r.isSuccess()) { - evt.setError(r.getError()); - bus.publish(evt); + reply.setError(r.getError()); + bus.reply(msg, reply); return; } ScanVmInstanceMetadataFromPrimaryStorageReply re = r.castReply(); @@ -1922,8 +1922,8 @@ public void run(MessageReply r) { .filter(e -> e.getVmCategory() != null) .filter(e -> !VmMetadataCategory.VM_TEMPLATE_CACHE.name().equals(e.getVmCategory())) .collect(Collectors.toList()); - evt.setVmInstanceMetadata(filtered); - bus.publish(evt); + reply.setVmInstanceMetadata(filtered); + bus.reply(msg, reply); } }); } From 5b3aa25644b041cc3a29727868c591713535f5c8 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Wed, 8 Apr 2026 17:10:40 +0800 Subject: [PATCH 119/129] [kvm]: fix NVRAM migration handler duplicate call Move NVRAM preparation logic from preVmMigration to preMigrateVm to avoid handling twice during VM migration. Remove migration type checks since the logic should apply to all migration scenarios. Resolves: ZSV-11438 Related: ZSV-11310 Change-Id: I6578706d6f79696578787079626f6b72616a6a7a --- .../org/zstack/kvm/efi/KvmSecureBootExtensions.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 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 b84bd996211..35ff290762a 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 @@ -81,8 +81,6 @@ 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.header.vm.VmMigrationType.PrimaryStorageMigration; import static org.zstack.header.vm.additions.VmHostFileSyncReason.PostMigration; import static org.zstack.header.vm.additions.VmHostFileSyncReason.BeforeHaStart; import static org.zstack.header.vm.additions.VmHostFileSyncReason.PrepareReRead; @@ -168,17 +166,12 @@ private void prepareNvRamToStartVmCmd(KVMAgentCommands.StartVmCmd cmd, NvRamSpec @Override public void preMigrateVm(VmInstanceInventory inv, String destHostUuid, Completion completion) { - completion.success(); // use preVmMigration instead of preMigrateVm to prevent from handle twice + prepareNvRamBeforeMigration(inv, destHostUuid, completion); } @Override public void preVmMigration(VmInstanceInventory vm, VmMigrationType type, String dstHostUuid, Completion completion) { - if (HostMigration != type && PrimaryStorageMigration != type) { - completion.success(); - return; - } - - prepareNvRamBeforeMigration(vm, dstHostUuid, completion); + completion.success(); // use preMigrateVm instead of preVmMigration to prevent from handle twice } private void prepareNvRamBeforeMigration(VmInstanceInventory vm, String dstHostUuid, Completion completion) { From 7ebe0f946e91df3cddbdbb80aaac4a8b52119150 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Wed, 8 Apr 2026 11:25:20 +0800 Subject: [PATCH 120/129] [kvm]: clone resource ref when revert snapshot Resolves: ZSV-11631 Change-Id: I616a776e78666179637976616c776162616c6533 --- .../org/zstack/kvm/tpm/KvmTpmExtensions.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) 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 11ba25bf55f..c7456771138 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 @@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.zstack.compute.vm.VmGlobalConfig; import org.zstack.compute.vm.devices.TpmEncryptedResourceKeyBackend; +import org.zstack.compute.vm.devices.TpmEncryptedResourceKeyBackend.CloneEncryptedResourceKeyContext; import org.zstack.core.Platform; import org.zstack.core.cloudbus.CloudBus; import org.zstack.core.cloudbus.CloudBusCallBack; @@ -15,6 +16,8 @@ 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.Flow; +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; @@ -24,12 +27,17 @@ import org.zstack.header.keyprovider.EncryptedResourceKeyManager.GetOrCreateResourceKeyContext; import org.zstack.header.keyprovider.EncryptedResourceKeyManager.ResourceKeyResult; import org.zstack.header.secret.SecretHostDefineMsg; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO_; 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.secret.SecretHostDefineReply; 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.VmHostFileType; import org.zstack.header.vm.additions.VmHostFileVO; import org.zstack.header.vm.additions.VmHostFileVO_; @@ -167,6 +175,53 @@ public void fail(ErrorCode errorCode) { } }); } + }).then(new Flow() { + String __name__ = "clone-tpm-resource-key-from-snapshot-source"; + + @Override + public boolean skip(Map data) { + return StringUtils.isBlank(findSourceTpmUuidFromSnapshotTpmBackupFile(context.backupFileUuid)) || + VmGlobalConfig.ALLOWED_TPM_VM_WITHOUT_KMS.value(Boolean.class); + } + + @Override + public void run(FlowTrigger trigger, Map data) { + String srcTpmUuid = findSourceTpmUuidFromSnapshotTpmBackupFile(context.backupFileUuid); + if (StringUtils.isBlank(srcTpmUuid)) { + trigger.next(); + return; + } + CloneEncryptedResourceKeyContext cloneCtx = new CloneEncryptedResourceKeyContext(); + cloneCtx.srcTpmUuid = srcTpmUuid; + cloneCtx.dstTpmUuid = context.tpmUuid; + cloneCtx.resetTpm = false; + resourceKeyBackend.cloneEncryptedResourceKey(cloneCtx, new Completion(trigger) { + @Override + public void success() { + context.providerUuid = resourceKeyBackend.findKeyProviderUuidByTpm(context.tpmUuid); + context.providerName = resourceKeyBackend.findKeyProviderNameByTpm(context.tpmUuid); + trigger.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + trigger.fail(errorCode); + } + }); + } + + @Override + public void rollback(FlowRollback trigger, Map data) { + if (StringUtils.isNotBlank(context.backupFileUuid)) { + try { + resourceKeyBackend.detachKeyProviderFromTpm(context.tpmUuid); + } catch (Exception e) { + logger.warn(String.format("failed to detach key provider ref for tpm[uuid:%s]: %s", + context.tpmUuid, e.getMessage())); + } + } + trigger.rollback(); + } }).then(new NoRollbackFlow() { String __name__ = "create-dek"; @@ -325,4 +380,28 @@ private void clearRollbackInfo(VmInstanceSpec spec) { spec.getDevicesSpec().getTpm().setResourceKeyCreatedNew(false); spec.getDevicesSpec().getTpm().setResourceKeyProviderUuid(null); } + + private String findSourceTpmUuidFromSnapshotTpmBackupFile(String tpmBackupFileUuid) { + if (StringUtils.isBlank(tpmBackupFileUuid)) { + return null; + } + VmHostBackupFileVO bf = Q.New(VmHostBackupFileVO.class) + .eq(VmHostBackupFileVO_.uuid, tpmBackupFileUuid) + .eq(VmHostBackupFileVO_.type, VmHostFileType.TpmState) + .find(); + if (bf == null || StringUtils.isBlank(bf.getResourceUuid())) { + return null; + } + String sourceVmUuid = Q.New(VolumeSnapshotGroupVO.class) + .select(VolumeSnapshotGroupVO_.vmInstanceUuid) + .eq(VolumeSnapshotGroupVO_.uuid, bf.getResourceUuid()) + .findValue(); + if (StringUtils.isBlank(sourceVmUuid)) { + return null; + } + return Q.New(TpmVO.class) + .eq(TpmVO_.vmInstanceUuid, sourceVmUuid) + .select(TpmVO_.uuid) + .findValue(); + } } From 5aeabece5d6e6ab6da0cf14f2f00aa11cfb5b421 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Wed, 8 Apr 2026 21:56:01 +0800 Subject: [PATCH 121/129] [sdk]: align sdk and tests Sync SDK rekey result schema to providerResults and add RekeyProviderResult model. Update premium rekey test assertions for provider-level counts and unbound-provider split behavior. Resolves: ZSV-11757 Change-Id: I68676e706f6a786773627074636f6e6a617a6e44 --- sdk/src/main/java/SourceClassMap.java | 2 + .../api/RekeyKeyProviderRefsResult.java | 18 ++--- .../keyprovider/api/RekeyProviderResult.java | 71 +++++++++++++++++++ 3 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyProviderResult.java diff --git a/sdk/src/main/java/SourceClassMap.java b/sdk/src/main/java/SourceClassMap.java index 6820b518a7f..86c9a8050b5 100644 --- a/sdk/src/main/java/SourceClassMap.java +++ b/sdk/src/main/java/SourceClassMap.java @@ -71,6 +71,7 @@ public class SourceClassMap { put("org.zstack.crypto.ccs.CCSCertificateAccountRefInventory", "org.zstack.sdk.CCSCertificateAccountRefInventory"); put("org.zstack.crypto.ccs.CCSCertificateInventory", "org.zstack.sdk.CCSCertificateInventory"); put("org.zstack.crypto.keyprovider.api.RekeyFailedResource", "org.zstack.sdk.keyprovider.api.RekeyFailedResource"); + put("org.zstack.crypto.keyprovider.api.RekeyProviderResult", "org.zstack.sdk.keyprovider.api.RekeyProviderResult"); put("org.zstack.crypto.keyprovider.api.RekeySkippedResource", "org.zstack.sdk.keyprovider.api.RekeySkippedResource"); put("org.zstack.crypto.securitymachine.thirdparty.aisino.AiSiNoSecretResourcePoolInventory", "org.zstack.sdk.AiSiNoSecretResourcePoolInventory"); put("org.zstack.crypto.securitymachine.thirdparty.flkSec.FlkSecSecretResourcePoolInventory", "org.zstack.sdk.FlkSecSecretResourcePoolInventory"); @@ -1269,6 +1270,7 @@ public class SourceClassMap { put("org.zstack.sdk.identity.role.RoleAccountRefInventory", "org.zstack.header.identity.role.RoleAccountRefInventory"); put("org.zstack.sdk.identity.role.RoleInventory", "org.zstack.header.identity.role.RoleInventory"); put("org.zstack.sdk.keyprovider.api.RekeyFailedResource", "org.zstack.crypto.keyprovider.api.RekeyFailedResource"); + put("org.zstack.sdk.keyprovider.api.RekeyProviderResult", "org.zstack.crypto.keyprovider.api.RekeyProviderResult"); put("org.zstack.sdk.keyprovider.api.RekeySkippedResource", "org.zstack.crypto.keyprovider.api.RekeySkippedResource"); put("org.zstack.sdk.license.entity.LicenseHistoryInventory", "org.zstack.license.entity.LicenseHistoryInventory"); put("org.zstack.sdk.license.entity.LicenseUsageView", "org.zstack.license.entity.LicenseUsageView"); 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 index b697dc044c3..32a5c938b10 100644 --- a/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsResult.java +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyKeyProviderRefsResult.java @@ -35,20 +35,12 @@ public int getFailedCount() { return this.failedCount; } - public java.util.List skippedResources; - public void setSkippedResources(java.util.List skippedResources) { - this.skippedResources = skippedResources; + public java.util.List providerResults; + public void setProviderResults(java.util.List providerResults) { + this.providerResults = providerResults; } - public java.util.List getSkippedResources() { - return this.skippedResources; - } - - public java.util.List failedResources; - public void setFailedResources(java.util.List failedResources) { - this.failedResources = failedResources; - } - public java.util.List getFailedResources() { - return this.failedResources; + public java.util.List getProviderResults() { + return this.providerResults; } } diff --git a/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyProviderResult.java b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyProviderResult.java new file mode 100644 index 00000000000..bbebfaa0fe0 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/keyprovider/api/RekeyProviderResult.java @@ -0,0 +1,71 @@ +package org.zstack.sdk.keyprovider.api; + + + +public class RekeyProviderResult { + + public java.lang.String providerUuid; + public void setProviderUuid(java.lang.String providerUuid) { + this.providerUuid = providerUuid; + } + public java.lang.String getProviderUuid() { + return this.providerUuid; + } + + public java.lang.String providerName; + public void setProviderName(java.lang.String providerName) { + this.providerName = providerName; + } + public java.lang.String getProviderName() { + return this.providerName; + } + + public int totalRefCount; + public void setTotalRefCount(int totalRefCount) { + this.totalRefCount = totalRefCount; + } + public int getTotalRefCount() { + return this.totalRefCount; + } + + public int successRefCount; + public void setSuccessRefCount(int successRefCount) { + this.successRefCount = successRefCount; + } + public int getSuccessRefCount() { + return this.successRefCount; + } + + public int skippedRefCount; + public void setSkippedRefCount(int skippedRefCount) { + this.skippedRefCount = skippedRefCount; + } + public int getSkippedRefCount() { + return this.skippedRefCount; + } + + public int failedRefCount; + public void setFailedRefCount(int failedRefCount) { + this.failedRefCount = failedRefCount; + } + public int getFailedRefCount() { + return this.failedRefCount; + } + + public java.util.List skippedResources; + public void setSkippedResources(java.util.List skippedResources) { + this.skippedResources = skippedResources; + } + public java.util.List getSkippedResources() { + return this.skippedResources; + } + + public java.util.List failedResources; + public void setFailedResources(java.util.List failedResources) { + this.failedResources = failedResources; + } + public java.util.List getFailedResources() { + return this.failedResources; + } + +} From 52600df772d17061c234e80ddecedbbc4053c5be Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Mon, 6 Apr 2026 17:39:06 +0800 Subject: [PATCH 122/129] [crypto]: secret get in vm pre instantiate Resolves: ZSV-11630 Change-Id: I616a776e78666179637976616c776162616c6577 --- .../DummyTpmEncryptedResourceKeyBackend.java | 5 + .../TpmEncryptedResourceKeyBackend.java | 5 + .../EncryptedResourceKeyManager.java | 9 ++ .../header/secret/SecretHostDefineMsg.java | 16 +-- .../header/secret/SecretHostGetMsg.java | 47 ++++++++ .../header/secret/SecretHostGetReply.java | 18 +++ .../java/org/zstack/kvm/KVMAgentCommands.java | 52 ++++++++- .../main/java/org/zstack/kvm/KVMConstant.java | 1 + .../src/main/java/org/zstack/kvm/KVMHost.java | 66 +++++++++-- .../org/zstack/kvm/tpm/KvmTpmExtensions.java | 110 +++++++++++++++--- .../org/zstack/testlib/KVMSimulator.groovy | 24 +++- 11 files changed, 315 insertions(+), 38 deletions(-) create mode 100644 header/src/main/java/org/zstack/header/secret/SecretHostGetMsg.java create mode 100644 header/src/main/java/org/zstack/header/secret/SecretHostGetReply.java 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 01b577d80ad..ebe04946506 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 @@ -33,6 +33,11 @@ public String findKeyProviderNameByTpm(String tpmUuid) { return null; } + @Override + public Integer findKeyVersionByTpm(String tpmUuid) { + return null; + } + @Override public String defaultKeyProviderUuid() { return null; 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 2992af5ec7a..4189641756d 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 @@ -35,6 +35,11 @@ public interface TpmEncryptedResourceKeyBackend { */ String findKeyProviderNameByTpm(String tpmUuid); + /** + * maybe null (when crypto module is not installed) + */ + Integer findKeyVersionByTpm(String tpmUuid); + /** * maybe null (when crypto module is not installed) */ diff --git a/header/src/main/java/org/zstack/header/keyprovider/EncryptedResourceKeyManager.java b/header/src/main/java/org/zstack/header/keyprovider/EncryptedResourceKeyManager.java index ecf37e08b94..8a61c02ce83 100644 --- a/header/src/main/java/org/zstack/header/keyprovider/EncryptedResourceKeyManager.java +++ b/header/src/main/java/org/zstack/header/keyprovider/EncryptedResourceKeyManager.java @@ -87,6 +87,7 @@ class ResourceKeyResult { private String resourceType; private String keyProviderUuid; private String keyProviderName; + private Integer keyVersion; private String dekBase64; private String secretRef; private boolean createdNewKey; @@ -124,6 +125,14 @@ public void setKeyProviderName(String keyProviderName) { this.keyProviderName = keyProviderName; } + public Integer getKeyVersion() { + return keyVersion; + } + + public void setKeyVersion(Integer keyVersion) { + this.keyVersion = keyVersion; + } + public String getDekBase64() { return dekBase64; } 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 1997267b055..2421e304d8d 100644 --- a/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java +++ b/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java @@ -5,9 +5,9 @@ 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. - * vmUuid, purpose, providerName are required by key-agent for DEK cache key. + * Request to ensure secret on KVM host (for VM e.g. vTPM). + * Caller provides plaintext DEK (dekBase64), then host seals it with host public key + * and forwards the envelope to key-agent. */ public class SecretHostDefineMsg extends NeedReplyMessage implements HostMessage { private String hostUuid; @@ -15,7 +15,7 @@ public class SecretHostDefineMsg extends NeedReplyMessage implements HostMessage private String dekBase64; private String vmUuid; private String purpose; - private String providerName; + private Integer keyVersion; private String description; @Override @@ -51,12 +51,12 @@ public void setPurpose(String purpose) { this.purpose = purpose; } - public String getProviderName() { - return providerName; + public Integer getKeyVersion() { + return keyVersion; } - public void setProviderName(String providerName) { - this.providerName = providerName; + public void setKeyVersion(Integer keyVersion) { + this.keyVersion = keyVersion; } public String getDescription() { diff --git a/header/src/main/java/org/zstack/header/secret/SecretHostGetMsg.java b/header/src/main/java/org/zstack/header/secret/SecretHostGetMsg.java new file mode 100644 index 00000000000..ecb787956cb --- /dev/null +++ b/header/src/main/java/org/zstack/header/secret/SecretHostGetMsg.java @@ -0,0 +1,47 @@ +package org.zstack.header.secret; + +import org.zstack.header.host.HostMessage; +import org.zstack.header.message.NeedReplyMessage; + +/** + * Request to get an existing secret on KVM host by vmUuid, purpose and keyVersion. + */ +public class SecretHostGetMsg extends NeedReplyMessage implements HostMessage { + private String hostUuid; + private String vmUuid; + private String purpose; + private Integer keyVersion; + + @Override + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + 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 Integer getKeyVersion() { + return keyVersion; + } + + public void setKeyVersion(Integer keyVersion) { + this.keyVersion = keyVersion; + } +} diff --git a/header/src/main/java/org/zstack/header/secret/SecretHostGetReply.java b/header/src/main/java/org/zstack/header/secret/SecretHostGetReply.java new file mode 100644 index 00000000000..cd474433110 --- /dev/null +++ b/header/src/main/java/org/zstack/header/secret/SecretHostGetReply.java @@ -0,0 +1,18 @@ +package org.zstack.header.secret; + +import org.zstack.header.message.MessageReply; + +/** Reply for SecretHostGetMsg. */ +public class SecretHostGetReply extends MessageReply { + public static final String ERROR_CODE_SECRET_NOT_FOUND = "KEY_AGENT_SECRET_NOT_FOUND"; + + private String secretUuid; + + public String getSecretUuid() { + return secretUuid; + } + + public void setSecretUuid(String secretUuid) { + this.secretUuid = secretUuid; + } +} 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 6ffc636a090..7e22d002f15 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -410,7 +410,7 @@ public static class SecretHostDefineCmd extends AgentCommand { private String encryptedDek; private String vmUuid; private String purpose; - private String providerName; + private Integer keyVersion; private String description; public String getEncryptedDek() { @@ -437,12 +437,12 @@ public void setPurpose(String purpose) { this.purpose = purpose; } - public String getProviderName() { - return providerName; + public Integer getKeyVersion() { + return keyVersion; } - public void setProviderName(String providerName) { - this.providerName = providerName; + public void setKeyVersion(Integer keyVersion) { + this.keyVersion = keyVersion; } public String getDescription() { @@ -466,6 +466,48 @@ public void setSecretUuid(String secretUuid) { } } + public static class SecretHostGetCmd extends AgentCommand { + private String vmUuid; + private String purpose; + private Integer keyVersion; + + 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 Integer getKeyVersion() { + return keyVersion; + } + + public void setKeyVersion(Integer keyVersion) { + this.keyVersion = keyVersion; + } + } + + public static class SecretHostGetResponse extends AgentResponse { + private String secretUuid; + + 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 1f7fd4731e0..3f8a8fd024a 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -129,6 +129,7 @@ public interface KVMConstant { 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_GET_SECRET_PATH = "/host/key/envelope/getSecret"; String KVM_ENSURE_SECRET_PATH = "/host/key/envelope/ensureSecret"; /** HTTP timeout in seconds for envelope key sync (verify/create/rotate/get) to agent. */ 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 256988fd0df..f4d6e2173bd 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -57,6 +57,8 @@ import org.zstack.header.host.MigrateVmOnHypervisorMsg.StorageMigrationPolicy; import org.zstack.header.secret.SecretHostDefineMsg; import org.zstack.header.secret.SecretHostDefineReply; +import org.zstack.header.secret.SecretHostGetMsg; +import org.zstack.header.secret.SecretHostGetReply; import org.zstack.header.message.APIMessage; import org.zstack.header.message.Message; import org.zstack.header.message.MessageReply; @@ -750,6 +752,8 @@ protected void handleLocalMessage(Message msg) { handle((GetFileDownloadProgressMsg) msg); } else if (msg instanceof RestartKvmAgentMsg) { handle((RestartKvmAgentMsg) msg); + } else if (msg instanceof SecretHostGetMsg) { + handle((SecretHostGetMsg) msg); } else if (msg instanceof SecretHostDefineMsg) { handle((SecretHostDefineMsg) msg); } else { @@ -5310,6 +5314,43 @@ public void handle(ErrorCode errCode, Map data) { }).start(); } + private void handle(SecretHostGetMsg msg) { + SecretHostGetReply reply = new SecretHostGetReply(); + if (StringUtils.isBlank(msg.getVmUuid()) || StringUtils.isBlank(msg.getPurpose()) || msg.getKeyVersion() == null) { + reply.setError(operr("vmUuid, purpose and keyVersion are required for get secret")); + bus.reply(msg, reply); + return; + } + + String url = buildUrl(KVMConstant.KVM_GET_SECRET_PATH); + KVMAgentCommands.SecretHostGetCmd cmd = new KVMAgentCommands.SecretHostGetCmd(); + cmd.setVmUuid(msg.getVmUuid()); + cmd.setPurpose(msg.getPurpose()); + cmd.setKeyVersion(msg.getKeyVersion()); + restf.asyncJsonPost(url, cmd, new JsonAsyncRESTCallback(msg, reply) { + @Override + public void fail(ErrorCode err) { + reply.setError(err != null ? err : operr("get secret on agent failed")); + bus.reply(msg, reply); + } + + @Override + public void success(KVMAgentCommands.SecretHostGetResponse rsp) { + if (rsp != null && rsp.isSuccess()) { + reply.setSecretUuid(rsp.getSecretUuid()); + } else { + reply.setError(buildSecretAgentError(rsp, "get secret failed")); + } + bus.reply(msg, reply); + } + + @Override + public Class getReturnClass() { + return KVMAgentCommands.SecretHostGetResponse.class; + } + }, TimeUnit.SECONDS, KVMConstant.ENVELOPE_KEY_HTTP_TIMEOUT_SEC); + } + private void handle(SecretHostDefineMsg msg) { SecretHostDefineReply reply = new SecretHostDefineReply(); if (org.apache.commons.lang.StringUtils.isBlank(msg.getDekBase64())) { @@ -5317,8 +5358,8 @@ 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")); + if (StringUtils.isBlank(msg.getVmUuid()) || StringUtils.isBlank(msg.getPurpose()) || msg.getKeyVersion() == null) { + reply.setError(operr("vmUuid, purpose and keyVersion are required for ensure secret")); bus.reply(msg, reply); return; } @@ -5396,7 +5437,7 @@ private void handle(SecretHostDefineMsg msg) { cmd.setEncryptedDek(envelopeDekBase64); cmd.setVmUuid(msg.getVmUuid()); cmd.setPurpose(msg.getPurpose()); - cmd.setProviderName(msg.getProviderName()); + cmd.setKeyVersion(msg.getKeyVersion()); cmd.setDescription(msg.getDescription() != null ? msg.getDescription() : ""); restf.asyncJsonPost(url, cmd, new JsonAsyncRESTCallback(msg, reply) { @Override @@ -5412,14 +5453,7 @@ public void success(KVMAgentCommands.SecretHostDefineResponse rsp) { reply.setSecretUuid(rsp.getSecretUuid()); } } else { - if (rsp != null && rsp.getError() != null) { - ErrorCode err = new ErrorCode(); - err.setCode(rsp.getError()); - err.setDetails(rsp.getError()); - reply.setError(err); - } else { - reply.setError(operr(rsp != null ? rsp.getError() : "ensure secret failed")); - } + reply.setError(buildSecretAgentError(rsp, "ensure secret failed")); } bus.reply(msg, reply); } @@ -5431,6 +5465,16 @@ public Class getReturnClass() { }, TimeUnit.SECONDS, KVMConstant.ENVELOPE_KEY_HTTP_TIMEOUT_SEC); } + private ErrorCode buildSecretAgentError(KVMAgentCommands.AgentResponse rsp, String defaultMessage) { + if (rsp != null && rsp.getError() != null) { + ErrorCode err = new ErrorCode(); + err.setCode(rsp.getError()); + err.setDetails(rsp.getError()); + return err; + } + return operr(defaultMessage); + } + @Override protected void deleteTakeOverFlag(Completion completion) { if (CoreGlobalProperty.UNIT_TEST_ON) { 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 c7456771138..5e21be54632 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 @@ -29,10 +29,12 @@ import org.zstack.header.secret.SecretHostDefineMsg; import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO; import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO_; +import org.zstack.header.secret.SecretHostDefineReply; +import org.zstack.header.secret.SecretHostGetMsg; +import org.zstack.header.secret.SecretHostGetReply; 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.secret.SecretHostDefineReply; import org.zstack.header.vm.PreVmInstantiateResourceExtensionPoint; import org.zstack.header.vm.VmInstanceSpec; import org.zstack.header.vm.VmInstantiateResourceException; @@ -148,7 +150,7 @@ public void preInstantiateVmResource(VmInstanceSpec spec, Completion completion) context.tpmUuid = tpmSpec.getTpmUuid(); context.backupFileUuid = tpmSpec.getBackupFileUuid(); // maybe null context.providerUuid = resourceKeyBackend.findKeyProviderUuidByTpm(context.tpmUuid); - context.providerName = resourceKeyBackend.findKeyProviderNameByTpm(context.tpmUuid); + context.keyVersion = resourceKeyBackend.findKeyVersionByTpm(context.tpmUuid); final SimpleFlowChain chain = new SimpleFlowChain(); chain.setName("prepare-tpm-resources-for-vm-" + spec.getVmInventory().getUuid()); @@ -223,12 +225,12 @@ public void rollback(FlowRollback trigger, Map data) { trigger.rollback(); } }).then(new NoRollbackFlow() { - String __name__ = "create-dek"; + String __name__ = "ensure-resource-key-ref"; @Override public boolean skip(Map data) { - boolean shouldSkip = VmGlobalConfig.ALLOWED_TPM_VM_WITHOUT_KMS.value(Boolean.class) && - (StringUtils.isBlank(context.providerUuid) && StringUtils.isBlank(context.providerName)); + boolean shouldSkip = VmGlobalConfig.ALLOWED_TPM_VM_WITHOUT_KMS.value(Boolean.class) + && StringUtils.isBlank(context.providerUuid); if (shouldSkip) { logger.info("skip create-dek: allowed.tpm.vm.without.kms is enabled and no KMS provider bound"); } @@ -237,11 +239,16 @@ public boolean skip(Map data) { @Override public void run(FlowTrigger trigger, Map data) { - if (StringUtils.isBlank(context.providerUuid) && StringUtils.isBlank(context.providerName)) { + if (StringUtils.isBlank(context.providerUuid)) { trigger.fail(operr("missing TPM resource key binding for tpm[uuid:%s], attachKeyProviderToTpm must run before create-dek", context.tpmUuid)); return; } + if (context.keyVersion != null) { + trigger.next(); + return; + } + GetOrCreateResourceKeyContext keyCtx = new GetOrCreateResourceKeyContext(); keyCtx.setResourceUuid(context.tpmUuid); keyCtx.setResourceType(TpmVO.class.getSimpleName()); @@ -254,9 +261,86 @@ public void run(FlowTrigger trigger, Map data) { public void success(ResourceKeyResult result) { tpmSpec.setResourceKeyCreatedNew(result.isCreatedNewKey()); tpmSpec.setResourceKeyProviderUuid(result.getKeyProviderUuid()); - context.providerUuid = result.getKeyProviderUuid(); - context.providerName = result.getKeyProviderName(); context.dekBase64 = result.getDekBase64(); + context.keyVersion = result.getKeyVersion(); + if (context.keyVersion == null) { + trigger.fail(operr("missing keyVersion for tpm[uuid:%s] after getOrCreateKey", context.tpmUuid)); + return; + } + trigger.next(); + } + + @Override + public void fail(ErrorCode errorCode) { + trigger.fail(errorCode); + } + }); + } + }).then(new NoRollbackFlow() { + String __name__ = "get-secret-on-host"; + + @Override + public boolean skip(Map data) { + return context.keyVersion == null; + } + + @Override + public void run(FlowTrigger trigger, Map data) { + SecretHostGetMsg innerMsg = new SecretHostGetMsg(); + innerMsg.setHostUuid(spec.getDestHost().getUuid()); + innerMsg.setVmUuid(spec.getVmInventory().getUuid()); + innerMsg.setPurpose("vtpm"); + innerMsg.setKeyVersion(context.keyVersion); + bus.makeTargetServiceIdByResourceUuid(innerMsg, HostConstant.SERVICE_ID, innerMsg.getHostUuid()); + bus.send(innerMsg, new CloudBusCallBack(trigger) { + @Override + public void run(MessageReply reply) { + if (reply.isSuccess()) { + SecretHostGetReply r = reply.castReply(); + spec.getDevicesSpec().getTpm().setSecretUuid(r.getSecretUuid()); + trigger.next(); + return; + } + + ErrorCode errorCode = reply.getError(); + if (errorCode != null + && (SecretHostGetReply.ERROR_CODE_SECRET_NOT_FOUND.equals(errorCode.getCode()) + || SecretHostGetReply.ERROR_CODE_SECRET_NOT_FOUND.equals(errorCode.getDetails()))) { + trigger.next(); + return; + } + + trigger.fail(errorCode != null ? errorCode : operr("get secret on host failed")); + } + }); + } + }).then(new NoRollbackFlow() { + String __name__ = "load-dek-for-host"; + + @Override + public boolean skip(Map data) { + return StringUtils.isNotBlank(spec.getDevicesSpec().getTpm().getSecretUuid()) || context.dekBase64 != null; + } + + @Override + public void run(FlowTrigger trigger, Map data) { + GetOrCreateResourceKeyContext keyCtx = new GetOrCreateResourceKeyContext(); + keyCtx.setResourceUuid(context.tpmUuid); + keyCtx.setResourceType(TpmVO.class.getSimpleName()); + keyCtx.setKeyProviderUuid(context.providerUuid); + keyCtx.setPurpose("vtpm"); + + resourceKeyManager.getOrCreateKey(keyCtx, new ReturnValueCompletion(trigger) { + @Override + public void success(ResourceKeyResult result) { + tpmSpec.setResourceKeyCreatedNew(result.isCreatedNewKey()); + tpmSpec.setResourceKeyProviderUuid(result.getKeyProviderUuid()); + context.dekBase64 = result.getDekBase64(); + context.keyVersion = result.getKeyVersion(); + if (context.keyVersion == null) { + trigger.fail(operr("missing keyVersion for tpm[uuid:%s] before ensure secret", context.tpmUuid)); + return; + } trigger.next(); } @@ -271,13 +355,13 @@ public void fail(ErrorCode errorCode) { @Override public boolean skip(Map data) { - return context.dekBase64 == null; + return context.dekBase64 == null || StringUtils.isNotBlank(spec.getDevicesSpec().getTpm().getSecretUuid()); } @Override public void run(FlowTrigger trigger, Map data) { - if (StringUtils.isBlank(context.providerName)) { - trigger.fail(operr("missing effective key provider name for tpm[uuid:%s] before define-secret-on-host", context.tpmUuid)); + if (context.keyVersion == null) { + trigger.fail(operr("missing keyVersion for tpm[uuid:%s] before define-secret-on-host", context.tpmUuid)); return; } @@ -286,7 +370,7 @@ public void run(FlowTrigger trigger, Map data) { innerMsg.setVmUuid(spec.getVmInventory().getUuid()); innerMsg.setDekBase64(context.dekBase64); innerMsg.setPurpose("vtpm"); - innerMsg.setProviderName(context.providerName); + innerMsg.setKeyVersion(context.keyVersion); innerMsg.setDescription("Define secret for VM " + spec.getVmInventory().getUuid()); bus.makeTargetServiceIdByResourceUuid(innerMsg, HostConstant.SERVICE_ID, innerMsg.getHostUuid()); bus.send(innerMsg, new CloudBusCallBack(trigger) { @@ -332,7 +416,7 @@ static class PrepareTpmResourceContext { String tpmUuid; String backupFileUuid; String providerUuid; - String providerName; + Integer keyVersion; String dekBase64; void clearSensitiveData() { diff --git a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy index c9b975069cd..416e4d8feb8 100755 --- a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy +++ b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy @@ -353,12 +353,26 @@ class KVMSimulator implements Simulator { spec.simulator(KVMConstant.KVM_ENSURE_SECRET_PATH) { HttpEntity e -> String hostUuid = e.getHeaders().getFirst(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID) SecretHostDefineCmd cmd = JSONObjectUtil.toObject(e.body, SecretHostDefineCmd.class) - String cacheKey = String.format("%s::%s", hostUuid ?: "", cmd?.vmUuid ?: "") + String cacheKey = buildHostSecretCacheKey(hostUuid, cmd?.vmUuid, cmd?.purpose, cmd?.keyVersion) def rsp = new SecretHostDefineResponse() rsp.secretUuid = ensureSecretUuidCache.computeIfAbsent(cacheKey) { Platform.uuid } return rsp } + spec.simulator(KVMConstant.KVM_GET_SECRET_PATH) { HttpEntity e -> + String hostUuid = e.getHeaders().getFirst(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID) + SecretHostGetCmd cmd = JSONObjectUtil.toObject(e.body, SecretHostGetCmd.class) + String cacheKey = buildHostSecretCacheKey(hostUuid, cmd?.vmUuid, cmd?.purpose, cmd?.keyVersion) + def rsp = new SecretHostGetResponse() + String secretUuid = ensureSecretUuidCache.get(cacheKey) + if (secretUuid == null) { + rsp.setError("KEY_AGENT_SECRET_NOT_FOUND") + } else { + rsp.secretUuid = secretUuid + } + return rsp + } + spec.simulator(KVMConstant.KVM_ECHO_PATH) { HttpEntity e -> Spec.checkHttpCallType(e, true) return [:] @@ -743,4 +757,12 @@ class KVMSimulator implements Simulator { return new BackupVmHostFileResponse() } } + + private static String buildHostSecretCacheKey(String hostUuid, String vmUuid, String purpose, Integer keyVersion) { + return String.format("%s::%s::%s::%s", + hostUuid ?: "", + vmUuid ?: "", + purpose ?: "", + keyVersion == null ? "" : String.valueOf(keyVersion)) + } } From 36e7d86f67e162899ce8795def381ceed33561ff Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Thu, 9 Apr 2026 11:46:44 +0800 Subject: [PATCH 123/129] [kvm]: remove TPM when VM deleted from DB Implement VmJustBeforeDeleteFromDbExtensionPoint to clean up TPM resources when VM is removed from database. This prevents TPM resource leakage and ensures proper cleanup. Related: ZSV-11310 Change-Id: I647a6d676377646b6f7773757363646b6977766b --- .../org/zstack/kvm/tpm/KvmTpmExtensions.java | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) 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 c7456771138..ea602a56fb9 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 @@ -33,9 +33,12 @@ import org.zstack.header.tpm.entity.TpmVO; import org.zstack.header.tpm.entity.TpmVO_; import org.zstack.header.secret.SecretHostDefineReply; +import org.zstack.header.tpm.message.RemoveTpmMsg; import org.zstack.header.vm.PreVmInstantiateResourceExtensionPoint; +import org.zstack.header.vm.VmInstanceInventory; import org.zstack.header.vm.VmInstanceSpec; import org.zstack.header.vm.VmInstantiateResourceException; +import org.zstack.header.vm.VmJustBeforeDeleteFromDbExtensionPoint; import org.zstack.header.vm.additions.VmHostBackupFileVO; import org.zstack.header.vm.additions.VmHostBackupFileVO_; import org.zstack.header.vm.additions.VmHostFileType; @@ -54,11 +57,13 @@ import java.time.Instant; import java.util.Map; +import static org.zstack.header.tpm.TpmConstants.SERVICE_ID; import static org.zstack.kvm.KVMConstant.*; import static org.zstack.core.Platform.operr; public class KvmTpmExtensions implements KVMStartVmExtensionPoint, - PreVmInstantiateResourceExtensionPoint { + PreVmInstantiateResourceExtensionPoint, + VmJustBeforeDeleteFromDbExtensionPoint { private static final CLogger logger = Utils.getLogger(KvmTpmExtensions.class); @Autowired @@ -373,6 +378,27 @@ public void fail(ErrorCode errorCode) { }); } + @Override + public void vmJustBeforeDeleteFromDb(VmInstanceInventory inv) { + String tpmUuid = Q.New(TpmVO.class) + .eq(TpmVO_.vmInstanceUuid, inv.getUuid()) + .select(TpmVO_.uuid) + .findValue(); + if (tpmUuid == null) { + return; + } + + RemoveTpmMsg removeMsg = new RemoveTpmMsg(); + removeMsg.setVmInstanceUuid(inv.getUuid()); + removeMsg.setTpmUuid(tpmUuid); + bus.makeTargetServiceIdByResourceUuid(removeMsg, SERVICE_ID, removeMsg.getTpmUuid()); + MessageReply reply = bus.call(removeMsg); + if (!reply.isSuccess()) { + logger.warn(String.format("failed to remove TPM[uuid:%s] of VM[uuid:%s], error: %s", + tpmUuid, inv.getUuid(), reply.getError())); + } + } + private void clearRollbackInfo(VmInstanceSpec spec) { if (spec.getDevicesSpec() == null || spec.getDevicesSpec().getTpm() == null) { return; From f1c6817a6511a0ca35c813f7e0906184bc4f4a0c Mon Sep 17 00:00:00 2001 From: "zhijian.liu" Date: Wed, 8 Apr 2026 18:30:20 +0800 Subject: [PATCH 124/129] [kms]: add delete secret for vm Resolves: ZSV-11630 --- conf/springConfigXml/Kvm.xml | 1 + .../header/secret/SecretHostDeleteMsg.java | 48 +++++++ .../header/secret/SecretHostDeleteReply.java | 7 ++ .../java/org/zstack/kvm/KVMAgentCommands.java | 33 +++++ .../main/java/org/zstack/kvm/KVMConstant.java | 1 + .../src/main/java/org/zstack/kvm/KVMHost.java | 40 ++++++ .../org/zstack/kvm/tpm/KvmTpmExtensions.java | 118 +++++++++++++++++- .../org/zstack/kvm/tpm/KvmTpmManager.java | 88 ++++++++++++- 8 files changed, 329 insertions(+), 7 deletions(-) create mode 100644 header/src/main/java/org/zstack/header/secret/SecretHostDeleteMsg.java create mode 100644 header/src/main/java/org/zstack/header/secret/SecretHostDeleteReply.java diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index 3417a5272c0..72eb462afd5 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -256,6 +256,7 @@ + diff --git a/header/src/main/java/org/zstack/header/secret/SecretHostDeleteMsg.java b/header/src/main/java/org/zstack/header/secret/SecretHostDeleteMsg.java new file mode 100644 index 00000000000..7b5c4bc6176 --- /dev/null +++ b/header/src/main/java/org/zstack/header/secret/SecretHostDeleteMsg.java @@ -0,0 +1,48 @@ +package org.zstack.header.secret; + +import org.zstack.header.host.HostMessage; +import org.zstack.header.message.NeedReplyMessage; + +/** + * Request to delete existing secret on KVM host by vmUuid/purpose/keyVersion. + * keyVersion can be null, then agent may remove all matched versions for the vm+purpose. + */ +public class SecretHostDeleteMsg extends NeedReplyMessage implements HostMessage { + private String hostUuid; + private String vmUuid; + private String purpose; + private Integer keyVersion; + + @Override + public String getHostUuid() { + return hostUuid; + } + + public void setHostUuid(String hostUuid) { + this.hostUuid = hostUuid; + } + + 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 Integer getKeyVersion() { + return keyVersion; + } + + public void setKeyVersion(Integer keyVersion) { + this.keyVersion = keyVersion; + } +} diff --git a/header/src/main/java/org/zstack/header/secret/SecretHostDeleteReply.java b/header/src/main/java/org/zstack/header/secret/SecretHostDeleteReply.java new file mode 100644 index 00000000000..6640b5b60ca --- /dev/null +++ b/header/src/main/java/org/zstack/header/secret/SecretHostDeleteReply.java @@ -0,0 +1,7 @@ +package org.zstack.header.secret; + +import org.zstack.header.message.MessageReply; + +/** Reply for SecretHostDeleteMsg. */ +public class SecretHostDeleteReply extends MessageReply { +} 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 7e22d002f15..af3044f1837 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -508,6 +508,39 @@ public void setSecretUuid(String secretUuid) { } } + public static class SecretHostDeleteCmd extends AgentCommand { + private String vmUuid; + private String purpose; + private Integer keyVersion; + + 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 Integer getKeyVersion() { + return keyVersion; + } + + public void setKeyVersion(Integer keyVersion) { + this.keyVersion = keyVersion; + } + } + + public static class SecretHostDeleteResponse extends AgentResponse { + } + 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 3f8a8fd024a..e24b6cb6e6d 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,7 @@ public interface KVMConstant { String KVM_VERIFY_ENVELOPE_KEY_PATH = "/host/key/envelope/checkEnvelopeKey"; String KVM_GET_SECRET_PATH = "/host/key/envelope/getSecret"; String KVM_ENSURE_SECRET_PATH = "/host/key/envelope/ensureSecret"; + String KVM_DELETE_SECRET_PATH = "/host/key/envelope/deleteSecret"; /** HTTP timeout in seconds for envelope key sync (verify/create/rotate/get) to agent. */ long ENVELOPE_KEY_HTTP_TIMEOUT_SEC = 5L; 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 f4d6e2173bd..7ea32f1c011 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -57,6 +57,8 @@ import org.zstack.header.host.MigrateVmOnHypervisorMsg.StorageMigrationPolicy; import org.zstack.header.secret.SecretHostDefineMsg; import org.zstack.header.secret.SecretHostDefineReply; +import org.zstack.header.secret.SecretHostDeleteMsg; +import org.zstack.header.secret.SecretHostDeleteReply; import org.zstack.header.secret.SecretHostGetMsg; import org.zstack.header.secret.SecretHostGetReply; import org.zstack.header.message.APIMessage; @@ -756,6 +758,8 @@ protected void handleLocalMessage(Message msg) { handle((SecretHostGetMsg) msg); } else if (msg instanceof SecretHostDefineMsg) { handle((SecretHostDefineMsg) msg); + } else if (msg instanceof SecretHostDeleteMsg) { + handle((SecretHostDeleteMsg) msg); } else { super.handleLocalMessage(msg); } @@ -5465,6 +5469,42 @@ public Class getReturnClass() { }, TimeUnit.SECONDS, KVMConstant.ENVELOPE_KEY_HTTP_TIMEOUT_SEC); } + private void handle(SecretHostDeleteMsg msg) { + SecretHostDeleteReply reply = new SecretHostDeleteReply(); + if (StringUtils.isBlank(msg.getVmUuid()) || StringUtils.isBlank(msg.getPurpose())) { + reply.setError(operr("vmUuid and purpose are required for delete secret")); + bus.reply(msg, reply); + return; + } + + String url = buildUrl(KVMConstant.KVM_DELETE_SECRET_PATH); + KVMAgentCommands.SecretHostDeleteCmd cmd = new KVMAgentCommands.SecretHostDeleteCmd(); + cmd.setVmUuid(msg.getVmUuid()); + cmd.setPurpose(msg.getPurpose()); + cmd.setKeyVersion(msg.getKeyVersion()); + + restf.asyncJsonPost(url, cmd, new JsonAsyncRESTCallback(msg, reply) { + @Override + public void fail(ErrorCode err) { + reply.setError(err != null ? err : operr("delete secret on agent failed")); + bus.reply(msg, reply); + } + + @Override + public void success(KVMAgentCommands.SecretHostDeleteResponse rsp) { + if (rsp == null || !rsp.isSuccess()) { + reply.setError(buildSecretAgentError(rsp, "delete secret failed")); + } + bus.reply(msg, reply); + } + + @Override + public Class getReturnClass() { + return KVMAgentCommands.SecretHostDeleteResponse.class; + } + }, TimeUnit.SECONDS, KVMConstant.ENVELOPE_KEY_HTTP_TIMEOUT_SEC); + } + private ErrorCode buildSecretAgentError(KVMAgentCommands.AgentResponse rsp, String defaultMessage) { if (rsp != null && rsp.getError() != null) { ErrorCode err = new ErrorCode(); 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 5e21be54632..942be0a977b 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 @@ -2,7 +2,6 @@ import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.zstack.compute.vm.VmGlobalConfig; import org.zstack.compute.vm.devices.TpmEncryptedResourceKeyBackend; import org.zstack.compute.vm.devices.TpmEncryptedResourceKeyBackend.CloneEncryptedResourceKeyContext; import org.zstack.core.Platform; @@ -21,11 +20,13 @@ import org.zstack.header.core.workflow.FlowTrigger; import org.zstack.header.core.workflow.NoRollbackFlow; import org.zstack.header.errorcode.ErrorCode; +import org.zstack.header.core.NoErrorCompletion; import org.zstack.header.host.HostConstant; import org.zstack.header.message.MessageReply; import org.zstack.header.keyprovider.EncryptedResourceKeyManager; import org.zstack.header.keyprovider.EncryptedResourceKeyManager.GetOrCreateResourceKeyContext; import org.zstack.header.keyprovider.EncryptedResourceKeyManager.ResourceKeyResult; +import org.zstack.header.secret.SecretHostDeleteMsg; import org.zstack.header.secret.SecretHostDefineMsg; import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO; import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO_; @@ -35,11 +36,17 @@ 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.HaStartVmInstanceMsg; import org.zstack.header.vm.PreVmInstantiateResourceExtensionPoint; +import org.zstack.header.vm.VmAfterExpungeExtensionPoint; +import org.zstack.header.vm.VmInstanceInventory; +import org.zstack.header.vm.VmInstanceMigrateExtensionPoint; 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.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_; @@ -60,7 +67,9 @@ import static org.zstack.core.Platform.operr; public class KvmTpmExtensions implements KVMStartVmExtensionPoint, - PreVmInstantiateResourceExtensionPoint { + PreVmInstantiateResourceExtensionPoint, + VmInstanceMigrateExtensionPoint, + VmAfterExpungeExtensionPoint { private static final CLogger logger = Utils.getLogger(KvmTpmExtensions.class); @Autowired @@ -122,6 +131,16 @@ public void beforeStartVmOnKvm(KVMHostInventory host, VmInstanceSpec spec, KVMAg @Override public void startVmOnKvmSuccess(KVMHostInventory host, VmInstanceSpec spec) { + if (spec.getMessage() instanceof HaStartVmInstanceMsg) { + String vmUuid = spec.getVmInventory() == null ? null : spec.getVmInventory().getUuid(); + String srcHostUuid = spec.getVmInventory() == null ? null : spec.getVmInventory().getLastHostUuid(); + Integer keyVersion = findTpmKeyVersionByVmUuid(vmUuid); + boolean vmIsOnDestHost = isVmCurrentlyOnExpectedHost(vmUuid, host.getUuid()); + if (vmIsOnDestHost && StringUtils.isNotBlank(srcHostUuid) && !host.getUuid().equals(srcHostUuid)) { + deleteHostSecretBestEffort(srcHostUuid, vmUuid, keyVersion, + "ha-start-success"); + } + } clearRollbackInfo(spec); } @@ -229,10 +248,11 @@ public void rollback(FlowRollback trigger, Map data) { @Override public boolean skip(Map data) { - boolean shouldSkip = VmGlobalConfig.ALLOWED_TPM_VM_WITHOUT_KMS.value(Boolean.class) - && StringUtils.isBlank(context.providerUuid); + boolean shouldSkip = StringUtils.isBlank(context.providerUuid); if (shouldSkip) { - logger.info("skip create-dek: allowed.tpm.vm.without.kms is enabled and no KMS provider bound"); + logger.info(String.format( + "skip ensure-resource-key-ref for tpm[uuid:%s] due to missing key provider binding, try host secret first", + context.tpmUuid)); } return shouldSkip; } @@ -253,7 +273,6 @@ public void run(FlowTrigger trigger, Map data) { keyCtx.setResourceUuid(context.tpmUuid); keyCtx.setResourceType(TpmVO.class.getSimpleName()); keyCtx.setKeyProviderUuid(context.providerUuid); - keyCtx.setKeyProviderName(context.providerName); keyCtx.setPurpose("vtpm"); resourceKeyManager.getOrCreateKey(keyCtx, new ReturnValueCompletion(trigger) { @@ -488,4 +507,91 @@ private String findSourceTpmUuidFromSnapshotTpmBackupFile(String tpmBackupFileUu .select(TpmVO_.uuid) .findValue(); } + + @Override + public void afterMigrateVm(VmInstanceInventory inv, String srcHostUuid, NoErrorCompletion completion) { + String vmUuid = inv == null ? null : inv.getUuid(); + String destHostUuid = inv == null ? null : inv.getHostUuid(); + if (StringUtils.isBlank(vmUuid) || StringUtils.isBlank(srcHostUuid) || srcHostUuid.equals(destHostUuid)) { + completion.done(); + return; + } + + if (!isVmCurrentlyOnExpectedHost(vmUuid, destHostUuid)) { + completion.done(); + return; + } + + Integer keyVersion = findTpmKeyVersionByVmUuid(vmUuid); + deleteHostSecretBestEffort(srcHostUuid, vmUuid, keyVersion, "after-migrate"); + completion.done(); + } + + @Override + public void vmAfterExpunge(VmInstanceInventory vm) { + String vmUuid = vm.getUuid(); + Integer keyVersion = findTpmKeyVersionByVmUuid(vmUuid); + + java.util.Set hostUuids = new java.util.HashSet<>(); + if (StringUtils.isNotBlank(vm.getHostUuid())) { + hostUuids.add(vm.getHostUuid()); + } + if (StringUtils.isNotBlank(vm.getLastHostUuid())) { + hostUuids.add(vm.getLastHostUuid()); + } + + if (hostUuids.isEmpty()) { + return; + } + + for (String hostUuid : hostUuids) { + deleteHostSecretBestEffort(hostUuid, vmUuid, keyVersion, "expunge"); + } + } + + private Integer findTpmKeyVersionByVmUuid(String vmUuid) { + if (StringUtils.isBlank(vmUuid)) { + return null; + } + String tpmUuid = Q.New(TpmVO.class) + .eq(TpmVO_.vmInstanceUuid, vmUuid) + .select(TpmVO_.uuid) + .findValue(); + return tpmUuid == null ? null : resourceKeyBackend.findKeyVersionByTpm(tpmUuid); + } + + private boolean isVmCurrentlyOnExpectedHost(String vmUuid, String expectedHostUuid) { + if (StringUtils.isBlank(vmUuid) || StringUtils.isBlank(expectedHostUuid)) { + return false; + } + + String currentHostUuid = Q.New(VmInstanceVO.class) + .select(VmInstanceVO_.hostUuid) + .eq(VmInstanceVO_.uuid, vmUuid) + .findValue(); + return expectedHostUuid.equals(currentHostUuid); + } + + private void deleteHostSecretBestEffort(String hostUuid, String vmUuid, Integer keyVersion, String reason) { + if (StringUtils.isBlank(hostUuid) || StringUtils.isBlank(vmUuid)) { + return; + } + + SecretHostDeleteMsg dmsg = new SecretHostDeleteMsg(); + dmsg.setHostUuid(hostUuid); + dmsg.setVmUuid(vmUuid); + dmsg.setPurpose("vtpm"); + dmsg.setKeyVersion(keyVersion); + bus.makeTargetServiceIdByResourceUuid(dmsg, HostConstant.SERVICE_ID, hostUuid); + bus.send(dmsg, new CloudBusCallBack(null) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + logger.warn(String.format( + "best-effort delete host secret failed on %s for vm[uuid:%s], host[uuid:%s]: %s", + reason, vmUuid, hostUuid, reply.getError().getDetails())); + } + } + }); + } } 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 a06a03aa82e..dc470cbbcde 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 @@ -26,9 +26,11 @@ import org.zstack.header.core.workflow.NoRollbackFlow; import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.errorcode.ErrorCodeList; +import org.zstack.header.host.HostConstant; import org.zstack.header.message.APIMessage; import org.zstack.header.message.Message; import org.zstack.header.message.MessageReply; +import org.zstack.header.secret.SecretHostDeleteMsg; import org.zstack.header.tpm.api.APIAddTpmEvent; import org.zstack.header.tpm.api.APIAddTpmMsg; import org.zstack.header.tpm.api.APIGetTpmCapabilityMsg; @@ -70,9 +72,11 @@ import org.zstack.utils.logging.CLogger; import java.util.ArrayList; +import java.util.HashSet; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import static org.zstack.compute.vm.VmGlobalConfig.RESET_TPM_AFTER_VM_CLONE; import static org.zstack.core.Platform.err; @@ -305,8 +309,9 @@ public String getName() { static class RemoveTpmFromVmContext { String vmInstanceUuid; String tpmUuid; + Integer keyVersion; - // enable when TPM delete/VM delete ... + // enable when TPM delete/VM delete operation boolean force; List hostFiles; @@ -339,6 +344,7 @@ private void removeTpmFromVm(RemoveTpmFromVmContext context, Completion completi .build()) .then(Flow.of("collect-vm-host-files") .handle(trigger -> { + context.keyVersion = tpmKeyBackend.findKeyVersionByTpm(context.tpmUuid); // DO NOT delete NvRam type VmHostFile: Maybe secure boot or other component related. context.hostFiles = Q.New(VmHostFileVO.class) .eq(VmHostFileVO_.vmInstanceUuid, context.vmInstanceUuid) @@ -395,6 +401,39 @@ public void done(ErrorCodeList errorCodeList) { }); }) .build()) + .then(Flow.of("delete-host-secret") + .skipIf(data -> context.hostFiles == null || context.hostFiles.isEmpty()) + .handle(trigger -> { + Set hostUuids = new HashSet<>(); + for (VmHostFileVO file : context.hostFiles) { + hostUuids.add(file.getHostUuid()); + } + + new While<>(new ArrayList<>(hostUuids)).each((hostUuid, whileCompletion) -> { + SecretHostDeleteMsg dmsg = new SecretHostDeleteMsg(); + dmsg.setHostUuid(hostUuid); + dmsg.setVmUuid(context.vmInstanceUuid); + dmsg.setPurpose("vtpm"); + dmsg.setKeyVersion(context.keyVersion); + bus.makeTargetServiceIdByResourceUuid(dmsg, HostConstant.SERVICE_ID, hostUuid); + bus.send(dmsg, new CloudBusCallBack(whileCompletion) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + logger.warn(String.format("failed to delete host secret on host[uuid:%s] for vm[uuid:%s], continue cleanup: %s", + hostUuid, context.vmInstanceUuid, reply.getError().getDetails())); + } + whileCompletion.done(); + } + }); + }).run(new WhileDoneCompletion(trigger) { + @Override + public void done(ErrorCodeList errorCodeList) { + trigger.next(); + } + }); + }) + .build()) .then(Flow.of("detach-resource-key") .handle(trigger -> { tpmKeyBackend.detachKeyProviderFromTpm(context.tpmUuid); @@ -548,6 +587,7 @@ public void handle(ErrorCode errCode, Map data) { static class ResetVmTpmContext { String vmInstanceUuid; + Integer keyVersion; List hostFiles; VmHostFileVO hostFileToDeleteLast; @@ -597,6 +637,13 @@ public String getName() { private void resetVmTpm(ResetVmTpmContext context, Completion completion) { String vmUuid = context.vmInstanceUuid; + String tpmUuid = Q.New(TpmVO.class) + .eq(TpmVO_.vmInstanceUuid, vmUuid) + .select(TpmVO_.uuid) + .findValue(); + if (tpmUuid != null) { + context.keyVersion = tpmKeyBackend.findKeyVersionByTpm(tpmUuid); + } SimpleFlowChain.of("reset-vm-tpm-" + vmUuid) .then(Flow.of("collect-vm-host-files") @@ -668,6 +715,45 @@ public void done(ErrorCodeList errorCodeList) { trigger.next(); }) .build()) + .then(Flow.of("delete-host-secret") + .handle(trigger -> { + Set hostUuids = new HashSet<>(); + for (VmHostFileVO file : context.hostFiles) { + hostUuids.add(file.getHostUuid()); + } + if (context.hostFileToDeleteLast != null) { + hostUuids.add(context.hostFileToDeleteLast.getHostUuid()); + } + if (hostUuids.isEmpty()) { + trigger.next(); + return; + } + + new While<>(new ArrayList<>(hostUuids)).each((hostUuid, whileCompletion) -> { + SecretHostDeleteMsg dmsg = new SecretHostDeleteMsg(); + dmsg.setHostUuid(hostUuid); + dmsg.setVmUuid(vmUuid); + dmsg.setPurpose("vtpm"); + dmsg.setKeyVersion(context.keyVersion); + bus.makeTargetServiceIdByResourceUuid(dmsg, HostConstant.SERVICE_ID, hostUuid); + bus.send(dmsg, new CloudBusCallBack(whileCompletion) { + @Override + public void run(MessageReply reply) { + if (!reply.isSuccess()) { + logger.warn(String.format("failed to delete host secret on host[uuid:%s] for vm[uuid:%s], continue reset: %s", + hostUuid, vmUuid, reply.getError().getDetails())); + } + whileCompletion.done(); + } + }); + }).run(new WhileDoneCompletion(trigger) { + @Override + public void done(ErrorCodeList errorCodeList) { + trigger.next(); + } + }); + }) + .build()) .then(Flow.of("check-if-any-error-in-command-sending") .handle(trigger -> { // If any host failed to delete, abort the chain to preserve From 74011f40b755006bbab93a4c862d663470461596 Mon Sep 17 00:00:00 2001 From: "zhijian.liu" Date: Thu, 9 Apr 2026 11:51:41 +0800 Subject: [PATCH 125/129] [kms]: add test case for delete secret Resolves: ZSV-11630 --- conf/springConfigXml/Kvm.xml | 2 + .../header/secret/SecretHostDeleteMsg.java | 4 - .../java/org/zstack/kvm/KVMAgentCommands.java | 27 ++++ .../main/java/org/zstack/kvm/KVMConstant.java | 1 + .../src/main/java/org/zstack/kvm/KVMHost.java | 62 +++++++-- .../org/zstack/kvm/tpm/KvmTpmExtensions.java | 46 ++++++- .../org/zstack/kvm/tpm/KvmTpmManager.java | 12 +- .../HostSecretKvmAgentSimulatorCase.groovy | 128 ++++++++++++++++++ .../tpm/KvmTpmHostSecretSimulatorTest.groovy | 10 ++ .../org/zstack/testlib/KVMSimulator.groovy | 34 ++++- 10 files changed, 299 insertions(+), 27 deletions(-) create mode 100644 test/src/test/groovy/org/zstack/test/integration/kvm/tpm/HostSecretKvmAgentSimulatorCase.groovy create mode 100644 test/src/test/groovy/org/zstack/test/integration/kvm/tpm/KvmTpmHostSecretSimulatorTest.groovy diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index 72eb462afd5..48f9956300c 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -256,7 +256,9 @@ + + diff --git a/header/src/main/java/org/zstack/header/secret/SecretHostDeleteMsg.java b/header/src/main/java/org/zstack/header/secret/SecretHostDeleteMsg.java index 7b5c4bc6176..7c119e3183b 100644 --- a/header/src/main/java/org/zstack/header/secret/SecretHostDeleteMsg.java +++ b/header/src/main/java/org/zstack/header/secret/SecretHostDeleteMsg.java @@ -3,10 +3,6 @@ import org.zstack.header.host.HostMessage; import org.zstack.header.message.NeedReplyMessage; -/** - * Request to delete existing secret on KVM host by vmUuid/purpose/keyVersion. - * keyVersion can be null, then agent may remove all matched versions for the vm+purpose. - */ public class SecretHostDeleteMsg extends NeedReplyMessage implements HostMessage { private String hostUuid; private String vmUuid; 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 af3044f1837..663357a193d 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -412,6 +412,7 @@ public static class SecretHostDefineCmd extends AgentCommand { private String purpose; private Integer keyVersion; private String description; + private String usageInstance; public String getEncryptedDek() { return encryptedDek; @@ -452,6 +453,14 @@ public String getDescription() { public void setDescription(String description) { this.description = description; } + + public String getUsageInstance() { + return usageInstance; + } + + public void setUsageInstance(String usageInstance) { + this.usageInstance = usageInstance; + } } public static class SecretHostDefineResponse extends AgentResponse { @@ -470,6 +479,7 @@ public static class SecretHostGetCmd extends AgentCommand { private String vmUuid; private String purpose; private Integer keyVersion; + private String usageInstance; public String getVmUuid() { return vmUuid; @@ -494,6 +504,14 @@ public Integer getKeyVersion() { public void setKeyVersion(Integer keyVersion) { this.keyVersion = keyVersion; } + + public String getUsageInstance() { + return usageInstance; + } + + public void setUsageInstance(String usageInstance) { + this.usageInstance = usageInstance; + } } public static class SecretHostGetResponse extends AgentResponse { @@ -512,6 +530,7 @@ public static class SecretHostDeleteCmd extends AgentCommand { private String vmUuid; private String purpose; private Integer keyVersion; + private String usageInstance; public String getVmUuid() { return vmUuid; @@ -536,6 +555,14 @@ public Integer getKeyVersion() { public void setKeyVersion(Integer keyVersion) { this.keyVersion = keyVersion; } + + public String getUsageInstance() { + return usageInstance; + } + + public void setUsageInstance(String usageInstance) { + this.usageInstance = usageInstance; + } } public static class SecretHostDeleteResponse extends AgentResponse { 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 e24b6cb6e6d..05142b6db6b 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMConstant.java @@ -138,6 +138,7 @@ public interface KVMConstant { /** Max size in bytes for DEK payload in SecretHostDefine (decoded from dekBase64). */ int MAX_DEK_BYTES = 1024; + String HOST_SECRET_USAGE_INSTANCE_VTPM = "tpm0"; String KVM_HOST_FILE_DOWNLOAD_PATH = "/host/file/download"; String KVM_HOST_FILE_UPLOAD_PATH = "/host/file/upload"; 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 7ea32f1c011..6ea791cfb28 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -5327,11 +5327,14 @@ private void handle(SecretHostGetMsg msg) { } String url = buildUrl(KVMConstant.KVM_GET_SECRET_PATH); + Map headers = new HashMap<>(); + headers.put(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID, getSelf().getUuid()); KVMAgentCommands.SecretHostGetCmd cmd = new KVMAgentCommands.SecretHostGetCmd(); cmd.setVmUuid(msg.getVmUuid()); cmd.setPurpose(msg.getPurpose()); cmd.setKeyVersion(msg.getKeyVersion()); - restf.asyncJsonPost(url, cmd, new JsonAsyncRESTCallback(msg, reply) { + cmd.setUsageInstance(KVMConstant.HOST_SECRET_USAGE_INSTANCE_VTPM); + restf.asyncJsonPost(url, cmd, headers, new JsonAsyncRESTCallback(msg, reply) { @Override public void fail(ErrorCode err) { reply.setError(err != null ? err : operr("get secret on agent failed")); @@ -5437,13 +5440,16 @@ private void handle(SecretHostDefineMsg msg) { } String envelopeDekBase64 = java.util.Base64.getEncoder().encodeToString(envelope); String url = buildUrl(KVMConstant.KVM_ENSURE_SECRET_PATH); + Map headers = new HashMap<>(); + headers.put(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID, getSelf().getUuid()); KVMAgentCommands.SecretHostDefineCmd cmd = new KVMAgentCommands.SecretHostDefineCmd(); cmd.setEncryptedDek(envelopeDekBase64); cmd.setVmUuid(msg.getVmUuid()); cmd.setPurpose(msg.getPurpose()); cmd.setKeyVersion(msg.getKeyVersion()); cmd.setDescription(msg.getDescription() != null ? msg.getDescription() : ""); - restf.asyncJsonPost(url, cmd, new JsonAsyncRESTCallback(msg, reply) { + cmd.setUsageInstance(KVMConstant.HOST_SECRET_USAGE_INSTANCE_VTPM); + restf.asyncJsonPost(url, cmd, headers, new JsonAsyncRESTCallback(msg, reply) { @Override public void fail(ErrorCode err) { reply.setError(err != null ? err : operr("ensure secret on agent failed")); @@ -5476,24 +5482,41 @@ private void handle(SecretHostDeleteMsg msg) { bus.reply(msg, reply); return; } + if (msg.getKeyVersion() == null) { + logger.debug(String.format( + "skip delete host secret on host[uuid:%s]: keyVersion is null (vm[uuid:%s], purpose:%s)", + getSelf().getUuid(), msg.getVmUuid(), msg.getPurpose())); + bus.reply(msg, reply); + return; + } String url = buildUrl(KVMConstant.KVM_DELETE_SECRET_PATH); + Map headers = new HashMap<>(); + headers.put(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID, getSelf().getUuid()); KVMAgentCommands.SecretHostDeleteCmd cmd = new KVMAgentCommands.SecretHostDeleteCmd(); cmd.setVmUuid(msg.getVmUuid()); cmd.setPurpose(msg.getPurpose()); cmd.setKeyVersion(msg.getKeyVersion()); + cmd.setUsageInstance(KVMConstant.HOST_SECRET_USAGE_INSTANCE_VTPM); - restf.asyncJsonPost(url, cmd, new JsonAsyncRESTCallback(msg, reply) { + restf.asyncJsonPost(url, cmd, headers, new JsonAsyncRESTCallback(msg, reply) { @Override public void fail(ErrorCode err) { - reply.setError(err != null ? err : operr("delete secret on agent failed")); + logger.warn(String.format( + "best-effort delete secret on agent failed on host[uuid:%s] for vm[uuid:%s]: %s", + getSelf().getUuid(), msg.getVmUuid(), + err != null ? err.getDetails() : "unknown error")); bus.reply(msg, reply); } @Override public void success(KVMAgentCommands.SecretHostDeleteResponse rsp) { if (rsp == null || !rsp.isSuccess()) { - reply.setError(buildSecretAgentError(rsp, "delete secret failed")); + ErrorCode agentErr = buildSecretAgentError(rsp, "delete secret failed"); + logger.warn(String.format( + "best-effort delete secret on agent failed on host[uuid:%s] for vm[uuid:%s]: %s", + getSelf().getUuid(), msg.getVmUuid(), + agentErr.getDetails())); } bus.reply(msg, reply); } @@ -5506,13 +5529,34 @@ public Class getReturnClass() { } private ErrorCode buildSecretAgentError(KVMAgentCommands.AgentResponse rsp, String defaultMessage) { - if (rsp != null && rsp.getError() != null) { + String raw = rsp != null ? StringUtils.trimToNull(rsp.getError()) : null; + if (raw == null) { + return operr(defaultMessage); + } + String stable = stableKeyAgentErrorCodeFromRawMessage(raw); + if (stable != null) { ErrorCode err = new ErrorCode(); - err.setCode(rsp.getError()); - err.setDetails(rsp.getError()); + err.setCode(stable); + err.setDetails(raw); return err; } - return operr(defaultMessage); + return operr("%s: %s", defaultMessage, raw); + } + + private static String stableKeyAgentErrorCodeFromRawMessage(String raw) { + String t = raw.trim(); + int colon = t.indexOf(':'); + String head = (colon >= 0 ? t.substring(0, colon) : t).trim(); + if (head.isEmpty()) { + return null; + } + if (SecretHostGetReply.ERROR_CODE_SECRET_NOT_FOUND.equals(head)) { + return head; + } + if (head.startsWith("KEY_AGENT_")) { + return head; + } + return null; } @Override 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 942be0a977b..cdcae1b0469 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 @@ -2,6 +2,7 @@ import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.vm.VmGlobalConfig; import org.zstack.compute.vm.devices.TpmEncryptedResourceKeyBackend; import org.zstack.compute.vm.devices.TpmEncryptedResourceKeyBackend.CloneEncryptedResourceKeyContext; import org.zstack.core.Platform; @@ -42,7 +43,9 @@ import org.zstack.header.vm.VmInstanceInventory; import org.zstack.header.vm.VmInstanceMigrateExtensionPoint; import org.zstack.header.vm.VmInstanceSpec; +import org.zstack.header.vm.VmInstanceState; import org.zstack.header.vm.VmInstantiateResourceException; +import org.zstack.header.vm.VmStateChangedExtensionPoint; import org.zstack.header.vm.additions.VmHostBackupFileVO; import org.zstack.header.vm.additions.VmHostBackupFileVO_; import org.zstack.header.vm.VmInstanceVO; @@ -69,7 +72,8 @@ public class KvmTpmExtensions implements KVMStartVmExtensionPoint, PreVmInstantiateResourceExtensionPoint, VmInstanceMigrateExtensionPoint, - VmAfterExpungeExtensionPoint { + VmAfterExpungeExtensionPoint, + VmStateChangedExtensionPoint { private static final CLogger logger = Utils.getLogger(KvmTpmExtensions.class); @Autowired @@ -220,7 +224,6 @@ public void run(FlowTrigger trigger, Map data) { @Override public void success() { context.providerUuid = resourceKeyBackend.findKeyProviderUuidByTpm(context.tpmUuid); - context.providerName = resourceKeyBackend.findKeyProviderNameByTpm(context.tpmUuid); trigger.next(); } @@ -322,9 +325,7 @@ public void run(MessageReply reply) { } ErrorCode errorCode = reply.getError(); - if (errorCode != null - && (SecretHostGetReply.ERROR_CODE_SECRET_NOT_FOUND.equals(errorCode.getCode()) - || SecretHostGetReply.ERROR_CODE_SECRET_NOT_FOUND.equals(errorCode.getDetails()))) { + if (errorCode != null && isVtpmSecretNotFoundOnHost(errorCode)) { trigger.next(); return; } @@ -527,6 +528,27 @@ public void afterMigrateVm(VmInstanceInventory inv, String srcHostUuid, NoErrorC completion.done(); } + @Override + public void vmStateChanged(VmInstanceInventory vm, VmInstanceState oldState, VmInstanceState newState) { + if (oldState != VmInstanceState.VolumeMigrating) { + return; + } + + String vmUuid = vm == null ? null : vm.getUuid(); + String srcHostUuid = vm == null ? null : vm.getLastHostUuid(); + String destHostUuid = vm == null ? null : vm.getHostUuid(); + if (StringUtils.isBlank(vmUuid) || StringUtils.isBlank(srcHostUuid) || srcHostUuid.equals(destHostUuid)) { + return; + } + + if (!isVmCurrentlyOnExpectedHost(vmUuid, destHostUuid)) { + return; + } + + Integer keyVersion = findTpmKeyVersionByVmUuid(vmUuid); + deleteHostSecretBestEffort(srcHostUuid, vmUuid, keyVersion, "volume-migrated-host-change"); + } + @Override public void vmAfterExpunge(VmInstanceInventory vm) { String vmUuid = vm.getUuid(); @@ -572,8 +594,16 @@ private boolean isVmCurrentlyOnExpectedHost(String vmUuid, String expectedHostUu return expectedHostUuid.equals(currentHostUuid); } + private static boolean isVtpmSecretNotFoundOnHost(ErrorCode errorCode) { + if (SecretHostGetReply.ERROR_CODE_SECRET_NOT_FOUND.equals(errorCode.getCode())) { + return true; + } + String details = errorCode.getDetails(); + return details != null && details.contains(SecretHostGetReply.ERROR_CODE_SECRET_NOT_FOUND); + } + private void deleteHostSecretBestEffort(String hostUuid, String vmUuid, Integer keyVersion, String reason) { - if (StringUtils.isBlank(hostUuid) || StringUtils.isBlank(vmUuid)) { + if (StringUtils.isBlank(hostUuid) || StringUtils.isBlank(vmUuid) || keyVersion == null) { return; } @@ -587,9 +617,11 @@ private void deleteHostSecretBestEffort(String hostUuid, String vmUuid, Integer @Override public void run(MessageReply reply) { if (!reply.isSuccess()) { + ErrorCode err = reply.getError(); + String errMsg = err != null && err.getDetails() != null ? err.getDetails() : "unknown error"; logger.warn(String.format( "best-effort delete host secret failed on %s for vm[uuid:%s], host[uuid:%s]: %s", - reason, vmUuid, hostUuid, reply.getError().getDetails())); + reason, vmUuid, hostUuid, errMsg)); } } }); 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 dc470cbbcde..99a962d0bcb 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 @@ -420,8 +420,10 @@ public void done(ErrorCodeList errorCodeList) { @Override public void run(MessageReply reply) { if (!reply.isSuccess()) { + ErrorCode err = reply.getError(); + String errMsg = err != null && err.getDetails() != null ? err.getDetails() : "unknown error"; logger.warn(String.format("failed to delete host secret on host[uuid:%s] for vm[uuid:%s], continue cleanup: %s", - hostUuid, context.vmInstanceUuid, reply.getError().getDetails())); + hostUuid, context.vmInstanceUuid, errMsg)); } whileCompletion.done(); } @@ -717,6 +719,10 @@ public void done(ErrorCodeList errorCodeList) { .build()) .then(Flow.of("delete-host-secret") .handle(trigger -> { + if (context.keyVersion == null) { + trigger.next(); + return; + } Set hostUuids = new HashSet<>(); for (VmHostFileVO file : context.hostFiles) { hostUuids.add(file.getHostUuid()); @@ -740,8 +746,10 @@ public void done(ErrorCodeList errorCodeList) { @Override public void run(MessageReply reply) { if (!reply.isSuccess()) { + ErrorCode err = reply.getError(); + String errMsg = err != null && err.getDetails() != null ? err.getDetails() : "unknown error"; logger.warn(String.format("failed to delete host secret on host[uuid:%s] for vm[uuid:%s], continue reset: %s", - hostUuid, vmUuid, reply.getError().getDetails())); + hostUuid, vmUuid, errMsg)); } whileCompletion.done(); } diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/tpm/HostSecretKvmAgentSimulatorCase.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/tpm/HostSecretKvmAgentSimulatorCase.groovy new file mode 100644 index 00000000000..43cc12ae770 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/kvm/tpm/HostSecretKvmAgentSimulatorCase.groovy @@ -0,0 +1,128 @@ +package org.zstack.test.integration.kvm.tpm + +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.zstack.header.Constants +import org.zstack.header.secret.SecretHostGetReply +import org.zstack.kvm.KVMAgentCommands +import org.zstack.kvm.KVMConstant +import org.zstack.test.integration.kvm.KvmTest +import org.zstack.testlib.EnvSpec +import org.zstack.testlib.KVMSimulator +import org.zstack.testlib.SubCase +import org.zstack.utils.gson.JSONObjectUtil + +class HostSecretKvmAgentSimulatorCase extends SubCase { + EnvSpec env + + @Override + void setup() { + useSpring(KvmTest.springSpec) + } + + @Override + void environment() { + env = makeEnv { + zone { + name = "zone" + cluster { + name = "cluster" + kvm { + name = "kvm1" + managementIp = "127.0.0.12" + username = "root" + password = "password" + } + } + } + } + } + + @Override + void test() { + env.create { + testSimulatedGetSecretErrorMatchesMnConstant() + testEnsureThenGetReturnsSameUuid() + testDeleteSecretRemovesCacheEntry() + } + } + + void testSimulatedGetSecretErrorMatchesMnConstant() { + KVMSimulator.resetSimulatedHostSecretCache() + def hostUuid = "sim-host-1" + def cmd = new KVMAgentCommands.SecretHostGetCmd() + cmd.vmUuid = "sim-vm-1" + cmd.purpose = "vtpm" + cmd.keyVersion = 1 + cmd.usageInstance = KVMConstant.HOST_SECRET_USAGE_INSTANCE_VTPM + def headers = new HttpHeaders() + headers.add(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID, hostUuid) + def entity = new HttpEntity(JSONObjectUtil.toJsonString(cmd), headers) + + def sim = env.getSimulator(KVMConstant.KVM_GET_SECRET_PATH) + assert sim != null + def rsp = sim(entity) as KVMAgentCommands.SecretHostGetResponse + assert !rsp.success + assert SecretHostGetReply.ERROR_CODE_SECRET_NOT_FOUND == rsp.error + } + + void testEnsureThenGetReturnsSameUuid() { + KVMSimulator.resetSimulatedHostSecretCache() + def hostUuid = "sim-host-2" + def vmUuid = "sim-vm-2" + def purpose = "vtpm" + def keyVersion = 2 + + def ensureCmd = new KVMAgentCommands.SecretHostDefineCmd() + ensureCmd.vmUuid = vmUuid + ensureCmd.purpose = purpose + ensureCmd.keyVersion = keyVersion + ensureCmd.encryptedDek = "ZHVtbXk=" + ensureCmd.description = "" + ensureCmd.usageInstance = KVMConstant.HOST_SECRET_USAGE_INSTANCE_VTPM + + def headers = new HttpHeaders() + headers.add(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID, hostUuid) + def ensureEntity = new HttpEntity(JSONObjectUtil.toJsonString(ensureCmd), headers) + def ensureSim = env.getSimulator(KVMConstant.KVM_ENSURE_SECRET_PATH) + def ensureRsp = ensureSim(ensureEntity) as KVMAgentCommands.SecretHostDefineResponse + assert ensureRsp.success + assert ensureRsp.secretUuid != null + + def getCmd = new KVMAgentCommands.SecretHostGetCmd() + getCmd.vmUuid = vmUuid + getCmd.purpose = purpose + getCmd.keyVersion = keyVersion + getCmd.usageInstance = KVMConstant.HOST_SECRET_USAGE_INSTANCE_VTPM + def getEntity = new HttpEntity(JSONObjectUtil.toJsonString(getCmd), headers) + def getSim = env.getSimulator(KVMConstant.KVM_GET_SECRET_PATH) + def getRsp = getSim(getEntity) as KVMAgentCommands.SecretHostGetResponse + assert getRsp.success + assert ensureRsp.secretUuid == getRsp.secretUuid + } + + void testDeleteSecretRemovesCacheEntry() { + KVMSimulator.resetSimulatedHostSecretCache() + def hostUuid = "sim-host-3" + def vmUuid = "sim-vm-3" + def purpose = "vtpm" + def keyVersion = 3 + + KVMSimulator.putSimulatedHostSecretForTest(hostUuid, vmUuid, purpose, keyVersion, "to-be-removed") + + def delCmd = new KVMAgentCommands.SecretHostDeleteCmd() + delCmd.vmUuid = vmUuid + delCmd.purpose = purpose + delCmd.keyVersion = keyVersion + delCmd.usageInstance = KVMConstant.HOST_SECRET_USAGE_INSTANCE_VTPM + def headers = new HttpHeaders() + headers.add(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID, hostUuid) + def delEntity = new HttpEntity(JSONObjectUtil.toJsonString(delCmd), headers) + def delSim = env.getSimulator(KVMConstant.KVM_DELETE_SECRET_PATH) + assert delSim != null + def delRsp = delSim(delEntity) as KVMAgentCommands.SecretHostDeleteResponse + assert delRsp.success + + assert KVMSimulator.getSimulatedHostSecretForTest(hostUuid, vmUuid, purpose, keyVersion) == null + } +} diff --git a/test/src/test/groovy/org/zstack/test/integration/kvm/tpm/KvmTpmHostSecretSimulatorTest.groovy b/test/src/test/groovy/org/zstack/test/integration/kvm/tpm/KvmTpmHostSecretSimulatorTest.groovy new file mode 100644 index 00000000000..13b60f51a67 --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/integration/kvm/tpm/KvmTpmHostSecretSimulatorTest.groovy @@ -0,0 +1,10 @@ +package org.zstack.test.integration.kvm.tpm + +import org.zstack.test.integration.kvm.KvmTest + +/** + * Runs {@link HostSecretKvmAgentSimulatorCase} against {@link org.zstack.testlib.KVMSimulator} + * host secret HTTP handlers (getSecret / ensureSecret / deleteSecret). + */ +class KvmTpmHostSecretSimulatorTest extends KvmTest { +} diff --git a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy index 416e4d8feb8..e229b0fd4c1 100755 --- a/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy +++ b/testlib/src/main/java/org/zstack/testlib/KVMSimulator.groovy @@ -353,7 +353,7 @@ class KVMSimulator implements Simulator { spec.simulator(KVMConstant.KVM_ENSURE_SECRET_PATH) { HttpEntity e -> String hostUuid = e.getHeaders().getFirst(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID) SecretHostDefineCmd cmd = JSONObjectUtil.toObject(e.body, SecretHostDefineCmd.class) - String cacheKey = buildHostSecretCacheKey(hostUuid, cmd?.vmUuid, cmd?.purpose, cmd?.keyVersion) + String cacheKey = buildHostSecretCacheKey(hostUuid, cmd?.vmUuid, cmd?.purpose, cmd?.keyVersion, cmd?.usageInstance) def rsp = new SecretHostDefineResponse() rsp.secretUuid = ensureSecretUuidCache.computeIfAbsent(cacheKey) { Platform.uuid } return rsp @@ -362,7 +362,7 @@ class KVMSimulator implements Simulator { spec.simulator(KVMConstant.KVM_GET_SECRET_PATH) { HttpEntity e -> String hostUuid = e.getHeaders().getFirst(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID) SecretHostGetCmd cmd = JSONObjectUtil.toObject(e.body, SecretHostGetCmd.class) - String cacheKey = buildHostSecretCacheKey(hostUuid, cmd?.vmUuid, cmd?.purpose, cmd?.keyVersion) + String cacheKey = buildHostSecretCacheKey(hostUuid, cmd?.vmUuid, cmd?.purpose, cmd?.keyVersion, cmd?.usageInstance) def rsp = new SecretHostGetResponse() String secretUuid = ensureSecretUuidCache.get(cacheKey) if (secretUuid == null) { @@ -373,6 +373,14 @@ class KVMSimulator implements Simulator { return rsp } + spec.simulator(KVMConstant.KVM_DELETE_SECRET_PATH) { HttpEntity e -> + String hostUuid = e.getHeaders().getFirst(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID) + SecretHostDeleteCmd cmd = JSONObjectUtil.toObject(e.body, SecretHostDeleteCmd.class) + String cacheKey = buildHostSecretCacheKey(hostUuid, cmd?.vmUuid, cmd?.purpose, cmd?.keyVersion, cmd?.usageInstance) + ensureSecretUuidCache.remove(cacheKey) + return new SecretHostDeleteResponse() + } + spec.simulator(KVMConstant.KVM_ECHO_PATH) { HttpEntity e -> Spec.checkHttpCallType(e, true) return [:] @@ -758,11 +766,27 @@ class KVMSimulator implements Simulator { } } - private static String buildHostSecretCacheKey(String hostUuid, String vmUuid, String purpose, Integer keyVersion) { - return String.format("%s::%s::%s::%s", + /** Resets simulated host vTPM secret entries for getSecret / ensureSecret / deleteSecret. */ + static void resetSimulatedHostSecretCache() { + ensureSecretUuidCache.clear() + } + + static void putSimulatedHostSecretForTest(String hostUuid, String vmUuid, String purpose, + Integer keyVersion, String secretUuid) { + ensureSecretUuidCache.put(buildHostSecretCacheKey(hostUuid, vmUuid, purpose, keyVersion, KVMConstant.HOST_SECRET_USAGE_INSTANCE_VTPM), secretUuid) + } + + static String getSimulatedHostSecretForTest(String hostUuid, String vmUuid, String purpose, Integer keyVersion) { + return ensureSecretUuidCache.get(buildHostSecretCacheKey(hostUuid, vmUuid, purpose, keyVersion, KVMConstant.HOST_SECRET_USAGE_INSTANCE_VTPM)) + } + + private static String buildHostSecretCacheKey(String hostUuid, String vmUuid, String purpose, Integer keyVersion, String usageInstance) { + String usage = usageInstance ?: KVMConstant.HOST_SECRET_USAGE_INSTANCE_VTPM + return String.format("%s::%s::%s::%s::%s", hostUuid ?: "", vmUuid ?: "", purpose ?: "", - keyVersion == null ? "" : String.valueOf(keyVersion)) + keyVersion == null ? "" : String.valueOf(keyVersion), + usage) } } From 3eee7995e62925df95944756a7a0e27984b43417 Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 10 Apr 2026 11:25:48 +0800 Subject: [PATCH 126/129] [kvm]: fix NPE in VmHostFileTracker sync When all getLastSyncDate() values in the stream are null, the min() operation with Comparator.nullsFirst() throws NPE because Optional.of() doesn't accept null. Filter null values before calling min() to avoid this issue. Resolves: ZSV-11779 Related: ZSV-11310 Change-Id: I646a65776a717465636e75616c70746763727862 --- .../main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java index 4ba9269aba4..c68369842fc 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/vmfiles/VmHostFileTracker.java @@ -269,7 +269,8 @@ private void syncVmHostFiles() { // check if force sync is needed based on lastSyncDate Timestamp oldestLastSync = group.stream() .map(VmHostFileVO::getLastSyncDate) - .min(Comparator.nullsFirst(Comparator.naturalOrder())) + .filter(Objects::nonNull) + .min(Comparator.naturalOrder()) .orElse(null); if (oldestLastSync != null && (now - oldestLastSync.getTime()) < forceSyncThresholdMs) { From b13926a6165ba2b08a4d38f3ed288dd8a976a99a Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Fri, 10 Apr 2026 17:07:27 +0800 Subject: [PATCH 127/129] [kvm]: disable host status check when syncing VM host files Add disableHostStatusCheck() method to KvmCommandSender to allow skipping host status validation. Apply it in KvmSecureBootManager when syncing VM host files, so the sync can proceed regardless of host connection state. Resolves: ZSV-11767 Related: ZSV-11310 Change-Id: I6c68756c7378636a6c6669656869717077716268 --- .../kvm/src/main/java/org/zstack/kvm/KvmCommandSender.java | 5 +++++ .../main/java/org/zstack/kvm/efi/KvmSecureBootManager.java | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KvmCommandSender.java b/plugin/kvm/src/main/java/org/zstack/kvm/KvmCommandSender.java index 139f417c221..d8e711d6718 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KvmCommandSender.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KvmCommandSender.java @@ -69,6 +69,11 @@ public KvmCommandSender(String hostUuid, boolean noStatusCheck) { DebugUtils.Assert(hostUuid != null, "hostUuid cannot be null"); } + public KvmCommandSender disableHostStatusCheck() { + this.noStatusCheck = true; + return this; + } + public void send(final Object cmd, final String path, final KvmCommandFailureChecker checker, final SteppingSendCallback completion) { List msgs = hostUuids.stream().map(huuid -> { KVMHostAsyncHttpCallMsg msg = new KVMHostAsyncHttpCallMsg(); 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 45480bb7aed..a9a8b152b2c 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 @@ -204,7 +204,8 @@ static class CloneVmHostFileContext { } private void handle(SyncVmHostFilesFromHostMsg msg) { - KvmCommandSender sender = new KvmCommandSender(msg.getHostUuid()); + KvmCommandSender sender = new KvmCommandSender(msg.getHostUuid()) + .disableHostStatusCheck(); KVMAgentCommands.ReadVmHostFileContentCmd cmd = new KVMAgentCommands.ReadVmHostFileContentCmd(); cmd.setHostFiles(new ArrayList<>()); From 14b3e0faaa80a2eea36e05fc7d2796c85d4a5efe Mon Sep 17 00:00:00 2001 From: Zhang Wenhao Date: Sat, 11 Apr 2026 17:45:18 +0800 Subject: [PATCH 128/129] [kvm]: mark TPM VM host files changed on start/shutdown When a VM with TPM starts or shuts down, NvRam/TpmState data must have changed. Preemptively set changeDate on the corresponding VmHostFileVO so the periodic tracker knows to sync them, even if the direct sync fails. Also add ResourceDestinationMaker check to all VM canonical event handlers in KvmSecureBootManager to ensure only the owning management node processes each event. Resolves: ZSV-11779 Related: ZSV-11310 Change-Id: I6e6d6c6c647175716669756b75756a6f72657277 --- .../compute/vm/devices/VmTpmManager.java | 30 ++++++- .../kvm/efi/KvmSecureBootExtensions.java | 45 ++++------ .../zstack/kvm/efi/KvmSecureBootManager.java | 90 ++++++++++++++++--- .../zstack/kvm/efi/KvmVmHostFileFactory.java | 19 ++++ 4 files changed, 139 insertions(+), 45 deletions(-) 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 757696c6c1d..b486fe09379 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,20 +1,28 @@ 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.header.vm.additions.VmHostFileType; import org.zstack.resourceconfig.ResourceConfig; import org.zstack.resourceconfig.ResourceConfigFacade; import org.zstack.utils.Utils; import org.zstack.utils.logging.CLogger; +import java.util.Collections; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; import static org.zstack.compute.vm.VmGlobalConfig.ENABLE_UEFI_SECURE_BOOT; +import static org.zstack.header.vm.additions.VmHostFileType.NvRam; +import static org.zstack.header.vm.additions.VmHostFileType.TpmState; +import static org.zstack.utils.CollectionDSL.list; public class VmTpmManager { private static final CLogger logger = Utils.getLogger(VmTpmManager.class); @@ -58,13 +66,27 @@ public static boolean isUefiBootMode(String bootMode) { } public boolean needRegisterNvRam(String vmUuid) { - boolean tpmExists = Q.New(TpmVO.class) + return needRegister(NvRam, vmUuid); + } + + public Set vmHostFileTypeNeedRegisterForVm(String vmUuid) { + String bootMode = VmSystemTags.BOOT_MODE.getTokenByResourceUuid(vmUuid, VmSystemTags.BOOT_MODE_TOKEN); + if (!isUefiBootMode(bootMode)) { + return Collections.emptySet(); + } + + boolean hasTpm = Q.New(TpmVO.class) .eq(TpmVO_.vmInstanceUuid, vmUuid) .isExists(); - if (tpmExists) { - return true; + if (hasTpm) { + return new HashSet<>(list(NvRam, TpmState)); } ResourceConfig resourceConfig = resourceConfigFacade.getResourceConfig(ENABLE_UEFI_SECURE_BOOT.getIdentity()); - return resourceConfig.getResourceConfigValue(vmUuid, Boolean.class); + return resourceConfig.getResourceConfigValue(vmUuid, Boolean.class) == Boolean.TRUE ? + new HashSet<>(list(NvRam)) : Collections.emptySet(); + } + + public boolean needRegister(VmHostFileType type, String vmUuid) { + return vmHostFileTypeNeedRegisterForVm(vmUuid).contains(type); } } 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 35ff290762a..a30a6691f93 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,7 +3,6 @@ import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; 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; @@ -23,8 +22,6 @@ import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.exception.CloudRuntimeException; 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.VmInstanceVO; @@ -79,7 +76,6 @@ 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.additions.VmHostFileSyncReason.PostMigration; import static org.zstack.header.vm.additions.VmHostFileSyncReason.BeforeHaStart; @@ -87,6 +83,7 @@ import static org.zstack.header.vm.additions.VmHostFileSyncReason.PrepareRead; import static org.zstack.header.vm.additions.VmHostFileSyncReason.ResourceRelease; import static org.zstack.header.vm.additions.VmHostFileSyncReason.SnapshotGroupOnlineBackup; +import static org.zstack.header.vm.additions.VmHostFileType.NvRam; import static org.zstack.kvm.KVMConstant.*; import static org.zstack.utils.CollectionDSL.list; @@ -107,6 +104,8 @@ public class KvmSecureBootExtensions implements KVMStartVmExtensionPoint, private ResourceConfigFacade resourceConfigFacade; @Autowired private DatabaseFacade databaseFacade; + @Autowired + private KvmVmHostFileFactory vmHostFileFactory; private final Object hostFileLock = new Object(); @@ -141,7 +140,7 @@ private void prepareNvRamToStartVmCmd(KVMAgentCommands.StartVmCmd cmd, NvRamSpec final Timestamp now = Timestamp.from(Instant.now()); VmHostFileVO nvRamFile = Q.New(VmHostFileVO.class) .eq(VmHostFileVO_.vmInstanceUuid, cmd.getVmInstanceUuid()) - .eq(VmHostFileVO_.type, VmHostFileType.NvRam) + .eq(VmHostFileVO_.type, NvRam) .eq(VmHostFileVO_.hostUuid, host.getUuid()) .find(); if (nvRamFile == null) { @@ -149,7 +148,7 @@ private void prepareNvRamToStartVmCmd(KVMAgentCommands.StartVmCmd cmd, NvRamSpec nvRamFile.setUuid(Platform.getUuid()); nvRamFile.setHostUuid(host.getUuid()); nvRamFile.setVmInstanceUuid(cmd.getVmInstanceUuid()); - nvRamFile.setType(VmHostFileType.NvRam); + nvRamFile.setType(NvRam); nvRamFile.setPath(volume.getInstallPath()); nvRamFile.setCreateDate(now); nvRamFile.setResourceName("NvRam file for " + cmd.getVmInstanceUuid()); @@ -180,29 +179,17 @@ private void prepareNvRamBeforeMigration(VmInstanceInventory vm, String dstHostU return; } - String tpmUuid = Q.New(TpmVO.class) - .eq(TpmVO_.vmInstanceUuid, vm.getUuid()) - .select(TpmVO_.uuid) - .findValue(); - boolean needRegisterNvRam = tpmUuid != null; + boolean needRegisterNvRam = vmHostFileFactory.needRegister(NvRam, vm.getUuid()); 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; - } + 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.setType(NvRam.toString()); to.setOperation(VmHostFileOperation.Prepare.toString()); RewriteVmHostFilesContext context = new RewriteVmHostFilesContext(); @@ -290,7 +277,7 @@ public void preInstantiateVmResource(VmInstanceSpec spec, Completion completion) PrepareHostFileContext context = new PrepareHostFileContext(); context.hostUuid = spec.getDestHost().getUuid(); context.vmUuid = spec.getVmInventory().getUuid(); - context.type = VmHostFileType.NvRam; + context.type = NvRam; context.backupUuid = nvRamSpec.getBackupFileUuid(); context.syncReason = "pre-instantiate VM resource"; prepareHostFileOnHost(context, completion); @@ -349,7 +336,7 @@ public void run(FlowTrigger trigger, Map data) { syncMsg.setVmUuid(context.vmUuid); syncMsg.setSyncReason(PrepareRead.reason(context.syncReason)); - if (vmHostFile.getType() == VmHostFileType.NvRam) { + if (vmHostFile.getType() == NvRam) { context.path = vmHostFile.getPath(); syncMsg.setNvRamPath(context.path); } else if (vmHostFile.getType() == VmHostFileType.TpmState) { @@ -471,7 +458,7 @@ public void run(FlowTrigger trigger, Map data) { syncMsg.setVmUuid(context.vmUuid); syncMsg.setSyncReason(PrepareReRead.reason(context.syncReason)); - if (context.type == VmHostFileType.NvRam) { + if (context.type == NvRam) { syncMsg.setNvRamPath(context.path); } else if (context.type == VmHostFileType.TpmState) { syncMsg.setTpmStateFolder(context.path); @@ -574,7 +561,7 @@ public void releaseVmResource(VmInstanceSpec spec, Completion completion) { syncMsg.setSyncReason(ResourceRelease.reason()); for (VmHostFileVO file : vmHostFiles) { - if (file.getType() == VmHostFileType.NvRam) { + if (file.getType() == NvRam) { syncMsg.setNvRamPath(file.getPath()); } else if (file.getType() == VmHostFileType.TpmState) { syncMsg.setTpmStateFolder(file.getPath()); @@ -633,7 +620,7 @@ public void beforeHaStartVmInstance(String vmUuid, String judgerClassName, List< syncMsg.setSyncReason(BeforeHaStart.reason()); for (VmHostFileVO file : vmHostFiles) { - if (file.getType() == VmHostFileType.NvRam) { + if (file.getType() == NvRam) { syncMsg.setNvRamPath(file.getPath()); } else if (file.getType() == VmHostFileType.TpmState) { syncMsg.setTpmStateFolder(file.getPath()); @@ -699,7 +686,7 @@ public void afterMigrateVm(VmInstanceInventory inv, String srcHostUuid, NoErrorC syncMsg.setSyncReason(PostMigration.reason()); for (VmHostFileVO file : vmHostFiles) { - if (file.getType() == VmHostFileType.NvRam) { + if (file.getType() == NvRam) { syncMsg.setNvRamPath(file.getPath()); } else if (file.getType() == VmHostFileType.TpmState) { syncMsg.setTpmStateFolder(file.getPath()); @@ -759,7 +746,7 @@ public void afterVolumeLiveSnapshotGroupCreatedOnBackend(CreateVolumesSnapshotOv syncMsg.setHostUuid(hostUuid); for (VmHostFileVO file : hostFiles) { - if (file.getType() == VmHostFileType.NvRam) { + if (file.getType() == NvRam) { syncMsg.setNvRamPath(buildNvramSnapshotBackupFilePath(vmUuid)); } else if (file.getType() == VmHostFileType.TpmState) { syncMsg.setTpmStateFolder(buildTpmStateSnapshotBackupFilePath(vmUuid)); 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 a9a8b152b2c..5a62cc6dc6d 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 @@ -8,6 +8,7 @@ import org.zstack.core.cloudbus.EventCallback; import org.zstack.core.cloudbus.EventFacadeImpl; import org.zstack.core.cloudbus.MessageSafe; +import org.zstack.core.cloudbus.ResourceDestinationMaker; import org.zstack.core.db.DatabaseFacade; import org.zstack.core.db.Q; import org.zstack.core.db.SQL; @@ -27,8 +28,6 @@ import org.zstack.header.exception.CloudRuntimeException; import org.zstack.header.message.Message; 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.VmCanonicalEvents; import org.zstack.header.vm.VmInstanceConstant; import org.zstack.header.vm.VmInstanceVO; @@ -76,7 +75,6 @@ import java.util.Set; import java.util.function.Function; -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.core.Platform.operr; import static org.zstack.header.vm.additions.VmHostFileSyncReason.PostClone; @@ -104,6 +102,8 @@ public class KvmSecureBootManager extends AbstractService { private KvmVmHostFileFactory vmHostFileFactory; @Autowired private TimeHelper timeHelper; + @Autowired + private ResourceDestinationMaker resourceDestinationMaker; @Override public boolean start() { @@ -118,10 +118,27 @@ public boolean stop() { @SuppressWarnings("rawtypes") private void setupCanonicalEvents() { + eventFacade.on(VmCanonicalEvents.VM_LIBVIRT_REPORT_START, new EventCallback() { + @Override + protected void run(Map tokens, Object data) { + String vmUuid = (String) data; + boolean managedByMe = resourceDestinationMaker.isManagedByUs(vmUuid); + if (!managedByMe) { + return; + } + markVmHostFilesChanged(vmUuid); + } + }); + eventFacade.on(VmCanonicalEvents.VM_LIBVIRT_REPORT_SHUTDOWN, new EventCallback() { @Override protected void run(Map tokens, Object data) { String vmUuid = (String) data; + boolean managedByMe = resourceDestinationMaker.isManagedByUs(vmUuid); + if (!managedByMe) { + return; + } + Tuple tuple = Q.New(VmInstanceVO.class) .select(VmInstanceVO_.hostUuid, VmInstanceVO_.lastHostUuid) .eq(VmInstanceVO_.uuid, vmUuid) @@ -134,6 +151,7 @@ protected void run(Map tokens, Object data) { if (hostUuid == null) { hostUuid = (String) tuple.get(1); } + markVmHostFilesChanged(vmUuid, hostUuid); List hostFiles = Q.New(VmHostFileVO.class) .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) @@ -173,6 +191,55 @@ public void run(MessageReply reply) { }); } + /** + * Preemptive judgment: when a VM with TPM (or enabled secure boot) starts or shuts down, + * the NvRam/TpmState data must have changed, so mark the corresponding + * VmHostFileVO.changeDate to current time. + */ + private void markVmHostFilesChanged(String vmUuid) { + 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); + } + if (hostUuid == null) { + return; + } + + markVmHostFilesChanged(vmUuid, hostUuid); + } + + private void markVmHostFilesChanged(String vmUuid, String hostUuid) { + if (hostUuid == null) { + return; + } + + final Set types = vmHostFileFactory.vmHostFileTypeNeedRegisterForVm(vmUuid); + if (types.isEmpty()) { + return; + } + + Timestamp now = new Timestamp(timeHelper.getCurrentTimeMillis()); + long updated = SQL.New(VmHostFileVO.class) + .eq(VmHostFileVO_.vmInstanceUuid, vmUuid) + .eq(VmHostFileVO_.hostUuid, hostUuid) + .in(VmHostFileVO_.type, types) + .set(VmHostFileVO_.changeDate, now) + .update(); + + if (updated > 0) { + logger.debug(String.format("preemptively marked VmHostFiles as changed for VM[uuid:%s] on host[uuid:%s], %d records updated", + vmUuid, hostUuid, updated)); + } + } + @Override public String getId() { return bus.makeLocalServiceId(VmInstanceConstant.SECURE_BOOT_SERVICE_ID); @@ -473,22 +540,21 @@ protected void scripts() { 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) { + final Set types = vmHostFileFactory.vmHostFileTypeNeedRegisterForVm(msg.getSrcVmUuid()); + if (types.isEmpty()) { bus.reply(msg, reply); return; } CloneVmHostFileContext context = new CloneVmHostFileContext(); - context.typesNeedClone.add(VmHostFileType.NvRam); - if (hasTpm) { + if (types.contains(VmHostFileType.NvRam)) { + context.typesNeedClone.add(VmHostFileType.NvRam); + } + + if (types.contains(VmHostFileType.TpmState)) { boolean resetTpm; if (msg.getResetTpm() == null) { - resourceConfig = resourceConfigFacade.getResourceConfig(RESET_TPM_AFTER_VM_CLONE.getIdentity()); + ResourceConfig resourceConfig = resourceConfigFacade.getResourceConfig(RESET_TPM_AFTER_VM_CLONE.getIdentity()); resetTpm = resourceConfig.getResourceConfigValue(msg.getSrcVmUuid(), Boolean.class); } else { resetTpm = msg.getResetTpm(); diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmVmHostFileFactory.java b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmVmHostFileFactory.java index 0aaaad0cc9a..c4e13957cdf 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmVmHostFileFactory.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/efi/KvmVmHostFileFactory.java @@ -1,15 +1,26 @@ package org.zstack.kvm.efi; +import org.springframework.beans.factory.annotation.Autowired; +import org.zstack.compute.vm.devices.VmTpmManager; import org.zstack.header.vm.additions.VmHostBackupFileVO; +import org.zstack.header.vm.additions.VmHostFileType; import org.zstack.header.vm.additions.VmHostFileVO; import org.zstack.kvm.tpm.TpmStateVmHostBackupFileBase; import org.zstack.kvm.tpm.TpmStateVmHostFileBase; import org.zstack.kvm.vmfiles.AbstractVmHostBackupFileBase; import org.zstack.kvm.vmfiles.AbstractVmHostFileBase; +import org.zstack.resourceconfig.ResourceConfigFacade; + +import java.util.Set; import static org.zstack.core.Platform.operr; public class KvmVmHostFileFactory { + @Autowired + private ResourceConfigFacade resourceConfigFacade; + @Autowired + private VmTpmManager vmTpmManager; + public AbstractVmHostFileBase createBase(VmHostFileVO file) { switch (file.getType()) { case NvRam: return new NvRamVmHostFileBase(file); @@ -25,4 +36,12 @@ public AbstractVmHostBackupFileBase createBackupBase(VmHostBackupFileVO backupFi default: throw operr("invalid VM host file type: " + backupFile.getType()).toException(); } } + + public Set vmHostFileTypeNeedRegisterForVm(String vmUuid) { + return vmTpmManager.vmHostFileTypeNeedRegisterForVm(vmUuid); + } + + public boolean needRegister(VmHostFileType type, String vmUuid) { + return vmTpmManager.needRegister(type, vmUuid); + } } From 8d32ee7c640b81871baf576f9595a37835339648 Mon Sep 17 00:00:00 2001 From: "zhong.zhou" Date: Sat, 11 Apr 2026 14:28:50 +0800 Subject: [PATCH 129/129] [kvm]: define secret on migrate destination also: - cleanup EncryptedResourceKeyRefVO when VM creation failed Resolves: ZSV-11729 Resolves: ZSV-11845 Change-Id: I616a776e78666179637976616c776162626c6533 --- .../DummyEncryptedResourceKeyManager.java | 8 + .../EncryptedResourceKeyManager.java | 36 ++-- .../header/secret/SecretHostDefineMsg.java | 18 ++ .../header/secret/SecretHostDeleteMsg.java | 9 + .../header/secret/SecretHostGetMsg.java | 9 + .../java/org/zstack/kvm/KVMAgentCommands.java | 27 ++- .../src/main/java/org/zstack/kvm/KVMHost.java | 27 ++- .../org/zstack/kvm/tpm/KvmTpmExtensions.java | 181 ++++++++++++------ .../org/zstack/kvm/tpm/KvmTpmManager.java | 2 + .../kvm/tpm/VtpmMigratePreAgentContext.java | 102 ++++++++++ 10 files changed, 326 insertions(+), 93 deletions(-) create mode 100644 plugin/kvm/src/main/java/org/zstack/kvm/tpm/VtpmMigratePreAgentContext.java diff --git a/compute/src/main/java/org/zstack/compute/vm/devices/DummyEncryptedResourceKeyManager.java b/compute/src/main/java/org/zstack/compute/vm/devices/DummyEncryptedResourceKeyManager.java index e7fb4cac4e2..0bd6a8eaaae 100644 --- a/compute/src/main/java/org/zstack/compute/vm/devices/DummyEncryptedResourceKeyManager.java +++ b/compute/src/main/java/org/zstack/compute/vm/devices/DummyEncryptedResourceKeyManager.java @@ -1,5 +1,6 @@ package org.zstack.compute.vm.devices; +import org.zstack.header.errorcode.OperationFailureException; import org.zstack.header.keyprovider.EncryptedResourceKeyManager; import org.zstack.header.core.Completion; import org.zstack.header.core.ReturnValueCompletion; @@ -19,6 +20,13 @@ public void getOrCreateKey(GetOrCreateResourceKeyContext ctx, completion.fail(operr("crypto module is not installed, cannot manage resource encryption keys")); } + @Override + public ResourceKeyResult getKey(GetOrCreateResourceKeyContext ctx) { + logger.warn(String.format("crypto module not installed, cannot get resource key for %s[uuid:%s]", + ctx.getResourceType(), ctx.getResourceUuid())); + throw new OperationFailureException(operr("crypto module is not installed, cannot manage resource encryption keys")); + } + @Override public void rollbackCreatedKey(ResourceKeyResult result, Completion completion) { completion.success(); diff --git a/header/src/main/java/org/zstack/header/keyprovider/EncryptedResourceKeyManager.java b/header/src/main/java/org/zstack/header/keyprovider/EncryptedResourceKeyManager.java index 8a61c02ce83..ffa38675402 100644 --- a/header/src/main/java/org/zstack/header/keyprovider/EncryptedResourceKeyManager.java +++ b/header/src/main/java/org/zstack/header/keyprovider/EncryptedResourceKeyManager.java @@ -26,11 +26,34 @@ public interface EncryptedResourceKeyManager { void getOrCreateKey(GetOrCreateResourceKeyContext ctx, ReturnValueCompletion completion); + /** + * Load the existing resource encryption key material only. + *

+ * Requires an {@code EncryptedResourceKeyRef} row and a usable secret reference already stored + * for the resource. Does not insert a ref row and does not call + * key-tool/KMS create APIs. + *

+ * The implementation may still call key-tool/KMS get/unwrap for the existing + * secret ref in order to return the plaintext DEK (for example defining the secret on the destination + * host during hot migration). That RPC is read-side materialization, not secret creation. + *

+ * On success, the implementation may update {@code EncryptedResourceKeyRef} provider columns to match + * the resolved key provider when they have drifted (same behavior as the existing-key branch of + * {@link #getOrCreateKey}). + * + * @param ctx same fields as {@link #getOrCreateKey}; identifies resource and provider + * @return {@link ResourceKeyResult} with {@code createdNewKey == false} on success + * @throws org.zstack.header.errorcode.OperationFailureException when the key cannot be loaded + */ + ResourceKeyResult getKey(GetOrCreateResourceKeyContext ctx); + /** * Roll back a newly created resource key during upper-layer workflow rollback. *

- * If the key record already existed before creation, implementation should restore it - * to its previous empty-placeholder state instead of deleting the relationship. + * When {@link ResourceKeyResult#isCreatedNewKey()} is true, the implementation deletes the + * key-tool secret if one was materialized, then removes the {@code EncryptedResourceKeyRef} row + * for the resource (same storage effect as detaching the key provider from the resource). + * When {@code createdNewKey} is false (existing secret was reused), this is a no-op. */ void rollbackCreatedKey(ResourceKeyResult result, Completion completion); @@ -91,7 +114,6 @@ class ResourceKeyResult { private String dekBase64; private String secretRef; private boolean createdNewKey; - private boolean refExistedBeforeCreate; public String getResourceUuid() { return resourceUuid; @@ -156,13 +178,5 @@ public boolean isCreatedNewKey() { public void setCreatedNewKey(boolean createdNewKey) { this.createdNewKey = createdNewKey; } - - public boolean isRefExistedBeforeCreate() { - return refExistedBeforeCreate; - } - - public void setRefExistedBeforeCreate(boolean refExistedBeforeCreate) { - this.refExistedBeforeCreate = refExistedBeforeCreate; - } } } 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 2421e304d8d..d3258f249fd 100644 --- a/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java +++ b/header/src/main/java/org/zstack/header/secret/SecretHostDefineMsg.java @@ -16,6 +16,8 @@ public class SecretHostDefineMsg extends NeedReplyMessage implements HostMessage private String vmUuid; private String purpose; private Integer keyVersion; + private String usageInstance; + private String secretUuid; private String description; @Override @@ -59,6 +61,22 @@ public void setKeyVersion(Integer keyVersion) { this.keyVersion = keyVersion; } + public String getUsageInstance() { + return usageInstance; + } + + public void setUsageInstance(String usageInstance) { + this.usageInstance = usageInstance; + } + + public String getSecretUuid() { + return secretUuid; + } + + public void setSecretUuid(String secretUuid) { + this.secretUuid = secretUuid; + } + public String getDescription() { return description; } diff --git a/header/src/main/java/org/zstack/header/secret/SecretHostDeleteMsg.java b/header/src/main/java/org/zstack/header/secret/SecretHostDeleteMsg.java index 7c119e3183b..f9502d16c67 100644 --- a/header/src/main/java/org/zstack/header/secret/SecretHostDeleteMsg.java +++ b/header/src/main/java/org/zstack/header/secret/SecretHostDeleteMsg.java @@ -8,6 +8,7 @@ public class SecretHostDeleteMsg extends NeedReplyMessage implements HostMessage private String vmUuid; private String purpose; private Integer keyVersion; + private String usageInstance; @Override public String getHostUuid() { @@ -41,4 +42,12 @@ public Integer getKeyVersion() { public void setKeyVersion(Integer keyVersion) { this.keyVersion = keyVersion; } + + public String getUsageInstance() { + return usageInstance; + } + + public void setUsageInstance(String usageInstance) { + this.usageInstance = usageInstance; + } } diff --git a/header/src/main/java/org/zstack/header/secret/SecretHostGetMsg.java b/header/src/main/java/org/zstack/header/secret/SecretHostGetMsg.java index ecb787956cb..f63284880e0 100644 --- a/header/src/main/java/org/zstack/header/secret/SecretHostGetMsg.java +++ b/header/src/main/java/org/zstack/header/secret/SecretHostGetMsg.java @@ -11,6 +11,7 @@ public class SecretHostGetMsg extends NeedReplyMessage implements HostMessage { private String vmUuid; private String purpose; private Integer keyVersion; + private String usageInstance; @Override public String getHostUuid() { @@ -44,4 +45,12 @@ public Integer getKeyVersion() { public void setKeyVersion(Integer keyVersion) { this.keyVersion = keyVersion; } + + public String getUsageInstance() { + return usageInstance; + } + + public void setUsageInstance(String usageInstance) { + this.usageInstance = usageInstance; + } } 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 663357a193d..5b675f4ff72 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMAgentCommands.java @@ -411,8 +411,9 @@ public static class SecretHostDefineCmd extends AgentCommand { private String vmUuid; private String purpose; private Integer keyVersion; - private String description; private String usageInstance; + private String secretUuid; + private String description; public String getEncryptedDek() { return encryptedDek; @@ -446,14 +447,6 @@ public void setKeyVersion(Integer keyVersion) { this.keyVersion = keyVersion; } - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - public String getUsageInstance() { return usageInstance; } @@ -461,6 +454,22 @@ public String getUsageInstance() { public void setUsageInstance(String usageInstance) { this.usageInstance = usageInstance; } + + public String getSecretUuid() { + return secretUuid; + } + + public void setSecretUuid(String secretUuid) { + this.secretUuid = secretUuid; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } } public static class SecretHostDefineResponse extends AgentResponse { 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 3b3b3202882..994380b326f 100755 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHost.java @@ -5320,8 +5320,9 @@ public void handle(ErrorCode errCode, Map data) { private void handle(SecretHostGetMsg msg) { SecretHostGetReply reply = new SecretHostGetReply(); - if (StringUtils.isBlank(msg.getVmUuid()) || StringUtils.isBlank(msg.getPurpose()) || msg.getKeyVersion() == null) { - reply.setError(operr("vmUuid, purpose and keyVersion are required for get secret")); + if (StringUtils.isBlank(msg.getVmUuid()) || StringUtils.isBlank(msg.getPurpose()) || + msg.getKeyVersion() == null || StringUtils.isBlank(msg.getUsageInstance())) { + reply.setError(operr("vmUuid, purpose, keyVersion and usageInstance are required for get secret")); bus.reply(msg, reply); return; } @@ -5331,7 +5332,7 @@ private void handle(SecretHostGetMsg msg) { cmd.setVmUuid(msg.getVmUuid()); cmd.setPurpose(msg.getPurpose()); cmd.setKeyVersion(msg.getKeyVersion()); - cmd.setUsageInstance(KVMConstant.HOST_SECRET_USAGE_INSTANCE_VTPM); + cmd.setUsageInstance(msg.getUsageInstance()); Map headers = new HashMap<>(); headers.put(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID, getSelf().getUuid()); Http http = new Http<>(url, cmd, KVMAgentCommands.SecretHostGetResponse.class); @@ -5372,8 +5373,9 @@ private void handle(SecretHostDefineMsg msg) { bus.reply(msg, reply); return; } - if (StringUtils.isBlank(msg.getVmUuid()) || StringUtils.isBlank(msg.getPurpose()) || msg.getKeyVersion() == null) { - reply.setError(operr("vmUuid, purpose and keyVersion are required for ensure secret")); + if (StringUtils.isBlank(msg.getVmUuid()) || StringUtils.isBlank(msg.getPurpose()) || + msg.getKeyVersion() == null || StringUtils.isBlank(msg.getUsageInstance())) { + reply.setError(operr("vmUuid, purpose, keyVersion and usageInstance are required for ensure secret")); bus.reply(msg, reply); return; } @@ -5452,8 +5454,9 @@ private void handle(SecretHostDefineMsg msg) { cmd.setVmUuid(msg.getVmUuid()); cmd.setPurpose(msg.getPurpose()); cmd.setKeyVersion(msg.getKeyVersion()); + cmd.setSecretUuid(msg.getSecretUuid()); cmd.setDescription(msg.getDescription() != null ? msg.getDescription() : ""); - cmd.setUsageInstance(KVMConstant.HOST_SECRET_USAGE_INSTANCE_VTPM); + cmd.setUsageInstance(msg.getUsageInstance()); Map headers = new HashMap<>(); headers.put(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID, getSelf().getUuid()); Http http = new Http<>(url, cmd, KVMAgentCommands.SecretHostDefineResponse.class); @@ -5486,13 +5489,9 @@ public Class getReturnClass() { private void handle(SecretHostDeleteMsg msg) { SecretHostDeleteReply reply = new SecretHostDeleteReply(); - if (StringUtils.isBlank(msg.getVmUuid()) || StringUtils.isBlank(msg.getPurpose())) { - reply.setError(operr("vmUuid and purpose are required for delete secret")); - bus.reply(msg, reply); - return; - } - if (msg.getKeyVersion() == null) { - reply.setError(operr("keyVersion is required for delete secret")); + if (StringUtils.isBlank(msg.getVmUuid()) || StringUtils.isBlank(msg.getPurpose()) || + StringUtils.isBlank(msg.getUsageInstance()) || msg.getKeyVersion() == null) { + reply.setError(operr("vmUuid, purpose, keyVersion and usageInstance are required for delete secret")); bus.reply(msg, reply); return; } @@ -5502,7 +5501,7 @@ private void handle(SecretHostDeleteMsg msg) { cmd.setVmUuid(msg.getVmUuid()); cmd.setPurpose(msg.getPurpose()); cmd.setKeyVersion(msg.getKeyVersion()); - cmd.setUsageInstance(KVMConstant.HOST_SECRET_USAGE_INSTANCE_VTPM); + cmd.setUsageInstance(msg.getUsageInstance()); Map headers = new HashMap<>(); headers.put(Constants.AGENT_HTTP_HEADER_RESOURCE_UUID, getSelf().getUuid()); Http http = new Http<>(url, cmd, KVMAgentCommands.SecretHostDeleteResponse.class); 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 eaa846aeb5c..c6302660f1d 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 @@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.zstack.compute.vm.VmGlobalConfig; import org.zstack.compute.vm.devices.TpmEncryptedResourceKeyBackend; +import org.zstack.compute.vm.devices.VmTpmManager; import org.zstack.compute.vm.devices.TpmEncryptedResourceKeyBackend.CloneEncryptedResourceKeyContext; import org.zstack.core.Platform; import org.zstack.core.cloudbus.CloudBus; @@ -14,6 +15,7 @@ import org.zstack.core.db.SQL; import org.zstack.core.workflow.SimpleFlowChain; import org.zstack.header.core.Completion; +import org.zstack.header.errorcode.OperationFailureException; import org.zstack.header.core.workflow.FlowDoneHandler; import org.zstack.header.core.workflow.FlowErrorHandler; import org.zstack.header.core.workflow.Flow; @@ -42,6 +44,7 @@ import org.zstack.header.secret.SecretHostDefineReply; import org.zstack.header.tpm.message.RemoveTpmMsg; import org.zstack.header.vm.PreVmInstantiateResourceExtensionPoint; +import org.zstack.header.vm.VmInstanceConstant; import org.zstack.header.vm.VmInstanceInventory; import org.zstack.header.vm.VmInstanceSpec; import org.zstack.header.vm.VmInstanceState; @@ -183,6 +186,8 @@ public void preInstantiateVmResource(VmInstanceSpec spec, Completion completion) context.backupFileUuid = tpmSpec.getBackupFileUuid(); // maybe null context.providerUuid = resourceKeyBackend.findKeyProviderUuidByTpm(context.tpmUuid); context.keyVersion = resourceKeyBackend.findKeyVersionByTpm(context.tpmUuid); + context.instantiateForNewVm = spec.getCurrentVmOperation() == VmInstanceConstant.VmOperation.NewCreate; + context.enableKeyProvider = !VmGlobalConfig.ALLOWED_TPM_VM_WITHOUT_KMS.value(Boolean.class); final SimpleFlowChain chain = new SimpleFlowChain(); chain.setName("prepare-tpm-resources-for-vm-" + spec.getVmInventory().getUuid()); @@ -214,8 +219,7 @@ public void fail(ErrorCode errorCode) { @Override public boolean skip(Map data) { - return StringUtils.isBlank(findSourceTpmUuidFromSnapshotTpmBackupFile(context.backupFileUuid)) || - VmGlobalConfig.ALLOWED_TPM_VM_WITHOUT_KMS.value(Boolean.class); + return !context.enableKeyProvider; } @Override @@ -243,6 +247,9 @@ public void fail(ErrorCode errorCode) { }); } + // Use clone op above and will not set rollback flag TpmSpec.resourceKeyCreatedNew + // to true, so use flow rollback instead preReleaseVmResource rollback. And we + // definitely don't need to delete keytool secret on shapshot case. @Override public void rollback(FlowRollback trigger, Map data) { if (StringUtils.isNotBlank(context.backupFileUuid)) { @@ -256,72 +263,36 @@ public void rollback(FlowRollback trigger, Map data) { trigger.rollback(); } }).then(new NoRollbackFlow() { - String __name__ = "ensure-resource-key-ref"; + String __name__ = "get-secret-on-host-first"; @Override public boolean skip(Map data) { - boolean shouldSkip = StringUtils.isBlank(context.providerUuid); - if (shouldSkip) { - logger.info(String.format( - "skip ensure-resource-key-ref for tpm[uuid:%s] due to missing key provider binding, try host secret first", - context.tpmUuid)); - } - return shouldSkip; + return !context.enableKeyProvider; } @Override public void run(FlowTrigger trigger, Map data) { + if (context.instantiateForNewVm && context.keyVersion == null) { + trigger.next(); + return; + } if (StringUtils.isBlank(context.providerUuid)) { - trigger.fail(operr("missing TPM resource key binding for tpm[uuid:%s], attachKeyProviderToTpm must run before create-dek", context.tpmUuid)); + trigger.fail(operr("missing TPM resource key binding for tpm[uuid:%s], attachKeyProviderToTpm must run before get-secret-on-host", + context.tpmUuid)); return; } - - if (context.keyVersion != null) { - trigger.next(); + if (!context.instantiateForNewVm && context.keyVersion == null) { + trigger.fail(operr("missing keyVersion for tpm[uuid:%s] before get secret on host", context.tpmUuid)); return; } - - GetOrCreateResourceKeyContext keyCtx = new GetOrCreateResourceKeyContext(); - keyCtx.setResourceUuid(context.tpmUuid); - keyCtx.setResourceType(TpmVO.class.getSimpleName()); - keyCtx.setKeyProviderUuid(context.providerUuid); - keyCtx.setPurpose("vtpm"); - - resourceKeyManager.getOrCreateKey(keyCtx, new ReturnValueCompletion(trigger) { - @Override - public void success(ResourceKeyResult result) { - tpmSpec.setResourceKeyCreatedNew(result.isCreatedNewKey()); - tpmSpec.setResourceKeyProviderUuid(result.getKeyProviderUuid()); - context.dekBase64 = result.getDekBase64(); - context.keyVersion = result.getKeyVersion(); - if (context.keyVersion == null) { - trigger.fail(operr("missing keyVersion for tpm[uuid:%s] after getOrCreateKey", context.tpmUuid)); - return; - } - trigger.next(); - } - - @Override - public void fail(ErrorCode errorCode) { - trigger.fail(errorCode); - } - }); - } - }).then(new NoRollbackFlow() { - String __name__ = "get-secret-on-host"; - - @Override - public boolean skip(Map data) { - return context.keyVersion == null; - } - - @Override - public void run(FlowTrigger trigger, Map data) { + // NewCreate cloned from an existing TPM may already carry keyVersion; allow it. + // For non-NewCreate, keyVersion must exist (validated above). SecretHostGetMsg innerMsg = new SecretHostGetMsg(); innerMsg.setHostUuid(spec.getDestHost().getUuid()); innerMsg.setVmUuid(spec.getVmInventory().getUuid()); innerMsg.setPurpose("vtpm"); innerMsg.setKeyVersion(context.keyVersion); + innerMsg.setUsageInstance(HOST_SECRET_USAGE_INSTANCE_VTPM); bus.makeTargetServiceIdByResourceUuid(innerMsg, HostConstant.SERVICE_ID, innerMsg.getHostUuid()); bus.send(innerMsg, new CloudBusCallBack(trigger) { @Override @@ -344,11 +315,11 @@ public void run(MessageReply reply) { }); } }).then(new NoRollbackFlow() { - String __name__ = "load-dek-for-host"; + String __name__ = "get-or-create-key-and-dek"; @Override public boolean skip(Map data) { - return StringUtils.isNotBlank(spec.getDevicesSpec().getTpm().getSecretUuid()) || context.dekBase64 != null; + return !context.enableKeyProvider; } @Override @@ -367,7 +338,7 @@ public void success(ResourceKeyResult result) { context.dekBase64 = result.getDekBase64(); context.keyVersion = result.getKeyVersion(); if (context.keyVersion == null) { - trigger.fail(operr("missing keyVersion for tpm[uuid:%s] before ensure secret", context.tpmUuid)); + trigger.fail(operr("missing keyVersion for tpm[uuid:%s] after getOrCreateKey", context.tpmUuid)); return; } trigger.next(); @@ -384,13 +355,13 @@ public void fail(ErrorCode errorCode) { @Override public boolean skip(Map data) { - return context.dekBase64 == null || StringUtils.isNotBlank(spec.getDevicesSpec().getTpm().getSecretUuid()); + return !context.enableKeyProvider; } @Override public void run(FlowTrigger trigger, Map data) { - if (context.keyVersion == null) { - trigger.fail(operr("missing keyVersion for tpm[uuid:%s] before define-secret-on-host", context.tpmUuid)); + if (context.dekBase64 == null) { + trigger.fail(operr("missing dekBase64 for tpm[uuid:%s] before define-secret-on-host", context.tpmUuid)); return; } @@ -400,6 +371,7 @@ public void run(FlowTrigger trigger, Map data) { innerMsg.setDekBase64(context.dekBase64); innerMsg.setPurpose("vtpm"); innerMsg.setKeyVersion(context.keyVersion); + innerMsg.setUsageInstance(HOST_SECRET_USAGE_INSTANCE_VTPM); innerMsg.setDescription("Define secret for VM " + spec.getVmInventory().getUuid()); bus.makeTargetServiceIdByResourceUuid(innerMsg, HostConstant.SERVICE_ID, innerMsg.getHostUuid()); bus.send(innerMsg, new CloudBusCallBack(trigger) { @@ -442,11 +414,13 @@ static class PrepareTpmStateHostFileContext { } static class PrepareTpmResourceContext { + boolean enableKeyProvider; String tpmUuid; String backupFileUuid; String providerUuid; Integer keyVersion; String dekBase64; + boolean instantiateForNewVm; void clearSensitiveData() { dekBase64 = null; @@ -466,8 +440,6 @@ public void preReleaseVmResource(VmInstanceSpec spec, Completion completion) { result.setResourceType(TpmVO.class.getSimpleName()); result.setKeyProviderUuid(tpmSpec.getResourceKeyProviderUuid()); result.setCreatedNewKey(true); - // TPM resource key creation requires attachKeyProviderToTpm to create a placeholder ref first. - result.setRefExistedBeforeCreate(true); resourceKeyManager.rollbackCreatedKey(result, new Completion(completion) { @Override @@ -562,6 +534,96 @@ private String findSourceTpmUuidFromSnapshotTpmBackupFile(String tpmBackupFileUu .findValue(); } + @Override + public void preMigrateVm(VmInstanceInventory inv, String destHostUuid, Completion completion) { + if (inv == null || StringUtils.isBlank(destHostUuid)) { + completion.success(); + return; + } + String srcHostUuid = inv.getHostUuid(); + if (StringUtils.isBlank(srcHostUuid)) { + completion.success(); + return; + } + try { + VtpmMigratePreAgentContext ctx = new VtpmMigratePreAgentContext(inv.getUuid(), srcHostUuid, destHostUuid); + ctx.setEnableKeyProvider(!VmGlobalConfig.ALLOWED_TPM_VM_WITHOUT_KMS.value(Boolean.class)); + prepareVtpmSecretOnHostsBeforeMigrate(ctx); + completion.success(); + } catch (OperationFailureException e) { + completion.fail(e.getErrorCode()); + } + } + + private void prepareVtpmSecretOnHostsBeforeMigrate(VtpmMigratePreAgentContext ctx) { + String tpmUuid = VmTpmManager.findTpmUuidForVmOrNull(ctx.getVmUuid()); + if (StringUtils.isBlank(tpmUuid)) { + return; + } + if (!ctx.isEnableKeyProvider()) { + return; + } + ctx.setTpmUuid(tpmUuid); + ctx.setProviderUuid(resourceKeyBackend.findKeyProviderUuidByTpm(tpmUuid)); + ctx.setProviderName(resourceKeyBackend.findKeyProviderNameByTpm(tpmUuid)); + if (StringUtils.isBlank(ctx.getProviderUuid()) && StringUtils.isBlank(ctx.getProviderName())) { + throw new OperationFailureException(operr("missing TPM resource key binding for tpm[uuid:%s] before migrate", tpmUuid)); + } + ctx.setKeyVersion(resourceKeyBackend.findKeyVersionByTpm(tpmUuid)); + if (ctx.getKeyVersion() == null) { + throw new OperationFailureException(operr("cannot find keyVersion for tpm[uuid:%s] before migrate", tpmUuid)); + } + + GetOrCreateResourceKeyContext keyCtx = new GetOrCreateResourceKeyContext(); + keyCtx.setResourceUuid(ctx.getTpmUuid()); + keyCtx.setResourceType(TpmVO.class.getSimpleName()); + keyCtx.setKeyProviderUuid(ctx.getProviderUuid()); + keyCtx.setKeyProviderName(ctx.getProviderName()); + keyCtx.setPurpose("vtpm"); + ResourceKeyResult result = resourceKeyManager.getKey(keyCtx); + if (StringUtils.isBlank(result.getDekBase64())) { + throw new OperationFailureException(operr("missing DEK for tpm[uuid:%s] after getKey before migrate", ctx.getTpmUuid())); + } + ctx.setResourceKeyResult(result); + + SecretHostGetMsg getMsg = new SecretHostGetMsg(); + getMsg.setHostUuid(ctx.getSrcHostUuid()); + getMsg.setVmUuid(ctx.getVmUuid()); + getMsg.setPurpose("vtpm"); + getMsg.setKeyVersion(ctx.getKeyVersion()); + getMsg.setUsageInstance(HOST_SECRET_USAGE_INSTANCE_VTPM); + bus.makeTargetServiceIdByResourceUuid(getMsg, HostConstant.SERVICE_ID, getMsg.getHostUuid()); + MessageReply getReply = bus.call(getMsg); + if (!getReply.isSuccess()) { + throw new OperationFailureException(getReply.getError() != null ? getReply.getError() + : operr("get secret on source host failed before migrate")); + } + SecretHostGetReply getR = getReply.castReply(); + if (StringUtils.isBlank(getR.getSecretUuid())) { + throw new OperationFailureException(operr("failed to get source secret uuid before migrate: empty secretUuid")); + } + ctx.setSourceSecretUuid(getR.getSecretUuid()); + + ResourceKeyResult keyResult = ctx.getResourceKeyResult(); + if (keyResult == null || StringUtils.isBlank(keyResult.getDekBase64())) { + throw new OperationFailureException(operr("missing DEK for tpm[uuid:%s] before ensure secret on destination", ctx.getTpmUuid())); + } + SecretHostDefineMsg defMsg = new SecretHostDefineMsg(); + defMsg.setHostUuid(ctx.getDstHostUuid()); + defMsg.setVmUuid(ctx.getVmUuid()); + defMsg.setDekBase64(keyResult.getDekBase64()); + defMsg.setPurpose("vtpm"); + defMsg.setKeyVersion(ctx.getKeyVersion()); + defMsg.setUsageInstance(HOST_SECRET_USAGE_INSTANCE_VTPM); + defMsg.setSecretUuid(ctx.getSourceSecretUuid()); + defMsg.setDescription(String.format("Define secret for VM %s before live migration", ctx.getVmUuid())); + bus.makeTargetServiceIdByResourceUuid(defMsg, HostConstant.SERVICE_ID, defMsg.getHostUuid()); + MessageReply defReply = bus.call(defMsg); + if (!defReply.isSuccess()) { + throw new OperationFailureException(defReply.getError()); + } + } + @Override public void afterMigrateVm(VmInstanceInventory inv, String srcHostUuid, NoErrorCompletion completion) { String vmUuid = inv == null ? null : inv.getUuid(); @@ -717,6 +779,7 @@ private void deleteHostSecretBestEffort(String hostUuid, String vmUuid, Integer dmsg.setVmUuid(vmUuid); dmsg.setPurpose("vtpm"); dmsg.setKeyVersion(keyVersion); + dmsg.setUsageInstance(HOST_SECRET_USAGE_INSTANCE_VTPM); bus.makeTargetServiceIdByResourceUuid(dmsg, HostConstant.SERVICE_ID, hostUuid); bus.send(dmsg, new CloudBusCallBack(null) { @Override 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 7b8da96fc97..6152dff890a 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 @@ -422,6 +422,7 @@ public void done(ErrorCodeList errorCodeList) { dmsg.setVmUuid(context.vmInstanceUuid); dmsg.setPurpose("vtpm"); dmsg.setKeyVersion(context.keyVersion); + dmsg.setUsageInstance(KVMConstant.HOST_SECRET_USAGE_INSTANCE_VTPM); bus.makeTargetServiceIdByResourceUuid(dmsg, HostConstant.SERVICE_ID, hostUuid); bus.send(dmsg, new CloudBusCallBack(whileCompletion) { @Override @@ -812,6 +813,7 @@ protected void scripts() { dmsg.setVmUuid(vmUuid); dmsg.setPurpose("vtpm"); dmsg.setKeyVersion(context.keyVersion); + dmsg.setUsageInstance(KVMConstant.HOST_SECRET_USAGE_INSTANCE_VTPM); bus.makeTargetServiceIdByResourceUuid(dmsg, HostConstant.SERVICE_ID, hostUuid); bus.send(dmsg, new CloudBusCallBack(whileCompletion) { @Override diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/tpm/VtpmMigratePreAgentContext.java b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/VtpmMigratePreAgentContext.java new file mode 100644 index 00000000000..06d7b7c6784 --- /dev/null +++ b/plugin/kvm/src/main/java/org/zstack/kvm/tpm/VtpmMigratePreAgentContext.java @@ -0,0 +1,102 @@ +package org.zstack.kvm.tpm; + +import org.zstack.header.keyprovider.EncryptedResourceKeyManager.ResourceKeyResult; + +import java.util.Objects; + +/** + * Context for vTPM secret preparation on the source KVM host immediately before the agent + * {@code migrate-vm} call. + *

+ * Built during {@link org.zstack.header.vm.VmInstanceMigrateExtensionPoint#preMigrateVm} (e.g. in + * {@link KvmTpmExtensions}) to hold VM/source/destination hosts and resolved TPM/key/secret fields. + * This replaces the previous split between a thin migrate wrapper + * and a separate object meant for FlowChain {@code Map} storage. + */ +public final class VtpmMigratePreAgentContext { + private final String vmUuid; + private final String srcHostUuid; + private final String dstHostUuid; + private boolean enableKeyProvider = true; + + private String tpmUuid; + private String providerUuid; + private String providerName; + private Integer keyVersion; + private ResourceKeyResult resourceKeyResult; + private String sourceSecretUuid; + + public VtpmMigratePreAgentContext(String vmUuid, String srcHostUuid, String dstHostUuid) { + this.vmUuid = Objects.requireNonNull(vmUuid, "vmUuid"); + this.srcHostUuid = Objects.requireNonNull(srcHostUuid, "srcHostUuid"); + this.dstHostUuid = Objects.requireNonNull(dstHostUuid, "dstHostUuid"); + } + + public String getVmUuid() { + return vmUuid; + } + + public String getSrcHostUuid() { + return srcHostUuid; + } + + public String getDstHostUuid() { + return dstHostUuid; + } + + public boolean isEnableKeyProvider() { + return enableKeyProvider; + } + + public void setEnableKeyProvider(boolean enableKeyProvider) { + this.enableKeyProvider = enableKeyProvider; + } + + public String getTpmUuid() { + return tpmUuid; + } + + public void setTpmUuid(String tpmUuid) { + this.tpmUuid = tpmUuid; + } + + public String getProviderUuid() { + return providerUuid; + } + + public void setProviderUuid(String providerUuid) { + this.providerUuid = providerUuid; + } + + public String getProviderName() { + return providerName; + } + + public void setProviderName(String providerName) { + this.providerName = providerName; + } + + public Integer getKeyVersion() { + return keyVersion; + } + + public void setKeyVersion(Integer keyVersion) { + this.keyVersion = keyVersion; + } + + public ResourceKeyResult getResourceKeyResult() { + return resourceKeyResult; + } + + public void setResourceKeyResult(ResourceKeyResult resourceKeyResult) { + this.resourceKeyResult = resourceKeyResult; + } + + public String getSourceSecretUuid() { + return sourceSecretUuid; + } + + public void setSourceSecretUuid(String sourceSecretUuid) { + this.sourceSecretUuid = sourceSecretUuid; + } +}