Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.zstack.compute.vm;

import org.zstack.core.cloudbus.CloudBusCallBack;
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.utils.Utils;
import org.zstack.utils.logging.CLogger;

public class CleanupVmInstanceMetadataOnPrimaryStorageGC extends TimeBasedGarbageCollector {
private static final CLogger logger = Utils.getLogger(CleanupVmInstanceMetadataOnPrimaryStorageGC.class);

@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;
}

if (hostUuid != null && !dbf.isExist(hostUuid, HostVO.class)) {
logger.debug(String.format("[MetadataCleanupGC] host[uuid:%s] no longer exists, " +
"cancel gc for vm[uuid:%s]", hostUuid, vmUuid));
completion.cancel();
return;
}

CleanupVmInstanceMetadataOnPrimaryStorageMsg msg = new CleanupVmInstanceMetadataOnPrimaryStorageMsg();
msg.setPrimaryStorageUuid(primaryStorageUuid);
msg.setVmUuid(vmUuid);
msg.setRootVolumeUuid(rootVolumeUuid);
msg.setMetadataPath(metadataPath);
msg.setHostUuid(hostUuid);

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());
}
Comment on lines +59 to +68
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

GC 未区分瞬态与永久性错误,可能导致无限重试。

completion.fail() 被调用时,TimeBasedGarbageCollector 会重新调度 GC 任务。如果错误是永久性的(如元数据路径无效、权限问题等),GC 会无限重试。

建议:

  1. 检查错误码,对永久性错误调用 completion.cancel() 而非 completion.fail()
  2. 或者添加重试次数限制
🛠️ 建议修改
             } 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());
+                // 对于永久性错误(如路径不存在),取消 GC 而非重试
+                if (reply.getError().isError(VmMetadataErrors.METADATA_NOT_FOUND) 
+                        || reply.getError().isError(PrimaryStorageErrors.PRIMARY_STORAGE_NOT_FOUND)) {
+                    logger.info(String.format("[MetadataCleanupGC] cancel gc for vm[uuid:%s] due to permanent error: %s",
+                            vmUuid, reply.getError().getCode()));
+                    completion.cancel();
+                } else {
+                    completion.fail(reply.getError());
+                }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@compute/src/main/java/org/zstack/compute/vm/CleanupVmInstanceMetadataOnPrimaryStorageGC.java`
around lines 59 - 68, The current run(MessageReply reply) in
CleanupVmInstanceMetadataOnPrimaryStorageGC blindly calls completion.fail()
which causes TimeBasedGarbageCollector to reschedule forever; update the error
handling in run(...) (class CleanupVmInstanceMetadataOnPrimaryStorageGC, method
run) to inspect reply.getError() (e.g. error code or type) and call
completion.cancel() for permanent errors (invalid path, permission denied,
not-found) while keeping completion.fail() for transient errors, or
alternatively implement a retry limit tied to this GC task (store/increment a
retry counter and cancel after N attempts) using vmUuid/primaryStorageUuid to
identify the task.

}
});
}
}
128 changes: 128 additions & 0 deletions compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
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;
}
Comment on lines +73 to +80
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 检查 buildVmMetadataPath 实现是否依赖数据库查询
ast-grep --pattern $'buildVmMetadataPath($_, $_) {
  $$$
}'

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# 查找 VmExpungeMetadataFlow.java 文件并检查 ext 对象的类型
find . -name "VmExpungeMetadataFlow.java" -type f

Repository: MatheMatrix/zstack

Length of output: 134


🏁 Script executed:

# 搜索 buildVmMetadataPath 方法定义
rg "buildVmMetadataPath" -A 5 -B 2

Repository: MatheMatrix/zstack

Length of output: 4443


🏁 Script executed:

# 查找流程顺序配置
fd "VmInstanceManager.xml" -type f

Repository: MatheMatrix/zstack

Length of output: 232


🏁 Script executed:

# 搜索 VmInstanceManager.xml 获取流程顺序
find . -name "*.xml" | xargs grep -l "VmExpungeMetadataFlow\|VmExpungeRootVolumeFlow" 2>/dev/null | head -5

Repository: MatheMatrix/zstack

Length of output: 106


🏁 Script executed:

# 查看 VmExpungeMetadataFlow.java 的完整内容
cat -n ./compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java

Repository: MatheMatrix/zstack

Length of output: 6889


🏁 Script executed:

# 搜索 VmExpungeRootVolumeFlow 
rg "class VmExpungeRootVolumeFlow" -A 30

Repository: MatheMatrix/zstack

Length of output: 3798


🏁 Script executed:

cat ./conf/springConfigXml/VmInstanceManager.xml | grep -A 5 -B 5 "VmExpunge"

Repository: MatheMatrix/zstack

Length of output: 763


补充 GC 机制处理元数据路径构建失败的情况。

VmExpungeMetadataFlow 在第 74 行调用 buildVmMetadataPath() 时捕获异常(第 75-80 行)并静默跳过,仅记录警告日志。与之对比,第 108 行在清理消息失败时会提交 GC 任务进行重试。这种不对称的处理机制导致当路径构建失败时(如 LocalStorage 扩展点中查询 PrimaryStorageVO 失败),无法通过 GC 进行恢复,元数据残留在主存储上。

建议在第 75-80 行的异常处理中也提交 GC 任务,确保路径构建失败不会导致元数据被遗弃:

} 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()));
    // 提交 GC 任务以确保元数据最终被清理
    submitGC(psUuid, vmUuid, rootVolumeUuid, metadataPath, hostUuid);
    trigger.next();
    return;
}

或者考虑在异常捕获时只捕获特定的预期异常,而非泛型 Exception。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java`
around lines 73 - 80, The catch block in VmExpungeMetadataFlow around the call
to buildVmMetadataPath currently logs and skips cleanup, which prevents retry
via GC; modify the exception handling to also enqueue a GC retry by calling the
existing submitGC routine (use the same parameters used later: psUuid, vmUuid,
rootVolumeUuid, metadataPath, hostUuid) before invoking trigger.next(), or
alternatively narrow the catch to expected exceptions; update the catch in the
method containing buildVmMetadataPath to submitGC(psUuid, vmUuid,
rootVolumeUuid, metadataPath, hostUuid) so failed path construction will be
retried by GC.


