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/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/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml index 88886397a1f..7d4e03f86b2 100755 --- a/conf/springConfigXml/Kvm.xml +++ b/conf/springConfigXml/Kvm.xml @@ -262,6 +262,7 @@ + @@ -269,6 +270,9 @@ + + + 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/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, } 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..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,8 +26,12 @@ 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; +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_; @@ -74,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 @@ -264,6 +269,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()) { @@ -410,11 +419,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") @@ -426,7 +439,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) @@ -445,9 +458,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()); } @@ -455,7 +468,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(); } @@ -467,18 +480,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", @@ -488,8 +530,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()); @@ -528,9 +570,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) { @@ -711,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/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..730e9c79cae 100755 --- a/test/src/test/resources/springConfigXml/Kvm.xml +++ b/test/src/test/resources/springConfigXml/Kvm.xml @@ -261,6 +261,7 @@ + @@ -268,6 +269,9 @@ +
+ +