diff --git a/compute/src/main/java/org/zstack/compute/vm/VmBootTimeUtils.java b/compute/src/main/java/org/zstack/compute/vm/VmBootTimeUtils.java new file mode 100644 index 00000000000..06ae1417b98 --- /dev/null +++ b/compute/src/main/java/org/zstack/compute/vm/VmBootTimeUtils.java @@ -0,0 +1,90 @@ +package org.zstack.compute.vm; + +import org.apache.commons.lang.StringUtils; +import org.zstack.core.db.Q; +import org.zstack.core.db.SQL; +import org.zstack.header.vm.VmInstanceState; +import org.zstack.header.vm.VmInstanceVO; +import org.zstack.header.vm.VmInstanceVO_; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +public class VmBootTimeUtils { + public static final String STOPPED_UPTIME = "0"; + public static final String UNKNOWN_UPTIME = ""; + private static final DateTimeFormatter BOOT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + public static boolean isBootTimeValidState(VmInstanceState state) { + return state != null && state != VmInstanceState.Stopped && state != VmInstanceState.Destroyed; + } + + public String getBootTime(String vmUuid) { + if (StringUtils.isBlank(vmUuid)) { + return null; + } + + Timestamp bootTime = Q.New(VmInstanceVO.class) + .select(VmInstanceVO_.bootTime) + .eq(VmInstanceVO_.uuid, vmUuid) + .findValue(); + return formatBootTime(bootTime); + } + + public void resetBootTime(String vmUuid) { + if (StringUtils.isBlank(vmUuid)) { + return; + } + + SQL.New(VmInstanceVO.class) + .eq(VmInstanceVO_.uuid, vmUuid) + .set(VmInstanceVO_.bootTime, Timestamp.valueOf(LocalDateTime.now())) + .update(); + } + + public void clearBootTime(String vmUuid) { + if (StringUtils.isBlank(vmUuid)) { + return; + } + + SQL.New(VmInstanceVO.class) + .eq(VmInstanceVO_.uuid, vmUuid) + .set(VmInstanceVO_.bootTime, (Timestamp) null) + .update(); + } + + public void backfillBootTimeIfMissing(String vmUuid, String hostUptime) { + Timestamp bootTime = parseBootTime(hostUptime); + if (StringUtils.isBlank(vmUuid) || bootTime == null) { + return; + } + + SQL.New(VmInstanceVO.class) + .eq(VmInstanceVO_.uuid, vmUuid) + .isNull(VmInstanceVO_.bootTime) + .set(VmInstanceVO_.bootTime, bootTime) + .update(); + } + + private static String formatBootTime(Timestamp bootTime) { + if (bootTime == null) { + return null; + } + + return bootTime.toLocalDateTime().format(BOOT_TIME_FORMATTER); + } + + private static Timestamp parseBootTime(String bootTime) { + if (StringUtils.isBlank(bootTime)) { + return null; + } + + try { + return Timestamp.valueOf(LocalDateTime.parse(bootTime, BOOT_TIME_FORMATTER)); + } catch (DateTimeParseException ignored) { + return null; + } + } +} \ No newline at end of file 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 c4efaee8303..09d66646ddd 100755 --- a/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java +++ b/compute/src/main/java/org/zstack/compute/vm/VmInstanceBase.java @@ -151,6 +151,7 @@ public class VmInstanceBase extends AbstractVmInstance { protected String syncThreadName; private final static StaticIpOperator ipOperator = new StaticIpOperator(); private final static VmConfigSyncHelper vmConfigSyncHelper = new VmConfigSyncHelper(); + private final static VmBootTimeUtils vmBootTimeUtils = new VmBootTimeUtils(); private void detachTpmKeyProviderBestEffort(String tpmUuid) { if (tpmUuid == null) { @@ -381,6 +382,7 @@ protected void scripts() { } self.setState(state); + self.setBootTime(getBootTimeAfterStateChanged(bs, state, getCurrentBootTimeForStateChange())); self = merge(self); } }; @@ -419,6 +421,35 @@ public void run(VmStateChangedExtensionPoint ext) { return self; } + private Timestamp getCurrentBootTimeForStateChange() { + if (self.getBootTime() != null) { + return self.getBootTime(); + } + + return Q.New(VmInstanceVO.class) + .select(VmInstanceVO_.bootTime) + .eq(VmInstanceVO_.uuid, self.getUuid()) + .findValue(); + } + + private Timestamp getBootTimeAfterStateChanged(VmInstanceState oldState, VmInstanceState newState, Timestamp currentBootTime) { + if (!VmBootTimeUtils.isBootTimeValidState(newState)) { + return null; + } + + if (newState != VmInstanceState.Running) { + return currentBootTime; + } + + if (oldState == VmInstanceState.Starting + || oldState == VmInstanceState.Rebooting + || (oldState == VmInstanceState.Resuming && currentBootTime == null)) { + return Timestamp.valueOf(LocalDateTime.now()); + } + + return currentBootTime; + } + @Override @MessageSafe public void handleMessage(final Message msg) { @@ -3724,6 +3755,13 @@ public void done(ErrorCodeList errorCodeList) { private void handle(APIGetVmUptimeMsg msg) { APIGetVmUptimeReply reply = new APIGetVmUptimeReply(); + String uptime = StringUtils.defaultString(vmBootTimeUtils.getBootTime(self.getUuid()), VmBootTimeUtils.UNKNOWN_UPTIME); + if (StringUtils.isNotEmpty(uptime)) { + reply.setUptime(uptime); + bus.reply(msg, reply); + return; + } + GetVmUptimeMsg gmsg = new GetVmUptimeMsg(); gmsg.setVmInstanceUuid(self.getUuid()); gmsg.setHostUuid(self.getHostUuid()); @@ -3732,12 +3770,12 @@ private void handle(APIGetVmUptimeMsg msg) { bus.send(gmsg, new CloudBusCallBack(msg) { @Override public void run(MessageReply r) { - if (!r.isSuccess()) { - reply.setSuccess(false); - reply.setError(r.getError()); - } else { + if (r.isSuccess()) { GetVmUptimeReply re = (GetVmUptimeReply) r; - reply.setUptime(re.getUptime()); + vmBootTimeUtils.backfillBootTimeIfMissing(self.getUuid(), re.getUptime()); + reply.setUptime(StringUtils.defaultString(vmBootTimeUtils.getBootTime(self.getUuid()), VmBootTimeUtils.UNKNOWN_UPTIME)); + } else { + reply.setUptime(VmBootTimeUtils.UNKNOWN_UPTIME); } bus.reply(msg, reply); } diff --git a/conf/db/zsv/V5.1.0__schema.sql b/conf/db/zsv/V5.1.0__schema.sql index f2302d4e9d4..9afa129e064 100644 --- a/conf/db/zsv/V5.1.0__schema.sql +++ b/conf/db/zsv/V5.1.0__schema.sql @@ -36,3 +36,37 @@ SET src.`type` = IF(ldap.serverType IN ('OpenLdap', 'WindowsAD'), ldap.serverTyp WHERE src.`type` = 'ldap'; CALL INSERT_COLUMN('ThirdPartyAccountSourceVO', 'updateAccountStrategies', 'varchar(255)', 0, '', 'createAccountStrategy'); + +-- Feature: VM boot time | ZSV-12297 + +CALL INSERT_COLUMN('VmInstanceEO', 'bootTime', 'timestamp NULL', 1, NULL, 'state'); + +CREATE OR REPLACE VIEW `zstack`.`VmInstanceVO` AS +SELECT uuid, + name, + description, + zoneUuid, + clusterUuid, + imageUuid, + hostUuid, + internalId, + lastHostUuid, + instanceOfferingUuid, + rootVolumeUuid, + defaultL3NetworkUuid, + type, + hypervisorType, + cpuNum, + cpuSpeed, + memorySize, + reservedMemorySize, + platform, + guestOsType, + allocatorStrategy, + createDate, + lastOpDate, + state, + architecture, + bootTime +FROM `zstack`.`VmInstanceEO` +WHERE deleted IS NULL; \ No newline at end of file diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceAO.java b/header/src/main/java/org/zstack/header/vm/VmInstanceAO.java index 001dbe2298b..780d86fc48c 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceAO.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceAO.java @@ -99,6 +99,9 @@ public class VmInstanceAO extends ResourceVO { @Enumerated(EnumType.STRING) private VmInstanceState state; + @Column + private Timestamp bootTime; + public VmInstanceAO() { } @@ -125,6 +128,7 @@ public VmInstanceAO(VmInstanceAO other) { this.createDate = other.createDate; this.lastOpDate = other.lastOpDate; this.state = other.state; + this.bootTime = other.bootTime; this.platform = other.platform; this.guestOsType = other.guestOsType; this.architecture = other.architecture; @@ -279,6 +283,14 @@ public void setState(VmInstanceState state) { this.state = state; } + public Timestamp getBootTime() { + return bootTime; + } + + public void setBootTime(Timestamp bootTime) { + this.bootTime = bootTime; + } + public String getRootVolumeUuid() { return rootVolumeUuid; } diff --git a/header/src/main/java/org/zstack/header/vm/VmInstanceAO_.java b/header/src/main/java/org/zstack/header/vm/VmInstanceAO_.java index 90dee2c2ef6..49d3a7079fc 100755 --- a/header/src/main/java/org/zstack/header/vm/VmInstanceAO_.java +++ b/header/src/main/java/org/zstack/header/vm/VmInstanceAO_.java @@ -31,5 +31,6 @@ public class VmInstanceAO_ extends ResourceVO_ { public static volatile SingularAttribute cpuSpeed; public static volatile SingularAttribute createDate; public static volatile SingularAttribute lastOpDate; + public static volatile SingularAttribute bootTime; public static volatile SingularAttribute state; } diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java index 7d0b2a7d342..4be7abd072e 100644 --- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java +++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostFactory.java @@ -7,6 +7,7 @@ import org.zstack.compute.vm.CrashStrategy; import org.zstack.compute.vm.VmGlobalConfig; import org.zstack.compute.vm.VmNicManager; +import org.zstack.compute.vm.VmBootTimeUtils; import org.zstack.core.CoreGlobalProperty; import org.zstack.core.Platform; import org.zstack.core.ansible.AnsibleFacade; @@ -96,6 +97,7 @@ public class KVMHostFactory extends AbstractService implements HypervisorFactory, Component, ManagementNodeReadyExtensionPoint, MaxDataVolumeNumberExtensionPoint, HypervisorMessageFactory, ProxyHardwareFactory { private static final CLogger logger = Utils.getLogger(KVMHostFactory.class); + private static final VmBootTimeUtils vmBootTimeUtils = new VmBootTimeUtils(); public static final HypervisorType hypervisorType = new HypervisorType(KVMConstant.KVM_HYPERVISOR_TYPE); public static final VolumeFormat QCOW2_FORMAT = new VolumeFormat(VolumeConstant.VOLUME_FORMAT_QCOW2, hypervisorType); @@ -539,6 +541,7 @@ public String handleSyncHttpCall(TransmitVmOperationToMnCmd cmd) { }); restf.registerSyncHttpCallHandler(KVMConstant.KVM_REPORT_VM_REBOOT_EVENT, ReportVmRebootEventCmd.class, cmd -> { + vmBootTimeUtils.resetBootTime(cmd.vmUuid); evf.fire(VmCanonicalEvents.VM_LIBVIRT_REPORT_REBOOT, cmd.vmUuid); return null;