String rootVolumeUuid = rootVolume.getUuid();
CleanupVmInstanceMetadataOnPrimaryStorageMsg cmsg = new CleanupVmInstanceMetadataOnPrimaryStorageMsg();
cmsg.setPrimaryStorageUuid(psUuid);
cmsg.setVmUuid(vmUuid);
cmsg.setMetadataPath(metadataPath);
cmsg.setRootVolumeUuid(rootVolumeUuid);

String hostUuid = spec.getVmInventory().getHostUuid();
if (hostUuid == null) {
hostUuid = spec.getVmInventory().getLastHostUuid();
}
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);
Comment on lines +102 to +124
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

不要把所有清理失败都无条件转成 GC 重试。

这里对任意 reply.isSuccess() == false 都提交 GC,没有区分可重试错误和永久错误。像路径或参数永久无效这类失败会持续制造无意义重试,建议按错误码过滤,或者在 GC 中设置明确的终止条件。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java`
around lines 102 - 124, The current code unconditionally calls submitGC(...)
whenever reply.isSuccess() is false, causing permanent errors (e.g. invalid
path/params) to produce endless GC retries; modify the error-handling in the
callback so you inspect reply.getError() and only call submitGC when the error
is retryable. Concretely, implement a helper (e.g., isRetryableError(ErrorCode))
and in the block that currently calls submitGC(...) check that helper against
reply.getError().getCode()/ErrorCode before scheduling GC; for non-retryable
errors just log a clear warn/error and do not submit GC (or alternatively mark
the GC task with a terminal condition/max attempts in
CleanupVmInstanceMetadataOnPrimaryStorageGC).


logger.info(String.format("[MetadataExpunge] submitted GC job [%s] for vm[uuid:%s] on ps[uuid:%s]", gc.NAME, vmUuid, psUuid));
}
}
36 changes: 36 additions & 0 deletions compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,40 @@ public class VmGlobalConfig {
@GlobalConfigValidation(validValues = {"None", "AuthenticAMD"})
@BindResourceConfig(value = {VmInstanceVO.class})
public static GlobalConfig VM_CPUID_VENDOR = new GlobalConfig(CATEGORY, "vm.cpuid.vendor");

@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 = 100)
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 = 0, numberLessThan = 86400)
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");
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,19 @@
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.vm.*;
import org.zstack.header.vm.cdrom.*;
import org.zstack.header.vm.metadata.APIRegisterVmInstanceFromMetadataMsg;
import org.zstack.header.vm.devices.VmInstanceResourceMetadataGroupVO;
import org.zstack.header.vm.devices.VmInstanceResourceMetadataGroupVO_;
import org.zstack.header.vm.metadata.VmInstanceMetadataConstants;
import org.zstack.header.vm.metadata.VmMetadataPathBuildExtensionPoint;
import org.zstack.header.volume.*;
import org.zstack.network.l2.L2NetworkHostUtils;
import org.zstack.resourceconfig.ResourceConfigFacade;
Expand Down Expand Up @@ -166,6 +171,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) {
Expand Down Expand Up @@ -1318,4 +1325,29 @@ 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"));
}
Comment on lines +1330 to +1333
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

把纯空白的 metadataPath 也当成非法输入。

现在只拦了 null/"",像 " " 这样的值仍会继续进入后端扩展,导致校验口径依赖具体主存储实现。

✍️ 建议修改
         String path = msg.getMetadataPath();
-        if (path == null || path.isEmpty()) {
+        if (path == null || path.trim().isEmpty()) {
             throw new ApiMessageInterceptionException(argerr("metadataPath cannot be empty"));
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java`
around lines 1330 - 1333, 在 VmInstanceApiInterceptor 中对 msg.getMetadataPath()
做空白字符串校验:当前只判断 null/"",需将纯空白(例如 "   ")也视为非法。修改对 metadataPath 的检查(在使用
msg.getMetadataPath() 的位置)为在判空前先 trim 或使用 isBlank 等方式(例如 check path == null ||
path.trim().isEmpty()),并在不合法时仍抛出
ApiMessageInterceptionException(argerr("metadataPath cannot be empty"))
保持原错误类型及信息。


// 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();
VmMetadataPathBuildExtensionPoint ext = (psType != null)
? pluginRgty.getExtensionFromMap(psType, VmMetadataPathBuildExtensionPoint.class) : null;
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(error));
}
}
}
4 changes: 4 additions & 0 deletions compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
26 changes: 26 additions & 0 deletions conf/db/upgrade/V5.0.0__schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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 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
Comment on lines +12 to +13
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

外键引用建议显式带上 zstack schema。

这个目录里的升级脚本约定是固定使用 zstack schema;这里的 REFERENCES \VmInstanceEO`/`ManagementNodeVO`` 省略 schema,后续在非默认库上下文执行时可读性和一致性都差一些。

🛠️ 建议修改
-    CONSTRAINT `fkVmMetadataDirtyVOVmInstanceEO` FOREIGN KEY (`vmInstanceUuid`)  REFERENCES `VmInstanceEO` (`uuid`) ON DELETE CASCADE,
-    CONSTRAINT `fkVmMetadataDirtyVOManagementNodeVO` FOREIGN KEY (`managementNodeUuid`) REFERENCES `ManagementNodeVO` (`uuid`) ON DELETE SET NULL
+    CONSTRAINT `fkVmMetadataDirtyVOVmInstanceEO` FOREIGN KEY (`vmInstanceUuid`) REFERENCES `zstack`.`VmInstanceEO` (`uuid`) ON DELETE CASCADE,
+    CONSTRAINT `fkVmMetadataDirtyVOManagementNodeVO` FOREIGN KEY (`managementNodeUuid`) REFERENCES `zstack`.`ManagementNodeVO` (`uuid`) ON DELETE SET NULL
...
-    CONSTRAINT `fkVmMetadataFingerprintVOVmInstanceEO` FOREIGN KEY (`vmInstanceUuid`)  REFERENCES `VmInstanceEO` (`uuid`) ON DELETE CASCADE
+    CONSTRAINT `fkVmMetadataFingerprintVOVmInstanceEO` FOREIGN KEY (`vmInstanceUuid`) REFERENCES `zstack`.`VmInstanceEO` (`uuid`) ON DELETE CASCADE

Based on learnings, in conf/db/upgrade scripts the schema is fixed as zstack, and referenced tables should be written with the zstack qualifier.

Also applies to: 25-25

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@conf/db/upgrade/V5.0.0__schema.sql` around lines 12 - 13, 在外键定义里将被引用表显式加上
zstack schema:把 CONSTRAINT `fkVmMetadataDirtyVOVmInstanceEO` 的 REFERENCES
`VmInstanceEO` (`uuid`) 修改为 REFERENCES `zstack`.`VmInstanceEO` (`uuid`),并把
CONSTRAINT `fkVmMetadataDirtyVOManagementNodeVO` 的 REFERENCES `ManagementNodeVO`
(`uuid`) 修改为 REFERENCES `zstack`.`ManagementNodeVO`
(`uuid`),确保两个外键(fkVmMetadataDirtyVOVmInstanceEO,
fkVmMetadataDirtyVOManagementNodeVO)都使用 zstack. 前缀以保持 conf/db/upgrade
目录脚本的一致性和在非默认库上下文的可读性。

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `zstack`.`VmMetadataFingerprintVO` (
`vmInstanceUuid` VARCHAR(32) NOT NULL,
`metadataSnapshot` LONGTEXT,
`lastFlushTime` TIMESTAMP NULL DEFAULT NULL,
`lastFlushFailed` TINYINT(1) NOT NULL DEFAULT 0,
`staleRecoveryCount` INT NOT NULL DEFAULT 0,
`lastOpDate` timestamp on update CURRENT_TIMESTAMP,
`createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59',
PRIMARY KEY (`vmInstanceUuid`),
CONSTRAINT `fkVmMetadataFingerprintVOVmInstanceEO` FOREIGN KEY (`vmInstanceUuid`) REFERENCES `VmInstanceEO` (`uuid`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Loading